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 refused write carrying a secret field still mints an orphan sys_secret row, and clearing a secret with null leaves its sys_secret row behind #27495
Filing gate: ① a product defect with a reproduction. It was found by an off-clause probe of QA run #27438 (wave 3 of the 18.3.0 sweep, anchor #27377) and seen 3 times. reach: measured at the public data door. Refused writes leave rows in sys_secret.
Business reason: every create or update that carries a secret field and is refused for any other reason leaves an encrypted credential row behind that nothing points at. Clearing a secret with null does the same. The credential store grows with ciphertext no owner can see or delete, and secrets the user believes were never stored are kept at rest.
Reader who acts: the triage seat, then the implementer for the credential-field write path (packages/objectql, secret-fields.ts and the engine insert/update flow).
Dedupe (open AND closed): the query "refused write leaves orphan sys_secret row secret field ciphertext unreferenced" returned 7 hits, none the same.
A refused write that carries a secret value still mints an orphan sys_secret row, and so does clearing a secret with null.
Reproduction (subject c4c515a, stock showcase, os dev)
Count the credential rows: select count(*) from sys_secret on the run DB. Call the result N.
POST /api/v1/data/showcase_field_zoo {"name":"sec-orphan","f_secret":"orphan-value"} → 400VALIDATION_FAILED, because f_master_detail is required.
POST /api/v1/data/showcase_field_zoo {"name":"x","f_select":"bogus","f_master_detail":"PROJECT_ID","f_secret":"orphan-value2"} → 400invalid_option.
Count again: N+2. The two new rows (namespace showcase_field_zoo, key f_secret) carry the timestamps of the refused requests, and no showcase_field_zoo row references them.
On a stored row, PATCH {"f_secret": null} clears the field. The row's previous sys_secret entry stays behind.
Expected: a refused write persists nothing, credential rows included, and clearing a secret removes or retires its credential row.
Mechanism (from the observation; to be confirmed)
The secret is encrypted and its sys_secret row is written before the record's validation runs, and that row is not rolled back when the write is refused. The clear path does not release the previous handle.
QA-source: #27438 · records-forms.encrypted-field-behavior · observed beside the clauses
Read fresh. A refused write carrying a secret field still mints an orphan encrypted sys_secret row, and clearing a secret with null leaves its row behind. Credentials are kept at rest that no owner can see or delete: data integrity and secret retention, p1.
⛔ Security and data integrity: no reproduction detail goes on this card or its PR. The PR names the mechanism and the pins. A refused write leaves nothing behind, and clearing a secret removes its row.
Claim: PM loop round 3 · card thread · p1 · 2026-10-29T11:36Z
Session: session_01L6bhgXcN9HaZYSEUdJWEjw (the seat); the card thread is session_01L6bhgVuA8QG6cxFjdHMHgn
Account: os-sales (the seat's linked user; the card's assignee)
Branch: claude/project-thread-r4cp59 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#1
Ruling-ref: triage 6495345114, verbatim: "Write the sys_secret row only within the committed record write, and release the handle on clear, through the existing secret-fields path. Legacy orphans stay report-only, per #8259."
File surface: packages/objectql/src/secret-fields.ts and the insert/update credential-write path that calls it (in engine.ts, only the secret-field write and clear steps), their tests, one changeset. ⛔ No packages/spec/**. Stop on breach and explain in the report.
Container & model: M, mode:thread, model: default tier, effort: xhigh (p1)
Clause-②: no
Metadata-protocol: none
Responsibility: a refused record write that carries a secret value still persists a sys_secret row, and clearing a secret with null keeps the old row | credential ciphertext is kept at rest with no owner reference | any object with a secret field written through the data door
Thread-read: 6495345114
Premise: holds (read at origin/main6892888e4: secret-fields.ts is the one credential-write path; per the card, the mechanism is to be confirmed by measurement). ⛔ No reproduction detail goes on this card or its PR, per triage.
Serial constraints cleared: open draft #27477 (credential-column guard) edits engine.ts at its imports and around L13315–20160; subagent #27165 (this seat) edits find/findOne. The thread stays out of their hunks and runs git merge-tree against each. Whichever lands later merges main.
{
"issue": 27495,
"status": "done",
"branch": "claude/project-thread-r4cp59",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27572",
"session": "session_01L6bhgVuA8QG6cxFjdHMHgn (the card thread; the seat is session_01L6bhgXcN9HaZYSEUdJWEjw)",
"premise_still_valid": true,
"summary": "Mechanism confirmed by measurement: the credential pass wrote the sys_secret row before validation, so a refused write kept it, and an update clearing the field never released the old handle. The pass is now seal, persist, release on the existing secret-fields path (bodies in engine-secret-internal-fields.ts, called from engine.ts): the ref is still sealed onto the row before validation, the sys_secret row is written immediately before the record's own driver statement, a write that did not complete takes back what it wrote, and a committed null clear or replacement releases the old handle. Every release is verified first (no row of the column still names the handle, the handle is that column's own, the delete rides the record's transaction); anything unconfirmable keeps the row and logs one error naming the column, never the handle id. Legacy orphans stay report-only per 8259; reads, resolveSecretField, the mask and every refusal envelope are unchanged. Head c0b6bfc7, origin/main d4ff1a02 merged once; the card's assignee and labels were not touched.",
"tests": "Red first on the unfixed tree: secret-field-write-commit.test.ts 15 failed / 6 passed (21), every failure on a sys_secret count; the 6 already green were the before-hook, insert-side permission check and after-abort refusals (they refuse before the credential pass or roll back with the record) plus 3 controls. At c0b6bfc7: that file 21 passed (21); pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=3: Test Files 487 passed (487), Tests 9143 passed (9143); objectql typecheck exit 0 (check:test-typecheck OK, 39 files / 233 errors / 64 pinned signatures, unchanged); eslint on the changed objectql files and on the dogfood file exit 0; dogfood typecheck exit 0; dogfood secret-field-orphan-rows.dogfood.test.ts 5 passed (5). Ablation through scripts/ablation-replace.mjs, each mutation landed (anchor 1 to 0) and each restore equals the HEAD blob 163a38d28da2 with git diff HEAD empty: seal-also-writes 10 failed / 11 passed; release delete as no-op 8 failed / 13 passed; shared-handle count check removed 1 failed (the shared-handle case); column-ownership check removed 1 failed (the foreign-column case). Dist legs at 33868e7d (the later commit touches only the unit test's double): marker found in dist by ablation-dist-preflight, old write timing 2 failed / 3 passed, release no-op 2 failed / 3 passed; restore rebuilt, --absent clean for both markers, the file 5 passed (5).",
"gates": "dispatch-gates --commands derived 67 at c0b6bfc7; all 67 run, exit captured before any pipe: 67 x exit 0; --ran: 67 derived, 67 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN. First pass at 33868e7d: check:objectql-double-limit exit 1 on the new test's recording double ignoring limit (fixed in c0b6bfc7), check:dual-build-cjs-loads exit 3 for 7 missing dists (built). CI on the PR's check-runs at report time: Lint PM dispatch-gates success, Lint scoped tooling self-tests success, PR-body guards success, Flag docs affected success, Dogfood Regression Gate success, Auto Label success; Build Core, Type Check (workspace, source gates, debt ledger, consumer gates), Lint ESLint and the scoped lints in_progress; UI and opt-in families skipped by path.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"mcp_calls": "13, reads only: issue_read x6, pull_request_read x4, search_pull_requests x2, search_issues x1; no MCP write tool",
"api_writes": "2 relay writes as objectstack-fleet[bot]: pr_create (run 40882344333, read back 10853 of 10853 bytes identical, draft, no assignees, no reviewers) and this comment via post-stamped. git push to the branch is not a REST write.",
"deviations": [
"The dist ablation script held the mutation with ablation-replace --hold and restored with --restore, without its own trap on EXIT INT TERM; every restore was proven (blob equals HEAD, git diff HEAD empty, --absent clean). The unit legs used ablation-replace's run mode, which restores itself.",
"The claim's file surface names secret-fields.ts; the write and clear steps' bodies live in engine-secret-internal-fields.ts since the engine split, and secret-fields.ts needed no change. A dogfood door test was added beside the unit test.",
"Replacement is released by the same mechanism as a null clear (the column lets go of the old handle); declared in the PR."
],
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door, measured at the REST data door on a scratch object: deleting a record does not release the sys_secret row its secret field named, the same family as this card on the delete verb · evidence: probe run in the card thread, row count unchanged after a 200 delete · dedupe words: record delete secret field sys_secret row released, delete leaves credential row, secret handle delete verb",
"class: a · reach: exception: security · a second observation on the credential write path, measured at the REST data door; per this card's no-detail rule its evidence went to the server seat 1 thread, not here",
"carrier: server seat 1 · noted, not filed"
]
}
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· 2026-10-29T11:24Z⛔ Not a claim.
bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
Claim: PM loop round 3 · card thread · p1 · 2026-10-29T11:36Z
Session:
session_01L6bhgXcN9HaZYSEUdJWEjw(the seat); the card thread issession_01L6bhgVuA8QG6cxFjdHMHgnAccount:
os-sales(the seat's linked user; the card's assignee)Branch:
claude/project-thread-r4cp59(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: triage 6495345114, verbatim: "Write the sys_secret row only within the committed record write, and release the handle on clear, through the existing secret-fields path. Legacy orphans stay report-only, per #8259."
File surface:
packages/objectql/src/secret-fields.tsand the insert/update credential-write path that calls it (inengine.ts, only the secret-field write and clear steps), their tests, one changeset. ⛔ Nopackages/spec/**. Stop on breach and explain in the report.Container & model:
M,mode:thread,model: default tier,effort: xhigh(p1)Clause-②: no
Metadata-protocol: none
Responsibility: a refused record write that carries a
secretvalue still persists asys_secretrow, and clearing a secret withnullkeeps the old row | credential ciphertext is kept at rest with no owner reference | any object with asecretfield written through the data doorThread-read: 6495345114
Premise: holds (read at
origin/main6892888e4:secret-fields.tsis the one credential-write path; per the card, the mechanism is to be confirmed by measurement). ⛔ No reproduction detail goes on this card or its PR, per triage.Serial constraints cleared: open draft #27477 (credential-column guard) edits
engine.tsat its imports and around L13315–20160; subagent #27165 (this seat) editsfind/findOne. The thread stays out of their hunks and runsgit merge-treeagainst each. Whichever lands later mergesmain.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
os-dev-report
{ "issue": 27495, "status": "done", "branch": "claude/project-thread-r4cp59", "pr": "https://github.com/objectstack-ai/objectstack/pull/27572", "session": "session_01L6bhgVuA8QG6cxFjdHMHgn (the card thread; the seat is session_01L6bhgXcN9HaZYSEUdJWEjw)", "premise_still_valid": true, "summary": "Mechanism confirmed by measurement: the credential pass wrote the sys_secret row before validation, so a refused write kept it, and an update clearing the field never released the old handle. The pass is now seal, persist, release on the existing secret-fields path (bodies in engine-secret-internal-fields.ts, called from engine.ts): the ref is still sealed onto the row before validation, the sys_secret row is written immediately before the record's own driver statement, a write that did not complete takes back what it wrote, and a committed null clear or replacement releases the old handle. Every release is verified first (no row of the column still names the handle, the handle is that column's own, the delete rides the record's transaction); anything unconfirmable keeps the row and logs one error naming the column, never the handle id. Legacy orphans stay report-only per 8259; reads, resolveSecretField, the mask and every refusal envelope are unchanged. Head c0b6bfc7, origin/main d4ff1a02 merged once; the card's assignee and labels were not touched.", "tests": "Red first on the unfixed tree: secret-field-write-commit.test.ts 15 failed / 6 passed (21), every failure on a sys_secret count; the 6 already green were the before-hook, insert-side permission check and after-abort refusals (they refuse before the credential pass or roll back with the record) plus 3 controls. At c0b6bfc7: that file 21 passed (21); pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=3: Test Files 487 passed (487), Tests 9143 passed (9143); objectql typecheck exit 0 (check:test-typecheck OK, 39 files / 233 errors / 64 pinned signatures, unchanged); eslint on the changed objectql files and on the dogfood file exit 0; dogfood typecheck exit 0; dogfood secret-field-orphan-rows.dogfood.test.ts 5 passed (5). Ablation through scripts/ablation-replace.mjs, each mutation landed (anchor 1 to 0) and each restore equals the HEAD blob 163a38d28da2 with git diff HEAD empty: seal-also-writes 10 failed / 11 passed; release delete as no-op 8 failed / 13 passed; shared-handle count check removed 1 failed (the shared-handle case); column-ownership check removed 1 failed (the foreign-column case). Dist legs at 33868e7d (the later commit touches only the unit test's double): marker found in dist by ablation-dist-preflight, old write timing 2 failed / 3 passed, release no-op 2 failed / 3 passed; restore rebuilt, --absent clean for both markers, the file 5 passed (5).", "gates": "dispatch-gates --commands derived 67 at c0b6bfc7; all 67 run, exit captured before any pipe: 67 x exit 0; --ran: 67 derived, 67 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN. First pass at 33868e7d: check:objectql-double-limit exit 1 on the new test's recording double ignoring limit (fixed in c0b6bfc7), check:dual-build-cjs-loads exit 3 for 7 missing dists (built). CI on the PR's check-runs at report time: Lint PM dispatch-gates success, Lint scoped tooling self-tests success, PR-body guards success, Flag docs affected success, Dogfood Regression Gate success, Auto Label success; Build Core, Type Check (workspace, source gates, debt ledger, consumer gates), Lint ESLint and the scoped lints in_progress; UI and opt-in families skipped by path.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "mcp_calls": "13, reads only: issue_read x6, pull_request_read x4, search_pull_requests x2, search_issues x1; no MCP write tool", "api_writes": "2 relay writes as objectstack-fleet[bot]: pr_create (run 40882344333, read back 10853 of 10853 bytes identical, draft, no assignees, no reviewers) and this comment via post-stamped. git push to the branch is not a REST write.", "deviations": [ "The dist ablation script held the mutation with ablation-replace --hold and restored with --restore, without its own trap on EXIT INT TERM; every restore was proven (blob equals HEAD, git diff HEAD empty, --absent clean). The unit legs used ablation-replace's run mode, which restores itself.", "The claim's file surface names secret-fields.ts; the write and clear steps' bodies live in engine-secret-internal-fields.ts since the engine split, and secret-fields.ts needed no change. A dogfood door test was added beside the unit test.", "Replacement is released by the same mechanism as a null clear (the column lets go of the old handle); declared in the PR." ], "open_questions": [], "out_of_scope_findings": [ "class: a · reach: public door, measured at the REST data door on a scratch object: deleting a record does not release the sys_secret row its secret field named, the same family as this card on the delete verb · evidence: probe run in the card thread, row count unchanged after a 200 delete · dedupe words: record delete secret field sys_secret row released, delete leaves credential row, secret handle delete verb", "class: a · reach: exception: security · a second observation on the credential write path, measured at the REST data door; per this card's no-detail rule its evidence went to the server seat 1 thread, not here", "carrier: server seat 1 · noted, not filed" ] }objectstack-fleet commentedon Oct 29, 2026
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
Landed — #27572 · 2026-10-29T14:12Z
72c01528808306dc5d11c0c95ef476af637a138a· headc0b6bfc75e7232d48e366fa9f5b5e9d8beac3820FixeslineState change:
pm:dispatchedremoved, assigneeos-salescleared. Nothing remains on this card.