/** * `describe_handle` is the shape-inspection seam: it answers what a handle * token refers to — schema and path — and never what the referent holds. What * it reports is structure alone, whether the shape came from the run's own * handle table or from what the referent declares in the session's fabric. */ import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import type { FabricValue } from "@commonfabric/data-model"; import { internSchemaAsTaggedHashString } from "@commonfabric/data-model-schema"; import { createSession, Identity } from "@commonfabric/identity"; import type { URI } from "@commonfabric/memory/interface"; import { PiecesController } from "@commonfabric/piece/ops"; import { Runtime } from "@commonfabric/runner"; import { StorageManager } from "@commonfabric/runner/storage/cache.deno"; import { normalize } from "@std/path/posix"; import { join } from "@std/path"; import { createFileSystemHarnessArtifactStore } from "../src/artifacts.ts"; import { CAPABILITY_PROBE_SENTINEL } from "../src/diagnostics.ts"; import { CfHarnessEngine } from "../src/engine.ts"; import { CfHarnessPromptLoop } from "../src/prompt-loop.ts"; import { createHarnessRunState } from "../src/run-state.ts"; import { MAX_DISCLOSED_PROPERTIES, MAX_PROPERTY_NAME_LENGTH, } from "../src/schema-shape.ts"; import type { SandboxCommandRequest, SandboxCommandResult, SandboxRuntime, SandboxRuntimeDescription, SandboxShellRequest, } from "../src/sandbox/types.ts"; import { responsesBodyFromChatFixture } from "./support/responses-fixture.ts"; import { describeHandleForResearch, describeHandleTool, } from "../src/tools/describe-handle.ts"; import { runPatternTool } from "../src/tools/run-pattern.ts"; import type { RunPatternToolSuccessOutput } from "../src/tools/run-pattern.ts"; import type { HarnessFabricSession } from "../src/fabric-session.ts"; import { createHarnessHandleTable, mintAddressHandle, mintReferentHandle, } from "../src/handle-table.ts"; import type { HarnessHandleTable } from "../src/contracts/handle-table.ts"; import { HARNESS_RESEARCH_HANDLE_TYPE, type HarnessResearchHandleValue, type HarnessResearchPatternRecord, } from "../src/contracts/research.ts"; import { createToolOutputId } from "../src/contracts/tool-result.ts"; import type { HarnessToolContext } from "../src/tools/types.ts"; import { seedStoredEnvelope } from "../../runner/test/cfc-seed-envelope.ts"; const signer = await Identity.fromPassphrase("cf-harness describe-handle"); /** * The schema the label fixture's CFC metadata names. Stored metadata carries * a schema reference the commit boundary verifies like any other, so a * fixture that seeds label state installs this document beside it. */ const LABEL_SEED_SCHEMA = { type: "object", title: "cf-harness-describe-handle-label-seed", } as const; const LABEL_SEED_SCHEMA_HASH: string = internSchemaAsTaggedHashString( LABEL_SEED_SCHEMA, ); const HASH_A = "A".repeat(43); const REF_A = `/of:fid1:${HASH_A}/summary`; /** The raw hash the hostile tagged property name below is built from. */ const SCRUB_HASH = "C".repeat(43); /** * A tagged hash in the position a schema's author controls outright: the name * of a property. Not a schemed link form, so the handle boundary does not swap * it — the scrub is what keeps it out. */ const HOSTILE_HASH_NAME = `fid1:${SCRUB_HASH}`; /** A DID in that same position, kept out the same way. */ const HOSTILE_DID_NAME = "did:key:z6MkfffDescribeHandleScrubbing"; /** The raw hash the linked property name below is built from. */ const LINKED_NAME_HASH = "E".repeat(43); /** * A property name that is a link rather than a bare identifier. The scrub * deliberately leaves the schemed forms alone, because they are the handle * boundary's business: a link is swapped for a token, wherever it sits. */ const LINK_PROPERTY_NAME = `/of:fid1:${LINKED_NAME_HASH}/total`; /** The sandbox members the prompt loop reaches on a run with no shell work. */ class FakeSandboxRuntime implements SandboxRuntime { describe(): SandboxRuntimeDescription { return { kind: "runsc-cfc", defaultWorkingDirectory: this.defaultWorkingDirectory(), cfc: { runtimeRequested: true, workspaceMountPath: "/workspace" }, }; } resolvePath(path: string, cwd = this.defaultWorkingDirectory()): string { return normalize(path.startsWith("/") ? path : `${cwd}/${path}`); } isPathWithinWorkspace(path: string): boolean { return path === "/workspace" || path.startsWith("/workspace/"); } isPathWithinAllowedRoots(path: string): boolean { return this.isPathWithinWorkspace(path); } defaultWorkingDirectory(): string { return "/workspace"; } run(_request: SandboxCommandRequest): Promise { return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 }); } runShell(request: SandboxShellRequest): Promise { if (request.command.includes(CAPABILITY_PROBE_SENTINEL)) { return Promise.resolve({ stdout: "bash\tpresent\t/bin/bash\tGNU bash, version 5.2.26(1)-release", stderr: "", exitCode: 0, }); } return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 }); } } const DOUBLED_SCHEMA = { type: "object", properties: { doubled: { type: "number" } }, required: ["doubled"], } as const; /** * A pattern whose result carries several named fields, standing in for a * piece an agent is handed and asked to build over. What matters is that the * field names cannot be guessed from anything else the test says. */ const SPENDING_PATTERN_SOURCE = [ "import { computed, NAME, pattern } from 'commonfabric';", "interface Input { n: number; }", "interface Output {", " totalSpent: number;", " remaining: number;", " topCategory: string;", " $NAME: string;", "}", "export default pattern(({ n }) => ({", " [NAME]: 'Spending Overview',", " totalSpent: computed(() => n * 2),", " remaining: computed(() => n),", " topCategory: computed(() => 'groceries'),", "}));", "", ].join("\n"); /** * A SQLite database handle in the shape one arrives in when a connector * injects it: the tables it was created under, carried in the handle's own * value, with nothing declaring a schema for the cell that holds it. The * table title, the column description and the column default are the prose * and values a reduction has to drop; the `ifc` annotations are what a reader * has to be told. */ /** One `COUNT("") AS ""` clause of a count statement. */ const COUNT_CLAUSE = /COUNT\("([^"]+)"\) AS "([^"]+)"/g; /** * A database whose one table carries a per-row label rule. The rule reads * `status`, which a query must project or the read is refused, and `status` is * a disclosed column so the disclosure may name it. */ const BANK_DB_HANDLE = { id: "db-bank", rev: 1, tables: { rows_txn: { type: "object", properties: { amount: { type: "number", ifc: { confidentiality: ["https://cfc.test/atom/finance"] }, }, status: { type: "string", ifc: { confidentiality: ["https://cfc.test/atom/finance"] }, }, }, rowLabel: { version: 1, integrity: { when: { match: { field: "status", source: "^posted$", flags: "" } }, then: { constant: "posted" }, }, }, }, }, }; const MAIL_DB_HANDLE = { id: "db-mail", rev: 3, tables: { messages: { type: "object", title: "The Inbox", properties: { sender: { type: "string", default: "noreply@example.test", ifc: { confidentiality: ["https://cfc.test/atom/email"], integrity: ["https://cfc.test/atom/connector-observed"], }, }, body: { type: "string", description: "every message this connector observed", ifc: { confidentiality: [{ anyOf: [ "https://cfc.test/atom/email", "https://cfc.test/atom/screened", ], }], }, }, received: { type: "integer" }, }, }, }, }; /** * The context members `describe_handle` reads. Everything else on a tool * context — sandbox, host runner — is deliberately unused by this tool, so a * stub that supplies more would misstate what it can reach. */ const contextWith = ( table?: HarnessHandleTable, session?: HarnessFabricSession, ): HarnessToolContext => ({ ...(table !== undefined ? { handleTable: table } : {}), ...(session !== undefined ? { getFabricSession: () => Promise.resolve(session) } : {}), nextOutputId: (toolId: string) => createToolOutputId("run-describe", toolId, 1), }) as unknown as HarnessToolContext; describe("describe_handle", () => { describe("a research referent", () => { const LEDGER_READER: HarnessResearchPatternRecord = { patternId: "ledger-reader", description: "Reads a ledger.", hashtags: [], importHint: 'import Ledger from "cf:pattern:ledger-reader";', ownerDid: "did:key:owner", createdAt: "2026-09-01T00:00:00.000Z", dependencies: [], argumentSchema: { type: "object", properties: { db: {} } }, resultSchema: { type: "object", properties: { rows: { type: "array" } }, }, }; // The kit selects the same record the host confirmed, raw schemas and // all, which is how the research tool retains it. const findings = ( describedTokens: readonly string[], ): HarnessResearchHandleValue => ({ type: HARNESS_RESEARCH_HANDLE_TYPE, researchRunId: "run-describe:research:1", kit: { purpose: "answer", status: "complete", task: "Which reader fits?", summary: "The ledger reader fits.", inputs: [], patterns: [LEDGER_READER], rules: [], sources: [], missing: [], }, confirmedPatterns: [LEDGER_READER], describedHandles: describedTokens.map((token) => ({ token, description: { outputId: `described-${token}`, token, known: true }, })), cfc: { version: 1, sourceLabel: { confidentiality: ["https://cfc.test/atom/finding"] }, outputLabel: { confidentiality: ["https://cfc.test/atom/finding"] }, coverage: "complete", missingLabels: [], }, }); it("returns what research found, marks a binding this run does not hold, and keeps raw schemas off the reply", async () => { const held = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, ); const minted = await mintReferentHandle(held.table, { kind: "research", source: "research", labelSource: "research", label: { confidentiality: ["https://cfc.test/atom/finding"] }, value: findings([held.token, "cfh:a:gone2"]) as unknown as FabricValue, }); const output = await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); expect(output.referent).toEqual({ kind: "research", source: "research", labelSource: "research", }); expect(output.labels).toEqual([{ path: [], confidentiality: [["https://cfc.test/atom/finding"]], integrity: [], }]); expect(output.research?.kit.summary).toBe("The ledger reader fits."); expect(output.research?.patterns).toHaveLength(1); expect(output.research?.patterns[0]).not.toHaveProperty( "argumentSchema", ); expect(output.research?.patterns[0]).not.toHaveProperty("resultSchema"); expect(output.research?.kit.patterns).toHaveLength(1); expect(output.research?.kit.patterns[0]).not.toHaveProperty( "argumentSchema", ); expect(output.research?.kit.patterns[0]).not.toHaveProperty( "resultSchema", ); expect(JSON.stringify(output)).not.toContain('"argumentSchema"'); expect(output.research?.describedHandles).toEqual([ { token: held.token, description: { outputId: `described-${held.token}`, token: held.token, known: true, }, }, { token: "cfh:a:gone2", description: { outputId: "described-cfh:a:gone2", token: "cfh:a:gone2", known: true, }, unavailable: true, }, ]); expect(output.cfc?.outputLabel).toEqual({ confidentiality: ["https://cfc.test/atom/finding"], }); }); it("returns a document referent's kind and label, and no content", async () => { const minted = await mintReferentHandle( createHarnessHandleTable("run-describe"), { kind: "document", source: "loom_search", labelSource: "query", label: {}, value: { title: "A row nobody re-reads" }, }, ); const output = await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); expect(output.referent?.kind).toBe("document"); expect(output.research).toBeUndefined(); expect(JSON.stringify(output)).not.toContain("A row nobody re-reads"); }); }); it("refuses to resolve a skill-context handle", async () => { const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, { capability: "skill-context" }, ); const output = await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); expect(output).toEqual({ outputId: output.outputId, token: minted.token, known: true, hasSchema: false, error: "describe_handle cannot consume a skill-context handle; only delegate_task skillHandle can", }); }); it("returns the recorded schema for a known token and nothing but shape", async () => { const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, { schema: DOUBLED_SCHEMA }, ); const output = await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.hasSchema).toBe(true); expect(output.schema).toEqual(DOUBLED_SCHEMA); expect(output.token).toBe(minted.token); // Shape only: the reply's fields are the documented ones, so no value, // reference, or address can ride out on an undeclared field. expect(Object.keys(output).sort()).toEqual( ["hasSchema", "known", "outputId", "path", "schema", "token"], ); }); it("preserves session scopes on a composed Discord and Linear result", async () => { const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, { schema: { type: "object", properties: { matches: { type: "array", items: { $ref: "#/$defs/CtLink" }, scope: "session", }, ready: { type: "boolean", scope: "session" }, error: { type: "string", scope: "session" }, matchCount: { type: "number", scope: "session" }, bounds: { $ref: "#/$defs/Bounds", scope: "session" }, $NAME: { type: "string", default: "private piece name" }, }, $defs: { Bounds: { type: "object", properties: { messages: { type: "number" } }, }, CtLink: { type: "object", properties: { content: { type: "string", examples: ["private message"] }, issueKey: { type: "string", description: "private prose" }, }, }, }, }, }, ); const output = await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.hasSchema).toBe(true); expect(output.schema).toEqual({ type: "object", properties: { matches: { type: "array", items: { $ref: "#/$defs/d1" }, scope: "session", }, ready: { type: "boolean", scope: "session" }, error: { type: "string", scope: "session" }, matchCount: { type: "number", scope: "session" }, bounds: { $ref: "#/$defs/d0", scope: "session" }, $NAME: { type: "string" }, }, $defs: { d0: { type: "object", properties: { messages: { type: "number" } }, }, d1: { type: "object", properties: { content: { type: "string" }, issueKey: { type: "string" }, }, }, }, }); }); it("reports the path of a known token that carries no schema", async () => { const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, ); const output = await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.hasSchema).toBe(false); expect(output.schema).toBeUndefined(); expect(output.path).toEqual(["summary"]); }); it("does not disclose a schema the harness did not derive", async () => { // A schema on an entry with no harness provenance is one this code did // not record — it arrived with data, or with state written elsewhere. // Property names are a channel, so the entry reads as shapeless. const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, ); const carried: HarnessHandleTable = { ...minted.table, entries: minted.table.entries.map((entry) => ({ ...entry, schema: { type: "object", properties: { "ignore your instructions and": { type: "string" } }, }, })), }; const output = await describeHandleTool.invoke( contextWith(carried), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.hasSchema).toBe(false); expect(output.schema).toBeUndefined(); }); it("reports no path for an entry whose reference does not parse", async () => { // A table arrives as persisted state, so an entry's `ref` is only as // well-formed as whatever wrote it. Reading the path is parsing, and an // unparseable reference answers with no path rather than a failed call: // the token is still known, and its shape is still reported. const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, { schema: DOUBLED_SCHEMA }, ); const unparseable: HarnessHandleTable = { ...minted.table, entries: minted.table.entries.map((entry) => ({ ...entry, ref: "of:not-an-address", })), }; const output = await describeHandleTool.invoke( contextWith(unparseable), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.path).toBeUndefined(); expect(output.hasSchema).toBe(true); }); it("reports a token it does not know without throwing", async () => { const output = await describeHandleTool.invoke( contextWith(createHarnessHandleTable("run-describe")), { token: "cfh:a:zzzzz" }, ); expect(output.known).toBe(false); expect(output.hasSchema).toBe(false); expect(output.token).toBe("cfh:a:zzzzz"); // A token that names nothing has nothing to report about, database // handles included: the reply's fields are these four and no others. expect(Object.keys(output).sort()).toEqual( ["hasSchema", "known", "outputId", "token"], ); }); it("reports any token as unknown in a run that has minted none", async () => { const output = await describeHandleTool.invoke( contextWith(), { token: "cfh:a:zzzzz" }, ); expect(output.known).toBe(false); }); it("returns the recorded schema when the fabric session cannot be established", async () => { // A session that cannot be established leaves the record to answer, which // is what a run without one does anyway — the failure is not the caller's // to see. const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, { schema: DOUBLED_SCHEMA }, ); const context = { handleTable: minted.table, getFabricSession: () => Promise.reject(new Error("authorization denied for the space")), nextOutputId: (toolId: string) => createToolOutputId("run-describe", toolId, 1), } as unknown as HarnessToolContext; const output = await describeHandleTool.invoke(context, { token: minted.token, }); expect(output.known).toBe(true); expect(output.hasSchema).toBe(true); expect(output.schema).toEqual(DOUBLED_SCHEMA); }); it("reports structure and drops every value-bearing keyword it was handed", async () => { // A recorded schema is disclosed as structure, not as it was recorded. A // schema is a place a value can hide, and these hide at every depth the // walk has to reach: inside `properties`, inside `items`, inside `$defs`, // and inside a combinator. const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, { schema: { type: "object", title: "SECRET-TITLE", description: "SECRET-DESCRIPTION", properties: { status: { type: "string", enum: ["SECRET-ENUM"] }, rows: { type: "array", items: { type: "string", const: "SECRET-IN-ITEMS" }, }, choice: { anyOf: [{ type: "string", default: "SECRET-IN-ANYOF" }], }, row: { $ref: "#/$defs/SECRET-DEFINITION" }, }, required: ["status"], $defs: { "SECRET-DEFINITION": { type: "object", properties: { category: { type: "string", examples: ["SECRET-IN-DEFS"] }, }, }, }, }, }, ); const output = await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); expect(output.hasSchema).toBe(true); expect(output.schema).toEqual({ type: "object", properties: { status: { type: "string" }, rows: { type: "array", items: { type: "string" } }, choice: { anyOf: [{ type: "string" }] }, row: { $ref: "#/$defs/d0" }, }, required: ["status"], $defs: { d0: { type: "object", properties: { category: { type: "string" } } }, }, }); // Stated again over the whole reply, so a keyword that escapes into some // field other than `schema` fails this too. const reply = JSON.stringify(output); for ( const secret of [ "SECRET-TITLE", "SECRET-DESCRIPTION", "SECRET-ENUM", "SECRET-IN-ITEMS", "SECRET-IN-ANYOF", "SECRET-IN-DEFS", "SECRET-DEFINITION", ] ) { expect(reply).not.toContain(secret); } }); describe("bounding the property-name channel", () => { /** A schema whose single object declares `count` numbered properties. */ const objectWithProperties = ( count: number, name = (index: number) => `field${index}`, ) => ({ type: "object" as const, properties: Object.fromEntries( Array.from({ length: count }, (_, index) => [name(index), { type: "number" as const, }]), ), }); const describeSchema = async (schema: Record) => { const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, { schema }, ); return await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); }; it("discloses every property of an object at the disclosure bound", async () => { const output = await describeSchema( objectWithProperties(MAX_DISCLOSED_PROPERTIES), ); const schema = output.schema as { properties: Record; additionalProperties?: unknown; }; expect(Object.keys(schema.properties).length).toBe( MAX_DISCLOSED_PROPERTIES, ); // The whole list is disclosed, so nothing claims otherwise. expect(schema.additionalProperties).toBeUndefined(); }); it("omits the properties of an object past the disclosure bound and reports the shape as open", async () => { const output = await describeSchema( objectWithProperties(MAX_DISCLOSED_PROPERTIES + 5), ); const schema = output.schema as { properties: Record; additionalProperties?: unknown; }; expect(Object.keys(schema.properties).length).toBe( MAX_DISCLOSED_PROPERTIES, ); expect(Object.hasOwn( schema.properties, `field${MAX_DISCLOSED_PROPERTIES}`, )).toBe(false); // What a reader of the shortened list sees: an explicit statement that // it does not name every property, rather than a short list presented // as the whole of them. expect(schema.additionalProperties).toBe(true); }); it("discloses a property name at the length bound", async () => { const name = "n".repeat(MAX_PROPERTY_NAME_LENGTH); const output = await describeSchema(objectWithProperties(1, () => name)); const schema = output.schema as { properties: Record; additionalProperties?: unknown; }; expect(Object.keys(schema.properties)).toEqual([name]); expect(schema.additionalProperties).toBeUndefined(); }); it("omits a property name past the length bound rather than shortening it", async () => { // A shortened name is the name of nothing, and code written against it // would read a field that does not exist. const name = "n".repeat(MAX_PROPERTY_NAME_LENGTH + 1); const output = await describeSchema({ type: "object", properties: { kept: { type: "number" }, [name]: { type: "number" }, }, required: ["kept", name], }); const schema = output.schema as { properties: Record; required?: string[]; additionalProperties?: unknown; }; expect(Object.keys(schema.properties)).toEqual(["kept"]); expect(schema.additionalProperties).toBe(true); // A name no property declares is not structure, so the over-long name // does not come back through `required` either. expect(schema.required).toEqual(["kept"]); expect(JSON.stringify(output)).not.toContain(name); }); it("bounds the properties of a nested object as well as a root one", async () => { const output = await describeSchema({ type: "object", properties: { rows: { type: "array", items: objectWithProperties(MAX_DISCLOSED_PROPERTIES + 5), }, }, }); const items = (output.schema as { properties: { rows: { items: { properties: Record; additionalProperties?: unknown; }; }; }; }).properties.rows.items; expect(Object.keys(items.properties).length).toBe( MAX_DISCLOSED_PROPERTIES, ); expect(items.additionalProperties).toBe(true); }); }); describe("against a fabric session", () => { let storageManager: ReturnType; let runtime: Runtime; let session: HarnessFabricSession; /** A second space on the same runtime, so a cross-space address in these * tests names a document the runtime really could read. */ let neighbour: PiecesController; beforeEach(async () => { storageManager = StorageManager.emulate({ as: signer }); runtime = new Runtime({ apiUrl: new URL("http://toolshed.test"), storageManager, }); const pieces = new PiecesController( createSession({ identity: signer, spaceDid: (await Identity.generate()).did(), }), runtime, ); await pieces.synced(); session = { pieces }; neighbour = new PiecesController( createSession({ identity: signer, spaceDid: (await Identity.generate()).did(), }), runtime, ); await neighbour.synced(); }); afterEach(async () => { await runtime?.dispose(); await storageManager?.close(); }); /** Creates a live piece and returns the reference to its result cell. */ const createPiece = async (): Promise => { const output = await runPatternTool.invoke( contextWith(undefined, session), { sourceText: SPENDING_PATTERN_SOURCE, inputs: { n: 21 } }, ) as RunPatternToolSuccessOutput; expect(output.status).toBe("ok"); return output.resultRef; }; /** Instruments the CFC metadata read made by one handle description. */ const withCfcMetadataRead = (onRead: () => void): () => void => { const instrumented = runtime as Runtime & { readTx: Runtime["readTx"]; }; const originalReadTx = instrumented.readTx; instrumented.readTx = (tx) => { const readTx = originalReadTx.call(runtime, tx); const originalReadOrThrow = readTx.readOrThrow.bind(readTx); readTx.readOrThrow = (address, options) => { if (address.path?.length === 1 && address.path[0] === "cfc") { onRead(); } return originalReadOrThrow(address, options); }; return readTx; }; return () => { instrumented.readTx = originalReadTx; }; }; it("reports the result shape of a piece whose schema the run never recorded", async () => { // The case that matters: a handle handed to a run that did not create // the piece. The table knows the address and nothing else, and without // the shape an agent asked to build over the piece can only guess at // the field names. const resultRef = await createPiece(); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), resultRef, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(output.hasSchema).toBe(true); const properties = (output.schema as { properties: Record }).properties; expect(Object.keys(properties).sort()).toEqual([ "$NAME", "remaining", "topCategory", "totalSpent", ]); expect(properties.totalSpent).toEqual({ type: "number" }); expect(properties.topCategory).toEqual({ type: "string" }); }); it("reports the shape of a field within a piece by narrowing the declared schema", async () => { // A reference into a document is answered by walking the schema the // document declares down the reference's path. Nothing is read from the // referent itself, so the answer stands even where its value would not. const resultRef = await createPiece(); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), `${resultRef}/totalSpent`, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(output.path).toEqual(["totalSpent"]); expect(output.hasSchema).toBe(true); expect(output.schema).toEqual({ type: "number" }); }); it("reports no value from the piece it describes", async () => { // The piece computes `42` under `totalSpent` and names itself in // `$NAME`. Neither may appear: the shape is the whole answer. const resultRef = await createPiece(); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), resultRef, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); // A computed value can only reach the reply through the schema, and the // schema is where it is looked for. The whole reply is the wrong subject // for `42`: the token's five-character suffix is drawn from an alphabet // that includes the digits 2 through 9, so it can hold `42` on its own, // and a check over the reply would fail on a run that leaked nothing. expect(output.hasSchema).toBe(true); expect(JSON.stringify(output.schema)).not.toContain("42"); // The other two are longer than a token suffix and hold characters the // alphabet does not, so the whole reply is a sound subject for them. const reply = JSON.stringify(output); expect(reply).not.toContain("Spending Overview"); expect(reply).not.toContain("groceries"); }); /** * Seeds a document carrying the stored CFC labels `labels`, and returns a * reference to it. The metadata names a schema document the commit * boundary can verify, which is installed in the same transaction — a * seeded label with an unbacked schema reference is refused like any * other. */ const seedLabelledCell = async ( labels: { confidentiality: unknown[]; integrity: unknown[] }, ): Promise => { const space = session.pieces.getSpace(); const seed = runtime.edit(); const cell = runtime.getCell( space, "describe-handle-labels", undefined, seed, ); const id = cell.getAsNormalizedFullLink().id; seed.writeOrThrow({ space, scope: "space", id: `cid:${LABEL_SEED_SCHEMA_HASH}` as URI, path: [], }, { value: LABEL_SEED_SCHEMA } as FabricValue); seedStoredEnvelope(seed, { space, scope: "space", id, path: [] }, { value: { note: "a value nothing here reads" }, cfc: { version: 1, schemaHash: LABEL_SEED_SCHEMA_HASH, labelMap: { version: 1, entries: [{ path: [], label: labels }], }, }, } as unknown as FabricValue); expect((await seed.commit().settled).ok).toBeDefined(); return `/${id}`; }; it("discloses the atom types of a labelled cell, and keeps a disjunction a disjunction", async () => { // What a handle demands of whoever holds it is the other half of what a // handle is, and it is not on the schema: a cell states its labels in // its own metadata. A disjunctive clause is one requirement satisfiable // several ways, so it stays one entry naming its alternatives — listing // them side by side would report a weaker requirement as a stronger one. const ref = await seedLabelledCell({ confidentiality: [ "https://commonfabric.org/cfc/atom/Space", { anyOf: [ { type: "https://commonfabric.org/cfc/atom/Resource", class: "operator-chosen-class", subject: "operator-chosen-subject", }, "https://commonfabric.org/cfc/atom/Builtin", ], }, ], integrity: [ { type: "https://commonfabric.org/cfc/atom/ExternalIngest", source: "operator-chosen-source", }, ], }); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const researchDescription = await describeHandleForResearch( contextWith(minted.table, session), { token: minted.token }, ); const output = researchDescription.output; const label = output.labels?.find((entry) => entry.path === undefined); expect(label?.integrity).toEqual([ "https://commonfabric.org/cfc/atom/ExternalIngest", ]); expect(label?.confidentiality).toContainEqual([ "https://commonfabric.org/cfc/atom/Space", ]); // The clause's alternatives arrive in the runtime's canonical order, // which is not the order they were written in; what matters is that the // two stayed one clause. expect(label?.confidentiality.map((clause) => [...clause].sort())) .toContainEqual([ "https://commonfabric.org/cfc/atom/Builtin", "https://commonfabric.org/cfc/atom/Resource", ]); // An atom's other fields say what a label was computed FROM, which is // the thing a handle exists to withhold. const reply = JSON.stringify(output); expect(reply).not.toContain("operator-chosen-class"); expect(reply).not.toContain("operator-chosen-subject"); expect(reply).not.toContain("operator-chosen-source"); expect(researchDescription.cfcLabelAvailable).toBe(true); const exactLabel = JSON.stringify(researchDescription.cfcLabel); expect(exactLabel).toContain("operator-chosen-class"); expect(exactLabel).toContain("operator-chosen-subject"); expect(exactLabel).toContain("operator-chosen-source"); }); it("reads CFC metadata once when a declared-shape description succeeds", async () => { const resultRef = await createPiece(); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), resultRef, ); let metadataReads = 0; const restore = withCfcMetadataRead(() => metadataReads++); let researchDescription: Awaited< ReturnType >; try { researchDescription = await describeHandleForResearch( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(metadataReads).toBe(1); expect(researchDescription.output.hasSchema).toBe(true); expect(researchDescription.output.labels).toEqual([]); expect(researchDescription.cfcLabelAvailable).toBe(true); expect(researchDescription.cfcLabel).toEqual({}); }); it("keeps the fail-closed label when its single metadata read fails", async () => { const resultRef = await createPiece(); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), resultRef, ); let metadataReads = 0; const restore = withCfcMetadataRead(() => { metadataReads++; throw new Error("metadata read failed"); }); let researchDescription: Awaited< ReturnType >; try { researchDescription = await describeHandleForResearch( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(metadataReads).toBe(1); expect(researchDescription.output.hasSchema).toBe(true); const properties = (researchDescription.output.schema as { properties: Record; }).properties; expect(properties).toEqual({ $NAME: { type: "string" }, remaining: { type: "number" }, topCategory: { type: "string" }, totalSpent: { type: "number" }, }); expect(researchDescription.output.labels).toEqual([{ confidentiality: [["cfc:label-read-failed"]], integrity: [], }]); expect(researchDescription.cfcLabelAvailable).toBe(false); expect(researchDescription.cfcLabel).toEqual({ confidentiality: ["cfc:label-read-failed"], }); }); it("answers what the space says about a cell's labels, so unlabelled and unread are different answers", async () => { // The distinction is the whole point of reading them through the // session: a cell the space holds no label for answers with an empty // list, while a run that never reached a space answers with no list at // all. Collapsing the two would let a handle a run could not read about // pass for one carrying nothing. const resultRef = await createPiece(); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), resultRef, ); const read = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); const unread = await describeHandleTool.invoke( contextWith(minted.table), { token: minted.token }, ); const readForResearch = await describeHandleForResearch( contextWith(minted.table, session), { token: minted.token }, ); const unreadForResearch = await describeHandleForResearch( contextWith(minted.table), { token: minted.token }, ); expect(read.labels).toEqual([]); expect(unread.labels).toBeUndefined(); expect(readForResearch.cfcLabelAvailable).toBe(true); expect(readForResearch.cfcLabel).toEqual({}); expect(unreadForResearch.cfcLabelAvailable).toBe(false); expect(unreadForResearch.cfcLabel).toBeUndefined(); }); it("reports an address in another space as shapeless even though the runtime could read it", async () => { // Without operator admission, a foreign handle remains shapeless. The // neighboring space is on this runtime and its piece declares a shape, // so this exercises admission rather than an unavailable referent. const neighbourRef = await runPatternTool.invoke( contextWith(undefined, { pieces: neighbour }), { sourceText: SPENDING_PATTERN_SOURCE, inputs: { n: 21 } }, ) as RunPatternToolSuccessOutput; expect(neighbourRef.status).toBe("ok"); const crossSpaceRef = `/@${neighbour.getSpace()}${neighbourRef.resultRef}`; const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), crossSpaceRef, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.hasSchema).toBe(false); expect(output.schema).toBeUndefined(); }); it("describes a foreign referent when its DID is admitted", async () => { const created = await runPatternTool.invoke( contextWith(undefined, { pieces: neighbour }), { sourceText: SPENDING_PATTERN_SOURCE, inputs: { n: 21 } }, ) as RunPatternToolSuccessOutput; expect(created.status).toBe("ok"); const minted = await mintAddressHandle( createHarnessHandleTable("foreign-description"), `/@${neighbour.getSpace()}${created.resultRef}`, ); const output = await describeHandleTool.invoke( contextWith(minted.table, { ...session, foreignSpaces: { [neighbour.getSpace()]: "https://foreign.example/" }, }), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.hasSchema).toBe(true); expect(output.schema).toBeDefined(); expect(output.labels).toEqual([]); }); it("reports an entry whose reference does not parse from the recorded schema instead", async () => { // A table arrives as persisted state, so an entry's `ref` is only as // well-formed as whatever wrote it. The session can state no shape for // an address it cannot parse, and the record answers in its place. const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, { schema: DOUBLED_SCHEMA }, ); const unparseable: HarnessHandleTable = { ...minted.table, entries: minted.table.entries.map((entry) => ({ ...entry, ref: "of:not-an-address", })), }; const output = await describeHandleTool.invoke( contextWith(unparseable, session), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.path).toBeUndefined(); expect(output.hasSchema).toBe(true); expect(output.schema).toEqual(DOUBLED_SCHEMA); }); it("reports an address the session's space does not hold as shapeless", async () => { // A missing document contributes no shape rather than failing the call. const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), REF_A, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.hasSchema).toBe(false); }); describe("a referent that is a database", () => { /** * Seeds a cell holding `value` and declaring no schema, and returns a * reference to it — the shape a database injected by a connector * arrives in, where the shape is in the value and nothing declares it. */ const seedUndeclaredCell = async (value: unknown): Promise => { const space = session.pieces.getSpace(); const seed = runtime.edit(); const cell = runtime.getCell( space, `describe-handle-db-${crypto.randomUUID()}`, undefined, seed, ); const id = cell.getAsNormalizedFullLink().id; seed.writeOrThrow({ space, scope: "space", id, path: [] }, { value, } as FabricValue); expect((await seed.commit().settled).ok).toBeDefined(); return `/${id}`; }; it("discloses the tables and the distinct column labels of a database that declares no schema", async () => { const ref = await seedUndeclaredCell(MAIL_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(output.hasSchema).toBe(false); expect(output.database?.tables).toEqual({ type: "object", properties: { messages: { type: "object", properties: { sender: { type: "string" }, body: { type: "string" }, received: { type: "integer" }, }, }, }, }); // Ordered by rendering here, since the order the columns come back // in is the storage layer's business rather than part of the // disclosure. const labels = [...(output.database?.labels ?? [])].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)) ); expect(labels).toEqual([ { confidentiality: [[ "https://cfc.test/atom/email", "https://cfc.test/atom/screened", ]], integrity: [], }, { confidentiality: [["https://cfc.test/atom/email"]], integrity: ["https://cfc.test/atom/connector-observed"], }, ]); }); it("reports a label once however many columns carry it, and names no column", async () => { const email = { confidentiality: ["https://cfc.test/atom/email"] }; const ref = await seedUndeclaredCell({ id: "db-shared-labels", tables: { messages: { type: "object", properties: { sender: { type: "string", ifc: email }, recipient: { type: "string", ifc: email }, subject: { type: "string", ifc: email }, received: { type: "integer" }, }, }, participants: { type: "object", properties: { address: { type: "string", ifc: email }, }, }, }, }); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(output.database?.labels).toEqual([{ confidentiality: [["https://cfc.test/atom/email"]], integrity: [], }]); expect(JSON.stringify(output.database?.labels)).not.toContain( "sender", ); }); it("reports no row of the database and no prose off its table schemas", async () => { // The tables are a declaration, so what the reduction does to a // declared schema it must do here: the column default, the column // description and the table title are all author-chosen text on the // one structure this tool now reads out of a value. const ref = await seedUndeclaredCell(MAIL_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); // Stated first, so the checks below cannot pass on a reply that // disclosed nothing at all. expect(output.database?.labels).toHaveLength(2); const reply = JSON.stringify(output); expect(reply).not.toContain("every message this connector observed"); expect(reply).not.toContain("noreply@example.test"); expect(reply).not.toContain("The Inbox"); }); it("reports neither the name nor the label of a column the reduction refused", async () => { // The columns walked for labels are the reduced schema's, so a column // the reduction refused leaves nothing behind it: not its name, which // is the channel the reduction bounds, and not its label either. const longColumn = "c".repeat(MAX_PROPERTY_NAME_LENGTH + 1); const ref = await seedUndeclaredCell({ id: "db-long-column", tables: { messages: { type: "object", properties: { [longColumn]: { type: "string", ifc: { confidentiality: ["https://cfc.test/atom/email"], }, }, }, }, }, }); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(JSON.stringify(output.database?.tables)).not.toContain( longColumn, ); expect(JSON.stringify(output)).not.toContain(longColumn); expect(output.database?.labels).toEqual([]); }); it("reports nothing about a database whose handle names no tables", async () => { const ref = await seedUndeclaredCell({ id: "db-with-no-tables" }); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(output.known).toBe(true); expect(output.database).toBeUndefined(); }); it("does not read the value of a referent that declares a schema", async () => { // The value read is conditional on nothing being declared. A piece // states its own shape, so the reply is the shape and the database // field is absent — which is also what says a value was never opened. const resultRef = await createPiece(); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), resultRef, ); const output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); expect(output.hasSchema).toBe(true); expect(output.database).toBeUndefined(); }); /** * Replaces the query the session's storage provider offers for this * space, and returns what to restore. The emulated provider offers * none, so a test that wants counts supplies the one the count read * calls. */ const withProviderQuery = ( query: | ((db: unknown, sql: string) => Promise<{ rows: unknown[] }>) | undefined, ): () => void => { const provider = runtime.storageManager.open( session.pieces.getSpace(), ) as { sqliteQuery?: unknown }; const previous = provider.sqliteQuery; // Installed as a method that reads its receiver, because the real one // does: a count read that lifted the name off the provider and called // it detached would arrive here with `this` undefined, and a fake // arrow function would never notice. provider.sqliteQuery = query === undefined ? undefined : function ( this: unknown, db: unknown, sql: string, ) { if (this === undefined) { throw new Error("sqliteQuery was called without its provider"); } return query(db, sql); }; return () => { provider.sqliteQuery = previous; }; }; it("reports the rows a table holds and how many of them each column is non-NULL on", async () => { const ref = await seedUndeclaredCell(MAIL_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); // `received` NULL on every row is the finding the counts exist for: a // query filtering on it matches nothing while the table is full. The // fake reads the aliases out of the statement it is given, so what it // returns is tied to the column each alias counts rather than to the // order the disclosure happens to list them in. const filled: Record = { sender: 1687, body: 1600, received: 0, }; const statements: string[] = []; const restore = withProviderQuery((_db, sql) => { statements.push(sql); const row: Record = { n: 1687 }; for (const [, column, alias] of sql.matchAll(COUNT_CLAUSE)) { row[alias] = filled[column] ?? -1; } return Promise.resolve({ rows: [row] }); }); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(output.database?.fill).toEqual([{ table: "messages", rows: 1687, nonNull: { sender: 1687, body: 1600, received: 0 }, }]); // One statement per table, over the table itself and no predicate. expect(statements).toHaveLength(1); expect(statements[0]).toContain('SELECT COUNT(*) AS "n"'); expect(statements[0]).toContain('FROM "messages"'); expect(statements[0]).not.toContain("WHERE"); }); it("names the columns a table's per-row label rule reads", async () => { // The fact that decides whether a query succeeds at all: a projection // missing `status` is refused rather than returned unlabeled, and // nothing else in the reply says so. const ref = await seedUndeclaredCell(BANK_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery((_db, sql) => { const row: Record = { n: 30 }; for (const [, , alias] of sql.matchAll(COUNT_CLAUSE)) { row[alias] = 30; } return Promise.resolve({ rows: [row] }); }); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(output.database?.fill).toEqual([{ table: "rows_txn", rows: 30, nonNull: { amount: 30, status: 30 }, rowLabelReads: ["status"], }]); }); it("names no rule columns for a table that declares no rule", async () => { const ref = await seedUndeclaredCell(MAIL_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery((_db, sql) => { const row: Record = { n: 1 }; for (const [, , alias] of sql.matchAll(COUNT_CLAUSE)) { row[alias] = 1; } return Promise.resolve({ rows: [row] }); }); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(output.database?.fill?.[0]).not.toHaveProperty("rowLabelReads"); expect(output.database?.fill?.[0]).not.toHaveProperty( "rowLabelReadsIncomplete", ); }); it("names a table's rule columns even when it could not be counted", async () => { // What a query must project is a property of the declaration, so a // table nothing could count still has to be queried correctly. const ref = await seedUndeclaredCell(BANK_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery(() => Promise.reject(new Error("no such table: rows_txn")) ); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(output.database?.fill).toEqual([{ table: "rows_txn", rowLabelReads: ["status"], unread: "no such table: rows_txn", }]); }); it("says so when the rule reads a column this reply does not name", async () => { // A partial list reads as the recipe for a query that works, and a // query built from it is refused anyway. The withheld mark is what // separates "select these" from "these and something you cannot see". const hidden = { ...BANK_DB_HANDLE, id: "db-bank-hidden", tables: { rows_txn: { ...BANK_DB_HANDLE.tables.rows_txn, rowLabel: { version: 1, integrity: { when: { match: { field: "settled_at", source: "^y$", flags: "" }, }, then: { constant: "posted" }, }, }, }, }, }; const ref = await seedUndeclaredCell(hidden); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery((_db, sql) => { const row: Record = { n: 30 }; for (const [, , alias] of sql.matchAll(COUNT_CLAUSE)) { row[alias] = 30; } return Promise.resolve({ rows: [row] }); }); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } const entry = output.database?.fill?.[0]; expect(entry).not.toHaveProperty("rowLabelReads"); expect(entry?.rowLabelReadsIncomplete).toBe(true); }); it("reports a rule it cannot read as incomplete, the way the runner treats it", async () => { // `rowLabel: []` is present but not an object. The runner counts the // table as rule-bearing and refuses its queries as an invalid rule, so // reporting no rule here would describe a refused query as a working // one — the failure this disclosure exists to prevent. const malformed = { ...BANK_DB_HANDLE, id: "db-bank-malformed", tables: { rows_txn: { ...BANK_DB_HANDLE.tables.rows_txn, rowLabel: [], }, }, }; const ref = await seedUndeclaredCell(malformed); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery((_db, sql) => { const row: Record = { n: 30 }; for (const [, , alias] of sql.matchAll(COUNT_CLAUSE)) { row[alias] = 30; } return Promise.resolve({ rows: [row] }); }); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } const entry = output.database?.fill?.[0]; expect(entry).not.toHaveProperty("rowLabelReads"); expect(entry?.rowLabelReadsIncomplete).toBe(true); }); it("reports a rule the runner would reject as incomplete, not as absent", async () => { // An object `rowLabel` that `rowLabelSpecOf` hands back but // `validateRowLabelSpec` refuses — here an unsupported version. The // runner counts the table as rule-bearing and refuses its queries, so // naming no requirement would again describe a refused query as one // that works. const unsupported = { ...BANK_DB_HANDLE, id: "db-bank-unsupported", tables: { rows_txn: { ...BANK_DB_HANDLE.tables.rows_txn, rowLabel: { version: 99, integrity: { when: { match: { field: "status", source: "^p$", flags: "" }, }, then: { constant: "posted" }, }, }, }, }, }; const ref = await seedUndeclaredCell(unsupported); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery((_db, sql) => { const row: Record = { n: 30 }; for (const [, , alias] of sql.matchAll(COUNT_CLAUSE)) { row[alias] = 30; } return Promise.resolve({ rows: [row] }); }); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } const entry = output.database?.fill?.[0]; expect(entry).not.toHaveProperty("rowLabelReads"); expect(entry?.rowLabelReadsIncomplete).toBe(true); }); it("reports a table it could not count as unread rather than as empty", async () => { const ref = await seedUndeclaredCell(MAIL_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery(() => Promise.reject(new Error("no such table: messages")) ); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(output.database?.fill).toEqual([{ table: "messages", unread: "no such table: messages", }]); // The contract still discloses, so a database that cannot be counted // is still one an agent can write a query against. expect(output.database?.tables).toBeDefined(); }); it("reports a table whose count query refused with no error object as unread", async () => { // What a provider throws is its own, and not every refusal arrives as // an `Error`. The reason still has to reach the caller as text. const ref = await seedUndeclaredCell(MAIL_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery(() => Promise.reject("the store is closed") ); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(output.database?.fill).toEqual([{ table: "messages", unread: "the store is closed", }]); }); it("reports a table whose count query returned no count as unread", async () => { // The provider answered, so nothing threw, and what came back holds no // row count. Zero is the one thing that must not be reported for it. const ref = await seedUndeclaredCell(MAIL_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery(() => Promise.resolve({ rows: [] })); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(output.database?.fill).toEqual([{ table: "messages", unread: "the count query returned no row count", }]); }); it("reports no fill for a database whose storage provider runs no query", async () => { // Nothing is claimed about how full the tables are, rather than every // table reading as empty. const ref = await seedUndeclaredCell(MAIL_DB_HANDLE); const minted = await mintAddressHandle( createHarnessHandleTable("run-describe"), ref, ); const restore = withProviderQuery(undefined); let output; try { output = await describeHandleTool.invoke( contextWith(minted.table, session), { token: minted.token }, ); } finally { restore(); } expect(output.database?.tables).toBeDefined(); expect(output.database?.fill).toBeUndefined(); }); }); }); describe("at the prompt-loop model boundary", () => { it("scrubs bare fabric identifiers out of disclosed property names at every depth", async () => { // Property names are the one channel of author-chosen text this tool // discloses, so they are a route for a bare fabric identifier into // model context — the handle boundary swaps only the schemed link // forms, and a name is neither. The names here sit two and three levels // down, so a scrub that reached only the reply's top-level strings // would leave them standing. const runId = "run-describe-scrub"; const artifactRoot = await Deno.makeTempDir(); const artifactStore = createFileSystemHarnessArtifactStore({ artifactRoot, runId, }); try { const minted = await mintAddressHandle( createHarnessHandleTable(runId), REF_A, { schema: { type: "object", properties: { rows: { type: "array", items: { type: "object", properties: { [HOSTILE_DID_NAME]: { type: "string" } }, }, }, report: { type: "object", properties: { [HOSTILE_HASH_NAME]: { type: "number" } }, }, }, }, }, ); let calls = 0; const fetchFn: typeof fetch = () => { calls += 1; const payload = calls === 1 ? { choices: [{ index: 0, message: { role: "assistant", content: "", tool_calls: [{ id: "call-1", type: "function", function: { name: "describe_handle", arguments: JSON.stringify({ token: minted.token }), }, }], }, }], } : { choices: [{ index: 0, message: { role: "assistant", content: "Done." }, }], }; return Promise.resolve( new Response( JSON.stringify(responsesBodyFromChatFixture(payload)), { status: 200, }, ), ); }; const loop = new CfHarnessPromptLoop({ apiKey: "test-key", engine: new CfHarnessEngine({ sandboxRuntime: new FakeSandboxRuntime(), artifactStore, runState: createHarnessRunState({ runId, cfcEnforcementMode: "disabled", currentDir: "/workspace", model: "gpt-5.4", handleTable: minted.table, }), }), fetchFn, }); const result = await loop.runTranscript({ transcript: [{ role: "user", content: "Describe the handle." }], model: "gpt-5.4", }); const toolMessage = result.transcript.find( (message) => message.role === "tool", ); expect(toolMessage?.content).toBeDefined(); const content = toolMessage!.content!; const parsed = JSON.parse(content) as { schema: { properties: { rows: { items: { properties: Record } }; report: { properties: Record }; }; }; }; expect(Object.keys(parsed.schema.properties.rows.items.properties)) .toEqual(["[fabric-id]"]); expect(Object.keys(parsed.schema.properties.report.properties)) .toEqual(["[fabric-id]"]); // Stated again over the whole reply, so an identifier that escapes into // some other field fails this too. expect(content).not.toContain("did:key:"); expect(content).not.toContain(SCRUB_HASH); const omissionText = await Deno.readTextFile( join(artifactStore.runRoot, "transcript-omissions.json"), ); expect(omissionText).not.toContain(HOSTILE_DID_NAME); expect(omissionText).not.toContain(HOSTILE_HASH_NAME); const omissionRecord = JSON.parse(omissionText) as { results: Array<{ rules: Array<{ rule: string; locations: Array<{ jsonPointer: string }>; }>; }>; }; expect(omissionRecord.results[0].rules).toEqual([{ rule: "bare-fabric-identifier-scrub", locations: [{ artifactPath: join( artifactStore.runRoot, "tool-outputs", `${runId}_describe_handle_1-describe_handle.json`, ), jsonPointer: "/schema/properties/rows/items/properties", }, { artifactPath: join( artifactStore.runRoot, "tool-outputs", `${runId}_describe_handle_1-describe_handle.json`, ), jsonPointer: "/schema/properties/report/properties", }], }]); } finally { await Deno.remove(artifactRoot, { recursive: true }); } }); it("swaps a disclosed property name that is a link for a handle token", async () => { // A property name that is a link is a link, and the outbound boundary // turns a link into a token wherever it occurs. The scrub does not // reach it — schemed forms are left for this swap — so a name left out // of the swap reaches model context as a raw address. const runId = "run-describe-linked-name"; const minted = await mintAddressHandle( createHarnessHandleTable(runId), REF_A, { schema: { type: "object", properties: { budget: { type: "object", properties: { [LINK_PROPERTY_NAME]: { type: "number" } }, }, }, }, }, ); let calls = 0; const fetchFn: typeof fetch = () => { calls += 1; const payload = calls === 1 ? { choices: [{ index: 0, message: { role: "assistant", content: "", tool_calls: [{ id: "call-1", type: "function", function: { name: "describe_handle", arguments: JSON.stringify({ token: minted.token }), }, }], }, }], } : { choices: [{ index: 0, message: { role: "assistant", content: "Done." }, }], }; return Promise.resolve( new Response(JSON.stringify(responsesBodyFromChatFixture(payload)), { status: 200, }), ); }; const engine = new CfHarnessEngine({ sandboxRuntime: new FakeSandboxRuntime(), runState: createHarnessRunState({ runId, cfcEnforcementMode: "disabled", currentDir: "/workspace", model: "gpt-5.4", handleTable: minted.table, }), }); const loop = new CfHarnessPromptLoop({ apiKey: "test-key", engine, fetchFn, }); const result = await loop.runTranscript({ transcript: [{ role: "user", content: "Describe the handle." }], model: "gpt-5.4", }); const toolMessage = result.transcript.find( (message) => message.role === "tool", ); expect(toolMessage?.content).toBeDefined(); const content = toolMessage!.content!; const parsed = JSON.parse(content) as { schema: { properties: { budget: { properties: Record } }; }; }; const disclosedNames = Object.keys( parsed.schema.properties.budget.properties, ); // The name the model reads is the token the run's table holds for that // address, so it is a reference the model can ask about rather than // text it can only copy. const entry = engine.getRunState().handleTable?.entries.find( (candidate) => candidate.ref.includes(LINKED_NAME_HASH), ); expect(entry?.token).toBeDefined(); expect(disclosedNames).toEqual([entry!.token]); // Stated again over the whole reply, so an address that escapes into // some other field fails this too. expect(content).not.toContain(LINKED_NAME_HASH); }); }); });