Repository navigation
feat(spec): IAIConversationService.get / list and IAIService.listPendingActions take the caller's execution context - #23197
Conversation
…cution context IAIConversationService.get / list and IAIService.listPendingActions gain an optional trailing `context?: ExecutionContext`, the type and position the other store contracts use. An implementation forwards it as the engine read's `options.context`; an omitted context leaves the read unchanged. Type-level and runtime pins beside the contract. Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ Co-authored-by: Claude <noreply@anthropic.com>
…-store-caller-context
📓 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:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 141 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 cb636e7e7125ae612bb81e055672783713eb4edd && git checkout cb636e7e7125ae612bb81e055672783713eb4edd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8323cad97e148fb1955511479adacad19aee10fd c128fd10a116ff96c85f750dc744c70bf7008c6f && git checkout -B drift-repro 8323cad97e148fb1955511479adacad19aee10fd && git merge --no-ff c128fd10a116ff96c85f750dc744c70bf7008c6f
node scripts/docs-audit/affected-docs.mjs --json 8323cad97e148fb1955511479adacad19aee10fd
|
Contract reviewServed-tier: Inputs read, and nothing else: card #23158 (body and all 4 comments: triage 6139367236, claim 6139494839, ① Derived judgments
② Semver level
③ Boundary flagsEvery dev flag (
Implemented-by: VERDICT: PASS |
Fixes #23158
Clause-②: yes (widening: an optional parameter on three published store-contract methods)
What changes
The three AI store reads take the caller's execution context as an optional trailing parameter. This is the contract change that objectstack-ai/cloud#2844 Q1 ruling C names (maintainer 「同意」, cloud comment
6138290535), in the shape triage's direction gives (6139367236).IAIConversationService.getget(conversationId)get(conversationId, context?: ExecutionContext)IAIConversationService.listlist(options?)list(options?, context?: ExecutionContext)IAIService.listPendingActionslistPendingActions?(filter?)listPendingActions?(filter?, context?: ExecutionContext)The TSDoc states the rule an implementation follows:
contextof every engine read the method makes (BaseEngineOptions.context), so the read runs as that caller. Under ADR-0096 D5 strict mode an engine read with no principal is refused with 403.list'soptions.userIdis called out as a filter on the owner. It is not the caller's identity.Files:
packages/spec/src/contracts/ai-service.ts(three signatures, their TSDoc, and one type-only import), a pin file beside it, and.changeset/23158-ai-store-caller-context.md(@objectstack/specminor, carrying theClause-②line and the FROM → TO). No generated artifact moved (check:generatedgreen, see below). No object'saccessor sharing changed.Measurements
H1: "the shape the other store contracts use"
Population at
origin/mainde24373ec,packages/spec/src/contracts/*.ts(test files excluded): the methods that take the caller's context.context: ExecutionContext, requiredsharing-service.ts: 15 (:249–:588) ·approval-service.ts: 12 (:881–:996) ·share-link-service.ts: 3 (:237,:245,:256)context?: ExecutionContext, optionalanalytics-service.ts: 3 (:312,:329,:351)context?: SecurityContext(aPartialofExecutionContext)security-service.ts: 20 (:418–:1049)data-engine.tsoptions?: BaseEngineOptions(:289–:311) ·objectql-engine.ts:198·knowledge-service.ts:43(executionContext?) ·automation-service.ts:709The majority is
ExecutionContextfrom../kernel/execution-context.zod.js, as a positional parameter after the method's own arguments: 33 methods across 4 contracts. In 31 of the 33 it is the last parameter, and in the other 2 one optional bag follows it.approval-service.ts'slistRequests(filter, context)has the same shape aslisthere. The parameter is optional, as triage directs and asIAnalyticsServicealready does. The import specifier carries.js, as 5 of the 7 sibling imports do.Conflict, stated rather than resolved silently: the card body's ask said "the same shape the engine's read verbs take", which is the options-bag form. Triage's direction (Zone 1, binding) says "the shape the other store contracts use", and this PR follows triage. The two forms meet at the engine call: the type-level pin below proves
ExecutionContextis assignable toBaseEngineOptions['context'], so an implementation forwards it with no conversion.H2: where the objects live, and the owner-reads-own-rows reading: NOT MEASURED here
None of the four objects is declared in this repository. On
origin/mainde24373ec, agit grepfor the declaration spellingname: 'OBJECT'returns nothing forai_conversations,ai_messages,ai_pending_actionsorai_agent_memories. The only hits are test fixtures inplugin-sharingand name references in the share-link routes. The control leg is the same grep forname: 'sys_user', which hitspackages/platform-objects/src/identity/sys-user.object.ts:46. This repository's liveness ledger (packages/spec/liveness/agent.json:96) cites the store atcloud @ef5a4344: packages/service-ai/src/agent-memory.ts#AgentMemoryStore — the store, over the object ai_agent_memories (access private, no API). I did not read that file directly.So triage's measurement ("with a caller threaded through, does an ordinary member read their own rows on these
access: privateobjects?") cannot run in this repository. It can run in objectstack-ai/cloud'sservice-ai, with this contract at cloud's pin. The cloud card that switches the 18 reads back to the caller's identity is the natural place to measure it. No sharing card is filed on an unmeasured premise. If that measurement shows an owner is refused, the sharing question is filed then, from there.H3: the pins, without an in-repo implementer
git greponorigin/mainfinds no implementer ofIAIConversationServiceor ofIAIService.listPendingActionsin this repository. The only users areai-service.test.ts's mocks and a type reference inpackages/runtime/src/domains/ai.ts:67, which never calls these methods. So the pins inpackages/spec/src/contracts/ai-store-caller-context.pin.test.tsare contract-level:check:test-typecheckundertsconfig.test.json):ExecutionContext | undefined, and nothing follows it (arity1 | 2/0 | 1 | 2);ExecutionContextis assignable to the non-nullBaseEngineOptions['context'];IDataEngine'sfind/findOne(aPick), is written to the TSDoc's forwarding rule.getmakes two,ai_conversationsthenai_messages) carries it asoptions.context, by reference, and the realBaseEngineOptionsSchema.safeParseaccepts those options.service-ai, and that implementer's own conformance lives with it.Reverse verification (ablation), one per method, from the committed state (the HEAD blob of
ai-service.tsis84b68834da23). Each leg ran throughnode scripts/ablation-replace.mjs: the anchor hit 1 → 0, the blob changed, and on restore the blob equalled HEAD andgit diff HEADwas empty. Nodist/rebuild was needed, because the pin imports./ai-serviceand../data/data-engine.zodby relative path, so the program readssrc/. Measured with the spec test-layer program (tsc --noEmit -p tsconfig.test.json):getwithoutcontextGetTakesTheCallerContextandGetArity(TS2344), TS2493, the in-testget(TS2322 / TS7006), the two-argument call (TS2554)listwithoutcontextListTakesTheCallerContextandListArity, the same classeslistPendingActionswithoutcontextListPendingActionsTakesTheCallerContextandListPendingActionsArity, the same classesThe observed direction is turning red, at the type layer. The runtime half stays green under each ablation, because types are erased at run time. That is expected: the contract is a type, and the type half is what fails.
H4: the typecheck debt ledger
The pinned entry
src/contracts/ai-service.test.ts:TS2739 … missing … getTurnState, updateis unmoved. A mock written against the shorterget/liststays assignable, so no new signature arrives.check:test-typecheckis green with the ledger untouched:52 file(s) / 246 error(s) / 135 pinned signature(s) held. The new pin file compiles with zero errors.packages/spec/test-typecheck-debt.jsonis not in this diff.Verification
Readings taken at HEAD
c128fd10a: this branch after mergingorigin/main32fa1942e, which touchedpackages/specin disjoint files.pnpm --filter @objectstack/spec build:VERDICT command-exit 0. The rebuiltdist/contracts/index.d.tscarries the three new signatures (:5745,:6084,:6094).pnpm --filter @objectstack/spec check:generated: exit 0,All 14 generated artifacts are up to date. That includescheck:api-surfaceandcheck:export-origins(no export added or removed) andcheck:test-typecheck.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/contracts/ai-store-caller-context.pin.test.ts src/contracts/ai-service.test.ts:Test Files 2 passed (2),Tests 27 passed (27).pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2:Test Files 662 passed (662),Tests 20114 passed | 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 85 commands. Each line was run with its exit code captured before any pipe.check:doc-formula-expressionspassed afterturbo run build --filter=@objectstack/formula --filter=@objectstack/lint:22 record-scoped formula example(s) across 474 files / 1391 TS blocks judged clean.check:lean-entry-closurepassed after building@objectstack/objectql:Admitted set held exactly (15 packages).check:dual-build-cjs-loads, which needs every package'sdist/, so a whole-workspace build. As a declared narrowing, all 19@objectstack/specCJS require entries the gate lists (--list) load in a freshnodewith 0 failures. The diff adds only a type-only import, which is erased from the emitted JS.--ranreconciliation:85 derived famil(ies) accounted for — 84 run, 1 NOT-MEASURED.pnpm exec eslint --no-inline-config --format jsonover the two changed.tsfiles reports 2 files, 0 errors and 0 warnings. Both files are in the config's population (--print-configresolves rules for each). The config enables no type-aware linting (eslint.config.mjs:327–:328: noparserOptions.project), so this diff cannot move the verdict on any untouched file. The fullpnpm lintis CI's run.Acceptance notes
IAIConversationService.getTurnStatereads the turn'sai_messages, andaddMessage/updateanswer with the conversation. None of them takes a caller context. The ruling and triage name exactly three methods, so this PR changes those three. Carrier: the cloud card that switches the 18 reads back, which will see whether any of these is among the reads D5 refuses.Generated by Claude Code