fvtt-world-cli 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -36
- package/dist/index.js +12 -2
- package/docs/README.md +5 -61
- package/docs/architecture.md +53 -209
- package/docs/commands.md +166 -444
- package/docs/compatibility.md +13 -22
- package/docs/getting-started.md +76 -74
- package/docs/images/module-settings.png +0 -0
- package/docs/protocol.md +228 -187
- package/docs/security.md +149 -322
- package/docs/skill.md +25 -30
- package/package.json +17 -6
- package/skills/foundry-world-editor/SKILL.md +8 -0
- package/skills/foundry-world-editor/SKILL.md.sha256 +1 -1
- package/bin/sync-installed-skill.mjs +0 -89
- package/docs/images/icon.svg +0 -16
- package/docs/images/mark.svg +0 -15
package/docs/protocol.md
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
# Protocol
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- [`packages/protocol/src/schemas/`](../packages/protocol/src/schemas/)
|
|
11
|
-
- [`packages/protocol/src/constants.js`](../packages/protocol/src/constants.js)
|
|
12
|
-
- [`packages/protocol/src/validation.js`](../packages/protocol/src/validation.js)
|
|
13
|
-
- `fvtt-world-cli commands --json`, `fvtt-world-cli schema <command>`, and
|
|
14
|
-
`fvtt-world-cli system info --json` for the installed checkout and the connected runtime
|
|
3
|
+
This reference defines the integration contract between local clients, the daemon, and the
|
|
4
|
+
Foundry bridge. For CLI workflows, use [Commands](commands.md).
|
|
5
|
+
|
|
6
|
+
Exact command and frame schemas come from the [protocol registry](../packages/protocol/src/commands.js)
|
|
7
|
+
and [family schemas](../packages/protocol/src/schemas/). The [constants](../packages/protocol/src/constants.js)
|
|
8
|
+
define error codes, timeouts, and limits. Inspect the installed version with `commands --json` and
|
|
9
|
+
`schema <command>`; use `system info --json` for connected-runtime details.
|
|
15
10
|
|
|
16
11
|
## Versioning
|
|
17
12
|
|
|
@@ -19,31 +14,16 @@ The protocol version equals the product release version. Every transport message
|
|
|
19
14
|
components in one installation must match exactly. The daemon and Foundry module reject a mismatch
|
|
20
15
|
instead of negotiating a subset of the contract.
|
|
21
16
|
|
|
22
|
-
Recover by updating the older component. Restart the daemon after updating the CLI package, or reload
|
|
23
|
-
the GM client after updating the Foundry module. A refused module load does not reconnect on its own.
|
|
24
|
-
Mismatch errors identify the older component when the two versions can be ordered.
|
|
25
|
-
|
|
26
17
|
## Transport model
|
|
27
18
|
|
|
28
19
|
The daemon listens on a loopback WebSocket endpoint. Credentials never appear in URLs. A local
|
|
29
|
-
client
|
|
30
|
-
|
|
31
|
-
openings immediately, and never assigns a role before authentication has succeeded. Browser sockets
|
|
32
|
-
are identified by their exact HTTP(S) Origin and participate only in pairing and bridge sessions;
|
|
33
|
-
they cannot assume the local-client role.
|
|
34
|
-
|
|
35
|
-
Every message type has a closed top-level schema. A malformed message yields a structured
|
|
36
|
-
`INVALID_MESSAGE` error where a response is possible, and an authenticated local client that sends
|
|
37
|
-
a malformed control request receives a correlated error and may keep using its connection.
|
|
20
|
+
client has no browser Origin and authenticates with `client.hello` as its first message. Browser
|
|
21
|
+
sockets require an exact HTTP(S) Origin and may only request pairing or a bridge session.
|
|
38
22
|
|
|
39
|
-
The daemon
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- a clean goodbye releases the slot immediately, while an abnormal close reserves it briefly for
|
|
44
|
-
the same pairing;
|
|
45
|
-
- a daemon-initiated release is terminal for the released client: it does not reconnect on its
|
|
46
|
-
own, and resuming is an explicit operator action.
|
|
23
|
+
The daemon imposes a first-frame deadline and assigns no role before authentication succeeds.
|
|
24
|
+
Every message has a closed top-level schema. Malformed openings close immediately; later malformed
|
|
25
|
+
messages return `INVALID_MESSAGE` where possible. An authenticated local client may continue after
|
|
26
|
+
a correlated control-request error.
|
|
47
27
|
|
|
48
28
|
## Size limits
|
|
49
29
|
|
|
@@ -64,65 +44,45 @@ Pairing is the one-time exchange that lets a GM browser become a bridge. Its gua
|
|
|
64
44
|
exactly once, to the requesting socket, only after the digest has been persisted;
|
|
65
45
|
- approving the same Origin/world/user/client again re-pairs that client's existing profile by
|
|
66
46
|
rotating its credential instead of accumulating duplicates;
|
|
67
|
-
- expiry, denial, socket close, and
|
|
68
|
-
path, so the browser-side authorization UI is never left waiting after daemon shutdown or expiry.
|
|
47
|
+
- expiry, denial, socket close, and daemon shutdown end the pending attempt.
|
|
69
48
|
|
|
70
49
|
### Client identity
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- `
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- A label is chosen once, in the browser, at pairing time. No control operation renames a stored
|
|
84
|
-
record; re-pairing is the way to change a label.
|
|
85
|
-
|
|
86
|
-
The bridge hello carries `clientId` at the top level, beside `pairingId` and `credential`, because it
|
|
87
|
-
is authentication material rather than session content: the daemon rejects a hello whose client id
|
|
88
|
-
does not match the stored pairing. The label is not resent on hello; the daemon's pairing record owns
|
|
89
|
-
it, and the browser keeps a copy only to display it.
|
|
51
|
+
Pairing identity includes a `client` object with a persistent `id` and human-readable `label`.
|
|
52
|
+
Records are unique per Origin, world, user, and client id. Re-pairing one browser does not affect
|
|
53
|
+
another browser using the same GM account.
|
|
54
|
+
|
|
55
|
+
- `id` accepts 8 to 64 hexadecimal characters and dashes.
|
|
56
|
+
- `label` accepts 1 to 64 Unicode characters, excluding whitespace-only values, C0/C1 controls,
|
|
57
|
+
zero-width characters, bidirectional controls, and Unicode tags.
|
|
58
|
+
- Labels need not be unique. Changing a stored label requires re-pairing.
|
|
59
|
+
|
|
60
|
+
The bridge hello carries `clientId`, `pairingId`, and `credential`. A client id mismatch returns
|
|
61
|
+
`UNAUTHORIZED`. The label is not resent; the stored pairing owns it.
|
|
90
62
|
|
|
91
63
|
## Daemon control
|
|
92
64
|
|
|
93
|
-
Authenticated local clients manage pairings and
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
never unpair the browser that is connected or the one the daemon is still holding a slot for. The
|
|
115
|
-
config is rewritten only when at least one record is removed. The result is `{ olderThanDays, pruned }`,
|
|
116
|
-
where `olderThanDays` is the cutoff the daemon applied, including the default when the caller omitted
|
|
117
|
-
it, and `pruned` carries the removed records in the same public, digest-free shape `auth.list`
|
|
118
|
-
serializes. The daemon computes the set at execution time; a caller that previewed candidates from
|
|
119
|
-
`auth.list` holds an advisory list, not the outcome, and the executed set may be wider than that
|
|
120
|
-
preview when a record crosses the cutoff between the two calls.
|
|
121
|
-
|
|
122
|
-
The active bridge itself may use
|
|
123
|
-
exactly one control operation: revoking its own pairing. A browser Unpair deletes its stored
|
|
124
|
-
credential only after a correlated successful revocation; on failure the credential is retained for
|
|
125
|
-
retry, and discarding it locally is a separate deliberate action.
|
|
65
|
+
Authenticated local clients manage pairings and bridge ownership through closed control requests.
|
|
66
|
+
Responses repeat the correlation id and operation. The protocol package defines the operation
|
|
67
|
+
registry; the [command guide](commands.md#authorization-commands) covers operator use.
|
|
68
|
+
|
|
69
|
+
`auth.await` returns the earliest live pending request in the public `auth.pending` shape, without
|
|
70
|
+
credentials. With no pending request, it parks until one arrives or a bounded wait expires with an
|
|
71
|
+
empty result. `timeoutMs` can shorten but not exceed the daemon's poll cap. A new request answers
|
|
72
|
+
all parked waiters; socket closure removes that client's waiter.
|
|
73
|
+
|
|
74
|
+
`auth.prune` accepts a non-negative `olderThanDays`, defaulting to 30. It removes records older than
|
|
75
|
+
the cutoff according to `lastSeenAt`, excluding the active pairing and a live reconnect-lease
|
|
76
|
+
holder. Approval, authenticated hellos, and disconnects refresh this timestamp. A valid hello
|
|
77
|
+
rejected with `BRIDGE_BUSY` refreshes it; an unauthorized hello does not.
|
|
78
|
+
|
|
79
|
+
The result is `{ olderThanDays, pruned }`, with removed records in the public `auth.list` shape.
|
|
80
|
+
The daemon computes the set at execution time, so an earlier candidate list is advisory and can
|
|
81
|
+
omit a record that later crosses the cutoff.
|
|
82
|
+
|
|
83
|
+
The active authenticated bridge may use only one control operation: revoking its own pairing.
|
|
84
|
+
It deletes its local credential only after a correlated successful revocation response. A failed
|
|
85
|
+
revocation retains the credential for retry.
|
|
126
86
|
|
|
127
87
|
## Bridge sessions
|
|
128
88
|
|
|
@@ -137,35 +97,147 @@ active session.
|
|
|
137
97
|
- `BRIDGE_BUSY` is terminal for that client instance but preserves the stored credential; the
|
|
138
98
|
operator releases the current owner and retries explicitly rather than pairing again.
|
|
139
99
|
- Only the exact active authenticated socket can release ownership with a goodbye.
|
|
140
|
-
-
|
|
141
|
-
|
|
142
|
-
|
|
100
|
+
- Another pairing cannot displace the active bridge. A same-pairing connection may take over;
|
|
101
|
+
requests forwarded to the displaced socket then fail with indeterminate delivery.
|
|
102
|
+
- A clean goodbye releases ownership immediately. An abnormal close reserves it briefly for the
|
|
103
|
+
same pairing. A daemon-initiated release clears ownership and stops automatic reconnects.
|
|
143
104
|
- If the connected user loses GM authority, the bridge answers the pending command with a
|
|
144
105
|
correlated `PERMISSION_DENIED` without dispatching it, so the caller knows the rejected command
|
|
145
106
|
started no mutation.
|
|
146
107
|
|
|
108
|
+
## Client-side status signal
|
|
109
|
+
|
|
110
|
+
The module emits `fvtt-world-cli.statusChanged` for macros and other modules in the GM client.
|
|
111
|
+
It fires when transport status or handshake acknowledgement changes and receives the same bridge
|
|
112
|
+
snapshot exposed by `system info`:
|
|
113
|
+
|
|
114
|
+
`status`, `url`, `helloAcknowledged`, `hasEstablishedSession`, `lastConnectedAt`,
|
|
115
|
+
`reconnectAttempts`, `terminalStopReason`, and `protocolVersionMismatch`.
|
|
116
|
+
|
|
117
|
+
Readiness requires both `status === "connected"` and `helloAcknowledged`. The socket opens before
|
|
118
|
+
the daemon acknowledges the handshake, and losing it clears the acknowledgement.
|
|
119
|
+
`protocolVersionMismatch` is normally `null`; on a mismatch it reports both versions and identifies
|
|
120
|
+
the older component as `module`, `cli-daemon`, or `unknown`.
|
|
121
|
+
|
|
122
|
+
This hook is local to the GM client. Credential changes do not trigger it, and it has no wire
|
|
123
|
+
protocol meaning.
|
|
124
|
+
|
|
147
125
|
## Commands and correlation
|
|
148
126
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
127
|
+
A request supplies a caller-chosen correlation id, a registered command name, and schema-validated
|
|
128
|
+
parameters. Responses repeat the id and carry either a result or an error. Validate both the
|
|
129
|
+
request envelope and command parameters against the protocol registry.
|
|
130
|
+
|
|
131
|
+
### Request and response example
|
|
132
|
+
|
|
133
|
+
After the local client has authenticated, it can send this request to the daemon. Replace
|
|
134
|
+
`<release-version>` with the installed protocol version. The active bridge must advertise the command.
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"protocolVersion": "<release-version>",
|
|
139
|
+
"type": "command.request",
|
|
140
|
+
"id": "check-bridge",
|
|
141
|
+
"command": "system.ping",
|
|
142
|
+
"params": {}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A success response repeats the id and carries the result. This example omits the timestamp and
|
|
147
|
+
bridge-status fields inside `result`:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"protocolVersion": "<release-version>",
|
|
152
|
+
"type": "command.response",
|
|
153
|
+
"id": "check-bridge",
|
|
154
|
+
"ok": true,
|
|
155
|
+
"result": { "pong": true }
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If no bridge is ready, the response instead carries an error. Message wording is illustrative:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"protocolVersion": "<release-version>",
|
|
164
|
+
"type": "command.response",
|
|
165
|
+
"id": "check-bridge",
|
|
166
|
+
"ok": false,
|
|
167
|
+
"error": {
|
|
168
|
+
"code": "BRIDGE_NOT_READY",
|
|
169
|
+
"message": "No Foundry bridge is connected."
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The protocol package exports `REQUEST_SCHEMA` and `COMMAND_RESPONSE_SCHEMA` for envelope validation.
|
|
154
175
|
|
|
155
176
|
## Result conventions
|
|
156
177
|
|
|
157
|
-
Document results
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
Document results use type-named keys and expose `id` as the public identifier. A source `_id`
|
|
179
|
+
mirror may accompany it. List responses use smaller projections than single-document reads;
|
|
180
|
+
filters apply before pagination. Previewed documents have no persistent identity, so an id
|
|
181
|
+
observed during a preview must not be reused.
|
|
182
|
+
|
|
183
|
+
Broadcast actions report `dispatched` and target users rather than a confirmed player-side result.
|
|
184
|
+
The active/inactive split is included where knowable.
|
|
185
|
+
|
|
186
|
+
### Macro results
|
|
187
|
+
|
|
188
|
+
`macro.execute` returns the macro's value and observed chat messages. `chatCapture` describes the
|
|
189
|
+
observation: `captured` for all expected messages, `not-created` when a chat macro created none,
|
|
190
|
+
`partial` for incomplete capture, or `unknown` when the client could not observe the chat log.
|
|
191
|
+
|
|
192
|
+
`MACRO_TIMEOUT` is indeterminate because the macro keeps running. A thrown error can follow partial
|
|
193
|
+
effects, while a macro that catches its own errors may return normally. Consumers must verify
|
|
194
|
+
world state when the return value does not establish the effect.
|
|
195
|
+
|
|
196
|
+
### Setting results
|
|
197
|
+
|
|
198
|
+
Setting writes return the observed `value` and its `previous` value. Registered types or callbacks
|
|
199
|
+
may normalize the input. Confirmation establishes that the previous value changed, not that the
|
|
200
|
+
requested value was stored exactly. Writing an already-stored value succeeds as unchanged without
|
|
201
|
+
calling Foundry. `requiresReload: true` indicates that the GM client needs a reload.
|
|
202
|
+
|
|
203
|
+
## Write confirmation
|
|
204
|
+
|
|
205
|
+
Document writes report success only after confirmation. Foundry can resolve a vetoed or invalid
|
|
206
|
+
write without throwing, and hooks can remove or rewrite part of a patch. Single and bulk updates
|
|
207
|
+
pass Foundry a private copy, then compare stored state with the original request.
|
|
208
|
+
|
|
209
|
+
A valid patch whose requested state is already stored remains a successful no-op. A validation
|
|
210
|
+
failure recovered after an unwritten update returns `INVALID_PARAMS`. Unconfirmed, partial, or
|
|
211
|
+
indeterminate updates can return `INTERNAL_ERROR` with these details:
|
|
212
|
+
|
|
213
|
+
- `fields`: requested fields whose state was not confirmed.
|
|
214
|
+
- `partial: true`: stored data changed in a requested field, but the full requested state was
|
|
215
|
+
not reached. A hook may have applied only part of a nested object or replaced a value.
|
|
216
|
+
- `changedFields`: requested top-level fields observed to change, including fields that changed
|
|
217
|
+
only in part. This does not attribute the change to this request rather than a concurrent write.
|
|
218
|
+
- `appliedFields`: changed top-level fields whose requested state was confirmed. This can be empty
|
|
219
|
+
even when `partial` is true.
|
|
220
|
+
- `indeterminate: true`: confirmation could not establish the outcome. Some or all of the write
|
|
221
|
+
may have persisted. `changedFields` is `null` when the before/after comparison was unavailable.
|
|
222
|
+
|
|
223
|
+
Parent patches that create embedded entries without `_id`, such as new `behaviors` in
|
|
224
|
+
`scene.region.update`, return an indeterminate error after writing because parent update results
|
|
225
|
+
cannot confirm those creations. A retry may duplicate them. Read the embedded collection first,
|
|
226
|
+
or use a dedicated embedded create command. Dry runs can still preview these patches.
|
|
227
|
+
|
|
228
|
+
After partial or indeterminate errors, read stored state and submit only remaining changes as a
|
|
229
|
+
new operation with a fresh idempotency key if using one.
|
|
230
|
+
|
|
231
|
+
### Patch shape checks
|
|
232
|
+
|
|
233
|
+
Single updates, bulk updates, and dry runs reject ambiguous field spellings, invalid or discarded
|
|
234
|
+
array values, and dotted writes inside arrays with `INVALID_PARAMS` before mutation. Recognized
|
|
235
|
+
legacy fields are checked against their migrated destinations as well. Send whole arrays; ordinary
|
|
236
|
+
dotted object-property writes remain supported where the schema permits them.
|
|
237
|
+
|
|
238
|
+
Errors identify the `field` and may include `arrayField`, `requested`, `stored`, or migration
|
|
239
|
+
destination details. Bulk failures add the entry's `index` and `id`; single failures use document
|
|
240
|
+
coordinates. These details describe a rejected patch, not a write result.
|
|
169
241
|
|
|
170
242
|
## Dry run
|
|
171
243
|
|
|
@@ -178,66 +250,58 @@ world can change between preview and commit.
|
|
|
178
250
|
|
|
179
251
|
## Idempotency
|
|
180
252
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
253
|
+
An idempotency key identifies one logical request. Reusing it with another command or payload
|
|
254
|
+
returns `IDEMPOTENCY_KEY_CONFLICT`. The cache is bounded and temporary; it cannot guarantee exactly
|
|
255
|
+
once execution across state loss. Keys remain subject to the [delivery rules](#delivery-states-and-retries).
|
|
256
|
+
|
|
257
|
+
An idempotency key covers the request and its approval:
|
|
258
|
+
|
|
259
|
+
- After `APPROVAL_PENDING`, the daemon links the key to that approval. A byte-identical retry returns
|
|
260
|
+
the same pending response. A different request with that key returns
|
|
261
|
+
`IDEMPOTENCY_KEY_CONFLICT`.
|
|
262
|
+
- An approved outcome becomes the cached final response. A denial, timeout, or confirmed
|
|
263
|
+
cancellation removes the link, so the same request can start a new approval.
|
|
264
|
+
- If the daemon cannot read the approval outcome, the key remains indeterminate and returns
|
|
265
|
+
`APPROVAL_UNKNOWN` until expiry. Read world state before retrying under a fresh key.
|
|
266
|
+
- If a bridge session ends before the daemon receives the first response, the daemon retains the key
|
|
267
|
+
as lost in flight. Reuse returns `BRIDGE_DISCONNECTED` with `reason: "lost-in-flight"`. Read world
|
|
268
|
+
state, then use a fresh key if the operation still needs to run.
|
|
269
|
+
- The daemon reserves bounded space before forwarding a keyed request. If no slot is available, it
|
|
270
|
+
returns `IDEMPOTENCY_STORE_FULL` before Foundry receives the request. Retry after earlier keys
|
|
271
|
+
settle or expire.
|
|
272
|
+
- Daemon restart, world switch, pairing switch, and expiry clear runtime idempotency state. Cached
|
|
273
|
+
successes may also be evicted. Later requests can then reach Foundry as new operations.
|
|
187
274
|
|
|
188
275
|
## Batch requests and bulk writes
|
|
189
276
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
277
|
+
[`exec --stdin`](commands.md#send-a-batch-of-commands) is a CLI wrapper over individual requests.
|
|
278
|
+
Its input ids and line indexes belong to CLI output, not the wire contract.
|
|
279
|
+
|
|
280
|
+
Bulk writes accept bounded arrays and prevalidate their elements. Persistence is not transactional;
|
|
281
|
+
results report `complete` and per-element `outcomes`, all of which the caller must inspect.
|
|
195
282
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
rather than failing the request, because a world routinely registers settings only for the systems
|
|
199
|
-
and modules it has active, so a partially resolvable set of keys is the normal case rather than an
|
|
200
|
-
error.
|
|
283
|
+
`get-many` fails the request if any requested id cannot be read. `setting.get-many` is the exception;
|
|
284
|
+
it reports an unregistered key as `SETTING_NOT_FOUND` on that row without failing the whole request.
|
|
201
285
|
|
|
202
286
|
## Error model
|
|
203
287
|
|
|
204
|
-
Errors
|
|
205
|
-
documented detail fields
|
|
206
|
-
code set
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
| Capability | `UNSUPPORTED_OPERATION` | Choose a supported workflow or runtime |
|
|
216
|
-
| Size/resource | `PAYLOAD_TOO_LARGE`, `QUERY_TOO_BROAD`, `IDEMPOTENCY_STORE_FULL` | Reduce, page, or resend the request later |
|
|
217
|
-
| Command policy | `COMMAND_DENIED` | Treat the command as unavailable on that GM client |
|
|
218
|
-
| Approval | `APPROVAL_PENDING`, `APPROVAL_DENIED`, `APPROVAL_TIMEOUT`, `APPROVAL_CANCELLED`, `APPROVAL_QUEUE_FULL`, `APPROVAL_UNKNOWN` | Apply the approval rules below |
|
|
219
|
-
| Bridge state | `BRIDGE_NOT_READY`, `BRIDGE_TIMEOUT`, `BRIDGE_DISCONNECTED` | Apply the delivery rules below |
|
|
220
|
-
| Indeterminate outcome | `MACRO_TIMEOUT` | Verify the effect by reads before retrying |
|
|
221
|
-
| Unexpected | `INTERNAL_ERROR` | Preserve details and investigate |
|
|
222
|
-
|
|
223
|
-
Foundry DataModel validation failures surface as parameter errors and are distinguished in details
|
|
224
|
-
where available. A failed nested lookup identifies the level that failed.
|
|
225
|
-
|
|
226
|
-
`UNSUPPORTED_PROTOCOL_VERSION` details carry `expectedVersion`, `actualVersion`, the rejecting
|
|
227
|
-
`handshake`, and `staleComponent`. The component is `module`, `cli-daemon`, or `unknown`. Consumers
|
|
228
|
-
can name the component that needs an update when the comparison identifies it. An unordered version
|
|
229
|
-
or unidentified peer produces `unknown`.
|
|
230
|
-
|
|
231
|
-
With JSON output, a failed command emits one structured error envelope on stdout and exits
|
|
232
|
-
non-zero. The exit code is a coarse process classification; the structured error code is the
|
|
233
|
-
authoritative automation signal. `exec --stdin` reports per-line errors and uses its own aggregate
|
|
234
|
-
exit status.
|
|
288
|
+
Errors carry a stable `code`, a human-readable `message`, and optional `details`. Branch on codes
|
|
289
|
+
and documented detail fields, not message text. The [constants](../packages/protocol/src/constants.js)
|
|
290
|
+
export the code set. CLI exit codes are coarse classifications; the JSON error is authoritative.
|
|
291
|
+
|
|
292
|
+
Validation errors identify invalid parameters; nested lookup failures identify the failed level.
|
|
293
|
+
Approval and delivery errors need the [retry rules](#delivery-states-and-retries), since a failure
|
|
294
|
+
does not always mean that nothing ran.
|
|
295
|
+
|
|
296
|
+
`UNSUPPORTED_PROTOCOL_VERSION` details contain `expectedVersion`, `actualVersion`, `handshake`,
|
|
297
|
+
and `staleComponent`. The last field identifies the older component as `module`, `cli-daemon`,
|
|
298
|
+
or `unknown` when the peer or version ordering cannot be established.
|
|
235
299
|
|
|
236
300
|
## Approval flow
|
|
237
301
|
|
|
238
|
-
A GM client's
|
|
239
|
-
|
|
240
|
-
|
|
302
|
+
A GM client's policy can hold an invocation for approval before dispatch. Pairing approval grants
|
|
303
|
+
a browser credential; command approval permits an invocation; confirmation checks a completed write.
|
|
304
|
+
See [Security](security.md#permissions-and-destructive-actions) for policy and review limits.
|
|
241
305
|
|
|
242
306
|
The wait has two phases because the decision can outlast a normal request timeout:
|
|
243
307
|
|
|
@@ -265,6 +329,12 @@ The wait has two phases because the decision can outlast a normal request timeou
|
|
|
265
329
|
- `APPROVAL_UNKNOWN` means the module no longer holds that approval. Reloading the GM client, ending
|
|
266
330
|
its bridge session, or expiry can remove the state. The command may not have started, or it may have
|
|
267
331
|
completed. Read world state before another write.
|
|
332
|
+
- For `macro.execute`, approval binds the body and type captured at admission. Drift or a missing
|
|
333
|
+
capture prevents execution and returns `APPROVAL_STALE` in the approved outcome's `response`.
|
|
334
|
+
Drift details include `macroId`, `requestedMacroId`, and `drifted`, which names `body`, `type`,
|
|
335
|
+
`existence`, or `identity`. A missing capture returns details with `command` instead. Read the
|
|
336
|
+
current macro before a fresh request, using a new idempotency key because the refusal can be
|
|
337
|
+
cached. No new approval is created automatically.
|
|
268
338
|
- A dry run bypasses approval and reports `approvalRequired: true` when the real command would wait.
|
|
269
339
|
The policy still refuses denied commands during a dry run.
|
|
270
340
|
- `policy.snapshot` reports `{ approve: [names], deny: [names] }`. The result is advisory because the
|
|
@@ -275,25 +345,6 @@ are closed. `approval.await`, `approval.cancel`, and `policy.snapshot` do not ap
|
|
|
275
345
|
`system.info` command inventory, or bridge status. `schema <command>` still returns their schemas.
|
|
276
346
|
The bridge handshake advertises them because the daemon must forward them.
|
|
277
347
|
|
|
278
|
-
An idempotency key covers the request and its approval:
|
|
279
|
-
|
|
280
|
-
- After `APPROVAL_PENDING`, the daemon links the key to that approval. A byte-identical retry returns
|
|
281
|
-
the same pending response. A different request with that key returns
|
|
282
|
-
`IDEMPOTENCY_KEY_CONFLICT`.
|
|
283
|
-
- An approved outcome becomes the cached final response. A denial, timeout, or confirmed
|
|
284
|
-
cancellation removes the link, so the same request can start a new approval.
|
|
285
|
-
- If the daemon cannot read the approval outcome, the key remains indeterminate and returns
|
|
286
|
-
`APPROVAL_UNKNOWN` until expiry. Read world state before retrying under a fresh key.
|
|
287
|
-
- If a bridge session ends before the daemon receives the first response, the daemon retains the key
|
|
288
|
-
as lost in flight. Reuse returns `BRIDGE_DISCONNECTED` with `reason: "lost-in-flight"`. Read world
|
|
289
|
-
state, then use a fresh key if the operation still needs to run.
|
|
290
|
-
- The daemon reserves bounded space before forwarding a keyed request. If no slot is available, it
|
|
291
|
-
returns `IDEMPOTENCY_STORE_FULL` before Foundry receives the request. Retry after earlier keys
|
|
292
|
-
settle or expire.
|
|
293
|
-
- Daemon restart, world switch, pairing switch, and expiry clear runtime idempotency state. A later
|
|
294
|
-
request can reach Foundry as a new operation, so an indeterminate result still requires a state
|
|
295
|
-
read first.
|
|
296
|
-
|
|
297
348
|
## Delivery states and retries
|
|
298
349
|
|
|
299
350
|
Retry safety is a function of whether the request reached Foundry:
|
|
@@ -308,22 +359,12 @@ Retry safety is a function of whether the request reached Foundry:
|
|
|
308
359
|
| `COMMAND_DENIED` | Refused before dispatch | Not executed; the command is unavailable on that GM client |
|
|
309
360
|
| `APPROVAL_DENIED`, `APPROVAL_TIMEOUT`, `APPROVAL_CANCELLED` | Reached Foundry, never dispatched | Not executed; the same request is safe to send again |
|
|
310
361
|
| `APPROVAL_QUEUE_FULL` | Refused before admission | Not executed; safe to retry when the waiting decisions clear |
|
|
362
|
+
| `APPROVAL_STALE` | Allowed, refused before dispatch | Not executed; read the macro and request fresh approval with a new idempotency key if using one |
|
|
311
363
|
| `IDEMPOTENCY_STORE_FULL` | No | Not executed; safe to retry when earlier keys settle or expire |
|
|
312
364
|
| `APPROVAL_UNKNOWN` | Unknown | May have committed; inspect state, then re-request under a fresh idempotency key |
|
|
365
|
+
| Update error with `partial` or `indeterminate` details | Yes | Read stored state; send only remaining changes as a new operation |
|
|
313
366
|
| Structured command rejection | Resolved with an error | Correct according to the code |
|
|
314
367
|
|
|
315
368
|
The distinction between connection-phase and response-wait failures is carried in structured error
|
|
316
369
|
details. Default waits, forward timeouts, heartbeats, and backoff bounds are defined in the
|
|
317
370
|
protocol and CLI constants; runtime flags can override the client and daemon request timeouts.
|
|
318
|
-
|
|
319
|
-
## Compatibility rules
|
|
320
|
-
|
|
321
|
-
- One release ships the CLI, daemon, and Foundry module as a compatible set. They share that release's
|
|
322
|
-
version. The bridge refuses mixed-release operation and does not negotiate a subset.
|
|
323
|
-
- Within a release line, additive result and handshake fields are how the contract evolves without
|
|
324
|
-
changing the meaning of existing fields; request schemas remain explicit and versioned.
|
|
325
|
-
- A bridge advertises the command set it can execute; the daemon forwards only advertised commands.
|
|
326
|
-
- Unsupported version-dependent behavior produces a predictable error rather than a false success.
|
|
327
|
-
|
|
328
|
-
Foundry-version behavior belongs in [Foundry compatibility](compatibility.md), while per-command
|
|
329
|
-
request shape remains discoverable from the registry.
|