Skip to content

feat(plugin-approvals): the request page's Details tab ends with the request snapshot's raw JSON, gated on studio.access - #23837

Draft
objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/issue-23746-approval-request-raw-json
Draft

objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/issue-23746-approval-request-raw-json

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23746
Clause-②: no

The services half of face 4 of the ruling on objectstack-ai/objectui#12438 (letter C, 6190549995, item 4). The served sys_approval_request_detail page's Details tab now ends with the request snapshot's raw JSON.

What changed

  • packages/plugins/plugin-approvals/src/sys-approval-request.page.ts: in slots.tabs.properties.items[0] (Details), children is now record:details, then { type: 'record:json', properties: { field: 'payload', label: { en, zh-CN, ja-JP, es-ES }, requiredPermissions: ['studio.access'] } }. The labels are the inbox side sheet's approvalsInbox.rawData copy, authored inline like the page's other labels. The docblock gains one Strategy bullet: the panel is a platform operator's debug view gated on studio.access, never the approver's read path.
  • packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts: the pins (below).
  • .changeset/23746-approval-request-raw-json.md: @objectstack/plugin-approvals patch, Clause-②: no.
  • No edit to sys-approval-request.object.ts, packages/spec/** or objectui.

The three mechanism assumptions, measured

1. record:json is declared and the literal is accepted. RecordJsonProps (packages/spec/src/ui/component.zod.ts:2606), registered at :8333 and in page.zod.ts:212. Measured on the real exported page (probe run at 51d33dfe57; the pins then hold the result):

page variant PageSchema.safeParse stack parse (ObjectStackDefinitionSchema) build rules / validateComponentProps
as shipped success success no finding
panel without requiredPermissions success success component-props-invalid at ...children[1].properties.requiredPermissions
panel with requiredPermissions: [] not run success component-props-invalid at the same path
panel with an extra path key not run success component-props-unknown-key at ...properties.path

So the page schema's parse does not judge a node's props bag. The props row does, through os validate's props gate and the build rules. The page pin therefore holds the gate with a control on that call. A page-parse check alone would not cover it.

2. payload is a key the page's record load serves. The read path the request page's record load takes, read end to end:

  • objectui (origin/main 63d00da, and the pin daeb77fc): apps/console/src/AppContent.tsx:305 wraps the console adapter in routeApprovalRecordReads. Its findOne of sys_approval_request reads GET /approvals/requests/:id through an ApiDataSource item door (approvalRequestsDataSource.ts). That door's findOne returns the response body unchanged (packages/core/src/adapters/ApiDataSource.ts:258).
  • objectstack: packages/rest/src/approvals-routes.ts:308-329 answers res.json(row) with row = svc.getRequest(id, context).
  • ApprovalService.getRequest builds the row with rowFromRequest (approval-service.ts:1166). That function sets payload: parseJson(row.payload_json, undefined) (:1187) and copies no payload_json key. Then redactPayloads narrows payload per reader (:1634).

The new served-key pin drives that producer. It runs ApprovalService.getRequest as the request's submitter over a stored row whose payload_json holds a snapshot. It asserts that the key the page's record:json names is served with the parsed snapshot, and that payload_json is not served. The generic data door (find/findOne on the engine) still answers payload_json, redacted (payload-redaction-middleware.ts). The request page does not load its record through that door.

3. Clause-②: no. I built @objectstack/plugin-approvals at the base 5b6c257b06 (a comparison worktree, closure from the shared turbo cache) and at 51d33dfe57. dist/index.d.ts is byte-identical in both builds: sha256 8d6525557b9d3cc681e0cf1e06c8dac6f163ebe535b4f36f99d1a87690d88b1e, and cmp exits 0. The page is not in the declaration closure: SysApprovalRequestDetailPage has 0 hits in the d.ts against 1 for the control declare class ApprovalService. It does ship in the JS: record:json has 1 hit in dist/index.mjs at head and 0 at base.

Pins

  • Children pin: the Details tab's children are exactly record:details then record:json. The panel's non-copy props are exactly { field: 'payload', requiredPermissions: ['studio.access'] }, and its label carries the plugin's four served locales. The label text is copy, so it is not pinned.
  • Served-key pin: described under assumption 2.
  • Gate control: a panel with its requiredPermissions removed is refused by validateComponentProps (component-props-invalid at that path).
  • Page parse: unchanged. PageSchema.safeParse of the exported page succeeds, and the component-type vocabulary, the props call and the build rules raise nothing on the page.

Ablations

Both run at e46780683b with node scripts/ablation-replace.mjs (wrap mode), anchored on the page file. The test imports the page by a relative src path, so no dist/ is involved.

A. Drop the block (anchor hit 1, then 0; blob 42f3675ea82c to 4be6cc90542c): 3 tests red, 15 green.

× tabs: Details is record:details then the raw snapshot panel, ...
  AssertionError: expected [ 'record:details' ] to deeply equal [ 'record:details', 'record:json' ]
× the request read serves the parsed snapshot under the panel's `field`, and never the stored column
  AssertionError: expected [] to have a length of 1 but got +0
× control: the raw snapshot panel without its required gate is refused by the same call
  TypeError: Cannot read properties of undefined (reading 'properties')

B. Point field at payload_json (anchor hit 1, then 0; replacement 0, then 1; blob 42f3675ea82c to 08ffac93f084): 2 tests red, 16 green. The served-key pin is red because that key is never served:

× the request read serves the parsed snapshot under the panel's `field`, and never the stored column
  - Expected
  + Received
    {
      "field": "payload_json",
  -   "served": {
  -     "amount": 1200,
  -     "id": "exp_1",
  -     "title": "Q1 travel",
  -   },
  +   "served": undefined,
    }
× tabs: ... AssertionError: expected { field: 'payload_json', ...(1) } to deeply equal { field: 'payload', ...(1) }

Restore after each leg, judged by the tool against the disk: blob after restore 42f3675ea82c equals the HEAD blob, and git diff HEAD is empty.

Tests and gates

Everything below ran at head 54aa39a949 unless a different sha is named. That head is this branch merged with origin/main 43567a8fcb. The merge brought two commits, and neither touches plugin-approvals, packages/spec/src/ui or packages/lint. After the merge I rebuilt the 61-package build closure and re-ran everything below.

  • Package suite: pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 exit 0: Test Files 78 passed (78), Tests 1084 passed (1084).
  • Page pin file: 18 of 18 pass, inside the suite above and alone at e46780683b.
  • Typecheck: pnpm --filter @objectstack/plugin-approvals typecheck exit 0. The test file is in the tsconfig.test.json program (--listFiles count 1) with 0 errors. The build tsconfig.json does not include tests.
  • Spec artifacts: pnpm --filter @objectstack/spec check:generated exit 0 after the merge. All 14 generated artifacts are up to date.
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 65 commands. All 65 ran, and every exit code was captured before any pipe: 65 exit 0. --ran reconciliation: "65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN". The zero is derived from the recorded exit codes, and none of them is 3.
    • On the first pass, check:objectql-double-limit and check:where-matcher each found a real problem in the new engine double. The double was blind to limit, it then copied rows before applying the bound, and its matcher could not be lifted from inside an async function. e46780683b fixes all three, and both gates are green on the head.
    • check:i18n and check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3, nothing measured). After the CLI build closure those gates name, both exit 0.
  • Not run locally, owned by CI: the repo-wide pnpm lint, test:repo, and the rest of the lint-workflow farm. eslint on the two touched TypeScript files exits 0.

Acceptance notes

  • The console has no record:json renderer yet. I found no registration at objectui origin/main 63d00da or at the pin daeb77fc, and the type is not in objectui's placeholder list. Until objectui registers the renderer (approvals face 4 of 5 (ruling C on objectui#12438): the served sys_approval_request page carries the raw-payload panel as a generic block gated on studio.access declared in the page metadata, as the inbox side sheet gates it objectui#12490 step 4) and this repo's .objectui-sha moves to it, SchemaRenderer draws this node as its red OBJUI-001 "Unknown component type: record:json" panel (packages/react/src/SchemaRenderer.tsx:2151-2175). That panel prints the node's schema JSON, not the payload, and every viewer of a request's Details tab sees it, approvers included. I read this from the source and did not check it in a browser. The step order (page edit, then renderer) is the ruling's. When this lands relative to the pin move is the landing seat's call. This PR stays draft.
  • One sentence in the unreleased spec changeset this edit makes false. .changeset/23745-record-json-block.md (pending, unreleased) says: "objectui draws it and @objectstack/plugin-approvals places it on the approval request page after this release". If this PR lands before that changeset is released, plugin-approvals places the block in the same release. The file is outside this card's surface and is left unchanged. The report to the dispatching seat names the one-line edit.
  • Wording, not a defect: the same changeset says a node without field or requiredPermissions "is refused at parse". Measured above, the refusal comes from the props gate at os validate / os build (component-props-invalid). The page and stack parses accept such a node.

Generated by Claude Code

…raw request snapshot as a record:json panel gated on studio.access

The Details tab of the served sys_approval_request_detail page now holds
record:details, then a record:json block on `payload` (the parsed snapshot
the request read serves), gated on requiredPermissions ['studio.access'].

The page pin holds the tab's children, and a new pin reads one request
through ApprovalService.getRequest (the read the console's request page
loads its record through) and asserts the key the panel names is served
and the stored column payload_json is not.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
… judged by the props call

The page schema's parse does not judge a node's props bag, so the pin that
the panel's requiredPermissions holds is the props call's refusal of a panel
without it.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
…er's bound and refuses operators it does not implement

check:objectql-double-limit and check:where-matcher judge every new find
double. The double now applies `limit` after the filter, by presence, and
copies only the bounded rows. Its matcher lives at module scope, where the
matcher gate can lift it, and throws on a combinator or operator outside
equality, `$in`, `$or` and `$and`.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-approvals, touching 2 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/api/plugin-endpoints.mdx (via /approvals/requests/:id (route, a path literal in a comment on a changed line))
  • content/docs/automation/approvals.mdx (via /approvals/requests/:id (route, a path literal in a comment on a changed line))
  • content/docs/automation/flows.mdx (via /approvals/requests/:id (route, a path literal in a comment on a changed line))
  • content/docs/kernel/services-checklist.mdx (via /approvals/requests/:id (route, a path literal in a comment on a changed line))
  • content/docs/permissions/administrator-guide.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/permissions/authorization.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/permissions/permission-sets.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/permissions/system-context.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/ui/apps.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/ui/audience-based-interfaces.mdx (via studio.access (literal, a string literal in slots))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/releases/v16.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/releases/v17/17-5.mdx (via studio.access (literal, a string literal in slots))
  • content/docs/releases/v17/17-6.mdx (via studio.access (literal, a string literal in slots))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0cfd0c49bd2635da048fae96b35a38448141fa9d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 67a62fb599e6a8997c63613edfcb827822d32b5a — the merge of head 54aa39a949185bbb90760ac3f128076f9d646e4d into base 0cfd0c49bd2635da048fae96b35a38448141fa9d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 67a62fb599e6a8997c63613edfcb827822d32b5a && git checkout 67a62fb599e6a8997c63613edfcb827822d32b5a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0cfd0c49bd2635da048fae96b35a38448141fa9d 54aa39a949185bbb90760ac3f128076f9d646e4d && git checkout -B drift-repro 0cfd0c49bd2635da048fae96b35a38448141fa9d && git merge --no-ff 54aa39a949185bbb90760ac3f128076f9d646e4d

node scripts/docs-audit/affected-docs.mjs --json 0cfd0c49bd2635da048fae96b35a38448141fa9d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 0cfd0c49bd2635da048fae96b35a38448141fa9d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · content reviewed, landing HELD · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T08:02Z

Read against GitHub at head 54aa39a949 and against objectui's own tree, not against the dev's report.

  • Form: draft, base main. The first line is Fixes #23746, then Clause-②: no. Three files, all on claim 6209561007's surface: the page, its pin, and a patch changeset for @objectstack/plugin-approvals. 147 changed lines.
  • Content: right.
    • The Details tab's children are record:details, then record:json with field: 'payload', the four-locale "Raw data (JSON)" label, and requiredPermissions: ['studio.access'].
    • The docblock gains the one Strategy bullet. There is no object, packages/spec or objectui edit.
  • The three premises, measured:
    1. The props are judged by the props gate (validateComponentProps / component-props-invalid), not by PageSchema.safeParse. The pin carries a control on that call.
    2. The page loads its record through objectui's routeApprovalRecordReads, then GET /approvals/requests/:id, then ApprovalService.getRequest. There, rowFromRequest serves the parsed snapshot as payload, redacted per reader, and never payload_json. This is pinned.
    3. dist/index.d.ts is byte-identical at base and head, so Clause-②: no holds.
  • Ablations: (A) dropping the block turns 3 cases red. (B) Pointing field at payload_json turns the served-key pin red (served: undefined). Both restores were proven by blob.
  • Gates: 65 derived, 65 exit 0, reconciled with --ran.
  • Deviations: accepted.
    • A second, temporary worktree, detached at the base with no edits, was used only to byte-compare the declaration. It was removed without --force.
    • The two probe scripts were deleted in the same command that ran them.
  • CI: still running at this write. It is read again before landing.

Landing is HELD. This is this seat's call as the landing seat.


Generated by Claude Code

This branch has not been deployed

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants