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 46774c3
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec,automation,app-shell): a flow screen field typed `file` collects an upload, and the run resumes with the uploaded file's id
8
+
9
+
Clause-②: yes (narrowing)
10
+
Metadata-protocol: semantic
11
+
12
+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition, stored row or stored shape is added, removed, renamed or re-shaped. `ScreenFieldConfigSchema.type` stays an open string, so every flow that parsed before still parses, and nothing exported is removed or narrowed. What changes is what a screen field already declared `type: 'file'` means: the console renders the upload widget for it, and the resume door refuses a value that is not a file id. The remedy is a change in what a caller posts to `resume` (the uploaded file's id), never a rewrite of a package source or a stored row, so there is nothing for `objectstack migrate meta` to rewrite. The other categories are closed on facts: the three packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no runtime interface, type or export is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
13
+
14
+
A wizard can now ask for a file before the record that will own it exists, and write everything after its last screen.
15
+
16
+
-**`@objectstack/spec`**: `ScreenFieldConfigSchema.type` (and the `ScreenFieldSpec` contract) documents `'file'` as the second value domain beside `'number'`. A `type: 'file'` field renders the platform upload widget. The upload goes through the file service and lands as a `sys_file` that no record owns yet. The resume carries that file's id, which is ADR-0104 D3's stored form, and nothing else. A later `create_record` or `update_record` node writes the id into an object's `file` field, and that write claims the file for the record. Under the default `runAs: 'user'`, the claim first makes the download door's read check as the run's user; a `runAs: 'system'` run's write is not checked, as no system write is.
17
+
-**`@objectstack/service-automation`**: the resume re-checks a `type: 'file'` field as a value domain. A value that is not a file id, judged by the spec's `isFileIdToken` (the same predicate the claim path reads), is refused with `400 INVALID_SCREEN_INPUT` and the field code `invalid_type` ("must be the id of an uploaded file"). That covers a URL, the expanded `{ id, name, url, … }` read form, a list and a number. The value is never reduced to an id. The refusal writes nothing and keeps the pause, so a corrected resume still lands. An empty optional field and a hidden field are not judged. `image`, `avatar`, `video` and `audio` stay open widget hints.
18
+
-**`@object-ui/app-shell`**: the flow screen dialog renders the object form's `FileField` for a `type: 'file'` field and uploads through the console's `UploadProvider`. While an upload is in flight, Submit is disabled and reads "Uploading…". The resume posts each file field as its bare id, and a prefilled expanded value is reduced to its id first.
19
+
20
+
**BREAKING** (a resume-acceptance narrowing) for a flow whose `screen` node already declares a field `type: 'file'`. None was found in this repository's examples or packages.
21
+
22
+
-**Before.**`type: 'file'` was an open widget hint the console did not know, so the field rendered a plain text input, and `POST /automation/:name/runs/:runId/resume` accepted any value for it.
23
+
-**Now.** The field renders the upload widget, and the resume refuses any value that is not a file id with `400 INVALID_SCREEN_INPUT` (`invalid_type`). The refusal keeps the pause.
24
+
-**Fix.** A caller that resumes the run itself, without the console, uploads the file through the file service first and posts the minted `sys_file` id for the field: FROM `{ "contract_file": "https://…/nda.pdf" }` TO `{ "contract_file": "FILE_ID" }`. A field that was never meant to collect an upload takes another `type`, such as `text`.
feat(ui): the Studio permission-set form offers the field permission `when` (row condition on the edit vote)
6
+
7
+
Clause-②: yes (widening)
8
+
Metadata-protocol: none
9
+
10
+
Part of #25866 — the Studio clause of ruling 6408558941 (part ①, item 4), after #25980 landed the key itself.
11
+
12
+
-**Before.** The permission matrix's field rows carried two checkboxes, `readable` and `editable`: no way to offer or see a `when`. The Read-off cascade could leave `readable: false` beside an authored `when` (the shape the spec refuses at publish), and the Read all / Write all shortcuts dropped the condition.
13
+
-**Now.** Each field row carries **When (edit)**: an authored condition shows as a chip, and the row opens the same CEL editor the row-level-security clauses use, linted the same way; a finding the publish gate would refuse holds the save. Read off clears the condition, Read all / Write all keep it, Clear drops the entry, blank text removes the key. `FieldPerm` gains `when`, carried verbatim by the package-door slice merge.
fix(plugin-grid): a bulk action's `confirmText` fills its `{{ count }}` hole with the number of records the run covers
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
A bulk action's `confirmText` is a `template` slot, and its declared scope names `count`, the number of records the run will cover. The bulk dialog printed the sentence as written, so a person confirming `Archive {{ count }} account(s)?` read the braces instead of the number. The confirm step now fills `{{ count }}` through the same template engine the flow notifications and the single-record `confirmText` render with, so the whitelisted formatters apply (`{{ count | number }}`) and a hole holds no logic. A sentence without a `{{ }}` hole is shown as written, the default sentence is unchanged, and a source the engine refuses is shown as written.
11
+
12
+
A hole naming one of the action's params prints empty for now; filling the params with their display values is the next change.
Copy file name to clipboardExpand all lines: content/docs/references/automation/builtin-node-config.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -278,7 +278,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
278
278
| :--- | :--- | :--- | :--- |
279
279
|**name**|`string`| ✅ | Field name (the flow variable the value binds to) — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}`|
280
280
|**label**|`string`| optional | Display label |
281
-
|**type**|`string`| optional | Input type |
281
+
|**type**|`string`| optional | Input type — an open widget hint (text, number, select, …); `file` renders the platform upload widget and the screen resumes with the uploaded file's id, which a later node writes into a `file` field |
282
282
|**required**|`boolean`| optional | Whether a value is required to submit |
283
283
|**options**|`{ value: any; label: string }[]`| optional | Choices for a select-style field; option values must stay distinct when read as text |
284
284
|**defaultValue**|`any`| optional | Prefilled value: a CEL value envelope `{ dialect: 'cel', source }` evaluated when the screen is served (the client receives the value, never the envelope), or a literal written as it is — a `{…}` template token is refused |
@@ -300,7 +300,7 @@ A value: a CEL value envelope `{ dialect: 'cel', source }` evaluated by the expr
300
300
| :--- | :--- | :--- | :--- |
301
301
|**name**|`string`| ✅ | Field name (the flow variable the value binds to) — a name without a leading `$` (the `$` names are the flow engine's own), read as `{{ name }}`|
302
302
|**label**|`string`| optional | Display label |
303
-
|**type**|`string`| optional | Input type |
303
+
|**type**|`string`| optional | Input type — an open widget hint (text, number, select, …); `file` renders the platform upload widget and the screen resumes with the uploaded file's id, which a later node writes into a `file` field |
304
304
|**required**|`boolean`| optional | Whether a value is required to submit |
305
305
|**options**|`{ value: any; label: string }[]`| optional | Choices for a select-style field; option values must stay distinct when read as text |
306
306
|**defaultValue**|`any`| optional | Prefilled value: a CEL value envelope `{ dialect: 'cel', source }` evaluated when the screen is served (the client receives the value, never the envelope), or a literal written as it is — a `{…}` template token is refused |
Copy file name to clipboardExpand all lines: docs/adr/0066-unified-authorization-model.md
+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
@@ -131,7 +131,7 @@ Captured for the record; **out of scope for Phases 1–4 above**. Each is anchor
131
131
-**⑦ Permission-set groups + subtractive *muting*.** Pure union does not scale governance ("permission-set explosion"); Salesforce added permission-set-group *muting* precisely to allow taking access away. Roles→permission-sets already bundle; a subtractive/deny layer (precedence step 4) is the missing piece for large-org administration. Pairs with delegated admin (#9).
132
132
-**⑧ FLS posture: unlisted fields are visible, and field grants union without deny***(recorded 2026-07 pre-launch architecture assessment)*. Runtime FLS is block-list-shaped: `field-masker.ts` hides a field only when an explicit rule marks it `readable:false`, so an **undeclared** sensitive field is exposed by default, and `getFieldPermissions` merges most-permissively, so one set's `readable:true` permanently out-votes another's `false`. Consequences: protecting a salary/ID-number field relies on discipline (grant it only where needed), not mechanism. The object-level `private` posture (D2/④) mitigates the unlisted-default-visible half for objects that adopt it; the union half is the field-level face of ⑦ — a muting/deny layer must cover **field** grants, not just object grants, when it lands. Until then, treat "sensitive field on a `public` object" as a smell in review.
133
133
134
-
> **Note (2026-10-26) — a field permission entry may carry a row condition (#25866, ruling of record 6408558941).** `FieldPermissionSchema.when` is a row predicate in the same canonical CEL as `rowLevelSecurity[].using`, on the **edit** vote only (`when` beside `readable: false` is refused at parse). The condition **narrows where an entry votes**: on a row that matches, the entry votes as declared; on a row that does not, the entry is **absent**, as if the set had not declared the field. It does **not** change the posture recorded here — field grants still **union without deny**: a matched conditioned lock is out-voted by a sibling's unconditioned `editable: true` exactly as before, an entry without `when` votes on every row, and only declaring entries vote. The write gate judges the stored row before an update and the incoming row of an insert, in process (`RLSCompiler.compileRowPredicate` + `storedFormCheckJudge`); a row that cannot be judged (a predicate update, a stored row the caller cannot read, a condition that does not resolve for the request) and every row-blind reader (`/auth/me/permissions`, the field projections, `os explain`) apply the entry as its lock, so no reading is wider than the gate's. The condition is validated like an RLS predicate at authoring (`rls-predicate-*` findings on `permissions[].fields[].when`). The muting/deny layer ⑦ still owes remains the mechanism for taking an edit away that another set grants.
134
+
> **Note (2026-10-26) — a field permission entry may carry a row condition (#25866, ruling of record 6408558941).** `FieldPermissionSchema.when` is a row predicate in the same canonical CEL as `rowLevelSecurity[].using`, on the **edit** vote only (`when` beside `readable: false` is refused at parse). The condition **narrows where an entry votes**: on a row that matches, the entry votes as declared; on a row that does not, the entry is **absent**, as if the set had not declared the field. It does **not** change the posture recorded here — field grants still **union without deny**: a matched conditioned lock is out-voted by a sibling's unconditioned `editable: true` exactly as before, an entry without `when` votes on every row, and only declaring entries vote. The write gate judges the stored row before an update and the incoming row of an insert, in process (`RLSCompiler.compileRowPredicate` + `storedFormCheckJudge`); a row that cannot be judged (a predicate update, a stored row the caller cannot read, a condition that does not resolve for the request) and every row-blind reader (`/auth/me/permissions`, the field projections, `os explain`) apply the entry as its lock, so no reading is wider than the gate's. The condition is validated like an RLS predicate at authoring (`rls-predicate-*` findings on `permissions[].fields[].when`). Taking an edit away that another set grants remains the job of the muting/deny layer ⑦ still owes.
135
135
- **⑨ Authoring-time validation for capability references** *(recorded 2026-07 pre-launch architecture assessment)*. `systemPermissions` / `requiredPermissions` are free strings today; a typo (`mange_users`) fails closed at runtime — the safe direction — but is undiscoverable: nothing reports that the referenced capability exists nowhere. When D1 lands the `sys_permission` registry, add the authoring/publish-gate lint alongside it: referencing an unregistered capability warns at author time (consistent with ADR-0049 honesty and the contract-first Prime Directive — reject at the producer, don't tolerate at the consumer). **Landed (2026-07):** `validateCapabilityReferences` in `@objectstack/lint` warns (never errors — cross-package capabilities and the fail-closed runtime make a hard gate wrong) when a `requiredPermissions` reference resolves against neither the built-in set (`@objectstack/spec` `PLATFORM_CAPABILITY_NAMES`), nor a capability the stack grants via a permission set's `systemPermissions`, nor a `sys_capability` seed row. Wired into `os validate` and `os lint`. `systemPermissions` (the declaration side) is deliberately not flagged.
136
136
137
137
> **Note (2026-10-14) — ⑨'s seed-row source is gone.** With `sys_capability` retired by #15204
0 commit comments