I reproduced an endpoint emitting a provision seed for a different resource from its declared binding, including a resource belonging to another provider.
The compiler checks the binding against the connector's resources, but the runtime accepts the seed's independently returned resource field without comparing it to that binding. The host receives a persistence work order for an undeclared resource kind, with the wrong lifecycle and billing schedule attached if it trusts that work order.
Reproduction
Save this as repro-provision-id.ts at the repository root and run:
deno run --allow-read --allow-env repro-provision-id.tsThis uses a mocked transport and needs no network or real credentials.
import { z } from "zod";
import { compileBundle } from "@shared/compiler";
import { defineProvider, defineResource, defineEndpoint, presets, sealUnit } from "@shared/core";
import { Engine, ENGINE_VERSION, directTransport } from "@monid/connector-engine";
const bundle = await compileBundle([{
provider: defineProvider({
name: "demo", meta: { displayName: "Demo", summary: "Demo provider." },
auth: { inject: presets.auth.header("x-key") },
request: { baseUrl: "https://api.demo.test" },
usage: { model: { kind: "FREE" } },
}),
resources: [{ name: "widget", def: defineResource({
slug: "widget", meta: { displayName: "Widget", summary: "Demo widget." },
data: z.strictObject({ color: z.string() }),
usage: { period: { unit: "MONTH", count: 1, anchor: "CREATION_TIME" },
lines: { rent: { consumes: { credit: "default", amount: 1 } } } },
lifecycle: {
verify: async () => ({ active: true }),
release: async () => ({ released: true }),
},
}) }],
endpoints: [{ name: "create", def: defineEndpoint({
meta: { displayName: "Create", summary: "Creates a widget.", categories: ["demo-cat"] },
request: { method: "POST", path: "/create" },
resources: { provisions: [{ id: "demo/widget", seed: () => ({
resource: "other/widget", externalId: "w-1", data: { color: "red" },
}) }] },
}) }],
}], {
compilerVersion: "0.1.0", builtWithEngineVersion: ENGINE_VERSION,
catalogVersion: "0.0.0-test", generatedAt: "1970-01-01T00:00:00.000Z",
leafCategories: [{ id: "demo-cat", displayName: "Demo" }],
});
const engine = new Engine({
resources: { owned: () => Promise.resolve([]) },
transport: directTransport({
params: () => Promise.resolve({ apiKey: "test" }),
fetch: () => Promise.resolve(new Response("{}")),
}),
});
const loaded = await engine.load(sealUnit(bundle, "demo#create"));
console.log("binding:", loaded.doc.resources?.provisions?.[0].id);
console.log("effects:", JSON.stringify((await loaded.run({})).resources));Observed:
binding: demo/widget
effects: {"provisions":[{"resource":"other/widget","externalId":"w-1","data":{"color":"red"}}]}Expected: reject with PROVISION_CONSTRUCT, because the post-success seed must match resources.provisions[0].id. A same-provider mismatch such as demo/other should also reject. A matching seed should pass, and null should continue to mean that no resource was provisioned.
Cause and proposed resolution
LoadedEndpoint.deriveEffects in engine/engine.ts places every non-null seed directly into effects.provisions. The seed hook contract validates the record's shape, but not its identity relative to the provision binding.
Compare the returned ID with the provision binding before emitting the effect, using the existing PROVISION_CONSTRUCT error category. This check belongs to the post-success provision hook; prerequisite ensure hooks intentionally may seed other resource docs and should keep that behavior.
Activity
0No activity yet.