Skip to content

fix(plugin-security)!: an external object that states no organization scope reads as empty for every caller, the platform owner included - #30146

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/sec-hardening-s
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/sec-hardening-s

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Clause-②: no (narrowing) — under the isolated and group postures, an external object registered after the boot that states no organization scope reads as empty for every caller, the declared platform owner included; nothing is widened, and the single posture is unchanged.
Metadata-protocol: none
Skills-docs: none — the published texts already state this rule for every caller, and this diff changes none of them: the ADR-0087 D3 entry external-object-organization-scope-declared ("every read is denied at Layer 0"), and the once-warning in external-organization-scope.ts. The ADR-0015 §10 amendment that states it is in #29866 (Tier H, still open).

维护者速读

  • 改了什么:isolated / group 姿态下,启动后才注册、且未声明组织范围的外部对象,对每个调用者读取都为空,平台所有者也不例外;平台所有者在其它对象上跨组织墙读取的行为不变,成员与 single 姿态不变。
  • 为什么改:安全频道裁决(2026-11-04,选项 A):未声明组织范围的外部对象是配置缺陷,这一拒绝对已验证的平台所有者同样成立;main 上已有的文案(D3 条目、一次性告警)本就这样写;ADR-0015 §10 的修订在 docs(adr): under a walled tenancy posture every external object states its organization scope (ADR-0015 §10) #29866(Tier H,未合并)。
  • 风险与回滚:风险低,只在一种启动时已按名拒绝的配置上收窄一个主体的读取,不放宽任何东西;回滚即撤销本 PR(一处条件)。
  • 先看:① packages/plugins/plugin-security/src/security-plugin-rls-filter.ts 跨墙分支前新增的条件 deniedForUndeclaredExternalScope;② packages/plugins/plugin-security/src/platform-owner-wall-bypass.test.ts 新增的顶层 describe;③ packages/runtime/src/plugin-driver-external-object.spike.test.ts 的 (h5c)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-security exec vitest run src/platform-owner-wall-bypass.test.ts,预期 26 个用例全部通过。
  • 需要你做的:无,席位落地。

What changes

  • The rule (ADR-0015 §10 as amended). Under a walled posture, the Layer 0 deny an external object earns by stating no organization scope judges the object's configuration, not the reader's organization, so it stands for every caller, the verified platform owner included. In computeLayeredRlsFilter the verified platform owner's read crossing (the OS_PLATFORM_OWNER_EMAIL read door) now stands down on exactly that verdict, and the owner probe is not consulted there. The recorded verdict stays deny.
  • Unchanged. The owner's reads still cross the isolated equality, the group union and the org-less deny, on local objects and on an external object that declares its own organization_id. Writes stay walled for everyone. Members and the single posture are unchanged. No type, verdict ladder or published export moves.
  • Docs in code. The owner-crossing module's header names the one verdict the read crossing does not lift.
  • Changeset. One patch changeset on @objectstack/plugin-security, titled fix(plugin-security)!:, with a BREAKING section (the rule, what is unchanged, who moves and the fix) and the ADR-0087 disposition not-required (already-registered external-object-organization-scope-declared): the existing entry already prescribes the operator's remedy.

Why

Security-channel ruling of 2026-11-04 (option A): the configuration-defect deny on an external object that states no organization scope stands for the verified platform owner too. The texts already on main say so: the D3 entry's "every read is denied at Layer 0", and the once-warning naming "denies every read and write". The ADR-0015 §10 amendment lands separately in #29866.

The declarations, justified

  • Clause-②: no (narrowing) and the !. Measured against the published release, @objectstack/plugin-security@18.3.0 (npm latest): its dist/index.js carries no undeclared-external-scope verdict (0 hits for externalOrganizationScopeUndeclared, against 1 hit for the owner crossing's audit event name as the positive control in the same file). So this diff narrows one principal's reads relative to the published release, on a configuration the boot already refuses by name when the stack declares it. Nothing is widened. It is a runtime security narrowing, not an accept-set widening, so no.
  • Metadata-protocol: none. Nothing an author can write changes; the three declarations are the ones main already accepts. The level follows the kind: patch.

Tests

  • The owner crossing's shared unit harness, a new top-level describe beside the crossing's existing pins (the shared harness's boot gains an optional schema): [ADR-0015 §10] an external object that states no organization scope reads as empty for every caller, the platform owner included, 7 cases.
    • isolated: the verified owner reads the deny sentinel (by identity), the recorded verdict is deny, no crossing is logged, and no sys_user row is read.
    • group: the same.
    • An org-less verified owner reads the deny sentinel on it as well.
    • CONTROL: the owner still crosses the wall on an external object that declares its own organization_id, and a member there keeps the equality.
    • CONTROL: the owner still crosses the wall, and the org-less deny, on a local object.
    • CONTROL: a member is unchanged under isolated and group.
    • CONTROL: single is unchanged for the owner and a member.
  • plugin-driver-external-object.spike.test.ts (h5c), beside (h5)/(h5b), on the real isolated boot: the owner's GET /data/spike_late answers 200 with no row, and no read without the deny sentinel reaches the remote. CONTROL in the same case: the owner's GET /data/spike_walled still crosses the wall and reads every organization's rows.
  • One-shot ablation (not committed): with the new condition replaced by true and plugin-security rebuilt, the 3 owner cases above and (h5c) went red and every other case stayed green; restored and rebuilt, all green. Readings below.

Acceptance notes

  • No out-of-scope finding to note.
  • Pre-fix readings are with the seat.

Verification

All readings are at or source-equal to head 471f235499: packages/ is byte-identical from 111074d2f6 (the fix commit) to 471f235499, and the later commits touch only the changeset and merge one skills-only main commit. Heavy runs went through scripts/pm/os-verify-lock.sh, and each reading is that wrapper's VERDICT line.

Unit: the crossing's pins and the federated Layer 0 pins (34/34)
pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/platform-owner-wall-bypass.test.ts src/federated-tenant-layer0.test.ts
 Test Files  2 passed (2)
      Tests  34 passed (34)
os-verify-lock: VERDICT command-exit 0
Full plugin-security suite and typechecks
pnpm --filter @objectstack/plugin-security test
 Test Files  242 passed (242)
      Tests  5273 passed | 45 skipped (5318)
plugin-security test exit=0
pnpm --filter @objectstack/plugin-security typecheck          exit=0
pnpm --filter @objectstack/runtime typecheck                  exit=0

The first runtime typecheck exited 1 at check:test-typecheck on organization-ownership-boot-guard.turso-remote.kernel.test.ts, a file this diff does not touch. Its import @objectstack/driver-turso is an optional peer that the runtime closure build does not build, so it had no dist/ here. After turbo run build --filter=@objectstack/driver-turso, the same typecheck passed (one && chain, VERDICT command-exit 0).

Spike harness: the real isolated boot (20/20)
pnpm --filter @objectstack/plugin-security build && pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/plugin-driver-external-object.spike.test.ts
 Test Files  1 passed (1)
      Tests  20 passed (20)
os-verify-lock: VERDICT command-exit 0

The spike resolves @objectstack/plugin-security through its dist/, so plugin-security was rebuilt first. The new condition's text is in dist/index.js and dist/index.mjs.

One-shot ablation (not committed)
  • Tool: scripts/ablation-replace.mjs in WRAP mode, under the lock. The anchor is the new condition in security-plugin-rls-filter.ts (1 hit, replaced by true). Blob f1bb2ff4e4 became 099c25dc90.
  • Mutate leg: plugin-security was rebuilt, and ablation-dist-preflight.mjs --absent passed (marker absent from all 6 built files; the source marker was lost).
    • The owner crossing's unit pins: 3 failed | 23 passed (26). The isolated, group and org-less owner cases went red.
    • Spike: 1 failed | 19 passed (20). (h5c) went red.
    • Every control case stayed green.
  • Restore leg: the blob after restore equals HEAD (f1bb2ff4e4), git diff HEAD is empty, and the whole-tree git status --porcelain is clean. After the rebuild, the present-mode preflight passed (marker in 2 built files) and the suites read 26/26 and 20/20.
  • The direction matched the prediction: red.
Derived gate families: 60 derived, 57 run, 3 NOT MEASURED, 0 unrun

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 60 commands off merge base 987ff33b8. Each command ran with its exit captured before any pipe, and --ran reconciled the records:

Run reconciliation — 60 derived, 57 run, 3 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 60 derived famil(ies) accounted for — 57 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3).
  • 56 of the 57 were re-run at 471f235499, all exit 0. Among them, check-adr-0087-registration reads: [BREAKING+bang+clause-②-narrowing] not-required (already-registered).
  • check-comment-mask-corpus (exit 0) is the repo-wide comment-mask sweep. It was taken at 0135956739, whose sources equal 471f235499's.
  • NOT MEASURED, left to CI's Lint & Repo Gates:
    • check:dual-build-cjs-loads: PREREQUISITE NOT MET, because it needs every package's dist/.
    • check:i18n: PREREQUISITE NOT MET, because it needs the workspace CLI build.
    • check:type-check-debt: its closure build never got the shared lock (queue-timeout). The two touched packages' own typechecks, both exit 0, give the narrower reading.
Full dogfood battery (the title carries a bang)
pnpm exec turbo run build --filter='@objectstack/dogfood^...' --concurrency=2 && pnpm --filter @objectstack/dogfood test
 Tasks:    69 successful, 69 total
 Test Files  417 passed | 3 skipped (420)
      Tests  3462 passed | 28 skipped (3490)
os-verify-lock: VERDICT command-exit 0 · held the lock 2613s (43m33s)
  • Not run locally: the test:repo vitest project (Test Core runs it on the PR and in the merge group). Neither changed test file is in a repo project list.

… scope reads as empty for every caller, the platform owner included

Under a walled tenancy posture, the Layer 0 deny an external object earns
by stating no organization scope (ADR-0015 §10) is a verdict on the
object's configuration, not a filter on the reader's organization. The
verified platform owner's read crossing still lifts the organization
filter and the org-less deny everywhere else; it no longer lifts this one,
and the recorded verdict stays `deny`.

Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj
Co-authored-by: Claude <noreply@anthropic.com>
…e reads as empty for every caller

Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj
Co-authored-by: Claude <noreply@anthropic.com>
…eady-registered)

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

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 1 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/permissions/permissions-matrix.mdx (via computeLayeredRlsFilter (symbol, a top-level function))
  • content/docs/permissions/sharing-rules.mdx (via computeLayeredRlsFilter (symbol, a top-level function))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 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 55337f4ce35dbe62434b23517547a0b5052b936c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1b34afcc82cbf730a4298b8e01f32f3705e88a26 — the merge of head 471f23549959eeb65866de78dc14eaf577262e68 into base 55337f4ce35dbe62434b23517547a0b5052b936c, 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 1b34afcc82cbf730a4298b8e01f32f3705e88a26 && git checkout 1b34afcc82cbf730a4298b8e01f32f3705e88a26
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55337f4ce35dbe62434b23517547a0b5052b936c 471f23549959eeb65866de78dc14eaf577262e68 && git checkout -B drift-repro 55337f4ce35dbe62434b23517547a0b5052b936c && git merge --no-ff 471f23549959eeb65866de78dc14eaf577262e68

node scripts/docs-audit/affected-docs.mjs --json 55337f4ce35dbe62434b23517547a0b5052b936c

⚠️ 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 55337f4ce35dbe62434b23517547a0b5052b936c → 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

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 471f235 · 2026-11-04T16:33Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 471f23549959eeb65866de78dc14eaf577262e68
Local-runs: none
Scope: packages/plugins/plugin-security/src/security-plugin-rls-filter.ts, packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts, packages/plugins/plugin-security/src/platform-owner-wall-bypass.test.ts, packages/runtime/src/plugin-driver-external-object.spike.test.ts + packages/core/src/security/tenant-layer0-verdict.ts, packages/plugins/plugin-security/src/tenant-layer.ts, packages/plugins/plugin-security/src/external-organization-scope.ts, packages/plugins/plugin-security/src/security-plugin.ts (getObjectSecurityMeta, the read-filter and write-wall callers), packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts, packages/plugins/plugin-security/src/explain-engine.ts, packages/spec/src/migrations/entries/semantic/18.external-object-organization-scope-declared.ts, .changeset/sec-hardening-g-external-object-organization-scope.md

Inputs: the PR body, its file list, the net diff against the merge base 987ff33b88, the check-runs on the head, and the ruling (maintainer, 2026-11-04, option A). Face: narrowing (Clause-②: no (narrowing) at line start in the PR body and the changeset), so the floor is every path of the diff outside the prose faces; the scope above adds the code the hunks cite.

Check-runs on the head: every check is completed and green or an expected skip; none is still running. Check Changeset is green (the changeset is this PR's own). Lint & Repo Gates is green, which includes the ADR-0087 registration gate re-validating the marker.

① Derived judgments

  1. The stand-down condition catches exactly the undeclared-external deny and nothing else. deniedForUndeclaredExternalScope is verdict.kind === 'deny' && input.externalOrganizationScopeUndeclared === true. Read in computeTenantLayer0Verdict (packages/core): the ladder answers none for the single posture and for a tenancy-disabled object before it reads the flag, and when it does read the flag set it returns deny ahead of every other rung. So whenever the flag is set under a wall the deny is that rung's, and whenever the deny is another rung's (the org-less session under isolated, the empty access set under group) the flag is unset, because externalOrganizationScopeOf answers null for every local object and walled/shared for an external object that declared. The two facts are therefore equivalent to "this deny is the configuration-defect deny", and the condition is exact. Tested on the cases asked:
    • local objects: flag unset, the owner still crosses the equality, the union and the org-less deny (test CONTROL, local object);
    • declared-walled external: walled, flag unset, the owner crosses, a member keeps the equality (test CONTROL);
    • an org-less owner on an undeclared external: the undeclared rung returns before the org-less rung, so the deny is the undeclared one and stands (pinned);
    • group and isolated: pinned; single: none before the flag, the branch is never entered (CONTROL);
    • delegated reads: security-plugin-authz-middleware.ts computes the delegator's own layered filter through the same function, so a delegator who is the owner meets the stand-down too;
    • explain, count and aggregate: explain-engine.ts and security-plugin-explain-access.ts call the same function, and every operation other than insert/update/delete is read-class in it, so count and aggregate take the same branch;
    • by-id reads: getReadFilter goes through computeRlsFilter into the same function; the by-id write pre-image carries the write operation and was walled already.
      isVerifiedPlatformOwnerSession has one consumer in the tree, this branch, so there is no second door for the owner's crossing to lift the verdict elsewhere. The tenant-field-unenforceable declaration is folded into the same flag by externalOrganizationScopeIsUndeclared, so a foreign tenancy.tenantField on an external object meets the same stand-down; that is the rule the boot refusal and the D3 entry already state for it, and the PR's prose ("states no organization scope the wall can enforce") covers it.
  2. The recorded verdict is honest. When the stand-down holds the function returns the computed { layer0: RLS_DENY_FILTER, tenantLayer0Verdict: { kind: 'deny' }, crossedByPlatformAdmin } untouched; crossedByPlatformAdmin is false there because it requires kind === 'none'. The verdict the engine reads and the filter the query carries are the same deny, so a reader learns nothing from a wall that still stands, and no crossing is logged (pinned: no bypass event, no sys_user read). The ADR-0095 D3 platform-admin crossing was already behind the undeclared rung on main (feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope #29867), so this diff makes the owner's crossing consistent with it rather than introducing a new ordering.
  3. The ablation is the right instrument and read the right way: replacing the condition with true reddened the three owner cases and (h5c) and nothing else; restored, all green with a clean tree. The pins fail for the reason the PR claims.
  4. The ADR-0087 marker not-required (already-registered external-object-organization-scope-declared) is right. The entry exists at the merge base and at the head (packages/spec/src/migrations/entries/semantic/18.external-object-organization-scope-declared.ts), which is the category's own test (named id resolves at HEAD and already existed at base). Its replacement prescribes the three declarations, which is the whole remedy here; nothing an author writes changes, so no new entry. The changeset carries exactly one marker, both declaration lines and the BREAKING section with who moves and the fix, as AGENTS.md step 3 requires of a breaking changeset.
  5. §6 holds. Title, body, changeset and every test name state the rule ("reads as empty for every caller, the platform owner included", "the deny sentinel", "no crossing is logged"). No advisory id, no attack wording. The only "bypass" tokens are pre-existing identifiers (platform-owner-wall-bypass.ts, PLATFORM_OWNER_WALL_BYPASS_EVENT) carried over, not new wording.
  6. One prose claim in the PR body is inaccurate, not in the changeset. The body says the texts "already on main" include ADR-0015 §10; the §10 amendment is in docs(adr): under a walled tenancy posture every external object states its organization scope (ADR-0015 §10) #29866 (docs, Tier H), still open and unmerged. The two code texts cited are on main (the D3 entry's "every read is denied at Layer 0"; the once-warning), and the changeset's "ADR-0015 §10 as amended" is the same citation the already-merged feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope #29867 changeset and the D3 entry use. Correction for the body, not a must-fix.

② Semver level

patch on @objectstack/plugin-security: Metadata-protocol: none is true (the three declarations an author writes are the ones main accepts, unchanged), and a non-protocol breaking change ships patch and keeps its BREAKING banner and ADR-0087 disposition (AGENTS.md step 3). Clause-②: no (narrowing) is true: nothing is widened, and one principal's reads narrow. Level, lines and marker agree.

The dev's open question, A or B: A — keep ! and no (narrowing). The arm's reference is the published accept set (clause2-line header: "a diff that NARROWS a published accept set"), and the published release is @objectstack/plugin-security@18.3.0 (tag on main), which carries no undeclared-external verdict; there, the owner's read of such an object crossed the wall. On main after #29867 the #12974 crossing still lifted the undeclared deny for the owner, which is what the three owner cases and (h5c) measure under ablation. So this PR, not #29867, is the diff that takes the owner's published read away; the unreleased #29867 note describes the object's deny without naming the owner, and a note that is pending does not turn a later narrowing into a non-change. Two BREAKING notes in one release, each naming who moves, is the honest CHANGELOG. The battery the ! owes was run at the head's source (417 files, 3462 passed). Under B the changeset would ship a narrowing with no banner, which is the shape the arm exists to prevent.

③ Boundary flags

④ Design judgment

  • security-plugin-rls-filter.ts against the ruling: the stand-down is one condition at the one crossing, keyed on the verdict and the input fact rather than on the object's shape re-derived, so it cannot disagree with the ladder that produced the deny; the owner probe is not consulted, so a stood-down read costs no sys_user row. The recorded verdict stays deny. Holds against the ruling (option A) and against the texts on main that already state the rule for every caller.
  • platform-owner-wall-bypass.ts header: names the one verdict the read crossing does not lift, pointing at where the stand-down lives. Holds.
  • Narrowing against the published spec on main: it does not take a declared capability away from a metadata author; it makes the platform's own refusal (the D3 entry's "every read is denied at Layer 0") real for the one principal it had not reached. The migration is the existing entry's remedy.
  • Tests: the shared harness gains an optional schema instead of a new file, the new describe sits beside the crossing's existing pins, and each case names the rule; the controls (declared-walled, local, member, single) are the cases that make the stand-down's exactness a measurement rather than a reading. The spike's (h5c) checks the deny reaches the remote on the real isolated boot and keeps the declared-walled crossing as its control. The scenario the PR is accepted against is covered.

Implemented-by: claude/sec-hardening-s
Reviewed-by: session_01QSeKYn5CmmdShCJifm6WYj

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

席位核对

ACCEPT · head 471f23549959eeb65866de78dc14eaf577262e68 · 2026-11-04T17:15Z

  • Shape: 5 files, +173/−5. Not governed. Under 3000. Patch for plugin-security, with !, Clause-②: no (narrowing), Metadata-protocol: none, and ADR-0087 not-required (already-registered external-object-organization-scope-declared).
  • Rule held (security-channel ruling of 2026-11-04, option A): under a walled tenancy posture, an external object that states no organization scope reads as empty for every caller, the verified platform owner included. The owner's crossing stands down on that one deny and on nothing else. The recorded verdict stays deny.
  • Contract review: comment 6636959649. Served-tier: CONTRACT_REVIEW_TIER. Head-sha equals this head. VERDICT PASS, no must-fix. The dev's open question is answered A: keep ! and the narrowing arm.
  • Check-runs at this head (latest per name): 15 skipped, 33 success. mergeable_state: clean.
  • Gates: dev ran dispatch-gates --ran: 60 derived, 57 run, 3 NOT MEASURED, all three green in CI's lanes. Full plugin-security suite: 5273 passed. Spike: 20/20. Ablation: red, then restored green. Full dogfood battery at the head's source: 417 files, 3462 tests.
  • Seat edit: the body's "already on main" sentence now says the ADR-0015 §10 amendment lands in docs(adr): under a walled tenancy posture every external object states its organization scope (ADR-0015 §10) #29866 (Tier H, open). This was the review's prose correction.
  • Release note for the maintainer: the next release must carry this PR together with feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope #29867.
  • Landing: ready, then auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 17:17
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 4, 2026 17:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 917f024 Nov 4, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/sec-hardening-s branch November 4, 2026 17:52
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

Development

Successfully merging this pull request may close these issues.

1 participant