Repository navigation
fix(objectql,plugin-security): the write preview answers a row's organization from the tenant wall's own verdict - #25215
Conversation
… walled write) Measurement-only commit: records what origin/main does today for the import dry run's preview of a walled insert, and what reading the wall's verdict would cost. Not a pin; deleted before any PR. Claude-Session: https://claude.ai/code/session_0157EiYDGYnrsXE9aHv1uMxU Co-authored-by: Claude <noreply@anthropic.com>
Measurement-only: the same two cells through POST /api/v1/data/:object/import, dry run against commit. Deleted before any PR. Claude-Session: https://claude.ai/code/session_0157EiYDGYnrsXE9aHv1uMxU Co-authored-by: Claude <noreply@anthropic.com>
…-qv7hgm Claude-Session: https://claude.ai/code/session_0157EiYDGYnrsXE9aHv1uMxU Co-authored-by: Claude <noreply@anthropic.com>
The probe recorded what origin/main did (readings M1-M5 on f7d7403) for the needs_decision report. Ruling 6336640643 (letter A) turns those readings into pins, which land with the fix; the measurement file itself is not a pin and goes first, as the resume order requires. Claude-Session: https://claude.ai/code/session_0157EiYDGYnrsXE9aHv1uMxU Co-authored-by: Claude <noreply@anthropic.com>
…nization from the tenant wall's own verdict validate() runs no middleware, so the Layer 0 tenant write wall never judged a preview row. On a walled deployment the import dry run reported a caller's organization_id dropped where the write stores it as sent, and answered valid a row naming an organization the write refuses 403. plugin-security: step 3.7's judgement is composed by one method, composeWriteTenantWall (the scope pre-test with its delegator leg, the parts, the forge guard, the emptied organization, the no-active-organization insert arms, and judgesOrganization). The middleware's payload half and stored-image seam call it, with unchanged refusals and log lines. writeWallVerdict answers each preview row from the same method, and is registered on the engine. objectql: registerWriteWallVerdict, an optional member beside registerWriteGateProbe. validate() asks it before the strips and again for a row the strips changed, as the write asks its wall before and after them. A judged organization is spared from the static readonly strip; a refused row answers valid false with the write's code and message, under organization_id or _record, and drops nothing. Unregistered, the strip runs as before. The registerWriteGateProbe docblock no longer says the probe runs step 3.7's organization wall: it asks only the no-active-organization pre-test. Clause-②: yes (widening) Metadata-protocol: none Claude-Session: https://claude.ai/code/session_0157EiYDGYnrsXE9aHv1uMxU Co-authored-by: Claude <noreply@anthropic.com>
check-system-context-census requires a row for every elevation read; the new writeWallVerdict answers "not asked" for a system context, as the elevated write meets no tenant wall. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157EiYDGYnrsXE9aHv1uMxU
…s row WriteWallRowVerdict names organization_id as the field a refusal answers under. It is a type, not a storage read of the injected column. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157EiYDGYnrsXE9aHv1uMxU
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c275a36b86cd1c7d4132d4ed4766ab939db676d3 && git checkout c275a36b86cd1c7d4132d4ed4766ab939db676d3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 03de83bfe145eea8f495bcbc9886105715721b3f a3088d01d2a9389bd8b510aa3187f67170e23cdd && git checkout -B drift-repro 03de83bfe145eea8f495bcbc9886105715721b3f && git merge --no-ff a3088d01d2a9389bd8b510aa3187f67170e23cdd
node scripts/docs-audit/affected-docs.mjs --json 03de83bfe145eea8f495bcbc9886105715721b3f
|
Contract reviewServed-tier: Judged against ruling 6336640643 (letter A, confirmed 6336790186), card #25062 and its comments, the PR body with its Acceptance notes, the dev report 6341141274, and the net diff against the merge base ① Derived judgments(a) Premise — one method, no second spelling: RIGHT. At head, step 3.7's judgement is composed by Behaviour preservation of the moved step, read condition by condition: parts computed when (b) Security direction: RIGHT, fail-closed on every exit. The write path's only change is the extraction above, with equal predicates, so the write admits nothing it refused before. The preview changes in two places only: it spares (c) Prohibitions: KEPT. The probe's boolean is untouched (body byte-identical; (d) Deviation 3, the second ask on the stripped row: INSIDE the ruling, and RIGHT. The ruling fixes the seam's signature, that (e) Exported surface: RIGHT in kind; the PR body's one-line parenthetical under-names it. The diff publishes: ② Semver levelBoth changesets grade ③ Boundary flags
Check-runs on For the seat, outside the verdict: (α) amend the PR body's Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
|
…s its write does update-repoint-organization-wall.test.ts pinned the preview's wall-blind answer, the very defect the write-wall verdict closes: an admitted re-point reported as dropped, a refused one admitted. It now pins the preview against the write: no drop for the admitted re-point, and the write's PERMISSION_DENIED code and message under organization_id for the refused one. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157EiYDGYnrsXE9aHv1uMxU
Contract reviewServed-tier: Why a new record. The head moved from What the delta is, proven on the committed trees. ① Derived judgments(a) The delta is test-only: RIGHT. One (b) The old pin pinned the defect the ruling orders fixed: RIGHT, so the rewrite is the ruling's consequence. The file entered (c) The new pin asserts ruling item 2 exactly; nothing loosened, no surviving control removed. Old assertions, three: ⑴ admitted preview (d) Anything else in the delta: NONE beyond prose. The header bullet is rewritten from "runs no middleware, so it is wall-blind" to "was wall-blind … now reads the wall's own per-row verdict (#25062)", and the Carried forward from record 6341404570, the hunks being byte-identical: (a) one method, no second spelling — ② Semver levelUnchanged by the move: both changesets are byte-identical to the reviewed head and the delta publishes nothing. ③ Boundary flags1–11 of record 6341404570 stand as written, every file they read being byte-identical at this head: the probe docblock finding closed;
Check-runs on For the seat, outside the verdict: (α) (β) (γ) of record 6341404570 stand; (δ) retitle the update twin of ③.13 for symmetry with the insert twin, here or in a follow-up; (ε) read Implemented-by: VERDICT: PASS |
席位核对 (head move)ACCEPT ·
|
Requested by Bill · project thread
Fixes #25062
Clause-②: yes (widening): one additive registration member on @objectstack/objectql's plugin-facing API
Metadata-protocol: none
Builds ruling 6336640643 (letter A, confirmed by the maintainer in 6336790186). Claim 6339442996,
Ruling-ref: 6336640643. Theplugin-securityhalf is the cross-lane touch declared on #21118 (comment 6339470244).Summary
ObjectQL.validate()(the import dry run) runs no middleware, so the Layer 0 tenant write wall never judged a preview row. On a walled deployment the preview reportedorganization_idas dropped where the write stores it as sent (group, a sister organization), and answeredvalidfor a row the write refuses with 403 (isolated, a foreign organization).Now the wall's own verdict travels to the engine as a value, per row, and the preview reads that value and nothing else:
plugin-security. Step 3.7's judgement of a write is composed by ONE method,composeWriteTenantWall, and the middleware itself calls it. That covers the scope pre-test with its ADR-0090 D10 delegator leg, the supplied-value forge guard, the emptied organization on update, the group posture: a member whose session has no active organization gets 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on a tenant-scoped create, where isolated answers 403 naming the missing organization #24784 / a platform operator whose session has no active organization gets 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on an insert into a posture-permitting tenant object, under group and isolated alike #24821 no-active-organization insert arms, and whether the wall judges the organization. The payload half and the stored-image seam both read it.tenantPartsandclearsWallare each declared once, inside it.SecurityPlugin.writeWallVerdict(object, operation, context, rows)answers each row from that same method, over the principals the middleware's own prologue resolves (resolveOperationPrincipals). It is registered on the engine asregisterWriteWallVerdict.objectql.registerWriteWallVerdict(fn)is new and optional, besideregisterWriteGateProbe, with the typesWriteWallVerdictandWriteWallRowVerdict.validate()asks for verdicts after the defaults:organization_idis spared from the staticreadonlystrip exactly asinsert()/update()spare it, sodroppedFieldsdoes not name it.valid: falsewith the write's codePERMISSION_DENIEDand the write's message.fieldisorganization_idfor a refused value and_recordfor refused caller state. The row runs no strip and reports no drop. Rows stay independent, and the set'svalidstays the conjunction.null: today's strip, unchanged.tenantIdoraccessible_org_ids. NoOperationContextmember is added, no middleware is ridden, and no contract text moves.registerWriteGateProbedocblock is corrected (see Acceptance notes). Thevalidate()docblock names the new behaviour and its limit.content/docs/permissions/system-context.mdxgains row 6c for the verdict's system-context exit, which the system-context census requires.Pins
packages/plugins/organizations/src/validate-preview-wall-verdict.test.ts(10)ObjectQL,SqliteWasmDriver,OrganizationsPluginandSecurityPlugin. Each case runs the write and the preview on the same row. Cell (a):groupsister andisolatedown organization, stored as sent and no drop in the preview. Cell (b):isolatedforeign andgroupoutside the membership set, write 403 and previewvalid: falsewith the write's code and message underorganization_id. Caller state under_record: ADR-0123 D2, #24784, #24821. Controls:singleposture, a non-tenant object (tenancy.enabled: false), and nothing registered (security: false)packages/rest/src/import-dryrun-tenant-wall-parity.test.ts(4)POST /api/v1/data/:object/import, the same rows dry then committed, and the row reports must agree: M5a, M5b, a mixed batch, and thesinglecontrolpackages/plugins/plugin-security/src/write-wall-verdict-equivalence.test.ts(21)isolated/group/single, member / orgless / group-without-active / operator, insert / by-id update. The real middleware, with the installed seam evaluated by the terminal, gives the same admit / refuse /judgesOrganization/ field aswriteWallVerdict. Plus a bulk case and thenullexitspackages/objectql/src/validate-write-wall-verdict.test.ts(14)null= today's strip, admitted judged and unjudged, an authoredreadonlyorganization_idkeeps its strip, a refused row mid-batch,_record, asked once after defaults with no hook run, asked again for a stripped row, update mode, and a wrong-length or throwing verdict rejectsengine-insert-wall-judged-organization.test.tskeeps its assertion; its last title now says "with no write-wall verdict registered", which is what it pins.Ablation (one-off, not kept)
The implementation was committed first (
3d071598). Then the plugin's feature-detect was renamed on disk toregisterWriteWallVerdictAblated, so the seam is never registered, and@objectstack/plugin-securitywas rebuilt.scripts/ablation-dist-preflight.mjsconfirmed the marker was indist.singlecontrol).AssertionError.Restored with
git checkout HEAD --.git diff HEADis empty, the package was rebuilt, and the preflight's--absentleg passed.Verification
All at head
0cc6f290aunless noted. Every heavy command ran underos-verify-lock.validate-write-wall-verdict,engine-insert-wall-judged-organizationandfederated-injected-column-readers32/32; plugin-securitywrite-wall-verdict-equivalence21/21; organizationsvalidate-preview-wall-verdict10/10; restimport-dryrun-tenant-wall-parity4/4. LockVERDICT command-exit 0.@objectstack/plugin-securityfull: 207 files, 4253 passed, 45 skipped, after the extraction. This is the behaviour-preservation check for the moved step 3.7.@objectstack/objectqlfull: 8662/8663 at3d071598. The one red wasfederated-injected-column-readers.test.ts: the newWriteWallRowVerdicttype namesorganization_idat module scope. It got itsnot-a-readrow in0cc6f290and is green at head.typecheckincludingcheck:test-typecheckfor objectql, plugin-security, organizations and rest. All OK, with no new test-typecheck debt (the shrink-only ledger held for objectql; 0 entries for the other three).dispatch-gates.mjs --commandsderives 106 families. All 106 ran, and--ranwith exit codes recorded reads106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN. Every recorded exit is 0. Some needed a rerun for reasons outside this diff:check-engine-split-ratioand thecheck-plugin-teardown-shapeself-test refused on the shallow clone and passed once it was deepened.check:i18n,check:dual-build-cjs-loadsandcheck:skill-examplesrefused for missingdist/and passed after their named build closures.check:query-options-erasureonce raced a concurrent.examples-buildwriter and passed alone.check-system-context-censuswas a real finding, fixed by row 6c.0cc6f290a.!and theClause-②arm iswidening.Acceptance notes
registerWriteGateProbedocblock listed "its ADR-0123 D2 organization wall (step 3.7)" among the arms the probe runs. The probe only asks step 3.7's scope pre-test, with no row. It does not ask the supplied-organization forge guard, and it answerstruefor a foreign organization (measurement M3). The docblock now says so, and points toregisterWriteWallVerdictfor the row judgement. Two questions, two members: the probe's boolean is unchanged.patch, not theminorthe dispatch named. The ruling sets the level "per AGENTS.md Post-Task Checklist step 3", andcheck-changeset-no-majorholds every entry of aMetadata-protocol: nonechangeset topatch. The new member is optional and additive.field: organization_id(or_record). The commit's row is built from a thrown 403, which has no field on it. Code and message are identical on both doors (M5b). The HTTP pin compares everything butfield, and pins the dry run'sfieldon its own.beforeInsert/beforeUpdatehook is judged by the write's stored-image half alone. This is named in thevalidate()docblock.nullexits.writeWallVerdictanswers "not asked" for a system context, a context with no user, and a caller the context prologue refuses before the wall is reached: principal-less, guest without an organization, a subsystem failure, or a missing or non-member delegator. The write refuses those callers before its wall, and the preview runs none of the write's caller gates.validate-preview-wall-verdict.probe.test.tsis deleted (941fa4e1), and its readings are now the pins above. The surface also grew by two ledger rows the repo's own guards require, beyond the claim's list. One issystem-context.mdxrow 6c:check-system-context-censusrefuses an elevation read that no row anchors, andwriteWallVerdictreadsisSystem. The other is afederated-injected-column-readers.test.tsREADERS row for the verdict type'sorganization_idliteral.Generated by Claude Code