Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 36 additions & 19 deletions content/docs/api/error-catalog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -181,28 +181,40 @@ substitute. See the [Data API](/docs/api/data-api).
### `MISSING_REQUIRED_FIELD`
**Cause:** A required field was not provided in the request body.
**Fix:** Include the missing field. Check `fields` for the field name — **except** on the
`controlled_by_parent` stored-master path below, where the status is `422` and no `fields`
`controlled_by_parent` stored required-master path below, where the status is `422` and no `fields`
array is sent; read the field name out of the message there.
**Retry:** `no_retry`

<Callout type="warn">
**`MISSING_REQUIRED_FIELD` is `400` with one exception: a `controlled_by_parent` detail
whose stored master reference is null answers `422`, without `fields`.**
whose stored reference to a required master is null answers `422`, without `fields`.**

An object whose `sharingModel` is `controlled_by_parent` derives its access from a master
record, so the gate authorizing writes to it resolves that master *before* the executor —
and the executor is where required-field validation runs. An update or delete **by id** of
a detail record whose *stored* master reference is null answers
`422 MISSING_REQUIRED_FIELD`, whatever the declaration: the caller sent no such field, so
nothing could be named in `fields` and no payload would fix it.
a detail record whose *stored* master reference is null, where the master is declared
`required: true`, answers `422 MISSING_REQUIRED_FIELD`, whether or not the master is marked
`readonly` or `system`: the caller sent no such field, so nothing could be named in `fields`
and no payload would fix it.

An optional master never answers this code. When the object's only `master_detail` is
declared `required: false` (an optional master, ADR-0055 §1 as amended), a stored empty
reference is not an error: the row belongs to no master and is its owner's. Its owner and a
holder of View All on the detail read it; its owner and a holder of Modify All on the detail
update or delete it. Everyone else who reaches the gate, View All included, is refused
`403 PERMISSION_DENIED`; a principal who cannot read the row meets the read door first
(`404 RECORD_NOT_FOUND`), as for every hidden row.

A request that leaves the master reference out, or empties it, does not answer this code.
On a `master_detail` that is `required` and neither `readonly` nor `system`, validation
answers it with the documented `400 VALIDATION_FAILED` and `fields`. A master reference
marked `readonly` or `system` is not one an author's write supplies, so it cannot be the
master (ADR-0055 §1): `ObjectSchema` refuses that declaration at parse
(`master-not-writable`), and on an object that reaches the runtime without the parse, every
write that sets the reference answers `422 INVALID_METADATA`:
On a `master_detail` that is `required: true` and neither `readonly` nor `system`, validation
answers it with the documented `400 VALIDATION_FAILED` and `fields`. On an optional master
an insert that leaves it out is not refused, and its creator owns the row; an update that
empties it is judged by the master it leaves, asking edit access on it
(`403 PERMISSION_DENIED` without it). A master reference marked `readonly` or `system`,
required or optional, is not one an author's write supplies, so it cannot be the master
(ADR-0055 §1): `ObjectSchema` refuses that declaration at parse (`master-not-writable`), and
on an object that reaches the runtime without the parse, every write that sets the
reference answers `422 INVALID_METADATA`:

- every insert, single or batch, whether or not it names a master;
- every update whose body carries the reference key, whatever the value, `null` included.
Expand All @@ -211,19 +223,24 @@ Its message starts `[Security] Configuration defect:` and carries the parse's ow
naming the object, the field, the flag and the fix. A delete, and an update that leaves the
key out, are answered as on any detail.

A `master_detail` with no `required`, and a `required` `lookup` on an object that declares no
`master_detail`, are not master references at all: the master is exactly one `master_detail`
declared `required: true` (ADR-0055 §1). `ObjectSchema` refuses such an object at parse, and
one that reaches the runtime unparsed answers every write `422 INVALID_METADATA`.
The master is exactly one `master_detail`: the one declared `required: true`, or, when the
object declares none, its only `master_detail` declared `required: false` (ADR-0055 §1 as
amended). Two `master_detail` fields with none required, two required ones, and a
`required` `lookup` on an object that declares no `master_detail` name no master at all:
`ObjectSchema` refuses such an object at parse, and one that reaches the runtime unparsed
answers every write `422 INVALID_METADATA`. Write `required` on the master either way:
`ObjectSchema.create()` forces an omitted one to `true` and `os lint` reports the omission
at `error`, while a raw parse reads it as `false`, the optional master.

The full matrix is on the protocol page:
[`MISSING_REQUIRED_FIELD`](/docs/protocol/kernel/error-handling#missing_required_field).

The `INVALID_METADATA` refusal is not going to be relaxed: required-field validation skips
`system` and `readonly` fields before its required check, so on those two declarations the
gate is the only thing standing between the request and a detail record outside its master
— which the `controlled_by_parent` read filter (`fk IN (readable masters)`) can never match,
leaving a record readable by nobody.
gate is the only thing standing between the request and, under a required master, a detail
record outside its master — which the read filter of a required master
(`fk IN (readable masters)`, with no arm for an empty reference) can never match, leaving a
record readable by nobody.

**Branch on `code`, treat `fields` as optional, and read the status off the response**
rather than deriving it from the table at the end of this page.
Expand Down Expand Up @@ -1011,7 +1028,7 @@ async function handleApiCall() {
| 404 | `not_found` | `RECORD_NOT_FOUND`, `OBJECT_NOT_FOUND`, `ENDPOINT_NOT_FOUND` |
| 409 | `conflict` | `CONCURRENT_MODIFICATION`, `UNIQUE_VIOLATION`, `DELETE_RESTRICTED` |
| 413 | `validation` | `PAYLOAD_TOO_LARGE` — an upload over the storage settings' `max_upload_mb`, or an import over its row ceiling (see [above](#payload_too_large)) |
| 422 | `validation` | `MISSING_REQUIRED_FIELD` on a `controlled_by_parent` detail whose stored master reference is null (see [above](#missing_required_field)) — this row is an exception to the 400 row, not a second home for the code |
| 422 | `validation` | `MISSING_REQUIRED_FIELD` on a `controlled_by_parent` detail whose stored reference to a required master is null; an optional master's empty reference is not an error (see [above](#missing_required_field)) — this row is an exception to the 400 row, not a second home for the code |
| 429 | `rate_limit` | `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED` |
| 500 | `server` | `INTERNAL_ERROR`, `DATABASE_ERROR` |
| 501 | `server` | `NOT_IMPLEMENTED` — the storage routes answer it when the adapter cannot issue presigned URLs |
Expand Down
23 changes: 15 additions & 8 deletions content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2139,7 +2139,7 @@ can produce, because both supply a live one.
| Types with no repository write path (`agent`) | Their write path records no history and would force a draft live — a half-write is worse than leaving the row to the read path |
| Rows that still fail the current schema after conversion | That is a genuine contract violation, not chain-owned history. The write path's rejection is correct; fix the row in Studio |
| A site the conversion chain leaves as stored because no lossless rewrite exists — above all a page filter carrying `$and` / `$or` / `$not` | Flattening a combinator changes which rows the page selects, so it is never done. Each site is printed as a `TODO` line under its row — path, block, and what blocks the rewrite — whatever the row's outcome; a row with nothing but TODOs is reported `skipped`. It does not fail the run, since no run of this pass can clear it: rewrite each site by hand |
| A stored shape protocol 18 refuses and no conversion rewrites — a `controlled_by_parent` object without exactly one required `master_detail` (two masters, none, a required `lookup` only, an explicit `required: false`), a view bound to the retired view-level API data source or a page component whose `data` prop binds it (`object-grid`, `object-map`, `object-gantt`, `object-tree`), a flow value slot carrying a `{…}` token (a `get_record` / `update_record` / `delete_record` filter operand among them) | Each would need a choice only its owner can make — which master, which page, what an absent value should do — so it is never rewritten. Each site is printed as a `TODO` line under its row, naming the step-18 entry that refuses it, the object, view, page component or flow node, and the owner's choices; a binding's request headers are withheld. Until it is rewritten by hand, the row is refused: an object registers but derives no master, a view registers under `[metadata_spec_invalid]`, a page still loads, its next save is refused at the metadata save door (422), and `os validate`, `os build` and `os lint` refuse the binding in source, and a flow is not registered |
| A stored shape protocol 18 refuses and no conversion rewrites — a `controlled_by_parent` object without exactly one master, which is one `master_detail` declared `required: true` or, with none, its only `master_detail` declared `required: false` (two masters, none, a required `lookup` only; an explicit `required: false` on the only `master_detail` is that optional master, admitted by `cbp-optional-master-admitted` and not listed), a view bound to the retired view-level API data source or a page component whose `data` prop binds it (`object-grid`, `object-map`, `object-gantt`, `object-tree`), a flow value slot carrying a `{…}` token (a `get_record` / `update_record` / `delete_record` filter operand among them) | Each would need a choice only its owner can make — which master, which page, what an absent value should do — so it is never rewritten. Each site is printed as a `TODO` line under its row, naming the step-18 entry that refuses it, the object, view, page component or flow node, and the owner's choices; a binding's request headers are withheld. Until it is rewritten by hand, the row is refused: an object registers but derives no master, a view registers under `[metadata_spec_invalid]`, a page still loads, its next save is refused at the metadata save door (422), and `os validate`, `os build` and `os lint` refuse the binding in source, and a flow is not registered |
| Any other site `registerFlow` refuses in a stored flow the parse accepts — a text slot's template hole, a decision's `mode`, a malformed predicate, a plugin node's undeclared config key, an `api` flow with no secret | The flow's own definition is wrong, and no conversion rewrites it. Each site is printed as a `TODO registerFlow` line under its row, in the words `registerFlow` refuses it with; a site already listed under a step-18 entry is not repeated. Until it is fixed, the flow is not registered at boot and never runs. A flow the parse itself refuses is reported among the rows that could not be rewritten, in the parse's words |

**One thing it lists and never writes: decision nodes that changed meaning at
Expand Down Expand Up @@ -2706,10 +2706,11 @@ os verify --dashboard-windows # Also prove each dashboard date
reported inconclusive, never proven.
With `--parent-write-reach`, another fresh boot proves ADR-0055's write rule
for every object declared `controlled_by_parent` (exactly one
`master_detail` field names its master): editing, deleting or creating a
detail is admitted exactly when the caller holds that verb on the detail's
object **and** can edit its master. The first half is read from the
server's effective permissions for the caller (`/auth/me/permissions`); the
`master_detail` field names its master, required or optional): editing,
deleting or creating a detail is admitted exactly when the caller holds
that verb on the detail's object **and** can edit its master. The first
half is read from the server's effective permissions for the caller
(`/auth/me/permissions`); the
second is measured, by an empty update of the master as the caller, which
runs every write gate the master has — for a master that is itself
controlled by its parent, the gate above it too. The writes go through the
Expand All @@ -2726,9 +2727,15 @@ os verify --dashboard-windows # Also prove each dashboard date
answer disagrees fails the run, and so does a persona that could not be
provisioned; a refusal that is not the platform's `403 PERMISSION_DENIED`
naming the detail and the operation (a rule, a hook's own code, a refusal
on another object) is reported inconclusive, never proven. An object
with two `master_detail` fields, none, or a chain at the write gate's depth
bound is reported `excluded`. A detail two levels below its root master (a
on another object) is reported inconclusive, never proven. For an optional
master (its only `master_detail` declared `required: false`) the run also
proves the records that name no master: such a record is its owner's, so
its edit or delete is admitted exactly when the caller holds that verb on
the detail's object **and** owns the record (`owner_id`) or holds Modify All
on that object, and a create that names no master exactly when the caller
holds create on it; a null-master record the persona cannot read is not
probed. An object with two `master_detail` fields, none, or a chain at the
write gate's depth bound is reported `excluded`. A detail two levels below its root master (a
line under a quote under an account) is answered the same way: its write is
admitted exactly when the caller holds the verb on the detail and may update
the middle master directly, which also needs edit access to the root, and
Expand Down
Loading
Loading