Skip to content

A connector auth key drained on save (#28640 ②B) still sits in cleartext at rest in sys_metadata_history snapshots #29484

Closed

Description

@objectstack-fleet

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Ruled: 6677709817 · letter d (c, then b) · 2026-11-06T05:28Z
Ruling-ref: 6677709817

Filing gate: ① an out-of-scope finding (class b, carrier: the seat) the #28640 dev noted on PR #29456 (os-dev-report 6581427705) and the seat's contract review confirmed (record 6581889597). ⛔ Not a claim. No grade: triage sets it.

What was measured

PR #29456 (ruling ②B, 6566912252) makes a save of a connector's served form drop a leftover undeclared auth key (for example a legacy auth.token) from the live sys_metadata row, and tells the author with a withheld-key-dropped advisory. The sys_metadata_history snapshots written before that save keep the pre-drain body, value included.

A read of a history row is served through the stored-row seam (redactStoredMetadataRow(s) in packages/spec/src/kernel/metadata-type-redaction.ts), which withholds the key, so there is no cleartext exit through the API. The value is cleartext only at rest, in history rows, for as long as history is retained.

Wanted

The maintainer's call on whether at-rest history must be scrubbed too: (a) leave it, documented (history is served redacted; retention bounds it); (b) a one-time scrub of undeclared auth keys from connector history snapshots, as a migration step; or (c) withhold at write time for future history rows only. Any option that rewrites stored history is a data change and goes to the maintainer first.

Reader who acts: domain:server (metadata history writer) with the domain:spec redaction owner, after triage.

Related: #28640 / PR #29456, ruling 6566912252.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:server · needs-user-decision · 2026-11-02T17:03Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 2, 2026
  4. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3

    Brief-claimed-by: session_01L9qibxdiybU8ehPKBroYE3 · batch #437 · 2026-11-06T04:58Z

    Director seat (the maintainer's decisions thread). Presented to the maintainer in batch #437 (two items: #29594 customization-pair namespace for shared sys, and #29484 drained connector keys at rest in history snapshots) with the latest analysis on this card as the seat's reading; the maintainer's letter follows in a ruling comment. Rule level only on the security item.

  5. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3

    Ruling: batch #437 item 2 · letter d (c, then b) · maintainer 「29484 同意」 2026-11-06T05:26Z

    Director seat (the maintainer's decisions thread). Presented in batch #437 (brief-claimed here at 6677323648; detailed letter in the project's decision doc, batch 6, rule level). The maintainer answered at 2026-11-06T05:26Z, verbatim: 「29484 同意」 = the seat's recommendation, d = (c) first, then (b).

    What the ruling binds (rule level):

    Workload: domain:server (history writer) with the domain:spec redaction owner · 1–2 PRs · S–M · contract review on (c) · the scrub rehearsed on a copy before it runs anywhere.

    Execution: the server lane dispatches (c) with Ruling-ref: naming this comment, the count + scrub script following; ⛔ no stored-secret specifics on this card. Label strokes are the dispatching seat's.

  6. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 7, 2026
  7. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    State: ruled, not dispatched → pm:queue · 2026-11-07T14:52Z

    ⛔ Not a claim.

    • Ruling, re-read now: letter d, meaning (c) first and then (b); maintainer 「29484 同意」, 2026-11-06T05:26Z (6677709817, director#2). Nothing before (c) waits on the maintainer, so needs-user-decision comes off, on the maintainer's 「可以」 (2026-11-07). The (b) scrub keeps its own maintainer step, as the ruling says: a read-only count is confirmed before any rewrite.
    • Half-state fixed: the ruling left the label strokes to the dispatching seat, and none were made. No PR names this card, so it had no pm:* state and no seat could see it. It is now pm:queue for domain:server, starting with step (c) under Ruling-ref: 6677709817.

    Generated by Claude Code

  8. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

    Claim: PM loop round 1 · 2026-11-07T17:16Z
    Session: session_01FEkQ5nFAEGcShB48YZExSP (card thread, dev) · seat session_01FEkQ5o8PE7ZtVjcor51wBZ
    Account: os-project-manager
    Branch: claude/project-thread-aru8xa
    Worktree: objectstack-issue-29484
    Domain: domain:server
    Seat: domain:server#5
    File surface: packages/metadata/** (the sys_metadata_history writer) + tests, root .changeset/; the shared drain predicate's current home only if it must move; ⛔ packages/spec/**
    Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: thread default (order asked xhigh; the coordinator sets effort only on the maintainer's word)
    Clause-②: no (narrowing) — Ruling-ref 6677709817; CONTRACT_REVIEW_TIER review owed before the queue
    Metadata-protocol: none
    Responsibility: packages/metadata history writer (objectstack) | the read exit redactStoredMetadataRow(s) already withholds it, at rest only | reached by every save of a connector carrying an undeclared auth key
    Thread-read: 6708495892
    Premise: holds: ruled d = (c) then (b) (6677709817, re-read by triage 6708495892); this claim covers (c) only
    Serial constraints cleared: none — no pm:dispatched card claims the history writer

    Scope: Step (c) only, PR says Refs #29484; (b) count + scrub follows under its own maintainer step. Ruling-ref: 6677709817 Order: /mnt/project-files/pm-orders/server/order-29484.md. Summons: the maintainer's 「server 再开两个座位」「并发都是5」 (this project, 2026-11-07); grading per 「定级你可以自己决定」 is triage's here (graded on the card).

  9. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 7, 2026
  10. 6 remaining items

  11. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 8, 2026
  12. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

    Claim: PM loop round 1 · #29484 · 2026-11-08T18:54Z · step (b) first half only (p2; re-queued by triage 6725104872)
    Session: session_01ADHkmPF1skfu2kx89BEXZi
    Account: os-steve
    Branch: claude/project-thread-l65x8h (the card thread's harness-pinned branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-29484
    Domain: domain:server
    Seat: domain:server#1
    File surface: packages/metadata/src/ and packages/metadata-protocol/src/ (a read-only count over history snapshots, reusing the step (c) predicate), the nearest existing one-time data command if one fits, their tests, a changeset (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier, effort: high
    Clause-②: no
    Metadata-protocol: none
    Ruling-ref: 6677709817
    Responsibility: n/a — step (b) of a ruled card; step (c) landed as PR #31242
    Thread-read: 6725104872
    Premise: holds (read at 99364ea7: step (c)'s write-time withholding is on main (PR #31242); no count or scrub over existing history exists)
    Serial constraints cleared: no open PR touches history-snapshot-body.ts. ⛔ Read-only: no rewrite, no run against any live or shared database. Per #29111, no specifics of any count go on this card or the PR; the second half (the rewrite) waits on the maintainer's confirmation and is not this claim. The PR says Refs #29484.
    Order: standard order + server-seat1/29484-delta.md (project files).

  13. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmS3xwR94UULpFfP1br
    os-dev-report

    {
      "issue": 29484,
      "status": "done",
      "branch": "claude/project-thread-l65x8h",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31655",
      "session": "session_01ADHkmS3xwR94UULpFfP1br",
      "premise_still_valid": true,
      "summary": "Step (b) first half of ruling 6677709817 only (PR says Refs #29484): scrubHistoryDrainedKeys(driver, { mode: 'count' }) in packages/metadata/src/migrations/history-drained-keys.ts, exported from @objectstack/metadata/migrations. It pages sys_metadata_history by primary key through a driver's find only and counts a row exactly when historySnapshotBody (PR #31242's helper, imported) would store a different body today; it reports affected rows, distinct (type, name) items, opaque row handles, and lists unreadable / unscrubbable rows beside the count. Only the count mode is wired; any other mode throws before a read. Step 0: the nearest one-time data tool convention for history is @objectstack/metadata/migrations (driver-level one-off migrations, same package as the predicate) beside metadata-protocol's recorded-by plan and its os migrate recorded-by door; no os migrate subcommand is added here (open question 1). The copy: a seeded driver-sqlite-wasm FILE, copied, with only the copy opened for the count.",
      "tests": "Head 7b4b2203. New packages/metadata/src/migrations/history-drained-keys.test.ts: 9 passed (seeded rows (i) counted, (ii) declared-only connector, (iii) non-connector, (iv) row written through the current DatabaseLoader writer not counted; delete row scanned not counted; non-JSON body listed; rows byte-identical after the run; result carries no fixture value or key name; non-count mode refused with the reader never called). pnpm --filter @objectstack/metadata test: Test Files 71 passed (71), Tests 981 passed (981); typecheck exit 0, the new files in its program (tsc --listFiles). Ablation A via scripts/ablation-replace.mjs on the predicate (... !== body -> ... !== body || true): anchor 1 -> 0, blob 058420598100 -> 8b8768ee5c6a, 4 failed | 5 passed (9), first red: expected [ …(5) ] to deeply equal [ 'h_seed_01', 'h_seed_02' ]; (ii)/(iii)/(iv) red as not-include; restored blob == HEAD, git diff HEAD empty. Ablation B on the mode guard: blob 058420598100 -> 8b4043f100a5, 1 failed | 8 passed (9): promise resolved instead of rejecting; restored blob == HEAD, git diff HEAD empty. The suite imports src, so no rebuild leg.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 7b4b2203: 60 families, all exit 0; --ran: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN. First pass had three exit 3 on prerequisites, re-run green: check-plugin-teardown-shape --self-test (fixture commit 621a4876 fetched), check:dual-build-cjs-loads and check:lean-entry-closure (after turbo run build --filter='./packages/**', 81/81). eslint --no-inline-config packages/metadata: exit 0. CI 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": "4 — mcp__github__issue_read x3 (card get, get_comments x2), mcp__github__pull_request_read x1 (PR 31655 get, draft + body read-back); all reads, no write tool.",
      "api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 42515989605, PR #31655, 6814 bytes sent == stored, no labels: the diff publishes, so a patch changeset and no skip-changeset; no assignees); post-stamped.mjs comment on #29484 (this report). Plus git push x2 (not REST).",
      "files_changed": [
        ".changeset/29484-history-drained-keys-count.md",
        "packages/metadata/src/migrations/history-drained-keys.test.ts",
        "packages/metadata/src/migrations/history-drained-keys.ts",
        "packages/metadata/src/migrations/index.ts"
      ],
      "line_budget": "+379 / -0 over 4 files (effort M); under the 3000-line human-merge threshold",
      "deviations": [
        "Card delta named the count's subject as undeclared auth keys on connector snapshots; the imported predicate is step (c)'s whole not-carried-forward set, so the count covers every position step (c) withholds (open question 2).",
        "No os migrate subcommand: the tool ships as a library function only (open question 1)."
      ],
      "open_questions": [
        {
          "question": "Wire an operator door for the count before the second half? Today the count runs only by calling the exported function with a driver opened on a restored copy.",
          "options": [
            "A: a small follow-up PR adding a count-only os migrate subcommand (read-only boot like os migrate recorded-by), registered in the CLI's migrate pins (json-stdout purity, exit-code, confirmation-exit); the second half adds --apply through the ADR-0119 D2 journal",
            "B: no door until the second half; whoever holds a deployment's database runs the function by hand on a restored copy"
          ],
          "recommendation": "A, because the maintainer must confirm the count BEFORE the rewrite exists, so the count needs an operator door now, and the CLI pins are paid once for the command rather than twice"
        },
        {
          "question": "The count's predicate is step (c)'s whole not-carried-forward set (the same historySnapshotBody step (c) stores through), wider than the auth-only wording of the ruling. Keep it?",
          "options": [
            "A: keep the shared predicate, so count, write-time withholding and the future rewrite cannot disagree",
            "B: an auth-only predicate for the scrub, which needs a second list beside the redactor's"
          ],
          "recommendation": "A, because the delta asks for the same predicate imported, not copied, and step (c) already withholds the whole set"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Stamped 2026-11-08T19:39Z.

  14. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

    Release: session_01ADHkmPF1skfu2kx89BEXZi · step (b) first half landed (PR #31655 merged 6c6cfcc1, 2026-11-08T20:30Z); what remains needs the maintainer's act, so the card leaves the active queue: pm:dispatched → needs-user-decision
    Ruling-ref: 6677709817

    待裁:已存旧历史的扫除,由谁在真实数据上跑计数

    一句话问题: 连接器在保存时被清掉的未声明 auth 键,仍以明文留在过去的历史快照里。裁决 6677709817(字母 d)要求先报一个只读计数给维护者确认,再改写。计数工具已经上线,但任何席位都碰不到真实部署的数据库,所以计数还没人跑过。

    Governing text: 裁决 6677709817 原文:"(b) second: a one-time scrub … runs in two steps: a read-only count first, reported to the maintainer through the advisory channel (#29111), and the rewrite only after his confirmation of that count … the scrub rehearsed on a copy before it runs anywhere"。#29111:安全发现不上公开卡。

    协议声明: 不改协议。Clause-②: no,Metadata-protocol: none。

    前提(附 re-check):

    • (c) 已在 main,新写入的历史快照不再带这些键。re-check:git log origin/main --oneline --grep "#31242" 应有 1 条。
    • 计数工具已在 main:scrubHistoryDrainedKeys(driver, { mode: 'count' }),导出于 @objectstack/metadata/migrations,只读,不是 count 的模式在读之前就拒绝。re-check:git grep -n "export async function scrubHistoryDrainedKeys" origin/main -- packages/metadata/src/migrations/ 应有 1 处。
    • 计数已在种子副本上演练(driver-sqlite-wasm 文件副本,PR feat(metadata): read-only count of the history snapshots the drained-key scrub would rewrite #31655 的测试):该计的计了,三个对照行都没计,跑完每一行逐字节不变。
    • 没有任何席位或卡线程接触过真实部署数据,所以目前不存在任何真实计数。

    选项:

    选项 做什么 客户可感知后果 工作量
    A 维护者(托管环境则由 cloud 运维)把一份备份还原成副本,在副本上调用上面的计数,经 advisory 通道私下交回 affectedRows / distinctItems;确认后,席位派后半段(改写模式 + CLI 入口,在副本上演练后再跑) 无;旧明文在确认前继续存在于历史表(API 读出口本来就脱敏) 维护者一次副本操作;后半段 server 车道 1 个 PR,先例 #31655(+379/−0)
    B 不等真实计数,凭种子副本演练直接放行改写 少一轮等待;但改写跑在一个没人看过规模的数据集上 后半段 1 个 PR;偏离裁决 d 的两步保险
    C 停在 (c),旧历史不扫 旧明文一直留到历史被保留期清掉 0;等于翻回已否决的 (a)

    业务含义: A 是"先盘点仓库,再按清单销毁";B 是"没盘点就销毁";C 是"只保证以后不进货"。

    四轴(业务立场): ① 长远看,"静止数据也在安全边界内"是平台该有的样子(主流 SaaS 的密钥轮换都包含历史与备份),A、B 都走向它,C 不走。② 拉动:今天没有客户撞上(读出口已脱敏),风险在数据库被直接读取的场景。③ 防 AI 犯错:A 让人在改写之前看到规模,出错时是响亮的数字不符;B 出错时是静默改错历史。④ 不扩散:A 不新增任何键,后半段只加一个一次性工具。

    推荐 A。 只看①选 A;②③④ 是否翻转:否。置信缺口:托管环境的副本由谁出(维护者还是 cloud 运维),席位没测,由维护者指定。

    裁后执行: 选 A 后,维护者(或其指定的运维)在副本上跑:

    import { scrubHistoryDrainedKeys } from '@objectstack/metadata/migrations';
    await scrubHistoryDrainedKeys(driver, { mode: 'count' });
    

    数字只走 advisory 通道(#29111),⛔ 不上本卡。维护者在本卡写 Ruled: 确认"已看过计数"之后,server 车道派后半段:改写模式(复用同一个谓词)、os migrate 入口、副本演练、上线运行。

    os-decision-facets
    ① 项目长远合理性:A 收窄特例。静止的历史与在线行同守一条规则,不另立契约。
    ② 实际业务拉动:今日无客户报障。风险在数据库被直接读取的场景,例如备份外泄。
    ③ 防 AI 犯错:A 在改写前给出可核对的数字,出错是响亮的不符;B 出错是静默改坏历史。
    ④ 创业阶段不扩散:A 不加任何声明键,后半段只是一个一次性工具。
    Prior rulings read: 29484, 28640 ②B, history scrub, 29111 → 3 hits (6677709817, 6566912252, #29111); ADR none; thread: none

    相关:#28640 / PR #29456(②B 保存时清键)· PR #31242((c) 写入时扣留)· PR #31655((b) 前半,计数工具)· #29111。

  15. 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 Nov 8, 2026
  16. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Brief-claimed-by: session_01AavokzJ5DndAwitDXvKy4U · batch #1 · 2026-11-12T01:58Z

    Presented to the maintainer in the triage seat's session, in the standing decision-batch format (decision-analysis.md): batch #1 = #32481, #29484, #31874, cloud#4315 and #32163. ⛔ Not a claim on the work.


    Generated by Claude Code

  17. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Ruling: batch #1 item 3 · letter C (as ruled: stop at (c)) · maintainer 「第1第2项同意,第 3 项 · #29484(安全):旧的历史快照里还留着明文密钥,扫除之前要先在真实数据上计数, 云中都是测试数据,不考虑历史数据。第4 第5项用 fable 重新评估和我讨论。」 2026-11-12T02:07Z

    Ruled in the triage seat's session on 2026-11-12, on decision batch #1 as presented (decision-analysis.md format; brief-claim on this card).


    Generated by Claude Code

  18. removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions