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 7309ef0
Browse filesBrowse the repository at this point in the historyBrowse files
A server-side producer can store a file and attach it to a record: `IFileAttachmentService.attachFile`, registered by `StorageServicePlugin` as the kernel service `file-attachments` (`FILE_ATTACHMENT_SERVICE`), writes the bytes, one `sys_file` row and one `sys_attachment` row linking it to the parent, under the checks every other attachment of that parent meets
7
+
8
+
Clause-②: yes (widening: `@objectstack/spec/contracts` gains `IFileAttachmentService`, `AttachFileInput`, `AttachedFile` and `FILE_ATTACHMENT_SERVICE`, and `StorageServicePlugin` registers and declares one more service)
9
+
10
+
-**The call.**`attachFile({ parentObject, parentId, organizationId, filename, mimeType, data })` answers `{ fileId, attachmentId, size }`. `organizationId` is required: the organization the caller acts in, or `null` for none. It acts as the system, for a producer with no browser upload in front of it (inbound mail first). Reach it with `ctx.getService(FILE_ATTACHMENT_SERVICE)` once the kernel has bootstrapped.
11
+
-**What it runs.** Each check is the one the rest of the platform already runs, not a copy:
12
+
- The `max_upload_mb` storage setting, through the same verdict the REST upload doors ask. Over it, the call is refused `413 PAYLOAD_TOO_LARGE` before anything is stored.
13
+
- The parent's organization. A parent walled into another organization than `organizationId` is refused `404 RECORD_NOT_FOUND`, as a missing parent is. Both rows are written in the parent's organization, or in the caller's for a global parent.
14
+
- Every gate on the `sys_attachment` insert, among them the `enable.files` opt-in: a parent object without it is refused `403 FILES_DISABLED`.
15
+
- Before the kernel has bootstrapped, or with no data engine, the call is refused `503 SERVICE_UNAVAILABLE`. A malformed input is refused `400 VALIDATION_ERROR`.
16
+
-**No refusal leaves a committed `sys_file`.** The row is written `pending` before the bytes, and turns `committed` only in the same transaction as the `sys_attachment` insert. A refused call removes the bytes and the row before it throws. If the process dies mid-call, the pending-upload retention reclaims both.
17
+
-`@objectstack/service-storage`: the upload doors' size refusal moved from the route module into `storage-limits.ts`, beside the limit it enforces, so the doors and the verb read one status, one code and one sentence. The doors answer exactly as before.
fix(spec,objectql)!: a permission set whose body `packageId` disagrees with the package that ships it is refused at `defineStack` / `os validate` / `os build` and at package registration, and every reader takes a set's owner from its binding only (ADR-0086 D3)
10
+
11
+
Clause-②: yes (narrowing)
12
+
13
+
<!-- adr-0087: not-required (already-registered permission-set-package-id-binding-mismatch-refused) this change widens that entry, in this diff, to the build and registration doors: same key, same ruling, same remedy, no stored row rewritten -->
14
+
15
+
**BREAKING** (an accept-set narrowing on a published authoring surface), shipped as `minor` under the launch-window convention for breaking changes.
16
+
17
+
ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the binding: the package that ships the set or that it is saved into, served as `_packageId`. The metadata save, restore and publish doors already refuse a body `packageId` that disagrees with it. Package source and package registration did not: a set whose body named another package was registered under the package that shipped it, with no refusal and no warning, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got.
18
+
19
+
**What is refused, and where.** One judge, `judgePermissionSetOwnerBinding` (new export of `@objectstack/spec/security`), serves every door:
20
+
21
+
-**The stack schema** (`ObjectStackDefinitionSchema`), so `defineStack`, `objectstack validate`, `objectstack build`, the lint scorer and an artifact load all refuse a set whose body `packageId` is not the id of the package that ships it: the stack's `manifest.id`, each `packages[]` body's own `id`, and `manifest.id` for a top-level set no body declares. A stack with no `manifest` ships its sets from no package, so any key there is refused, the empty string included. The issue sits at the set's `packageId` (`permissions.0.packageId`); `defineStack` throws `STACK_SCHEMA_INVALID` (422) carrying it.
22
+
-**Package registration** (`ObjectQL.registerApp`, the manifest and a nested plugin, for a manifest no schema parse judged) throws `400 VALIDATION_ERROR` with `issues[]` at `packageId` before the set is filed, the envelope the same seam throws for a view container whose `name` disagrees with its key.
23
+
24
+
The message prescribes first, names both values, and offers both remedies: drop the key, or ship the set from the package it names. No error code is minted.
25
+
26
+
**Readers.**`@objectstack/plugin-security`'s packaged-set lock (`classifyPackagedPermissionSet`) and `@objectstack/cli`'s permission-set name-collision report take a set's package from its binding only. A set filed into no package whose body named one is no longer locked as that package's, and a stack with no `manifest` declares no owner for the collision report.
27
+
28
+
**Unchanged.** A body `packageId` equal to the package that ships the set is accepted, and an absent one is accepted everywhere. A top-level copy of a set a `packages[]` body declares (an artifact built with both halves) is judged on its body, not against the artifact's own id. The metadata save, restore and publish doors answer in the same words as before (`@objectstack/metadata-protocol` now calls the shared judge). Stored `sys_metadata` rows are not rewritten.
29
+
30
+
## FROM → TO
31
+
32
+
| door | FROM | TO |
33
+
|:--|:--|:--|
34
+
|`defineStack` / `objectstack validate` / `objectstack build`|`defineStack({ manifest: { id: 'com.acme.crm', … }, permissions: [{ name: 'crm_member', packageId: 'com.acme.other', … }] })` accepted; the set was owned by `com.acme.crm`| refused at `permissions.0.packageId`, `STACK_SCHEMA_INVALID` 422 |
35
+
|`ObjectQL.registerApp`| the same manifest registered the set under `com.acme.crm`, silently |`400 VALIDATION_ERROR`, `issues[]` at `packageId`, nothing filed |
36
+
| an artifact built before this change | loaded, the set owned by the shipping package | refused at load; rebuild it without the key |
37
+
38
+
**Remedy, in one line.** Drop `packageId` from the permission set in your stack source (the package that ships a set is its owner), or move the set into the stack of the package it names, then build again.
fix(spec): the two step-18 flow-binding D3 entries name only the reads a renamed variable has on protocol 18, a text-slot hole and a CEL expression
6
+
7
+
Clause-②: no
8
+
9
+
The `replacement` text of two ADR-0087 D3 semantic entries, `flow-binding-name-dollar-refused` and `flow-binding-variable-dollar-name-refused`, listed three ways a flow reads the old variable name, for the author to rename. Protocol 18 has only the first two. The protocol upgrade guide and `spec-changes.json` are generated from these entries, so they now say what the two entries' release notes already say.
10
+
11
+
| FROM | TO |
12
+
|:--|:--|
13
+
| Rename every read of the old name with it — a text-slot hole, a CEL expression, a single-brace token in a value position | Rename every read of the old name with it — a text-slot hole, a CEL expression |
14
+
15
+
No protocol-18 value position reads a flow variable through a single-brace `{…}` token. A value slot takes a CEL value envelope or a literal, and a `{…}` token there is refused. The one `{…}` string a value slot hands on is a CRUD `filter` operand's whole-string query placeholder (`{current_user_id}`, `{today}`): the query engine reads it, and it names no flow variable. A text slot reads `{{ }}` holes.
16
+
17
+
Text only. No schema, door, conversion or entry id changes, and each entry keeps its `fromVersion`, `toVersion` and every other field.
Copy file name to clipboardExpand all lines: content/docs/permissions/tenant-audit-census.mdx
+20-20Lines changed: 20 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -83,7 +83,7 @@ what moved this page's population from 225 to 227; nothing about the two sites
83
83
changed, only whether this instrument could see them.
84
84
85
85
**The expensive failure direction is a keyword.** Sites whose receiver the author
86
-
typed `any` have no type to read, and there are 40 of them — just under a fifth
86
+
typed `any` have no type to read, and there are 41 of them — just under a fifth
87
87
of the population, concentrated in exactly the seed and bootstrap paths this
88
88
control exists for. Scoring an unreadable receiver as "not an engine" would have
89
89
dropped every one of them silently, with a clean exit and a smaller number that
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122
122
123
123
The same holds twice over for the context. An options argument spelled as a
124
124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125
-
forwarding shim cannot, and **53 of the 226 sites are spelled that way**. A
125
+
forwarding shim cannot, and **53 of the 228 sites are spelled that way**. A
126
126
context resolved from an inline literal or a local `const` can be tested for
127
127
`isSystem`; one arriving from a helper call cannot.
128
128
@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187
187
188
188
| carried figure | where it survives | this census |
189
189
| :--- | :--- | ---: |
190
-
| 175 write call sites | quoted in the merged changeset |**226**|
190
+
| 175 write call sites | quoted in the merged changeset |**228**|
191
191
| 24 carrying no tenant context | quoted in the merged changeset |**2** provable and tenancy-enabled; **32** more whose options argument is unreadable |
192
-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card |**153 of 226** decidable, **73** undecidable |
193
-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration**|**not reproduced**: 121 decidably elevated, 0 decidably not, 97 undecidable |
192
+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card |**155 of 228** decidable, **73** undecidable |
193
+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration**|**not reproduced**: 121 decidably elevated, 0 decidably not, 99 undecidable |
194
194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195
195
196
196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -200,18 +200,18 @@ be stated is what this instrument counts, which is written above and re-runnable
200
200
at any commit.
201
201
202
202
Two structural facts do plausibly widen this reading against any hand or regex
203
-
one, and both are counted in the generated tables below: the 40 sites reached
203
+
one, and both are counted in the generated tables below: the 41 sites reached
204
204
through an erased (`any`) receiver, and the 50 that name their object through a
205
205
`const` rather than inline. An instrument that read either the way a person does
206
206
would report a smaller number and would not say so.
207
207
208
208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209
209
figure has no surviving corroboration anywhere in the tree.** This census reads
210
-
121 of 226 (54%) as decidably elevated, with 97 more whose elevation is a
210
+
121 of 228 (53%) as decidably elevated, with 99 more whose elevation is a
211
211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212
212
answer is that a static reading cannot settle it.
213
213
214
-
⇒ **Cite `2 / 226`, and say what it is**: the sites whose options argument was
214
+
⇒ **Cite `2 / 228`, and say what it is**: the sites whose options argument was
215
215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216
216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217
217
without tenant context" — **32 further sites** have an options argument this
@@ -223,28 +223,28 @@ cannot read, and they are neither in nor out.
223
223
224
224
| what | count |
225
225
| :--- | ---: |
226
-
| write call sites on the application surface |**226**|
227
-
| …whose object name is statically decidable |153|
226
+
| write call sites on the application surface |**228**|
227
+
| …whose object name is statically decidable |155|
228
228
| …whose object name is chosen at run time | 73 |
229
-
| …against an object with tenancy ENABLED |151|
229
+
| …against an object with tenancy ENABLED |153|
230
230
| …against an object that declares tenancy off | 1 |
231
-
| threading a tenant context |165|
231
+
| threading a tenant context |167|
232
232
| PROVABLY carrying none (options read, no context key) |**8**|
233
233
| …of those, against a decidably tenancy-enabled object |**2**|
234
234
| options argument UNREADABLE — may or may not carry one | 53 |
235
235
| …of those, against a decidably tenancy-enabled object | 32 |
236
236
| threading a decidably ELEVATED (`isSystem`) context | 121 |
237
237
| threading a context that is decidably NOT elevated | 0 |
238
-
| threading a context whose elevation is a run-time fact |97|
238
+
| threading a context whose elevation is a run-time fact |99|
239
239
240
240
| how the instrument reached the site | count |
241
241
| :--- | ---: |
242
-
| receiver carried a readable engine type |186|
243
-
| receiver erased, placed by the object NAME |27|
242
+
| receiver carried a readable engine type |187|
243
+
| receiver erased, placed by the object NAME |28|
244
244
| receiver erased, placed by an `object: string` PARAMETER | 7 |
245
245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 6 |
246
246
247
-
| object name spelled inline |103|
247
+
| object name spelled inline |105|
248
248
| object name spelled through a `const`| 50 |
249
249
| object name is an `object: string` parameter | 9 |
250
250
| object name is some other run-time expression | 64 |
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297
297
their values are not compared. The reasoning, and the measurement behind it,
Copy file name to clipboardExpand all lines: content/docs/references/security/permission.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
@@ -117,7 +117,7 @@ const result = AdminScopeSchema.parse(data);
117
117
|**name**|`string`| ✅ | Permission set name, unique per organization (lowercase snake_case) |
118
118
|**label**|`string`| optional | Display label |
119
119
|**description**|`string`| optional | Human-readable description shown in Setup |
120
-
|**packageId**|`string`| optional |[ADR-0086 D3] Owning package id for a package-shipped set (absent = env-authored); the metadata save, restore and publish doors refuse a value that disagrees with the package the set is saved into|
120
+
|**packageId**|`string`| optional |[ADR-0086 D3] Owning package id for a package-shipped set (absent = env-authored); a value that disagrees with the package that ships the set, or that it is saved into, is refused by the stack schema (defineStack, os validate, os build), the package registration, and the metadata save, restore and publish doors|
121
121
|**managedBy**|`Enum<'package' \| 'platform' \| 'user'>`| optional |[ADR-0086 D3] Record provenance: package (upgrade-owned metadata) vs platform/user (env config) |
122
122
|**isDefault**|`boolean`| optional (default: `false`) |[ADR-0090 D5] App baseline for the everyone position: app-level sets are auto-bound at boot (guarded, idempotent); package-level sets become install-time suggestions an admin confirms |
0 commit comments