fvtt-world-cli 1.0.0 → 1.1.0

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/docs/README.md CHANGED
@@ -29,7 +29,7 @@ inventory in prose; [Agent skill](skill.md) explains why it exists and how it is
29
29
  | [Getting started](getting-started.md) | People | First-run daemon, pairing, and bridge walkthrough |
30
30
  | [Commands](commands.md) | People | Capabilities, common workflows, shared command behavior, and navigation by task |
31
31
  | [Agent skill](skill.md) | Agent operators | Why the packaged skill exists, installing it, how updates and removal work |
32
- | [Protocol](protocol.md) | Implementers and automation consumers | Transport, session, delivery, and error semantics — the integration contract |
32
+ | [Protocol](protocol.md) | Implementers and automation consumers | Integration contract for transport, sessions, delivery, approval, and errors |
33
33
  | [Architecture](architecture.md) | Contributors | Component responsibilities and request flow |
34
34
  | [Security](security.md) | Operators and contributors | Trust boundaries, permissions, managed files, and known risks |
35
35
  | [Foundry compatibility](compatibility.md) | Operators and contributors | Current differences between supported Foundry major versions |
@@ -47,7 +47,7 @@ fvtt-world-cli actor update --help
47
47
  fvtt-world-cli docs protocol
48
48
  ```
49
49
 
50
- `commands --json` enumerates the command registry and identifies mutations. `schema` returns the
50
+ `commands --json` enumerates the available commands and identifies mutations. `schema` returns the
51
51
  request schema used by local validation. Command help maps protocol parameters to CLI flags. `docs`
52
52
  lists and prints these documentation files as shipped with the installed CLI.
53
53
 
@@ -55,6 +55,7 @@ not copied into documentation.
55
55
 
56
56
  - second-boundary request validation;
57
57
  - GM permission checks;
58
+ - the command-permission gate and the human approval loop it opens;
58
59
  - protected-metadata sanitization;
59
60
  - document lookup and serialization;
60
61
  - capability adaptation across supported Foundry versions;
@@ -63,6 +64,18 @@ not copied into documentation.
63
64
  - observable write confirmation;
64
65
  - managed-file containment.
65
66
 
67
+ The bridge checks command permissions after readiness, GM authority, parameter validation, and write
68
+ permission. It checks them immediately before handler lookup. Direct commands and commands released
69
+ by GM approval therefore use the same guards. The approved route skips only the permission gate for
70
+ that invocation.
71
+
72
+ The default permission profile is generated from one three-bucket rule in the protocol package,
73
+ with deny taking precedence over approve over allow: an explicit list denies commands that can
74
+ execute code, change who can do what, or persist outside the world's own data; destructive
75
+ commands and listed exceptions require approval; everything else is allowed. Keeping the rule in
76
+ the protocol package, next to the registry, means a new command cannot ship without a default
77
+ behavior, and the generated profile is byte-pinned so the rule and its output cannot drift apart.
78
+
66
79
  The module ships plain browser-compatible JavaScript. Its generated protocol mirror is produced from
67
80
  the canonical protocol package.
68
81
 
@@ -90,6 +103,11 @@ CLI invocation
90
103
  The bridge advertises its supported commands during the handshake. The daemon forwards only commands
91
104
  advertised by the active session.
92
105
 
106
+ A command that requires GM approval pauses before dispatch. The module keeps the invocation in
107
+ memory and returns a pending approval. The CLI then requests the outcome through short polls keyed by
108
+ the approval identifier. If the GM allows the command, the module runs the normal guard sequence
109
+ again before dispatch.
110
+
93
111
  ## Validation boundaries
94
112
 
95
113
  The CLI validation pass provides fast feedback and avoids unnecessary connections. The bridge repeats
@@ -121,6 +139,10 @@ A dry run performs the same preparation and guards as a real command, then stops
121
139
  Real commands confirm stored state where their contract depends on a write landing. Native Foundry
122
140
  batch calls can partially apply, so bulk results include per-element outcomes.
123
141
 
142
+ A capability check asks whether the connected Foundry can perform an operation. Approval is the GM's
143
+ decision to let one invocation run. Confirmation checks whether Foundry persisted a completed write.
144
+ An approved command can still fail during execution or confirmation.
145
+
124
146
  Idempotency keys reduce duplicate effects across response loss while the relevant daemon/bridge cache
125
147
  entry exists. They do not create durable distributed transactions.
126
148
 
@@ -160,11 +182,11 @@ re-pairing rotates only the re-pairing browser's credential. Making the browser
160
182
  also makes the human label meaningful, so the label travels with the pairing request instead of being
161
183
  editable daemon-side metadata: it is fixed between pairing approvals, and an approval that reuses an
162
184
  existing record adopts the label that request carried. The design keeps slot ownership
163
- unambiguous: a socket receives its role only after completed authentication rather
164
- than from a claimed message type; only a same-pairing socket can take over the slot, as the
165
- tab-reload recovery path; intentional goodbye, release, and revocation clear ownership before close
166
- handling, while only an abnormal close creates a short reclaim lease; and daemon-initiated release is
167
- terminal for the released client so reconnection remains an explicit operator action. Every way a
185
+ unambiguous. A socket receives its role only after completed authentication rather than from a
186
+ claimed message type, and only a same-pairing socket can take over the slot, as the tab-reload
187
+ recovery path. Intentional goodbye, release, and revocation clear ownership before close handling,
188
+ while only an abnormal close creates a short reclaim lease. Daemon-initiated release is terminal
189
+ for the released client, so reconnection remains an explicit operator action. Every way a
168
190
  pairing attempt can end shares one idempotent cleanup path, so the authorization UI cannot retain a
169
191
  stale pending state.
170
192
 
@@ -177,7 +199,15 @@ One of those operations parks instead of answering at once: the wait for a pairi
177
199
  response until a request arrives or the daemon's own park cap elapses. That cap is what keeps a parked
178
200
  answer inside the caller's request timeout, so an unanswered wait ends in an empty result the CLI
179
201
  re-issues rather than in a transport failure; a cap at or above the client's wait would turn every
180
- wait into one.
202
+ unanswered wait into a transport failure.
203
+
204
+ Approval waits use the same bounded polling pattern inside the Foundry module. The module answers the
205
+ original request with a pending approval. Later polls wait within the transport timeout and ask again
206
+ after an ordinary reconnect.
207
+
208
+ Both halves of the wait use runtime state. The browser session holds the decision and retained
209
+ outcome. The daemon links an idempotency key to the approval in its idempotency store. Neither half
210
+ persists this state. Losing it produces an indeterminate result rather than an automatic retry.
181
211
 
182
212
  Normative handshake, takeover, lease, and release semantics are defined in
183
213
  [Protocol](protocol.md#bridge-sessions); the authentication guarantees and host validation rules are
@@ -193,11 +223,13 @@ carries no wire-protocol meaning; the daemon and the CLI neither send nor observ
193
223
 
194
224
  The hook fires once per actual change, on the client transport status or on the handshake
195
225
  acknowledgement, and receives the same snapshot that `system info` reports as `bridge`: `status`,
196
- `url`, `helloAcknowledged`, `hasEstablishedSession`, `lastConnectedAt`, `reconnectAttempts`, and
197
- `terminalStopReason`. Readiness is `status === "connected"` together with `helloAcknowledged`, because
198
- an open socket precedes the daemon's acknowledgement. A snapshot never reports an acknowledged
199
- handshake on a client that is no longer connected: losing the socket resets the acknowledgement before
200
- the status transition that publishes it, so consumers cannot observe that contradictory pair.
226
+ `url`, `helloAcknowledged`, `hasEstablishedSession`, `lastConnectedAt`, `reconnectAttempts`,
227
+ `terminalStopReason`, and `protocolVersionMismatch`. The last field is `null` unless the module
228
+ refuses a handshake because the protocol versions differ. It then reports both versions and names
229
+ the older component as `module`, `cli-daemon`, or `unknown`. The status window uses that value to
230
+ show the required update. Readiness requires `status === "connected"` and `helloAcknowledged` because
231
+ the socket opens before the daemon acknowledges the handshake. Losing the socket clears the
232
+ acknowledgement before publishing the status change.
201
233
  `helloAcknowledged` stays in the snapshot for consumers that need the distinction; the module's own
202
234
  windows fold it into the connection state they display rather than showing it as its own field.
203
235
 
package/docs/commands.md CHANGED
@@ -65,21 +65,23 @@ the active slot can be cleared with `bridge release`. The daemon never prints a
65
65
  drops, which is reported as `DAEMON_UNAVAILABLE` rather than a silent stop and is safe to re-run once
66
66
  the daemon is back. `y` or `yes` approves the displayed code and exits 0, any other answer denies that
67
67
  code and exits 1 with `PAIRING_DECLINED`.
68
- Ctrl+C, or an ended stdin, at the confirmation instead leaves the request untouched, and it stays
69
- pending until it expires or another run answers for it. An ended stdin, and Ctrl+C while output goes
70
- to the terminal too, report `PAIRING_PROMPT_ABORTED` and exit 1; with output redirected the prompt
71
- does not read keystrokes itself, so Ctrl+C ends the run as an ordinary interrupt instead.
68
+ Interrupting the confirmation instead leaves the request untouched: after Ctrl+C or an ended
69
+ stdin it stays pending until it expires or another run answers for it. An ended stdin reports
70
+ `PAIRING_PROMPT_ABORTED` and exits 1, and Ctrl+C does the same while output goes to the terminal;
71
+ with output redirected the prompt does not read keystrokes itself, so Ctrl+C ends the run as an
72
+ ordinary interrupt instead.
72
73
  Delivery is a long-poll control call the CLI re-issues on its own, and the daemon answers each call
73
74
  inside its own park cap, so an ordinary wait does not end in a timeout; a daemon that stops answering
74
75
  altogether still trips the client's request timeout and is reported as `DAEMON_UNAVAILABLE`. The
75
76
  command is interactive-only: `--json`, and a stdin that is not a terminal, each fail
76
77
  immediately and name `auth pending` plus `auth approve --yes` as the path for scripts.
77
- `--timeout-ms` cannot cut the wait short either a value below the daemon's park cap plus five
78
- seconds is raised to it for the long-poll call, because a smaller client wait would abandon a parked
79
- response the daemon is still holding. That long poll is the only call the flag cannot shorten: the
80
- approval or denial that follows the answer, and the listing an interactive `auth approve` or
81
- `auth prune` reads before its prompt, take `--timeout-ms` as given for their client wait, as the
82
- other `auth` verbs and `bridge release` do; without the flag that wait is 60 seconds.
78
+ `--timeout-ms` cannot cut the wait short either. For the long-poll call, a value below the
79
+ daemon's park cap plus five seconds is raised to it, because a smaller client wait would abandon
80
+ a parked response the daemon is still holding. That long poll is the only call the flag cannot
81
+ shorten. The approval or denial that follows the answer takes `--timeout-ms` as given for its
82
+ client wait, and so does the listing an interactive `auth approve` or `auth prune` reads before
83
+ its prompt, as the other `auth` verbs and `bridge release` do. Without the flag that wait is
84
+ 60 seconds.
83
85
  - `auth status` shows bridge state and public profile metadata.
84
86
  - `auth pending` lists approval candidates with code, expiry, exact Origin, world, GM, browser client
85
87
  id, browser label, and module version.
@@ -87,13 +89,13 @@ the active slot can be cleared with `bridge release`. The daemon never prints a
87
89
  request is pending. Interactive use confirms the displayed identity, including the browser label and
88
90
  client id, and then approves that exact request: one that expired or disappeared while the prompt
89
91
  waited fails rather than approving whatever else is pending. When no single pending request can be
90
- shown nothing pending, an unknown code, or several candidates it names the live pending codes and
91
- stops instead of asking. Answering anything but `y` or `yes` cancels the approval, reports
92
- `PAIRING_DECLINED` and exits 1 the same code and exit the bare `auth` wait reports for a declined
93
- request, which the wait also denies outright while this verb leaves it pending. Interrupting the
94
- prompt also leaves the request pending, and reports `PAIRING_PROMPT_ABORTED` with exit 1 on the same
95
- terms as the wait. Scripts must pass `--yes`. The stored label is the one the browser sent — approval
96
- does not rename it.
92
+ shown, because nothing is pending, the code is unknown, or several candidates exist, it names the
93
+ live pending codes and stops instead of asking. Answering anything but `y` or `yes` cancels the
94
+ approval, reports `PAIRING_DECLINED` and exits 1, the same code and exit the bare `auth` wait
95
+ reports for a declined request; the wait also denies the request outright, while this verb leaves
96
+ it pending. Interrupting the prompt also leaves the request pending, and reports
97
+ `PAIRING_PROMPT_ABORTED` with exit 1 on the same terms as the wait. Scripts must pass `--yes`.
98
+ The stored label is the one the browser sent; approval does not rename it.
97
99
  - `auth deny <code>` rejects a pending request.
98
100
  - `auth list` shows non-secret profile metadata, including each profile's `clientId` and `label`.
99
101
  A label is set in the browser at pairing time and no control operation renames a stored record, so
@@ -107,11 +109,11 @@ the active slot can be cleared with `bridge release`. The daemon never prints a
107
109
  - `auth prune [--older-than <days>] [--yes]` deletes the profiles that have gone unused. A profile is
108
110
  a candidate when its `lastSeenAt` is older than the cutoff, which defaults to 30 days; the active
109
111
  bridge profile and the holder of an abnormal-disconnect lease are never deleted, however idle their
110
- stored timestamp looks. Interactive use lists the candidates it found label, client id, world, GM,
111
- last-seen timestamp, and pairing id and asks once for the whole set. The listing skips the profile
112
- `auth list` reports as active, so a browser that has stayed connected past the cutoff is neither shown
113
- nor counted in the prompt, matching what the daemon will do. Answering anything but `y` or
114
- `yes` removes nothing, reports `PAIRING_DECLINED` and exits 1; interrupting the prompt reports
112
+ stored timestamp looks. Interactive use lists the candidates it found, with label, client id,
113
+ world, GM, last-seen timestamp, and pairing id, and asks once for the whole set. The listing
114
+ skips the profile `auth list` reports as active, so a browser that has stayed connected past the
115
+ cutoff is neither shown nor counted in the prompt, matching what the daemon will do. Answering
116
+ anything but `y` or `yes` removes nothing, reports `PAIRING_DECLINED` and exits 1; interrupting the prompt reports
115
117
  `PAIRING_PROMPT_ABORTED` and exits 1 on the same terms as `auth approve`. A listing that found no
116
118
  candidate asks nothing and still runs the operation, so the command's output is the daemon's own empty
117
119
  result rather than a local verdict. The listing is a preview only: the daemon recomputes the set,
@@ -120,7 +122,7 @@ the active slot can be cleared with `bridge release`. The daemon never prints a
120
122
  crossed the cutoff while the prompt waited is removed although it was never listed; it can equally be
121
123
  smaller, because a profile that became active or was already removed between the two steps is
122
124
  reported as the daemon left it. There is no dry-run mode, and `--older-than 0` treats every profile
123
- as idle, which is the one case where a preview can name a lease holder the daemon then keeps
125
+ as idle, which is the one case where a preview can name a lease holder the daemon then keeps.
124
126
  `auth list` does not expose the lease, and a lease holder's last-seen timestamp is fresh by
125
127
  definition, so no realistic threshold selects it. Scripts pass `--yes`, which skips both the preview
126
128
  and the prompt; `--json` requires `--yes` as well, because the confirmation is never mixed into JSON
@@ -132,8 +134,8 @@ the active slot can be cleared with `bridge release`. The daemon never prints a
132
134
  An active browser stopped by release stays stopped until its operator chooses Connect.
133
135
 
134
136
  Every command in this section is answered by the daemon alone, with no Foundry browser involved, so
135
- a daemon that is not running or not reachable ends any of them the `auth` verbs and
136
- `bridge release` alike with `DAEMON_UNAVAILABLE` and exit 3 rather than a command-level failure.
137
+ a daemon that is not running or not reachable ends any of them, the `auth` verbs and
138
+ `bridge release` alike, with `DAEMON_UNAVAILABLE` and exit 3 rather than a command-level failure.
137
139
 
138
140
  In Foundry, Connect reuses the stored browser credential and Disconnect releases the slot without
139
141
  touching it. Unpair waits for confirmed daemon revocation
@@ -162,8 +164,10 @@ on the CLI:
162
164
  | Tokens embedded in a scene | `scene.token.*` | `fvtt-world-cli scene token …` |
163
165
  | Effects on a placed token | `scene.token.effect.*` | `fvtt-world-cli scene token effect …` |
164
166
 
165
- Use `fvtt-world-cli commands --json` for the complete current inventory. The exact operation set
167
+ Use `fvtt-world-cli commands --json` for the current inventory. The exact operation set
166
168
  varies by family, so a nearby document family is not a reliable guide to what another one supports.
169
+ With a bridge connected, that inventory is also filtered by the GM client's command permissions, as
170
+ [Discovery under a policy](#discovery-under-a-policy) describes.
167
171
 
168
172
  ## Capability map
169
173
 
@@ -171,7 +175,22 @@ varies by family, so a nearby document family is not a reliable guide to what an
171
175
 
172
176
  - `actor`, `item`, `journal`, `scene`, `macro`, `playlist`, `table`, and `cards` manage world
173
177
  documents.
174
- - `chat` reads, creates, and deletes chat messages.
178
+ - `chat` reads, creates, and deletes chat messages. `chat flush` erases the entire log at once and
179
+ asks for GM approval by default because nothing brings the messages back.
180
+ - `user` manages Foundry user accounts. `user update` edits harmless profile fields, `user create`
181
+ and `user delete` ask for approval by default, and `user role set` plus `user permissions set` are
182
+ [off by default](#commands-that-are-off-by-default). No command reads or writes a password, and
183
+ the account holding the bridge cannot demote or delete itself; see
184
+ [Security](security.md#users).
185
+ - `setting` lists registrations and reads values, including `setting get-many` for batch reads.
186
+ `setting set` and `setting set-many` write world, client, and user scopes but are
187
+ [off by default](#commands-that-are-off-by-default); they refuse this module's own namespace in
188
+ every mode, and a write to an already-stored value is reported as unchanged without touching
189
+ Foundry. The result's `value` is the value read back after the write, not the value requested: a
190
+ setting's registered type or `onChange` handler may normalize or clamp what was asked for, so
191
+ compare `value` against the requested input when the exact stored form matters. The write is
192
+ confirmed only in that Foundry no longer holds the `previous` value. A result carrying
193
+ `requiresReload: true` names the follow-up that Foundry expects, which `system reload` performs.
175
194
  - `combat` manages encounters and exposes explicit encounter transitions.
176
195
  - `folder` manages document organization.
177
196
  - Dedicated `*.ownership.set` commands change supported document ownership.
@@ -191,7 +210,11 @@ Common world-document operations include `list`, `get`, `get-many`, `create`, `u
191
210
  - `scene.token`, `tile`, `sound`, `wall`, `note`, `drawing`, `light`, `template`, and `region` manage
192
211
  scene placeables.
193
212
  - `scene.region.behavior` manages region behaviors; writes that supply executable core behavior
194
- types are rejected (see [Security](security.md#executable-content)).
213
+ types are rejected (see [Security](security.md#executable-content)). The separate
214
+ `scene.region.behavior.executable` family, [off by default](#commands-that-are-off-by-default),
215
+ authors `executeMacro` behaviors that reference an existing world macro; `executeScript` has no
216
+ command surface at all. Deleting an executable behavior uses the ordinary
217
+ `scene region behavior delete`.
195
218
 
196
219
  Embedded commands require the complete parent ID chain; a read of the parent supplies those IDs when
197
220
  they are not already known.
@@ -204,7 +227,38 @@ Some commands invoke a typed Foundry action instead of ordinary CRUD:
204
227
  - roll-table draw and reset;
205
228
  - card shuffle, reset, deal, draw, and pass;
206
229
  - combat start, activation, advancement, and initiative;
207
- - scene thumbnail generation and fog reset.
230
+ - scene thumbnail generation and fog reset;
231
+ - scene activation (`scene activate`) and pulling active users to a scene (`scene pull-users`);
232
+ - showing a journal entry (`journal show`) or an image (`image show`) to players;
233
+ - pausing or resuming the game clock for everyone (`game pause`);
234
+ - reloading the GM client (`system reload`), which asks for approval by default because it drops
235
+ the bridge until the client reconnects;
236
+ - macro execution (`macro execute`), [off by default](#commands-that-are-off-by-default).
237
+
238
+ `scene pull-users`, `journal show`, and `image show` reach every connected player when no
239
+ `--user-ids` list is given. A supplied list has to name at least one user: an empty one is
240
+ indistinguishable from "everyone" on the Foundry side, while the result would report that nobody was
241
+ addressed.
242
+
243
+ `macro execute` runs a world macro the GM can already execute, waits for it to finish up to a
244
+ bounded `--macro-timeout-ms`, and reports the returned value plus the chat messages it observed the
245
+ macro create. A macro that outlives the timeout keeps running in the GM browser and the command
246
+ returns the indeterminate `MACRO_TIMEOUT`, so the effect is verified by reads. A script macro that
247
+ throws fails the command and the error names what the macro raised; the outcome is partial, because
248
+ whatever the macro changed before it threw stays changed. A macro that catches its own errors still
249
+ reports a `null` return, and a macro is free to reload the page or navigate away, which ends the
250
+ bridge session the same way any disconnect does. Effects therefore deserve a read-back whenever the
251
+ return value alone does not prove them.
252
+
253
+ Each key of `--args-json` becomes a named variable Foundry splices into the compiled macro, so an
254
+ argument name must be a plain JavaScript identifier (letters, digits, `_` or `$`, not starting with
255
+ a digit) and cannot reuse a name Foundry already binds (`speaker`, `actor`, `token`, `character`,
256
+ `scope`). Any other name is refused before the macro runs.
257
+
258
+ The result's `chatCapture` field says how much of the chat the run observed: `captured` when every
259
+ message the macro was expected to create was seen, `not-created` when a chat macro created none,
260
+ `partial` when only some were seen, and `unknown` when this client could not watch the chat log at
261
+ all.
208
262
 
209
263
  Actions can have Foundry, system, or module side effects. Their result describes what the bridge can
210
264
  confirm, which may differ from a document post-state, so each action's schema and help are worth
@@ -216,7 +270,8 @@ reading before automating it.
216
270
  - `world.audit-files` finds document references to missing managed assets.
217
271
  - `compendium.list`, `compendium.index`, and `compendium.get` read pack content.
218
272
  - Supported `*.import-from-compendium` commands create world documents from pack sources.
219
- - `user` and `setting` provide read-only discovery surfaces.
273
+ - `user list`/`user get` and `setting list`/`setting get`/`setting get-many` are the discovery
274
+ side of the [user and setting families](#world-content).
220
275
 
221
276
  ### Managed files
222
277
 
@@ -228,6 +283,106 @@ See [Security](security.md#file-write-boundary) before automating file writes.
228
283
 
229
284
  ## Shared command behavior
230
285
 
286
+ ### Command permissions and approval
287
+
288
+ Every command has one of three behaviors in the GM client that holds the bridge: allow, approve, or
289
+ deny. A GM edits them under Configure Settings → Module Settings → World CLI → Command permissions.
290
+ The settings belong to the browser profile, so another browser or machine can apply a different
291
+ policy.
292
+
293
+ An approved command waits for the GM instead of failing. Foundry opens the Command Approval window.
294
+ It shows the command, its targets, and its parameters. The CLI writes one status line to stderr:
295
+
296
+ ```
297
+ Waiting for GM approval in Foundry (command actor.delete, expires 2026-08-28T18:20:00.000Z). Press Ctrl+C to request cancellation.
298
+ ```
299
+
300
+ The line uses stderr in both output modes, so `--json` stdout still carries one envelope. Allow runs
301
+ the command at the time of the decision. The module repeats the normal guards first because world
302
+ state and permissions may have changed while the request waited. The CLI then returns the command's
303
+ success or error.
304
+
305
+ The wait tolerates short daemon outages, bridge reconnects, and poll timeouts while the approval
306
+ remains open. Other outcomes use structured errors:
307
+
308
+ | Code | Meaning | State |
309
+ |---|---|---|
310
+ | `COMMAND_DENIED` | The permission is deny, whether at the request or by the time an approved command runs | Not executed |
311
+ | `APPROVAL_DENIED` | The GM chose Deny | Not executed |
312
+ | `APPROVAL_TIMEOUT` | No decision was taken before the approval expired | Not executed |
313
+ | `APPROVAL_CANCELLED` | A cancellation the GM client confirmed won the decision | Not executed |
314
+ | `APPROVAL_QUEUE_FULL` | The module refused admission before showing the request | Not executed |
315
+ | `APPROVAL_UNKNOWN` | The module no longer holds the decision | Indeterminate |
316
+
317
+ `APPROVAL_UNKNOWN` means the client can no longer prove whether the command ran. Read the affected
318
+ world state before trying again. `APPROVAL_QUEUE_FULL` means the module refused the request before
319
+ execution. Retry after the GM clears earlier requests.
320
+
321
+ Ctrl+C asks the GM client to cancel a waiting decision. Only `APPROVAL_CANCELLED` proves that the
322
+ command will not run. If the command has started or the client cannot confirm cancellation, the CLI
323
+ reports an indeterminate result.
324
+
325
+ The default policy sorts commands into the three behaviors by what a mistake would cost. Commands
326
+ that can execute code, change who can do what, or persist outside the world's own data are denied
327
+ until a human enables them; the next section lists them. Commands that destroy world data ask for
328
+ approval, as do `system.reload` and `user.create`; the destructive set is the `delete` and
329
+ `delete-many` verbs, plus `file.move`, `scene.fog.reset`, and `chat.flush`. The remaining commands
330
+ run on their own unless they are exempt from the policy. The Command permissions window and
331
+ `fvtt-world-cli commands --json` show the current inventory.
332
+
333
+ #### Commands that are off by default
334
+
335
+ The following commands ship with the deny behavior. They stay invisible to
336
+ [discovery](#discovery-under-a-policy) and refuse to run, even as dry runs, until a GM enables
337
+ them in the Command permissions window of the browser profile holding the bridge:
338
+
339
+ - `macro.execute`
340
+ - `setting.set`
341
+ - `setting.set-many`
342
+ - `user.role.set`
343
+ - `user.permissions.set`
344
+ - `scene.region.behavior.executable.create`
345
+ - `scene.region.behavior.executable.update`
346
+ - `scene.region.behavior.executable.clone`
347
+
348
+ They are denied by default because each one executes code, changes who can do what, or persists
349
+ outside the world's own data. Enabling one is a per-browser-profile decision, and the approval
350
+ behavior remains available as a middle ground: a GM who wants to see every macro body before it
351
+ runs sets `macro.execute` to approve rather than allow. [Security](security.md) describes what each
352
+ of these surfaces can and cannot do.
353
+
354
+ `system.ping`, `system.info`, and the internal approval-wait commands always run. This keeps the
355
+ bridge able to report its state and finish an existing decision. The permissions window omits those
356
+ commands. Pairing and other `auth` operations run in the daemon, outside the command policy.
357
+
358
+ Foundry plays its standard interface notification when a request enters an empty queue. The `Play a
359
+ sound on approval requests` setting controls it. Browsers may delay the first sound until the GM
360
+ interacts with the page after a reload.
361
+
362
+ Set the approval deadline with `Approval timeout (minutes)` in the main Module Settings form. The
363
+ form and protocol enforce the supported range. Expiry refuses the command without running it.
364
+
365
+ Approval state lives in the GM client's memory. Reloading that client or ending its bridge session
366
+ can produce `BRIDGE_DISCONNECTED` or `APPROVAL_UNKNOWN`. Both are indeterminate. Read the affected
367
+ world state before another write, then use a fresh idempotency key if the command still needs to run.
368
+ The complete state and retry contract is in [Protocol](protocol.md#approval-flow).
369
+
370
+ ### Discovery under a policy
371
+
372
+ With the bridge reachable, `commands` and `commands --json` describe what that GM client will
373
+ actually run. The listing omits denied commands. JSON marks approval waits with `"approval": true`;
374
+ plain output uses an `approval` tag. The JSON envelope also contains
375
+ `policy: { "applied": true, "source": "bridge" }`.
376
+
377
+ When no bridge answers, the listing falls back to the full static registry and says so:
378
+ `policy: { "applied": false, "source": "static", "reason": … }` in JSON, and a warning on stderr in
379
+ plain output. Only an unavailable daemon or bridge triggers fallback. Authentication, validation,
380
+ and protocol failures return errors.
381
+
382
+ `schema`, `--help`, and this documentation describe the whole static registry. If a caller sends a
383
+ denied command found there, the GM client returns `COMMAND_DENIED`. Discovery hides; the client
384
+ enforces.
385
+
231
386
  ### JSON output
232
387
 
233
388
  Use `--json` for automation. Successful requests use a stable envelope:
@@ -303,9 +458,13 @@ All mutation commands accept the global `--dry-run` flag:
303
458
  fvtt-world-cli --dry-run actor update --actor-id <id> --name "New name" --json
304
459
  ```
305
460
 
306
- The result uses the normal command shape and includes `dryRun: true`. The preview contract — what a
307
- dry run executes, what it can report, and its non-reservation of state — is defined in
308
- [Protocol](protocol.md#dry-run).
461
+ The result uses the normal command shape and includes `dryRun: true`. The preview contract is
462
+ defined in [Protocol](protocol.md#dry-run): what a dry run executes, what it can report, and its
463
+ non-reservation of state.
464
+
465
+ Approval does not hold a preview. A command whose permission is approve previews without asking the
466
+ GM, and its result carries `approvalRequired: true` so the caller knows the commit will wait. The GM
467
+ client returns `COMMAND_DENIED` for a denied preview.
309
468
 
310
469
  ### Idempotency and retries
311
470
 
@@ -361,7 +520,9 @@ First-run setup is covered in [Getting started](getting-started.md).
361
520
 
362
521
  ## Unsupported boundaries
363
522
 
364
- The CLI intentionally does not provide arbitrary JavaScript execution, direct world-database writes,
365
- unrestricted filesystem access, generic RPC, compendium editing, setting writes, or transactional
366
- Foundry batches. Consult [Security](security.md) for the trust boundary and
367
- [Foundry compatibility](compatibility.md) for version-dependent capabilities.
523
+ The CLI intentionally does not provide arbitrary JavaScript evaluation, direct world-database
524
+ writes, unrestricted filesystem access, generic RPC, compendium editing, or transactional Foundry
525
+ batches. Code runs only through `macro.execute` and `executeMacro` region behaviors, both off by
526
+ default; when a GM keeps them on approve, the window shows a macro execution's own body and names
527
+ the macro a region behavior would run. [Security](security.md) describes the trust
528
+ boundary and [Foundry compatibility](compatibility.md) the version-dependent capabilities.
@@ -7,9 +7,9 @@ result honestly.
7
7
  ## Operator contract
8
8
 
9
9
  - `fvtt-world-cli system info --json` reports the connected Foundry, system, module, limits, and
10
- advertised command set.
10
+ discoverable command inventory.
11
11
  - `UNSUPPORTED_OPERATION` is a capability result, not a transient transport failure.
12
- - A dry run validates a proposed mutation, but values that require execution are not a forecast.
12
+ - A dry run validates a proposed mutation; it does not forecast values that require execution.
13
13
 
14
14
  ## Known differences
15
15
 
@@ -21,6 +21,8 @@ result honestly.
21
21
  | Scene placeable fields | Older document models | Some families add fields | Open-family writes pass through sanitized data; reads expose the documented projection |
22
22
  | Region behaviors | Core v13 type set | Additional core types | Executable core behavior types remain guarded; other types are Foundry-validated |
23
23
  | Combat and action APIs | Version-specific signatures | Version-specific signatures | The bridge adapts known signatures and refuses unsupported behavior |
24
+ | Scene activation | `Scene#activate` runs for any caller | `Scene#activate` throws for non-GM users | The bridge requires GM authority on both versions |
25
+ | User permission names | `TEMPLATE_CREATE` | Renamed to `REGION_CREATE` | `user.permissions.set` validates keys against the connected version's live permission set rather than a fixed list |
24
26
 
25
27
  This table describes current operator-visible differences, not implementation evidence. Exact
26
28
  capabilities remain defined by the connected bridge and exercised by live smoke tests.
@@ -16,7 +16,7 @@ tool is in use.
16
16
 
17
17
  The daemon should be running before the Foundry GM client loads the world: the module tries to
18
18
  connect once when the world loads, and if the daemon is down at that moment it gives up with a
19
- warning. Connecting after that is a manual retry Connect in the scene controls, or a client
19
+ warning. Connecting after that is a manual retry: Connect in the scene controls, or a client
20
20
  reload.
21
21
 
22
22
  ## 2. Pair the Foundry GM client
@@ -27,10 +27,10 @@ browser.
27
27
  It starts in the module's Authorization window. The quickest way there is the `World CLI` group at
28
28
  the bottom of the left scene controls; its icon doubles as a status light:
29
29
 
30
- - muted grey not paired yet
31
- - amber connecting
32
- - green the bridge is up
33
- - red stopped or failed
30
+ - muted grey: not paired yet
31
+ - amber: connecting
32
+ - green: the bridge is up
33
+ - red: stopped or failed
34
34
 
35
35
  The same windows are also reachable through Configure Settings → Module Settings:
36
36
 
@@ -48,15 +48,15 @@ record.
48
48
 
49
49
  ![The Authorization window before pairing](images/authorization-window.png)
50
50
 
51
- Choose Pair, then approve the request from a terminal on the daemon's machine the approval is
51
+ Choose Pair, then approve the request from a terminal on the daemon's machine. The approval is
52
52
  what turns the request into a stored permission:
53
53
 
54
54
  ```bash
55
55
  fvtt-world-cli auth
56
56
  ```
57
57
 
58
- The command waits for the pairing request (starting it before or after clicking Pair both work),
59
- shows who is asking origin, world, GM, browser label, client id and asks
58
+ The command waits for the pairing request; starting it before or after clicking Pair works either
59
+ way. It shows who is asking, with the origin, world, GM, browser label, and client id, and asks
60
60
  `Approve pairing request <code>? [y/N]`. Typing `y` and pressing Enter approves it; any other
61
61
  answer denies it. To read a request over before deciding, or to approve from a script, the same
62
62
  approval exists as two steps:
@@ -68,7 +68,7 @@ fvtt-world-cli auth approve <code> # add --yes in a script to skip the confirm
68
68
 
69
69
  ## 3. Confirm the bridge
70
70
 
71
- On approval the browser stores its pairing credential and starts the bridge immediately — no
71
+ On approval the browser stores its pairing credential and starts the bridge immediately. No
72
72
  reload is needed, and the scene-controls icon turns green. From now on the bridge connects on its
73
73
  own as soon as the world loads: that is the client-scoped `Connect automatically` setting, enabled
74
74
  by default; with it disabled the bridge stays offline until Connect is chosen.
@@ -81,6 +81,15 @@ fvtt-world-cli system info --json
81
81
 
82
82
  It reports `bridge.status` as `connected`.
83
83
 
84
+ Commands use the permissions stored for that browser profile. Run `fvtt-world-cli commands` after
85
+ the bridge connects to see which commands the client will run. A daemon that rejects the browser
86
+ credential returns an error instead of the static registry.
87
+
88
+ Destructive commands ask the GM by default. Set each command's behavior under Configure Settings →
89
+ Module Settings → World CLI → Command permissions. Set the waiting time in the `Approval timeout
90
+ (minutes)` field in the main Module Settings form. [Commands](commands.md#command-permissions-and-approval)
91
+ describes the behavior in full.
92
+
84
93
  ## When something deviates
85
94
 
86
95
  The Bridge status window (in the scene controls or the module settings) is the first place to
@@ -96,7 +105,7 @@ abnormal-disconnect lease.
96
105
 
97
106
  One bridge is active at a time, so a second paired browser's connection attempt stops on
98
107
  `BRIDGE_BUSY` and stays stopped. Switching browsers is a Disconnect on the active one followed by
99
- Connect on the other pairing plays no part in it.
108
+ Connect on the other; pairing plays no part in it.
100
109
 
101
110
  Authorization holds the two exits. Unpair revokes this browser's access and removes its credential
102
111
  once the daemon confirms the revocation. Forget local is the recovery for a daemon that cannot be