spec: declare confirmedBlueprintIdentity on the protocol's ToolExecutionContext - #18676
Conversation
…cutionContext` A published handler already authorizes on this consent digest — cloud's `apply_blueprint` gate makes a matching blueprint identity one clause of the decision to build a whole app (cloud#1954 / cloud PR #2005) — while the member it reads was declared only on cloud's augmented context and reached by a structural cast. The protocol is the baseline, so the field is declared here. - `ToolExecutionContext.confirmedBlueprintIdentity?: string`, with the provenance docblock `userMessageText` carries: stamped by whichever layer owns the agent route, only by in-process server code on that route, never from a request body; `undefined` means "no confirmed identity on this turn" and authorizes nothing. - `confirmed-blueprint-identity-contract.pin.test.ts` pins the declaration half — the member, its thread through `ChatWithToolsOptions.toolExecutionContext`, its optionality and its `string` type — each negative leg paired with a lit positive one. - Changeset: `@objectstack/spec` minor. `Clause-②: yes` (widening). Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
…y-protocol Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3dd6d151830a894ef8a6f0b1aba02b4f95bd9bc9 && git checkout 3dd6d151830a894ef8a6f0b1aba02b4f95bd9bc9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7f7b8557dfb459719e1eb473cd9ea5f10a7322b9 5ddd6f45eea88e13fdd75fa0120638d1c2331f21 && git checkout -B drift-repro 7f7b8557dfb459719e1eb473cd9ea5f10a7322b9 && git merge --no-ff 5ddd6f45eea88e13fdd75fa0120638d1c2331f21
node scripts/docs-audit/affected-docs.mjs --json 7f7b8557dfb459719e1eb473cd9ea5f10a7322b9
|
契约复核记录 — 交付后复核,档位
|
Contract reviewHead: Tier: Verdict: PASS Full record, transcribed verbatim and unedited by this seat: comment Reviewed-by: ⭐ 本条是补记,且补记的原因是本席的顺序错误 —— 记下来而不是抹掉本席先摘了两侧 闸门要的三个记号,本席 14:5xZ 发的那条记录一个都不满足(已实测,⛔ 不是推断):
⇒ 从闸门的视角,那正是它点名的半态:「a cleared gate with nothing behind it, indistinguishable from never reviewing」。闸门是对的,本席是错的。 ⛔ 本条不是对复核代理记录的编辑。 记录按「逐字落档,要么整条采信要么整条作废,⛔ 永不编辑」处理,原样留在 正确顺序应是:先验记录形态 → 再摘载体 → 再复跑检查。 本席做成了摘在前。下一张卡按前者走。
Generated by Claude Code |
Contract reviewServed-tier: 80/80 档位读数是本席自己 grep 复核那一轮转录里 harness 盖的 served-model 字段所得:80 个带戳的 assistant 轮次,80 个在档,0 个脱档。⛔ 不是派发时传的 ① Derived judgments逐字全文见本 PR 评论
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #15937
ToolExecutionContextinpackages/spec/src/contracts/ai-service.tsis the contract a tool handler may rely on. A published handler already authorizes onconfirmedBlueprintIdentity— cloud'sapply_blueprintgate makes a matching blueprint-identity digest one clause of the decision to build a whole app (cloud#1954 / cloud PR #2005) — while the member it reads was declared only on cloud's own augmentedToolExecutionContextand reached by a structural cast. This declares it where the contract lives.Clause-②: yes (widening) — one new OPTIONAL member on a published interface, so the shape a consumer writes against grows. Nothing previously admitted is refused, no member is renamed or retired, and no producer is required to write it. Contract-review tier. The
needs:contract-reviewcarrier is the seat's on both sides and this PR touches neither.The ruling this implements
The maintainer's ruling is recorded on #15937 at comment 5553002827 (2026-09-05), on option 1, verbatim 「同意」. A triage stroke the next morning re-hung
needs-user-decisionon an already-decided card, and the director seat reversed it at 5559258277 with "Nothing to re-decide". So the implementer's brief is the ruling comment, not the card body's two-option analysis.Ruling items 1 and 2 are implemented here. Item 3 is a read-and-report and is answered below. Item 4 — delete cloud's augmentation at
tool-registry.ts:45, replace the cast inconfirm-gate.tswith the typed read — is a cloud follow-up card blocked on this field being published and pinned. It is not in this diff, and this seat neither touched cloud nor filed that card.What landed
ToolExecutionContext.confirmedBlueprintIdentity?: string, placed next touserMessageTextbecause that is the same class of field: route-owner-populated, advisory-to-nobody, never client-derived. Its docblock carries the four things item 1 asks for.undefinedmeans "no confirmed identity on this turn" and authorizes nothing — the same fail-closed readingactorandisSystemalready carry (Security: AI ToolExecutionContext contract documentssystem-level as the missing-actor default — a contract-level fall-open across all data tools #2991).confirmed-blueprint-identity-contract.pin.test.ts— a new pin, following the file's existingaction-confirmation-contract.pin.test.tsconvention.ToolExecutionContexthad no test naming it anywhere in this repository before this PR (git grep -l ToolExecutionContextover the tree lit 8 files: two CHANGELOGs, two generated artefacts, one consumer, the contract itself — and zero test files). Six legs, each negative one paired with a lit positive one on the same helper:ToolExecutionContextChatWithToolsOptions.toolExecutionContextundefinedstring | undefinedstring, notanyany/unknownleaves the directive unused — TS2578Changeset:
@objectstack/specminor, per ruling item 2.Item 3 —
systemInvocation: read, reported, NOT folded inMeasured on this base, repo-wide with same-scope controls:
systemInvocationuserMessageTextisSystemHalf of the class holds and half is not measurable from here. The class this card names has two halves: (a) absent from the protocol, and (b) load-bearing for an authorization decision a published handler makes through a structural cast. (a) is confirmed above. (b) lives in
objectstack-ai/cloud, which is outside this card's scope and which this seat did not open — so it is reported as unmeasured rather than assumed.What the protocol side does say is an asymmetry worth handing to whoever picks this up:
confirmedBlueprintIdentityhad no protocol counterpart at all, whereassystemInvocationsits besideisSystem, which this same interface declares and whose docblock calls it "the ONLY way to obtain system behaviour from the tool loop". So the first question forsystemInvocationis not "declare it" but "does it duplicateisSystem" — and only a "no" makes it this card's case. That is a different first question, so it is not plainly the same class of omission, it needs its own card, and this PR does not widen into it.Premise re-check on this base
The dispatch flagged three premises. All three were re-measured rather than inherited.
30be2ac0bb:confirmedBlueprintIdentityacrosspackages/= 0 files (grep exit 1); lit controluserMessageText= 1 file; lit controlToolExecutionContext= 8 files; dark control = 0. The one-file control proves the grep reachespackages/and finds the analogue field and nothing more — the 8-file control is the wide one.scripts/pm/git-history.mjs touchanswersfe0d9a4241ab782628e5a7bc4ac61baeb505fbd1, 2026-09-07T10:00:37Z, +134 lines — two days after the 2026-09-05 ruling, and proved without fetching, the touch and its parent sitting above the shallow floor. Reading that commit's own diff: not one added or removed line names aToolExecutionContextmember. The +134 is the action-confirmation contract (spec: define the confirmation contract behindaction.ai.requiresConfirmation#16531) elsewhere in the file. So the interface anduserMessageText's docblock were re-read on this base and the docblock matched in shape is the one on the tree; the ruling's quoted field list still describes it, its "UI-context block" beingcurrentObjectName/currentViewName/surfaceContext.confirmedBlueprintIdentity"alongsideuserMessageTextandsystemInvocation". Of those three,userMessageText's single repo-wide hit is this spec file — the protocol, not an augmentation — whilesystemInvocationhas zero. The three were never alike in where they are declared, which is the whole reason this card exists.Verification
Readings are cited at the commit they were taken on.
pnpm lint(the repo-wideeslint . --no-inline-config) is CI's run; what is below is a declared narrowing with its evidence.pnpm --filter @objectstack/spec build, exit 0.packages/spec/dist/contracts/index.d.tscarries the member, so the readings below are against a rebuilt tree, not a cached one.git statusclean after the build: no generator moved a tracked artefact, andapi-surface/contracts.jsonis unmoved because it records exported symbol names, not interface members.pnpm --filter @objectstack/spec typecheck, exit 0 (that istsc --noEmit,check:scripts-typecheckandcheck:test-typecheckin series). The test-layer ratchet holds at 54 files / 259 errors / 144 pinned signatures, unchanged.pnpm --filter @objectstack/spec test, exit 0: 486 files, 13856 tests passed. The new pin, run on its own, is 6 passed.bf79a0cctoa646979419), andcheck:test-typecheckwent from exit 0 to exit 1, naming 10 type errors in the new pin file. The restore leg put HEAD's bytes back —git hash-objectback tobf79a0cc3f6fb0505e0edf6c5665c3f004e371f4,git diff HEADempty,git status --porcelainempty — and the same gate returned to exit 0. Both legs ran under atrap ... EXIT INT TERMrestore with absolute paths. Nodistpreflight: the pin imports./ai-servicerelatively, so it resolves tosrc, and adistreading would be about the wrong artefact.5ddd6f45ee. ① The population comes from eslint's own config: the base block isfiles: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], so the changeset.mdis outside it entirely — eslint says so itself, "File ignored because no matching configuration was supplied". ②--format jsoncounts 3 entries, of which 2 are in-population: 0 errors, 0 warnings on both.tsfiles. ③ Invariance, quoted fromeslint.config.mjs: "this repo runs oneeslint.config.mjs, which never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file, test or not." With no type-aware rule, nothing in this diff can move the verdict on a file the diff does not touch; the config's file rosters are likewise untouched.check:nul-bytesexit 0 over 8798 files, plus a directgrep -naPsweep of the three changed files, which found none.The full re-derived gate sweep, every exit code, and the reconciliation against
dispatch-gates.mjs --ranare in theos-dev-reportcomment on #15937.Acceptance notes
Noted while in the file, not filed and not fixed here:
userMessageText's docblock, the line directly above the new member, reads "(cloud, post-ADR-0025)" — a bare ADR number for what is, in context, cloud's record. AGENTS.md Prime Directive 13 is explicit: an ADR lives in the repository whose code it governs, cloud decisions are cited "ascloud ADR-NNNN— ⛔ never as a bare number, whichscripts/check-adr-anchors.mjsresolves against this registry (the two number independently)". This repository's ownADR-0025isdocs/adr/0025-plugin-package-distribution.md, and every other bareADR-0025in the tree means that one, so a reader following this citation lands on a real page about plugin packaging. The new member spells itcloud ADR-0025, which is why the two lines differ. Not folded in: it is a different defect class from this card's, so the bounded in-place exemption does not open, andcheck:adr-anchorsdoes not read source docblocks at all (it checks only the entries underscripts/adr-anchors/), so nothing is currently red. Reported with dedupe words in the report comment for the triage seat to file or discard.ToolExecutionContextcarrying no test at all before this PR is now half-closed: the new pin asserts the new member and the thread it travels on, deliberately not the other twelve members. Widening it to the whole interface would be scope this card does not carry.Provenance: authored by an
os-devseat under the PM dispatch on #15937, sessionsession_01LvwGppdonww4zGLWZo5rho(https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho).Generated by Claude Code