Skip to content

Commit 2de914e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21727-protocol-incompatible-4xx
2 parents b5bdd1b + ea7ff39 commit 2de914e

24 files changed

Lines changed: 1502 additions & 148 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
A refused flow resume answers the engine's own code, on the REST resume door and the MCP `resume_run` tool alike, as the 17.1.0 release notes, `client.automation.resume()`'s documentation and the flows guide already state (#21724).
7+
8+
Clause-②: yes (widening)
9+
10+
- **What moves on the wire.** `POST /api/v1/automation/:name/runs/:runId/resume` used to hand the error builder a status and no code for the engine's refusals, so `error.code` was derived from the status. It now carries the engine's code:
11+
12+
| Refusal | Status | `error.code` before | `error.code` now |
13+
|---|---|---|---|
14+
| a screen input that breaks the screen's declared fields | 400 | `VALIDATION_ERROR` | `INVALID_SCREEN_INPUT` |
15+
| a signal that writes an engine-reserved `$` name | 400 | `VALIDATION_ERROR` | `INVALID_SIGNAL` |
16+
| an unknown run, a run whose flow is gone, or a run whose paused node was edited away | 404 | `RESOURCE_NOT_FOUND` | `RUN_NOT_FOUND` |
17+
| the suspended-run store is unreadable | 503 | `SERVICE_UNAVAILABLE` | `STORE_UNAVAILABLE` |
18+
| another resume already holds the run | 409 | `RESOURCE_CONFLICT` | `RESUME_IN_PROGRESS` |
19+
20+
`PERMISSION_DENIED` (403) is unchanged. No status moves, nothing that was accepted is refused, and a refused resume still leaves the run paused, so a corrected resume still completes it. A caller that branched on the status-derived code reads the documented one instead: `err.code` from `client.automation.resume()` is now `INVALID_SCREEN_INPUT`, `INVALID_SIGNAL` or `RUN_NOT_FOUND`, which is what lets it tell a bad screen value from a reserved signal name.
21+
- **`@objectstack/spec` — `minor`.** The ADR-0112 error-code ledger registers `INVALID_SCREEN_INPUT` under `@objectstack/service-automation`, beside `INVALID_SIGNAL` and `RUN_NOT_FOUND`. The engine already returned it and the docs already promised it, but `ErrorCode`, and therefore `ApiErrorSchema`, refused it. The published vocabulary gains one member and loses none.
22+
- **`@objectstack/runtime` — `minor`.** The resume door's answer set gains the five codes above. Both doors read one classifier, so the REST route and `resume_run` answer the same code for the same engine result.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
A flow saved through the metadata API is armed on the running engine at once, as hooks and actions saved through the same door already are
6+
7+
Clause-②: no
8+
9+
`PUT /api/v1/meta/flow/:name` answered `200 "Saved flow … (env-wide, state=active)"` and `GET /api/v1/meta/flow/:name` served the row, but the automation engine registered nothing until the process restarted: `GET /api/v1/automation/:name` and `POST /api/v1/automation/:name/trigger` answered `404 Flow not found`, and a record-triggered flow never fired. The engine armed flows only at boot, at `kernel:ready` and on `metadata:reloaded`, and only the publish doors announce that event.
10+
11+
The automation service now listens to the metadata protocol's post-write signal (`onMetadataMutation`), the one ObjectQL already re-binds authored hooks and actions on, and makes the engine follow the stored row of each flow it names:
12+
13+
- an active save or a publish registers the flow, or re-registers it over the definition the engine held;
14+
- a save whose `status` is `'obsolete'` or `'invalid'` keeps it registered and unbound, as a boot does;
15+
- a delete unregisters it;
16+
- a draft save changes nothing until it is published.
17+
18+
The flow is re-read through the same execution view, precedence and env-wide scope the boot reads, so a save never arms a flow beyond the reach a restart would give it. The save answers first, and the registration follows it by one read of the stored metadata.
19+
20+
A publish raises this signal and `metadata:reloaded` together, and the published flow is still registered once, not twice. `PUT /api/v1/automation/:name`, which registers the flow before it saves it, is not registered a second time either.
21+
22+
A run already executing keeps the definition it started with. A suspended run resumes against the definition registered when it resumes, and answers `RUN_NOT_FOUND` once its flow is deleted. Both already held for a re-registration through a publish or `PUT /api/v1/automation/:name`.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/driver-sql": patch
3+
---
4+
5+
A connect to a SQLite file that is already `auto_vacuum=INCREMENTAL` no longer writes to it. `SqlDriver.connect()` now reads `PRAGMA auto_vacuum` first and runs `PRAGMA auto_vacuum = INCREMENTAL` only when the file answers something else.
6+
7+
Clause-②: no
8+
9+
- **What changed on disk.** On a file that already answered INCREMENTAL, the setter changed no mode, but it still stamped two header counters: the file change counter (bytes 24–27) and the version-valid-for number (bytes 92–95). So a read-only command such as `os migrate duplicates`, which the CLI docs say writes nothing at all, changed the file's md5 on every run. Reading the pragma changes no bytes, so such a file now comes through a connect, and a whole `os migrate duplicates` run, byte-identical.
10+
- **Unchanged.** A fresh file and `:memory:` still come out INCREMENTAL. A legacy NONE file that already holds tables still gets the setter, which leaves it NONE until a `VACUUM` (`os db clean`), exactly as before. The journal-mode step already read first and set only on a difference, and it is untouched. Postgres and MySQL issue no PRAGMA.
11+
- **The WASM SQLite driver** inherits the rule. A connect and disconnect no longer rewrites an already-INCREMENTAL image file, because the setter was what marked the image dirty.
12+
- ⛔ No config key, export, error code or accepted input changes.

‎content/docs/automation/flows.mdx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,15 @@ export default defineStack({
18451845
});
18461846
```
18471847

1848+
A flow authored at runtime through the metadata API follows its stored row
1849+
on the running engine, with no restart. An active save
1850+
(`PUT /api/v1/meta/flow/:name`) or a publish registers the flow, or
1851+
re-registers it over the definition the engine held. A save
1852+
whose `status` is `'obsolete'` or `'invalid'` keeps it registered and
1853+
unbound, as a boot does. A delete unregisters it. A draft save changes
1854+
nothing until it is published. The write answers first, and the
1855+
registration follows it by one read of the stored metadata.
1856+
18481857
Registration is not arming. A `record_change`, `schedule`, `time_relative` or
18491858
`api` flow fires only when its trigger is installed, and that takes **both**
18501859
capability tokens on the same stack: `requires: ['automation', 'triggers']`.

‎content/docs/references/api/contract.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const result = ApiErrorSchema.parse(data);
2828

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
31-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +320 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
31+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +321 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3232
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
3333
| **message** | `string` | ✅ | Readable error message |
3434
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -209,6 +209,7 @@ const result = ApiErrorSchema.parse(data);
209209
* `INVALID_PHONE`
210210
* `INVALID_REQUEST`
211211
* `INVALID_RESUME_TOKEN`
212+
* `INVALID_SCREEN_INPUT`
212213
* `INVALID_SIGNAL`
213214
* `INVALID_SIGNATURE`
214215
* `INVALID_STATE`

‎content/docs/references/api/error-code-ledger.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ const result = ErrorCode.parse(data);
376376
* `INVALID_PHONE`
377377
* `INVALID_REQUEST`
378378
* `INVALID_RESUME_TOKEN`
379+
* `INVALID_SCREEN_INPUT`
379380
* `INVALID_SIGNAL`
380381
* `INVALID_SIGNATURE`
381382
* `INVALID_STATE`

‎docs/qa/platform-checklist/areas/access-security.json‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@
412412
"title": "CRUD × permission-set matrix: every access-matrix.json row holds — allowed verbs succeed, withheld verbs 403, VAMA bounded",
413413
"since": "v15",
414414
"status": "active",
415-
"revision": 4,
415+
"revision": 5,
416416
"priority": "P0",
417417
"surface": "api",
418418
"personas": [
@@ -430,7 +430,8 @@
430430
"every authenticated member ALSO holds the everyone baseline showcase_member_default additively (ADR-0090 D5) — the effective expectation per cell is the UNION of the tested set's row and the baseline's row for that object; compute the union before judging a cell, or a baseline-granted read will look like a matrix violation. Measured on the committed table: 9 of the 164 showcase_* cells (41 rows × 4 verbs at revision 4) are granted ONLY by the baseline (showcase_auditor and showcase_executive can CREATE an inquiry and create+edit a private note; guest_portal can READ an inquiry; showcase_manager and showcase_ops can CREATE an inquiry). showcase-crud-persona-matrix.dogfood.test.ts computes the union and asserts those 9 as allowed.",
431431
"access-matrix.json is the APP's declared matrix; the platform's own baseline permission sets are additionally in force and grant some sys_* surface the app matrix never mentions (measured: a plain member reads /data/sys_user 200, self-scoped). So the absence of a row is NOT a prediction of denial on sys_* objects, and the automated sweep judges the showcase_* rows only — see automated.ref.",
432432
"clause 5 (guest_portal create-without-read) is UNOBSERVABLE on a stock boot — record it blocked(fixture), never pass or fail. The guest anchor ships unbound (binding guest → showcase_guest_portal is an admin action; the app only ships the capability, permission-sets.ts GuestPortalPermissionSet), a member persona holding the set also holds the baseline's read on showcase_inquiry, and QA run #21056 measured REST anonymous create AND read both answering 401 even with the anchor bound. The only anonymous create lane is the public form (publicFormGrant on the inquiry form view, ADR-0056 Option A), whose grant is create plus read-back of the row it created — a different contract from this clause's",
433-
"sys_user create and delete answer 405 OBJECT_API_METHOD_NOT_ALLOWED for EVERY caller, admin included: sys_user declares apiMethods ['get','list','update','bulk'] (identity writes belong to better-auth, ADR-0092), and the API-exposure gate answers before any permission is judged. Score those two delegate × sys_user cells against 405, not 403"
433+
"sys_user create and delete answer 405 OBJECT_API_METHOD_NOT_ALLOWED for EVERY caller, admin included: sys_user declares apiMethods ['get','list','update','bulk'] (identity writes belong to better-auth, ADR-0092), and the API-exposure gate answers before any permission is judged. Score those two delegate × sys_user cells against 405, not 403",
434+
"A1 (acceptance[0], every allowed cell succeeds) is a KNOWN FAIL on 17.x until the v18 ADR-0131 C1 business-unit leg lands (#21057, target:v18, gated by #15193): the seeded sys_business_unit rows carry organization_id null, so the showcase_field_ops_delegate × sys_user_position in-subtree create (bu_west_coast under bu_field_ops) answers 403 PERMISSION_DENIED \"outside the delegated subtree\" and the delegate's scope resolves to an empty subtree (the gate fails closed: over-refusal, never exposure). Measured discriminator (#21057 comments 5927366808 and 5979438043): once the organization is stamped on the seeded units, the in-subtree create flips to 201 while the out-of-subtree create (bu_hq_finance) stays 403 — so the cause is the unstamped seed, not the gate. Score A1 FAIL in the run table and read it as known (#21057), not new; do NOT re-derive it, do NOT weaken the clause, do NOT stamp the units to make it pass (that would hide the very gap this entry tracks). It turns green when the v18 C1 leg lands — that flip is the signal the gap closes, and this entry is then removed (ruling: #21057 comment 5980557328, ADR-0131 D3/D14)"
434435
]
435436
},
436437
"steps": [
@@ -535,6 +536,12 @@
535536
"date": "2026-10-01",
536537
"change": "checklist-accuracy findings of QA run #21056. Counts re-measured from access-matrix.json: 47 rows (41 showcase_* + 6 delegate sys_*), the pin's 164 cells split 87 allow / 77 deny, the same 9 baseline-only cells; showcase_client_liaison joined the variants and the seeded-set list. acceptance[1] names the 405 OBJECT_API_METHOD_NOT_ALLOWED sys_user create/delete cells (sys_user apiMethods). Clause 5 recorded unobservable on a stock boot (knownGaps; steps[5]). automated.ref no longer reads the permission-zoo pin as vouching for the dev boot's delegate cells (#21057). steps[0] mints members invite-first under the invite_only default; a new step names the ownership-floor trap on the delegate's edit/delete probes",
537538
"ref": "#21060"
539+
},
540+
{
541+
"revision": 5,
542+
"date": "2026-10-04",
543+
"change": "knownGaps records that A1 is a known FAIL on 17.x: #21057 (seeded business units carry no organization, so the field-ops delegate's in-subtree create is refused) is ruled to wait for ADR-0131 C1's business-unit leg in v18 (target:v18, gated by #15193). The clause, the priority and every other item are unchanged; A1 stays a FAIL in the run table and goes green when the v18 leg lands",
544+
"ref": "#21747 (maintainer ruling: #21057 comment 5980557328)"
538545
}
539546
]
540547
},

0 commit comments

Comments
 (0)