# Part XVII. Commands with an envelope

*This part is proposed for 0.2. It carries out what the steward decided on 11 October 2026 for 0.2 (open question 5): a common command envelope around every command or batch, machine-readable error codes for refusals, and a precondition on every command (Part V, 3, 6 and 10; the changelog's "For 0.2"). It is not part of OpenEik 0.1, and no 0.1 conformance class requires it. A batch of 0.1 stays valid (section 8). Its own questions, open questions 53 to 60, the steward decided on 25 October 2026, each as this part proposed it (section 14). `@openeik/tools` applies it when asked for the proposal, since 25 October 2026 (section 13).*

## 1. Status and scope

Part V says what a command is, how a processor checks it and what it answers. Part VII says how an agent reaches a processor. Neither says who sent a batch, what it was made from, whether it is a second try of one already applied, or what is wrong with a refused command in terms a program can act on. An agent that times out cannot tell whether its batch was applied; a host cannot tell an agent's write from its person's; a refusal is a sentence.

This part says it. A writer sends its batch in an **envelope**: an id for the operation, who sends it, the line of work and the version the commands were made from, what to do with them, and why, in words. A processor answers every refusal with a **code**, the member of the command that is wrong, a message and, where one is known, a fix. And every command names the state it was made from.

What it leaves out:

- **Transport.** How an envelope travels (an HTTP request, an MCP tool call, a file) is the host's. OpenEik still defines no protocol between stores (Part VI, 6; open question 28).
- **Accounts.** Who a person is, and which accounts and agents may act for them, is the host's. The envelope names them; the host checks them (section 4).
- **The log.** Part V, 10 leaves the format of a log of accepted batches open, and so does this part, beyond what a processor records of an operation (section 3).
- **Undo across writers, and change events.** An undo that can be rebased, and events that say why a record changed, are later work.

### 1.1 At 0.2 this text moves into

- Part V: the envelope as a section of its own; the codes into 6 and 9; rule 4 of 3 made a requirement, and the fourteen kinds' preconditions into the tables of 2;
- Part VI: the agent beside the author in a version (3), and a proposal made from an operation (8.7);
- Part VII: attribution and the codes in the write loop (6), in place of 10's note;
- Conformance: what a command processor and an agent host do with an envelope;
- Security and privacy: attribution as personal data, an agent's identity, and operation ids.

## 2. The envelope

An envelope is a JSON object, held to [`schemas/proposed/envelope/envelope.schema.json`](https://openeik.org/schemas/0.1/proposed/envelope/envelope.schema.json):

| Member | | Meaning |
|---|---|---|
| `format` | required | `"openeik-envelope/0.1"` |
| `operationId` | required | the operation's id (section 3) |
| `by` | required | who sends it: `{person, agent?, app?}` (section 4) |
| `target` | required | the line of work and the version the commands were made from: `{ref, version, exact?}` (section 5) |
| `mode` | required | `apply`, `dryRun` or `propose` (section 6) |
| `intent` | required | what the change is for, in words for people (section 7) |
| `commands` | required | the batch: one or more commands of Part V, in order |

An agent renames the west wall of `examples/one-storey-house`, read at a version of `main`:

```json
{
  "by": {"agent": {"name": "an-agent", "session": "s-41c2", "version": "2.1"}, "app": "an-app 1.0", "person": "u-7f3c"},
  "commands": [
    {"expectedRevision": 1, "id": "wall-west", "kind": "update", "patch": {"name": "West wall (render)"}}
  ],
  "format": "openeik-envelope/0.1",
  "intent": "Name the west wall by its finish, as the client asked",
  "mode": "apply",
  "operationId": "0b6f6a4e-2c1d-4f7e-9a51-3d2b8c7e1f00",
  "target": {"ref": "main", "version": "7d1e9f3b2c4a6e8d0f1b3c5e7a9d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c1e3a5d"}
}
```

1. A processor MUST refuse an envelope whose `format` it does not implement (`envelope-format`), and one that does not validate against its schema (`envelope-schema`), naming the member that is wrong. It applies none of its commands.
2. A member the envelope does not define MUST be refused (`envelope-schema`), but an extension member (`<prefix>:<name>`, Part I, 11.2), which is kept and has no meaning here. A misspelt member of an envelope changes what happens to every command in it: `operationID` would lose the operation's id, and `exacts` the writer's wish.
3. The commands are a batch of Part V, 7: applied in order, all or nothing, and checked as Part V, 6 says, each with its kind's precondition (section 9).
4. Nothing in an envelope but its commands changes the model. Its other members say whether, where and for whom the commands are applied, and what is recorded beside them (section 4.3).

*Note (informative).* The plan's envelope (the specification plan of 9 October 2026, and the decision register's F11) named its members `protocol`, `opId`, `actor` (`principal`, `agent`, `onBehalfOf`, `session`), `target` (`branch`, `base`), `mode`, `intent` and `ops`, and had a list of `profiles` beside them. This part names them as the proposal file of Part VI, 8.7 does (`format`, `by`, `intent`, `commands`), so that a proposal made from an operation carries them across unchanged (section 6.3). Profiles are left to capability negotiation, which Part VII, 10 leaves for later.

## 3. The operation

An **operation** is one envelope, and what it does. Its id lets a writer send it again, after a lost answer or a timeout, without its commands being applied twice.

1. **The id.** `operationId` is 8 to 128 ASCII letters, digits and `.`, `_`, `:` and `-`, starting with a letter or a digit. A writer makes one for each operation and SHOULD make it a UUID (RFC 9562, version 4 or 7). It MUST NOT give two operations one id, and MUST NOT put personal data into an id.
2. **The same operation.** Two envelopes with one id are one operation sent twice where their **digests** are equal: the SHA-256, as 64 lowercase hexadecimal digits, of the envelope's canonical JSON (Part III, 6), every member included. A writer that sends an operation again sends the same envelope.
3. **A processor that keeps a log** of what it applies (a host, a version store, an agent host that keeps a folder) MUST record each operation it applies, by its id, with its digest and its answer, together with the change it applied: in one transaction with the log entry, the version or the ref's move, so that it never holds the change without the record, nor the record without the change. It MUST keep the record for at least 24 hours after the operation, and SHOULD keep it as long as it keeps the change's log entry. For an id it has recorded, it MUST then:
   - for an envelope with the same digest, answer with the recorded answer, marked as such (`operation.repeated: true`), and apply nothing;
   - for an envelope with another digest, refuse it (`operation-reused`) and apply nothing.

   For an id whose operation it is still applying, it MUST refuse the second (`operation-in-progress`), and apply nothing; the writer sends it again later.
4. **What is recorded.** An operation applied is recorded, and so is a proposal a host makes (6.3, rule 2), since each changes something. A refused operation, a dry run and a proposal file (6.3, rule 1) change nothing on the target, and are not recorded: the writer may send the same id again, with the same envelope or a mended one.
5. **A processor that keeps no log** (a library that applies commands to a model it is given) cannot tell a second try from a first. It MUST say so in its answer (`operation.kept: false`), and applies the operation as a new one. In an envelope every command gives its precondition (section 9), so an operation applied before is refused the second time, with a `revision-conflict` or an `id-duplicate`, never applied twice; the writer reads the records again and sees whether its first try was applied.
6. An operation's id is no part of the model, nor of a version (Part VI, 3): it is the processor's record of what was sent, and a squashed push makes one version of many operations (Part VI, 6.3, rule 3).

*Note (informative).* What the reference implementation does: its commands carry no id (`commandSchema`). Its bridge gives each request a UUID for the transport alone, and its cloud log keeps one accepted batch per row, sequenced by compare-and-swap (`append_commands(project, expected_seq, batch, author, client)`, `project_commands`: `seq`, `commands`, `author`, `client_id`); `client_id` lets a page know its own batch when it comes back, not a retry. A batch whose answer was lost is refused the second time by its preconditions where it has them (`Revision conflict …`, `Element ID already exists.`), and applied twice where it has none. Its billing records do what rule 3 asks, by an idempotency key, with a key taken by another purchase refused (`idempotency_key_mismatch`). Reference: `asLogged` and `batchError` in `src/core/model/store.ts`; `append_commands` in `supabase/migrations/20260919160000_client_log_writes.sql`.

## 4. Attribution

### 4.1 Who sends it

`by` says for whom the operation is made, which agent made it, if one did, and the application it came through:

| Member | | Meaning |
|---|---|---|
| `person` | required | the person the operation is made for, by an id the processor knows them by |
| `agent` | where an agent made it | `{name, version?, session?}`: the agent, as it names itself, its version, and the session it works in, as the host named it |
| `app` | optional | the application the operation came through, with its version, as a proposal file's `by.app` names one (`"an-analysis-app 0.5.0"`) |

1. `person` is an id: on a host, the id of the person's account; in a folder, a person's id of Part XIV, 5, or the id the person gives. It MUST NOT be an email address (Security and privacy, 2.3).
2. An operation that an agent made, on a person's word or on its own, MUST name the agent. One that a person made in an application's own interface names none.
3. An agent writes for a person: `person` is the person it works for, never the agent. An agent that works for no one in particular (a scheduled check) writes for the person who set it to work.

### 4.2 What a host checks

1. A host MUST authenticate the writer of an envelope, and MUST refuse one whose `by.person` is neither the person it authenticated nor one that person may act for by the host's rules (`person-mismatch`).
2. What an agent says of itself proves nothing: its `name`, `version` and `session` are its words. A host that can tell an agent's credential from a person's, such as a token issued to a client other than the host's own pages, MUST record an operation sent with an agent's credential as an agent's, naming the client it checked, even where the envelope names no agent; and MUST NOT record such an operation as the person's own.
3. An agent writes with the rights of the credential it holds, at most the person's. A host MAY give it fewer: it may refuse an agent's `apply` to a line of work a person edits (6.4).

### 4.3 What is recorded

1. **A version.** A version made from operations (Part VI, 3) has the person as its `author`. At 0.2 a version's commit object gains an optional member `agents`: each agent whose operations it holds, as `{name, version?}`, once each, sorted as their canonical JSON is by UTF-16 code units; a version that holds none has no `agents`. Until then a host names the agent in the version's `note` (Security and privacy, 3.3).
2. **A proposal file** made from an operation (6.3) has `by.person` the envelope's, `by.app` the envelope's `app`, or else the agent's name and version, and `intent` the envelope's. At 0.2 its `by` gains `agent`, as the envelope gives it.
3. **A log.** A processor that keeps a log of accepted batches SHOULD record beside each its operation's id and digest, `by` (and the client it checked, 4.2, rule 2), the target, the mode and the intent.

*Note (informative).* The reference implementation's cloud log has `author`, the user's id, and `client_id`, the page that sent the batch. An agent writes through the tab of the person whose session it uses, so its batches are the person's; an agent's calls are counted beside them, by the tool and the way it came (`assistant`, by connector, bridge or the browser's own tools), never with their arguments. EikHub, the host the reference implementation pushes to, records whether a proposal was opened with an agent's token (a client other than its own pages) and which client, and does not let the person whose agent opened a proposal merge it, unless they are the building's only admin and confirm it. Reference: `project_commands` in `supabase/migrations/20260919120000_accounts_and_projects.sql`; `assistantEvent` in `src/cloud/telemetryCounts.ts`.

## 5. The target

`target` names the line of work the commands are for, and the version of it they were made from:

| Member | | Meaning |
|---|---|---|
| `ref` | required | a ref of Part VI, 3: `main`, `alternatives/<slug>`, or another the processor keeps |
| `version` | required | the version the writer read and made the commands from, the **base**: a version's id (Part VI, 3), a git commit's id for a folder kept in git, or `null` where the writer read no version |
| `exact` | optional | `true`: apply to the base only |

1. A processor MUST refuse a `ref` it does not keep, or one it does not let the writer write (`target-unknown`). A processor that keeps one line of work, a folder or a session, calls it `main`.
2. Where the ref's newest version is the base, or the base is `null`, the commands are applied to the ref's newest version.
3. Where the ref has moved on from the base:
   - with `exact: true`, the processor MUST refuse the operation (`base-moved`), naming the ref's newest version, and apply nothing;
   - otherwise, it applies the commands to the ref's newest version, and their preconditions decide (section 9): a command whose record changed since the base is refused, and the others are applied to the model as it is now. The answer says that the ref had moved (`target.moved`).
4. A processor that keeps versions MUST move the ref from the version it applied the commands to, by compare-and-swap (Part VI, 3, rule 3). Where the swap fails, another write having come in between, it applies the commands again to the newer version by rule 3, or refuses them where the envelope asks for its base exactly.
5. For the mode `propose`, the base is what the proposal is made against (6.3).
6. A processor that keeps no versions, such as a library applying commands to a model it is given, compares the base only with a version its caller names, and says whether it compared them (`target.compared`).

*Note (informative).* The reference implementation's cloud log moves by compare-and-swap on its sequence (`expected_seq`). A tab whose batch met a moved sequence runs its unsent batches again on the newest model, keeps those that pass and names those that do not, which is rule 3 without `exact`, done by the writer. Reference: `mergeOnto` in `src/cloud/syncMerge.ts`. A base given by a host's sequence rather than a version is open question 56.

## 6. The mode

### 6.1 Apply

The batch is applied as Part V, 7 says, all or nothing. Accepted, the change is made, the ref moves (section 5) and the operation is recorded (section 3).

### 6.2 Dry run

Every check a real run makes is made, and nothing changes (Part V, 7.3). A failing dry run answers for every command. It is 0.1's `dryRun: true`, and is not recorded.

### 6.3 Propose

Nothing changes on the target. The processor checks the batch as a dry run, on the base where it keeps versions, else on the model it has. Where the batch passes it makes a proposal of it, and where it fails, none. The proposal is one of:

1. a **proposal file** (Part VI, 8.7) that an application keeps in its folder: `format`, the envelope's `intent`, `base` (the envelope's base version, and each record the commands change as it is there, by its hash and revision), `by` (4.3, rule 2), the commands, and the dry run that passed; or
2. a **host's proposal**: the commands applied on a line of work of its own, made from the base for the proposal, and offered for merging into `ref`, with the envelope's intent and `by`. The answer names it (section 11).

A person accepts a proposal, as Part VI, 8.7, rules 4 and 5 say.

A suggestion, a change that a party offers to records another party owns, is such a proposal, with no form of its own ([Part XVI, 5, rule 5](https://openeik.org/spec/0.1/ownership#5); decided on 30 October 2026).

### 6.4 What a processor may refuse

1. A host MAY refuse an `apply` to a ref it holds for review, such as an agent's to a line a person edits (Part VI, 8.7, rule 5), with `mode-not-allowed`, and SHOULD say that it takes a proposal.
2. A processor MUST NOT make a proposal of an `apply`, nor apply a `propose`: the writer chose the mode.
3. A processor that implements this part implements `apply` and `dryRun`. One that does not implement `propose` refuses it (`mode-not-implemented`).

*Note (informative).* The reference implementation has `apply` and dry runs (`execute` and `execute_batch` with `dryRun`). A work order there carries proposed commands, which it previews on a copy and checks again when they are accepted. EikHub, where agents propose and people merge, gives no token a merge: an agent's `apply` to a building's main line would be 6.4's refusal. Reference: `execute_batch` in `src/automation.ts`; `src/core/orders/proposal.ts`.

## 7. The intent

`intent` is what the change is for, in a sentence a person reads first: "Name the west wall by its finish, as the client asked."

1. It is plain text of 1 to 1000 characters. Nothing in it is markup, a link or an instruction: a host shows it as text, and an agent that reads it reads it as data (Security and privacy, 3.1).
2. It is the writer's account of the change. What the change does is what its commands do.
3. It becomes a proposal's intent (6.3), and a processor that keeps a log SHOULD record it with the operation (4.3, rule 3).

## 8. A batch without an envelope

1. A batch of 0.1 stays valid: a list of commands, or one command, without an envelope. A processor that implements this part MUST still take one as Part V says, its mode `apply` or a dry run as the processor's own flag says, its target the line the processor is on, its base unread, and with no operation: it is not recorded, and a second try is told from a first only by its preconditions. Its commands need not give their preconditions (Part V, 3, rule 4 stays a SHOULD for them), and its refusals carry codes (section 10).
2. An object whose `format` begins with `openeik-envelope/` is an envelope; any other object is a command, and a list is a batch of commands.
3. A host MAY take a writer's commands only in an envelope, and refuses a bare batch then (`envelope-required`): it may ask that of agents, and of the writers it does not run itself. A host that lets agents write SHOULD take an agent's commands only in an envelope, so that every agent's write is attributed and can be sent again safely; a person's own application may go on sending bare batches.

## 9. A precondition on every command

### 9.1 Rule 4 made a requirement

In an envelope, every command MUST give its kind's precondition, which Part V, 3 lists and 9.2 adds to, and a processor MUST refuse one that gives none (`precondition-missing`), naming the member it lacks. The precondition names the state the command was made from, so a command made from a record that has changed since is refused, not applied over the change.

1. An upsert names a record that does not exist yet with `null`: `expectedRevision: null` for `type.upsert`, `material.upsert`, `grid.upsert` and the upserts of the documentation profile and the domain operations, and `expectedLevel: null` for `level.upsert`. A processor MUST refuse an upsert with `null` of a record that exists, as a conflict, and one with a revision of a record that does not, as a precondition with nothing to name (`precondition-target`). In 0.1 a `type.upsert`, `view.upsert`, `sheet.upsert` or `render.upsert` of a new record may say revision 0; in an envelope it says `null`, so that a number always names a record that is there.
2. A `create` gives `expectedHostRevision` where its element has a host. One without a host needs nothing more: that its id is new is its precondition (Part V, 2.1).
3. A precondition that compares a state (`expectedSite`, `expectedEnvelope`, `expectedSettings`, `expectedInformation`, `expectedExcavation`, `expectedPrecast`, `expectedStandards`) gives the state as read: `null` where there was none, `{}` for rooms' settings and project information that were empty.
4. A command inside another, as in `original.edit`, gives its own precondition too.
5. A replay of commands a log accepted is not refused for a precondition it lacks (Part V, 8.4): the rule is newer than they are.

### 9.2 The fourteen kinds

Part V, 3's note lists fourteen kinds of the reference vocabulary that take no precondition in 0.1. Each gets one:

| Kind | Precondition | It names |
|---|---|---|
| `project.excavation` | `expectedExcavation` | the project's `excavation` as read, or `null` where it had none |
| `project.precast` | `expectedPrecast` | the project's `precast` as read, or `null` |
| `project.standards` | `expectedStandards` | the project's `standards` as read, or `null` |
| `image.upsert` | `expectedHash` | the hash (Part III, 6) of the image's record as read, or `null` for a new image; an image has no revision, and its record holds its data, which the hash names without sending it back |
| `image.delete` | `expectedHash` | the hash of the image's record as read |
| `render.delete` | `expectedRevision` | the render's revision |
| `rebar.quick` | `expectedHostRevision` | the revision of the host the bars are worked out from |
| `mesh.lay` | `expectedHostRevision` | the revision of the host the mesh is laid on |
| `mep.downpipe` | `expectedGutterRevision` | the revision of the gutter the downpipe is worked out from |
| `trusses.plan` | `expectedRevision` | the revision of the plan view it writes, or `null` for a new one |
| `original.make` | `expectedRevisions` | `{"<element id>": <revision>, …}`: the revision of each element of `elementIds`, and of no other |
| `original.frame` | `expectedRevision` | the group's revision |
| `original.edit` | `expectedRevision` | the group's revision; each command of the edit gives its own as well |
| `copy.place` | `expectedOriginalRevision` | the revision of the group it places (`copy.of`) |

Their schemas are in `$defs/preconditions` of the envelope's schema, and the member each kind takes is in the proposal's index (`schemas/proposed/envelope/index.json`). A precondition of 9.2 compares as Part V, 3 says: a revision with the record's, a state as a JSON value, a hash with the record's hash.

*Note (informative).* A domain operation that works out records from a host, as `rebar.quick` does, names the host's revision: what it writes depends on the host as it was read. `trusses.plan` works its plan out from the roofs as they stand, and names only the view it writes, which is all it changes. In the reference implementation, of the 91 kinds, 51 require a precondition, 26 take one that may be left out and 14 take none (Part V, 3); with this part, all 91 take one in an envelope.

## 10. Refusals with codes

### 10.1 The form of a code

A code is a lowercase name of words joined by hyphens, as the validator's codes are (`revision-conflict`, `opening-fit`): `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`. A processor's own code, for a rule this specification does not state, carries its prefix (Part I, 11.1): `acme:sliver`.

The plan proposed codes such as `EIK-WALL-012`: a namespace, a domain and a number. This part takes the validator's form instead, for three reasons:

1. **One rule, one code.** The validator of `@openeik/tools` names every finding with a code of this form, more than 200 of them, which the conformance suite's cases name. A refusal for a rule the validator checks carries that code (10.5), so a command refused for an opening that does not fit and a folder reported for one say `opening-fit` alike, and a program that handles one handles the other. Numbered codes would have been a second name for each rule.
2. **A code that says what it is.** An agent reads `revision-conflict` and knows to read again; `EIK-CMD-003` it has to look up. The rule a refusal breaks travels beside its code (`rule: "V.3.1"`), so a code needs no numbers to be found in the text.
3. **Not by domain.** Most refusals are of no one category: a conflict, a missing target, a member a kind does not define. Codes grouped by domain would give each a home it does not have.

### 10.2 The refusal

A refused command's answer (Part V, 9) holds its refusals. Each is an object:

| Member | | Meaning |
|---|---|---|
| `code` | required | the code (10.1) |
| `rule` | required | the rule it breaks: a Part's number, the section and the rule, as `V.3.1` or `II.6.1.5` |
| `pointer` | required | a JSON pointer (RFC 6901) into the command, at the member that is wrong: `/expectedRevision`, `/patch/profile`; `""` for the whole command |
| `message` | required | what is wrong, for people, in the terms of the domain (Part V, 6) |
| `fix` | optional | what would make the command pass, where it is known (10.3) |
| `data` | optional | values for a program: a conflict's `expected` and `current`, the records that use what a delete would remove (`users`) |
| `record` | optional | where in the model the refusal is: `{list, id, pointer}`, a record and a pointer in it |

1. A processor MUST give each refusal a code, a rule, a pointer and a message. A command may have several refusals: the whole model's check (Part V, 6.3) may find several rules broken after it.
2. The pointer is into the command as sent. A refusal of a command inside another (`original.edit`) points into the outer command: `/commands/0/expectedRevision`. A refusal of the envelope itself points into the envelope (`/target/version`), and one that the whole model's check finds points at the member of the command that wrote the record, where the command is an `update` whose patch gives it or a `create` of the record, and at the whole command otherwise, with `record` naming where the rule is broken.
3. The message is the one Part V, 6 asks for: in a batch it names the command by its index, kind and id (the answer's `error`), and its refusals give their messages without that lead.

From `@openeik/tools`, on `examples/one-storey-house`, the second command of Part V, 6's example:

```json
{"code": "profile-short", "message": "A wall or beam requires two distinct endpoints.", "pointer": "/patch/profile", "rule": "II.6.1.2"}
```

and its first:

```json
{
  "code": "revision-conflict",
  "data": {"current": 1, "expected": 0},
  "fix": {"read": [{"id": "wall-south", "list": "elements"}]},
  "message": "Revision conflict: expected 0, current 1. Read and retry.",
  "pointer": "/expectedRevision",
  "rule": "V.3.1"
}
```

and its third, which the whole model's check finds after it, on the window the patch moved:

```json
{
  "code": "opening-fit",
  "message": "Opening must fit within its host wall: it reaches 11250 mm along a wall 10000 mm long.",
  "pointer": "/patch/along",
  "record": {"id": "window-south", "list": "elements", "pointer": "/along"},
  "rule": "II.6.1.5"
}
```

### 10.3 Fixes

A `fix` says what would make the command pass, where the processor knows it. It is one of:

- `{"patch": [...]}`: a JSON Patch (RFC 6902) of the command, such as `{"op": "move", "from": "/expectedRevison", "path": "/expectedRevision"}` for a misspelt member, or `{"op": "replace", "path": "/element/width", "value": 1200}` for a size the type sets;
- `{"command": {...}}`: a command to send in the command's place;
- `{"read": [{"list": ..., "id": ...}, ...]}`: the records to read again, and make the command from.

1. A fix changes nothing by itself. A writer that takes it sends a new command, checked as any is.
2. A fix MUST NOT write a precondition the writer did not read: the fix of a conflict is a `read`, never the command with the current revision written in, which would apply it over the change the precondition guards against.
3. A fix is a suggestion. Where several would do, or the choice is the person's (where the elements of a level that is deleted go), a processor gives none, and its message says what the choices are.

### 10.4 The catalogue

These are the codes of a command processor's refusals that are its own. `@openeik/tools` lists them as `COMMAND_CODES`. The messages are the reference implementation's, or `@openeik/tools`'s where the reference has none.

**The command's shape** (Part V, 1, 4 and 8; stage 1 of Part V, 6):

| Code | Rule | Refused when | Pointer | Fix | Message (reference) |
|---|---|---|---|---|---|
| `command-not-object` | V.1.1 | a command is no JSON object | `""` | | |
| `kind-missing` | V.1.1 | a command names no kind, as a string | `/kind` | | |
| `kind-unknown` | V.1.2 | its kind is none of the vocabulary's | `/kind` | | |
| `kind-not-implemented` | V.2 | its kind is of a profile or of the reference vocabulary that the processor does not implement | `/kind` | | |
| `command-member-unknown` | V.1.4 | it holds a member at its top level that its kind does not define | the member | a `move` to the member meant, where each is a misspelling | "Unrecognized key(s) in object", with "did you mean" |
| `command-schema` | V.1.2 | it does not match its kind's schema | the first member that does not | | the schema's words, by path |
| `command-not-json` | V.8.2 | it holds what JSON cannot: NaN, an infinity, a BigInt, itself | the first such value | | |
| `command-too-deep` | V.8.2 | it nests deeper than any record may | where it does | | |
| `patch-undefined` | V.4.3 | a patch says `undefined` for a member its record has | the member | `add` it as `null` | "The patch says undefined for …. … Send null to take a field away." |
| `patch-fixed` | V.4.2 | a patch writes an element's `id`, `category` or `revision` | the member | | |
| `patch-legacy` | V.4.6 | a patch writes `topRoofId`, `topCeilingId` or `roofCutId` | the member | | |
| `patch-removes` | V.4.2 | a patch takes away, with `null`, a member every element has | the member | | |

**The command's own rules** (Part V, 3 and 5; stage 2 of Part V, 6):

| Code | Rule | Refused when | Pointer | Fix | Message (reference) |
|---|---|---|---|---|---|
| `target-missing` | V.6.2 | the record the command changes, deletes, copies or names as its target is not there | its id | | "Element not found.", "Type not found.", "Level no longer exists." |
| `element-of-copy` | II.10 | the element it names is a placed copy's, worked out and never stored | its id | | |
| `revision-conflict` | V.3.1 | a revision precondition does not hold | the precondition | `read` the record | "Revision conflict: expected 3, current 4. Read and retry." |
| `state-conflict` | V.3.3 | a state precondition does not hold | the precondition | `read` the record | "Project name changed since it was read. Read and retry.", "Level conflict. Read the current level before updating." |
| `precondition-missing` | V.3.2 | a precondition is required and not given: a strict upsert of a record that exists, or any command in an envelope | the member it lacks | `read` the record | "Grid … exists (revision 2): give expectedRevision 2 to change it." |
| `precondition-target` | V.3.2 | a precondition names a record that is not there: a revision of a new record, a host's revision for an element without a host | the precondition | leave it out, or in an envelope `null` | |
| `in-use` | V.5.4 | a delete is refused while records use what it would remove: a type, a level and its plans, a view | the target | | "Type … is used by 2 elements (…). Reassign them …" |
| `level-last` | II.3 | it would remove the last level | the target | | |
| `level-move-to` | V.5.4 | a level's delete names in `moveTo` no other level | `/moveTo` | | |
| `group-member` | II.10 | it changes an element of a group other than in the group's edit | the element | | "… is part of the group G1 (instance G1). Edit the group to change it …" |
| `group-by-hand` | II.10 | it writes `original`, `copy` or `born` itself | the member | | |
| `group-twice` | II.10 | an element, or one that stands in it, is of another group already | the element | | "… is of the group G1 already. An element is in one group." |
| `group-not-member` | II.10 | it takes out of a group an element not of it | the element | | |
| `group-hosted` | II.10 | it takes out of a group an element that stands in another of the group, without that element | the element | | |
| `group-instances` | II.10 | a group is dissolved while it has other instances | the group | | |
| `group-origin` | II.10 | a group's elements have no place of their own, and no origin is given | `/elementIds` | | |
| `edit-nested` | II.10 | an `original.edit` holds another | the inner edit | | |
| `copies-not-derived` | II.10 | it needs a copy's elements worked out, which the processor does not derive | the instance | | |

**The envelope** (sections 2 to 8, and 12):

| Code | Rule | Refused when | Pointer |
|---|---|---|---|
| `envelope-schema` | XVII.2 | the envelope does not match its schema, or holds a member it does not define | the member |
| `envelope-format` | XVII.2 | it names a format the processor does not implement | `/format` |
| `envelope-required` | XVII.8 | the host takes this writer's commands only in an envelope | `""` |
| `operation-reused` | XVII.3 | an operation id the processor recorded comes with another envelope | `/operationId` |
| `operation-in-progress` | XVII.3 | an operation id comes again while its first is being applied | `/operationId` |
| `person-mismatch` | XVII.4.2 | `by.person` is not the person the host authenticated, nor one they may act for | `/by/person` |
| `target-unknown` | XVII.5 | the target's ref is not one the processor keeps, or not one the writer may write | `/target/ref` |
| `base-moved` | XVII.5 | the ref has moved since the base, and the envelope asks for its base exactly | `/target/version` |
| `mode-not-implemented` | XVII.6.4 | the mode is one the processor does not implement | `/mode` |
| `mode-not-allowed` | XVII.6.4 | the host does not let this writer apply to this ref | `/mode` |
| `not-permitted` | XVII.12 | the writer may not change the target, or a record the commands change | the command, or `""` |
| `too-large` | XVII.12 | the envelope holds more commands or bytes than the processor takes in one | `/commands` |

A processor MAY refuse for a rule this specification does not state, with a code of its own under its prefix: the reference implementation's refusals for a pinned element ("Unpin the element before moving, reshaping or deleting it."), a sliver thinner than 0.1 mm, a type's name taken in its category, or walls that cannot join as a T would be `stomra:pinned`, `stomra:sliver`, `stomra:type-name-taken` and `stomra:join-tee` until a later version names their rules.

### 10.5 The validator's codes

1. A refusal for a rule the validator checks carries the validator's code for that rule, the code `@openeik/tools` gives the same finding in a folder (its `CODES`). A command's own stage refuses with these where its rule is the validator's: `id-duplicate` (I.6.1), `ref-missing` (I.7), `type-category` (I.7.1), `host-category` (II.6.1), `profile-short` (II.6.1.2), `type-size` (II.5.3), `top-host` (IV.5.3.1), `trim-host` (II.6.7.2), `railing-host` (II.12.2.2), `railing-edges` (II.12.2.3), `generic-shape` (II.6.6.2), `generic-points` (II.6.1.12), `category-unknown` (I.10.5), `extension-null` (I.11.2.3) and `schema-invalid` (III.3.2).
2. A batch the whole model's check refuses (Part V, 6.3) gives, for each error the batch brought into the model, the validator's code and rule, the command after which it appeared (Part V, 7.1), and the `record` it is on. An error the model had before the batch is no command's: the answer gives it apart (section 11), with its code.
3. Its severity is a refusal's: a code that the validator gives as a warning, such as `category-unknown`, is a refusal where a command would break the rule (a command makes no record of a category it does not know, Part V, 1.5).
4. No code is both a processor's own and a validator's.

*Note (informative).* The specification names no validator codes in 0.1: the conformance suite's cases name the reference library's ([conformance/README.md](https://gitlab.com/openeik/spec/-/blob/main/conformance/README.md), "Codes"), which may be renamed once the specification defines codes. Rule 1 makes the codes a refusal carries the standard's, and with them the validator's that a refusal can carry; whether the rest of the validator's codes become the standard's in 0.2 too is open question 53.

## 11. The answer

The answer of Part V, 9, held to [`schemas/proposed/envelope/answer.schema.json`](https://openeik.org/schemas/0.1/proposed/envelope/answer.schema.json), gains:

| Member | | Meaning |
|---|---|---|
| `mode` | always | the mode the batch was run in |
| `operation` | for an envelope | `{id, digest, kept, repeated?}`: the operation's id and digest, whether the processor records operations (section 3), and `repeated: true` where this answer is the recorded one, given again |
| `target` | for an envelope | `{ref, base, version, compared, moved?}`: the target's ref and base, the version the commands met (`null` where the processor keeps none), whether the two were compared, and whether the ref had moved since the base |
| `results[i].refusals` | for a refused command | its refusals (10.2) |
| `refusals` | where there are any | the refusals that are no one command's: the envelope's (sections 2 to 8), and the errors the model had before the batch |
| `proposal` | for an accepted `propose` | the proposal file (6.3, rule 1), or the host's proposal: `{id, ref, url?}` (6.3, rule 2) |

1. `dryRun` is `true` for the modes `dryRun` and `propose`: nothing was committed to the target.
2. A refused command keeps its `error`, the message of Part V, 6 that names it, beside its `refusals`.
3. An envelope refused before its commands are tried has no results, and its refusal in `refusals`.
4. A repeated operation's answer is the recorded one: it says what the first did, and nothing was applied again.

From `@openeik/tools`, the envelope of section 2 applied to `examples/one-storey-house`, with operations recorded and the model's version named as its base (`verify` and the wall's record shortened):

```json
{
  "affected": ["wall-west"],
  "dryRun": false,
  "mode": "apply",
  "ok": true,
  "operation": {"digest": "9d758d99b3b57f8ccc7227500f08316cf564dbfef9916a2bb14c63c0d8caa9e3", "id": "0b6f6a4e-2c1d-4f7e-9a51-3d2b8c7e1f00", "kept": true},
  "results": [{"index": 0, "kind": "update", "ok": true, "result": {"id": "wall-west", "name": "West wall (render)", "revision": 2, "…": "…"}}],
  "target": {"base": "7d1e9f3b2c4a6e8d0f1b3c5e7a9d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c1e3a5d", "compared": true, "moved": false, "ref": "main", "version": "7d1e9f3b2c4a6e8d0f1b3c5e7a9d2f4b6c8e0a1d3f5b7c9e2a4d6f8b0c1e3a5d"},
  "verify": {"filter": {"elementIds": ["wall-west"]}, "…": "…"}
}
```

*Note (informative).* The reference implementation's refusals are text. Its batch failure carries the command's `index` and `commandId` beside the message, and its failing dry run answers `{ok: false, dryRun: true, sequence, failed, results: [{index, ok, kind, commandId?, error?}]}`; over MCP only the text reaches the agent. Reference: `batchError` and `dryRunReport` in `src/core/model/store.ts`; `execute_batch` in `src/automation.ts`; `registerModelTools` in `src/core/automation/mcpTools.ts`.

## 12. Security and privacy

1. **Attribution is personal data.** `by.person` names a person, and an operation recorded with its `by` says who did what and when. It is an id, never an email address (4.1, rule 1). A processor SHOULD keep the record of operations only as long as section 3 needs it and its log keeps the change, and the agents in a version (4.3, rule 1) are kept by every store and clone that has the version (Security and privacy, 2.4).
2. **An agent's identity is its credential.** What an agent says of itself, its name, version and session, is unchecked. A host decides what an agent may do by the credential it holds (4.2), records an operation sent with an agent's credential as an agent's whatever the envelope says, and MUST NOT let the envelope widen what the credential allows.
3. **Operation ids carry nothing.** An id is made for the operation alone, from no personal data (3, rule 1); it is seen by whoever reads the processor's log. An id that comes back with another envelope is refused (`operation-reused`), so that a writer cannot be answered with another writer's result by guessing its id: a processor that keeps a log MUST keep operations apart by the person they were made for, and MUST NOT give an operation's recorded answer to a writer who is neither its person nor one that may act for them.
4. **The intent is text.** It is shown as text, never as markup, and an agent reads it as data, not as an instruction (section 7; Security and privacy, 3.1).
5. **Fixes are suggestions.** A writer applies a fix as a new command, checked as any is; a fix never bypasses a precondition (10.3, rule 2). An agent SHOULD NOT apply a fix that deletes or moves records without the person's word.
6. **Limits.** A processor MAY refuse an envelope larger than it takes in one (`too-large`), and a change a writer may not make (`not-permitted`), such as one to records another party owns where it enforces ownership (Part XVI, 4).

## 13. Conformance

A command processor that implements this part takes envelopes and bare batches (section 8), checks an envelope as section 2 says, records operations where it keeps a log (section 3), applies to its target as section 5 says, implements `apply` and `dryRun` (section 6), requires every command in an envelope to give its precondition (section 9), and answers every refusal with a code, a rule, a pointer and a message (sections 10 and 11). An agent host that implements it authenticates the writer and checks attribution (section 4.2).

*Note (informative).* `@openeik/tools` implements the processor's part since 25 October 2026, for the core record operations, when asked for the proposal (`applyCommands(model, commands, {proposals: ["envelope"]})`; `openeik apply --proposal envelope`). With the proposal, every refusal carries its code, its rule, its pointer and a fix where one is known, a batch in an envelope or not; without it, its answers are as they were. It records an operation it applied in `answers`, a map its caller keeps, and answers an operation sent again from it; without `answers` it says `operation.kept: false`. It keeps one line of work, `main`, or the ref its caller names (`ref`), and refuses an envelope for another (`target-unknown`); it compares the base with the version its caller names (`version`), applies to a moved ref unless `exact` asks otherwise, and makes a proposal file for `propose`. What a store or a host does itself it leaves to them: keeping the record durably with the change, refusing an operation in progress, authenticating the person, moving the ref. Of the fourteen kinds of 9.2 it implements the seven that are core record operations. The proposal's conformance cases, for the command processor, are in `conformance/proposed/envelope/` (since 30 October 2026), and `@openeik/tools` passes all of them. The reference implementation implements none of this part yet.

## 14. The decisions

Open questions 53 to 60 ([Open questions, 3](https://openeik.org/spec/0.1/open-questions#3)) are this part's. The steward decided them on 25 October 2026, each as this part proposed it.

| Question | Decided |
|---|---|
| 53 | The form of a code: the validator's lowercase, hyphenated names, one code for one rule whether a refusal or a finding reports it, rather than the plan's `EIK-WALL-012`; and the validator's codes made the standard's in 0.2 with them (10.1 and 10.5) |
| 54 | A moved base: applied to the ref's newest version, the preconditions deciding, unless the envelope asks for its base `exact` (5, rule 3) |
| 55 | The agent in the history: a version's commit object gains `agents`, and a proposal file's `by` gains `agent`, in 0.2 (4.3) |
| 56 | A base by sequence: not yet; the base is a version or `null`, and a host that keeps a change sequence rather than versions relies on the commands' preconditions, until a second host asks for a `sequence` in `target` (section 5) |
| 57 | How long an operation is recorded: at least 24 hours, and as long as its change's log entry where that is kept (3, rule 3) |
| 58 | A person by id only: never an email address in an envelope, though a proposal file's `by.person` and a result manifest's `made.by` may hold one in 0.1 (4.1, rule 1) |
| 59 | Envelopes for agents: an agent host that implements this part MUST take envelopes, and SHOULD require them of agents; a person's own application may send bare batches (section 8, rules 1 and 3) |
| 60 | An envelope's own members: a member it does not define refused, rather than kept as a record's is (2, rule 2) |
