@kontextmind/kxm 0.7.96 → 0.7.97
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +22 -0
- package/docs/concepts/trust-model.md +3 -2
- package/docs/glossary.md +1 -1
- package/docs/guides/peer-messaging.md +2 -2
- package/docs/guides/pi-workers.md +1 -1
- package/docs/guides/webhook-workflows.md +53 -18
- package/docs/operations/deploy.md +1 -1
- package/docs/operations/troubleshooting.md +3 -2
- package/docs/reference/cli-reference.md +6 -6
- package/docs/reference/configuration.md +2 -2
- package/docs/reference/http-api.md +6 -6
- package/docs/reference/tools.md +1 -1
- package/docs/reference/workflow-definitions.md +2 -2
- package/docs/start/quickstart-pi.md +1 -1
- package/examples/workflow-signal.ts +4 -5
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/claude-hook.js +11 -1
- package/plugins/kxm/dist/cli.js +159 -74
- package/plugins/kxm/dist/client.js +3 -1
- package/plugins/kxm/dist/core.js +11 -1
- package/plugins/kxm/dist/extension.js +45 -13
- package/plugins/kxm/dist/mcp-server.js +20 -4
- package/plugins/kxm/dist/runtime-supervisor.js +1 -3
- package/plugins/kxm/dist/runtime.js +17 -3
- package/plugins/kxm/dist/server.js +115 -20
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/workflows.ts +12 -7
- package/plugins/kxm/src/cli.ts +19 -5
- package/plugins/kxm/src/client.ts +4 -0
- package/plugins/kxm/src/commands.ts +23 -1
- package/plugins/kxm/src/extension.ts +20 -14
- package/plugins/kxm/src/github-watch.ts +8 -5
- package/plugins/kxm/src/hub-env.ts +19 -1
- package/plugins/kxm/src/hub.ts +105 -21
- package/plugins/kxm/src/improve-sources.ts +2 -7
- package/plugins/kxm/src/mcp-server.ts +9 -2
- package/plugins/kxm/src/runtime-store.ts +23 -0
- package/plugins/kxm/src/workflow.ts +70 -1
- package/scripts/smoke-multi-pi.mjs +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -307,6 +307,28 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
307
307
|
|
|
308
308
|
### Fixed
|
|
309
309
|
|
|
310
|
+
- **Signed webhooks cannot be replayed.** KXM's own webhook senders now sign the
|
|
311
|
+
timestamp, delivery ID, definition, run and signal key along with the body
|
|
312
|
+
(`x-kxm-signature`, `x-kxm-timestamp`, `x-kxm-delivery-id`; see
|
|
313
|
+
`docs/guides/webhook-workflows.md#kxm-sender-contract`), and the hub refuses a signature
|
|
314
|
+
more than 300 seconds old. Every `generic` workflow start and every signal callback
|
|
315
|
+
must use this contract; a body-only `X-Hub-Signature-256` there is refused. Jira and
|
|
316
|
+
GitHub deliveries keep their provider signature, and a signed body starts at most one
|
|
317
|
+
run (`webhook_payload_replayed`). A reused delivery ID with a different body is
|
|
318
|
+
refused with 409 `webhook_delivery_conflict`, and a duplicate start returns only
|
|
319
|
+
`duplicate`, `runId` and `status`. Update any custom sender to the new contract.
|
|
320
|
+
- **Agents never borrow the hub admin token.** With `KXM_AUTH_TOKEN` unset, the Pi
|
|
321
|
+
extension and the `kxm peer` / `kxm workflow` agent commands used the persisted admin
|
|
322
|
+
token. They now use only this project's saved project token, as the Claude MCP server
|
|
323
|
+
does, and otherwise stop with a message naming the fix (`project_token_missing`, exit 2,
|
|
324
|
+
on the CLI).
|
|
325
|
+
- **The hop limit bounds agent forwarding chains.** `kxm_send` and `kxm_fanout` from Pi
|
|
326
|
+
or the Claude MCP server send one hop past the inbound request being handled, so a
|
|
327
|
+
chain of agents forwarding to each other stops at `hop_limit_reached`.
|
|
328
|
+
- **Workflow prompts no longer point agents at `.kxm/config`**, a path KXM refuses.
|
|
329
|
+
- **`kxm gate signal` and `kxm workflow wait` inside a KXM project reach the hub for hub
|
|
330
|
+
runs.** They go to the local Runtime only for a run its store holds.
|
|
331
|
+
|
|
310
332
|
- **The Claude plugin's SessionStart hook is one bundled, read-only, project-scoped
|
|
311
333
|
script.** The two shell hooks it replaces (`kxm session brief --status` and
|
|
312
334
|
`kxm memory brief`) exited 127 without `kxm` on `PATH`, ran whichever `kxm` was on
|
|
@@ -55,8 +55,9 @@ flowchart LR
|
|
|
55
55
|
|
|
56
56
|
- **Claude Code plugin.** The MCP server uses the plugin's `auth_token` setting, else the project token the hub saved for this project. It never falls back to the saved admin token; without a project token, its tools report that and do nothing.
|
|
57
57
|
- **SessionStart hook.** It reads local state read-only. It mints no token, writes no file, and never prints a token.
|
|
58
|
-
- **Pi extension.** It uses `KXM_AUTH_TOKEN`, else the token
|
|
59
|
-
-
|
|
58
|
+
- **Pi extension.** It uses `KXM_AUTH_TOKEN`, else the project token the hub saved for this project. It never uses the saved admin token, nor the one a hub it auto-started generated; without a project token it reports the fix and stays offline.
|
|
59
|
+
- **`kxm peer` and `kxm workflow` agent commands.** They act as a peer agent, so they choose a token as the Pi extension does, and exit 2 with `project_token_missing` when none resolves.
|
|
60
|
+
- **Dashboard and Runtime supervisor.** Operator tools use `KXM_AUTH_TOKEN`, else the saved project token, else the saved admin token.
|
|
60
61
|
|
|
61
62
|
## Project isolation
|
|
62
63
|
|
package/docs/glossary.md
CHANGED
|
@@ -184,7 +184,7 @@ The coordinator's recorded result for the active [stage](#stage): `passed`, `war
|
|
|
184
184
|
|
|
185
185
|
### Delivery ID
|
|
186
186
|
|
|
187
|
-
The identifier a webhook sender puts in `
|
|
187
|
+
The identifier a webhook sender puts in `x-kxm-delivery-id`, which the KXM sender contract signs, or for a Jira or GitHub delivery in `X-Atlassian-Webhook-Identifier` or `X-GitHub-Delivery`. The hub requires one and deduplicates on it: a repeated start with the same body returns the existing run ID, and the same ID with a different body is refused.
|
|
188
188
|
|
|
189
189
|
### Journal
|
|
190
190
|
|
|
@@ -246,7 +246,7 @@ Neither field proves where a reply came from. Workflow evidence uses `workflowCo
|
|
|
246
246
|
|
|
247
247
|
When a request expires, both the sender and the recipient receive an `expired` event.
|
|
248
248
|
|
|
249
|
-
The hop limit refuses a request whose `hops` count has reached `maxHops` (`hop_limit_reached`).
|
|
249
|
+
The hop limit refuses a request whose `hops` count has reached `maxHops` (`hop_limit_reached`). In Pi and in Claude Code, `kxm_send` and `kxm_fanout` send one hop past the inbound request the session is handling, under that request's `maxHops`, so a chain of agents forwarding work to each other stops at the limit. The Claude Code MCP server counts from the furthest request in its open inbox. `kxm peer send` from the CLI handles no inbound request, so it always starts a new chain at hop 0.
|
|
250
250
|
|
|
251
251
|
## Queue work for an offline agent
|
|
252
252
|
|
|
@@ -272,7 +272,7 @@ Tools and the CLI report the message text; the HTTP response also carries the co
|
|
|
272
272
|
| `online target not found: <name>` | `target_not_found` | The target is offline or unknown. Check `kxm_list`; use `allowOffline` for a registered agent. |
|
|
273
273
|
| `cannot send a request to yourself` | `self_target` | Send to a different agent. |
|
|
274
274
|
| `idempotency key was already used for another request` | `idempotency_conflict` | A different request reused the key. Repeat the original exactly, or use a new key for new work. |
|
|
275
|
-
| `hop limit reached (<hops>/<max>)
|
|
275
|
+
| `hop limit reached (<hops>/<max>): …` | `hop_limit_reached` | Forwarding would extend the chain past `maxHops`. Answer the inbound request directly instead of forwarding it. |
|
|
276
276
|
| `ttlMs must be an integer between 1000 and 604800000` | `protocol_error` | Use a TTL from 1 second to 7 days. |
|
|
277
277
|
| `message is not visible to this agent` | `message_forbidden` | Run as the sender or recipient. |
|
|
278
278
|
| `message already has a reply` | `duplicate_reply` | The request is already answered. |
|
|
@@ -60,7 +60,7 @@ would start worker
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
> [!IMPORTANT]
|
|
63
|
-
> Set `KXM_AUTH_TOKEN` to the project token in the worker's environment. When it is unset, the Pi extension
|
|
63
|
+
> Set `KXM_AUTH_TOKEN` to the project token in the worker's environment. When it is unset, the Pi extension uses only the project token saved for this project on this machine. It never falls back to the saved admin token, so without a project token the worker's Pi session stays offline and says so.
|
|
64
64
|
|
|
65
65
|
| Flag | Environment variable | Effect |
|
|
66
66
|
|---|---|---|
|
|
@@ -47,7 +47,7 @@ sequenceDiagram
|
|
|
47
47
|
Hub-->>Coord: completed = true
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
The hub verifies the signature
|
|
50
|
+
The hub verifies the delivery's signature, refuses replays, deduplicates retries by delivery ID, and records the run before it answers. It keeps a SHA-256 hash of the payload and the rendered prompt, not the raw body, so keep prompt templates narrow.
|
|
51
51
|
|
|
52
52
|
## Copy the example definition
|
|
53
53
|
|
|
@@ -102,7 +102,7 @@ A file holds a JSON array of definitions. The hub checks every limit below at st
|
|
|
102
102
|
| Field | Meaning |
|
|
103
103
|
|---|---|
|
|
104
104
|
| `id` | Up to 64 characters; the last segment of the webhook URL. |
|
|
105
|
-
| `source` | `jira`, `github`, or `generic` (the default). A label on the run. |
|
|
105
|
+
| `source` | `jira`, `github`, or `generic` (the default). A label on the run, and it decides which signatures the hub accepts: see [Signatures and delivery IDs](#signatures-and-delivery-ids). |
|
|
106
106
|
| `project`, `target` | Hub project, and the coordinator's agent name or ID. |
|
|
107
107
|
| `secretEnv` | Variable holding the start secret. `secret` takes a literal instead; prefer the variable. |
|
|
108
108
|
| `signalSecretEnv` | Variable holding the callback secret. Without it, callbacks use the start secret. |
|
|
@@ -180,7 +180,7 @@ kxm agent worker --name coordinator --project product --model <pi-model> \
|
|
|
180
180
|
|
|
181
181
|
## Send a test delivery
|
|
182
182
|
|
|
183
|
-
`kxm workflow start` signs a payload and posts it to the hub at `KXM_SERVER_URL`, as a provider would. It signs with `KXM_WORKFLOW_SECRET`, or with the definition's own start secret when `KXM_WEBHOOK_WORKFLOWS_FILE` is set in the same shell.
|
|
183
|
+
`kxm workflow start` signs a payload under the [KXM sender contract](#kxm-sender-contract) and posts it to the hub at `KXM_SERVER_URL`, as a provider would. It signs with `KXM_WORKFLOW_SECRET`, or with the definition's own start secret when `KXM_WEBHOOK_WORKFLOWS_FILE` is set in the same shell.
|
|
184
184
|
|
|
185
185
|
```bash
|
|
186
186
|
export KXM_SERVER_URL=http://127.0.0.1:7331
|
|
@@ -195,7 +195,7 @@ Expected output:
|
|
|
195
195
|
started workflow run_7c187d2a0fde408ea408f0d8c53201c8
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
Repeating the command with the same delivery ID returns the same run
|
|
198
|
+
Repeating the command with the same delivery ID and payload returns the same run ID; the same delivery ID with a different payload is refused with `409`. Inspect runs from the hub's workspace on the hub host; these commands read its local SQLite store:
|
|
199
199
|
|
|
200
200
|
```bash
|
|
201
201
|
kxm workflow list
|
|
@@ -204,26 +204,60 @@ kxm workflow get run_7c187d2a0fde408ea408f0d8c53201c8 --json
|
|
|
204
204
|
|
|
205
205
|
## Connect Jira
|
|
206
206
|
|
|
207
|
-
In Jira, create a webhook for the `jira:issue_updated` event that posts to `https://<kxm-host>/v1/webhooks/jira-development`, and set the same start secret.
|
|
207
|
+
In Jira, create a webhook for the `jira:issue_updated` event that posts to `https://<kxm-host>/v1/webhooks/jira-development`, and set the same start secret.
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
### Signatures and delivery IDs
|
|
210
|
+
|
|
211
|
+
The hub accepts two ways to sign a start:
|
|
212
|
+
|
|
213
|
+
| Headers | Sent by | Accepted by |
|
|
210
214
|
|---|---|---|
|
|
211
|
-
| `
|
|
212
|
-
| `X-Atlassian-Webhook-Identifier` | Jira |
|
|
213
|
-
| `X-GitHub-Delivery` | GitHub |
|
|
214
|
-
|
|
215
|
+
| `x-kxm-signature`, `x-kxm-timestamp`, `x-kxm-delivery-id` | `kxm` and your own senders | Every definition. See [KXM sender contract](#kxm-sender-contract). |
|
|
216
|
+
| `X-Hub-Signature-256` or `X-Hub-Signature`, with `X-Atlassian-Webhook-Identifier` | Jira | A `jira` definition only. |
|
|
217
|
+
| `X-Hub-Signature-256` with `X-GitHub-Delivery` | GitHub | A `github` definition only. |
|
|
218
|
+
|
|
219
|
+
Jira and GitHub sign only the body, as `sha256=<hex>`; other algorithms are refused. Because the delivery ID is not signed, the body is the delivery's replay identity: a signed body starts at most one run, under one delivery ID.
|
|
215
220
|
|
|
216
221
|
| Response | Meaning |
|
|
217
222
|
|---|---|
|
|
218
223
|
| `202` | Run created and coordinator prompt queued. |
|
|
219
|
-
| `200` with `"duplicate": true` | The delivery ID
|
|
224
|
+
| `200` with `"duplicate": true` | The delivery ID and body were seen before. Only `runId` and `status` come back, never the run. |
|
|
220
225
|
| `204` | The event or filter did not match. Nothing was stored. |
|
|
221
|
-
| `401` | Missing, unsupported, or wrong signature. |
|
|
226
|
+
| `401` | Missing, unsupported, or wrong signature, or a KXM signature outside its 300-second window. |
|
|
222
227
|
| `404` | No definition with that ID. |
|
|
223
|
-
| `409` | The coordinator has never registered. |
|
|
228
|
+
| `409` `workflow_target_unavailable` | The coordinator has never registered. |
|
|
229
|
+
| `409` `webhook_delivery_conflict` | The delivery ID was already used with a different body. |
|
|
230
|
+
| `409` `webhook_payload_replayed` | A Jira or GitHub body already started a run under another delivery ID. |
|
|
224
231
|
|
|
225
232
|
Webhook authentication authorizes only workflow creation. The `report` stage updates Jira through the coordinator's own authorized Jira tool; never put Jira credentials in a definition or prompt.
|
|
226
233
|
|
|
234
|
+
## KXM sender contract
|
|
235
|
+
|
|
236
|
+
A body-only signature cannot tell a retry from a replay, so KXM's own senders sign more: `kxm workflow start`, `kxm gate signal`, `kxm gate github watch`, and [`examples/workflow-signal.ts`](../../examples/workflow-signal.ts). A start of a `generic` definition and every signal must use this contract; a body-only signature there is refused with `401 webhook_signature_missing`.
|
|
237
|
+
|
|
238
|
+
| Header | Value |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `x-kxm-delivery-id` | Stable retry identifier, at most 128 characters |
|
|
241
|
+
| `x-kxm-timestamp` | Unix seconds at send time |
|
|
242
|
+
| `x-kxm-signature` | `sha256=` and the hex HMAC-SHA256 of the signed material, under the start or callback secret |
|
|
243
|
+
|
|
244
|
+
The signed material is seven newline-terminated fields followed by the exact body bytes. No field may contain a line break. `workflowWebhookHeaders` in `plugins/kxm/src/workflow.ts` builds all three headers.
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
kxm-webhook-v1
|
|
248
|
+
start | signal
|
|
249
|
+
<x-kxm-timestamp>
|
|
250
|
+
<x-kxm-delivery-id>
|
|
251
|
+
<definition ID>
|
|
252
|
+
<run ID; empty for a start>
|
|
253
|
+
<signal key; empty for a start>
|
|
254
|
+
<body>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
- A signature over any other timestamp, delivery ID, definition, run, signal key, or body is refused with `401 webhook_signature_invalid`, so a captured request cannot be replayed under a new delivery ID or against another run.
|
|
258
|
+
- An authentic signature whose timestamp is more than 300 seconds from the hub clock is refused with `401 webhook_timestamp_expired`. Sign at send time: a retry re-signs with a fresh timestamp and keeps the same delivery ID and body, which the hub answers as a duplicate.
|
|
259
|
+
- The signed kind keeps a start signature from ever verifying as a signal, even when both use the start secret.
|
|
260
|
+
|
|
227
261
|
## Follow the coordinator procedure
|
|
228
262
|
|
|
229
263
|
For every stage, the coordinator:
|
|
@@ -276,7 +310,7 @@ Tool call (`kxm_workflow_wait`):
|
|
|
276
310
|
|
|
277
311
|
Store the run ID and signal key where the external system can find them, such as pull-request metadata; they are not secrets.
|
|
278
312
|
|
|
279
|
-
The external system reports back with a signed signal to `POST /v1/webhooks/<definition-id>/runs/<run-id>/signals/<signal-key>`. The body is `{"status": "passed" | "warning" | "failed", "summary": "...", "evidence": {...}}`, signed with the callback secret
|
|
313
|
+
The external system reports back with a signed signal to `POST /v1/webhooks/<definition-id>/runs/<run-id>/signals/<signal-key>`. The body is `{"status": "passed" | "warning" | "failed", "summary": "...", "evidence": {...}}`, signed with the callback secret under the [KXM sender contract](#kxm-sender-contract), bound to the route's run ID and signal key, with a stable delivery ID.
|
|
280
314
|
|
|
281
315
|
- `passed` applies the evidence rule to the saved and new evidence together, then advances or completes the run.
|
|
282
316
|
- `warning` or `failed` uses up an attempt, records an error, and sends the coordinator a correction prompt while attempts remain.
|
|
@@ -303,8 +337,8 @@ Expected output:
|
|
|
303
337
|
posted signed signal
|
|
304
338
|
```
|
|
305
339
|
|
|
306
|
-
> [!
|
|
307
|
-
>
|
|
340
|
+
> [!NOTE]
|
|
341
|
+
> Hub and Runtime run IDs share the `run_<32-hex>` shape. Inside a KXM project, `kxm gate signal` and `kxm workflow wait` send a run to the Runtime supervisor only when the project's Runtime store holds that run; a hub run goes to the hub. Check with `--dry-run`: the hub path prints `would post signed signal`, the Runtime path `would post signal to KXM run`.
|
|
308
342
|
|
|
309
343
|
For your own adapters, [`examples/workflow-signal.ts`](../../examples/workflow-signal.ts) shows the same signed request in about 60 lines.
|
|
310
344
|
|
|
@@ -339,7 +373,7 @@ If a stage's peer policy declares a lower `degradation.minProducers`, an operato
|
|
|
339
373
|
## Security notes
|
|
340
374
|
|
|
341
375
|
- The start secret authorizes creating runs; the callback secret authorizes only checkpointing a waiting run with a matching signal key. Keep them separate with `signalSecretEnv`.
|
|
342
|
-
-
|
|
376
|
+
- A KXM signature covers the timestamp, delivery ID, definition, run, and signal key, and expires after 300 seconds, so a captured request cannot be replayed under a new delivery ID or against another run. Jira and GitHub sign only the body: a captured provider delivery re-sent under its own delivery ID only returns the duplicate, and a body starts at most one run. Terminate TLS and restrict ingress anyway.
|
|
343
377
|
- Repository rules, human approvals, and harness permissions stay in charge of pushing, merging, and changing Jira.
|
|
344
378
|
|
|
345
379
|
## Troubleshooting
|
|
@@ -353,7 +387,8 @@ If a stage's peer policy declares a lower `degradation.minProducers`, an operato
|
|
|
353
387
|
| `stage <id> is missing required evidence: <key>` | A required key is missing or empty. | Supply every key from `kxm_workflow_get`. |
|
|
354
388
|
| `stage <id> is not currently active` or `workflow is waiting` | Wrong stage, or the stage is paused for a signal. | Use `currentStage`; send the signal instead of a checkpoint. |
|
|
355
389
|
| The run fails right after the coordinator replies | It settled before the last checkpoint. | Checkpoint every stage, or wait, before replying. |
|
|
356
|
-
| `kxm gate signal` prints `signed signal failed` | Wrong signal key, run not waiting,
|
|
390
|
+
| `kxm gate signal` prints `signed signal failed` | Wrong signal key, run not waiting, a delivery-ID conflict, or a clock more than 300 seconds off. | Compare the run's `waiting.signalKey`; use a new delivery ID for a new result; check the sender's clock. |
|
|
391
|
+
| A custom sender gets `401 webhook_signature_missing` | It sends only `X-Hub-Signature-256` to a `generic` definition or a signal. | Sign with the [KXM sender contract](#kxm-sender-contract). |
|
|
357
392
|
|
|
358
393
|
## Next steps
|
|
359
394
|
|
|
@@ -68,7 +68,7 @@ Expected output on first start:
|
|
|
68
68
|
kxm hub: using newly generated KXM_AUTH_TOKEN from /home/kxm/.local/state/kxm/hub-env.json
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Operator clients on the same machine (`kxm dash`, the Runtime) read the same file. They use `KXM_AUTH_TOKEN` from their environment first, then the saved project token for their project, then the saved admin token. Agent clients (the Pi extension, the Claude Code MCP server, and the `kxm peer` and `kxm workflow` agent commands) never fall back to the admin token.
|
|
72
72
|
|
|
73
73
|
To rotate the admin token, restart the hub with a new `KXM_AUTH_TOKEN`; the hub saves it in place of the old one and keeps the saved project tokens. Rotate a project token by restarting with an updated full `KXM_PROJECT_TOKENS` map. Then restart every client that held the old value. Deleting `hub-env.json` also works, but it drops the saved project tokens too.
|
|
74
74
|
|
|
@@ -183,14 +183,15 @@ The hub does not poll GitHub. Run `kxm gate github watch` with the run's `--run-
|
|
|
183
183
|
|
|
184
184
|
| Response | Meaning |
|
|
185
185
|
|---|---|
|
|
186
|
-
| 401 | The
|
|
186
|
+
| 401 | The signature is missing, does not match, or is older than 300 seconds (`webhook_timestamp_expired`: check the sender's clock). Signals and `generic` starts need the [KXM sender contract](../guides/webhook-workflows.md#kxm-sender-contract); a callback must use `signalSecretEnv` when the definition sets it |
|
|
187
187
|
| 400 | The delivery ID or JSON body is missing; `workflow_evidence_incomplete` names `missingRequirements`; `invalid_workflow_evidence` means non-string or duplicate keys |
|
|
188
188
|
| 404 | The definition or run ID does not match this hub |
|
|
189
189
|
| 409 `workflow_target_unavailable` | The configured coordinator has never registered; start it once with the matching project and name |
|
|
190
190
|
| 409 `workflow_not_waiting` | No wait is active: `kxm_workflow_wait` was not called, the deadline failed the run, or a prior signal advanced it |
|
|
191
191
|
| 409 `workflow_signal_mismatch`, `workflow_signal_context_mismatch` | Use the run's exact `waiting.signalKey`; fix or omit `workflow.run`, `workflow.stage` and `workflow.signal` evidence |
|
|
192
192
|
| 204 | The event or filter did not match, so no run was intended |
|
|
193
|
-
|
|
|
193
|
+
| 409 `webhook_delivery_conflict`, `webhook_payload_replayed` | The delivery ID was reused with a different body, or a Jira or GitHub body already started a run under another delivery ID |
|
|
194
|
+
| 200 with `duplicate: true` | A retry of the same delivery ID and body was deduplicated |
|
|
194
195
|
|
|
195
196
|
A failed or timed-out callback consumes that wait. Re-enter the wait and start a new `github watch` or `kxm gate signal`; keep an explicit `--delivery-id` only for retries of one unchanged body. See [Run webhook workflows](../guides/webhook-workflows.md).
|
|
196
197
|
|
|
@@ -1826,7 +1826,7 @@ kxm workflow record wf_123 lesson "Flaky test hid a race" --stage-id verify --ev
|
|
|
1826
1826
|
kxm workflow wait [runId] [stageId] [signalKey] [summary] [--evidence <json>] [--evidence-refs <json>] [--timeout-ms <ms>]
|
|
1827
1827
|
```
|
|
1828
1828
|
|
|
1829
|
-
Wait for a workflow signal callback: pauses the active stage until a signed external callback checkpoints it.
|
|
1829
|
+
Wait for a workflow signal callback: pauses the active stage until a signed external callback checkpoints it. Inside a project, a run that this project's Runtime store holds goes to the Runtime instead of the hub (the supervisor starts if needed); any other run ID, including a hub workflow run of the same `run_` + 32-hex shape, goes to the hub. The Runtime has no wait state yet: it checks that the run exists, answers `waiting: true`, and records nothing, so the command prints `waiting for signal on KXM run <id>` although the run is unchanged.
|
|
1830
1830
|
|
|
1831
1831
|
| Option | Argument | Default | Description |
|
|
1832
1832
|
|---|---|---|---|
|
|
@@ -1878,7 +1878,7 @@ KXM_WORKFLOW_ID=provenance-review KXM_WORKFLOW_SIGNAL_SECRET="$SIGNAL_SECRET" kx
|
|
|
1878
1878
|
kxm workflow start [definitionId] [--payload <json|@file>] [--delivery-id <id>] [--event <name>]
|
|
1879
1879
|
```
|
|
1880
1880
|
|
|
1881
|
-
POST a signed workflow-start webhook to `<hub>/v1/webhooks/<definitionId
|
|
1881
|
+
POST a signed workflow-start webhook to `<hub>/v1/webhooks/<definitionId>` under the [KXM sender contract](../guides/webhook-workflows.md#kxm-sender-contract): `x-kxm-signature` is an HMAC-SHA256 over the timestamp (`x-kxm-timestamp`), the delivery ID (`x-kxm-delivery-id`), the definition ID and the body, so a captured request cannot start a second run under another delivery ID. Repeating a delivery ID with the same body returns the existing run ID as a duplicate; with a different body the hub answers 409 `webhook_delivery_conflict`.
|
|
1882
1882
|
|
|
1883
1883
|
| Option | Argument | Default | Description |
|
|
1884
1884
|
|---|---|---|---|
|
|
@@ -2166,9 +2166,9 @@ Post a signed workflow callback that checkpoints a waiting stage.
|
|
|
2166
2166
|
| `--recovery-action` | `<action>` | none | KXM recovery action: retry, fail, cancel, unblock |
|
|
2167
2167
|
|
|
2168
2168
|
- Arguments: `<runId>` Workflow run ID; `<signalKey>` Wait signal key; `<status>` passed, warning, or failed; `<summary>` Callback summary; `[evidence...]` required-key=evidence pairs.
|
|
2169
|
-
-
|
|
2170
|
-
- Otherwise it is a hub webhook callback: `KXM_WORKFLOW_ID` names the definition, and the secret is the definition's `signalSecretEnv` (falling back to `secretEnv`) when a definition source is configured, else `KXM_WORKFLOW_SIGNAL_SECRET`. JSON keys: `duplicate`, `deliveryId`.
|
|
2171
|
-
- Reuse `--delivery-id` to retry one unchanged callback without a duplicate.
|
|
2169
|
+
- Inside a project, a run that this project's Runtime store holds is signaled in the Runtime (the supervisor starts if needed) and `--recovery-action` is passed through. JSON keys: `runId`, `signalKey`, `status`, `unblocked`, `deliveryId`. Hub workflow runs share the `run_` + 32-hex shape, so the store, not the ID, decides.
|
|
2170
|
+
- Otherwise it is a hub webhook callback: `KXM_WORKFLOW_ID` names the definition, and the secret is the definition's `signalSecretEnv` (falling back to `secretEnv`) when a definition source is configured, else `KXM_WORKFLOW_SIGNAL_SECRET`. The callback is signed under the [KXM sender contract](../guides/webhook-workflows.md#kxm-sender-contract), bound to its timestamp, delivery ID, definition, run and signal key. JSON keys: `duplicate`, `deliveryId`.
|
|
2171
|
+
- Reuse `--delivery-id` to retry one unchanged callback without a duplicate; each send re-signs with a fresh timestamp.
|
|
2172
2172
|
- Honors `--dry-run`. Exit 2 for a missing argument, an invalid status, malformed or duplicate evidence keys, or missing `KXM_WORKFLOW_ID` or secret; exit 1 for `signal_failed`.
|
|
2173
2173
|
|
|
2174
2174
|
```bash
|
|
@@ -2227,7 +2227,7 @@ Captured without a GitHub token. With `GITHUB_TOKEN` set, the same command polls
|
|
|
2227
2227
|
|
|
2228
2228
|
## `kxm peer`
|
|
2229
2229
|
|
|
2230
|
-
Peer agent messaging through the hub. Each invocation connects to the hub as a short-lived agent named `KXM_AGENT_NAME` (default `cli-<pid>`) in project `KXM_PROJECT` (default: the `package.json` name, else the directory name), using `KXM_AUTH_TOKEN`,
|
|
2230
|
+
Peer agent messaging through the hub. Each invocation connects to the hub as a short-lived agent named `KXM_AGENT_NAME` (default `cli-<pid>`) in project `KXM_PROJECT` (default: the `package.json` name, else the directory name), using `KXM_AUTH_TOKEN`, else this project's saved project token from `hub-env.json`. It never uses the persisted admin token: with neither, the command exits 2 with `project_token_missing` (`nextAction: "export_kxm_auth_token"`) before contacting the hub. The `kxm workflow` agent verbs (`checkpoint`, `record`, `wait`, `get`, `list`) connect the same way. That agent appears in `peer list` and the dashboard. Every subcommand accepts `--payload <json>` with the tool's fields as one object; explicit flags override it. Tool policy from `KXM_ATTEMPT_TOKEN`, `KXM_SESSION_TOKEN`, or the on-disk session token is enforced first (`tool_policy_denied`, `session_token_invalid`, `attempt_token_invalid`).
|
|
2231
2231
|
|
|
2232
2232
|
All subcommands need a hub, honor `--dry-run` (printing the parsed `args` without connecting), print the hub's result object on success, and print `{"ok":false,"error":"command_failed","detail":"..."}` with exit 1 on failure. Malformed `--payload` exits 2 with `invalid_payload`.
|
|
2233
2233
|
|
|
@@ -114,9 +114,9 @@ Every agent client (the Pi extension, the Claude Code MCP server, and the `kxm p
|
|
|
114
114
|
|
|
115
115
|
| Harness | Default name | Default purpose | Token when `KXM_AUTH_TOKEN` is unset |
|
|
116
116
|
|---|---|---|---|
|
|
117
|
-
| Pi extension | The Pi session name, else `pi-<pid>` | `General-purpose coding agent` |
|
|
117
|
+
| Pi extension | The Pi session name, else `pi-<pid>` | `General-purpose coding agent` | This project's saved project token only; never the admin token |
|
|
118
118
|
| Claude Code MCP server | `claude` from the plugin settings, else `claude-<pid>` | `Claude Code implementation and review agent` | This project's saved project token only; never the admin token |
|
|
119
|
-
| `kxm peer` commands | `cli-<pid>` | `CLI agent client` |
|
|
119
|
+
| `kxm peer` and `kxm workflow` agent commands | `cli-<pid>` | `CLI agent client` | This project's saved project token only; never the admin token (`project_token_missing`, exit 2) |
|
|
120
120
|
|
|
121
121
|
A clean shutdown marks an identity offline. Reconnecting with the same project and name resumes its durable agent ID and rotates its agent key. If the Claude Code name is already online in the project, the MCP server registers once more as `<name>-<pid>`.
|
|
122
122
|
|
|
@@ -26,7 +26,7 @@ Each route requires one of these credential classes. Send tokens as `Authorizati
|
|
|
26
26
|
| Project | The project's token, or the admin token for a project with no token of its own | Admission for agent registration and Runtime sync |
|
|
27
27
|
| Agent | The project token plus `x-kxm-agent-id` and `x-kxm-agent-key` from registration | The agent key rotates on every registration (401 `invalid_agent_identity` when stale) |
|
|
28
28
|
| Agent or admin | Agent headers for your own project, or the admin token with an explicit `project` | Admin calls may name a caller in `x-kxm-caller-id` |
|
|
29
|
-
| Signed |
|
|
29
|
+
| Signed | The [KXM sender contract](../guides/webhook-workflows.md#kxm-sender-contract): `x-kxm-signature` over the timestamp, delivery ID, route and body, within 300 seconds. A `jira` or `github` start may instead carry its provider's body HMAC | No bearer token; the secret is the workflow definition's |
|
|
30
30
|
|
|
31
31
|
A wrong or missing token is 401 `invalid_auth`. The [trust model](../concepts/trust-model.md) explains which person or process holds each credential.
|
|
32
32
|
|
|
@@ -75,7 +75,7 @@ sequenceDiagram
|
|
|
75
75
|
|
|
76
76
|
| Method | Path | Auth | Purpose |
|
|
77
77
|
|---|---|---|---|
|
|
78
|
-
| `POST` | `/v1/messages` | Agent | Send a request: `target`, `content`, optional `delivery`, `correlationId`, `idempotencyKey`, `workflowContext`, `ttlMs`, `maxHops`, `allowOffline`. 202 new; 200 with `idempotent: true` for an exact retry |
|
|
78
|
+
| `POST` | `/v1/messages` | Agent | Send a request: `target`, `content`, optional `delivery`, `correlationId`, `idempotencyKey`, `workflowContext`, `ttlMs`, `hops`, `maxHops`, `allowOffline`. `kxm_send` and `kxm_fanout` set `hops` one past the inbound request being handled. 202 new; 200 with `idempotent: true` for an exact retry |
|
|
79
79
|
| `GET` | `/v1/messages/<id>` | Agent | Read a message you sent or received |
|
|
80
80
|
| `POST` | `/v1/messages/<id>/ack` | Agent | Recipient marks a queued message `delivered` |
|
|
81
81
|
| `POST` | `/v1/messages/<id>/reply` | Agent | Recipient replies with `content`; the sender gets a `reply` event |
|
|
@@ -115,13 +115,13 @@ Key errors: `workflow_not_found` (404), `workflow_stage_out_of_order`, `workflow
|
|
|
115
115
|
| `POST` | `/v1/webhooks/<definitionId>` | Signed with the definition's start secret | Start a run and prompt the coordinator (status codes below) |
|
|
116
116
|
| `POST` | `/v1/webhooks/<definitionId>/runs/<runId>/signals/<signalKey>` | Signed with the signal secret, else the start secret | Report an external result for a waiting stage: `status` (`passed`, `warning`, `failed`), `summary`, `evidence`. 202 when the coordinator is resumed, 200 otherwise |
|
|
117
117
|
|
|
118
|
-
A start answers 202 for a new run, 200 with `duplicate: true` for a known delivery ID, and 204 when the event or filter does not match.
|
|
118
|
+
A start answers 202 for a new run, 200 with `duplicate: true`, `runId` and `status` for a known delivery ID and body, and 204 when the event or filter does not match.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
Every signal, and every start of a `generic` definition, is signed under the KXM sender contract, and its delivery ID is the signed `x-kxm-delivery-id`. A `jira` or `github` start may instead carry the provider's body HMAC in `x-hub-signature-256` (or `x-hub-signature`); its delivery ID is then that provider's own header, `x-atlassian-webhook-identifier` or `x-github-delivery`, and the signed body starts at most one run. The event comes from `x-github-event`, else the payload's `webhookEvent` or `event` field.
|
|
121
121
|
|
|
122
|
-
A start with a known delivery ID
|
|
122
|
+
A start with a known delivery ID and a different body is refused with 409 `webhook_delivery_conflict`. A signal with a known delivery ID returns the original receipt when the body matches and 409 `workflow_signal_delivery_conflict` when it does not. Deduplication lasts as long as the run is retained.
|
|
123
123
|
|
|
124
|
-
Key errors: `webhook_not_found` (404), `webhook_signature_missing`, `webhook_signature_unsupported`, `webhook_signature_invalid` (401), `workflow_target_unavailable` (409, the coordinator never registered), `workflow_not_waiting`, `workflow_signal_mismatch` (409, the run waits for another key) and `workflow_signal_context_mismatch` (409, evidence names another run, stage or signal). See [Webhook workflows](../guides/webhook-workflows.md).
|
|
124
|
+
Key errors: `webhook_not_found` (404), `webhook_signature_missing`, `webhook_signature_unsupported`, `webhook_signature_invalid`, `webhook_timestamp_invalid`, `webhook_timestamp_expired` (401), `webhook_delivery_conflict`, `webhook_payload_replayed` (409, a provider body already started a run under another delivery ID), `workflow_target_unavailable` (409, the coordinator never registered), `workflow_not_waiting`, `workflow_signal_mismatch` (409, the run waits for another key) and `workflow_signal_context_mismatch` (409, evidence names another run, stage or signal). See [Webhook workflows](../guides/webhook-workflows.md).
|
|
125
125
|
|
|
126
126
|
## Context and state
|
|
127
127
|
|
package/docs/reference/tools.md
CHANGED
|
@@ -44,7 +44,7 @@ The workflow tools act on hub [webhook workflow runs](workflow-definitions.md#we
|
|
|
44
44
|
|
|
45
45
|
### Identity and credentials
|
|
46
46
|
|
|
47
|
-
Each tool call acts as the calling session's registered agent in one hub project. The Claude Code MCP server authenticates with a project token only: `auth_token` from the plugin settings, else this project's token saved in `hub-env.json`, and never the admin token. The Pi extension uses `KXM_AUTH_TOKEN`,
|
|
47
|
+
Each tool call acts as the calling session's registered agent in one hub project. The Claude Code MCP server authenticates with a project token only: `auth_token` from the plugin settings, else this project's token saved in `hub-env.json`, and never the admin token. The Pi extension uses `KXM_AUTH_TOKEN`, else this project's saved project token, and never the admin token, including one that hub auto-start generated. `kxm_send` and `kxm_fanout` send one hop past the inbound request the session is handling, so the hub's hop limit stops a chain of forwarding agents. See [Agent settings](configuration.md#agent-settings).
|
|
48
48
|
|
|
49
49
|
### Tool policy
|
|
50
50
|
|
|
@@ -49,7 +49,7 @@ Keep the file out of `.kxm/config/workflows/`: any JSON there counts as legacy c
|
|
|
49
49
|
| Field | Type and limits | Default | Effect |
|
|
50
50
|
|---|---|---|---|
|
|
51
51
|
| `id` | String, 64 characters, unique | Required | The URL segment in `/v1/webhooks/<id>` and the name `kxm workflow start` takes |
|
|
52
|
-
| `source` | `jira`, `github` or `generic` | `generic` | Recorded on each run;
|
|
52
|
+
| `source` | `jira`, `github` or `generic` | `generic` | Recorded on each run. A `jira` or `github` definition also accepts its provider's body-only signature and delivery header; a `generic` one accepts only the KXM sender contract |
|
|
53
53
|
| `project` | String, 128 characters | Required | Hub project the run and its coordinator belong to |
|
|
54
54
|
| `target` | Agent name or ID, 80 characters | Required | The coordinator. It must have registered at least once before a run starts; it may be offline |
|
|
55
55
|
| `secretEnv` | Environment variable name | Use this or `secret` | Variable holding the start secret, read when the definitions are parsed |
|
|
@@ -148,7 +148,7 @@ Every transition taken is journaled as a `state-change` entry.
|
|
|
148
148
|
|
|
149
149
|
`kxm_workflow_wait` parks the active stage in `waiting` under a `signalKey` until a signed callback arrives or the wait expires (1 second to 30 days, default 24 hours; expiry fails the run). The coordinator can then reply to release its turn.
|
|
150
150
|
|
|
151
|
-
The callback is `POST /v1/webhooks/<id>/runs/<runId>/signals/<signalKey>`, signed
|
|
151
|
+
The callback is `POST /v1/webhooks/<id>/runs/<runId>/signals/<signalKey>`, signed under the [KXM sender contract](../guides/webhook-workflows.md#kxm-sender-contract) with the signal secret (or the start secret), bound to its timestamp, delivery ID, run and signal key. Its body carries `status`, `summary` and `evidence`. Evidence keys `workflow.run`, `workflow.stage` and `workflow.signal`, when present, must match the route (`workflow_signal_context_mismatch`). The signal checkpoints the stage with its status; if work remains, the hub sends the coordinator a resume message. See the [HTTP API](http-api.md#webhooks-and-signals) for headers and deduplication.
|
|
152
152
|
|
|
153
153
|
When `autoResumeLimit` is set and a stage's attempts reach it without passing, the stage escalates instead of retrying: it waits for signal key `audit_escalation` for 24 hours and records a `kxm.terminal-receipt.v1` receipt. A signed `audit_escalation` signal, or `kxm role resume`, resumes it. Anyone holding the signal secret can clear an escalation.
|
|
154
154
|
|
|
@@ -155,7 +155,7 @@ kxm hub view: health=ok; planner; 1 online agent(s)
|
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
> [!NOTE]
|
|
158
|
-
> Without `KXM_AUTH_TOKEN`, the extension signs in with
|
|
158
|
+
> Without `KXM_AUTH_TOKEN`, the extension signs in with the project token saved for this project on this machine, and never with the saved admin token. With neither, it reports the fix and stays offline. Set the project token so that each agent holds only its own project's credential.
|
|
159
159
|
|
|
160
160
|
## 7. Start the reviewer and send a request
|
|
161
161
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { canonicalWorkflowEvidenceKey } from "../plugins/kxm/src/workflow.ts";
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { canonicalWorkflowEvidenceKey, workflowWebhookHeaders } from "../plugins/kxm/src/workflow.ts";
|
|
3
3
|
|
|
4
4
|
const [runId, signalKey, status, summary, ...evidenceArgs] = process.argv.slice(2);
|
|
5
5
|
const serverUrl = process.env.KXM_SERVER_URL?.trim() || "http://127.0.0.1:7331";
|
|
@@ -31,7 +31,6 @@ if (!runId || !signalKey || !status || !summary || !definitionId || !secret) {
|
|
|
31
31
|
}
|
|
32
32
|
const evidence = Object.fromEntries(evidenceEntries);
|
|
33
33
|
const body = JSON.stringify({ status, summary, evidence });
|
|
34
|
-
const signature = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
35
34
|
const deliveryId = process.env.KXM_SIGNAL_DELIVERY_ID?.trim() || `example-${randomUUID()}`;
|
|
36
35
|
const endpoint = [
|
|
37
36
|
serverUrl.replace(/\/$/, ""),
|
|
@@ -46,8 +45,8 @@ if (!runId || !signalKey || !status || !summary || !definitionId || !secret) {
|
|
|
46
45
|
method: "POST",
|
|
47
46
|
headers: {
|
|
48
47
|
"content-type": "application/json",
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// Signs the timestamp, delivery ID, definition, run, signal key, and body.
|
|
49
|
+
...workflowWebhookHeaders({ secret, scope: { definitionId, runId, signalKey }, deliveryId, body }),
|
|
51
50
|
},
|
|
52
51
|
body,
|
|
53
52
|
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.97",
|
|
6
6
|
"description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "KontextMind",
|
|
@@ -8913,6 +8913,7 @@ __export(commands_exports, {
|
|
|
8913
8913
|
AGENT_COMMANDS_MAP: () => AGENT_COMMANDS_MAP,
|
|
8914
8914
|
clearSessionTokenFromDisk: () => clearSessionTokenFromDisk,
|
|
8915
8915
|
enforceToolPolicy: () => enforceToolPolicy,
|
|
8916
|
+
forwardedHops: () => forwardedHops,
|
|
8916
8917
|
getCliAgentCommands: () => getCliAgentCommands,
|
|
8917
8918
|
getMcpTools: () => getMcpTools,
|
|
8918
8919
|
getPiToolDefinitions: () => getPiToolDefinitions,
|
|
@@ -8932,6 +8933,13 @@ import { createHash, randomUUID as randomUUID3, timingSafeEqual } from "node:cry
|
|
|
8932
8933
|
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8933
8934
|
import { homedir as homedir3 } from "node:os";
|
|
8934
8935
|
import { dirname as dirname3, join as join7, resolve as resolve5 } from "node:path";
|
|
8936
|
+
function forwardedHops(handling) {
|
|
8937
|
+
if (!handling?.length) return void 0;
|
|
8938
|
+
return {
|
|
8939
|
+
hops: Math.max(...handling.map((message) => message.hops)) + 1,
|
|
8940
|
+
maxHops: Math.min(...handling.map((message) => message.maxHops))
|
|
8941
|
+
};
|
|
8942
|
+
}
|
|
8935
8943
|
function requiredString(value, name) {
|
|
8936
8944
|
if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} is required`);
|
|
8937
8945
|
return value.trim();
|
|
@@ -9336,12 +9344,13 @@ var init_commands = __esm({
|
|
|
9336
9344
|
required: ["target", "content"],
|
|
9337
9345
|
additionalProperties: false
|
|
9338
9346
|
},
|
|
9339
|
-
async execute(client, args) {
|
|
9347
|
+
async execute(client, args, context) {
|
|
9340
9348
|
const delivery = optionalString(args.delivery);
|
|
9341
9349
|
const correlationId = optionalString(args.correlationId);
|
|
9342
9350
|
const idempotencyKey = optionalString(args.idempotencyKey);
|
|
9343
9351
|
const workflowContext = optionalWorkflowContext(args.workflowContext);
|
|
9344
9352
|
const message = await client.send({
|
|
9353
|
+
...forwardedHops(context?.handling),
|
|
9345
9354
|
target: requiredString(args.target, "target"),
|
|
9346
9355
|
content: requiredString(args.content, "content"),
|
|
9347
9356
|
...delivery ? { delivery } : {},
|
|
@@ -9419,6 +9428,7 @@ var init_commands = __esm({
|
|
|
9419
9428
|
const targets = Array.isArray(args.targets) ? args.targets.map((t) => requiredString(t, "target")) : [];
|
|
9420
9429
|
return {
|
|
9421
9430
|
responses: await client.fanout({
|
|
9431
|
+
...forwardedHops(context?.handling),
|
|
9422
9432
|
targets,
|
|
9423
9433
|
content: requiredString(args.content, "content"),
|
|
9424
9434
|
...optionalString(args.correlationId) ? { correlationId: optionalString(args.correlationId) } : {},
|