Skip to content

security(metadata): api-backend.rest-metadata-config-contract clause 4 (object-schema field masking) fails at 251a7dd4 — detail withheld pending maintainer #21723

Description

@objectstack-fleet

QA-source: #21720 · api-backend.rest-metadata-config-contract · acceptance[3]

The 17.7 pre-release run #21720 (subject 251a7dd4) failed clause 4 of api-backend.rest-metadata-config-contract (rev 3), the object-schema field-masking contract (ADR-0106).

  • Class: authorization (a disclosure across a permission boundary). Under RUNNER rule 2 the reproduction and the mechanism are withheld pending maintainer; they are held in the runner session (Claude Code session session_018zT8d8NpiQ1ExhuNd5TxY6) and reported to the maintainer there.
  • Verification: reproduced by the runner (3×, 2 fresh sessions) and independently by a verifier (RUNNER rule 7) on two fresh databases: CONFIRMED, narrower than a data leak — no record values cross the boundary.
  • Severity as judged by the verifier: P1 / medium.
  • Not a 17.7 regression: the code involved is unchanged from 17.6.0.
  • Owning repo: objectstack. No existing card covers it (all open issues searched).

Full run evidence (per-item table; this clause's row carries the same withholding): #21720.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · security · priority:p1 · domain:engine · area:access · pm:queue. A field-masking disclosure across a permission boundary, graded from the card text

    Triage seat (objectstack-wide, first touch) · session_018zT8d8NpiQ1ExhuNd5TxY6 · 2026-10-04T11:51Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only. The withheld reproduction stays withheld (RUNNER rule 2).

    Path: permissions that actually hold | api-backend.rest-metadata-config-contract | P52

    Filing gate. Category ①, extracted under the RUNNER extraction obligation from run #21720. This card falls under the "may disclose data" exception. reach: the card records a confirmed public-entry failure, reproduced by the runner and independently by a verifier on fresh databases. Acting reader: the domain:engine execution seat.

    Why p1 and security.

    • It is an authorization-class defect: a disclosure across a permission boundary. Under North Star priority rule 1, safety comes first.
    • The checklist item is P1, and the verifier's own grade is P1 / medium.
    • No record values cross the boundary, which is why it is p1 and not p0.

    Routing. The fix lands in the engine package family, domain:engine, as confirmed against the held detail. The axis is area:access, the field-level permission axis, even though the failing item sits on the API road row.

    Direction. The governing contract is the one the card cites, ADR-0106's object-schema field masking. The fix makes the declared masking hold on the path the clause tests. ⛔ It does not narrow the contract on the consumer side.

    The withheld detail. The claiming seat obtains the reproduction and the mechanism from the holding session (session_018zT8d8NpiQ1ExhuNd5TxY6) on the maintainer's word. ⛔ They are never restated on this card or its PR. The PR's pins must cover both directions of the clause: the restricted persona is masked, and the permitted persona is the control. They go in without a narrative of the recipe.

    Dedupe. I read all 118 open issues; none covers this clause. This agrees with the card's own search.

    Size/model suggestion: S–M, mode:subagent.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (17.7 pre-release defect from QA run #21720, 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-21723-metadata-mask
    Worktree: objectstack-issue-21723
    Domain: domain:engine (card label, as triage set it); expected surface packages/metadata-core
    File surface: packages/metadata-core/src, packages/qa/dogfood/test, .changeset/
    Container & model: M, mode:subagent, model: opus (default tier; no path-derived mandate)
    Clause-②: no
    Thread-read: 5979633494
    Serial constraints cleared: none named

    Provenance: the maintainer, in Claude Code session session_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-04, verbatim: 「21732 和 #21723 你可以派发处理」; landing per the standing order 「开发完整就进队列合并」. This session holds the withheld reproduction; the dispatch carries it privately. The withheld detail stays off this card and its PR.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21723,
      "status": "done",
      "branch": "claude/issue-21723-metadata-mask",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21743",
      "session": "session_018zT8d8NpiQ1ExhuNd5TxY6",
      "premise_still_valid": true,
      "summary": "applyObjectSchemaMask (metadata-core) now removes a denied field's REFERENCES from the served object document, not only its fields entry. This is done by a new module, object-schema-fls-references.ts, which classifies every ObjectSchema and FieldSchema key once. Rule entries (validations/indexes/activityMilestones) are dropped whole. Role pointers are deleted. Name lists, including a readable field's relatedListColumns/dependsOn, are filtered. Expressions, including a readable field's formula and visibleWhen/readonlyWhen/requiredWhen, are deleted. List views and actions are filtered or dropped. Foreign-object positions are left alone, and an unclassified key that mentions a denied field is deleted (fail-safe). A test pins both tables to the live spec key sets. Every exit (by-name, list and layers, on REST and the runtime) runs this one projection, so no exit was edited. The contract fixture now names its four fields in every position kind. The residue check matches identifier tokens anywhere, so an expression reference fails it. Projection cases also carry retained facts, so over-masking fails too. Scope decisions are justified in the PR body. Live proof on a fresh /tmp showcase boot with the withheld recipe: the member's by-name, list and layers reads carry 0 identifier references to the 3 denied fields, and the admin control still carries 13 / 10 / 20.",
      "tests": "Head 4ab5f92b9: metadata-core build+typecheck+test 18 files / 350 passed. rest meta-object-fls + meta-item-save-capability-gate + meta-compound-save-and-reset-capability-gate: 3 files / 123 passed. runtime domains/meta-object-fls: 85 passed. rest meta-alternate-door-read-gates, meta-type-read-capability, import-template, import-template-route: 445 passed. runtime 8 meta read/list/parity files: 856 passed. Ablation via scripts/ablation-replace.mjs on committed HEAD: the reference projection was replaced with the old shape, anchor x1 to x0, blob af3c4e5e to 4492227a. Result: 16 of 24 failed in object-schema-fls-references.test.ts, including all 3 contract projection cases (residue at $.stageField). Restored to the HEAD blob af3c4e5e with git diff HEAD empty. No dist was needed: the suite imports src. dispatch-gates --commands derived 62 families; all 62 ran and exited 0, and --ran reconciliation gave 62 accounted for and a derived zero NOT-MEASURED. Lint narrowed to the 4 changed TS files: eslint json reported 4 files, 0 errors, 0 warnings; the config never enables type-aware linting, so untouched files are invariant; full pnpm lint is left to CI. Final head bbaec097c, a docblock-only change: metadata-core build+typecheck+test 350/350 again, and nul-bytes, type-check-coverage, type-check-debt, doc-authoring, issue-citations, published-files, cross-package-test-inputs and test-source-alias all exited 0. CI on the PR: in_progress at report time. cli integration layer: not touched.",
      "mcp_calls": "0",
      "api_writes": "3 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, each executed by fleet-write.yml as the fleet bot): pr_create POST /repos/objectstack-ai/objectstack/pulls (#21743, draft forced, body read back byte-identical); assignees POST /repos/objectstack-ai/objectstack/issues/21743/assignees (hotlong, read back); this os-dev-report comment POST /repos/objectstack-ai/objectstack/issues/21723/comments. Plus git push (not REST). Zero label writes: the dispatch named none, and the changeset exists, so skip-changeset does not apply.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: ADR-0106 D5(4) follow-up (PM to route;承接者:无 named yet) · noted, not filed · other metadata types (view/page/dataset) that name a hidden field are not projected; deferred by the ADR and by the dispatch · dedupe words: D5(4) outlet audit, view hidden field, metadata mask other types",
        "carrier: same D5(4) follow-up · noted in PR Acceptance notes, not filed · foreign-object positions (object B's lookupColumns/displayField/lookupFilters naming object A's fields, and a parent's summaryOperations.field naming a child field) are judged against B's denied set, never A's; masking them needs a cross-object projection. reach: none measured; on the stock showcase no such position names a denied field (member full list read scanned) · dedupe words: cross-object mask, lookupColumns hidden field, summaryOperations FLS"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21723,
      "status": "done",
      "branch": "claude/issue-21723-metadata-mask",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21743",
      "session": "session_018zT8d8NpiQ1ExhuNd5TxY6",
      "premise_still_valid": true,
      "summary": "Follow-up round on the pre-merge review (CHANGES-NEEDED), two new commits on the branch, no force-push; head c0a2b9990, still a draft. F1: `inlineAmountField` is now a pointer and `inlineColumns` is scrubbed column by column against THIS object. A column whose `name` is denied, or whose `expr` reads a denied field, is dropped. `readonlyWhen`/`requiredWhen` are expressions. label/type/reference/displayField/idField are kept. The new INLINE_COLUMN_POSITIONS table is pinned closed against InlineGridColumnSchema, and that pin caught `width`/`required` on first run. The comment for the other-object keys was re-audited: displayField/descriptionField/lookupColumns/lookupFilters name the reference target and summaryOperations reads the rolled-up child, so they stay kept. F2: classified positions test object keys only inside field-keyed blocks (FilterCondition, lifecycle onlyWhen, action patch; `$` operator keys excluded). Values of closed-vocabulary keys (type, dialect, severity, events, format, operator, order, direction, transitions, initialStates, regex, schema) are not read. relatedListFilter and both onlyWhen moved to the field-keyed reading. The unclassified fail-safe path still reads every key. Pins cover denied fields named `type` and `source`, plus a denied `and` against `$and`. F3: the reference walk carries a path-scoped WeakSet guard: cycles terminate, and shared sub-objects are still read in each position's own mode. There is a cyclic-document test. F4: a listViews entry keyed by a denied name is dropped. A fieldGroups key is kept and documented as a group namespace, because dropping it would regroup readable fields that point at it. F5: the contract fixture uses `expression` and now covers list views (column, filter, denied-name key), actions and the inline grid (by name, by expr, amount field). The field count stays at four. F6: an accepted-limitations section is added to the module docblock. Changeset scope text updated. CONFLICT, standard text: the dispatch asked me to edit the PR body's scope, but os-dev.md says dev writes the PR body once and never PATCHes it, and on conflict os-dev.md wins. So I did not edit it. Seat rewrite: (1) Foreign row: drop `inlineColumns`/`inlineAmountField`. (2) New row 'Inline grid | `inlineColumns`, `inlineAmountField` | a column that is or is computed from a denied field is dropped, a denied amount field is deleted'. (3) Replace 'Object keys are tested too, because a filter condition is keyed by field name' with 'In a classified position a key is a reference only inside a field-keyed block (FilterCondition, lifecycle onlyWhen, action patch). The unclassified path tests every key.' (4) Add 'A list view keyed by a denied name is dropped.' (5) Pin sentence: add InlineGridColumnSchema. (6) Verification: cite head c0a2b9990 with the numbers in `tests`.",
      "tests": "All on code head 59d4a4173; the final head c0a2b9990 changes only the changeset. metadata-core vitest + typecheck re-run on c0a2b9990: 18 files, 362/362 passed, typecheck exit 0, and the test files are inside the tsc program per --listFiles. Consumer suites after rebuilding the rest/runtime dependency closure (dist carries the new code, grep-confirmed): rest meta-object-fls + meta-item-save-capability-gate + meta-compound-save-and-reset-capability-gate, 3 files, 123/123; runtime domains/meta-object-fls, 1 file, 85/85. Ablation via scripts/ablation-replace.mjs on the committed tree, trap restore, each leg restored to the HEAD blob 48d2c49e87c8 with an empty `git diff HEAD`. Leg A, inline grid back to keep: anchor 1->0, blob changed, 5 failed / 31 passed, including all three contract projection cases. Leg B, classified expression back to the all-keys reading: anchor 1->0, 1 failed (the CEL-envelope/`source` pin). Live proof, in-process: the stock example's real object that declares an inline grid, with a scratch-authored denial of 2 fields that both sit in its inline columns (the stock permission sets deny no field there). Old head bbaec097c: 2 residual references, both inside the inline grid, 7 columns served. New head: 0 residual, 5 columns. NOT MEASURED, HTTP leg: not re-booted on the shared box. The mask function every exit calls is the one measured, and the exits are contract-driven above. dispatch-gates --commands --repo objectstack-ai/objectstack derived 62 families; 60 exited 0. NOT MEASURED: check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET, needs a full workspace build); check:type-check-debt (exit 124, the whole-workspace tsc re-measure was killed by my 280s timeout; check:type-check-coverage itself exited 0). --ran reconciliation: 62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN, exit 0. After the changeset commit, re-run on c0a2b9990 and all exit 0: check-changeset-no-major, check-empty-changeset, check:changeset-gate-self-tests, check:doc-authoring, check:published-files, check:nul-bytes, check:issue-citations, check-closing-keyword-parity. Narrowed eslint `--no-inline-config --format json` over the 4 changed TS files: 4 files, 0 errors, 0 warnings. The population is eslint.config.mjs `files: **/*.{ts,…}`. `--print-config` shows parserOptions.project null, so linting is not type-aware and untouched files' verdicts cannot move; full `pnpm lint` is left to CI. Control-byte self-scan over changed files: no hits. CI on c0a2b9990: 13 completed, 17 in_progress at report time.",
      "mcp_calls": "0",
      "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/21723/comments (this os-dev-report, via the fleet-write relay). Also 3 git pushes (not REST); no PR body PATCH; no label write (labels read back unchanged: documentation, size/xl, tests, tooling).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

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

    The fix (PR #21743) adds one exported type, FlsContractRetention, to @objectstack/metadata-core's published ./testing entry (the ADR-0106 contract table consumers drive). A new export on a published entry widens the public surface, so the changeset now grades @objectstack/metadata-core minor (head 8a8f488ae) and the PR body declares yes (widening). No other part of the claim changes.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21723,
      "status": "done",
      "branch": "claude/issue-21723-metadata-mask",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21743",
      "session": "session_018zT8d8NpiQ1ExhuNd5TxY6",
      "premise_still_valid": true,
      "summary": "Second follow-up round (head 80e5f775de, two commits on top of 8a8f488ae). N1: name lists that admit object entries now judge EVERY facet through a classified per-entry table instead of `field`/`name` only. List-view `columns` use LIST_COLUMN_POSITIONS (field pointer; nested `prefix` = COLUMN_PREFIX_POSITIONS {field: pointer, type: keep}; `summary` = bare vocabulary string kept, or COLUMN_SUMMARY_POSITIONS {type: keep, field: pointer}; `action` = registered action id and `type` = renderer name, both keep); a column whose facet reads a denied field is dropped (not the whole view): this matches how the module already treats a name-list entry and an inline-grid column computed from a denied field (entry dropped whole), and dropping the view would over-mask the caller's readable columns for a column-local reference. Unclassified column keys go the fail-safe unclassified way. Audit against the spec: object-level highlightFields/searchableFields, publicSharing.redactFields, field relatedListColumns, and list-view hiddenFields/fieldOrder/searchableFields/filterableFields are string-only (z.array(z.string())); the only other object-form name list is field `dependsOn` ({field, param}), whose `param` is the lookup target's remote filter key, now classified in DEPENDS_ON_ENTRY_POSITIONS {field: pointer, param: keep}. An object entry in a string-only list is now read fail-safe (any mention drops it). ListColumnSchema, ColumnPrefixSchema and ColumnSummaryConfigSchema are pinned both directions. FLS_CONTRACT_OBJECT gains a fourth list view with three object-form columns (plain, nested prefix pointer, nested summary pointer); field count stays four; retained facts added for both projection cases and the unmasked count moved to 4 views. N2: a field-keyed key, a name-list string entry, a pointer, an inline-grid column name and a ttl field are judged on the whole name AND its root dotted segment (namesDeniedField), so a path rooted at a denied field reads as a reference; pinned. N3: left as is and documented in the module's Accepted limitations — a param `name` defaults to its `field` and names the request-body key the action commonly writes, so treating it as a non-reference is not safe. Changeset scope text updated. PR body proposal for the seat (append to the scope list): 'Object-form list columns are dropped when any facet (field, prefix.field, summary.field) names a denied field; the column, prefix and summary tables are pinned to the live spec. A dotted path rooted at a denied field counts as a reference to it.' plus the ablation and test lines from `tests` below.",
      "tests": "All on HEAD 80e5f775de. Dist closure rebuilt first: os-verify-lock `pnpm --workspace-concurrency=2 --filter '@objectstack/runtime^...' --filter '@objectstack/rest^...' build` VERDICT command-exit 0 (metadata-core build: Done; dist/testing.js carries the new fixture view). metadata-core: `vitest run --maxWorkers=2` Test Files 18 passed, Tests 369 passed; `typecheck` (tsc --noEmit && tsc --noEmit -p tsconfig.test.json, the latter covers *.test.ts) exit 0; VERDICT command-exit 0. Consumers (upstream-direction closure built, consumers read metadata-core dist): rest `vitest run src/meta-object-fls.test.ts src/meta-item-save-capability-gate.test.ts src/meta-compound-save-and-reset-capability-gate.test.ts` Test Files 3 passed, Tests 123 passed; runtime `vitest run src/domains/meta-object-fls.test.ts` Test Files 1 passed, Tests 85 passed; VERDICT command-exit 0. Ablation of N1 (committed state, trap restore with absolute path, via scripts/ablation-replace.mjs): anchor `return !entryReadsDenied(entry as Record...` x1 -> x0, replacement (judge an object entry by its `field` only, the pre-fix behaviour) x0 -> x1, blob ec907917 -> 5399229a; subject resolves via relative src import, so no dist leg needed; object-schema-fls-references.test.ts went 5 failed / 38 passed: both new column tests plus contract cases restricted-caller/field-vanishes-whole, restricted-caller/required-permissions-cause, guest-fallback/D7, residue reported at $.listViews.compact.columns[1].prefix.field; restore: blob == HEAD ec907917, git diff HEAD empty, hash-object matches HEAD blob. dispatch-gates --commands --repo objectstack-ai/objectstack (tree 80e5f775de, flagged 9 commits behind origin/main; stale files engine-double-contract.pinned.json and test-shard-timings.json, not on this diff): 62 commands; 59 exit 0 (incl. check:nul-bytes, check:test-source-alias, check:published-files, check:dts-closure, check:engine-double-contract); NOT MEASURED: check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (needs full-workspace dist); NOT MEASURED: check:query-options-erasure and check:type-check-debt, reason: exceeded a 240s local cap on a box shared with a browser QA run, CI-owned. `--ran` reconciliation: 62 derived, 59 run, 3 NOT-MEASURED, 0 UNRUN, exit 0. Narrowed eslint: population = eslint.config.mjs `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` object (each of the 4 changed .ts files resolves a config via --print-config); `eslint --no-inline-config --format json` on the 4 changed files: 4 files, 0 errors, 0 warnings, exit 0; invariance: the config never enables type-aware linting (no parserOptions.project), so this diff cannot move a verdict on an untouched file. CI convergence: in_progress (not awaited).",
      "mcp_calls": "0",
      "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/21723/comments (os-dev-report, via fleet-write relay); git push is not REST; PR body not edited; no labels written this round",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. added a commit that references this issue on Oct 7, 2026
    a6a7547
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:enginepriority:p1High: required for production / M2security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions