@@ -216,6 +216,47 @@ need no changes, other than gaining enforcement of the `requiredPermissions`
216216they already declared. Callers that hard-coded a ` target ` in an action URL
217217switch to the action's ` name ` .
218218
219+ ### An action's declared params are enforced at dispatch (ADR-0104 D2, #3438 )
220+
221+ An action's ` params[] ` (` required ` , ` options ` , ` multiple ` , ` reference ` ) was a
222+ complete contract that informed ** only the client dialog** . The server passed
223+ ` reqBody.params ` to the handler unvalidated on both the REST and MCP paths, so
224+ a wrong bag could return a success envelope while the handler quietly ignored
225+ it — the #3405 shape, one layer out. It is now checked before the handler runs;
226+ a violation is ` 400 VALIDATION_FAILED ` (REST) or a thrown error (MCP).
227+
228+ | Violation | Example |
229+ | ---| ---|
230+ | missing ` required ` param | declared ` p_text ` absent from the bag |
231+ | value outside ` options ` | ` p_priority: 'NOT_AN_OPTION' ` |
232+ | ` multiple ` shape | a bare string where an array is declared |
233+ | ` reference ` shape | a non-id where ` reference: 'sys_user' ` is declared |
234+ | undeclared key | ` bogus: 123 ` |
235+
236+ ``` diff
237+ - OS_ACTION_PARAMS_STRICT_ENABLED=1 # removed — enforcement is the default
238+ + OS_ALLOW_LAX_ACTION_PARAMS=1 # escape hatch: warn and pass, as before
239+ ```
240+
241+ ** Only already-wrong calls break** , and each rejection names the offending param
242+ and the declared list, so the fix is one edit at the call site. Actions
243+ declaring no ` params ` are untouched — there is nothing to validate against —
244+ and the dispatcher's own ` recordId ` / ` objectName ` are allowlisted, so keys
245+ dispatch merges in are never unknown-key errors. If an integration you cannot
246+ reach in time is affected, ` OS_ALLOW_LAX_ACTION_PARAMS=1 ` restores the old
247+ pass-through in one restart; the violation still logs once per action, so the
248+ drift stays visible rather than becoming invisible again.
249+
250+ The RC line carried this warn-first behind an ` OS_ACTION_PARAMS_STRICT_ENABLED `
251+ opt-in. ** That variable does not exist in 17.0.** The window was closed on
252+ purpose rather than deferred to 18.0: what a violation strands is a * caller* ,
253+ not data — no stored row becomes unwritable, the party who sees the error is
254+ the party who can fix it, and the hatch makes it reversible. Deferring would
255+ have charged every deployment a second upgrade ceremony to postpone a break
256+ that costs one edited call. The value-shape half of the same ADR went the other
257+ way for the opposite reason: it rejects on the basis of data already at rest,
258+ so it stays gated per deployment (see * Files become platform records* below).
259+
219260### A flow run with no trigger user may not touch data (#3760 )
220261
221262An effective ` runAs: 'user' ` run that resolved ** no trigger user** used to
@@ -925,10 +966,10 @@ For media fields specifically:
925966 toward an explicit ` url ` field, so "managed file" and "external link" stop
926967 being the same declaration).
927968
928- ** Value-shape checking is warn-first. ** A not-yet-backfilled row still writes and
929- the author gets a warning naming the field. Hard rejection arrives only when a
930- deployment opts into ` OS_DATA_VALUE_SHAPE_STRICT_ENABLED ` — which it should do
931- * after * running the backfill and confirming reconciliation :
969+ ** Value-shape checking follows your own migration, not the version number. ** A
970+ not-yet-backfilled row still writes, and the author gets a warning naming the
971+ field. Media fields start * rejecting * malformed values only once ** this
972+ deployment ** has run the migration and passed its self-check :
932973
933974``` bash
934975os migrate files-to-references # dry run: reports, writes nothing
@@ -938,9 +979,9 @@ os migrate files-to-references --apply # converts, verifies, records the flag
938979The run backfills legacy file-field values (inline metadata blobs, own-resolver
939980URLs, ` data: ` URIs) into owned ` sys_file ` references and reconciles the ownership
940981ledger against what records actually hold. The ** deployment-level flag it
941- records** — never the platform version — is what authorises irreversible
942- behaviour, and media value shapes enforce only once * this * deployment has
943- verified its own migration.
982+ records** — never the platform version — is what authorises both strict media
983+ value shapes and irreversible file collection. Upgrading changes neither;
984+ running the migration does .
944985
945986** Released-file collection is live behind that same flag** (#3459 ). On a
946987verified deployment, a field file whose one owning record lets go — the field
@@ -952,6 +993,16 @@ reclaiming the row and its bytes. A deployment that never migrates keeps every
952993released file forever: upgrading is not consent — passing your own
953994migration's self-check is.
954995
996+ Two knobs sit either side of that flag on the value-shape half.
997+ ` OS_ALLOW_LAX_MEDIA_VALUES=1 ` returns a verified deployment to warnings, for an
998+ operator who hits an unforeseen rejection and needs writes flowing while they
999+ diagnose. ` OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1 ` goes the other way and opts
1000+ * every* value class into strict immediately — including the reference
1001+ (` lookup ` /` user ` /…) and structured-JSON (` location ` /` address ` /…) types, whose
1002+ own per-deployment gate is still being built (#3438 ). Those stay warn-only by
1003+ default until it lands, because unlike media they have no migration standing
1004+ behind them yet.
1005+
9551006### Approvals: dynamic approver routing (#3447 )
9561007
9571008- ** ` expression ` approvers.** A CEL expression resolves * who* approves at node
@@ -1237,9 +1288,17 @@ objectui commits on top of the pin 16.1.0 shipped.
12371288- ** Auth:** plan for the better-auth 1.7 account-identity backfill — check the
12381289 boot log for federated accounts whose IdP is no longer registered, and stamp or
12391290 remove those rows.
1291+ - ** Actions:** check any programmatic caller that posts ` params ` — a bag the
1292+ server used to accept silently now 400s if it misses a ` required ` param,
1293+ breaks ` options ` /` multiple ` /` reference ` , or carries an undeclared key. The
1294+ error names the param and the declared list. ` OS_ALLOW_LAX_ACTION_PARAMS=1 `
1295+ buys time for an integration you cannot reach today.
12401296- ** Files:** run ` os migrate files-to-references ` (dry run first, then
1241- ` --apply ` ), reconcile, and only then set
1242- ` OS_DATA_VALUE_SHAPE_STRICT_ENABLED ` .
1297+ ` --apply ` ) and reconcile — passing its self-check is what turns on strict
1298+ media value shapes * and* released-file collection for this deployment, so
1299+ read the report before you ` --apply ` . Do ** not** reach for
1300+ ` OS_DATA_VALUE_SHAPE_STRICT_ENABLED ` to get there: it opts every value class
1301+ in at once, including ones with no migration behind them.
12431302- ** Datasources:** verify every declared datasource connects in every
12441303 environment — a bound datasource that cannot connect now fails the boot
12451304 instead of failing every later query.
0 commit comments