@embassys/ambassador 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -8
- package/dist/agent-capabilities.d.ts +7 -2
- package/dist/agent-capabilities.js +73 -25
- package/dist/agent-capabilities.js.map +1 -1
- package/dist/ambassador-options.d.ts +4 -0
- package/dist/ambassador-options.js +7 -1
- package/dist/ambassador-options.js.map +1 -1
- package/dist/central-enrollment.js +1 -5
- package/dist/central-enrollment.js.map +1 -1
- package/dist/cli.js +17 -2
- package/dist/cli.js.map +1 -1
- package/dist/credential-store.d.ts +4 -4
- package/dist/credential-store.js +6 -98
- package/dist/credential-store.js.map +1 -1
- package/dist/delivery-profile.d.ts +10 -7
- package/dist/delivery-profile.js +62 -44
- package/dist/delivery-profile.js.map +1 -1
- package/dist/direct-delivery.d.ts +3 -1
- package/dist/direct-delivery.js +116 -15
- package/dist/direct-delivery.js.map +1 -1
- package/dist/gateway-application.d.ts +4 -0
- package/dist/gateway-application.js +14 -6
- package/dist/gateway-application.js.map +1 -1
- package/dist/gateway-paths.d.ts +2 -0
- package/dist/gateway-paths.js +2 -0
- package/dist/gateway-paths.js.map +1 -1
- package/dist/guided-registration.d.ts +2 -1
- package/dist/guided-registration.js +15 -7
- package/dist/guided-registration.js.map +1 -1
- package/dist/process-lock.js +1 -0
- package/dist/process-lock.js.map +1 -1
- package/dist/sqlite-artifact.d.ts +9 -1
- package/dist/sqlite-artifact.js +23 -8
- package/dist/sqlite-artifact.js.map +1 -1
- package/dist/webhook-secret-store.d.ts +16 -0
- package/dist/webhook-secret-store.js +49 -0
- package/dist/webhook-secret-store.js.map +1 -0
- package/dist/windows-access-control.d.ts +6 -0
- package/dist/windows-access-control.js +161 -0
- package/dist/windows-access-control.js.map +1 -0
- package/docs/development-reset.md +27 -0
- package/docs/getting-started-claude.md +8 -6
- package/docs/getting-started-codex.md +7 -6
- package/docs/getting-started-gemini.md +5 -4
- package/docs/getting-started-hermes.md +78 -27
- package/docs/getting-started-openclaw.md +87 -24
- package/docs/live-qualification.md +89 -16
- package/integrations/openclaw-ambassador/index.mjs +171 -0
- package/integrations/openclaw-ambassador/openclaw.plugin.json +73 -0
- package/integrations/openclaw-ambassador/package.json +11 -0
- package/integrations/openclaw-ambassador/receiver.d.mts +43 -0
- package/integrations/openclaw-ambassador/receiver.mjs +164 -0
- package/package.json +3 -1
|
@@ -3,47 +3,110 @@
|
|
|
3
3
|
## Before you start
|
|
4
4
|
|
|
5
5
|
- Install Node.js `>=24.19.0 <25`.
|
|
6
|
-
- Install and authenticate OpenClaw
|
|
6
|
+
- Install and authenticate OpenClaw.
|
|
7
7
|
- Make sure `openclaw` is on `PATH` for direct delivery.
|
|
8
8
|
- Ambassador never receives your provider credential.
|
|
9
9
|
|
|
10
10
|
## Set up direct delivery
|
|
11
11
|
|
|
12
|
-
1. From the directory OpenClaw may access, keep Ambassador running:
|
|
12
|
+
1. From the directory OpenClaw may access, keep the latest Ambassador running:
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
15
|
npx --yes @embassys/ambassador@latest start
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
2. Add
|
|
19
|
-
|
|
18
|
+
2. Add the endpoint printed by Ambassador as an unauthenticated Streamable
|
|
19
|
+
HTTP MCP server, then probe it:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
openclaw mcp set ambassador \
|
|
23
|
+
'{"url":"http://127.0.0.1:8787/mcp","transport":"streamable-http","enabled":true}'
|
|
24
|
+
openclaw mcp doctor ambassador --probe
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Replace the URL if Ambassador printed a different loopback port. Do not
|
|
28
|
+
configure authentication.
|
|
20
29
|
3. Start or restart OpenClaw so it sees the MCP server.
|
|
21
|
-
4. Ask OpenClaw to register your email
|
|
22
|
-
tool.
|
|
30
|
+
4. Ask OpenClaw to register your email. It calls Ambassador's
|
|
31
|
+
`register_agent` tool.
|
|
23
32
|
5. Choose **Send directly to this OpenClaw agent**.
|
|
24
33
|
6. Enter the six-digit code sent to your email.
|
|
25
34
|
|
|
26
|
-
Ambassador
|
|
35
|
+
Ambassador launches `openclaw acp` when a central message arrives. OpenClaw
|
|
27
36
|
does not accept session MCP injection, so keep the provider-side MCP entry from
|
|
28
|
-
step 2 configured.
|
|
37
|
+
step 2 configured. Reported versions are diagnostic only: Ambassador tries the
|
|
38
|
+
fixed ACP v1 command and exact `openclaw-acp` identity, then reports a bounded
|
|
39
|
+
startup, initialization, session, or delivery failure if they are incompatible.
|
|
40
|
+
|
|
41
|
+
## Set up webhook delivery
|
|
42
|
+
|
|
43
|
+
Webhook mode needs the receiver plugin shipped inside the latest Ambassador
|
|
44
|
+
package. Install Ambassador and the plugin once:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npm install --global @embassys/ambassador@latest
|
|
48
|
+
openclaw plugins install --accept-capabilities \
|
|
49
|
+
"$(npm root --global)/@embassys/ambassador/integrations/openclaw-ambassador"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The capability is an exact authenticated HTTP route. Review and accept it only
|
|
53
|
+
from the Ambassador package you installed.
|
|
54
|
+
|
|
55
|
+
1. With `ambassador start` still running, choose **Send to a webhook** during
|
|
56
|
+
registration. Ambassador responds with this setup command:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
ambassador webhook-secret
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Ambassador creates the secret, encrypts it in its own owner-only state, and
|
|
63
|
+
displays it. Repeating the command displays the same value; it does not
|
|
64
|
+
rotate it.
|
|
65
|
+
|
|
66
|
+
2. Store the displayed value in OpenClaw without putting it in shell history:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
openclaw secrets store set AMBASSADOR_WEBHOOK_SECRET --value-file -
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Paste the value, press Enter, then send end-of-file (`Ctrl-D`). Point the
|
|
73
|
+
plugin at that store entry:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
openclaw config set plugins.entries.embassys-ambassador.config.secret \
|
|
77
|
+
--ref-source store --ref-provider default \
|
|
78
|
+
--ref-id AMBASSADOR_WEBHOOK_SECRET
|
|
79
|
+
openclaw plugins enable embassys-ambassador --accept-capabilities
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The plugin starts the configured OpenClaw agent, which defaults to `main`.
|
|
83
|
+
It does not select a model or expose the webhook secret to the model.
|
|
84
|
+
|
|
85
|
+
3. Restart the OpenClaw gateway and run `openclaw plugins doctor`. The local
|
|
86
|
+
receiver URL is normally:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
http://127.0.0.1:18789/embassys/ambassador
|
|
90
|
+
```
|
|
29
91
|
|
|
30
|
-
|
|
92
|
+
Use the actual configured gateway port. A non-loopback receiver must use an
|
|
93
|
+
HTTPS URL.
|
|
31
94
|
|
|
32
|
-
|
|
95
|
+
4. Retry `register_agent` with webhook selected and that URL. MCP carries only
|
|
96
|
+
`delivery.mode` and `delivery.url`; it never carries the secret or a secret
|
|
97
|
+
name.
|
|
33
98
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
99
|
+
The plugin verifies Ambassador's bearer token, exact-body HMAC V2 signature,
|
|
100
|
+
five-minute timestamp window, request ID, and idempotency key before placing the
|
|
101
|
+
message on a bounded in-memory service queue. That service starts the normal
|
|
102
|
+
OpenClaw model turn outside the completed HTTP request. A webhook `202` proves
|
|
103
|
+
that OpenClaw accepted custody. It does not by itself prove that the model later
|
|
104
|
+
called Ambassador MCP; end-to-end checks must wait for the correlated
|
|
105
|
+
permission or action response.
|
|
39
106
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
the secret value.
|
|
107
|
+
If OpenClaw reports `model execution failed`, run `openclaw plugins doctor` and
|
|
108
|
+
check that the agent's own provider credential and Ambassador MCP entry are
|
|
109
|
+
available to the OpenClaw gateway. Ambassador intentionally logs only a safe
|
|
110
|
+
failure category, not the provider error or message body.
|
|
45
111
|
|
|
46
|
-
|
|
47
|
-
Production still checks the exact MCP client and ACP identities in the compiled
|
|
48
|
-
profile, so a new incompatible release fails closed. See
|
|
49
|
-
[Qualification](qualification.md) for compatibility evidence.
|
|
112
|
+
For local reruns, see [Reset local test state](development-reset.md).
|
|
@@ -11,10 +11,10 @@ general reply operations. It does test the deployed, action-specific
|
|
|
11
11
|
The runner covers the current package name, guided registration, one
|
|
12
12
|
full-message webhook target, and one direct target. The default direct target
|
|
13
13
|
is the deterministic mock ACP agent. Separately confirmed modes use the fixed
|
|
14
|
-
Codex or
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Codex, Hermes, or OpenClaw profiles. Real-provider modes use isolated provider
|
|
15
|
+
configuration copies. Installed-version probes are observational; production
|
|
16
|
+
requires the exact known client and ACP agent names and then tries the fixed
|
|
17
|
+
ACP v1 contract.
|
|
18
18
|
|
|
19
19
|
## Safety
|
|
20
20
|
|
|
@@ -29,11 +29,12 @@ still decides ACP compatibility by exact identity matching.
|
|
|
29
29
|
|
|
30
30
|
1. Pack and scan the exact candidate package.
|
|
31
31
|
2. Create two disposable identities through loopback local MCP.
|
|
32
|
-
3. Use the exact enabled `clientInfo`
|
|
32
|
+
3. Use the exact enabled `clientInfo.name` for one webhook profile and one
|
|
33
33
|
direct profile. Prove a dual-mode profile advertises direct as its default;
|
|
34
34
|
prove a direct-only profile proceeds without a delivery question.
|
|
35
35
|
4. Receive and use both verification emails without persisting their codes.
|
|
36
|
-
5. Restart and prove encrypted credential
|
|
36
|
+
5. Restart and prove encrypted credential, encrypted webhook-secret, and
|
|
37
|
+
nonsecret profile loading.
|
|
37
38
|
6. Prove valid Bearer plus DPoP requests and the negative DPoP matrix.
|
|
38
39
|
7. Validate the live action catalog against the recorded fixture schemas.
|
|
39
40
|
8. Request and decide one synthetic `get_phone_number` permission.
|
|
@@ -63,8 +64,8 @@ mode, the runner uses the mock ACP fixture compiled by `pnpm run test:build`
|
|
|
63
64
|
and does not run a paid provider.
|
|
64
65
|
|
|
65
66
|
For the real Codex mode, prepare an owner-only temporary home containing only
|
|
66
|
-
the copied Codex authentication needed for the run. Put
|
|
67
|
-
`codex-acp`
|
|
67
|
+
the copied Codex authentication needed for the run. Put the installed
|
|
68
|
+
`codex-acp` on `PATH`, then set:
|
|
68
69
|
|
|
69
70
|
```sh
|
|
70
71
|
export AMBASSADOR_LIVE_DIRECT_AGENT=codex
|
|
@@ -76,15 +77,16 @@ pnpm run qualify:live
|
|
|
76
77
|
The runner rejects an ordinary user home, records the installed version when
|
|
77
78
|
one can be observed, uses the compiled-in Codex command and profile, and never
|
|
78
79
|
accepts a command override. The observation does not establish compatibility;
|
|
79
|
-
the exact
|
|
80
|
+
ACP v1 initialization with the exact known agent name remains authoritative.
|
|
81
|
+
The runner also lets
|
|
80
82
|
abandoned server-side polls expire after the restart check before it enqueues a
|
|
81
83
|
message. Delete the isolated home after the run.
|
|
82
84
|
|
|
83
85
|
For Hermes, prepare an owner-only temporary home containing only `.hermes/.env`,
|
|
84
86
|
`.hermes/auth.json`, `.hermes/config.yaml`, and
|
|
85
87
|
`.hermes/shared/nous_auth.json` copied from the authenticated installation.
|
|
86
|
-
Remove unrelated MCP entries only from that copy. Put
|
|
87
|
-
|
|
88
|
+
Remove unrelated MCP entries only from that copy. Put the installed Hermes
|
|
89
|
+
Agent and `hermes-acp` on `PATH`, then choose one fixed mode:
|
|
88
90
|
|
|
89
91
|
```sh
|
|
90
92
|
export AMBASSADOR_HERMES_QUALIFICATION_HOME=/absolute/path/to/isolated/home
|
|
@@ -103,14 +105,46 @@ pnpm run qualify:live
|
|
|
103
105
|
```
|
|
104
106
|
|
|
105
107
|
The runner rejects the ordinary Hermes home and non-owner-only copies. It uses
|
|
106
|
-
exact MCP
|
|
107
|
-
`hermes-acp` for direct mode, and configures
|
|
108
|
-
copy. The runner records the installed
|
|
109
|
-
use that observation as a compatibility
|
|
110
|
-
|
|
108
|
+
the exact MCP client name `mcp` with a deliberately non-release version value,
|
|
109
|
+
launches only compiled-in `hermes-acp` for direct mode, and configures
|
|
110
|
+
Ambassador MCP only in the isolated copy. The runner records the installed
|
|
111
|
+
Hermes version when it can, but does not use that observation as a compatibility
|
|
112
|
+
gate; direct mode requires ACP v1 and the exact `hermes-agent` name. Webhook
|
|
113
|
+
mode starts Hermes's authenticated generic route,
|
|
111
114
|
requires its bearer filter and native HMAC V2 validation, and suppresses
|
|
112
115
|
provider output. Delete the isolated home after every attempt.
|
|
113
116
|
|
|
117
|
+
For OpenClaw, prepare an owner-only temporary home containing copies of
|
|
118
|
+
`.openclaw/openclaw.json`, `.openclaw/state/openclaw.sqlite`, and
|
|
119
|
+
`.openclaw/agents/main/agent/openclaw-agent.sqlite`. Copy only the provider
|
|
120
|
+
credential used by that OpenClaw agent; for the tested Codex-backed agent this
|
|
121
|
+
also means `.codex/auth.json` and its provider configuration. Use SQLite's
|
|
122
|
+
backup operation for live database copies. Put the installed `openclaw` on
|
|
123
|
+
`PATH`, then choose one fixed mode:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
export AMBASSADOR_OPENCLAW_QUALIFICATION_HOME=/absolute/path/to/isolated/home
|
|
127
|
+
export AMBASSADOR_LIVE_DIRECT_AGENT=openclaw-direct
|
|
128
|
+
export AMBASSADOR_CONFIRM_LIVE_QUALIFICATION=run-live-qualification-with-real-openclaw-direct-and-two-disposable-mailosaur-identities
|
|
129
|
+
pnpm run qualify:live
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
or:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
export AMBASSADOR_OPENCLAW_QUALIFICATION_HOME=/absolute/path/to/isolated/home
|
|
136
|
+
export AMBASSADOR_LIVE_DIRECT_AGENT=openclaw-webhook
|
|
137
|
+
export AMBASSADOR_CONFIRM_LIVE_QUALIFICATION=run-live-qualification-with-real-openclaw-webhook-and-two-disposable-mailosaur-identities
|
|
138
|
+
pnpm run qualify:live
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The runner rejects the ordinary OpenClaw home. It configures Ambassador MCP
|
|
142
|
+
only in the copy. Direct mode launches the fixed `openclaw acp` profile and
|
|
143
|
+
requires ACP v1 plus exact agent name `openclaw-acp`. Webhook mode installs the
|
|
144
|
+
package-shipped receiver into the copy, creates the secret through the packed
|
|
145
|
+
Ambassador CLI, stores it through OpenClaw's secret store, and runs the real
|
|
146
|
+
OpenClaw gateway. Delete the isolated home after every attempt.
|
|
147
|
+
|
|
114
148
|
## Required report
|
|
115
149
|
|
|
116
150
|
Record only:
|
|
@@ -262,6 +296,45 @@ These observations approve the source registry's exact Hermes ACP 0.20.5
|
|
|
262
296
|
entry. They do not show that published Ambassador 0.2.7 supports Hermes 0.20.5
|
|
263
297
|
direct mode. Ambassador 0.2.8 contains the candidate change.
|
|
264
298
|
|
|
299
|
+
## OpenClaw observations
|
|
300
|
+
|
|
301
|
+
On 2026-09-03, authenticated OpenClaw 2026.8.2 ran on macOS arm64 with Node
|
|
302
|
+
24.19.0 and passed the complete live correlated-result flow in direct and
|
|
303
|
+
webhook modes with the Ambassador 0.2.10 candidate. Both modes registered and
|
|
304
|
+
verified two disposable identities, reloaded encrypted Ambassador state after
|
|
305
|
+
restart, exercised live REST and DPoP plus the deployed action catalog, and
|
|
306
|
+
completed the synthetic phone-number permission and action round trip. The
|
|
307
|
+
real OpenClaw model called `respond_to_permission` and called
|
|
308
|
+
`submit_action_result` exactly once. The controlled requester received the
|
|
309
|
+
correlated final response, and local completion or webhook custody preceded
|
|
310
|
+
central acknowledgement. Final candidate digests and the separate mode results
|
|
311
|
+
are recorded in [Delivery qualification](qualification.md).
|
|
312
|
+
|
|
313
|
+
Direct mode proved ACP v1 initialization through fixed `openclaw acp`, exact
|
|
314
|
+
agent name `openclaw-acp`, provider-side Ambassador MCP configuration, real
|
|
315
|
+
model execution, and correlated submission. An earlier isolation attempt
|
|
316
|
+
omitted the credential for the agent's configured provider backend; OpenClaw
|
|
317
|
+
then ended the model turn with an authentication failure before any Ambassador
|
|
318
|
+
MCP call. Adding that owner-only credential to the isolated copy made the
|
|
319
|
+
unchanged direct flow pass. This was an isolation-fixture failure, not an
|
|
320
|
+
Ambassador ACP incompatibility.
|
|
321
|
+
|
|
322
|
+
Webhook mode proved the package-shipped route's bearer and exact-body HMAC V2
|
|
323
|
+
checks, bounded custody queue, real model execution, Ambassador MCP calls, and
|
|
324
|
+
the final response. Earlier receiver attempts returned `202` and Ambassador
|
|
325
|
+
correctly acknowledged central, but OpenClaw made no model or MCP call. The
|
|
326
|
+
first implementation omitted required embedded-run fields. After those were
|
|
327
|
+
added, the detached run inherited the HTTP handler's released work-admission
|
|
328
|
+
lease and OpenClaw rejected it with the safe class `GatewayDrainingError`. A
|
|
329
|
+
plugin-service queue created outside the request context removes that false
|
|
330
|
+
drain path. A `202` still proves custody only; the passing run waited for the
|
|
331
|
+
model calls and requester response.
|
|
332
|
+
|
|
333
|
+
All OpenClaw attempts used an owner-only isolated home. Mailosaur messages,
|
|
334
|
+
temporary Ambassador state, the OpenClaw copy, and copied provider credentials
|
|
335
|
+
were removed after qualification. No provider output or message content was
|
|
336
|
+
recorded.
|
|
337
|
+
|
|
265
338
|
## Earlier direct observation
|
|
266
339
|
|
|
267
340
|
On 2026-09-02, real Codex had already passed delivery, injected Ambassador MCP
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import { resolveConfiguredSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildAmbassadorPrompt,
|
|
6
|
+
classifyOpenClawExecutionError,
|
|
7
|
+
createBoundedOpenClawWorkQueue,
|
|
8
|
+
verifyAmbassadorWebhook,
|
|
9
|
+
} from "./receiver.mjs";
|
|
10
|
+
|
|
11
|
+
const MAX_BODY_BYTES = 512 * 1024;
|
|
12
|
+
const SECRET = /^[a-f0-9]{48}$/u;
|
|
13
|
+
const AGENT_ID = /^[A-Za-z0-9._~-]{1,128}$/u;
|
|
14
|
+
const RECEIPT_TTL_MS = 60 * 60 * 1_000;
|
|
15
|
+
const MAX_RECEIPTS = 1_024;
|
|
16
|
+
const MAX_PENDING_MODEL_TURNS = 64;
|
|
17
|
+
|
|
18
|
+
function response(res, status) {
|
|
19
|
+
res.statusCode = status;
|
|
20
|
+
res.setHeader("cache-control", "no-store");
|
|
21
|
+
res.end();
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readBody(req) {
|
|
26
|
+
const declared = req.headers["content-length"];
|
|
27
|
+
if (declared !== undefined) {
|
|
28
|
+
const value = Array.isArray(declared) ? Number.NaN : Number(declared);
|
|
29
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_BODY_BYTES) return undefined;
|
|
30
|
+
}
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
for await (const chunk of req) {
|
|
34
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
35
|
+
size += bytes.byteLength;
|
|
36
|
+
if (size > MAX_BODY_BYTES) {
|
|
37
|
+
req.resume();
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
chunks.push(bytes);
|
|
41
|
+
}
|
|
42
|
+
return Buffer.concat(chunks, size);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requestHeaders(req) {
|
|
46
|
+
const headers = new Headers();
|
|
47
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
48
|
+
if (typeof value === "string") headers.set(name, value);
|
|
49
|
+
}
|
|
50
|
+
return headers;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default definePluginEntry({
|
|
54
|
+
id: "embassys-ambassador",
|
|
55
|
+
name: "Embassys Ambassador",
|
|
56
|
+
description: "Accepts authenticated Ambassador webhooks and starts an OpenClaw model turn.",
|
|
57
|
+
register(api) {
|
|
58
|
+
const secret = api.pluginConfig?.secret;
|
|
59
|
+
const configuredAgentId = api.pluginConfig?.agentId;
|
|
60
|
+
const agentId = configuredAgentId === undefined ? "main" : configuredAgentId;
|
|
61
|
+
if (secret === undefined) {
|
|
62
|
+
api.logger.warn("Embassys Ambassador webhook is not configured");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
!(
|
|
67
|
+
(typeof secret === "string" && SECRET.test(secret)) ||
|
|
68
|
+
(secret !== null && typeof secret === "object" && !Array.isArray(secret))
|
|
69
|
+
)
|
|
70
|
+
) {
|
|
71
|
+
throw new Error("Embassys Ambassador webhook configuration is invalid");
|
|
72
|
+
}
|
|
73
|
+
if (typeof agentId !== "string" || !AGENT_ID.test(agentId)) {
|
|
74
|
+
throw new Error("Embassys Ambassador webhook configuration is invalid");
|
|
75
|
+
}
|
|
76
|
+
const receipts = new Map();
|
|
77
|
+
const workQueue = createBoundedOpenClawWorkQueue(MAX_PENDING_MODEL_TURNS);
|
|
78
|
+
let acceptingWork = false;
|
|
79
|
+
let activeRunController;
|
|
80
|
+
let serviceLoop;
|
|
81
|
+
|
|
82
|
+
api.registerService({
|
|
83
|
+
id: "embassys-ambassador-model-turns",
|
|
84
|
+
start(ctx) {
|
|
85
|
+
acceptingWork = true;
|
|
86
|
+
serviceLoop = (async () => {
|
|
87
|
+
while (acceptingWork) {
|
|
88
|
+
const work = await workQueue.next();
|
|
89
|
+
if (work === undefined) return;
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
activeRunController = controller;
|
|
92
|
+
try {
|
|
93
|
+
const config = api.runtime.config.current();
|
|
94
|
+
await api.runtime.agent.runEmbeddedAgent({
|
|
95
|
+
sessionId: work.requestId,
|
|
96
|
+
runId: work.requestId,
|
|
97
|
+
timeoutMs: api.runtime.agent.resolveAgentTimeoutMs({ cfg: config }),
|
|
98
|
+
agentId,
|
|
99
|
+
workspaceDir: api.runtime.agent.resolveAgentWorkspaceDir(config, agentId),
|
|
100
|
+
config,
|
|
101
|
+
prompt: work.prompt,
|
|
102
|
+
trigger: "manual",
|
|
103
|
+
initialTurnTainted: true,
|
|
104
|
+
abortSignal: controller.signal,
|
|
105
|
+
});
|
|
106
|
+
ctx.serviceHealth?.clearFailure();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (!controller.signal.aborted) {
|
|
109
|
+
const classification = classifyOpenClawExecutionError(error);
|
|
110
|
+
api.logger.error(`Embassys Ambassador model execution failed (${classification})`);
|
|
111
|
+
ctx.serviceHealth?.reportFailure(
|
|
112
|
+
new Error(`Embassys Ambassador model execution failed (${classification})`),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
if (activeRunController === controller) activeRunController = undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
},
|
|
121
|
+
async stop() {
|
|
122
|
+
acceptingWork = false;
|
|
123
|
+
workQueue.close();
|
|
124
|
+
activeRunController?.abort();
|
|
125
|
+
await serviceLoop;
|
|
126
|
+
serviceLoop = undefined;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
api.registerHttpRoute({
|
|
131
|
+
path: "/embassys/ambassador",
|
|
132
|
+
auth: "plugin",
|
|
133
|
+
match: "exact",
|
|
134
|
+
async handler(req, res) {
|
|
135
|
+
const body = await readBody(req);
|
|
136
|
+
if (body === undefined) return response(res, 413);
|
|
137
|
+
const resolvedSecret = await resolveConfiguredSecretInputString({
|
|
138
|
+
config: api.runtime.config.current(),
|
|
139
|
+
env: process.env,
|
|
140
|
+
value: secret,
|
|
141
|
+
path: "plugins.entries.embassys-ambassador.config.secret",
|
|
142
|
+
unresolvedReasonStyle: "generic",
|
|
143
|
+
}).catch(() => ({}));
|
|
144
|
+
if (typeof resolvedSecret.value !== "string" || !SECRET.test(resolvedSecret.value)) {
|
|
145
|
+
return response(res, 503);
|
|
146
|
+
}
|
|
147
|
+
const verification = verifyAmbassadorWebhook({
|
|
148
|
+
method: req.method ?? "",
|
|
149
|
+
headers: requestHeaders(req),
|
|
150
|
+
body,
|
|
151
|
+
secret: resolvedSecret.value,
|
|
152
|
+
nowSeconds: Math.floor(Date.now() / 1_000),
|
|
153
|
+
});
|
|
154
|
+
if (!verification.ok) return response(res, verification.status);
|
|
155
|
+
|
|
156
|
+
const requestId = requestHeaders(req).get("idempotency-key");
|
|
157
|
+
if (requestId === null) return response(res, 400);
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
for (const [id, expiresAt] of receipts) {
|
|
160
|
+
if (expiresAt <= now) receipts.delete(id);
|
|
161
|
+
}
|
|
162
|
+
if (receipts.has(requestId)) return response(res, 202);
|
|
163
|
+
if (!acceptingWork || receipts.size >= MAX_RECEIPTS) return response(res, 503);
|
|
164
|
+
const prompt = buildAmbassadorPrompt(verification.message);
|
|
165
|
+
if (!workQueue.enqueue({ requestId, prompt })) return response(res, 503);
|
|
166
|
+
receipts.set(requestId, now + RECEIPT_TTL_MS);
|
|
167
|
+
return response(res, 202);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "embassys-ambassador",
|
|
3
|
+
"name": "Embassys Ambassador",
|
|
4
|
+
"description": "Accepts authenticated Ambassador webhooks and starts an OpenClaw model turn.",
|
|
5
|
+
"activation": {
|
|
6
|
+
"onStartup": true
|
|
7
|
+
},
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"$defs": {
|
|
11
|
+
"secretRef": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"source": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["env", "file", "exec", "store"]
|
|
18
|
+
},
|
|
19
|
+
"provider": {
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
22
|
+
"id": {
|
|
23
|
+
"type": "string"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"required": ["source", "provider", "id"]
|
|
27
|
+
},
|
|
28
|
+
"secretInput": {
|
|
29
|
+
"anyOf": [
|
|
30
|
+
{
|
|
31
|
+
"type": "string",
|
|
32
|
+
"pattern": "^[a-f0-9]{48}$"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"$ref": "#/$defs/secretRef"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"properties": {
|
|
41
|
+
"secret": {
|
|
42
|
+
"$ref": "#/$defs/secretInput"
|
|
43
|
+
},
|
|
44
|
+
"agentId": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"minLength": 1,
|
|
47
|
+
"maxLength": 128
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": false
|
|
51
|
+
},
|
|
52
|
+
"configContracts": {
|
|
53
|
+
"secretInputs": {
|
|
54
|
+
"paths": [
|
|
55
|
+
{
|
|
56
|
+
"path": "secret",
|
|
57
|
+
"expected": "string",
|
|
58
|
+
"ownerKind": "route"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"uiHints": {
|
|
64
|
+
"secret": {
|
|
65
|
+
"label": "Ambassador webhook secret",
|
|
66
|
+
"sensitive": true
|
|
67
|
+
},
|
|
68
|
+
"agentId": {
|
|
69
|
+
"label": "OpenClaw agent ID",
|
|
70
|
+
"placeholder": "main"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface AmbassadorWebhookVerificationInput {
|
|
2
|
+
readonly method: string;
|
|
3
|
+
readonly headers: Headers;
|
|
4
|
+
readonly body: Uint8Array;
|
|
5
|
+
readonly secret: string;
|
|
6
|
+
readonly nowSeconds: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type AmbassadorWebhookVerification =
|
|
10
|
+
| { readonly ok: true; readonly message: Record<string, unknown> }
|
|
11
|
+
| { readonly ok: false; readonly status: 400 | 401 | 405 | 413 };
|
|
12
|
+
|
|
13
|
+
export function verifyAmbassadorWebhook(
|
|
14
|
+
input: AmbassadorWebhookVerificationInput,
|
|
15
|
+
): AmbassadorWebhookVerification;
|
|
16
|
+
|
|
17
|
+
export function buildAmbassadorPrompt(message: Record<string, unknown>): string;
|
|
18
|
+
|
|
19
|
+
export type OpenClawExecutionErrorClassification =
|
|
20
|
+
| "plugin_runtime_scope"
|
|
21
|
+
| "session_admission"
|
|
22
|
+
| "plugin_admission"
|
|
23
|
+
| "model_start"
|
|
24
|
+
| "workspace"
|
|
25
|
+
| "configuration"
|
|
26
|
+
| "unknown";
|
|
27
|
+
|
|
28
|
+
export function classifyOpenClawExecutionError(
|
|
29
|
+
error: unknown,
|
|
30
|
+
): OpenClawExecutionErrorClassification;
|
|
31
|
+
|
|
32
|
+
export interface OpenClawQueuedWork {
|
|
33
|
+
readonly requestId: string;
|
|
34
|
+
readonly prompt: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BoundedOpenClawWorkQueue<T> {
|
|
38
|
+
enqueue(value: T): boolean;
|
|
39
|
+
next(): Promise<T | undefined>;
|
|
40
|
+
close(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createBoundedOpenClawWorkQueue<T>(capacity: number): BoundedOpenClawWorkQueue<T>;
|