fvtt-world-cli 1.0.0 → 1.1.1
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 +18 -15
- package/bin/fvtt-world-cli.js +12 -0
- package/dist/index.js +2479 -128
- package/docs/README.md +2 -2
- package/docs/architecture.md +43 -11
- package/docs/commands.md +198 -37
- package/docs/compatibility.md +4 -2
- package/docs/getting-started.md +19 -10
- package/docs/protocol.md +114 -23
- package/docs/security.md +155 -22
- package/docs/skill.md +12 -8
- package/package.json +5 -6
- package/skills/foundry-world-editor/SKILL.md +68 -29
- package/skills/foundry-world-editor/SKILL.md.sha256 +1 -1
- package/bin/sync-installed-skill.mjs +0 -89
package/docs/protocol.md
CHANGED
|
@@ -15,15 +15,18 @@ size limits are defined by the protocol package and discoverable at runtime:
|
|
|
15
15
|
|
|
16
16
|
## Versioning
|
|
17
17
|
|
|
18
|
-
The protocol
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
The protocol version equals the product release version. Every transport message carries it, and all
|
|
19
|
+
components in one installation must match exactly. The daemon and Foundry module reject a mismatch
|
|
20
|
+
instead of negotiating a subset of the contract.
|
|
21
|
+
|
|
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.
|
|
22
25
|
|
|
23
26
|
## Transport model
|
|
24
27
|
|
|
25
28
|
The daemon listens on a loopback WebSocket endpoint. Credentials never appear in URLs. A local
|
|
26
|
-
client
|
|
29
|
+
client, the CLI or a future Companion, has no browser Origin and must establish its role with its
|
|
27
30
|
first message: the daemon enforces a short deadline for a valid first frame, closes malformed
|
|
28
31
|
openings immediately, and never assigns a role before authentication has succeeded. Browser sockets
|
|
29
32
|
are identified by their exact HTTP(S) Origin and participate only in pairing and bridge sessions;
|
|
@@ -36,7 +39,7 @@ a malformed control request receives a correlated error and may keep using its c
|
|
|
36
39
|
The daemon accepts one active authenticated bridge at a time:
|
|
37
40
|
|
|
38
41
|
- another pairing cannot displace the active bridge and is rejected as `BRIDGE_BUSY`;
|
|
39
|
-
- a new socket from the same pairing takes the slot over
|
|
42
|
+
- a new socket from the same pairing takes the slot over, the tab-reload recovery path;
|
|
40
43
|
- a clean goodbye releases the slot immediately, while an abnormal close reserves it briefly for
|
|
41
44
|
the same pairing;
|
|
42
45
|
- a daemon-initiated release is terminal for the released client: it does not reconnect on its
|
|
@@ -56,13 +59,13 @@ applied to the transport only after the daemon restarts.
|
|
|
56
59
|
Pairing is the one-time exchange that lets a GM browser become a bridge. Its guarantees:
|
|
57
60
|
|
|
58
61
|
- a pending request is bound to the socket that made it and disappears when that socket closes;
|
|
59
|
-
-
|
|
62
|
+
- pairing codes expire after a bounded interval;
|
|
60
63
|
- the daemon persists only a digest of the bridge credential; the clear credential is delivered
|
|
61
64
|
exactly once, to the requesting socket, only after the digest has been persisted;
|
|
62
65
|
- approving the same Origin/world/user/client again re-pairs that client's existing profile by
|
|
63
66
|
rotating its credential instead of accumulating duplicates;
|
|
64
|
-
- expiry, denial, socket close, and
|
|
65
|
-
the browser-side authorization UI is never left waiting after daemon shutdown or expiry.
|
|
67
|
+
- expiry, denial, socket close, and a granted pairing all end the attempt through one idempotent
|
|
68
|
+
path, so the browser-side authorization UI is never left waiting after daemon shutdown or expiry.
|
|
66
69
|
|
|
67
70
|
### Client identity
|
|
68
71
|
|
|
@@ -82,7 +85,7 @@ independent records and neither re-pair disturbs the other.
|
|
|
82
85
|
|
|
83
86
|
The bridge hello carries `clientId` at the top level, beside `pairingId` and `credential`, because it
|
|
84
87
|
is authentication material rather than session content: the daemon rejects a hello whose client id
|
|
85
|
-
does not match the stored pairing. The label is not resent on hello
|
|
88
|
+
does not match the stored pairing. The label is not resent on hello; the daemon's pairing record owns
|
|
86
89
|
it, and the browser keeps a copy only to display it.
|
|
87
90
|
|
|
88
91
|
## Daemon control
|
|
@@ -110,8 +113,8 @@ never stamps it. The pairing that owns the active bridge, and the holder of a li
|
|
|
110
113
|
abnormal-disconnect lease, are excluded from removal regardless of their timestamps, so pruning can
|
|
111
114
|
never unpair the browser that is connected or the one the daemon is still holding a slot for. The
|
|
112
115
|
config is rewritten only when at least one record is removed. The result is `{ olderThanDays, pruned }`,
|
|
113
|
-
where `olderThanDays` is the cutoff the daemon applied
|
|
114
|
-
it
|
|
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`
|
|
115
118
|
serializes. The daemon computes the set at execution time; a caller that previewed candidates from
|
|
116
119
|
`auth.list` holds an advisory list, not the outcome, and the executed set may be wider than that
|
|
117
120
|
preview when a record crosses the cutoff between the two calls.
|
|
@@ -153,6 +156,12 @@ exclusive: success carries a result, failure carries a structured error.
|
|
|
153
156
|
|
|
154
157
|
Document results live under a type-named key inside the result. Collection, action, and bulk
|
|
155
158
|
results use their documented keys, which vary between commands but are stable for each one.
|
|
159
|
+
Broadcast commands that change no document, pulling users to a scene and showing a journal entry
|
|
160
|
+
or image, report `dispatched` rather than a confirmed post-state, because a socket broadcast offers
|
|
161
|
+
nothing to read back; the result names the users it targeted and the active/inactive split where
|
|
162
|
+
that is knowable. `macro.execute` reports the macro's returned value and observed chat messages,
|
|
163
|
+
and a timeout there is indeterminate: the macro keeps running in the GM client, so `MACRO_TIMEOUT`
|
|
164
|
+
callers verify effects by reads instead of retrying blindly.
|
|
156
165
|
Serialized projections expose `id` as the public identifier; a source `_id` mirror may accompany
|
|
157
166
|
it. A previewed new document has no persistent identity, and an id observed during a preview must
|
|
158
167
|
not be reused. List-like responses that paginate return their collection with a total and a
|
|
@@ -164,7 +173,7 @@ Mutation commands accept a dry-run request that passes through validation, resol
|
|
|
164
173
|
sanitization, permission checks, capability checks, and preparation, then returns before
|
|
165
174
|
persistence using the normal result shape with an explicit dry-run marker. Only values knowable
|
|
166
175
|
before execution are reported: random selection, rendering, hooks, and other execution-dependent
|
|
167
|
-
observations may be absent or explicitly unconfirmed. A successful preview reserves nothing
|
|
176
|
+
observations may be absent or explicitly unconfirmed. A successful preview reserves nothing; the
|
|
168
177
|
world can change between preview and commit.
|
|
169
178
|
|
|
170
179
|
## Idempotency
|
|
@@ -172,9 +181,9 @@ world can change between preview and commit.
|
|
|
172
181
|
Commands with duplicate-creation or non-repeatable-action risk accept or require an idempotency
|
|
173
182
|
key identifying one logical request. Reusing a key with a different command or payload is rejected
|
|
174
183
|
as `IDEMPOTENCY_KEY_CONFLICT`. Idempotency memory is runtime state: bounded, and cleared by daemon
|
|
175
|
-
restart, bridge replacement, world switch,
|
|
176
|
-
response loss
|
|
177
|
-
world-state read.
|
|
184
|
+
restart, bridge replacement, world switch, or expiry. The daemon may also evict cached successes.
|
|
185
|
+
Idempotency reduces duplicate effects across response loss. It is not a durable transaction, so an
|
|
186
|
+
indeterminate delivery still ends with a world-state read.
|
|
178
187
|
|
|
179
188
|
## Batch requests and bulk writes
|
|
180
189
|
|
|
@@ -184,6 +193,12 @@ commands over bounded arrays: elements are prevalidated, but Foundry persistence
|
|
|
184
193
|
transactional, so the result reports overall completeness plus a per-element outcome, and every
|
|
185
194
|
outcome carries its own meaning.
|
|
186
195
|
|
|
196
|
+
Batch reads (`get-many`) fail the whole request when any requested id cannot be read, with one
|
|
197
|
+
exception: `setting.get-many` reports an unregistered key on its own result row (`SETTING_NOT_FOUND`)
|
|
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.
|
|
201
|
+
|
|
187
202
|
## Error model
|
|
188
203
|
|
|
189
204
|
Errors have a stable code, a human-readable message, and optional structured details. The code and
|
|
@@ -195,21 +210,90 @@ code set is exported by the protocol package. The classes consumers act on:
|
|
|
195
210
|
| Request/schema | `INVALID_PARAMS`, `UNKNOWN_COMMAND` | Correct the request or resolve version skew |
|
|
196
211
|
| Authentication/permission | `UNAUTHORIZED`, `PERMISSION_DENIED` | Restore credentials or authority |
|
|
197
212
|
| Pairing | `PAIRING_REQUIRED`, `PAIRING_EXPIRED`, `BRIDGE_BUSY` | Pair, retry revocation, or release the active owner as indicated |
|
|
198
|
-
| Lookup | `*_NOT_FOUND` | Refresh ids and world state |
|
|
199
|
-
| Safety/policy | `DELETE_FORBIDDEN`, `PATH_NOT_ALLOWED` | Change the requested operation |
|
|
213
|
+
| Lookup | `*_NOT_FOUND`, `SETTING_UNREGISTERED` | Refresh ids and world state |
|
|
214
|
+
| Safety/policy | `DELETE_FORBIDDEN`, `PATH_NOT_ALLOWED`, `SETTING_PROTECTED`, `USER_SELF_PROTECTED` | Change the requested operation |
|
|
200
215
|
| Capability | `UNSUPPORTED_OPERATION` | Choose a supported workflow or runtime |
|
|
201
|
-
| Size/resource | `PAYLOAD_TOO_LARGE`, `QUERY_TOO_BROAD` | Reduce or
|
|
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 |
|
|
202
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 |
|
|
203
221
|
| Unexpected | `INTERNAL_ERROR` | Preserve details and investigate |
|
|
204
222
|
|
|
205
223
|
Foundry DataModel validation failures surface as parameter errors and are distinguished in details
|
|
206
224
|
where available. A failed nested lookup identifies the level that failed.
|
|
207
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
|
+
|
|
208
231
|
With JSON output, a failed command emits one structured error envelope on stdout and exits
|
|
209
232
|
non-zero. The exit code is a coarse process classification; the structured error code is the
|
|
210
233
|
authoritative automation signal. `exec --stdin` reports per-line errors and uses its own aggregate
|
|
211
234
|
exit status.
|
|
212
235
|
|
|
236
|
+
## Approval flow
|
|
237
|
+
|
|
238
|
+
A GM client's command policy can require approval before a command runs. Approval is the GM's
|
|
239
|
+
decision about one invocation. Pairing approval grants a browser credential, while confirmation
|
|
240
|
+
checks a completed write. The active GM client's policy controls the invocation.
|
|
241
|
+
|
|
242
|
+
The wait has two phases because the decision can outlast a normal request timeout:
|
|
243
|
+
|
|
244
|
+
- The original request returns `APPROVAL_PENDING`. Its details contain `approvalId`, `expiresAt` in
|
|
245
|
+
epoch milliseconds, and `command`. A consumer without approval support stops on this error. The CLI
|
|
246
|
+
converts it into a blocking wait.
|
|
247
|
+
- `approval.await { approvalId, waitMs? }` asks for the current state. `waitMs` cannot exceed
|
|
248
|
+
`APPROVAL_AWAIT_PARK_CAP_MS`. The result is either
|
|
249
|
+
`{ approvalId, status: "pending", expiresAt? }` or
|
|
250
|
+
`{ approvalId, status: "resolved", outcome, response? }`. Once execution starts, the approval can
|
|
251
|
+
no longer expire.
|
|
252
|
+
- Terminal outcomes are `approved`, `denied`, `timeout`, and `cancelled`. An approved outcome carries
|
|
253
|
+
the command response, including handler errors, and uses the approval identifier as its envelope
|
|
254
|
+
`id`. The other outcomes mean the command did not run. The CLI reports them as `APPROVAL_DENIED`,
|
|
255
|
+
`APPROVAL_TIMEOUT`, or `APPROVAL_CANCELLED`.
|
|
256
|
+
- The module retains terminal outcomes for bounded repeat reads. It may discard an outcome after a
|
|
257
|
+
client has read it, but it does not discard an unread outcome to admit a new request. A later read
|
|
258
|
+
of discarded state returns `APPROVAL_UNKNOWN`.
|
|
259
|
+
- `approval.cancel { approvalId }` returns `cancelled`, `executing`, `resolved`, or `unknown`. Only
|
|
260
|
+
`cancelled` proves that the command will not run. Use `approval.await` after `resolved` to read the
|
|
261
|
+
decision.
|
|
262
|
+
- `APPROVAL_QUEUE_FULL` means the module refused admission before display or execution. Its `reason`
|
|
263
|
+
is `pending-count`, `pending-bytes`, or `retained-count`. The request is safe to retry after earlier
|
|
264
|
+
approvals clear.
|
|
265
|
+
- `APPROVAL_UNKNOWN` means the module no longer holds that approval. Reloading the GM client, ending
|
|
266
|
+
its bridge session, or expiry can remove the state. The command may not have started, or it may have
|
|
267
|
+
completed. Read world state before another write.
|
|
268
|
+
- A dry run bypasses approval and reports `approvalRequired: true` when the real command would wait.
|
|
269
|
+
The policy still refuses denied commands during a dry run.
|
|
270
|
+
- `policy.snapshot` reports `{ approve: [names], deny: [names] }`. The result is advisory because the
|
|
271
|
+
policy can change before dispatch.
|
|
272
|
+
|
|
273
|
+
The module supplies each opaque `approvalId`; callers do not construct one. Approval request schemas
|
|
274
|
+
are closed. `approval.await`, `approval.cancel`, and `policy.snapshot` do not appear in `commands`,
|
|
275
|
+
`system.info` command inventory, or bridge status. `schema <command>` still returns their schemas.
|
|
276
|
+
The bridge handshake advertises them because the daemon must forward them.
|
|
277
|
+
|
|
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
|
+
|
|
213
297
|
## Delivery states and retries
|
|
214
298
|
|
|
215
299
|
Retry safety is a function of whether the request reached Foundry:
|
|
@@ -219,8 +303,13 @@ Retry safety is a function of whether the request reached Foundry:
|
|
|
219
303
|
| Client could not connect | No | Safe to retry after restoring the daemon |
|
|
220
304
|
| `BRIDGE_NOT_READY` | No | Safe to retry after a bridge connects |
|
|
221
305
|
| Response timeout after send | Possibly | May have committed; inspect state or reuse the same idempotency key |
|
|
222
|
-
| `BRIDGE_TIMEOUT` | Yes | May have committed; inspect state or reuse the same idempotency key |
|
|
223
|
-
| `BRIDGE_DISCONNECTED` | Yes or in flight | May have committed; inspect state |
|
|
306
|
+
| `BRIDGE_TIMEOUT` | Yes | May have committed; inspect state or reuse the same idempotency key while that bridge session lasts |
|
|
307
|
+
| `BRIDGE_DISCONNECTED` | Yes or in flight | May have committed; inspect state, then re-request under a fresh idempotency key |
|
|
308
|
+
| `COMMAND_DENIED` | Refused before dispatch | Not executed; the command is unavailable on that GM client |
|
|
309
|
+
| `APPROVAL_DENIED`, `APPROVAL_TIMEOUT`, `APPROVAL_CANCELLED` | Reached Foundry, never dispatched | Not executed; the same request is safe to send again |
|
|
310
|
+
| `APPROVAL_QUEUE_FULL` | Refused before admission | Not executed; safe to retry when the waiting decisions clear |
|
|
311
|
+
| `IDEMPOTENCY_STORE_FULL` | No | Not executed; safe to retry when earlier keys settle or expire |
|
|
312
|
+
| `APPROVAL_UNKNOWN` | Unknown | May have committed; inspect state, then re-request under a fresh idempotency key |
|
|
224
313
|
| Structured command rejection | Resolved with an error | Correct according to the code |
|
|
225
314
|
|
|
226
315
|
The distinction between connection-phase and response-wait failures is carried in structured error
|
|
@@ -229,8 +318,10 @@ protocol and CLI constants; runtime flags can override the client and daemon req
|
|
|
229
318
|
|
|
230
319
|
## Compatibility rules
|
|
231
320
|
|
|
232
|
-
-
|
|
233
|
-
|
|
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.
|
|
234
325
|
- A bridge advertises the command set it can execute; the daemon forwards only advertised commands.
|
|
235
326
|
- Unsupported version-dependent behavior produces a predictable error rather than a false success.
|
|
236
327
|
|
package/docs/security.md
CHANGED
|
@@ -33,15 +33,16 @@ public multi-tenant API.
|
|
|
33
33
|
storage is shared by every tab of one browser, so a first-use identifier can be generated twice when
|
|
34
34
|
two tabs start pairing at once; the browser discards an approved credential whose identifier no longer
|
|
35
35
|
matches the stored one instead of keeping a pairing the hello gate would refuse.
|
|
36
|
-
- The browser label attached to a pairing is constrained by the protocol schema
|
|
37
|
-
with whitespace-only values rejected along with control, zero-width,
|
|
38
|
-
Unicode tag characters
|
|
39
|
-
whitespace-only label removes the simplest way to send a label that reads
|
|
40
|
-
guarantee that a label renders visibly, because blank-rendering filler
|
|
36
|
+
- The browser label attached to a pairing is constrained by the protocol schema to 1 to 64
|
|
37
|
+
characters, with whitespace-only values rejected along with control, zero-width,
|
|
38
|
+
bidirectional-override, and Unicode tag characters; a violating request is refused rather than
|
|
39
|
+
repaired. Rejecting a whitespace-only label removes the simplest way to send a label that reads
|
|
40
|
+
as nothing; it is not a guarantee that a label renders visibly, because blank-rendering filler
|
|
41
|
+
characters pass the pattern.
|
|
41
42
|
The client identifier printed beside the label is what reliably distinguishes two browsers, including
|
|
42
43
|
two that share one label. The pairing socket is unauthenticated, so any local process can send a
|
|
43
44
|
pairing request, and an unfiltered label would reach a terminal as an escape-sequence injection or as
|
|
44
|
-
hidden text in agent output
|
|
45
|
+
hidden text in agent output. Both approval prompts, the one `auth approve` shows and the one the
|
|
45
46
|
`auth` pairing wait shows, print it as raw text, and JSON output escapes C0 control characters but
|
|
46
47
|
not C1, zero-width, bidirectional-override, or tag ones. The constraint covers the label field only;
|
|
47
48
|
the identity fields printed beside it are described under Known risks.
|
|
@@ -52,7 +53,9 @@ public multi-tenant API.
|
|
|
52
53
|
any other local process is still refused rather than repaired.
|
|
53
54
|
- Browser pairing and bridge sockets require a syntactically valid HTTP(S) Origin, matched exactly
|
|
54
55
|
after pairing. Credentials never appear in URLs, command lines, environment variables, or output.
|
|
55
|
-
- The bridge starts only for a configured GM client.
|
|
56
|
+
- The bridge starts only for a configured GM client. The module hides its settings and category from
|
|
57
|
+
users below the Assistant GM role, and only gamemasters can open its windows. The settings remain
|
|
58
|
+
registered in that browser profile for a later GM session.
|
|
56
59
|
- Another pairing cannot displace the active bridge. A same-pairing reconnect may take over after a
|
|
57
60
|
reload; requests already forwarded to the displaced socket fail immediately as indeterminate.
|
|
58
61
|
Abnormal close retains a 30-second lease against other pairings, while intentional goodbye,
|
|
@@ -79,7 +82,7 @@ runtime address does not broaden that authority or invalidate an explicitly conf
|
|
|
79
82
|
|
|
80
83
|
The bridge accepts only explicit commands registered in the protocol and advertised during the
|
|
81
84
|
handshake. Each command has a schema and a known handler. There is no generic method invocation or
|
|
82
|
-
universal
|
|
85
|
+
universal "edit anything" endpoint.
|
|
83
86
|
|
|
84
87
|
Mutations use Foundry Document APIs or a reviewed typed Foundry action. Raw live world databases are
|
|
85
88
|
outside the command surface.
|
|
@@ -106,6 +109,43 @@ Foundry hooks can veto or partially apply writes. Mutation handlers confirm obse
|
|
|
106
109
|
where the command contract requires it and return structured partial or failed outcomes instead of
|
|
107
110
|
claiming success.
|
|
108
111
|
|
|
112
|
+
Every command also carries an allow, approve, or deny permission in the GM client holding the
|
|
113
|
+
bridge. The Foundry module enforces it after authentication, GM authority, and write-permission
|
|
114
|
+
checks but before dispatch. It covers reads, writes, bulk envelopes, and previews. The default
|
|
115
|
+
policy denies commands that can execute code, change who can do what, or persist outside the
|
|
116
|
+
world's own data; requires approval for destructive commands plus `system.reload` and
|
|
117
|
+
`user.create`, where the destructive set is the `delete` and `delete-many` verbs, `file.move`,
|
|
118
|
+
`scene.fog.reset`, and `chat.flush`; and allows the other policy-controlled commands. A
|
|
119
|
+
denied-by-default command is a deliberate opt-in: it exists so a GM can enable it knowingly, not
|
|
120
|
+
so it can run out of the box.
|
|
121
|
+
|
|
122
|
+
`system.ping`, `system.info`, and the internal approval-wait commands always run. They let the bridge
|
|
123
|
+
report its state and finish decisions already taken without changing world content. Stored policy
|
|
124
|
+
overrides cannot change them, and the permissions window omits them. Pairing and other `auth`
|
|
125
|
+
operations run in the daemon, so the command policy does not apply.
|
|
126
|
+
|
|
127
|
+
A denied command is absent from the CLI's `commands` listing while the bridge is reachable. This
|
|
128
|
+
reduces irrelevant choices for automated callers but does not enforce the policy. The Foundry module
|
|
129
|
+
enforces it at dispatch and returns `COMMAND_DENIED` if a caller sends the command anyway.
|
|
130
|
+
|
|
131
|
+
The original request does not execute a command that requires approval. The module holds it in
|
|
132
|
+
memory until the GM decides. After Allow, the module repeats readiness, GM authority, parameter,
|
|
133
|
+
write-permission, policy, and family checks. A command changed to deny while waiting returns
|
|
134
|
+
`COMMAND_DENIED`. Changing it between allow and approve does not create a second approval. A denied,
|
|
135
|
+
expired, or confirmed-cancelled request never runs.
|
|
136
|
+
|
|
137
|
+
The module returns a random 128-bit `approvalId` only to the original caller. Reading or cancelling
|
|
138
|
+
the decision requires that identifier.
|
|
139
|
+
|
|
140
|
+
The GM-only Command Approval window shows the command, remaining time, target documents or managed
|
|
141
|
+
paths, and parameters. It reports binary upload content by size instead of rendering the payload,
|
|
142
|
+
and any text longer than 16,384 characters, including a macro body, by its character count for the
|
|
143
|
+
same reason. A macro long enough to cross that line is therefore approved on its name and origin
|
|
144
|
+
rather than on its code, and `macro.create` runs by itself under the shipped defaults, so a GM who
|
|
145
|
+
wants every body reviewable moves `macro.create` and `macro.update` to approve as well. The command
|
|
146
|
+
envelope has no caller identity, so the window cannot name the requester. The GM approves the
|
|
147
|
+
displayed invocation, not a person or process.
|
|
148
|
+
|
|
109
149
|
## Document ownership
|
|
110
150
|
|
|
111
151
|
Ownership is access policy rather than ordinary document content. Raw `ownership` is excluded from
|
|
@@ -119,13 +159,36 @@ family's behavior.
|
|
|
119
159
|
|
|
120
160
|
## Executable content
|
|
121
161
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
162
|
+
The bridge executes no JavaScript that the GM has not explicitly enabled and cannot see before it
|
|
163
|
+
runs. There is no `eval`-style command, and CLI-supplied data cannot create a hidden execution path
|
|
164
|
+
through ordinary writes.
|
|
165
|
+
|
|
166
|
+
- `macro.execute` is the one way to run code, and it runs only stored world macros. It is denied by
|
|
167
|
+
default; a GM who enables it can keep it on approve, where the Command Approval window shows the
|
|
168
|
+
macro's type and its command body, up to the length cap described above, before anything runs.
|
|
169
|
+
The `macro.create → macro.execute → macro.delete` chain is the sanctioned path for ad-hoc code,
|
|
170
|
+
so a GM who wants only vetted macros to run sets `macro.create` and `macro.update` to approve or
|
|
171
|
+
deny while `macro.execute` stays enabled. A script macro that throws fails the command with a
|
|
172
|
+
structured error naming what the macro raised, and the failure is partial by nature: whatever
|
|
173
|
+
the macro changed before it threw stays changed. A macro that catches its own errors still
|
|
174
|
+
reports a `null` return, so results do not prove success and effects deserve a read-back.
|
|
175
|
+
- Macro bodies and chat content written by ordinary document commands are stored, not executed.
|
|
126
176
|
- Action commands invoke only their fixed typed Foundry methods.
|
|
127
|
-
-
|
|
128
|
-
|
|
177
|
+
- Ordinary region-behavior writes reject core script- or macro-executing types through a shared
|
|
178
|
+
guard, including on nested behaviors supplied with a region write. The dedicated
|
|
179
|
+
`scene.region.behavior.executable` commands, denied by default, accept exactly the `executeMacro`
|
|
180
|
+
type and require the referenced world macro to exist; the approval window names the macro, the
|
|
181
|
+
trigger events, and whether the behavior fires for everyone. Those commands accept a behavior's
|
|
182
|
+
`system` only in a spelling the guard and that window can both read: a create takes only the plain
|
|
183
|
+
`system` object, an update or clone also accepts single-field `system.<field>` paths, and no route
|
|
184
|
+
accepts both spellings at once, a path deeper than one field (`system.events.<n>`), a
|
|
185
|
+
forced-replacement or forced-deletion operator key, or a read field carrying the wrong type (a
|
|
186
|
+
non-array `events`, a non-boolean `everyone`). Each of those could store a macro reference or a
|
|
187
|
+
trigger set that differs from the one the macro check validated and the approval window showed. An
|
|
188
|
+
`executeMacro` behavior triggers later on player-driven region events, with `everyone: true`
|
|
189
|
+
running the macro on every connected client.
|
|
190
|
+
- `executeScript` behaviors are not supported on any route: Foundry runs their source in every
|
|
191
|
+
connected player's browser with no per-user execution check, which no popup can make reviewable.
|
|
129
192
|
|
|
130
193
|
Installed systems and modules remain trusted Foundry code. They can register behavior types, hooks,
|
|
131
194
|
ActiveEffect interpretations, or other data-driven features whose effects the bridge cannot classify
|
|
@@ -138,11 +201,49 @@ or chat output after the direct command.
|
|
|
138
201
|
|
|
139
202
|
## Settings
|
|
140
203
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
204
|
+
Listing returns registration metadata, while reading a value requires an explicit namespace and key,
|
|
205
|
+
singly or in a batch. Values are serialized with bounded depth, node count, and byte size, and this
|
|
206
|
+
module's own secret-bearing settings are redacted from every read.
|
|
207
|
+
|
|
208
|
+
`setting.set` and `setting.set-many` exist because module configuration is a legitimate
|
|
209
|
+
administration task, and they are denied by default because settings can alter global security and
|
|
210
|
+
runtime behavior and frequently invoke module callbacks: a write to `core.permissions` or
|
|
211
|
+
`core.moduleConfiguration` changes what the GM client itself can do or load. Writes go through
|
|
212
|
+
Foundry's own registration and DataField validation, and only registered settings are writable. The
|
|
213
|
+
approval behavior shows the stored value next to the proposed one before a write runs.
|
|
214
|
+
|
|
215
|
+
The command policy stays beyond the CLI's reach by construction: the write commands refuse this
|
|
216
|
+
module's namespace with a structured error in every mode, real, dry-run, and per bulk element
|
|
217
|
+
alike, so no policy setting, credential, or approval timeout can be changed from the surface the
|
|
218
|
+
policy governs. The client-scoped `commandPolicy` and `approvalTimeoutMinutes` settings remain readable
|
|
219
|
+
because they contain no secrets, and only a GM using Foundry changes them: the Command permissions
|
|
220
|
+
window writes the policy, and the main Module Settings form writes the timeout and enforces its
|
|
221
|
+
bounds. Another browser profile or machine has its own values.
|
|
222
|
+
|
|
223
|
+
## Users
|
|
224
|
+
|
|
225
|
+
User accounts are managed through explicit per-purpose commands rather than one open patch surface.
|
|
226
|
+
|
|
227
|
+
- No command reads or writes `password` or `passwordSalt`. Foundry transmits a set password in
|
|
228
|
+
clear text and hashes it server-side, so a password path through the bridge would expose secrets
|
|
229
|
+
in transcripts and logs; password changes stay in Foundry's own UI.
|
|
230
|
+
- `user.update` edits profile fields and is allowed by default because none of them grant
|
|
231
|
+
authority; the fields are name, color, pronouns, avatar, assigned character, and flags.
|
|
232
|
+
- `user.create` and `user.delete` ask for approval by default. `user.role.set` and
|
|
233
|
+
`user.permissions.set` are denied by default because they change who can do what.
|
|
234
|
+
- A `user.create` carries whatever role it asks for, up to the caller's own, so an approved one can
|
|
235
|
+
mint a gamemaster. That is why it asks for approval rather than running by itself: the approval
|
|
236
|
+
window names the account and the role, including the player role Foundry gives when the command
|
|
237
|
+
asks for none, and the GM reading it is the review point. A GM who does not want that decision
|
|
238
|
+
in the loop at all sets `user.create` to deny.
|
|
239
|
+
- The bridge GM's own account is self-protected: `user.role.set`, `user.permissions.set`, and
|
|
240
|
+
`user.delete` aimed at the user holding the bridge are refused with a structured error, so
|
|
241
|
+
automation cannot demote, remove, or strip the restricted menus from the account it runs through.
|
|
242
|
+
A second GM account carries no such guard; deciding about it is exactly what enabling the command
|
|
243
|
+
means.
|
|
244
|
+
- Foundry's server-side limits stay in force underneath: a role cannot be raised above the caller's
|
|
245
|
+
own, and the last gamemaster account can be neither demoted nor deleted. Those refusals surface
|
|
246
|
+
as permission errors with Foundry's own message.
|
|
146
247
|
|
|
147
248
|
## Search
|
|
148
249
|
|
|
@@ -165,8 +266,11 @@ Reads address normalized managed-data paths. Writes are restricted to the active
|
|
|
165
266
|
- `packs/` and all descendants.
|
|
166
267
|
|
|
167
268
|
Containment and exclusions are segment-aware and are checked before payload decoding or capability
|
|
168
|
-
dispatch.
|
|
169
|
-
|
|
269
|
+
dispatch. Path normalization rejects a segment whose literal or percent-decoded form is `.` or `..`,
|
|
270
|
+
and a segment whose decoded form contains a path separator (so a percent-encoded `/` or `\` cannot
|
|
271
|
+
smuggle traversal inside one segment). Absolute host paths, traversal, sibling-prefix tricks, and
|
|
272
|
+
percent-encoded traversal (`%2e%2e`, `..%2f..`) therefore cannot cross the boundary; this applies to
|
|
273
|
+
every managed-path caller, including `image show`.
|
|
170
274
|
|
|
171
275
|
The module accepts upload content over the authenticated local transport and a managed data-relative
|
|
172
276
|
destination. It does not read arbitrary files from the operator machine. The CLI may read an explicit
|
|
@@ -190,6 +294,9 @@ The daemon and module bound uploads, WebSocket frames, search work, batch sizes,
|
|
|
190
294
|
shapes. Oversized operations return structured errors where possible without dropping the shared
|
|
191
295
|
bridge session.
|
|
192
296
|
|
|
297
|
+
The module limits the number and combined size of approval requests. It refuses an excess request
|
|
298
|
+
before display or execution. It does not discard an unread outcome to admit a new request.
|
|
299
|
+
|
|
193
300
|
The system remains susceptible to ordinary local denial of service by an authorized caller issuing
|
|
194
301
|
many expensive Foundry operations. It is designed for cooperative local automation, not hostile
|
|
195
302
|
multi-user scheduling.
|
|
@@ -204,7 +311,7 @@ multi-user scheduling.
|
|
|
204
311
|
- The label and the client identifier are the only pairing identity fields constrained against terminal
|
|
205
312
|
escape sequences. World title and GM name arrive on the same unauthenticated pairing request without
|
|
206
313
|
that constraint, and the CLI prints them as raw text wherever it renders a pending request or a
|
|
207
|
-
stored profile
|
|
314
|
+
stored profile: both approval prompts and the `auth prune` candidate listing, which precedes an
|
|
208
315
|
irreversible deletion. A local process can therefore redraw the identity lines an operator reads
|
|
209
316
|
before answering, and a title that survived one approval is re-rendered from the stored record every
|
|
210
317
|
time a later command lists it.
|
|
@@ -219,7 +326,28 @@ multi-user scheduling.
|
|
|
219
326
|
- Native Foundry batch operations are not transactional and can partially apply.
|
|
220
327
|
- Search and read commands can expose GM-visible world content to the local caller.
|
|
221
328
|
- Large but permitted content can consume browser memory and processing time.
|
|
329
|
+
- An approved command executes when the GM decides, so the world may have changed while it waited.
|
|
330
|
+
Approval does not lock world state.
|
|
331
|
+
- Decisions waiting for a GM live only in that browser session. Reloading the GM client or ending
|
|
332
|
+
its bridge session discards them. Waiting callers receive an indeterminate result.
|
|
333
|
+
- A caller that disappears without a confirmed cancellation leaves its request actionable on the
|
|
334
|
+
GM's screen until the GM decides or the timeout expires. The command envelope carries no client
|
|
335
|
+
identity, so the module cannot tell that the caller is gone.
|
|
336
|
+
- The daemon keeps reservations, approval links, and lost-in-flight idempotency keys in one bounded
|
|
337
|
+
store shared by its clients. It returns `IDEMPOTENCY_STORE_FULL` instead of evicting an
|
|
338
|
+
indeterminate key. Capacity returns when earlier keys settle or expire, or when a daemon restart,
|
|
339
|
+
world switch, or pairing switch clears the store.
|
|
340
|
+
- Command permissions belong to a browser profile. A second paired browser, or the same browser with
|
|
341
|
+
a fresh profile, holds its own permissions, and whichever client holds the bridge is the one whose
|
|
342
|
+
permissions apply.
|
|
222
343
|
- Declarative content can reference existing executable or module-interpreted content.
|
|
344
|
+
- `image show` accepts an arbitrary off-host `http(s)` URL as its source and pushes it to the
|
|
345
|
+
targeted players' clients unchanged, so an enabled caller can make those browsers fetch and display
|
|
346
|
+
a remote image the operator did not host. Only local sources are confined to the managed data
|
|
347
|
+
boundary; a URL source is a display of external content, not a file read.
|
|
348
|
+
- An enabled `macro.execute` or executable-behavior command makes the GM's enablement and approval
|
|
349
|
+
discipline the effective code-review boundary; an enabled `setting.set` can change core settings
|
|
350
|
+
that alter the GM client's own capabilities or take it down until a reload.
|
|
223
351
|
|
|
224
352
|
## Operator guidance
|
|
225
353
|
|
|
@@ -228,6 +356,11 @@ multi-user scheduling.
|
|
|
228
356
|
- Use JSON output, dry runs, stable idempotency keys, and post-write reads for automation.
|
|
229
357
|
- Review every outcome of a bulk operation.
|
|
230
358
|
- Treat forwarded timeouts and disconnects as potentially committed.
|
|
359
|
+
- Set the approval timeout to the time a GM realistically needs to answer. A long timeout keeps a
|
|
360
|
+
request actionable long after its caller gave up; a short one refuses work the GM would have
|
|
361
|
+
approved. Either way the expiry never executes the command.
|
|
362
|
+
- Configure command permissions in every browser profile that holds the bridge, and review them
|
|
363
|
+
after an update that adds commands.
|
|
231
364
|
- Review installed systems and modules before authoring automation-sensitive data.
|
|
232
365
|
- Back up important worlds before large migrations.
|
|
233
366
|
- Run the live smoke workflow only in a designated test world.
|
package/docs/skill.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent skill
|
|
2
2
|
|
|
3
|
-
fvtt-world-cli ships with `foundry-world-editor`, an installable Agent Skill
|
|
3
|
+
fvtt-world-cli ships with `foundry-world-editor`, an installable Agent Skill, a short operating
|
|
4
4
|
manual that teaches an AI agent to drive this CLI safely: how to bring the stack up and check its
|
|
5
5
|
health, the read → preview → commit → verify loop, how to classify failures before retrying, and
|
|
6
6
|
where the hard safety boundaries are. Skills follow the open Agent Skills standard, so the same
|
|
@@ -18,8 +18,8 @@ fvtt-world-cli skill install
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
The default installation delegates to the ecosystem's skills CLI (`npx skills add`), which detects
|
|
21
|
-
the agents present on the machine. It keeps one canonical copy under `~/.agents/skills
|
|
22
|
-
vendor-neutral location of the Agent Skills standard
|
|
21
|
+
the agents present on the machine. It keeps one canonical copy under `~/.agents/skills`, the
|
|
22
|
+
vendor-neutral location of the Agent Skills standard, and points each agent's own skill directory
|
|
23
23
|
at it, so every agent reads the same single copy.
|
|
24
24
|
|
|
25
25
|
An explicit destination works without the skills CLI or network access:
|
|
@@ -32,16 +32,20 @@ fvtt-world-cli skill install --to <skills-directory>
|
|
|
32
32
|
copy participates in updates later. `--link` symlinks instead of copying, which keeps the installed
|
|
33
33
|
skill permanently identical to the CLI it came from.
|
|
34
34
|
|
|
35
|
+
A bridge daemon started while no copy is installed anywhere prints a reminder with the
|
|
36
|
+
installation command, so a missing skill does not go unnoticed.
|
|
37
|
+
|
|
35
38
|
## Staying up to date
|
|
36
39
|
|
|
37
40
|
The skill is versioned together with the CLI, and the CLI keeps installed copies current on its
|
|
38
|
-
own:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
own: the bridge daemon checks every known copy at startup and refreshes outdated ones, so a package
|
|
42
|
+
update takes effect the next time the daemon starts. The check runs inside the CLI itself; nothing
|
|
43
|
+
about installation or updates relies on npm lifecycle scripts, which npm blocks by default. For
|
|
44
|
+
updates, a copy is one of two kinds:
|
|
41
45
|
|
|
42
|
-
- An **unmodified** copy
|
|
46
|
+
- An **unmodified** copy, exactly what some version of the CLI shipped, is replaced silently with
|
|
43
47
|
the current version.
|
|
44
|
-
- A **modified** copy
|
|
48
|
+
- A **modified** copy, one with local edits, is never replaced automatically. It produces a
|
|
45
49
|
warning instead, and keeps producing it until the difference is resolved.
|
|
46
50
|
|
|
47
51
|
The explicit update command follows the same rule and can override it:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fvtt-world-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Safe local CLI and authenticated bridge for automating live Foundry VTT worlds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "DroiD16",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"type": "module",
|
|
22
22
|
"bin": {
|
|
23
|
-
"fvtt-world-cli": "
|
|
24
|
-
"worldctl": "
|
|
23
|
+
"fvtt-world-cli": "bin/fvtt-world-cli.js",
|
|
24
|
+
"worldctl": "bin/fvtt-world-cli.js"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"bin/",
|
|
@@ -34,8 +34,7 @@
|
|
|
34
34
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --external:commander --external:pino --external:tsx --external:ws --external:zod --outfile=dist/index.js",
|
|
35
35
|
"typecheck": "tsc --project ../../tsconfig.json",
|
|
36
36
|
"prepack": "npm run build && node ./bin/prepare-package-assets.mjs",
|
|
37
|
-
"postpack": "node ./bin/prepare-package-assets.mjs --clean"
|
|
38
|
-
"postinstall": "node ./bin/sync-installed-skill.mjs"
|
|
37
|
+
"postpack": "node ./bin/prepare-package-assets.mjs --clean"
|
|
39
38
|
},
|
|
40
39
|
"dependencies": {
|
|
41
40
|
"commander": "^13.1.0",
|
|
@@ -45,6 +44,6 @@
|
|
|
45
44
|
"zod": "^3.24.3"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
|
-
"@fvtt-world-cli/protocol": "1.
|
|
47
|
+
"@fvtt-world-cli/protocol": "1.1.1"
|
|
49
48
|
}
|
|
50
49
|
}
|