You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
objectql: a secret field accepts an existing secret: ref from a non-system write, so it can name another record's credential row #27582
Filing gate: ① a product defect, measured by the #27495 card thread while implementing PR #27572. ⛔ Security: no reproduction steps, requests, payloads or handle sources go on this card or its PR. The evidence is held by the server seat 1 thread.
reach: exception: security. Reaching it first needs another record's handle id, and every read path masks the stored ref.
Business reason: a secret field's stored value should only ever name a credential that its own record's write sealed. As it stands, one record's secret field can name another record's credential row, and the field then resolves to that other record's credential.
Reader who acts: the triage seat, then the implementer for the credential write path (packages/objectql, encryptSecretFields in engine-secret-internal-fields.ts).
Dedupe (open AND closed): the query "secret ref isSecretRef pre-made handle another record" returned 1 hit, not the same: #27495, which is about refused writes and the null clear.
Back-link: #27495 · ⛔ Not a claim. Grading is triage's.
Mechanism
encryptSecretFields passes through, unchanged, a value that is already a secret: ref (the isSecretRef branch), for every caller, system or not. So one record's secret field can point at another record's credential row.
Bound
PR #27572 adds a release verification: no remaining row of the column may name the handle, and the sys_secret row's namespace / key must be that column's own. Because of it, this path cannot be used through #27572 to delete another record's credential row.
Read fresh. A secret field accepts an existing secret: ref from a non-system write, so one record can name another record's credential row. That is a boundary on stored credentials, so p1.
⛔ No reproduction detail on this card or its PR: the mechanism and the pins only.
Claim: PM loop round 2 · in-seat (disclosure discipline: the evidence stays in the seat's session, never on this card or its PR)
Session: session_01L6bhgUCizoYWQVJWjhdR6F
Account: os-sales
Branch: claude/issue-27582-secret-ref-own-record-only
Worktree: objectstack-issue-27582
Domain: domain:server
Seat: domain:server#2
File surface: packages/objectql/src/engine-secret-internal-fields.ts (encryptSecretFields, the already-a-ref branch) and tests; packages/objectql/src/engine.ts only to pass the existing system-context flag if it is not already in reach (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate), effort: xhigh (p1, security)
Clause-②: no
Metadata-protocol: none
Responsibility: the objectql credential write path (this lane) | #27572's release verification bounds deletion, not reference | non-system writers of a secret field
Thread-read: 6498936750
Premise: holds (read on main 2026-10-29T15:23Z: the already-a-ref branch of encryptSecretFields passes the value through for every caller)
Serial constraints cleared: engine-secret-internal-fields.ts — #27581 (this seat, card thread, the delete release region); the later lander merges main. engine.ts — seat 1's queue names #25522 (pm:queue, no cut claimed)
{
"issue": 27582,
"status": "done",
"branch": "claude/issue-27582-secret-ref-own-record-only",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27695",
"session": "session_01L6bhgUCizoYWQVJWjhdR6F — the seat's id (in-seat subagent = parent's); it is the Claude-Session trailer on every commit",
"premise_still_valid": true,
"summary": "Rule, as ruled: a non-system write may only seal a new value or keep its own record's ref. It lives in the same secret-fields path as #27572: the ref branch of `encryptSecretFields` (engine-secret-internal-fields.ts). The system principal (`context.isSystem === true`) keeps its pass-through. Any other ref is kept only when every row the write lands on already holds exactly that value in that column. The key is then dropped, so the column is not rewritten. Anything else throws the new `SecretRefWriteError` (ADR-0112 `VALIDATION_ERROR`/400, the pair `EmptyCredentialWriteError` uses) before any credential row or record is written. Nothing is looked up, so the answer is the same whatever the ref names. engine.ts's three call sites pass the write's own stored rows: none for an insert, the pre-image for a by-id update, the matched-row memo for a predicate update. That is wider than the claim's engine.ts surface, which was the system flag only; the PR's Acceptance notes declare it. Docs: system-context.mdx row 23e plus its register line (check:system-context-census).",
"tests": "Red first at bf7eff90, tests before the fix: unit `Tests 11 failed | 8 passed (19)`, dogfood `Tests 3 failed | 2 passed (5)`. Green at 75e18b19, after merging origin/main 5e2f3864, under one lock hold (VERDICT command-exit 0): dogfood-closure build 0; objectql typecheck 0; objectql whole suite `Test Files 491 passed (491)` and `Tests 9224 passed (9224)`; 9 credential-channel dogfood files `Test Files 9 passed (9)` and `Tests 60 passed (60)`. Ablations at 75e18b19 on the new unit file (19 cases), each restore proven blob == HEAD with an empty `git diff HEAD`: A1 system check widened to every caller 13 failed; A2 no-stored-row clause deleted 7 failed; A3 every→some 1 failed; A4 any stored ref counts 3 failed; A5 kept key not dropped 2 failed; A6 by-id passes no stored row 3 failed; A7 message carries the value 2 failed. Dist leg D (A1 held, objectql rebuilt, REST dogfood file): preflight present 0, then `3 failed | 2 passed (5)`; after restore and rebuild, preflight absent 0, then `5 passed (5)`. Two earlier first attempts (A2, A5) were refused by the tool, so nothing was mutated; they were redone in the corrected forms above and are not counted.",
"gates": "Local, at 75e18b19: `dispatch-gates --commands` derives 94, the same list as before the merge. All 94 ran with each exit code captured before any pipe, and all 94 exit 0. `--ran` reconcile: 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. check:type-check-debt first answered 3 (queue timeout on the shared lock, not measured); re-run inside this run's own hold, it exited 0. eslint --no-inline-config on packages/objectql and on the new dogfood file: 0 and 0. CI on the PR head 75e18b19: 50 check-runs, 37 success, 13 skipped, 0 other. Success includes Test Core (1–8/8 and aggregate), TypeScript Type Check, Type Check · workspace / debt ledger / consumer gates / source gates, Lint & Repo Gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · CI, release & engine gates, Dogfood Regression Gate, Build Core, Build Docs, Spec property liveness, Check Changeset, PR-body guards, Check PR Size, Governed Surface Queue Guard, Check Documentation Links.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm. The targeted credential-channel dogfood files ran anyway: 75e18b19 · VERDICT command-exit 0 · Test Files 9/9 · Tests 60/60 · no failures",
"mcp_calls": "0",
"api_writes": "2 — (1) relay pr_create: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/pulls → #27695, draft, no assignees, no reviewers, no labels, body read back byte-identical; (2) this comment via post-stamped, relayed: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/issues/27582/comments. git push is not a REST write",
"open_questions": [],
"out_of_scope_findings": ["none"]
}
This corrects one field of the os-dev-report above. PR #27695 now has head ed18660403e13dfee68d9f80d277ab49bebd5cf7. The head's only change over 75e18b1 is the changeset (.changeset/27582-secret-field-own-ref-only.md), which now declares the narrowing. No code or test file changed.
{
"dogfood_battery": "ed186604 · full battery, pnpm --filter @objectstack/dogfood test under the lock, no shard, no path · dogfood-closure build (turbo run build --filter='@objectstack/dogfood^...') exit 0 · VERDICT command-exit 0 · Test Files 353 passed | 3 skipped (356) · Tests 2849 passed | 20 skipped (2869) · no failures"
}
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 真挡得住的权限 | records-forms.encrypted-field-behavior | P②
Triage: p1 ·
domain:server·area:records· security · 2026-10-29T14:08Z⛔ Not a claim.
secretfield accepts an existingsecret:ref from a non-system write, so one record can name another record's credential row. That is a boundary on stored credentials, so p1.pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Claim: PM loop round 2 · in-seat (disclosure discipline: the evidence stays in the seat's session, never on this card or its PR)
Session:
session_01L6bhgUCizoYWQVJWjhdR6FAccount:
os-salesBranch:
claude/issue-27582-secret-ref-own-record-onlyWorktree:
objectstack-issue-27582Domain:
domain:serverSeat:
domain:server#2File surface:
packages/objectql/src/engine-secret-internal-fields.ts(encryptSecretFields, the already-a-ref branch) and tests;packages/objectql/src/engine.tsonly to pass the existing system-context flag if it is not already in reach (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (dispatch-gates --tier: no path-derived mandate),effort: xhigh(p1, security)Clause-②: no
Metadata-protocol: none
Responsibility:
the objectql credential write path (this lane) | #27572's release verification bounds deletion, not reference | non-system writers of a secret fieldThread-read: 6498936750
Premise: holds (read on main 2026-10-29T15:23Z: the already-a-ref branch of
encryptSecretFieldspasses the value through for every caller)Serial constraints cleared:
engine-secret-internal-fields.ts— #27581 (this seat, card thread, the delete release region); the later lander merges main.engine.ts— seat 1's queue names #25522 (pm:queue, no cut claimed)objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
os-dev-report
{ "issue": 27582, "status": "done", "branch": "claude/issue-27582-secret-ref-own-record-only", "pr": "https://github.com/objectstack-ai/objectstack/pull/27695", "session": "session_01L6bhgUCizoYWQVJWjhdR6F — the seat's id (in-seat subagent = parent's); it is the Claude-Session trailer on every commit", "premise_still_valid": true, "summary": "Rule, as ruled: a non-system write may only seal a new value or keep its own record's ref. It lives in the same secret-fields path as #27572: the ref branch of `encryptSecretFields` (engine-secret-internal-fields.ts). The system principal (`context.isSystem === true`) keeps its pass-through. Any other ref is kept only when every row the write lands on already holds exactly that value in that column. The key is then dropped, so the column is not rewritten. Anything else throws the new `SecretRefWriteError` (ADR-0112 `VALIDATION_ERROR`/400, the pair `EmptyCredentialWriteError` uses) before any credential row or record is written. Nothing is looked up, so the answer is the same whatever the ref names. engine.ts's three call sites pass the write's own stored rows: none for an insert, the pre-image for a by-id update, the matched-row memo for a predicate update. That is wider than the claim's engine.ts surface, which was the system flag only; the PR's Acceptance notes declare it. Docs: system-context.mdx row 23e plus its register line (check:system-context-census).", "tests": "Red first at bf7eff90, tests before the fix: unit `Tests 11 failed | 8 passed (19)`, dogfood `Tests 3 failed | 2 passed (5)`. Green at 75e18b19, after merging origin/main 5e2f3864, under one lock hold (VERDICT command-exit 0): dogfood-closure build 0; objectql typecheck 0; objectql whole suite `Test Files 491 passed (491)` and `Tests 9224 passed (9224)`; 9 credential-channel dogfood files `Test Files 9 passed (9)` and `Tests 60 passed (60)`. Ablations at 75e18b19 on the new unit file (19 cases), each restore proven blob == HEAD with an empty `git diff HEAD`: A1 system check widened to every caller 13 failed; A2 no-stored-row clause deleted 7 failed; A3 every→some 1 failed; A4 any stored ref counts 3 failed; A5 kept key not dropped 2 failed; A6 by-id passes no stored row 3 failed; A7 message carries the value 2 failed. Dist leg D (A1 held, objectql rebuilt, REST dogfood file): preflight present 0, then `3 failed | 2 passed (5)`; after restore and rebuild, preflight absent 0, then `5 passed (5)`. Two earlier first attempts (A2, A5) were refused by the tool, so nothing was mutated; they were redone in the corrected forms above and are not counted.", "gates": "Local, at 75e18b19: `dispatch-gates --commands` derives 94, the same list as before the merge. All 94 ran with each exit code captured before any pipe, and all 94 exit 0. `--ran` reconcile: 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. check:type-check-debt first answered 3 (queue timeout on the shared lock, not measured); re-run inside this run's own hold, it exited 0. eslint --no-inline-config on packages/objectql and on the new dogfood file: 0 and 0. CI on the PR head 75e18b19: 50 check-runs, 37 success, 13 skipped, 0 other. Success includes Test Core (1–8/8 and aggregate), TypeScript Type Check, Type Check · workspace / debt ledger / consumer gates / source gates, Lint & Repo Gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · CI, release & engine gates, Dogfood Regression Gate, Build Core, Build Docs, Spec property liveness, Check Changeset, PR-body guards, Check PR Size, Governed Surface Queue Guard, Check Documentation Links.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm. The targeted credential-channel dogfood files ran anyway: 75e18b19 · VERDICT command-exit 0 · Test Files 9/9 · Tests 60/60 · no failures", "mcp_calls": "0", "api_writes": "2 — (1) relay pr_create: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/pulls → #27695, draft, no assignees, no reviewers, no labels, body read back byte-identical; (2) this comment via post-stamped, relayed: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/issues/27582/comments. git push is not a REST write", "open_questions": [], "out_of_scope_findings": ["none"] }changeset(objectql): declare the secret-field ref rule as an accept-s…
objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
os-dev-report addendum
This corrects one field of the os-dev-report above. PR #27695 now has head
ed18660403e13dfee68d9f80d277ab49bebd5cf7. The head's only change over 75e18b1 is the changeset (.changeset/27582-secret-field-own-ref-only.md), which now declares the narrowing. No code or test file changed.{ "dogfood_battery": "ed186604 · full battery, pnpm --filter @objectstack/dogfood test under the lock, no shard, no path · dogfood-closure build (turbo run build --filter='@objectstack/dogfood^...') exit 0 · VERDICT command-exit 0 · Test Files 353 passed | 3 skipped (356) · Tests 2849 passed | 20 skipped (2869) · no failures" }objectstack-fleet commentedon Oct 29, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
ACCEPT → PR #27695 comment 6507953006 · head
ed186604· 2026-10-29T20:47Z · landing sent (contract review PASS6507924217). Follow-ups: #27721, #27722.