Skip to content

feat(plugin-security): under a walled posture, deployment-level acceptance of package defaults is host configuration (acceptedDefaultPermissionSets, ADR-0131 D5) - #23884

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23385-walled-accepted-defaults
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23385-walled-accepted-defaults

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23385
Clause-②: yes

The code half of #23385. Under a walled posture, a host can declare the deployment-level acceptance of package defaults as configuration: SecurityPluginOptions.acceptedDefaultPermissionSets, under ADR-0131 D5. The walled refusal now names that option instead of "create the Default Organization". The card's third item, the ADR-0131 amendment line, is #23883 (Tier H, the maintainer lands it). #23385 remains open until that PR lands, and this PR does not close it.

Authority: objectstack-ai/cloud#2872 ruling A′ (6160165869, maintainer 「同意」), item 3, as triage quoted and verified it in 6205580248.

What changed

packages/plugins/plugin-security:

  • SecurityPluginOptions.acceptedDefaultPermissionSets?: readonly string[] is new. It is read only under a walled posture, using the module's existing postureEnforcesWall predicate on the posture the plugin already resolves in start(). There is no second reading of the posture. When it is declared, possibly as [], three things follow:
    • readAcceptedEveryoneSets derives everyone from the declared names. Each name goes through the same per-set checks an accepted row goes through: the catalog serves it under a package, it declares isDefault: true, and describeAnchorForbiddenBits passes (ADR-0090 D7). A name that fails is withheld with its reason, at error for a forbidden bit, as for a row.
    • syncAudienceBindingSuggestions reads and writes nothing, so the boot has no throw and the two (non-fatal) warn sites stay silent. reconcileAudienceBindingSuggestions still counts existing rows once, now as "not read" and naming the option.
    • The list, accept and dismiss doors answer 409 SUGGESTION_STATE naming the option. The deployment authority still answers first, so a non-operator gets 403.
  • Walled, undeclared: behaviour is unchanged. With no organization to own the record, the refusal now reads "…Under a walled posture the deployment-level acceptance is host configuration (ADR-0131 D5), never a row a reserved organization owns (ADR-0131 §1.5). Remedy: declare … acceptedDefaultPermissionSets …".
  • single: unchanged, including NO_RECORD_OWNER_MESSAGE's text. Every pre-existing call passes no DeploymentAcceptance, which reads as the record.
  • Refused loudly. A malformed value (not a list of non-empty names) is refused at construction (TypeError). A declaration under single is refused at start(), the first moment the posture is known. single would never read it, so it would only look enforced.
  • Exported widening, all optional or additive: a trailing deployment?: DeploymentAcceptance on readAcceptedEveryoneSets, syncAudienceBindingSuggestions and reconcileAudienceBindingSuggestions; SuggestionDeps.acceptedDefaultPermissionSets?; the DeploymentAcceptance type; and WithheldAcceptance.source.
  • Changeset: one minor for @objectstack/plugin-security, Clause-②: yes (widening).

Not touched: no NULL-organization_id row, no new table, no packages/spec file, not builtin-positions.ts.

Which cases read the declared list: measured, then decided

resolveDefaultOrganizationId returns the default slug, else the only organization, else nothing. Today, on a walled deployment:

organizations today undeclared (this PR) declared (this PR)
a default slug exists record owned by it unchanged the declaration; record not read
exactly one, no default record owned by that tenant unchanged the declaration; record not read
two or more, no default 409 / boot warn, "create the Default Organization" 409 / boot warn, names the option the declaration; no warn

Call: a declaration under a wall is the only source, whatever the organization count. The four-axis reason:

  • ① Need: cloud's walled control plane falls in row 3 (the audit cloud#2872 6159344518: 48 and 12 organizations, no default). Reading the list only in row 3 would leave a host's declaration ignored the day a default-slug organization appears.
  • ② Long term: D5 says platform standing under a wall is configuration, and §1.5 rejects an organization owning deployment-level rows. Under a wall, then, a declaration should replace the record, not sit beside it. Two sources would need a merge rule that nothing in the record governs.
  • ③ AI-proof: "declared ⇒ enforced, always" leaves no posture × organization-count matrix to get wrong. A declaration nothing reads (under single) or a malformed one is refused at boot or construction, never skipped.
  • ④ Scope: no new state. A walled host that declares nothing keeps today's behaviour byte for byte, apart from the message.

Tests (at 70b3cb0927)

  • pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 over the whole package: 187 files passed, 3725 tests passed, 45 skipped. pnpm --filter @objectstack/plugin-security typecheck: exit 0.
  • New pins, one per acceptance line of the card, in the [ADR-0131 D5] the walled posture … block of suggested-audience-bindings.test.ts (8 tests) and in walled-accepted-defaults.boot.test.ts (3 tests, booting a real ObjectKernel):
    • walled, no default, declared []: the reconcile resolves (no throw, so the plugin's warn sites log nothing), the captured warns are [], everyone is the baseline, and accept, dismiss and list answer SUGGESTION_STATE/409 with a message whose first sentence is pinned and which names the option;
    • a declared set is granted through everyone (absent before), with no row written;
    • a declared high-privilege set is withheld forbidden: true and never granted; an unknown, non-isDefault or package-less name is withheld with its reason;
    • declared where a default organization holds an accepted row: that row is not read;
    • walled, undeclared: 409 with the new message (first sentence pinned, names the option and §1.5, no "create the Default Organization"), identical at the accept door;
    • single, both undeclared and with a declaration passed to the module: the message keeps "Remedy: create the Default Organization (slug 'default')";
    • the option's shape refusals, and the single-posture boot refusal with its controls (single undeclared boots; walled declared boots).
  • The single-posture suggestion tests already in the file are byte-for-byte unchanged and green. The new block is appended.

Ablation (node scripts/ablation-replace.mjs; the tests import src/ relatively, so no dist/ leg; both restores verified blob == HEAD and git diff HEAD empty):

  1. hostDeclaredAcceptance returning undefined (the option's read removed): 5 red. They are declared [] (no warn, everyone = baseline, the door refusal); the declared set granted; the forbidden set withheld; unserved names withheld; and the record not read when declared. The other 47 stayed green. That includes the undeclared-walled message, the single pins and the shape refusals, which do not depend on the read.
  2. noRecordOwnerMessage always returning NO_RECORD_OWNER_MESSAGE: 1 red, the walled-undeclared message pin.

Gates (local, at 70b3cb0927)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 67 families from the 6 changed paths (625 changed lines). All 67 ran, with exit codes captured before any pipe, and all exited 0. --ran reconciliation: "67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero — all 67 recorded an exit code and none of them is 3)."

On the first pass, three families exited 3 (prerequisite not met) and one hit my 300 s wrapper. Each was then re-run to a verdict:

  • check-plugin-teardown-shape --self-test needed an unshallow clone;
  • check:type-check-debt ran to exit 0 under the lock in 114 s, and it built every package;
  • check:dual-build-cjs-loads and check:i18n exited 0 after that build.

Acceptance notes

  • The posture reading reused: resolveTenancyPosture() (packages/types/src/env.ts:147) is the deployment's env reading, which plugin-auth's tenancy service is built from. The security plugin already reads that service once in start() (this.tenancyPosture). This PR reads that resolved value through postureEnforcesWall (packages/spec/src/security/tenancy-posture.ts:53), the predicate assertDeploymentAuthority in this module already uses. It does not call resolveTenancyPosture() again, which would be a second reading beside the service.
  • When declared, the list door also refuses with 409. The card names only the acceptance door. With no record, a list would either be empty and hide the declared isDefault sets, or show prompts no door can act on.
  • WithheldAcceptance.source is a new required field on an exported, output-only type. Only this package constructs the type.
  • Observation, not filed: under a wall with exactly one organization, today's record is owned by that tenant. Once a second organization exists, the record's rows count as "not read" (reported by the existing warn) and its acceptances stop granting. A host that declares the option is unaffected.
  • The cloud adoption (dropping the boot-log warn check ruling A′ added to cloud#2709) is cloud's, at a later pin.

🤖 Generated with Claude Code

https://claude.ai/code/session_015jUkR2UVa8FFGk3rWDX49C


Generated by Claude Code

@github-actions github-actions Bot added size/l 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-security, touching 27 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via security.suggestedBindings.confirm (sdk, the route ledger binds it to POST /api/v1/security/suggested-bindings/:id/confirm, selected by route anchor /security/suggested-bindings/:id/confirm; the route ledger binds it to POST /security/suggested-bindings/:id/confirm))
  • content/docs/permissions/field-level-security.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/index.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/permission-sets.mdx (via /security/suggested-bindings/:id/confirm (route, bridged from symbol confirmAudienceBindingSuggestion — its route source's handler names it), /security/suggested-bindings/:id/dismiss (route, bridged from symbol dismissAudienceBindingSuggestion — its route source's handler names it))
  • content/docs/plugins/packages.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/ui/forms.mdx (via SecurityPlugin (symbol, a top-level class))

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

  • content/docs/releases/implementation-status.mdx (via SecurityPlugin (symbol, a top-level class))

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
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 15 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 94114c1f269f6b3d885f24b6f4689dce29504574 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5372a4a3360de8b84c015f4b253c73db04b95f0a — the merge of head 70b3cb0927cd817a09c947928dc33ed222e28e8e into base 94114c1f269f6b3d885f24b6f4689dce29504574, 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 5372a4a3360de8b84c015f4b253c73db04b95f0a && git checkout 5372a4a3360de8b84c015f4b253c73db04b95f0a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 94114c1f269f6b3d885f24b6f4689dce29504574 70b3cb0927cd817a09c947928dc33ed222e28e8e && git checkout -B drift-repro 94114c1f269f6b3d885f24b6f4689dce29504574 && git merge --no-ff 70b3cb0927cd817a09c947928dc33ed222e28e8e

node scripts/docs-audit/affected-docs.mjs --json 94114c1f269f6b3d885f24b6f4689dce29504574

⚠️ 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 94114c1f269f6b3d885f24b6f4689dce29504574 → 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

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 70b3cb0927cd817a09c947928dc33ed222e28e8e
Local-runs: none
Scope: packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/suggested-audience-bindings.ts, packages/plugins/plugin-security/src/index.ts + packages/spec/src/security/tenancy-posture.ts (postureEnforcesWall, normalizeTenancyPosture), packages/spec/src/security/high-privilege.ts (describeAnchorForbiddenBits, describeHighPrivilegeBits), packages/core/src/security/security-catalog.ts (the reader's packageId and disabled-package rule), packages/plugins/plugin-security/src/builtin-positions.ts (declareEveryoneBaseline), packages/plugins/plugin-security/src/app-default-permission-set.ts (appSecurityPluginOptions), packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts, packages/plugins/plugin-security/src/walled-accepted-defaults.boot.test.ts, .changeset/23385-walled-accepted-defaults.md, docs/adr/0131 (D3, D5, D7, D11, section 1.5), docs/adr/0090 (D5, D7, Alternatives 3)

Inputs: card #23385 (body and all seven comments, the ruling as triage quoted and verified it in 6205580248), this PR's body, file list and net diff against main at this head (six files, +582/-43; the three-dot and two-dot diffs of those paths against current main are identical, and no commit on main since the merge-base 34b740f touches plugin-security, ADR-0131, tenancy-posture, high-privilege or the security catalog), and the 40 check-runs on this head. Nothing was built, run or re-run.

① Derived judgments

  1. SecurityPluginOptions.acceptedDefaultPermissionSets?: readonly string[] — a new optional member of the options interface reached through the package entry by SecurityPlugin's constructor: a widening. RIGHT. Read once at construction by readAcceptedDefaultPermissionSetsOption: undefined is undeclared; a list of non-empty strings is deduplicated and frozen; anything else (null, a bare string, an empty or non-string entry) is a TypeError at construction, never read as "nothing accepted". RIGHT. Nit, not a defect: names are not trimmed, so a padded name is later withheld as unserved — loud, not silent.

  2. Read only under the walled posture. hostDeclaredAcceptance returns the list only when deployment.posture is set and postureEnforcesWall(posture) holds (posture !== 'single', tenancy-posture.ts). The posture is the plugin's one resolution in start() (tenancy service through normalizeTenancyPosture, else the org-scoping probe, else single), handed on through deploymentAcceptance() and SuggestionDeps.posture(); no second reading. RIGHT. A declaration under single is refused at start(), directly after that resolution, by refuseAcceptedDefaultsWithoutWall: the plugin's start throws and the kernel rolls back (boot pin: "failed to start - rollback complete"). RIGHT. Limit, stated not failed: start()'s two degraded early returns (no ObjectQL or metadata service; an engine without middleware) sit above the refusal, so on such a boot a declaration is neither read nor refused — that boot already warns that security middleware is not registered, and nothing derives everyone from the option there.

  3. The grant path, traced on every derivation. readAcceptedEveryoneSets(ql, metadata, deployment): declared, the candidates are the declared names and the record is not read; otherwise the pre-existing row path (Default Organization, confirmed, deployment_decision). Each candidate then passes the same four checks a row passes, in order: the catalog serves the name (securityCatalogReaderOf(ql).resolve('permission', set), which does not serve a disabled package's item); the serving entry names a package (entry.packageId, the binding's _packageId — for a row it must equal the accepting package, for a declared name it must exist, ADR-0086 D3); isDefault: true; and describeAnchorForbiddenBits(definition, 'everyone', capabilities) (ADR-0090 D7: a system permission not excused by a declared capability, View or Modify All, delete or transfer, export). A failing name is withheld with its reason and never granted, forbidden: true for the D7 case. declareEveryone runs at kernel:ready, on every landed permission mutation, on package-uninstall cleanup and from the doors' rederiveEveryone, so a catalog change after boot (a set gains a forbidden bit, loses isDefault, its package is disabled or uninstalled) drops the grant at the next derivation, exactly as for an accepted row. RIGHT. Answer to the gate question: no declared value can put a high-privilege set, an unserved set, a package-less set or a non-isDefault set on everyone; [] grants nothing beyond the baseline, and an empty array is "declared" on every branch (sync, reconcile, derivation, all three doors — each checked).

  4. Withheld and logged at error, not refused at boot. Judged RIGHT under "declared means enforced". The direction is fail-closed: the gate refuses the grant, and what goes unenforced is only the host's over-claim, which declareEveryone reports on every derivation — error for a forbidden bit, warn for the three other reasons — naming the set, the reason and the remedy (remove the name, or serve the set again as some package's isDefault set). That is the level AGENTS.md's degradation rule assigns to "looks normal while a claim did not land". Refusing the boot instead would make a post-boot package change a boot-breaker and would refuse a name whose package is not installed yet; the row path chose withhold-and-report for the same reasons, and the two paths now behave alike. The path a misconfigured host takes: construction accepts well-formed names; boot proceeds; everyone is the baseline plus the names that pass; every failing name is reported at every derivation; the three doors answer 409 naming the option; nothing silently grants. What the option lacks relative to the accept door is an acceptance-time 403 for a forbidden set; the error line on every derivation is the substitute, and the module pin and the boot pin cover it.

  5. The sync and the reconcile. syncAudienceBindingSuggestions returns zero before any read or write when declared; reconcileAudienceBindingSuggestions skips the owner read, counts every existing row as not read and warns once per pass naming the option (rows kept, never deleted); the plugin's two (non-fatal) warn sites stay silent because nothing throws. RIGHT. Side effect recorded under ③: the early return is above collectDeclaredSuggestions, whose once-per-engine dropped-manifest-permissions report therefore does not run on a declared wall.

  6. The doors. List, accept and dismiss all check the deployment authority first (a non-operator under a wall is 403, unchanged) and then refuse with 409 SUGGESTION_STATE and hostDeclaredMessage when declared (requireRecordOwner for accept and dismiss, an explicit check in list). Consistent. RIGHT. Walled and undeclared with no organization to own the record: list throws noRecordOwnerMessage from the sync when a package declares any isDefault set, as today; accept and dismiss answer 409 with the same text; the boot reconcile's (non-fatal) warn carries it. Behaviour unchanged, message new: it names the option, ADR-0131 D5 and section 1.5, says "an empty list when none is accepted", and no longer says "create the Default Organization". RIGHT. The one-organization-no-default case is unchanged in both arms, as the PR's table states. Carried-over wording nit: "there is more than one organization" is also what a deployment with zero organizations is told (resolveDefaultOrganizationId answers undefined for both); the old message said the same.

  7. single is byte-for-byte unchanged. NO_RECORD_OWNER_MESSAGE is untouched (context lines in the diff); noRecordOwnerMessage returns it under single and under a missing posture; hostDeclaredAcceptance returns undefined there; the record path inside readAcceptedEveryoneSets is a refactor into candidates whose four checks, reason strings and packageId are the same as before, plus the new source: 'record'. The pre-existing tests in suggested-audience-bindings.test.ts are byte-unchanged (the diff adds three import names and one appended block) and green on this head. RIGHT.

  8. Exported widening. A trailing optional deployment?: DeploymentAcceptance on readAcceptedEveryoneSets, syncAudienceBindingSuggestions and reconcileAudienceBindingSuggestions — additive; every pre-existing caller (the plugin's four sites and two tests inside the package; none outside the package on main, none in objectui) still compiles and reads the record. SuggestionDeps.acceptedDefaultPermissionSets? — additive. DeploymentAcceptance newly re-exported from index.ts — needed, since the public option and the widened signatures name it. readAcceptedDefaultPermissionSetsOption and refuseAcceptedDefaultsWithoutWall are module exports not re-exported from index.ts: package-internal, not on the published surface. All RIGHT for Clause-②: yes (widening).

  9. WithheldAcceptance.source: 'record' | 'host-configuration' as a REQUIRED field. The type is the output of readAcceptedEveryoneSets and is accepted as input by no exported function, so the package's accept set narrows nowhere; readers gain a field the producer always sets. Constructors: on main the only one is the producer's own withheld.push (repo-wide read; metadata-core's withheld.push(name) is another type), and objectui has none; an out-of-tree test double would need the new field — the only sense in which this could be called a narrowing, and not an accept-set one. RIGHT as required; optional would make every reader handle undefined for a value that is never absent.

  10. The plugin's withheld log line branches on w.source: the host-configuration line names the option and its remedy; error for forbidden, warn otherwise — the existing level policy. RIGHT.

  11. Reach. The option is reachable only by a host that constructs SecurityPlugin itself (cloud's control plane, embeddings). The CLI host (serve.ts, schema-migration-plugins.ts) constructs from appSecurityPluginOptions(config), which returns { fallbackPermissionSet } or nothing; there is no objectstack.config.ts key, so nothing an author writes is silently inert. RIGHT for this card (cloud adopts at a later pin); a CLI-hosted walled deployment cannot declare it until that host passes it through — a follow-up, not a defect of this diff.

  12. The card's prohibitions. No NULL-organization_id row (the declared path writes nothing; the undeclared path is unchanged), no new table, no packages/spec file, builtin-positions.ts untouched. RIGHT.

② Semver level

.changeset/23385-walled-accepted-defaults.md: '@objectstack/plugin-security': minor, Clause-②: yes (widening). It matches what the diff publishes — one new optional option, one new exported type, three trailing optional parameters, one optional SuggestionDeps member, one required field on an output-only type; nothing removed, renamed or narrowed in the accept set, so no ADR-0087 marker is owed. Each claim in its body (what was wrong, what is new, refused loudly, the exported widening) is true of the diff. Check Changeset on this head: success. RIGHT: minor.

③ Boundary flags

Dev deviations (os-dev-report 6214518188):

Open questions, each answered:

  1. Declared ⇒ sole source whatever the organization count — A, agreed. Under a wall D5 and section 1.5 put deployment standing in configuration; two sources would need a merge rule nothing governs; "declared means enforced" leaves no posture-by-count matrix; the undeclared host is unchanged. The one consequence (rows an organization accepted earlier are reported once as not read, naming the option) is pinned.
  2. The list door refusing with 409 when declared — A, agreed. An empty list would hide the declared isDefault sets and show prompts no door can act on; the refusal names the option.
  3. WithheldAcceptance.source required — A, confirmed (item 9 above).
  4. Where PR docs(adr-0131): amend D5 — under a walled posture, deployment-level acceptance of package defaults is host configuration #23883's line sits — answered on that PR's record.
  5. The posture reading — A, agreed: the plugin's resolved tenancyPosture through postureEnforcesWall; a second resolveTenancyPosture() call would be the second reading the card forbids.

Out-of-scope observation (one organization and no default slug under a wall): an observation with no failing public door; it stays unfiled under Prime Directive #10. Agreed.

Added by this review:

  • ADR-0090 D5 and its Alternatives 3 rejected an environment-level defaultPermissionSets setting as a second distribution channel outside the position system. This option is not that: the grant still flows through the everyone position's derived definition, so the explain path and the position audit see it; what the ruling moves under a wall is where the acceptance is recorded (configuration instead of a resolved_by row). Not a contradiction. PR docs(adr-0131): amend D5 — under a walled posture, deployment-level acceptance of package defaults is host configuration #23883's line could name that rejected alternative to pre-empt the reading — optional.
  • The sync's early return skips collectDeclaredSuggestions on a declared wall, so its once-per-engine report of a package whose manifest carries permissions in the ADR-0025 reading does not run there; a host that names such a set still sees it withheld as unserved. A diagnostic loss only; worth carrying into the follow-up that passes the option through the CLI host, if one is filed.
  • The Served-tier: token: the dispatch asked for a contract-tier read of both PRs; the face rule (reviewFace over this PR's file list: the declaration face, no published schema and no governed path) fixes the token to TIER_DEFAULT, exact and not a floor, so that is what this record carries. The reading covered every file on the Scope: line.
  • Check-runs on this head: all 40 completed; every gate-carrying job is success (Lint & Repo Gates, TypeScript Type Check, Test Core 1 to 6 and the aggregate, Build Core, Governed Surface Queue Guard, Check Changeset, Check PR Size, both single-writer guards, the Part-of and claim guards, Check Documentation Links); the skips (Build Docs, Console Pin Gate, the Dogfood shards and Verify CLI, Packed-tarball smoke, Temporal Conformance) are the expected-skips roster's PR-run skips. No local run.
  • Landing: not a governed PR; it lands by the owning seat through the queue once ready, with this record as its Clause-②: yes review of record. The needs:contract-review label is not on the PR, so nothing is removed. The card stays open until docs(adr-0131): amend D5 — under a walled posture, deployment-level acceptance of package defaults is host configuration #23883 lands (Part of #23385).

Implemented-by: claude/issue-23385-walled-accepted-defaults
Reviewed-by: session_01HjR8RUwTPQ3TgbfKihmxp9

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #23884 @ 70b3cb0927 (#23385, PR A) · domain:spec seat 1 · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T12:34Z

Reviewer of record. Every item below was checked against GitHub and the code, not against the report (6214518188 on #23385).

  • Shape: Draft, base main. The first line is Part of #23385, with no closing keyword, then Clause-②: yes, matching claim 6213732282. 6 files, +582/−43, none governed. Assignee os-elon-musk.
  • The option: SecurityPluginOptions.acceptedDefaultPermissionSets?: readonly string[].
    • It is read once at construction; a malformed value is a TypeError. It is read only when postureEnforcesWall(posture) holds, on the plugin's single posture resolution.
    • Declared under single, it is refused at start() and the kernel rolls back.
    • single's record path and NO_RECORD_OWNER_MESSAGE are unchanged.
  • The grant path: a declared name passes the four checks an accepted row passes, on every derivation: the catalog serves it, a package owns it, it is isDefault, and describeAnchorForbiddenBits finds nothing. A failing name is withheld and logged, at error for a forbidden bit. The seat's dispatch had leaned towards a boot refusal. The review shows why per-derivation is the right shape: the row path behaves the same way, and the catalog can change after boot, so a boot-time check would not hold.
  • The doors: declared, list / accept / dismiss answer 409 SUGGESTION_STATE naming the option. Undeclared under a wall, today's behaviour stands, with a message that names the option and ADR-0131 §1.5 instead of "create the Default Organization".
  • Contract review: PASS 6215570234 on this head. It is served at TIER_DEFAULT, the face the recogniser derives for a Clause-②: yes declaration with no schema or governed path. The reading covered every file on its Scope line, and found nothing that grants silently.
  • The report's five open questions, the seat's calls: all A, as implemented and as the review confirmed:
    1. declared is the sole source at every organization count;
    2. the list door refuses too;
    3. WithheldAcceptance.source is required;
    4. the ADR line sits at the end of D5;
    5. the posture is the plugin's own resolution.
  • Deviations accepted: src/index.ts exports the DeploymentAcceptance type the widened signatures take. The boot test is a new file in the same package.
  • CI on this head: 34 success, 6 skipped by their filters, 0 failed. main touched none of the reviewed surfaces since the base.

PR B (#23883, the ADR-0131 line, Tier H) lands after this PR or with it.


Generated by Claude Code

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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants