Skip to content

security(datasource): credential-shaped values in some datasource configurations are stored and served without the platform's secret handling — detail withheld pending maintainer #21840

Description

@objectstack-fleet

Found while verifying a candidate from the 17.7 pre-release console run #21784 (integration-system area, outside the item's clauses), by an independent verifier (RUNNER rule 7) on current main.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: an API a customer can call — external data; secrets can be neither written in nor read out | integration-system.datasource-credential-refusal-matrix | P2

    Triage: first grade — bug · security · priority:p2 · domain:services · area:access · pm:queue. Credential-shaped datasource values follow the platform's secret handling on write and on every read

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T07:54Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only. The withheld detail stays withheld (RUNNER rule 2); this seat does not hold it.

    Triage: lands in the datasource configuration's write and read doors (service-datasource, by class) ⇒ domain:services; rationale: other configurations already refuse or store such values as secrets, and these do not.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (17.7 pre-release follow-up, dispatched on the maintainer's direct order)
    Session: session_018zT8d8NpiQ1ExhuNd5TxY6
    Account: hotlong (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-21840-datasource-secret-keys
    Worktree: objectstack-issue-21840
    Domain: domain:services (card label, as triage set it)
    File surface: packages/spec/src/data, packages/services/service-datasource/src, packages/qa/dogfood/test, .changeset/
    Container & model: M, mode:subagent, model: opus (default tier; no path-derived mandate)
    Clause-②: no
    Thread-read: 5990389094
    Serial constraints cleared: none named

    Provenance: the maintainer, in Claude Code session session_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-05, verbatim: 「都开单派发」; landing per the standing order 「开发完整就进队列合并」. Dispatched one at a time within the session's load cap. Where the card withholds detail, this session holds it and the dispatch carries it privately.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21840,
      "status": "done",
      "branch": "claude/issue-21840-datasource-secret-keys",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21877",
      "session": "session_018zT8d8NpiQ1ExhuNd5TxY6",
      "premise_still_valid": true,
      "summary": "Premise confirmed on origin/main 9f9510f25e: for a datasource whose driver ships no config contract, the write door judged nothing and the read redactor withheld only the canonical credential spellings and URL credentials. Fix, class-level: one name predicate in packages/spec/src/data/driver/common.zod.ts judges credential-shaped config keys (and connection-string credential segments) for contractless drivers only; DatasourceSchema refuses that material at publish with the bound-secret remedy, and the single existing redactor redactDatasourceConfig withholds it, so every read door that already routes through it (meta, admin, data door, history, audit and activity copies) is covered with no second helper. Option chosen for (b): refuse at write, not route to the secret binder, per ADR-0015 section 10 and ADR-0062 D3: nothing says which key a contractless factory reads, so a silent move into the single bound secret would break the connection quietly (the reasoning the credential-migration planner already encodes); known-driver behaviour unchanged at both doors. Environment placeholders, empty strings, non-string values and array data stay accepted, which keeps the existing contractless-placeholder boundary pins green. The credential-migration planner in service-datasource now reports such keys as residue instead of nothing-to-migrate. Draft PR open, assignee mirrored from the card; no labels written (the dispatch named none and skip-changeset does not apply). File surface beyond the claim: one new ADR anchor, scripts/adr-anchors/packages__spec__src__data__datasource.zod.ts.json (AGENTS.md directive 13), plus the regenerated packages/spec api-surface and export-origins shards.",
      "tests": "HEAD 6a4fb54fca. spec: new src/data/datasource-contractless-credentials.test.ts 164 passed; spec src in three batches 121 files 4084 tests, 257 files 6630 tests, 188 files 6226 tests, all passed. service-datasource: new __tests__/datasource-contractless-credentials.test.ts; whole package 38 files 730 tests passed. dogfood: new test/datasource-contractless-credentials.dogfood.test.ts on the showcase boot with admin routes and AuditPlugin mounted as os serve mounts them, cold boot on one db file: write doors refuse (422 INVALID_METADATA naming six positions; admin create 400), clean body saves, ten read doors serve a seeded pre-refusal row with a positive-control marker and no credential, audit copy and its data door clean: 4 of 4 passed. Consumer suites: metadata-protocol redaction 4 files 76 tests, plugin-audit stored-metadata 2 files 24 tests passed. Typecheck spec, service-datasource, dogfood green. Ablation (fix committed first): ablation-replace on the read-side contractless switch, anchor 1 to 0, blob 61116efee71e to a17509115dbd, spec rebuilt, dist preflight marker in 8 built files; dogfood went 2 failed 2 passed (8 read doors and the audit copy red, write test green as expected); restore blob 61116efee71e, git diff HEAD empty, rebuilt, preflight --absent over 228 files, tree clean. Gates: dispatch-gates --ran 98 derived, 97 run, 1 NOT MEASURED; check:generated green after regenerating api-surface and export-origins (four new exports); check:type-check-debt re-measure green; check-adr-0087-registration green (not-required no-migration-prescription). Lint narrowed: eslint --no-inline-config --format json over the 7 changed source files, 7 files 0 errors 0 warnings, each in the config population via --print-config, no type-aware linting in eslint.config.mjs so untouched files cannot move. NOT MEASURED: check:dual-build-cjs-loads, reason PREREQUISITE NOT MET (eight unrelated packages unbuilt in this worktree). NOT MEASURED: packages/spec/scripts tests (55 files, no diff path), reason exceeded the foreground window behind the shared lock. Both declared to CI.",
      "mcp_calls": "0",
      "api_writes": "3 — fleet relay repository_dispatch POSTs, each executed as objectstack-fleet[bot]: pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft), label-write assign (POST /repos/objectstack-ai/objectstack/issues/21877/assignees hotlong), this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/21840/comments). git push is not REST.",
      "open_questions": [
        {
          "question": "Clause-② line: the claim carries Clause-② no, while the diff adds four public spec exports and narrows the DatasourceSchema accept set for contractless drivers. The changeset and the PR body carry Clause-② yes (narrowing), with spec minor and a not-required ADR-0087 disposition.",
          "options": [
            "A keep yes (narrowing) as written",
            "B rewrite to match the claim"
          ],
          "recommendation": "A, because the dispatch asked for an accurate line and both facts (new exports, narrowed accept set) are true; the changeset gates read it green."
        },
        {
          "question": "A contractless plugin driver that needs more than one secret has no remedy for the second after this refusal: the datasource secret binder fills exactly one slot.",
          "options": [
            "A accept as the existing one-slot limit and record it",
            "B open a decision on a multi-slot binder contract"
          ],
          "recommendation": "A for this card (no driver in this repository is contractless, and the limit predates the change); B only if a real multi-secret plugin driver is named."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted, not filed: the verify harness mounts the datasource admin service but neither its REST routes nor an audit writer, so the dogfood file mounts both itself the way os serve does; observation only, recorded in the dogfood file header.",
        "carrier: none (承接者:无) · noted, not filed: on a legacy contractless row that still holds a credential, an admin edit that changes config carries the withheld value forward and is then refused; sending that key as an empty string clears it; recorded in the PR Acceptance notes."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Correction to the claim above: Clause-②: yes (narrowing), not no.

    PR #21877 does two things to a published surface. It adds four exports to @objectstack/spec (the credential-key predicate and the connection-string helpers), which widens the surface. It also narrows DatasourceSchema's accept set: for a driver that ships no config contract, credential material in config that used to publish is now refused. That narrowing is BREAKING. The changeset grades spec minor, carries the BREAKING banner and the ADR-0087 not-required (no-migration-prescription) marker, and the PR is titled fix(spec)!:. A contract-tier review is owed on the PR head. No other part of the claim changes.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21840,
      "status": "done",
      "branch": "claude/issue-21840-datasource-secret-keys",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21877",
      "session": "session_018zT8d8NpiQ1ExhuNd5TxY6",
      "premise_still_valid": true,
      "summary": "Round 2 on the security review, head 97f650b73c (from 6a4fb54fca). The contractless judgment moved into one walk in a new module, packages/spec/src/data/driver/contractless-credentials.ts (findContractlessCredentials over a word-based isCredentialShapedConfigKey), and both doors read it: DatasourceSchema refuses every finding, redactDatasourceConfig withholds every finding, so the doors are symmetric by construction (pinned: refused set equals redacted set, and the served shape is accepted). All seven review items addressed, contractless drivers only: (1) arrays are walked and their object elements judged by key, plus name/value header pairs; plain row data stays accepted; (2) whole-key word judgment with the added stems, plural and value/pem/json qualifiers, service-account key material, descriptor and flag/measure exclusions; (3) URL property tails, libpq keyword/value strings and scheme-less userinfo; (4) only the upper-case env-name placeholder grammar is exempt; (5) numbers and arrays under credential keys are refused, booleans are flags, a subtree past the depth cap is refused; (6) descriptor and identity leaves inside a credential-shaped object are not refused; (7) after a dropped segment, following segments go with it until a known connection-string keyword. restoreRedactedConfig in service-datasource now restores array positions by index; the migration planner reads the same walk. Changeset rule text rewritten to match the code. PR body not edited. The old predicate block in common.zod.ts is gone (that file is back to its main-branch content).",
      "tests": "HEAD 97f650b73c. spec: src in two batches, 283 files 8751 passed + 1 todo, and 283 files 8235 passed; the rewritten contractless test file covers every item in both directions. service-datasource: 38 files 733 passed (adds array restore and author-changed / author-removed controls). dogfood datasource-contractless-credentials: 4 of 4 passed (fixture extended with an array-element password and an Authorization header pair; write door names 8 positions; 10 read doors and the audit copy clean, positive controls present). metadata-protocol redaction 4 files 76 passed; plugin-audit stored-metadata 2 files 24 passed. Typecheck spec, service-datasource, dogfood green. check:generated green after regenerating api-surface and export-origins (8 new export entries). check:type-check-debt re-measure green. dispatch-gates --ran: 98 derived, 97 run, 1 NOT MEASURED. Narrowed lint: eslint --no-inline-config --format json over the 9 changed source files, 0 errors 0 warnings. Ablation (committed first, ablation-replace on the array-element walk): the first two attempts were no-ops caught by ablation-dist-preflight (the bundler dropped both side-effect-free marker spellings; nothing ran on them); the third, a call-shaped marker, landed (blob ef73faca1ac9 to bc838a1a501b, marker in 20 built files) and the dogfood file went 3 red of 4 (write positions missing the array ones; read doors leaking the array-element secret; audit). Restore: blob back to ef73faca1ac9, git diff HEAD empty, rebuilt, preflight --absent over 228 files, tree clean, dogfood 4 of 4 again. NOT MEASURED: check:dual-build-cjs-loads, reason PREREQUISITE NOT MET (unrelated packages unbuilt). NOT MEASURED: packages/spec/scripts tests, reason exceeded the foreground window behind the shared lock. Both declared to CI.",
      "mcp_calls": "0",
      "api_writes": "1 — fleet relay repository_dispatch, executed as objectstack-fleet[bot]: this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/21840/comments). git push is not REST.",
      "open_questions": [
        {
          "question": "The metadata save door's carry-forward restores a withheld value inside an array only by element id. A legacy contractless row that still stores a credential inside an array element without an id loses it on an untouched metadata-door save of the served body. The admin edit door restores by index and is not affected. Such a value is now refused at write anyway.",
          "options": ["A accept: the credential has to be rebound through the secret anyway; state it in the changeset", "B follow-up card: let the datasource carry-forward restore by index, in metadata-protocol"],
          "recommendation": "B as its own card, because the file is outside this card's surface and the change touches a shared carry-forward path; A meanwhile."
        },
        {
          "question": "Placeholder grammar: the repo has no runtime environment-placeholder grammar (the spec's refusal treats any dollar-brace span as a placeholder). The exemption uses the review's upper-case env-name grammar.",
          "options": ["A keep the upper-case grammar", "B widen to mixed case"],
          "recommendation": "A, because every placeholder fixture in the repo is upper case and the review asked for that grammar."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted, not filed: the metadata-door carry-forward point in open_questions[0]; it belongs with whoever next touches packages/metadata-protocol/src/metadata-redaction.ts."
      ]
    }
    

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 21840,
    "status": "done",
    "branch": "claude/issue-21840-datasource-secret-keys",
    "pr": "#21877",
    "session": "session_018zT8d8NpiQ1ExhuNd5TxY6",
    "premise_still_valid": true,
    "summary": "Round 2 resumed after a container restart; head is 5c95e43, one commit on top of 97f650b. I checked all seven review asks against the code. 2, 4, 5 and 6 were already done. 1, 3 and 7 had gaps: some credential shapes inside strings were detected but not fully redacted, and a list of [name, value] header tuples was not walked. The new commit closes those gaps in the one shared walk, so the write door and the read door change together. It also stops one-word keys matching on a substring, stops a stem that already ends in s from taking a plural s, and stops the read door shifting array siblings when it drops an element. The changeset's rule text is rewritten to match the code clause by clause. Nothing was judged infeasible. The PR body was not edited and is now stale; see open_questions.",
    "tests": "All runs are against the tree that was pushed as 5c95e43. Spec suite in two batches: 178 files / 6495 tests passed, then 616 files / 18656 tests passed (1 todo each); together that is all 794 spec test files. service-datasource: 38 files / 734 tests passed, typecheck green. metadata-protocol: 214 files passed, 3 skipped / 27745 tests passed, 19 skipped. plugin-audit: 39 files / 630 tests passed. The dogfood contractless file: 4 of 4 passed (built closure, dist-resolved). Spec typecheck green (test layer within its debt ledger). check:generated: all 15 artifacts up to date, no api-surface change. Ablations ran on the committed tree through scripts/ablation-replace.mjs, which verified each anchor hit 1 to 0 on disk. (1) Removing the array-element recursion turned 5 tests red: the write-door array, inverted-pin and tuple cases, and two read-door cases. (2) Removing bearer from the last-word stems turned 2 tests red: the predicate case and the write-door case. Each restore was proven: blob equals the HEAD blob 2bb9373623e1 and git diff HEAD is empty. dispatch-gates --ran: 98 derived, 97 run with exit 0, 1 NOT MEASURED. The NOT MEASURED one is check:dual-build-cjs-loads, exit 3: it needs a full repo build. check:type-check-debt timed out on the first pass and passed on a second run with a longer budget. The tree was 13 commits behind origin/main when pushed; main was not merged in.",
    "mcp_calls": "0",
    "api_writes": "2 — git push of the branch (not REST); POST issues/21840/comments for this report via scripts/pm/post-stamped.mjs",
    "open_questions": [
    {
    "question": "The PR body is from round 1. It still says non-string values and array data are accepted, and it describes the read door as a separate predicate. That is now wrong. I was told not to edit the body, so it needs the seat's rewrite.",
    "options": ["A seat replaces the Write door / Read door bullets with the changeset's refused-and-accepted lists and updates the test counts", "B leave the body as is and let the changeset be the description of record"],
    "recommendation": "A, because reviewers read the PR body, and right now it contradicts the code and the changeset."
    },
    {
    "question": "The placeholder exemption only accepts upper-case environment names. Directive 9 env vars are upper-case. The repo's script-side env regexes also accept lower-case names. Which should the exemption accept?",
    "options": ["A keep upper-case only (current; lower-case placeholders are judged as written)", "B accept the POSIX name grammar with lower-case letters"],
    "recommendation": "A, because a value meant as a placeholder has the narrowest room to hide a literal secret, and the platform's own env names are upper-case."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — the scheme-less userinfo shape can catch a user:x@host-shaped value that is not a credential (for example a mailto-style string). No real config was measured carrying one. Lookup words: scheme-less userinfo, false positive, contractless",
    "carrier: none · noted, not filed — a multi-word key whose first word is a plural credential stem but which names a service (secretsManager holding a provider name) is judged credential-shaped. Over-refusal, not a leak. Lookup words: secretsManager, credential-shaped, descriptor",
    "carrier: none · noted, not filed — the shared RFC 3986 userinfo parse in driver/common.zod.ts ends the authority at the first slash, so an unencoded slash in a URL password is not read as userinfo. WHATWG clients parse it the same way, so it is not a working credential. Lookup words: urlUserinfo, slash in password"
    ]
    }

  7. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21840,
      "status": "done",
      "branch": "claude/issue-21840-datasource-secret-keys",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21877",
      "session": "session_018zT8d8NpiQ1ExhuNd5TxY6",
      "premise_still_valid": true,
      "summary": "Round 3, head 2320178c9d (merge of origin/main 25eb7de8ad as d03009726a, then one fix commit; check:generated was clean after the merge, and api-surface/export-origins were regenerated for the two new exports). All ten review items fixed at fix level. (1) The key split is linear (a lookahead replaces the capturing run), and every judged name (object key, segment key, query/form/fragment parameter, header name) is capped at 256 chars after NFKC; an over-long one is judged credential-shaped. The other new regexes were audited: each is anchored or matches a fixed width, and the libpq parse no longer re-slices the string per pair. (2) restoreRedactedConfig resolves every array hop by identity: the same index if the array is unchanged, otherwise a unique match on the served projection. A value whose element changed, is gone or is ambiguous is dropped, and an array-element leaf is carried only into an untouched array. (3) The libpq scan is lenient, one pass, with quoted values; an unclosed quote runs to the end. (4) Name: value header lines, raw-headers lists, a tuple under a header-ish key, tuples longer than 2, every pair label. (5) ssl/tls key qualifiers, privkey, the data/content/hex/string/str/raw/hash qualifiers, basicauth/bearerauth/digestauth; NFKC first, and any non-ASCII character after it is judged conservatively. (6) Token-shaped URL usernames, sig and X-Amz-Signature, JSON-encoded strings (parsed and walked, depth-bounded), form-encoded strings, fragment pairs; redaction re-checks its own output and serves an empty string when the rewrite cannot clear one. (7) A descriptor key exempts leaves only. (8) The /meta carry-forward follows id-less elements and nested arrays by the same rule. The comment that said the datasource redactor never crosses an array is corrected. The flow pin that expected an id-less start node to be dropped on an UNTOUCHED save was inverted on purpose: untouched now carries the value, and an edited node is dropped. (9) Bytes are one value, typed-array indices are never touched, and a Map/Set gives a new finding kind, opaque, refused and withheld whole. (10) A bare key/keys counts as credential material only inside a credential-shaped or header-ish holder, or when its value looks like a secret (looksLikeSecretValue: 16+ chars, no whitespace, mixing upper, lower and digit; or 32+ token-alphabet chars with a digit). Elsewhere, key keeps its full judgment. The changeset rule text is rewritten to match, and @objectstack/metadata-protocol patch is added. The PR body is NOT edited and is now stale in one bullet; see open_questions.",
      "tests": "All at HEAD 2320178c9d unless noted. spec, batched under the verify lock: local project src/data+kernel+shared+system 252 files / 8037 passed + 1 todo; local project, the rest, 619 files / 18799 passed + 1 todo; repo project 53 files / 902 passed. The contractless file alone: 349 passed. service-datasource: 40 files / 756 passed. metadata-protocol: 217 files passed, 3 skipped / 27925 passed, 19 skipped (new file metadata-redaction-array-carry-forward.test.ts: 7 tests). plugin-audit: 39 files / 630 passed. dogfood: 204 files passed, 1 skipped / 1578 passed, 9 skipped. The suites ran on the parent commit 462f4041be; the amend only changed one test line, an optional chain, and that file and the contractless spec file were re-run green at 2320178c9d. typecheck: spec, service-datasource, metadata-protocol, plugin-audit and dogfood all exit 0 (one TS2532 in the new service test was fixed by that amend). check:generated: All 15 up to date. Ablations, each landed and restored through scripts/ablation-replace.mjs under the verify lock; the subjects resolve from src via relative imports, so no dist rebuild was involved. Item 1A, length cap removed: the over-long-key pin went red (1 failed, 2 passed), while the timing pins stayed green on the linear split. Item 1B, cap removed and the old capture split restored: the timing pin went red. Item 2, matchingElement made positional: 5 of the new 6 identity pins went red (delete, reorder, renamed header, ambiguous, through-service). Every restore was proven blob == HEAD with an empty git diff HEAD, and both files were green again (349 and 24) at HEAD afterwards. The first 1A attempt was refused by the tool before any run, because the replacement text was already inside the anchor so its count could not rise; it was re-run with a marker. dispatch-gates --ran with recorded exit codes: 99 derived, 98 run green, 1 NOT MEASURED. NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3); it reads every package's dist, and 8 packages outside this diff are unbuilt. CI runs it. After the amend, these were re-run green at 2320178c9d: check:nul-bytes, cross-package-test-inputs, test-source-alias, type-check-coverage, issue-citations, doc-authoring, adr-0087-registration, empty-changeset, published-files. The control-byte self-scan of every changed file found no hits. The derived gate list was unchanged after the amend.",
      "mcp_calls": "0",
      "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/21840/comments (this os-dev-report, via scripts/pm/post-stamped.mjs); plus one git push (not REST). One REST read of the PR body (GET pulls/21877).",
      "open_questions": [
        {
          "question": "The PR body is stale in one place and silent in another; dev may not PATCH it. Line 19 says restoreRedactedConfig carries array elements forward by position, and nothing mentions metadata-protocol. Suggested seat edit: replace that bullet with 'restoreRedactedConfig resolves each array hop by identity: the same index in an array left as served, else the one element equal to the served projection, unique on both sides; a value whose element changed, is gone or is ambiguous is dropped'. Add a bullet: 'packages/metadata-protocol/src/metadata-redaction.ts: the /meta PUT carry-forward applies the same rule to id-less elements and nested arrays, which it used to skip'. Refresh the consumer-suite counts from tests.",
          "options": ["A seat edits the body with the text above", "B leave the body; the changeset carries the rule"],
          "recommendation": "A, because the body states a now-false rule (by position), and reviewers read the body"
        }
      ],
      "out_of_scope_findings": [
        "carrier: this PR (Acceptance notes, seat to add) · noted, not filed — the 256-char cap also judges a long prose segment key before an = conservatively, so a contractless config string of more than 256 chars before its first = is refused; the review asked for this posture explicitly",
        "carrier: this PR · noted, not filed — libpq sslkey is a file path in libpq yet is now credential-shaped (item 5 asked for sslKey); a contractless config carrying sslkey=PATH is refused, a conservative false positive"
      ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21840,
      "status": "done",
      "branch": "claude/issue-21840-datasource-secret-keys",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21877",
      "session": "session_018zT8d8NpiQ1ExhuNd5TxY6",
      "premise_still_valid": true,
      "summary": "Round 4, head 5c405846a5 (5 commits on 2320178c9d: 44206622fb, 14b639b861, a changeset commit, f32b41fcd4, 5c405846a5; origin/main was not merged in). All six re-review items are fixed at fix level. (1) restoreRedactedConfig now redacts the grafted config again and keeps a graft only when the redaction still withholds its landing path, repeating until nothing more drops. This is the same loop as the /meta planCarryForward. An untouched Save, where the patch equals the served projection, skips the second walk. (2) PEM private-key armour (BEGIN ... PRIVATE KEY, including RSA, EC, DSA, ENCRYPTED, OPENSSH and PGP PRIVATE KEY BLOCK) is secret material in any string and in bytes, and its block is removed on read. A bare key under an ssl, tls, mtls, x509, pfx or pkcs12 holder, or any holder word starting with cert, is key material. pfx, pkcs12 and p12 are now credential words, so pfx bytes are judged. (3) A non-ASCII key is credential-shaped only when its non-ASCII text sits inside or next to a credential word. That means a credential word of another script, a confusable-letter or format-character reading that is credential-shaped, an ASCII credential word touching a non-ASCII character, or a credential word of four or more letters with at most one non-ASCII stand-in or insertion per four letters. Otherwise a non-ASCII run is a word of its own, so a CJK field name, Groesse-style names with umlauts and cafe-with-accent are accepted. The rule is shared by query, form and segment parameter names. (4) A single-line Name: value string is read as a header line only under a header-ish key; a multi-line string always is. A libpq or segment value that starts with a SQL bind placeholder ($1, ?, :name) is not a credential. Opaque URI schemes (mailto, sip, sips, tel, urn, xmpp, news, im, pres) are skipped before the scheme-less userinfo reading, and a time of day before the @ is not userinfo. (5) A string or bytes longer than 64 KiB (MAX_JUDGED_STRING_LENGTH) is judged credential material without being read: refused at write and served empty or dropped on read. The changeset states this. (6) A bare key value also counts 16 or more characters of hex holding a letter, and digit-free base64 whose case flips like random text. Camel case and paths stay names. While testing item 1 end to end, I found and fixed an in-PR round-3 inconsistency. The read projection of a key-labelled pair could itself hold a finding, so the write door refused an untouched Save of what was served. Now a key in a list element is a pair label, not the header named key, and the read projection is judged again until it holds no finding, with at most 8 passes before it is served empty. The changeset rule text was updated to match the code. The PR body was not edited; the changes the PM needs to make are in open_questions[0].",
      "tests": "All at head 5c405846a5 unless noted. spec: the whole local project, 619 files / 18884 tests passed (1 todo). The contractless file alone has 434 tests, round-4 additions included in both directions: PEM per armour, TLS holders, pfx, non-ASCII accepted and refused sets, parameter names, prose/SQL/mailto/sip/tel/urn/time accepted with controls still refused, the 64 KiB cap with an at-cap control, hex and base64 bare-key values with name controls, bounded-time cases, and served-projection-is-write-accepted cases. service-datasource: 40 files / 762 tests passed, including 6 new carry-forward tests (label renamed, label deleted, key: label renamed, renamed to another credential name keeps the value, the control untouched Save, and a through-the-service re-read). metadata-protocol: 217 files passed, 3 skipped / 27925 passed, 19 skipped. plugin-audit: 39 files / 630 passed. dogfood: the whole package in 4 batches, 204 files passed, 1 skipped (477+401+384+317 tests passed). The new contractless dogfood case 4 drives the admin PATCH edit door on a seeded legacy row with renamed labels; the stored row and two read doors carry no withheld value, and the positive control (the edit reached the stored row) holds. Typecheck green: spec (including scripts and test layer), service-datasource, metadata-protocol, plugin-audit, dogfood. check:generated green after regenerating api-surface and export-origins for MAX_JUDGED_STRING_LENGTH and EmbeddedCredentialOptions. Ablations through scripts/ablation-replace.mjs, each anchor verified on disk 1 to 0, each restore proven by blob == HEAD and an empty git diff HEAD. The tests import the subject from src, so no dist rebuild was needed. Item 1: making the re-judgment keep every graft turned 4 service tests red (renamed, deleted, key: renamed, through-service), and the control and the renamed-to-credential case stayed green. A first attempt was refused by the tool (the replacement contained the anchor, so the count did not drop) and never ran. Item 2A: dropping the TLS-holder clause turned 1 test red. A PEM under ssl.key is still caught by the armour reading, which is defence in depth. A first 2A attempt reported no tests (a shell-quoting mangle) and was discarded as not measured. Item 2B: removing the PEM detection turned 7 tests red. The restore legs re-ran 434/434 and 30/30 green. Gates: dispatch-gates --repo objectstack-ai/objectstack --ran derived 99 families: 98 run with exit 0, 1 NOT MEASURED. NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3, needs a full repo build), declared to CI. check:type-check-debt timed out at my 300 s per-gate budget and passed on a rerun under the lock (79/80 packages, debt unchanged). The derivation warned that the tree is at least 3 commits behind origin/main and that scripts/engine-double-contract.pinned.json changed there. This diff touches no fake engine.",
      "mcp_calls": "0",
      "api_writes": "4 - git push x3 (44206622fb; f32b41fcd4 carrying 3 commits; 5c405846a5), plus 1 relay comment op (POST /repos/objectstack-ai/objectstack/issues/21840/comments via fleet-write dispatch). Reads: GET pulls/21877 and GET issues/comments/6002380150 (REST).",
      "open_questions": [
        {
          "question": "PR body changes the PM must make (I did not edit the body).",
          "options": [
            "A: update the What changed, Tests and Acceptance notes sections as listed in the recommendation",
            "B: leave the body and point reviewers to this report"
          ],
          "recommendation": "A. (a) The Key judgment bullet: replace the round-3 non-ASCII rule with the round-4 one (credential-shaped only inside or next to a credential word; otherwise a non-ASCII run is its own word). (b) Write door, credential inside a string: add PEM private-key armour, the 64 KiB unread cap, the header-line reading (multi-line, or single-line under a header-ish key), SQL bind placeholders not being values, and opaque schemes and times of day not being userinfo. (c) Add a bullet for bare key: ssl/tls/cert holders, a list element's key being a label, and hex/base64 values; and add pfx, pkcs12, p12. (d) Read door: the projection is judged again until clean (at most 8 passes). (e) Edit round trip: add the re-judgment fixed point. (f) New exports: add MAX_JUDGED_STRING_LENGTH and EmbeddedCredentialOptions, and the headerish field on the finding. (g) Tests: replace with the round-4 figures in this report at head 5c405846a5. (h) Acceptance notes: an inline value over 64 KiB, such as a large CA bundle, is now refused; a libpq or segment value that starts with :word is read as a SQL placeholder (a known over-acceptance at the edge); the other-script credential words match as substrings (a known over-refusal); a key-labelled pair placed directly under a header-ish key is served without its label."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) - the derivation reports this tree at least 3 commits behind origin/main; the branch was not merged with main this round. Noted, not filed."
      ]
    }

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Work on PR #21877 is paused until the approach decision on #21921 is made. The fourth independent security review of the key-name heuristic, at head 5c405846a5, again found a new blocking gap. This time it was in the carry-forward fix from the previous round. Whichever option #21921 settles on decides whether the PR continues, is reduced, or is replaced. No further development round is dispatched before then.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Closed as not planned, per the maintainer's ruling on #21921 (「插件驱动 是写代码开发,安全性应该由写代码的人控制,不应该我们盲猜」, then 「同意作废,文档补一句」). The finding is limited to drivers the platform ships no config contract for, and it is readable only by platform administrators. That driver's config is the plugin author's responsibility: credentials belong in the bound secret (external.credentialsRef). PR #21877 is closed without merging. The datasource docs gain one sentence saying so.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions