import { z } from "zod"; import { procedure } from "../protocol/route-meta.js"; import { PRIVACY_KNOWN_BATCH_MAX, PRIVACY_IMAGE_ID, PrivacyDictionarySchema, PrivacyImagePairSchema, PrivacyKnownSourceSchema, PrivacyKnownValueSchema, PrivacyLedgerEntrySchema, PRIVACY_PREVIEW_MAX, PrivacyPreviewSchema, PrivacyRevealSchema, PrivacyShieldPolicySchema, PrivacyShieldStatusSchema, } from "../schemas/privacy.js"; import { OkSchema } from "../schemas/shared.js"; // The privacy shield's policy, its log, and the datasets taught to it. The policy is the owner's alone: it is stored // off the workspace (an agent could otherwise turn off the shield it is held to), and only the owner may change it. // Teaching values only ever masks more, so the agent's CLI may do it; forgetting them is the owner's. export const privacyContract = { status: procedure .route({ method: "GET", path: "/privacy/shield", summary: "The privacy shield and what it covers", description: "Whether personal data is kept from untrusted model providers, which providers are trusted, which local readers are installed, and how many values it has learned.", }) .meta({ agent: true }) .output(PrivacyShieldStatusSchema), setPolicy: procedure .route({ method: "POST", path: "/privacy/shield", summary: "Change the privacy shield", description: "Replaces the policy whole. Turning the shield on puts every turn that starts from then on, whose runtime can be shielded, behind the gateway, and refuses the turns that cannot be shielded on an untrusted provider; a turn already running keeps the route it started with. A change to what is masked or trusted holds from the next model request.", }) .meta({ floor: "owner", control: "never", panel: false }) .input(PrivacyShieldPolicySchema) .output(OkSchema), log: procedure .route({ method: "GET", path: "/privacy/log", summary: "What the privacy shield did lately", description: "Each model request the gateway handled: which provider, whether it was trusted, how many of each kind of personal data it found, and the tokens it gave with the masked text around them. Never the values.", }) .output(z.array(PrivacyLedgerEntrySchema)), reveal: procedure .route({ method: "POST", path: "/privacy/reveal", summary: "Read tokens back to their values", description: "The value each token stands for, from the vault, so the owner can check what the shield masked and spot a value it should have left alone. Only the owner may ask; a token the vault never gave out is left out.", }) .meta({ floor: "owner", control: "never", panel: false }) .input(PrivacyRevealSchema) .output(z.record(z.string(), z.string())), image: procedure .route({ method: "POST", path: "/privacy/image", summary: "An image before and after masking", description: "The picture as it was and as the shield painted it before it left, for a log entry's image replacement. The picture before holds the personal data, so only the owner may ask. Only the most recent images are kept; an older one answers not found.", }) .meta({ floor: "owner", control: "never", panel: false }) .input(z.object({ id: z.string().regex(PRIVACY_IMAGE_ID) })) .output(PrivacyImagePairSchema), dictionary: procedure .route({ method: "GET", path: "/privacy/dictionary", summary: "The name lists the shield finds names by", description: "Every list the dictionary holds (first names, surnames, words that are names only beside other evidence, titles), the owner's own lists among them, whether each is switched on, how many words each has and where they come from. With a query, what the dictionary as the owner set it makes of it as a name, and for one word the listed words starting with it.", }) .meta({ agent: true }) .input(z.object({ query: z.string().max(100).optional().describe("A word, the start of one, or a full name.") })) .output(PrivacyDictionarySchema), preview: procedure .route({ method: "POST", path: "/privacy/preview", summary: "Try the shield on a text", description: "What the shield would mask in the text with the policy as saved (every detector, the name lists, the owner's own rules, the values the vault knows) and what found each. Nothing is given a token or kept, whatever the switch says.", }) .input(z.object({ text: z.string().max(PRIVACY_PREVIEW_MAX) })) .output(PrivacyPreviewSchema), sources: procedure .route({ method: "GET", path: "/privacy/known", summary: "The datasets taught to the shield", description: "Each source values were taught from, and how many. The values themselves are never sent back.", }) .meta({ agent: true }) .output(z.array(PrivacyKnownSourceSchema)), learn: procedure .route({ method: "POST", path: "/privacy/known", summary: "Teach the shield a dataset's values", description: "Each value is masked wherever it appears from now on, in every form it is written, whether or not the detectors would have found it. Teaching only ever masks more, so the agent may do it.", }) .meta({ agent: true }) .input( z.object({ source: z.string().min(1).max(200).describe("Where the values came from: a file and its column, a table."), values: z.array(PrivacyKnownValueSchema).max(PRIVACY_KNOWN_BATCH_MAX), }), ) .output(z.object({ added: z.number().int(), known: z.number().int() })), forget: procedure .route({ method: "POST", path: "/privacy/known/forget", summary: "Forget a taught dataset", description: "Stops matching the values taught from one source. Tokens already given to them still resolve, so earlier conversations keep reading right.", }) .meta({ floor: "owner", control: "never", panel: false }) .input(z.object({ source: z.string().min(1).max(200) })) .output(z.object({ forgotten: z.number().int() })), };