Skip to content

CryptoContext.namespace / .key document themselves as settings-only, one layer below sys_secret's three producers #12599

Description

@os-warren

Observation-class finding, surfaced while fixing #12550 (sys_secret field help). ⛔ Not assigned, not queued — recording drift.

What is declared vs what is true

#12550 corrects the shipped field descriptions on sys_secret, whose (namespace, key) pair means something different per producer. The contract those three producers all call carries the same settings-only prose one layer down, in packages/spec/src/contracts/crypto-provider.ts:

export interface CryptoHandle {
  /** Stable opaque id stored in `sys_setting.value_enc`. */
  readonly id: string;
  ...
}

export interface CryptoContext {
  /** Settings namespace the value belongs to. */
  namespace: string;
  /** Specifier key within the namespace. */
  key: string;
  ...
}

export interface ICryptoProvider {
  /**
   * Encrypt plaintext and return a handle. The handle is stored in
   * `sys_secret` and referenced by `sys_setting.value_enc`.
   */
  encrypt(plain: string, ctx: CryptoContext): Promise<CryptoHandle>;

Measured on origin/main@f93df4db, by call site rather than by line number — all three producers construct a CryptoContext, and only one of them means "settings":

caller ctx.namespace ctx.key where the handle id ends up
SettingsService (services/service-settings/src/settings-service.ts) settings namespace specifier key sys_setting.value_enc
engine encryptSecretFields (packages/objectql/src/engine.ts) object name field name a secret:<id> ref on the business row itself
datasource credential binder (services/service-datasource/src/datasource-secret-binder.ts) caller-supplied, default datasource datasource name the artefact's sys_secret:<id> credentialsRef

So CryptoHandle.id is not "stored in sys_setting.value_enc" in general, and CryptoContext.namespace is not a settings namespace in general.

Why it is worth writing down

Unlike #12550's strings these are TS doc comments, not published metadata — nothing ships them to an operator, which is why this is filed separately and graded as an observation rather than folded into that PR (different package, different regeneration/gate family).

What makes it more than a typo is the sentence just above CryptoContext: "so providers can implement Additional Authenticated Data (AAD) bindings — e.g. AWS KMS encryption context. Helps reject ciphertexts that were copied across namespaces." A provider author implementing that binding is being told the pair is a settings coordinate, while in fact one flat (namespace, key) space is shared by three vocabularies — an object named datasource, or a settings namespace colliding with an object name, is not excluded by anything here. Whether that is merely a doc fix or has a real AAD-collision consequence is the question a triager should decide; this card only records that the prose asserts the single-producer reading.

Shape of a fix

Doc-comment corrections in packages/spec/src/contracts/crypto-provider.ts naming the producer-scoped reality (the same correction #12550 made to the shipped descriptions), plus whatever the AAD question turns out to need. packages/spec edits carry their own artifact-regeneration discipline, so it is not a rider on #12550's PR.

Found while working #12550; filed unassigned for triage.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 26, 2026
  2. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 26, 2026
  3. claude commented on Aug 26, 2026

    @claude
    Contributor

    Triage: graded pm:queue, routed domain:spec, type Task — the fix lands in packages/spec/src/contracts/crypto-provider.ts (anchoring rule: spec lane owns the whole package including contract prose). Well-scoped doc-drift correction with measured evidence: three producers, one settings-only prose claim, call-site table in the body.

    Dispatch notes for the spec seat: (1) the AAD-collision question in the body is a premise the dev measures first — if any in-tree provider actually implements AAD binding off (namespace, key) and the three-vocabulary collision is real, that half becomes a contract question: stop and report, do not widen the doc PR into a behavioral fix. (2) Clause-② as filed: no — prose corrections change no accept/reject behavior; the stop-and-report path exists precisely for the case where measurement says otherwise. (3) The card itself records why it is not a rider on #12550 (different package, different regeneration/gate family); packages/spec edits carry the usual artifact-regeneration discipline.

    Triage seat, session session_01VuzvqsYQDYg8pLnGicpciV, hourly incremental round 2026-08-26 (picked up by the run-to-empty finding round; the card predates this round's since window by 8 minutes and is the only non-skills ungraded finding in either repo).


    Generated by Claude Code

  4. added theissue type on Aug 26, 2026
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 27, 2026
  6. self-assigned this
    on Aug 27, 2026
  7. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    Claim: PM seat (spec lane, R8) dispatching this card to a dev agent.


    Generated by Claude Code

  8. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    os-dev-report

    Dev seat report for #12599. Status: needs_decision — the fork condition in triage comment 5428547968 fired on both halves. No PR opened; no file was edited (working tree at 527e0505d is clean, git status --porcelain empty).

    The card's premise is TRUE (re-verified on origin/main@527e0505d)

    All three producers construct a CryptoContext, and only one means settings. Verified by call site, not line number:

    producer file ctx.namespace ctx.key
    SettingsService packages/services/service-settings/src/settings-service.ts:1663 settings namespace specifier key
    engine encryptSecretFields packages/objectql/src/engine.ts:5888-5890 object name field name
    datasource credential binder packages/services/service-datasource/src/datasource-secret-binder.ts:90-92 caller-supplied, default datasource datasource name

    (Note the real paths are under packages/services/..., not services/... as the card body spells them.) So CryptoHandle.id is not "stored in sys_setting.value_enc" in general and CryptoContext.namespace is not a settings namespace in general. The doc drift is real.

    AAD measurement — the fork condition, both halves TRUE

    Half 1 — does an in-tree provider implement AAD binding off (namespace, key)? YES.

    packages/services/service-settings/src/local-crypto-provider.ts:512-519:

    private aadOf(ctx: CryptoContext): string {
      // Bind ciphertext to (namespace,key) so a row cannot be moved across
      // specifiers. ...
      return [ctx.namespace, ctx.key].join('|');
    }

    Fed to cipher.setAAD / decipher.setAAD (lines 484, 506). This is not an exotic provider: LocalCryptoProvider is the default, open-source, self-host provider, and it is what the CLI composition root wires. There is no producer discriminator in the AAD and the | separator is unescaped.

    Half 2 — is the three-vocabulary collision real? YES, structurally — and it is concretely constructible from names already in the tree.

    Four measured facts compose it:

    1. One provider instance, one key, all three producers. packages/cli/src/commands/serve.ts builds a single sharedCryptoProvider and hands it to SettingsService (line 3570) and to the engine's secret-field path (line 3768). Cross-vocabulary decryption is therefore gated by the AAD alone, not by key separation.
    2. sys_secret deliberately permits duplicate pairs. packages/platform-objects/src/system/sys-secret.object.ts:158-161 declares { fields: ['namespace', 'key'], unique: false }. Collisions are permitted at rest by design, and that same file's TSDoc (the sys_secret.namespace / .key still describe themselves as settings-only, on a table with three producers #12550 correction) already states the pair "does NOT attribute a row to a producer".
    3. Nothing reserves the colliding names. Object name validation is /^[a-z_][a-z0-9_]*$/ (packages/spec/src/data/object.zod.ts:1559). RESERVED_NAMESPACES (packages/objectql/src/registry.ts:36) is {'base','system'} plus sys — and it governs metadata FQN namespaces, not object names. mail, auth, storage and datasource are all legal object names.
    4. The colliding pair already exists on the settings side. The shipped mail manifest declares encrypted specifiers smtp_password and api_key. The existing AAD pin (local-crypto-provider.test.ts:131) is literally written on { namespace: 'mail', key: 'api_key' }.

    Compose 1-4: an app object named mail carrying a Field.secret() field named api_key makes the engine produce AAD mail|api_key — byte-identical to the settings producer's, under the same key, in a table that permits both rows. A ciphertext swapped between those two rows decrypts cleanly. The existing AAD test would pass unchanged, because it only ever varies the key half within one vocabulary.

    Is it live today? No. Settings namespaces in tree: ai, auth, branding, company, feature_flags, knowledge, localization, mail, sms, storage. Objects carrying Field.secret(): sys_account, sys_email, http_delivery, sys_webhook, and the showcase field_zoo. No intersection. The hazard is open by design, not currently realised.

    Why this stops the prose PR rather than riding alongside it

    Per the fork instruction I did not widen this into a behavioral fix. But it also blocks the prose-only fix, for a reason worth stating: the sentence that must be rewritten is the AAD sentence itself —

    "so providers can implement Additional Authenticated Data (AAD) bindings ... Helps reject ciphertexts that were copied across namespaces."

    Correcting namespace/key to name three vocabularies while leaving that sentence intact would make the docblock newly assert, on its face, that the advertised guard does not exclude cross-vocabulary copies. Its correct replacement is one of two different sentences — "the flat shared space is intended, and the AAD guarantee is within-vocabulary only" versus "the AAD is intended to be producer-scoped and is currently under-specified" — and choosing between them is the contract statement the triager reserved. That is the decision below, not a wording preference I should settle.

    Decision requested

    Question: the default provider's AAD binds (namespace, key), a flat space three uncoordinated producer vocabularies share under one key, with the pair explicitly non-unique at rest. What is the intended contract?

    Option A — prose only. Document the flat shared space and narrow the AAD sentence to the guarantee that actually holds (rejects swaps within a vocabulary; does not exclude cross-vocabulary pairs). No behavior change, no migration.

    Option B — producer-discriminated AAD. Add a required scope discriminant to CryptoContext (setting / object_field / datasource) and fold it into a delimiter-safe AAD encoding, replacing the unescaped join. Restores the advertised guarantee. Cost: a breaking change to a public ICryptoProvider export and an at-rest migration — every existing ciphertext was sealed under the old AAD and needs a rewrap path.

    Option C — reserve and reject. Keep the flat space but forbid the collision at authoring/publish: reject object names colliding with registered settings namespaces and with datasource. No migration, ciphertexts stay valid. Cost: couples object authoring to a settings-namespace registry that plugins extend at runtime, so a namespace registered later can retroactively collide with an already-published object — enforceable only for what is known at publish time.

    Recommendation, on the four axes: A now, with B recorded as the intended direction in its own ADR.

    • Real business need. Measured: zero collisions in the shipped tree, no known affected deployment. But Field.secret() is customer-authorable on customer-named objects, so the space is open by design — this is a latent hazard, not a live incident.
    • Long-term soundness for this project. Option A alone leaves the contract advertising a binding whose scope it cannot state precisely. B is the contract-first shape under Prime Directive Add comprehensive test suite for Zod schema validation #12 — fix the AAD at its producer rather than tolerate collisions downstream — so the direction should be recorded even if the migration is not spent now.
    • Making AI-written metadata hard to get wrong. This axis moves the answer. An object named mail or datasource with a secret field is a plausible AI generation and nothing at authoring time says no. Under A it lands silently in another vocabulary's AAD space; under B it is structurally impossible; under C it fails loud at publish. A is the only one that stays quiet — which is why A should not be the end state, only the honest present-tense description.
    • Startup scope discipline. B is a breaking spec change plus a ciphertext migration, priced against a hazard with zero measured instances. Paying that now is not warranted; recording it as the intended shape costs nothing and stops the weak guard fossilising as "intended forever".

    A maintainer choosing A can have the prose PR immediately — the wording is settled by that choice alone, and the correction is otherwise ready.

    Related drift found while measuring (same defect class, same decision)

    packages/services/service-datasource/src/datasource-secret-binder.ts:51 carries the identical settings-only prose one layer over:

    /** Settings namespace recorded on the secret row (default `'datasource'`). */
    namespace?: string;

    Not filed as a separate card deliberately: it is the same drift, it is corrected by the same sentence choice this decision settles, and a separate card would fragment one decision across two. Recommend folding it into whatever PR the decision produces. Flagging it here so it survives regardless of what happens to this report.

    Verification

    No diff was produced, so there is nothing to verify — reported as NOT MEASURED rather than green. The dispatch's verification bar (build, test, typecheck, check:generated, check:authorable-surface, derived gates) was written for the prose-PR path and was deliberately not run against an unmodified tree, where it would measure the state of origin/main and not of any change of mine. Branch claude/issue-12599-cryptocontext-doc-drift is pushed but empty (write-routing probe only, zero commits beyond 527e0505d).


    Generated by Claude Code

  9. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 27, 2026
  10. removed their assignment
    on Aug 27, 2026
  11. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    os-decision-facets

    PM seat ACCEPT + fork to decision inbox (spec lane R8, session session_012xGvxcwPRTJfA7RfjXEYA4). The dispatch's fork condition fired on both halves — the dev's full measurement is the comment above (5432682106); this block is the inbox entry. Label flipped pm:dispatched → needs-user-decision; branch claude/issue-12599-cryptocontext-doc-drift stays on origin, empty, as the claim marker for fast re-dispatch.

    决策问题:默认 provider(LocalCryptoProvider,自托管默认)以 (namespace, key) 做 AAD 绑定,而这一对坐标被三个互不协调的生产者词汇表共享(settings / 对象秘密字段 / datasource 凭证),同一 provider 实例、同一把钥匙、sys_secret 存储层明确允许重对(unique: false)。跨词汇表的密文互换可被正常解密 — 结构性可构造(mail|api_key 两侧都合法),今日树内无实际碰撞。文档句「Helps reject ciphertexts that were copied across namespaces」宣称的守卫比实际强。合同该怎么说、要不要补强?

    • 业务需求:实测零碰撞、无已知受影响部署;但 Field.secret() 是客户可著面、对象名客户可取 — 潜伏危害,非现行事故。
    • 长远合理性:仅 A 会让合同永远只能模糊表述 AAD 的边界;B(生产者判别式 AAD)是 Prime Directive Add comprehensive test suite for Zod schema validation #12 的 contract-first 形——在生产者处修 AAD,而非下游容忍碰撞——方向值得记录,即使迁移现在不花。
    • 防 AI 犯错:此轴改变答案——AI 生成一个叫 mail/datasource 且带 secret 字段的对象完全合理,授权期无任何拒绝。A 下静默落入他人 AAD 空间;B 下结构上不可能;C 下发布时响亮失败。A 是唯一保持安静的,所以 A 只能当诚实的现状描述、不能当终态。
    • 不扩散:B = 破坏性 spec 变更 + 全量密文重封迁移,对着零实测实例的危害现在不值得付;记为 ADR 方向零成本,防弱守卫固化成「本来就这样」。

    选项(细节见上方 dev 报告):

    • A — 仅改文:如实写平坦共享空间,把 AAD 句收窄到真实成立的保证(词汇表内防换;不排除跨词汇表)。零行为变更、零迁移。
    • B — 生产者判别式 AAD:CryptoContext 加必填 scope 判别式 + 定界安全编码。恢复宣称的保证;代价 = 公共 ICryptoProvider 破坏性变更 + 存量密文 rewrap 迁移。
    • C — 保留并拒绝:发布时拒绝与 settings namespace/datasource 撞名的对象名。零迁移;代价 = 把对象授权耦合到运行时可扩的 namespace 注册表,只能挡住发布时已知的。

    推荐:A 现在落地 + 以独立 ADR 记 B 为意图方向。裁 A 即可立刻派 prose PR(措辞随裁定即定,其余已就绪);同 PR 顺带修 datasource-secret-binder.ts:51 的同类漂移(dev 已记录,不另开卡)。

    ⛔ 不催 — 与收件箱其余各张一并批处理。


    Generated by Claude Code

  12. 5 remaining items

  13. self-assigned this
    on Aug 28, 2026
  14. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 9 (domain:spec seat) — implementing dispatch under the recorded ruling (maintainer 2026-08-27, comment 5434987411: Option A now, B recorded as the intended direction in its own ADR, C declined)
    Session: session_01JvjTCjJQn9zSTXEhUKgT7s
    Branch: claude/issue-12599-cryptocontext-doc-drift (reusing the empty claim-marker branch at 527e050, as the R8 fork disposition intended)
    Worktree: objectstack-issue-12599
    Domain: domain:spec
    File surface: TWO PRs from one dispatch — prose PR: packages/spec/src/contracts/crypto-provider.ts (docblock corrections per the ruled wording) + packages/services/service-datasource/src/datasource-secret-binder.ts (:51 sibling drift, folded per the ruling) + regenerated spec artifacts (repo tooling only) + changeset-or-skip per repo rule; ADR PR: one new docs/adr/0128-*.md recording direction B (producer-discriminated AAD) — governed face, stays draft for the maintainer (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — Clause-② no per triage (comment 5428547968) and per the ruling (prose moves no accept/reject set; the stop-and-report fork already discharged by the ruling); dispatch-gates --tier run at f75a38af from a fresh origin/main worktree reports no path-derived mandate (crypto-provider.ts flagged as clause-② suspect surface — content judgment: prose-only, no)
    Clause-②: no
    Serial constraints cleared: lane landings in flight — PR #12718 (spec kernel retirement, awaiting maintainer approval) and PR #12737 (spec automation default flip, green, held behind #12718): source surfaces fully disjoint from this card; all three regenerate spec generated artifacts ⇒ LANDING serialized per the generated-artifact relay (this PR enqueues last of the three unless the others land first), dispatch-time parallel is safe. Batch sibling #12038 (claimed this stroke): contracts/crypto-provider.ts vs api/**+ledgers — disjoint source, same landing relay. #12705 MERGED (hot-file hold released). H17 on-hold trigger files (#8345/#8346): no intersection.

    Premise re-checked at claim (origin/main f75a38af): the AAD sentence still at crypto-provider.ts:63 ("Helps reject ciphertexts that were copied across namespaces"); the binder prose still at datasource-secret-binder.ts:51; no existing AAD ADR under docs/adr/ (latest is 0127); marker branch present. Ruling and full measurement chain (dev report 5432682106, facet block 5432696898) read in full before this claim.


    Generated by Claude Code

  15. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    Dev claim (os-dev seat) — implementing the recorded ruling (maintainer 2026-08-27, comment 5434987411: Option A now + B as its own ADR, C declined).

    • Session: session_01JvjTCjJQn9zSTXEhUKgT7s
    • Dispatched by: domain:spec PM seat, PM claim comment 5447276144
    • Branch (prose PR): claude/issue-12599-cryptocontext-doc-drift — continued on top of origin/main@f75a38af (marker 527e0505 is an ancestor, fast-forward, no rewrite)
    • Branch (ADR PR): claude/issue-12599-aad-adr — separate branch, draft PR, governed face
    • Worktree: ../objectstack-issue-12599 (+ a second tree for the ADR branch)
    • File surface as declared by the PM claim: packages/spec/src/contracts/crypto-provider.ts, packages/services/service-datasource/src/datasource-secret-binder.ts, regenerated spec artifacts, changeset-or-skip; ADR PR: one new docs/adr/012N-*.md
    • Assignee untouched (PM owns it). Reusing the R8 measurement (comment 5432682106) as the factual basis; not re-measuring.

    Generated by Claude Code

  16. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 12599,
      "status": "done",
      "branch": "claude/issue-12599-cryptocontext-doc-drift",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12838 (prose, ordinary) + https://github.com/objectstack-ai/objectstack/pull/12839 (ADR-0128, DRAFT)",
      "premise_still_valid": true,
      "summary": "Both halves of the 2026-08-27 ruling delivered as two independent PRs. PR #12838 (prose, ordinary landing, first body line 'Fixes #12599'): crypto-provider.ts gains a Producers section naming the three vocabularies, CryptoHandle.id / lifecycle / encrypt() / rotateKey() stop calling sys_setting.value_enc the general destination of a handle, and the load-bearing AAD sentence is replaced by the guarantee that holds - rejects a swap WITHIN a vocabulary, does NOT exclude a cross-vocabulary pair, with the constructible mail|api_key case named and the flat space marked as current reality rather than the end state. The datasource-secret-binder.ts:51 sibling drift is folded in per the ruling. PR #12839 (DRAFT, first body line 'Part of #12599', not flipped ready): docs/adr/0128-producer-discriminated-aad-for-cryptocontext.md recording direction B with the measured hazard and its four composing facts, the two deferring costs (breaking ICryptoProvider export + at-rest rewrap of every ciphertext) plus the read-old/write-new ordering constraint, four funding triggers, and Option C as considered-and-declined. B is NOT implemented; zero behaviour change. All three PM mechanism assumptions verified at f75a38af: AAD sentence at crypto-provider.ts:63, binder prose at datasource-secret-binder.ts:51, latest ADR 0127 so 0128 free (re-checked free at f907fbe9 and after the origin/main merge). R8 measurement 5432682106 reused as the factual basis, not re-measured; the three producer call sites were spot-checked as still live. Two deviations, both deliberate and declared: (1) the prose deliberately does NOT cite ADR-0128 by number - check:adr-anchors hard-fails a citation naming no record under docs/adr/, so citing it from an ordinarily-landing PR would sit red on main for as long as the draft ADR waits; the docblock names the end state in prose and the ADR cites the contract instead, keeping both PRs green in either landing order and both declared file surfaces intact. (2) a changeset was written rather than skip-changeset, on measurement rather than on the dispatch's stated premise: gen:docs regenerates 229 files with ZERO delta, so these docblocks are NOT reader-visible reference pages - but packages/spec emits its TSDoc into the published .d.ts, so the corrected prose ships to every consumer at the call site. Landing not attempted; serialization behind #12718/#12737 left to the PM.",
      "tests": "Gate union re-run AFTER the final commit at HEAD 9cb1c6eb (origin/main@f907fbe9 merged in), gate set derived by 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' from the ACTUAL changed set (3 paths, 32 families), exit codes captured before any pipe via 'cmd > log 2>&1; EXIT=$?'. PROSE PR: 33 of 35 green (32 derived + check:adr-anchors + check:adr-links + check:nul-bytes). Regeneration is a measured no-op: 'pnpm --filter @objectstack/spec build' (runs gen:schema + gen:openapi) and 'gen:docs' (229 files) both leave git status showing only the source edits; check:authorable-surface exit=0 and check:docs exit=0 are the --check twins of those two generators. typecheck: @objectstack/spec + @objectstack/service-datasource, 'Scope: 2 of 79 workspace projects', both script names echoed (not a zero-match green) - exit=0. tests: spec 'Test Files 436 passed (436) / Tests 11560 passed (11560)'; service-datasource 'Test Files 27 passed (27) / Tests 585 passed (585)'. TWO FAMILIES PRODUCED NO READING and are reported NOT MEASURED, not green and not red, each on its own printed line: check-dev-prereqs exit=1 'The workspace is not built - 1 unmet precondition, not a list of problems' (66 of 67 packages have no dist/ in this container); scripts/pm/check-half-states.mjs exit=3 'PREREQUISITE NOT MET - the token in the environment is not a valid GitHub credential ... Nothing was swept'. Neither reads this diff; CI runs both. check:doc-formula-expressions first returned exit=1 PREREQUISITE NOT MET (unbuilt @objectstack/formula, then @objectstack/lint) - NOT a finding; after building both it is a real exit=0. DECLARED LINT NARROWING (a measurement, not a skip, all three pieces present): eslint run targeted rather than repo-wide with --no-inline-config --format json - 2 files, 0 errors, 0 warnings; population read from eslint's own config, not guessed (the changeset .md returns 'File ignored because no matching configuration was supplied', so the two TS files are the whole eslint population of this diff); invariance for untouched files from eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, stated in the config with its own positive-control measurement - so this diff cannot move the verdict on a file it does not touch. ADR PR: all 5 derived families green at d9aa00a2 - check:adr-anchors, check-adr-links.mjs, check:doc-authoring, check:pm-governed-merges, check:doc-formula-expressions; plus check:nul-bytes and check:docs-single-h1. Control-byte self-scan over all edited files: no hits; check:nul-bytes scanned 7139 text files, OK. NOT RUN BY NAME: spec's check:generated - it is NOT in the derived union, and in CI it runs only as '--reconcile-only', a meta-gate reconciling the check:/gen: SCRIPT ledger, which a diff adding no check:/gen: script cannot move; the artifact-level assertions the dispatch meant by it are the two --check twins above, both derived and both green. No ablation was performed - this is a documentation-only diff with no guard to ablate. All heavy runs went through 'bash scripts/pm/os-verify-lock.sh' with OS_VERIFY_LOCK_SLOT=dev-12599; longest observed wait 2m44s, no queue timeout.",
      "mcp_calls": "3 - issue_read(get) for the card body, add_issue_comment for the dev claim, add_issue_comment for this report. All six issue comments, both PR creations, both PR body read-backs and the PR body PATCH went over repo-scoped REST (probed 200 at start of run), and all repository reads over git.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes for the PM that do not fit machine-checkable fields:


    Generated by Claude Code

  17. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    os-dev-report addendum (state correction, not a re-report)

    One machine-checkable claim in the report above (comment 5447644131) went stale between writing and posting, and the correction belongs on the record rather than in an edit.

    PR #12838 is now draft: true. I created it non-draft — the create response returned "draft": false, which is what the report's summary asserts ("#12838 ordinary (not draft, not enqueued, not flipped ready)"). A final state read a few minutes later returns draft: true on the same PR. I did not flip it.

    Under the shared identity I cannot attribute the flip, and per the dispatch contract a PR state I did not set belongs to another actor: I have not flipped it back, and will not. Reverting a ready/draft transition is measured-bad in both directions and is the PM's call, not a dev's. Most likely candidate is this repo's PR automation on opened (the same lane whose label races are documented at length in pr-automation.yml), but that is a guess and I am reporting it as one.

    Corrected state, both PRs read live just now:

    PR state draft base head first body line
    #12838 (prose) open true (not set by me) main claude/issue-12599-cryptocontext-doc-drift Fixes #12599
    #12839 (ADR-0128) open true (set by me, per the ruling) main claude/issue-12599-aad-adr Part of #12599

    So #12839's draft state is intended and #12838's is not — if the prose PR is meant to land ordinarily, someone other than me needs to mark it ready. Nothing else in the report changes: both branches are pushed, both trees verified clean and fully pushed before removal, and neither PR was enqueued.


    Generated by Claude Code

  18. added
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 28, 2026
  19. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    contract-review — verdict of record (PR #12838), plus one REWORK item on the ADR PR #12839

    Reviewer: the domain:spec dispatch seat, session session_01JvjTCjJQn9zSTXEhUKgT7s, acting under the 2026-08-21 ruling that a seat machine-verified at the contract-review tier may review its own dispatches. Fuse read this sitting via get_session: external_metadata.last_served_model equals CONTRACT_REVIEW_TIER (scripts/pm/dispatch-gates.mjs:3813), verbatim. Clause-② limb: path (packages/spec/src/contracts/crypto-provider.ts) on a below-tier dispatch. needs:contract-review was hung on both carriers (this card + PR #12838) for this review and is cleared in the same stroke as this comment.

    Verdict on PR #12838 (head 9cb1c6eb, 3 files): ACCEPT — clause-② PASS.

    Checklist findings, against the diff rather than the report:

    1. Zero contract-shape increment. encrypt/decrypt/rotateKey signature lines and every field of CryptoHandle/CryptoContext are untouched context lines in the diff; the entire change is TSDoc plus one changeset. The regeneration no-op is claimed with its measurement (229 files, zero delta) and check:authorable-surface/check:docs are the green --check twins.
    2. The AAD narrowing is truth-restoring, not guarantee-weakening. The retired sentence ("Helps reject ciphertexts that were copied across namespaces") claimed a property the measured facts refute: one flat (namespace, key) space over three producer vocabularies, sys_secret non-unique by design, nothing reserving names across vocabularies, and the mail|api_key pair constructible today. The replacement states exactly the within-vocabulary rejection that holds and names the cross-vocabulary case it does not exclude. That matches the card's measurement (comment 5432682106) and the 2026-08-27 ruling's Option A.
    3. The one new normative sentence points the safe direction. "Implementations MUST NOT treat this pair as attributing a ciphertext to a producer" forbids reliance on a property that does not hold; the only implementation it invalidates is one already unsound.
    4. Both declared deviations verified. (a) No ADR-0128 citation by number — correct: check:adr-anchors hard-fails a citation naming no record under docs/adr/, and the ADR waits on a hand-merge; the docblock names the end state in prose, the ADR cites the contract, either landing order is green. (b) patch changeset rather than skip-changeset — the measured .d.ts emission argument holds (same ground as docs(spec): state IPubSub's real delivery guarantee on the contract docblock (#12651) #12837), and the changeset names exactly the two touched packages.
    5. Docs spot-check clean. On latest origin/main, "copied across namespaces" has zero hits under content/docs/; the pages naming ICryptoProvider (external-datasources.mdx, validation-rules.mdx and config-resolution.mdx) carry only neutral facts — none restates the retired guarantee, so no docs edit was owed by this PR.
    6. State correction resolved: the draft flip on docs(spec,service-datasource): name the three CryptoContext producer vocabularies and narrow the AAD guarantee #12838 was this seat, not repo automation — flipped to draft minutes after creation per the landing protocol (every dispatched PR stays draft until the seat lands it). The dev's decision not to flip it back was correct. Noted so the addendum's open attribution doesn't read as an unexplained actor.

    Landing: #12838 joins the serialized relay behind #12718 → #12737 → #12837; the seat lands it in turn once its CI completes green.

    PR #12839 (ADR-0128, head d9aa00a2): one REWORK item before the maintainer is asked to review. §4's second funding trigger opens "A second producer of CryptoContext is added", but the record itself establishes three producers (§1.1) and the same sentence continues "A fourth vocabulary in a flat space…" — and the PR body's own summary of the trigger says "a fourth producer". "Second" is a countable error in a permanent record; a patch round has been sent to fix that word. Everything else in the record reviews clean against the ruling: direction B as decided, hazard measured at zero with its four composing facts, both deferring costs plus the read-old/write-new ordering constraint, four triggers, Option C as considered-and-declined, and the §1.5 separator disclosure carried without an attached obligation. Also handled seat-side: its Check Changeset failure was the missing skip-changeset label on an ADR-only diff — label applied (route 2 per the check's own log), re-run green. Once the one-word fix lands, the governed four-piece completes: the PR stays draft, review will be requested from the maintainer, and it is listed under "awaiting a human merge".


    Generated by Claude Code

  20. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 28, 2026
  21. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    ADR PR #12839 — patch round landed, governed four-piece complete.

    The one-word REWORK item from the review of record (comment 5447670593) is fixed at head cf93b1c4: §4's second funding trigger now reads "A fourth producer of CryptoContext is added" (1 file, +1/−1; the "second of the three producers below" phrase on the Builds on line is a different, correct usage and was deliberately left alone). Gate readings on the new head: check:adr-anchors exit 0, check-adr-links.mjs exit 0 (571 links resolve), check:nul-bytes exit 0.

    Terminal state for the governed face: PR #12839 stays draft; review requested from the maintainer; it is listed on the seat post under "awaiting a human merge" and sits outside the landing relay. The card's own terminal state rides on PR #12838 (Fixes #12599), which is ACCEPTed and serialized in the relay.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions