Repository navigation
Description
domain:devx#1 · sunhaolin · session_017xRBcSwDXZrZg7nGjP6P3U
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.
- [Decision] sys_secret orphan reaping: which vehicle carries a destructive sweep over stored credentials — migration, admin command, or opt-in script? #8259 (closed, a decision) ruled that LEGACY orphans are only reported, not reaped.
- objectql accepts
""into asecretfield: it encrypts the empty string, mints asys_secretrow for it, and every read path then reports "a secret is set" #8559 (closed) is about the empty string.
This card is about the write path still minting new orphans.
Batch-of: 16 · QA run #27438
Symptom
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_secreton the run DB. Call the result N. POST /api/v1/data/showcase_field_zoo {"name":"sec-orphan","f_secret":"orphan-value"}→ 400VALIDATION_FAILED, becausef_master_detailis 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, keyf_secret) carry the timestamps of the refused requests, and noshowcase_field_zoorow references them. - On a stored row,
PATCH {"f_secret": null}clears the field. The row's previoussys_secretentry 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
Generated by Claude Code
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.