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
service-settings: sys_setting_audit.old_hash is never written (hard-coded null) and new_hash for a non-secret value is an FNV-32 digest, while both columns declare "SHA-256 of the … value" #26465
Filing gate: ① a declared-but-unenforced field (ADR-0049 enforce-or-remove), measured live. Found by a probe beside the clauses of a QA run.
Business reason: an operator reading the settings audit trail to see whether a change actually changed anything ("lets operators detect duplicate writes", in the column's own docblock) cannot. Every row's old_hash is null, including over a previously set value, and the digest format is not the one the schema promises.
Reader: the service-settings lane, at packages/services/service-settings/src/settings-service.ts (the audit write) and src/crypto-adapter.ts (digest). The declarations are in packages/platform-objects/src/system/sys-setting-audit.object.ts and its twin sys-platform-setting-audit.object.ts.
Dedupe (open AND closed): searched "sys_setting_audit old_hash always null". The only hit is #23804 (closed, sys_user_preference writes not audited), which is a different defect.
reach: any settings write through PUT /api/settings/:ns. Measured on a fresh stock showcase boot, 756220c4, as admin.
Checklist item priority: P1
type: Bug
QA-source: #26456 · platform-core.settings-hub-roundtrip · observed beside the clauses
Reproduction
On a fresh showcase boot, the dev seed has already written localization.timezone = UTC (tenant). Its audit row carries new_hash: "fnv32:582aa22d".
As admin, PUT /api/settings/localization with {"timezone":"Asia/Tokyo"} → 200.
Read the new audit row (GET /api/v1/data/sys_setting_audit, newest first). It shows action: set, new_hash: "fnv32:43850a34", and old_hash: null, although the previous value was set.
Expected, per the declaration: old_hash holds the digest of the previous value ("Null when previously unset"), and both hashes are "SHA-256 of the … value (canonicalised)". Actual:old_hash is always null, and non-secret digests are FNV-1a 32-bit.
Mechanism
settings-service.ts:2498 passes oldHash: null on every write. The previous value is never read for the audit.
For a non-secret value, the digest is this.crypto.digest(stableStringify(rawValue)), which is the adapter's FNV-1a (crypto-adapter.ts:80–88, "short, stable, non-cryptographic. Audit-only"). Secret values take a separate keyed digest, secretAuditDigest. That path is correct and is not part of this card.
sys-setting-audit.object.ts:137–156 declares both columns as SHA-256.
So the fix is either to write the previous value's digest and align the declared format (or the digest), or to remove or re-describe the column.
p2, ADR-0049 enforce-or-remove:sys_setting_audit.old_hash is always null, and new_hash is FNV-32 while the column says SHA-256. Default to enforce: write the prior value's digest and use the declared algorithm, in both audit objects. If the dev finds no reader, the remove arm is a schema change: it comes back to triage first. Found by the 18.2.0 wave-2 sweep ([sweep] v18.2.0 release test sweep #25807).
Claim: PM loop round 4 · card thread · 18.2.0 wave-2 split with server seat 1
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5ojTc5Fbj8xxXSfo1K
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-su83v0 (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#2
Ruling-ref: triage 6434134831 (ADR-0049 enforce arm: write the previous value's digest, use the declared SHA-256; the remove arm returns to triage)
File surface: packages/services/service-settings/src/settings-service.ts (the audit write), src/crypto-adapter.ts (an audit digest path), their tests, and a changeset on @objectstack/service-settings. ⛔ No packages/platform-objects/** column change (the declarations stand), ⛔ no backfill of stored rows, ⛔ the keyed secret digest path stays as is
Container & model: S, mode:thread, default model and effort (set at thread open)
Clause-②: no
Metadata-protocol: none
Responsibility: service-settings audit write (ADR-0049 declared-but-unenforced)
Thread-read: 6434134831
Premise: holds (read 2026-10-27T05:45Z at origin/main45de8c6cba: card open, unassigned, pm:queue with its triage line; the split with server seat 1 gives this card to seat 2); the dev re-measures it first, and a refutation is a premise_still_valid: false hand-back
Resume-from: none — fresh from origin/main
Serial constraints cleared: yes — the dev re-measures open PRs on the file surface before the first edit and before pr_create
State: pm:queue → pm:dispatched, assigned to os-project-manager, in this act.
The card thread found a path reader this fix falsifies. docs/qa/platform-checklist/areas/platform-core.json, the settings secret-digest item at about :3150-3237, uses a non-secret key's fnv32: digest as its step-7 control. Once non-secret digests are SHA-256, that control can never match.
The amendment: the file surface now also covers that checklist item. Its control is recomputed for the new digest, and the item's history gains a change entry. This is the same defect class and a mechanical edit, and no governed path is involved.
os-dev-report
{
"issue": 26465,
"status": "done",
"branch": "claude/project-thread-su83v0",
"pr": "#26532",
"session": "session_01FEkQ5ojTc5Fbj8xxXSfo1K",
"premise_still_valid": true,
"summary": "ADR-0049 enforce arm. old_hash is now the declared digest of the previous value. It comes off the identity probe upsertRow already makes (the previous row is snapshotted before the update, and returned beside previousEnc), so there is no extra read. It is null for a first write, after a reset, and for a previously secret row. The non-secret digest is now settingAuditDigest (crypto-adapter.ts): 'sha256:' + hex SHA-256 of the canonical JSON, used instead of CryptoAdapter.digest, on both audit objects and on the sys_audit_log config_change digest (one shared variable). The secret path (secretAuditDigest, keyed hmac-sha256) is byte-identical, and a secret write keeps old_hash null. Claim amendment (granted by the seat): the QA checklist item platform-core.settings-audit-secret-fingerprint goes rev 4 -> 5, so its step-7 control computes sha256 instead of fnv32. Also corrected: one in-package docblock in sys-secret-orphan-report.ts. Changeset: @objectstack/service-settings patch.",
"tests": "New src/settings-audit-old-hash.test.ts (7 cases) drives PUT /api/settings/:namespace through registerSettingsRoutes, over a real ObjectQL with the real SysSetting/SysPlatformSetting/SysSecret/SysSettingAudit/SysPlatformSettingAudit and the plugin's buildSettingAuditWriter, and reads the rows back from the driver store. Tenant rung: first set (old null), overwrite (old = digest of previous), same-value write (old == new), set->reset->set. User scope: a write reads its own row, not another user's. Global rung: set, overwrite, same value. Secret control: keyed new_hash, null old_hash. Updated the non-secret case in settings-audit-secret-digest.test.ts (FNV -> sha256). Ablation, once, with scripts/ablation-replace.mjs: 'oldHash: isEncrypted ? null : SettingsService.previousAuditDigest(previous),' -> 'oldHash: null,', anchor 1->0, blob 7425ed93->3b7fd19c. Result: 5 of 7 red; first-set and the secret control stay green as expected. Restore: blob == HEAD, git diff HEAD empty. The test imports src (no dist hop). Final head a397e85 (merged origin/main b7f2b0c), under the lock: service-settings build + typecheck + vitest -> Test Files 61 passed (61), Tests 1007 passed (1007), VERDICT command-exit 0. tsc --listFiles includes both test files. The first full run had also caught a real bug that is now fixed: an engine may return the live stored object, so the previous row is copied before the update; settings-system-context.pin's reap case was red until then.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on a397e85: 64 commands, each run with its exit captured before any pipe, all 64 exit 0. --ran: '64 derived famil(ies) accounted for, 64 run, 0 NOT-MEASURED'; changed lines 484, under 3000. First pass, before the merge: check-plugin-teardown-shape --self-test exit 3 (shallow clone; passed after git fetch --unshallow) and check:dual-build-cjs-loads exit 3 (no dist on other packages). Both were exit 0 in the final pass. eslint --no-inline-config packages/services/service-settings/src exit 0. PR check-runs: in_progress at report time, not waited on.",
"dogfood_battery": "n/a: no '!' in the PR title and no (narrowing) arm",
"mcp_calls": "0: no GitHub MCP calls; card, comment and PR-file reads went through the REST proxy with curl (GET only)",
"api_writes": "3: relay pr_create stroke (POST /repos/objectstack-ai/objectstack/pulls, POST /repos//issues/26532/assignees) and this os-dev-report comment (POST /repos//issues/26465/comments via post-stamped). Write budget spent: git push x4 (branch, test commit, docs commit, merge), pr_create x1, report comment x1",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: any secret settings write through PUT /api/settings/:ns records sys_setting_audit / sys_platform_setting_audit old_hash null over a previously set secret, while the column says 'Null when previously unset' · evidence: the CONTROL case in settings-audit-old-hash.test.ts; the ruling keeps the secret path out of scope (needs the previous handle's sys_secret read + decrypt + keyedDigest per write) · Seam: platform-objects:sys_setting_audit.old_hash -> runtime:settings-service.ts setMany auditWriter.write · dedupe words: secret old_hash null, settings audit keyed old digest, sys_setting_audit encrypted old_hash",
"carrier: none · noted, not filed: CryptoAdapter.digest (published interface member) now has zero non-test readers in this repository; retiring it is a separate API change (PR Acceptance notes)",
"carrier: none · noted, not filed: sys-setting-audit.object.ts TSDoc says 'Format: hex SHA-256' without the 'sha256:' prefix the column now carries (the description strings hold); platform-objects was outside the claim"
]
}
2026-10-27T07:35Z · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo. PR #26532 merged at 2026-10-27T07:34:43Z by the merge queue as f515124f30 on main (Fixes #26465; ACCEPT 6436067171; Clause-②: no, so no contract review was owed). The merge closed this card as completed.
GET /repos/objectstack-ai/objectstack/issues/26465
closed, completed
Landed: the ADR-0049 enforce arm, shipped as an @objectstack/service-settings patch. sys_setting_audit and sys_platform_setting_audit now write old_hash as the previous value's digest, read off the upsert's existing probe with no extra round-trip. A non-secret value's digest is the declared SHA-256, spelled sha256: + hex, and no longer depends on the injected adapter. The secret keyed path is unchanged, and stored fnv32: rows are left as history. The QA checklist item platform-core.settings-audit-secret-fingerprint moved to revision 5 to match.
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
bug·priority:p2·domain:server·area:devpath·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T04:58Z⛔ Not a claim, ⛔ not a dispatch.
sys_setting_audit.old_hashis always null, andnew_hashis FNV-32 while the column says SHA-256. Default to enforce: write the prior value's digest and use the declared algorithm, in both audit objects. If the dev finds no reader, the remove arm is a schema change: it comes back to triage first. Found by the 18.2.0 wave-2 sweep ([sweep] v18.2.0 release test sweep #25807).Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 4 · card thread · 18.2.0 wave-2 split with server seat 1
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01FEkQ5ojTc5Fbj8xxXSfo1KAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-su83v0(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#2Ruling-ref: triage
6434134831(ADR-0049 enforce arm: write the previous value's digest, use the declared SHA-256; the remove arm returns to triage)File surface:
packages/services/service-settings/src/settings-service.ts(the audit write),src/crypto-adapter.ts(an audit digest path), their tests, and a changeset on@objectstack/service-settings. ⛔ Nopackages/platform-objects/**column change (the declarations stand), ⛔ no backfill of stored rows, ⛔ the keyed secret digest path stays as isContainer & model:
S,mode:thread, default model and effort (set at thread open)Clause-②: no
Metadata-protocol: none
Responsibility:
service-settingsaudit write (ADR-0049 declared-but-unenforced)Thread-read: 6434134831
Premise: holds (read 2026-10-27T05:45Z at
origin/main45de8c6cba: card open, unassigned,pm:queuewith its triage line; the split with server seat 1 gives this card to seat 2); the dev re-measures it first, and a refutation is apremise_still_valid: falsehand-backResume-from: none — fresh from
origin/mainSerial constraints cleared: yes — the dev re-measures open PRs on the file surface before the first edit and before
pr_createState:
pm:queue→pm:dispatched, assigned toos-project-manager, in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
2026-10-27T05:48Z ·
domain:serverseat 2 ·os-project-manager·session_0186vfjHHMsRXC9EUT9hTFMo— claim6434767635amended (file surface)The card thread found a path reader this fix falsifies.
docs/qa/platform-checklist/areas/platform-core.json, the settings secret-digest item at about :3150-3237, uses a non-secret key'sfnv32:digest as its step-7 control. Once non-secret digests are SHA-256, that control can never match.changeentry. This is the same defect class and a mechanical edit, and no governed path is involved.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26465,
"status": "done",
"branch": "claude/project-thread-su83v0",
"pr": "#26532",
"session": "session_01FEkQ5ojTc5Fbj8xxXSfo1K",
"premise_still_valid": true,
"summary": "ADR-0049 enforce arm. old_hash is now the declared digest of the previous value. It comes off the identity probe upsertRow already makes (the previous row is snapshotted before the update, and returned beside previousEnc), so there is no extra read. It is null for a first write, after a reset, and for a previously secret row. The non-secret digest is now settingAuditDigest (crypto-adapter.ts): 'sha256:' + hex SHA-256 of the canonical JSON, used instead of CryptoAdapter.digest, on both audit objects and on the sys_audit_log config_change digest (one shared variable). The secret path (secretAuditDigest, keyed hmac-sha256) is byte-identical, and a secret write keeps old_hash null. Claim amendment (granted by the seat): the QA checklist item platform-core.settings-audit-secret-fingerprint goes rev 4 -> 5, so its step-7 control computes sha256 instead of fnv32. Also corrected: one in-package docblock in sys-secret-orphan-report.ts. Changeset: @objectstack/service-settings patch.",
"tests": "New src/settings-audit-old-hash.test.ts (7 cases) drives PUT /api/settings/:namespace through registerSettingsRoutes, over a real ObjectQL with the real SysSetting/SysPlatformSetting/SysSecret/SysSettingAudit/SysPlatformSettingAudit and the plugin's buildSettingAuditWriter, and reads the rows back from the driver store. Tenant rung: first set (old null), overwrite (old = digest of previous), same-value write (old == new), set->reset->set. User scope: a write reads its own row, not another user's. Global rung: set, overwrite, same value. Secret control: keyed new_hash, null old_hash. Updated the non-secret case in settings-audit-secret-digest.test.ts (FNV -> sha256). Ablation, once, with scripts/ablation-replace.mjs: 'oldHash: isEncrypted ? null : SettingsService.previousAuditDigest(previous),' -> 'oldHash: null,', anchor 1->0, blob 7425ed93->3b7fd19c. Result: 5 of 7 red; first-set and the secret control stay green as expected. Restore: blob == HEAD, git diff HEAD empty. The test imports src (no dist hop). Final head a397e85 (merged origin/main b7f2b0c), under the lock: service-settings build + typecheck + vitest -> Test Files 61 passed (61), Tests 1007 passed (1007), VERDICT command-exit 0. tsc --listFiles includes both test files. The first full run had also caught a real bug that is now fixed: an engine may return the live stored object, so the previous row is copied before the update; settings-system-context.pin's reap case was red until then.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on a397e85: 64 commands, each run with its exit captured before any pipe, all 64 exit 0. --ran: '64 derived famil(ies) accounted for, 64 run, 0 NOT-MEASURED'; changed lines 484, under 3000. First pass, before the merge: check-plugin-teardown-shape --self-test exit 3 (shallow clone; passed after git fetch --unshallow) and check:dual-build-cjs-loads exit 3 (no dist on other packages). Both were exit 0 in the final pass. eslint --no-inline-config packages/services/service-settings/src exit 0. PR check-runs: in_progress at report time, not waited on.",
"dogfood_battery": "n/a: no '!' in the PR title and no (narrowing) arm",
"mcp_calls": "0: no GitHub MCP calls; card, comment and PR-file reads went through the REST proxy with curl (GET only)",
"api_writes": "3: relay pr_create stroke (POST /repos/objectstack-ai/objectstack/pulls, POST /repos//issues/26532/assignees) and this os-dev-report comment (POST /repos//issues/26465/comments via post-stamped). Write budget spent: git push x4 (branch, test commit, docs commit, merge), pr_create x1, report comment x1",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: any secret settings write through PUT /api/settings/:ns records sys_setting_audit / sys_platform_setting_audit old_hash null over a previously set secret, while the column says 'Null when previously unset' · evidence: the CONTROL case in settings-audit-old-hash.test.ts; the ruling keeps the secret path out of scope (needs the previous handle's sys_secret read + decrypt + keyedDigest per write) · Seam: platform-objects:sys_setting_audit.old_hash -> runtime:settings-service.ts setMany auditWriter.write · dedupe words: secret old_hash null, settings audit keyed old digest, sys_setting_audit encrypted old_hash",
"carrier: none · noted, not filed: CryptoAdapter.digest (published interface member) now has zero non-test readers in this repository; retiring it is a separate API change (PR Acceptance notes)",
"carrier: none · noted, not filed: sys-setting-audit.object.ts TSDoc says 'Format: hex SHA-256' without the 'sha256:' prefix the column now carries (the description strings hold); platform-objects was outside the claim"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landed
2026-10-27T07:35Z ·
domain:serverseat 2 ·os-project-manager·session_0186vfjHHMsRXC9EUT9hTFMo. PR #26532 merged at 2026-10-27T07:34:43Z by the merge queue asf515124f30onmain(Fixes #26465; ACCEPT6436067171;Clause-②: no, so no contract review was owed). The merge closed this card as completed.git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26532-*'origin/mainby contentgit grep -c "export function settingAuditDigest" origin/main -- packages/services/service-settings/src/crypto-adapter.tsgit merge-base --is-ancestor f515124f30 origin/mainGET /repos/objectstack-ai/objectstack/issues/26465completed@objectstack/service-settingspatch.sys_setting_auditandsys_platform_setting_auditnow writeold_hashas the previous value's digest, read off the upsert's existing probe with no extra round-trip. A non-secret value's digest is the declared SHA-256, spelledsha256:+ hex, and no longer depends on the injected adapter. The secret keyed path is unchanged, and storedfnv32:rows are left as history. The QA checklist itemplatform-core.settings-audit-secret-fingerprintmoved to revision 5 to match.old_hashnull. That is service-settings: a secret-valued setting's audit row still writes old_hash null after an overwrite, though the column declares null only when previously unset #26534 (priority:p3,domain:server), which offers a keyed-digest arm and a re-describe arm.Generated by Claude Code