Skip to content

Commit d88c29c

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16004-skill-protocol-lines
2 parents 38550e0 + 3e270d4 commit d88c29c

53 files changed

Lines changed: 2674 additions & 229 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/plugin-sharing": patch
3+
"@objectstack/plugin-approvals": patch
4+
"@objectstack/plugin-security": patch
5+
"@objectstack/runtime": patch
6+
---
7+
8+
Four server-side authorization sites stop deriving platform-operator authority from a NAME in `ExecutionContext.positions`, and read the ADR-0095 posture rung instead.
9+
10+
`positions[]` is the security axis, so it carries ADR-0057 D4 `sys_user_position` names alongside the built-ins. `sys_user_position` is `apiEnabled` and its `position` values are unconstrained, so a tenant could mint a row spelling `platform_admin` for one of their own users: `resolveUserAuthzGrants` pushed that name straight onto `grants.positions`, while `grants.posture` — derived from the unscoped `admin_full_access` grant and nothing else — correctly stayed `MEMBER`. Every reader of the name therefore answered `true` for a principal enforcement treats as an ordinary member. `resolve-authz-context.ts` states the rule at `hasPlatformAdminStanding` ("read the RUNG — never `positions.includes(...)`"), but a comment is not a gate and these four had not followed it.
11+
12+
Each site now tests `posture === 'PLATFORM_ADMIN'`, byte-for-byte what `hasPlatformAdminStanding` returns:
13+
14+
- **`plugin-sharing`**`hasPlatformAuthority`. The minted row satisfied `assertResolvableAdminScope`, so an org-less caller holding only the ORG-scoped `manage_sharing` capability was answered with **every tenant's** sharing rules, and could delete platform-global rules. The `manage_platform_settings` capability spelling is unchanged.
15+
- **`plugin-approvals`**`isOverrideActor`. This predicate already read the rung and then ORed the name onto it, which is no protection: an OR is only as strong as its weakest arm. Because the platform arm deliberately crosses the tenant wall, the minted row let a member of one organization approve, reject or recall a **different organization's** pending request while holding no slot in its slate. The `ADMIN_FULL_ACCESS` capability arm and both TENANT_ADMIN arms are unchanged.
16+
- **`runtime`** — the ADR-0126 §5 activation gate. Under a `group` or `isolated` posture this gate is the only thing between a tenant org admin and the **install-wide** `sys_metadata_activation` row, so the minted row reopened #10243 with a durable row behind it.
17+
- **`plugin-security`**`derivePosture` in the explain engine. Narrower than the other three, and stated precisely rather than overclaimed: the name-read sat behind an early `ctx.posture` return that `buildContextForUser` always populates, so the shipping path was already gated and a D4 row never moved it. What the read did reach was a posture-less hand-built context, where it made the panel **report** `PLATFORM_ADMIN` for a principal enforcement treats as a MEMBER — a misreport rather than an admission, but in the one tool an administrator opens to check exactly this.
18+
19+
No behaviour changes for a genuine platform operator: their resolved context carries the rung, and the built-in position is still projected onto `positions[]` for display and predicate use. What changes is that the name alone no longer answers the authorization question.
20+
21+
Graded `patch` on the surface it moves: no exported type, signature or contract changes, and no authorable metadata is added, removed or renamed. The only observable difference is that a principal who never held the capability grant stops being admitted — which is the defect, not a feature anyone could have depended on.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
`$error` now names the most recent failure in a flow run, whichever way that failure arrived.
6+
7+
The automation engine has two failure arms. When a node FAILS BY RETURNING `{ success: false }`, the engine rewrote the run-wide `$error` (and `<nodeId>.error`) and then decided whether a `fault` edge could route it. When a node FAILED BY THROWING — a `timeoutMs` firing, a dying nested container, a thrown guard — it did both **inside** the `fault`-edge branch, so a thrown failure with no `fault` edge of its own left `$error` holding an earlier, unrelated failure's value.
8+
9+
A node inside a structured region never has a `fault` edge of its own: the region's synthetic sub-flow carries only the region's own edges. So every thrown failure inside a `try_catch`, `loop` body or other region hit this. The result was not a crash but a plausible-looking wrong value: **the message and the code came from two different failures**`{ code: 'DUPLICATE_RECORD', message: "Node 'mk' timed out after 20ms" }` — and a catch region branching on `{$error.code}` swallowed a store failure as "the row is already there" while the run reported success.
10+
11+
The throw arm now publishes `$error` and `<nodeId>.error` before deciding whether the failure routes, exactly as the returned-failure arm does. What a thrown failure publishes is `{ nodeId, message }`: there is no node result on that path, so no `output` and no classified `code` exist to carry — and that absence is the right answer for a throw rather than a reason to leave a stale `code` standing.
12+
13+
Routing is unchanged. A guard refusal that throws (ADR-0049's unscoped-run refusal, for one) is still un-routable, still fatal, and still reports its own message; the thrown value itself is rethrown exactly as caught.

content/docs/permissions/system-context.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,15 @@ The largest single consumer — **17 of the 105 sites**.
137137
| 36 | `sys_record_share` reads are **not** self-scoped | Get: tenant-wide share listing without `manage_sharing` | `sharing-plugin.ts:1189` |
138138
| 37 | Share-link policy `enabled` check bypassed; system callers re-enter under a system context | Get: link **creation** while the policy is off — resolution is **not** bypassed since #14033 (`publicSharing.enabled` is a standing policy held at every redemption): a link minted this way does not resolve until the block is enabled | `plugin-sharing/src/share-link-service.ts:459`, `:513`, `:517`, `:590`, `:620` |
139139
| 38 | Sharing-rule provenance stamp skipped | Lose: the row is not marked as an admin customization — seeder / `defineRule` / boot reconcilers are "the package door" | `sharing-rule-provenance.ts:66` |
140-
| 39 | Sharing-rule service write + delete paths return early | Lose: the manage-rules gate on the service surface, and the platform-global-rule delete guard | `sharing-rule-service.ts:278`, `:503` |
140+
| 39 | Sharing-rule service write + delete paths return early | Lose: the manage-rules gate on the service surface, and the platform-global-rule delete guard | `sharing-rule-service.ts:279`, `:518` |
141141

142142
### 4. Approvals, reports, attachments, comments, knowledge
143143

144144
| # | Behaviour when `isSystem` | Package | What you get / what you lose | Anchor |
145145
|:--|:---|:---|:---|:---|
146146
| 40 | **Approval record lock released** — a locked record is writable | plugin-approvals | Get: engine self-writes (the status mirror) pass. Lose: the lock that stops edits while an approval is live. Note there is deliberately **no admin exemption** here — only `isSystem` | `lifecycle-hooks.ts:347` |
147147
| 41 | Delegation write guard bypassed | plugin-approvals | Get: service / seed / import may write delegation rows naming another delegator | `lifecycle-hooks.ts:570` |
148-
| 42 | Approval actor / submitter / pending-approver checks bypassed (8 sites) | plugin-approvals | Get: approve, reject, recall, reassign without being a pending approver or the submitter | `plugin-approvals/src/approval-service.ts:1093`, `:1202`, `:3458`, `:3606`, `:3774`, `:3845`, `:4034`, `:4074` |
148+
| 42 | Approval actor / submitter / pending-approver checks bypassed (8 sites) | plugin-approvals | Get: approve, reject, recall, reassign without being a pending approver or the submitter | `plugin-approvals/src/approval-service.ts:1096`, `:1219`, `:3475`, `:3623`, `:3791`, `:3862`, `:4051`, `:4091` |
149149
| 43 | Saved-report ownership is **assignable**, and an update may reassign it | plugin-reports | Get: `ownerId` from input is honoured. A non-system caller always owns what it creates and can never reassign | `plugin-reports/src/report-service.ts:404`, `:425` |
150150
| 44 | Saved-report access / export / mutation gates bypassed | plugin-reports | Get: read, bulk-export and overwrite any report | `plugin-reports/src/report-service.ts:343`, `:372`, `:447`, `:684` |
151151
| 45 | Attachment access hooks return early (insert + update + delete, and the read AST) | service-storage | Lose: attachment visibility scoping | `attachment-access-hooks.ts:300`, `:349`, `:448`, `:524` |
@@ -164,7 +164,7 @@ The largest single consumer — **17 of the 105 sites**.
164164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
165165
| 54 | Package REST route capability gate bypassed | rest | Get: package read/write over REST without `manage_metadata` / `studio.access` / `setup.access` | `package-routes.ts:102` |
166166
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `domains/packages.ts:250`, `:283` |
167-
| 56 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `activation-gate.ts:138`, `:189` |
167+
| 56 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `activation-gate.ts:157`, `:211` |
168168
| 57 | Automation run-state read, flow-authoring write and unrelated-screen read all pass | runtime | Get: run state, flow writes and screen reads with no grant | `domains/automation.ts:254`, `:545`, `:635` |
169169
| 58 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers | `suggested-audience-bindings.ts:703` |
170170
| 59 | Email-template / webhook provenance stamps skipped | plugin-email, plugin-webhooks | Lose: the row is not marked as an admin customization | `email-template-provenance.ts:77`, `webhook-provenance.ts:68` |

examples/app-showcase/access-matrix.json

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,39 @@
7878
"modifyAllRecords": false,
7979
"sharingModel": "public_read_write"
8080
},
81+
{
82+
"permissionSet": "showcase_contributor",
83+
"object": "showcase_expense_line",
84+
"create": true,
85+
"read": true,
86+
"edit": true,
87+
"delete": false,
88+
"viewAllRecords": false,
89+
"modifyAllRecords": false,
90+
"sharingModel": "controlled_by_parent"
91+
},
92+
{
93+
"permissionSet": "showcase_contributor",
94+
"object": "showcase_expense_report",
95+
"create": true,
96+
"read": true,
97+
"edit": true,
98+
"delete": false,
99+
"viewAllRecords": false,
100+
"modifyAllRecords": false,
101+
"sharingModel": "public_read_write"
102+
},
103+
{
104+
"permissionSet": "showcase_contributor",
105+
"object": "showcase_field_zoo",
106+
"create": true,
107+
"read": true,
108+
"edit": true,
109+
"delete": false,
110+
"viewAllRecords": false,
111+
"modifyAllRecords": false,
112+
"sharingModel": "public_read_write"
113+
},
81114
{
82115
"permissionSet": "showcase_contributor",
83116
"object": "showcase_invoice",
@@ -275,6 +308,61 @@
275308
"modifyAllRecords": false,
276309
"sharingModel": "public_read"
277310
},
311+
{
312+
"permissionSet": "showcase_member_default",
313+
"object": "showcase_business_unit",
314+
"create": false,
315+
"read": true,
316+
"edit": false,
317+
"delete": false,
318+
"viewAllRecords": false,
319+
"modifyAllRecords": false,
320+
"sharingModel": "public_read_write"
321+
},
322+
{
323+
"permissionSet": "showcase_member_default",
324+
"object": "showcase_cascade",
325+
"create": true,
326+
"read": true,
327+
"edit": true,
328+
"delete": false,
329+
"viewAllRecords": false,
330+
"modifyAllRecords": false,
331+
"sharingModel": "public_read_write"
332+
},
333+
{
334+
"permissionSet": "showcase_member_default",
335+
"object": "showcase_category",
336+
"create": false,
337+
"read": true,
338+
"edit": false,
339+
"delete": false,
340+
"viewAllRecords": false,
341+
"modifyAllRecords": false,
342+
"sharingModel": "public_read_write"
343+
},
344+
{
345+
"permissionSet": "showcase_member_default",
346+
"object": "showcase_expense_report",
347+
"create": false,
348+
"read": true,
349+
"edit": false,
350+
"delete": false,
351+
"viewAllRecords": false,
352+
"modifyAllRecords": false,
353+
"sharingModel": "public_read_write"
354+
},
355+
{
356+
"permissionSet": "showcase_member_default",
357+
"object": "showcase_field_zoo",
358+
"create": false,
359+
"read": true,
360+
"edit": false,
361+
"delete": false,
362+
"viewAllRecords": false,
363+
"modifyAllRecords": false,
364+
"sharingModel": "public_read_write"
365+
},
278366
{
279367
"permissionSet": "showcase_member_default",
280368
"object": "showcase_inquiry",
@@ -286,6 +374,17 @@
286374
"modifyAllRecords": false,
287375
"sharingModel": "private"
288376
},
377+
{
378+
"permissionSet": "showcase_member_default",
379+
"object": "showcase_preference",
380+
"create": true,
381+
"read": true,
382+
"edit": true,
383+
"delete": false,
384+
"viewAllRecords": false,
385+
"modifyAllRecords": false,
386+
"sharingModel": "public_read_write"
387+
},
289388
{
290389
"permissionSet": "showcase_member_default",
291390
"object": "showcase_private_note",
@@ -330,6 +429,17 @@
330429
"modifyAllRecords": false,
331430
"sharingModel": "public_read_write"
332431
},
432+
{
433+
"permissionSet": "showcase_member_default",
434+
"object": "showcase_team",
435+
"create": false,
436+
"read": true,
437+
"edit": false,
438+
"delete": false,
439+
"viewAllRecords": false,
440+
"modifyAllRecords": false,
441+
"sharingModel": "public_read_write"
442+
},
333443
{
334444
"permissionSet": "showcase_ops",
335445
"object": "showcase_announcement",
@@ -341,6 +451,28 @@
341451
"modifyAllRecords": true,
342452
"sharingModel": "public_read"
343453
},
454+
{
455+
"permissionSet": "showcase_ops",
456+
"object": "showcase_business_unit",
457+
"create": true,
458+
"read": true,
459+
"edit": true,
460+
"delete": false,
461+
"viewAllRecords": false,
462+
"modifyAllRecords": false,
463+
"sharingModel": "public_read_write"
464+
},
465+
{
466+
"permissionSet": "showcase_ops",
467+
"object": "showcase_category",
468+
"create": true,
469+
"read": true,
470+
"edit": true,
471+
"delete": false,
472+
"viewAllRecords": false,
473+
"modifyAllRecords": false,
474+
"sharingModel": "public_read_write"
475+
},
344476
{
345477
"permissionSet": "showcase_ops",
346478
"object": "showcase_inquiry",
@@ -364,6 +496,28 @@
364496
"viewAllRecords": false,
365497
"modifyAllRecords": false,
366498
"sharingModel": "public_read_write"
499+
},
500+
{
501+
"permissionSet": "showcase_ops",
502+
"object": "showcase_project_membership",
503+
"create": true,
504+
"read": true,
505+
"edit": true,
506+
"delete": true,
507+
"viewAllRecords": false,
508+
"modifyAllRecords": false,
509+
"sharingModel": "public_read_write"
510+
},
511+
{
512+
"permissionSet": "showcase_ops",
513+
"object": "showcase_team",
514+
"create": true,
515+
"read": true,
516+
"edit": true,
517+
"delete": false,
518+
"viewAllRecords": false,
519+
"modifyAllRecords": false,
520+
"sharingModel": "public_read_write"
367521
}
368522
]
369523
}

examples/app-showcase/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@objectstack/cli": "workspace:*",
3939
"@objectstack/core": "workspace:*",
4040
"@objectstack/formula": "workspace:*",
41+
"@objectstack/lint": "workspace:*",
4142
"@objectstack/objectql": "workspace:*",
4243
"@objectstack/plugin-approvals": "workspace:*",
4344
"@objectstack/plugin-email": "workspace:*",

examples/app-showcase/src/data/objects/cascading-select.object.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,19 @@ import { P } from '@objectstack/spec';
2626
* unbound (a system write) — the acting user is bound from the request on
2727
* authenticated writes (engine `buildEvalUser`).
2828
*
29-
* `sharingModel: 'public_read_write'` so the seeded admin (and the live e2e,
30-
* objectui `e2e/live/cascading-options.spec.ts`) can create records without a
31-
* bespoke permission set; belonging to no permission set, it is intentionally
32-
* absent from the ADR-0090 access-matrix snapshot.
29+
* `sharingModel: 'public_read_write'` is the RECORD baseline (gate ②) — no
30+
* sharing rule is needed for one persona to see another's row. It is NOT what
31+
* makes the object reachable: object-level CRUD (gate ①) comes only from a
32+
* permission set and is checked first. This comment used to say the wide OWD let
33+
* the seeded admin and the live e2e create records "without a bespoke permission
34+
* set". What actually carried the admin was their built-in WILDCARD set: with no
35+
* grant anywhere, `nav-object-ungranted` warned on every build and reports the
36+
* `nav_cascade` entry as permission-denied for every principal that does not
37+
* hold that wildcard set. `showcase_member_default` now grants read/create/edit,
38+
* so the object is in the access-matrix snapshot and a plain member holds the
39+
* capability the navigation entry already claimed for them. Note where the
40+
* narrowing really lives: on one OPTION (`tier: 'restricted'`, admin-only),
41+
* not on the object.
3342
*
3443
* The server verdict is unit-covered by objectql
3544
* `rule-validator.option-visibility.test.ts`; this object is the served fixture

examples/app-showcase/src/data/objects/expense-report.object.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,20 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
3131
*/
3232
export const ExpenseReport = ObjectSchema.create({
3333
name: 'showcase_expense_report',
34-
// [ADR-0090 D1] grandfather stamp: world-writable demo object so any seeded
35-
// persona (and the browser e2e) can create/edit reports without a bespoke
36-
// permission set. Belonging to no permission set, it is intentionally absent
37-
// from the access-matrix snapshot (cf. showcase_cascade).
34+
// [ADR-0090 D1] grandfather stamp: the RECORD baseline is world-writable, so
35+
// no sharing rule or scope depth is needed to see another persona's report.
36+
//
37+
// That is gate ② and it is the whole of what this dial buys. It does NOT make
38+
// the object reachable: object-level CRUD is gate ①, it is granted only by a
39+
// permission set, and it is checked FIRST. This comment used to claim the wide
40+
// OWD let "any seeded persona create/edit reports without a bespoke permission
41+
// set" — measurably false, and the app shipped the proof: the object sat in
42+
// the shared navigation with no grant anywhere, so `nav-object-ungranted`
43+
// warned on every build, reporting the entry as permission-denied for every
44+
// principal except one holding the platform's built-in wildcard admin set.
45+
// The seeded admin holds exactly that set, which is why it looked fine.
46+
// The grants now live on `showcase_contributor` (file/edit) and
47+
// `showcase_member_default` (read), and both appear in access-matrix.json.
3848
sharingModel: 'public_read_write',
3949
label: 'Expense Report',
4050
pluralLabel: 'Expense Reports',

0 commit comments

Comments
 (0)