Repository navigation
Commit b9dbc3d
feat(plugin-security): under a walled posture, deployment-level acceptance of package defaults is host configuration (acceptedDefaultPermissionSets, ADR-0131 D5) (#23884)
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.com/claude-code)
https://claude.ai/code/session_015jUkR2UVa8FFGk3rWDX49C
---
_Generated by [Claude
Code](https://claude.ai/code/session_015jUkR2UVa8FFGk3rWDX49C)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent abb6df0 commit b9dbc3d
6 files changed
Lines changed: 582 additions & 43 deletions
File tree
- .changeset
- packages/plugins/plugin-security/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
| 106 | + | |
106 | 107 | | |
107 | 108 | | |
108 | 109 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
68 | 71 | | |
69 | 72 | | |
70 | 73 | | |
| |||
907 | 910 | | |
908 | 911 | | |
909 | 912 | | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
910 | 941 | | |
911 | 942 | | |
912 | 943 | | |
| |||
1166 | 1197 | | |
1167 | 1198 | | |
1168 | 1199 | | |
| 1200 | + | |
| 1201 | + | |
1169 | 1202 | | |
1170 | 1203 | | |
1171 | 1204 | | |
| |||
1479 | 1512 | | |
1480 | 1513 | | |
1481 | 1514 | | |
| 1515 | + | |
| 1516 | + | |
| 1517 | + | |
| 1518 | + | |
| 1519 | + | |
| 1520 | + | |
| 1521 | + | |
| 1522 | + | |
| 1523 | + | |
1482 | 1524 | | |
1483 | 1525 | | |
1484 | 1526 | | |
| |||
1895 | 1937 | | |
1896 | 1938 | | |
1897 | 1939 | | |
| 1940 | + | |
| 1941 | + | |
| 1942 | + | |
| 1943 | + | |
1898 | 1944 | | |
1899 | 1945 | | |
1900 | 1946 | | |
| |||
2012 | 2058 | | |
2013 | 2059 | | |
2014 | 2060 | | |
| 2061 | + | |
2015 | 2062 | | |
2016 | 2063 | | |
2017 | 2064 | | |
| |||
4375 | 4422 | | |
4376 | 4423 | | |
4377 | 4424 | | |
4378 | | - | |
| 4425 | + | |
4379 | 4426 | | |
4380 | 4427 | | |
4381 | 4428 | | |
| |||
4471 | 4518 | | |
4472 | 4519 | | |
4473 | 4520 | | |
4474 | | - | |
| 4521 | + | |
4475 | 4522 | | |
4476 | 4523 | | |
4477 | 4524 | | |
| |||
4670 | 4717 | | |
4671 | 4718 | | |
4672 | 4719 | | |
4673 | | - | |
| 4720 | + | |
4674 | 4721 | | |
4675 | 4722 | | |
4676 | | - | |
4677 | | - | |
4678 | | - | |
4679 | | - | |
| 4723 | + | |
| 4724 | + | |
| 4725 | + | |
| 4726 | + | |
| 4727 | + | |
| 4728 | + | |
| 4729 | + | |
4680 | 4730 | | |
4681 | 4731 | | |
4682 | 4732 | | |
| |||
0 commit comments