Skip to content

Commit a832564

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15179-remedy-authority-scripts-pm
2 parents bf3cf68 + 919beca commit a832564

31 files changed

Lines changed: 3653 additions & 129 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@objectstack/rest': minor
3+
---
4+
5+
REST no longer reads a FAILED authorization-input lookup as "this check does not apply" — the tenancy-posture and ADR-0069 auth-gate seams in `computeExecCtx` fail closed
6+
7+
Two seams inside `RestServer.computeExecCtx` absorbed a FAILURE into the same `undefined` an
8+
ABSENT wiring produces, and both feed authorization inputs. Unlike the sibling repairs in this
9+
family — where an unknown was answered as a REFUSAL — these two pointed the other way: a failure
10+
read as *permissive*, so a refusal was SKIPPED rather than produced. Driven on a real
11+
`ObjectKernel`, each fault beside a positive control that is the same fixture with the one fault
12+
removed:
13+
14+
| wiring | before | after |
15+
|:--|:--|:--|
16+
| healthy `isolated` tenancy, ex-member's org-stamped API key | 401 refused | 401 — unchanged |
17+
| `tenancy` never registered (supported no-tenancy composition) | 200 served | 200 — unchanged |
18+
| `tenancy` registered and FAILED to construct | **200 served, full grants** | **503** |
19+
| auth gate INACTIVE | admitted | admitted — unchanged |
20+
| auth gate ACTIVE, healthy re-read, gated user | 403 | 403 — unchanged |
21+
| `isAuthGateActive()` itself THROWS | admitted | admitted — unchanged |
22+
| auth gate ACTIVE, session re-read FAILS | **admitted, no wire trace** | **503** |
23+
24+
- **Tenancy posture.** Only the service registry's *branded* "never registered" rejection is
25+
absorbed — the `isServiceNotRegisteredError` discriminator the shipped `objectQLProvider`
26+
already uses one layer down. Every other rejection (a factory that threw, a scoped registration
27+
resolved without a scope id, a circular service dependency) raises the same loud
28+
`AuthzStoreUnavailableError` the data-engine seam raises, so the door answers a server-side
29+
outage instead of serving the request. The classification is the registry's, never message text.
30+
The WIRING fact is taken from the kernel's presence and never inferred from what the read
31+
returned.
32+
- **ADR-0069 auth gate.** Fails closed in one precisely measured window only: `isAuthGateActive()`
33+
answered `true` **and** the gate's session re-read then failed. A gate the deployment declared
34+
active no longer vanishes silently. The common inactive path, a probe that throws, and a
35+
successful re-read carrying no gate all keep their existing behaviour.
36+
37+
Boot behaviour is unchanged: no composition that starts today stops starting. Single-kernel REST
38+
deployments — the wiring with no `kernel-manager` service — keep their current behaviour exactly,
39+
including the fact that `computeExecCtx` reads no tenancy posture there. What that wiring actually
40+
skips is being measured separately and is not changed here.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/cli": patch
4+
---
5+
6+
fix(objectql,cli): a navigation contribution relocated past a missing group now says so, at `warn` and at build time
7+
8+
A package that injects navigation into another package's app names the target
9+
container by id (`navigationContributions[].group`). When that id matches no
10+
`type: "group"` node in the target app, `SchemaRegistry.applyNavContributions`
11+
appends the items at the app's **top level** and continues.
12+
13+
That relocation is unchanged, deliberately. The merge is a read-time fold
14+
precisely so registration order does not matter — `registerAppNavContribution`
15+
does not require the target app to exist yet — and a package contributing into
16+
an *optional* group has to keep working. Refusing would trade both away.
17+
18+
What changes is that it is no longer invisible. The only trace used to be one
19+
log line gated at `info`/`debug`, so a deployment running at
20+
`OS_REGISTRY_LOG=warn` watched its information architecture change in complete
21+
silence: a typo'd group id — exactly what an AI author emits — turned a nested
22+
menu entry into a top-level one, and because the entry was still *present*, no
23+
smoke test noticed. That is worse than a dropped entry, which someone notices.
24+
25+
The trace is now a real diagnostic naming the contributing package, the target
26+
app, the missing group id and the relocated items:
27+
28+
- **At runtime.** An ADR-0038 `BuildIssue`-family record (ADR-0112 D6c — a
29+
diagnostics code, lowercase and out of the error ledger) is carried on the
30+
app and reachable as `registry.getAppNavDiagnostics(appName)`, and announced
31+
through `console.warn`, so it survives `OS_REGISTRY_LOG=warn` and reaches
32+
`os doctor` / boot output. Emitted once per registry per distinct mis-aim:
33+
the fold runs on every read of the app, and a line printed per request is as
34+
unreadable as one never printed. A deployment that asks for `silent` still
35+
gets silence, and still keeps the record.
36+
- **At authoring time.** `os build` and `os validate` answer the same question
37+
over a composed artifact, through the same predicate, and report the same
38+
finding where an author sees it first — in the text output and in `--json`
39+
under the existing `warnings` key, beside the authoring-rule advisories and
40+
capability hints. A contribution aimed at an app no package in the artifact
41+
ships is not reported: contributing into an app another artifact installs is
42+
the supported case, and is why the merge is a fold.
43+
44+
**Nothing is refused.** No new failure, no ordering constraint, no change to
45+
what installs or to what `os build` accepts — a diagnostic was added and a
46+
refusal was not.
47+
48+
`examples/app-multi-package` now demonstrates the mechanism it was missing: the
49+
App package publishes a `sales_group` container and the Orders module
50+
contributes its nav entry into it, which is what a module split converts an
51+
app's own navigation into.

‎.claude/skills/spec-property-retirement/SKILL.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ conversion 是消费者跟的。两个都要写。
271271
(``.toThrow(/<key>.*removed.*use `<replacement>`/s)`` —— `s` 标志是 house
272272
style,因为消息跨行),一条阳性,对非 strict 剥除路径断言
273273
`not.toHaveProperty(key)`。参考 `packages/spec/src/ai/agent.test.ts:69-95`。
274+
⭐ **缺席 pin 一律 tree-scoped,永不 file-scoped**:file-scoped 只看得见作者想得
275+
到的那个文件,漏网的恰是他不知道的引用(实测五处、三个包、两张卡,其中两处还
276+
是删除**之后**新写的)。排除 `**/CHANGELOG.md`、`.changeset/` 与 pin 自身,理由
277+
写在旁边,⛔ 不建允许清单文件;⛔ 缺席断言不是陈旧提及,「修好它」即删掉守卫。
274278
- [ ] **Examples** —— `examples/app-showcase/**` 必须停止编写该键。墓碑路线上
275279
`tsc` 替你找齐。
276280
- [ ] **已发布 skills** —— 教这个键的 `skills/*/SKILL.md`(表格、`defineX` 示例)

‎content/docs/permissions/system-context.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ not on any flag.
6464
## How the flag is set
6565

6666
`isSystem` is **server-constructed and never client-supplied**. Inbound HTTP
67-
cannot set it (`packages/rest/src/rest-server.ts:1520`, `:1549`), and neither
67+
cannot set it (`packages/rest/src/rest-server.ts:1524`, `:1553`), and neither
6868
can an action body (`packages/runtime/src/domains/actions.ts:404`). It is
6969
written by internal callers only, as an option on the engine call:
7070

@@ -103,7 +103,7 @@ that silently does not happen.
103103
| 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:246` |
104104
| 15 | **Read-audit rows are not written** | plugin-audit | Lose: the "a person opened this record" trail. `sudo()` keeps the caller's `userId`, so this flag is the only thing separating a human read from a platform one | `read-audit.ts:556` |
105105
| 16 | Approval snapshot payload redaction skipped | plugin-approvals | Get: the whole snapshot on `find` / `findOne` — the audit/replay channel. Lose: field-visibility redaction over approval payloads | `payload-redaction-middleware.ts:115` |
106-
| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1552` |
106+
| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1556` |
107107

108108
### 2. Write pipeline and data integrity
109109

@@ -158,7 +158,7 @@ The largest single consumer — **17 of the 106 sites**.
158158
|:--|:---|:---|:---|:---|
159159
| 48 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes | `action-execution.ts:138` |
160160
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` |
161-
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4789`, `:6203`, `:6451`, `:6882`, `:7075` |
161+
| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:4888`, `:6302`, `:6550`, `:6981`, `:7174` |
162162
| 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` |
163163
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:411`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:326`, `external-datasource-routes.ts:302`, `package-routes.ts:97` |
164164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
@@ -199,7 +199,7 @@ assuming `isSystem` covers it is a documented source of bugs.
199199
| "It preserves a supplied `updated_at` / `updated_by`" | **No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only | `field.zod.ts:1580` (#3493 / #6640) |
200200
| "It stamps `created_by`" | **No.** Audit stamping reads `userId` from the context. A user-less system write stamps nothing — that is today's behaviour, not an error | `runtime-identity.ts:280`–`281` |
201201
| "It bypasses every guard" | **No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one | `last-admin-guard.ts:299` |
202-
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1520`, `:1549`; `domains/actions.ts:404` |
202+
| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1524`, `:1553`; `domains/actions.ts:404` |
203203

204204
---
205205

@@ -253,7 +253,7 @@ should recognise it instead of re-deriving it.
253253
rule-materialised grant that the next reconcile silently restores.
254254

255255
5. **`applySystemFields` does not read this flag.** It is named as if it did.
256-
`packages/objectql/src/registry.ts:464` is **schema-side column
256+
`packages/objectql/src/registry.ts:475` is **schema-side column
257257
provisioning** — which columns an object carries — and consumes
258258
`ExecutionContext.isSystem` zero times. The write-time ownership behaviour
259259
people attribute to it is row 2, in `plugin-security`.

‎content/docs/ui/setup-app.mdx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,37 @@ A few notable entries:
7979
live in `plugin-audit`, but they are not contributed as Setup nav
8080
entries.)
8181

82+
### When a contribution names an anchor that is not there
83+
84+
Aiming at a `group` id the target app does not declare is **not** refused and
85+
the entry is **not** dropped: the items are appended at the app's **top level**
86+
and the merge continues. That is deliberate — the merge is a read-time fold
87+
precisely so registration order does not matter (a contributor may register
88+
before the app it aims at), and a contribution into an *optional* anchor has to
89+
keep working when the plugin owning that anchor is not loaded.
90+
91+
It is, however, **loud**. The relocation emits a
92+
`nav_contribution_group_missing` diagnostic at `warn` — naming the contributing
93+
package, the target app, the missing group id and the relocated items — so it
94+
survives `OS_REGISTRY_LOG=warn` and appears in boot output. The same finding is
95+
carried on the app itself, readable as
96+
`registry.getAppNavDiagnostics(appName)`, and it is raised once per distinct
97+
mis-aim rather than once per read of the app.
98+
99+
`os build` **and `os validate`** answer the same question at compile time
100+
whenever the contributing package and the target app are composed into one
101+
artifact. Both print the finding and both carry it in `--json` under the
102+
existing `warnings` key, beside the authoring-rule advisories and the
103+
capability hints — the payload is deliberately closed, so a new class of
104+
finding fills a declared key rather than adding one. They **report** there;
105+
neither fails the build.
106+
107+
⚠️ The anchor id is the whole contract between a shell and its contributors,
108+
and a contributor cannot see the shell's ids at authoring time. A typo
109+
therefore produces a menu that renders, passes a smoke test, and has silently
110+
moved the entry one level up — which is why the diagnostic exists rather than a
111+
refusal.
112+
82113
## Why a shell + contributions
83114

84115
The Setup App is a shell of empty group anchors rather than a fixed

‎docs/adr/0005-metadata-customization-overlay.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
> **v5.0 update (2026):** Throughout this document, the term *project* has been renamed to *environment* (no aliases; CLI flags, URL paths, schemas, env vars all hard-renamed). See [ADR-0006 v4 — the v5.0 rename and its no-alias decision](./0006-project-environment-split.v4.md#the-v50-rename-and-its-no-alias-decision) for the rationale. The body below is preserved verbatim for historical context.
44
55

6-
**Status**: Accepted (2026-05-16) · **Amended** (2026-05-22, see "Amendment: post-ADR-0006 v4 scope") · **Amended** (2026-04-13, branch concept removed — see [ADR-0008 §0](./0008-metadata-repository-and-change-log.md#0-2026-04-13-amendment--drop-project-and-branch-from-metaref)) · **Amended** (2026-08-09, #6825 — the Phase-1 overlay-index migration is deleted; see "Amendment (2026-08-09, #6825): overlay-index delivery after the Phase-1 migration was deleted")
6+
**Status**: Accepted (2026-05-16) · **Amended** (2026-05-22, see "Amendment: post-ADR-0006 v4 scope") · **Amended** (2026-04-13, branch concept removed — see [ADR-0008 §0](./0008-metadata-repository-and-change-log.md#0-2026-04-13-amendment--drop-project-and-branch-from-metaref)) · **Amended** (2026-08-09, #6825 — the Phase-1 overlay-index migration is deleted; see "Amendment (2026-08-09, #6825): overlay-index delivery after the Phase-1 migration was deleted") · **Amended** (2026-09-04, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D6 — the per-organization overlay axis is **retired for now**: `sys_metadata` carries no organization column, org-scoped writes of the five tier-A types are refused, and the environment layer is the whole ledger; the environment → code layered resolution and the "no overlay row = the registry" reading are unchanged)
77
**Deciders**: ObjectStack Protocol Architects
88
**Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (Package as first-class citizen), [ADR-0004](./0004-cloud-multi-kernel.md) (Cloud + per-project kernels)
99
**Amended by**: [ADR-0006 v4](./0006-project-environment-split.v4.md) (drops `sys_project` entirely), [ADR-0008](./0008-metadata-repository-and-change-log.md) (re-expresses overlay as `LayeredRepository`; subsequently drops `project`/`branch` from `MetaRef`), [ADR-0029 D9](./0029-kernel-object-ownership-and-platform-objects-decomposition.md#amendment-2026-08-09-6853-a-tenant-overlay-of-an-object-is-its-own-contributor-layer-not-a-second-own) (for `object` only: the overlay is a registry contributor LAYER over the packaged owner, resolved as `base = overlay ?? own`, instead of a destructive in-place overwrite)

‎docs/adr/0017-object-has-many-view.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0017: Object has-many View (Independent View Entities)
22

3-
**Status**: Accepted · Implemented Phases 1–4 (2026-05-30) · Phase 5 (Studio designer) deferred
3+
**Status**: Accepted · Implemented Phases 1–4 (2026-05-30) · Phase 5 (Studio designer) deferred · **Amended** (2026-09-04, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D13 — §3.4's `sys_view_definition` store and §3.6's objectui repointing are **retired as inert**: verified on 2026-09-04 that no framework writer or reader of its rows exists, that `objectui` never referenced the object in its history (its runtime-view CRUD writes the ADR-0005 `view` overlay via `client.meta.saveItem`), and that a per-user view scope is a parked v18 direction. Runtime-authored views are environment metadata; §3.5's `GET /meta/view?object=` filter stands)
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (package as first-class citizen), [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0010](./0010-metadata-protection-model.md) (L1/L2/L3 protection)
66
**Consumers**: `@objectstack/spec`, `@objectstack/metadata`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/platform-objects`, `../objectui` (runtime switcher + Studio designer)

0 commit comments

Comments
 (0)