You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4d4040c
Browse filesBrowse the repository at this point in the historyBrowse files
docs(permissions): the hand-written pages describe the security catalog main serves, not the four retired row objects (#23322)
Fixes#22976
Clause-②: no (docs)
## What this changes
After C3 stage 8 (#23222) and stage 7b (#23302), `main` serves
positions, permission sets and capabilities as catalog definitions. The
environment registry holds them and answers them by name (ADR-0131 D3),
and assignments name them. Nine of the ten hand-written pages on the
card still described the four retired row objects (`sys_position`,
`sys_permission_set`, `sys_position_permission_set`, `sys_capability`)
as stored tables. Some also described the ADR-0094 projector, the
data-door write-through, binding rows, or the row-object Setup pages as
live. This PR rewrites those passages to say what `main` does. Each
rewrite was checked against source at `main` `404fc477e5`. The tenth
page, `protocol/backward-compatibility.mdx`, has one mention, and that
mention is correct release history, so it is unchanged.
Docs only: 9 files, +149 / −136. There are no edits under
`content/docs/releases/**`, `CHANGELOG.md`, ADRs (the `domain:skills`
card #22978) or the generated reference pages.
## Re-measure on `main` `404fc477e5`
The grep `sys_position|sys_permission_set|sys_capability` over
`content/docs/**`, excluding `releases/**` and `references/**`, matches
exactly the card's 10 pages: authorization 11, permission-sets 6,
delegated-administration 5, capabilities 3, positions 3, system-context
2, and 1 each on the other four. A second grep for the removed
mechanisms (projector, projection, write-through, backfill, walled
catalog data door, `isRowActive`, `SystemObjectName.POSITION`, the
data-door URLs) adds no page. Its hits outside these pages describe
mechanisms that are still live:
- `deployment/cli.mdx`: "mutation projectors". The datasource-admin
plugin still registers one.
- `kernel/cluster.mdx`: the registry write-through,
`applyRegistryWriteThrough`.
- The seed-tenancy, membership and summary-null backfills, which are
unrelated to the catalog.
The two generated reference pages that still mention the objects
(`references/api/protocol.mdx` 3, `references/identity/eval-user.mdx` 2)
render spec `.describe()` text. That text is residue for the 8-post
stage, so those pages are not edited here.
Each mention was judged either a mis-statement or a correct retired
statement. A correct "no row is written" or "retired" sentence is kept,
as the card allows.
| Page | Line on `main` | Verdict | Done |
| :--- | :--- | :--- | :--- |
| `concepts/metadata-lifecycle` | 141 | Mis-statement. It names `POST
/api/v1/data/sys_permission_set` as a clone path and says "the refusal
does not name it yet". | The row now names what the refusal names: the
Clone action or the clone door. |
| `data-modeling/objects` | 336 | Mis-statement. It lists
`sys_position_permission_set` as a member of the `system-data` bucket. |
Only the two assignment tables are listed. |
| `deployment/environment-variables` | 70 | Mis-statement. The
grants-cache watched set lists three retired objects. | Five objects, as
in the code. |
| `permissions/authorization` | 36 | Mis-statement. Assignment is
described as rows of `sys_permission_set` and `sys_position`. | Catalog
definitions, plus the two assignment rows that name them. |
| | 158, 169, 199, 207, 598 | Correct ("no row is written"). | Kept. |
| | 233, 244 (Two doors) | Mis-statement. The projector, the data-door
redirect, the data-layer provenance gate, and delete as reset on the
data door. | Rewritten to the metadata door: `manage_metadata`,
operator-only under a wall, `NOT_OVERRIDABLE` naming the clone. |
| | 415–416 | Mis-statement. "Setup's Deactivate action writes" the
row's `active` column. | Setup's catalog switch writes the activation
ledger. |
| | 607 | Mis-statement. The lint counts a `sys_capability` seed. | A
seed row is not counted. |
| `permissions/capabilities` | 71, 210 | Correct. | Kept. |
| | 215 (callout) | First sentence correct. "Clearing its `active` flag"
implies the row is still served. | The object is retired; the ceremony
handles old rows. |
| `permissions/delegated-administration` | 29, 37, 38, 53–54 |
Mis-statement. The example, the governed-table list, the action-flag
row, and the zoo's binding-write case. | Rewritten. |
| `permissions/permission-sets` | 155 | Correct. | Kept. |
| | 241, 247–248 | Junction row and data-layer anchor binding described
as live. | The junction is retired; `everyone` is derived, and the gate
runs on the derived definition. |
| | 327 (adminScope example) | Mis-statement (`manageBindings`,
`sys_position_permission_set`). | Example fixed. |
| | 344–347 | "Data layer refuses forging", "position bindings" revoked
on uninstall. | What uninstall does on `main`. |
| | 366, 378 (the "record is a projection" section) | Mis-statement. |
Rewritten as "One authoritative store — the definition". |
| | 409, 444 | Data door and record described as live. | Removed. |
| `permissions/positions` | 11, 62 | The junction and
`sys_position.name` described as live. | Reworded. |
| | 85 | Correct. | Kept. |
| | Anchors section | Anchor binding described as an admin act and as a
data-layer gate. | Derived definition; no caller writes either anchor. |
| `permissions/system-context` | 111 (row 11) | The projector skip is
described as live. | Annotated: the plugin no longer registers it; the
module is still exported. |
| | 130 (row 23b) | Correct (a name only a legacy row carries is
refused). | Kept. |
| | 187 (row 55b) | On `main` it still names the data door on the three
retired objects. | **Left to #23231**, whose branch rewrites this row
(see Shared pages). |
| | 410–412 | "Resolves against the granting organization's own catalog
row". | The read targets an unregistered object and gets no rows. |
| `protocol/backward-compatibility` | 75 | Correct release history
(`17.2.0` retired `sys_position.permissions`). | Kept. |
## Sources for the rewrites (`main` `404fc477e5`)
- **Catalog read:** `packages/core/src/security/security-catalog.ts`
(module doc). Positions, permission sets and capabilities are read by
name from the engine registry first, then from the metadata service. No
row is read.
- **Assignments store names:** `sys-user-permission-set.object.ts:109`
(`permission_set`, `Field.text`) and `:86` (the legacy
`permission_set_id`, plain text, which grants nothing).
`sys-user-position.object.ts:84` (`position`). The plugin registers
three objects (`objects/index.ts:12-14`).
- **Setup under `single` and under a wall:**
- `metadata-core/src/meta-write-capability.ts:82` admits a `/meta` write
for `manage_metadata` or `isSystem` only.
- `:169-195` (`walledCatalogWriteVerdict`): under `group` or `isolated`,
only the platform operator or `isSystem` may write the catalog. Under
`single` the verdict is inert (`:182`).
- Setup nav: `plugin-security/src/security-plugin.ts:1543-1545`
(`/apps/setup/metadata/TYPE?scope=environment`).
- Setup's active switch, at the objectui pin `1fb1ced3`:
`app-shell/src/views/metadata-admin/catalog-activation.ts` and
`ResourceListPage.tsx:148`. It writes `POST
/security/_activation/:type/:name`.
- **Packaged sets:**
`metadata-protocol/src/packaged-base-regime.ts:194-199` (the clone
routes the refusal names) and `managedItemSealedSentence`
(`NOT_OVERRIDABLE` / 403). Delete: `protocol.ts:17772`
(`refusePackagedBaseRemoval`). `permission` merges at read, so a delete
removes only a stored row, and the package's definition stays served.
- **Grants cache:**
`core/src/security/resolve-user-grants-cache.ts:117-125` (watched set).
`security-plugin.ts:1850-1866` bumps the `metadata` epoch on every
metadata change.
- **Lint:** `lint/src/validate-capability-references.ts:16-26`. A seed
row is not a source.
- **Delegated-admin gate:**
- `delegated-admin-gate.ts:131-149` (the governed set still names the
two retired objects) and `:396-405` (dispatch).
- `:818`: `manageAssignments` also gates direct grants.
- `:993-1048`: `manageBindings` and `authorEnvironmentSets` still take
part in containment.
- The zoo's binding cases are gone:
`showcase-permission-zoo.dogfood.test.ts:173-177`.
- **Uninstall:**
`plugin-security/src/cleanup-package-permissions.ts:13-25` revokes the
grants that name a declared set.
- **Anchors:** `plugin-security/src/builtin-positions.ts:121` registers
`guest` with no `permissionSets`. `:138` derives `everyone` from the
baseline.
- **Legacy reads:** `objectql/src/engine.ts:10276` (`resolveObjectName`
throws `OBJECT_NOT_FOUND` for an unregistered object).
`plugin-security/src/auto-org-admin-grant.ts:154-176` catches the error
and answers `[]`.
- **Old databases:**
`cli/src/utils/organization-ownership-apply.ts:38-53` and `:399-405`.
Under `single`, authored sets and positions are converted. Authored
capability rows are reported. Seeded copies are deleted.
### Measured against ADR-0131 D2/D3 text
The ADR's original reading was that Setup authoring under `single` needs
no Studio capability (the organization admin's object grants). The ADR's
own text withdraws that reading: the 2026-10-10 note in D2 and D3
(#22621) says Setup authoring under `single` requires `manage_metadata`.
`main` does what the withdrawal says (`meta-write-capability.ts:82`),
and the pages follow `main`.
## Shared pages
- **#23303** (open, draft) edits `system-context.mdx` (it adds row 47c
after row 47) and `backward-compatibility.mdx` (lines 117 and 195, the
package count). This PR touches neither hunk. It does not edit
`backward-compatibility.mdx` at all, and its `system-context.mdx` edits
are rows 11 and the census paragraph near line 410. Whichever PR lands
second merges `main`.
- **#23231** has a branch and no PR yet
(`claude/issue-23231-walled-environment-metadata-writes` at
`e7fdcc48e2`). Its branch edits three of these pages: `capabilities.mdx`
(the walled-refusal paragraph, lines 243–249), `permission-sets.mdx`
(the walled `/packages` sentences, lines 354–360) and
`system-context.mdx` (row 55b). This PR leaves those lines unchanged. On
`permission-sets.mdx`, lines 351–353 sit unchanged between the two
hunks. Row 55b on `main` still names the data door on the three retired
objects. #23231's rewrite already scopes that clause to "while those
objects are registered", so the row is left to it.
## Acceptance notes
- **Not in this card (family residue, noted, not filed):**
- 8-post:
- The spec `.describe()` texts still cite the retired objects:
`permission.zod.ts` `packageId` and `managedBy` ("persisted on
`sys_permission_set` records", `bootstrapDeclaredPermissions`),
`AdminScopeSchema.manageBindings`, and `sys-user-position.object.ts`
`position` ("references `sys_position.name`"). Two generated reference
pages render that text.
- `adminScope.manageBindings` and `authorEnvironmentSets` are
spec-declared keys whose primary gates target objects `main` no longer
registers. They now take part only in strict containment. The showcase
still declares both as `false`. The `permission` liveness row's
`adminScope` evidence still cites the two retired objects.
- 7a: `createPermissionSetWriteThrough` and
`registerPermissionSetProjection` are still exported from
`@objectstack/plugin-security` (`index.ts:79-80`), but nothing registers
them.
- C7b: the legacy-id reads of `sys_permission_set` and `sys_position`
(`auto-org-admin-grant.ts:343-356`, `delegated-admin-gate.ts:1317`,
`:1330`, and the grant-name backfill) throw `OBJECT_NOT_FOUND`. The
errors are caught, so each read answers no rows.
- `delegated-administration.mdx` says "`sys_member` is not one of the
gate's governed objects" (the invitation role cap). `GOVERNED_OBJECTS`
lists it (`delegated-admin-gate.ts:148`). This is unrelated to the
catalog objects, so it is not changed here.
- `permissions/administrator-guide.mdx` is not added to the page list.
It names none of the four objects and none of the removed mechanisms.
Its Setup walkthrough was not checked against the console at the pin.
## Verification
Local runs on head `260137da82`. CI is not complete yet.
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derived 44 commands; all 44 ran and
exited 0. `--ran` reconciles: "44 derived famil(ies) accounted for — 44
run, 0 NOT-MEASURED (a DERIVED zero — all 44 recorded an exit code and
none of them is 3)". Selected verdict lines:
- `check:doc-anchors`: 499 internal fragment links resolve, including
the new `permission-sets#package-suggestion--isdefault-adr-0090-d5`.
- `check:system-context-census`: "OK — 133 elevation read sites … the
page cites 126 symbol(s) against 126 required".
- `check:doc-authoring`, `check:corpus-claim-drift`, `check:role-word`,
`check:overlay-whitelist-table`, `check:docs-transcript-drift`, spec
`check:docs` ("226 generated files in sync") and spec
`check:skill-examples` (263 prose examples type-check): all green.
- Two first runs exited 3 with `PREREQUISITE NOT MET`. Both were re-run
after the lock-held builds of `@objectstack/lint...` and
`@objectstack/client-react...` (`VERDICT command-exit 0`), and both then
exited 0.
- **Links.** `lychee 0.24.2` with `check-links.yml`'s argv (`--offline
--root-dir content --fallback-extensions mdx,md --config lychee.toml`
over the four globs): "2460 Total … 0 Errors". `node
scripts/check-adr-links.mjs`: 730 links resolve.
- **MDX compile.** `@mdx-js/mdx` 3.1.1 with `remark-gfm` compiled all
nine edited pages (9/9). Control: an unclosed `Callout` fails with
"Expected a closing tag".
- **`skip-changeset`.** The diff is only `content/docs/**`. Of the 71
non-private `package.json` manifests, none has a `files[]` entry naming
`content/`. Control: the same scan for `liveness` matches
`packages/spec`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx)_
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/concepts/metadata-lifecycle.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -138,7 +138,7 @@ See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/
138
138
| :--- | :--- |
139
139
|`flow`| Clone it under a new name (`POST /api/v1/automation/:name/clone`, body `{name, label}`), or switch it off (`POST /api/v1/automation/:name/toggle`, body `{enabled: false}`). A flow that holds a credential (an inbound hook's `secret`, an `http` node's `signingSecret`) is not cloned in one step: the clone door refuses it with `409`, because a copy would share the secret, and the copy is authored as a new flow with its own. |
140
140
|`action`| Switch it off (`POST /api/v1/actions/_activation/:object/:action`, body `{enabled: false}`; `:object` is `global` for an object-less action). No clone is offered. |
141
-
|`permission`| Clone it under a new name: the "Clone" action on the permission set, or `POST /api/v1/data/sys_permission_set` with a new name. The clone door `POST /api/v1/security/permission-sets/:name/clone` (body `{name, label}`) copies the whole definition from the security catalog without reading a `sys_permission_set` record; the refusal does not name it yet. |
141
+
|`permission`| Clone it under a new name: the "Clone" action on the permission set, or the clone door `POST /api/v1/security/permission-sets/:name/clone` (body `{name, label}`) with a new name. The clone door copies the whole definition from the security catalog. |
142
142
143
143
A permission set and a position also have a switch, outside the refusal: `POST /api/v1/security/_activation/:type/:name` (`:type` = `permission` or `position`), body `{enabled: false}`, switches one off for the deployment. The refusal does not name it: a packaged position's refusal keeps the managed seal's edit remedy. The audience anchors `everyone` and `guest` are not switchable.
|`OS_DEV_CRYPTO_KEY`| string | — | Development convenience crypto key, consulted after `OS_SECRET_KEY`. Do not use in production. |
68
68
|`OS_CLUSTER_DRIVER`| string |`memory`| Cluster coordination driver id. When set to anything other than `memory`, the runtime treats the deployment as multi-node (and requires `OS_SECRET_KEY`). Non-memory drivers are opt-in sibling packages (e.g. `redis` via `@objectstack/service-cluster-redis`) — see [Cluster](/docs/kernel/cluster). |
69
69
|`OS_REDIS_URL`| url | — | Connection URL passed to a non-memory cluster driver (e.g. `OS_CLUSTER_DRIVER=redis`). |
70
-
| `OS_AUTHZ_GRANTS_CACHE_TTL_MS` | number | `0` | Staleness bound, in milliseconds, for the cross-request authorization grants cache (#11633). `0` (the default) means **off** — a real path, not a degenerate TTL. A non-zero value caches the resolved authorization grants envelope (`resolveUserAuthzGrants` — positions, permission sets, posture, RLS peer ids) per `(user, organization, seed)` on each node. On the writing node the cache is retired **immediately** by any write to a watched authorization object (`sys_member`, `sys_user_position`, `sys_user_permission_set`, `sys_position`, `sys_position_permission_set`, `sys_permission_set`, `sys_user`, `sys_metadata_activation`) and by any `metadata.changed` (a permission set can be declared in metadata); `sys_session` is deliberately not watched, so session-activity writes cannot flush it. Entries additionally expire at the earliest upcoming ADR-0091 validity boundary — `min(ttl, nextBoundary)` — because a `valid_until` passing is a permission change with **no write anywhere**. The permission explainer and `runAs:'user'` automation runs always read uncached. When a value is set with no cross-node invalidation bus — no cluster service, or the in-process `memory` driver, which fans out to nobody — the boot says so loudly, every time: the TTL is then the whole bound on how long this replica may honour a grant another replica revoked. A malformed value is treated as `0` and warned about rather than silently read as "disabled". Deployment config only; it is deliberately not a settings row, because a cached path must not serve the knob that bounds the cache. |
70
+
| `OS_AUTHZ_GRANTS_CACHE_TTL_MS` | number | `0` | Staleness bound, in milliseconds, for the cross-request authorization grants cache (#11633). `0` (the default) means **off** — a real path, not a degenerate TTL. A non-zero value caches the resolved authorization grants envelope (`resolveUserAuthzGrants` — positions, permission sets, posture, RLS peer ids) per `(user, organization, seed)` on each node. On the writing node the cache is retired **immediately** by any write to a watched authorization object (`sys_member`, `sys_user_position`, `sys_user_permission_set`, `sys_user`, `sys_metadata_activation`) and by any `metadata.changed` (positions and permission sets are catalog definitions, so a change to one is a metadata change); `sys_session` is deliberately not watched, so session-activity writes cannot flush it. Entries additionally expire at the earliest upcoming ADR-0091 validity boundary — `min(ttl, nextBoundary)` — because a `valid_until` passing is a permission change with **no write anywhere**. The permission explainer and `runAs:'user'` automation runs always read uncached. When a value is set with no cross-node invalidation bus — no cluster service, or the in-process `memory` driver, which fans out to nobody — the boot says so loudly, every time: the TTL is then the whole bound on how long this replica may honour a grant another replica revoked. A malformed value is treated as `0` and warned about rather than silently read as "disabled". Deployment config only; it is deliberately not a settings row, because a cached path must not serve the knob that bounds the cache. |
0 commit comments