/** * `propose_provider` — draft a provider (recipe + captured secrets) from ONE * captured request, one match + redaction per target value. Authored once as a * plain {@link RegisteredTool}; hosts pass their own backend resolver. The * draft (recipe + real secret header values) is held server-side under an * opaque `draftId`; the result carries only the secret-free provider. */ import { objectSchema, type ResolveBackend } from '../../../authoring/util.ts' import { draftProvider, type DraftTarget } from '../../../provider/draft.ts' import { OPRF_MAX_BYTES } from '../../../provider/matcher.ts' import type { OprfMode } from '../../../provider/schema.ts' import { verificationWarnings } from '../../../provider/verification-checks.ts' import { defineTool, type RegisteredTool } from '../../../tools/registry.ts' export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool { return defineTool>( { name: 'propose_provider', description: 'Draft a provider from ONE captured request. Pass `targets` (an ' + 'array): one entry per value to extract, each with its `value` ' + '(verbatim) and a distinct `name` (its param). The provider gets ' + 'one match + redaction per target. Returns a draftId + recipe ' + '(never secrets). Prefer a SINGLE request holding ALL values. ' + 'Single-value shorthand: `target` + `name`. Per-target `hash` ' + 'OPRF-hashes that value. BY DEFAULT each `name` is shortened to ' + 'its last alphanumeric token, lowercased, to form the param key ' + '(for example, "employer_name" → "name") — pick names that stay ' + 'distinct ' + 'after shortening ("firstName"/"surname", not "first_name"/' + '"last_name"). Pass `shortenNames: false` to use each `name` ' + 'VERBATIM as the param key instead — it becomes both a regex ' + 'capture-group name and a paramValues key, so it must be a plain ' + 'identifier (letters/digits/underscore, not starting with a ' + 'digit); we recommend lower_snake_case (for example, ' + '"employer_name"), ' + 'but you choose the exact name. For a parameter used only to build a ' + 'later claim request, preserve a `REQ_` prefix (body, header, or ' + 'intermediate value) or `URL_` prefix (path or query value) by ' + 'setting `shortenNames: false`. The prefix hides that internal ' + 'parameter from the data-point UI; it remains available to named ' + 'regexes and `{{param}}` substitutions.', inputSchema: objectSchema( { requestId: { type: 'string' }, name: { type: 'string' }, shortenNames: { type: 'boolean', description: 'Default true: shorten each target `name` to its last ' + 'alphanumeric token, lowercased, to form the param key. ' + 'Set false to use `name` verbatim instead (must be a plain ' + 'identifier — lower_snake_case recommended).', }, targets: { type: 'array', items: { type: 'object', properties: { value: { type: 'string' }, name: { type: 'string', description: 'Parameter key. See `shortenNames`. Keep it unique. ' + 'For hidden internal values, use `REQ_` or `URL_`. Set ' + '`shortenNames: false`. The prefix hides it from the ' + 'data-point UI but preserves substitution.', }, hash: { type: 'string', enum: ['oprf-raw', 'oprf', 'oprf-mpc'], }, }, required: ['value', 'name'], }, }, target: { type: 'string' }, hash: { type: 'string', enum: ['oprf-raw', 'oprf', 'oprf-mpc'] }, }, ['requestId', 'name'], ), }, async(args) => { const backend = resolveBackend(args) const requestId = String(args.requestId) const providerName = String(args.name) const defaultHash = args.hash as OprfMode | undefined const shortenNames = typeof args.shortenNames === 'boolean' ? args.shortenNames : true // Explicit multi-value `targets`, else the single `target` shorthand. const raw = Array.isArray(args.targets) ? args.targets : [] const targets: DraftTarget[] = raw.length ? raw.map((t) => { const o = t as Record return { value: String(o.value), name: String(o.name), ...(typeof o.hash === 'string' ? { hash: o.hash as OprfMode } : {}), } }) : typeof args.target === 'string' ? [{ value: args.target, name: providerName, ...(defaultHash ? { hash: defaultHash } : {}), }] : [] if(targets.length === 0) { throw new Error( 'propose_provider needs a `target` value or a non-empty ' + '`targets` array', ) } for(const t of targets) { const bytes = Buffer.byteLength(t.value, 'utf8') if((t.hash ?? defaultHash) !== undefined && bytes > OPRF_MAX_BYTES) { throw new Error( `OPRF caps hashed values at ${OPRF_MAX_BYTES} bytes; got ` + `${bytes}-byte "${t.value}". Use a tighter target or drop ` + '`hash`.', ) } } const captured = backend.capture.requests.get(requestId) if(!captured) { throw new Error(`no captured request ${requestId}`) } if(!captured.responseBody) { throw new Error(`request body unavailable for ${requestId}`) } const { provider, secretRefs, extractions } = draftProvider( captured, targets, providerName, defaultHash, { shortenNames }, ) // Materialize the real secret header values for later replay/prove; // they stay in the server-side draft, never in this result. const secrets: Record = {} for(const ref of secretRefs) { const real = captured.requestHeaders[ref.name] if(real !== undefined) { secrets[ref.name] = real } } const draftId = backend.drafts.put({ provider, secretRefs, secrets, requestId, extractions, }) const warnings = verificationWarnings(provider) return { draftId, provider, secretRefs, ...(warnings.length ? { verificationWarnings: warnings } : {}), } }, ) }