@fastagent-sh/fastagent 0.21.1 → 0.22.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 +9 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -40
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +233 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +166 -229
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- package/dist/schedule/schedule.js +0 -23
package/dist/channels/busy.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SHARED process-wide in-flight work signal. Channels ACK a webhook fast and run the turn
|
|
3
|
-
* fire-and-forget on this process's event loop (serve.ts) — so "is this process busy?" is not
|
|
4
|
-
* derivable from open HTTP requests. The two shared execution primitives (turn-queue chains,
|
|
5
|
-
* task-tracker side tasks) report here; a serving surface that must stay alive while background
|
|
6
|
-
* work runs (the AgentCore adapter's /ping → HealthyBusy) reads it.
|
|
7
|
-
*
|
|
8
|
-
* Deliberately a counter, not a registry: consumers need only "any work in flight?"; keeping the
|
|
9
|
-
* module dependency-free lets both channel primitives import it without cycles.
|
|
10
|
-
*/
|
|
1
|
+
/** Webhooks ACK before background turns finish, so open requests cannot determine runtime idleness. */
|
|
11
2
|
let inFlight = 0;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Mark one unit of background work as started. Returns its completion callback — idempotent, so a
|
|
15
|
-
* caller may safely settle it from multiple cleanup paths (finally + catch) without double-counting.
|
|
16
|
-
*/
|
|
3
|
+
/** Mark background work as started. */
|
|
17
4
|
export function beginWork() {
|
|
18
5
|
inFlight += 1;
|
|
19
6
|
let done = false;
|
|
@@ -22,32 +9,8 @@ export function beginWork() {
|
|
|
22
9
|
return;
|
|
23
10
|
done = true;
|
|
24
11
|
inFlight -= 1;
|
|
25
|
-
if (inFlight > 0)
|
|
26
|
-
return;
|
|
27
|
-
for (const listener of idleListeners) {
|
|
28
|
-
// A listener fault must not corrupt the counter or starve the others (this runs inside a
|
|
29
|
-
// channel's `finally`): report and carry on.
|
|
30
|
-
try {
|
|
31
|
-
listener();
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
console.error(`[fastagent] idle listener failed: ${String(e)}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Subscribe to the 0-in-flight edge: the moment the process finishes its last background turn.
|
|
41
|
-
* Returns an unsubscribe. Used by the AgentCore adapter to push its state snapshot exactly when the
|
|
42
|
-
* state root has settled and before the platform may reclaim the idle microVM.
|
|
43
|
-
*/
|
|
44
|
-
export function onIdle(listener) {
|
|
45
|
-
idleListeners.add(listener);
|
|
46
|
-
return () => {
|
|
47
|
-
idleListeners.delete(listener);
|
|
48
12
|
};
|
|
49
13
|
}
|
|
50
|
-
/** How many units of background work are currently in flight (0 = idle). */
|
|
51
14
|
export function activeWork() {
|
|
52
15
|
return inFlight;
|
|
53
16
|
}
|
|
@@ -1,76 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The session control plane over HTTP + SSE (docs/design/session-control.md §13). Engine-neutral:
|
|
3
|
-
* consumes only the `SessionControl` contract. One transport serves every remote consumer (Web
|
|
4
|
-
* panel, desktop app, `fastagent attach`); the embedded API stays semantic-only and the ENVELOPE
|
|
5
|
-
* lives here: `id` (request correlation — implicit in HTTP), `epoch` (serving-process incarnation
|
|
6
|
-
* — INFORMATIONAL, for consumers correlating across connections; a restart surfaces as its
|
|
7
|
-
* connections dropping, so no one fences on it), `seq` (per-connection monotonic, detects loss in
|
|
8
|
-
* transit).
|
|
9
|
-
*
|
|
10
|
-
* SECURITY: these routes steer and stop runs, rewrite session properties, and delete records — a
|
|
11
|
-
* remote-control surface. The bearer token is REQUIRED (there is no unauthenticated mode) and is the only auth the framework owns; anything
|
|
12
|
-
* beyond a shared secret (principals, per-permission split, audit) is the wrapping host's job
|
|
13
|
-
* (design §14). Locally the serving process generates a per-boot token and writes it to
|
|
14
|
-
* `<stateRoot>/control.json` for local discovery — filesystem permissions guard the token, and the
|
|
15
|
-
* token guards the routes. That premise is shared-filesystem: a deployment breaks it, so there the
|
|
16
|
-
* DEPLOYER supplies the token ({@link CONTROL_TOKEN_ENV}) and both sides know it. How far those routes REACH is the bind address: all interfaces by
|
|
17
|
-
* default (containers require it), so the port is LAN-reachable and the mount warns accordingly —
|
|
18
|
-
* `--bind 127.0.0.1` (or `http.host`) closes exactly that reach, and the warning goes quiet because
|
|
19
|
-
* there is none left to state.
|
|
20
|
-
*/
|
|
21
|
-
import type { Agent } from "../agent.ts";
|
|
22
1
|
import { type SessionControl, type SessionEvent } from "../session.ts";
|
|
23
2
|
import { type PrefixMount } from "./serve.ts";
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
3
|
+
/** The prefix this plane OWNS: everything under it is the plane's to answer. */
|
|
4
|
+
export declare const CONTROL_PREFIX = "/control";
|
|
5
|
+
/**
|
|
6
|
+
* A plane handler: the request, its URL (for query parameters), and the session id the path named (`""` where the
|
|
7
|
+
* path has none).
|
|
8
|
+
*/
|
|
9
|
+
type PlaneHandler = (req: Request, url: URL, session: string) => Response | Promise<Response>;
|
|
10
|
+
/**
|
|
11
|
+
* The plane's route table: `"<METHOD> <path>"` → handler, where at most one path segment is {@link SESSION_SEGMENT}.
|
|
12
|
+
*/
|
|
28
13
|
export type PlaneRoutes = Record<string, PlaneHandler>;
|
|
29
|
-
/** The token, when the DEPLOYER owns it rather than the box (`mountSessionControl` reads it, `deploy`
|
|
30
|
-
* carries it). Declared here with the prefix because both are the plane's public names: the serving
|
|
31
|
-
* side and the deploy side must spell it identically, and a rename that hits only one of them fails
|
|
32
|
-
* silently — the box mints its own and every caller the runbook told gets a 401. */
|
|
33
|
-
export declare const CONTROL_TOKEN_ENV = "FASTAGENT_CONTROL_TOKEN";
|
|
34
14
|
/** The SSE payload: one control-plane event in its transport envelope. */
|
|
35
15
|
export interface WireEvent {
|
|
36
16
|
sessionId: string;
|
|
37
|
-
/** Serving-process incarnation (per `createControlPlane` call).
|
|
38
|
-
* live continuity is gone — run the reconnect steps (entries cursor + state). */
|
|
17
|
+
/** Serving-process incarnation (per `createControlPlane` call). */
|
|
39
18
|
epoch: string;
|
|
40
|
-
/** Per-connection monotonic counter.
|
|
19
|
+
/** Per-connection monotonic counter. */
|
|
41
20
|
seq: number;
|
|
42
21
|
event: SessionEvent;
|
|
43
22
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* remote-control endpoint must not be constructible by omission. */
|
|
47
|
-
token: string;
|
|
48
|
-
/** The DATA plane over the wire: when provided, `POST /control/invoke` mounts the standard
|
|
49
|
-
* invoke handler behind the same bearer token — a remote client (Web panel, desktop app,
|
|
50
|
-
* `attach`) can START runs regardless of which channels occupy `/invoke`. Same contract, same
|
|
51
|
-
* SSE event stream; disconnecting the response cancels the run (SPEC cancellation). */
|
|
52
|
-
agent?: Agent;
|
|
53
|
-
}
|
|
23
|
+
/** Create the control plane as a mountable prefix owner. */
|
|
24
|
+
export declare function createControlPlane(control: SessionControl): PrefixMount;
|
|
54
25
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* stream are sub-resources, and the things that HAPPEN to a run are posted to `…/actions`.
|
|
58
|
-
* The plane OWNS the prefix: it answers its own 404/405/preflight and puts CORS headers on every
|
|
59
|
-
* reply — see {@link planeApp}.
|
|
26
|
+
* Mount a plane route table as a {@link PrefixMount} — the plane owns a PREFIX, while a route table is a set of paths,
|
|
27
|
+
* at most one segment of which is a session id.
|
|
60
28
|
*/
|
|
61
|
-
export declare function createControlPlane(control: SessionControl, options: ControlPlaneOptions): PrefixMount;
|
|
62
|
-
/** Mount a plane route table as a {@link PrefixMount} — the plane owns a PREFIX, while a route
|
|
63
|
-
* table is a set of paths, at most one segment of which is a session id. */
|
|
64
29
|
export declare function mountControlPlane(routes: PlaneRoutes): PrefixMount;
|
|
65
|
-
|
|
66
|
-
* The plane's route table. Exported so the conformance sweeps derive their route list from what is
|
|
67
|
-
* actually mounted, rather than from a hand-kept copy that cannot notice a new route.
|
|
68
|
-
*
|
|
69
|
-
* The shape mirrors the contract: a collection, a resource, its sub-resources, and one action
|
|
70
|
-
* endpoint. What a session IS gets `GET`; what a session HAS gets `PATCH` (properties, last-wins);
|
|
71
|
-
* what happens TO a run gets `POST …/actions` (not a property — an event in time). `PUT` on a
|
|
72
|
-
* session id is the fork, and it is a PUT because a fork is idempotent by construction: the id is
|
|
73
|
-
* the caller's, the body says where the history came from, and repeating it changes nothing.
|
|
74
|
-
*/
|
|
75
|
-
export declare function controlPlaneRoutes(control: SessionControl, options: ControlPlaneOptions): PlaneRoutes;
|
|
30
|
+
export declare function controlPlaneRoutes(control: SessionControl): PlaneRoutes;
|
|
76
31
|
export {};
|
package/dist/channels/control.js
CHANGED
|
@@ -2,43 +2,17 @@ import { INVALID_COMMAND_CODE, SESSIONS_UNAVAILABLE_CODE, UNSUPPORTED_CAPABILITY
|
|
|
2
2
|
import { parseRouteKey, withoutBody } from "./serve.js";
|
|
3
3
|
import { log } from "../log.js";
|
|
4
4
|
import { readBodyCapped } from "./body.js";
|
|
5
|
-
import { MAX_BODY_BYTES
|
|
5
|
+
import { MAX_BODY_BYTES } from "./http.js";
|
|
6
6
|
import { sseResponse } from "./sse.js";
|
|
7
7
|
import { text } from "./respond.js";
|
|
8
|
-
import { secretEquals } from "./secret.js";
|
|
9
8
|
/** The prefix this plane OWNS: everything under it is the plane's to answer. */
|
|
10
|
-
const CONTROL_PREFIX = "/control";
|
|
11
|
-
/** The one variable segment in this plane's paths: a percent-encoded session id.
|
|
12
|
-
* keys so the table reads like the URLs it serves. */
|
|
9
|
+
export const CONTROL_PREFIX = "/control";
|
|
10
|
+
/** The one variable segment in this plane's paths: a percent-encoded session id. */
|
|
13
11
|
const SESSION_SEGMENT = "{session}";
|
|
14
|
-
/** The token, when the DEPLOYER owns it rather than the box (`mountSessionControl` reads it, `deploy`
|
|
15
|
-
* carries it). Declared here with the prefix because both are the plane's public names: the serving
|
|
16
|
-
* side and the deploy side must spell it identically, and a rename that hits only one of them fails
|
|
17
|
-
* silently — the box mints its own and every caller the runbook told gets a 401. */
|
|
18
|
-
export const CONTROL_TOKEN_ENV = "FASTAGENT_CONTROL_TOKEN";
|
|
19
12
|
const json = (value, status = 200) => new Response(`${JSON.stringify(value)}\n`, { status, headers: { "content-type": "application/json" } });
|
|
20
|
-
/**
|
|
21
|
-
* The plane as one mounted sub-application rather than routes sharing a prefix.
|
|
22
|
-
*
|
|
23
|
-
* CORS belongs to every reply that leaves the plane — including the ones no route produces (an
|
|
24
|
-
* unknown path, an unserved method, a throwing handler). As separate routes those came from the
|
|
25
|
-
* host, outside anything the plane could decorate. Owning the prefix makes them its own answers,
|
|
26
|
-
* headers applied at the single exit they share.
|
|
27
|
-
*
|
|
28
|
-
* `*` is the right origin: authorisation is the bearer token — never the origin, never a cookie —
|
|
29
|
-
* so an origin that cannot present it gets 401 either way, and a deployment cannot know the origins
|
|
30
|
-
* of the GUIs that will manage it (§14's asymmetry).
|
|
31
|
-
*
|
|
32
|
-
* `authorization` is not CORS-safelisted, so EVERY call preflights, including a plain GET.
|
|
33
|
-
* `content-type` is not either (only three values are, and `application/json` is not among them),
|
|
34
|
-
* so a browser POSTing to dispatch/invoke names it — allowing just `authorization` leaves precisely
|
|
35
|
-
* the WRITE routes unreachable while reads work.
|
|
36
|
-
*/
|
|
13
|
+
/** The plane as one mounted sub-application rather than routes sharing a prefix. */
|
|
37
14
|
function planeApp(routes) {
|
|
38
|
-
/** A route key's path split into segments, with `{session}` marked.
|
|
39
|
-
* SEGMENT rather than by regex because a session id is an opaque Caller string: percent-encoded
|
|
40
|
-
* it can contain anything, and `URL.pathname` leaves `%2F` encoded — so splitting on `/` cannot
|
|
41
|
-
* be fooled by an id that contains one. */
|
|
15
|
+
/** A route key's path split into segments, with `{session}` marked. */
|
|
42
16
|
const compiled = Object.entries(routes).map(([key, handler]) => {
|
|
43
17
|
const { method, path } = parseRouteKey(key);
|
|
44
18
|
return { method, path, segments: path.split("/"), handler };
|
|
@@ -54,8 +28,7 @@ function planeApp(routes) {
|
|
|
54
28
|
for (const [i, expected] of route.segments.entries()) {
|
|
55
29
|
const actual = segments[i];
|
|
56
30
|
if (expected === SESSION_SEGMENT) {
|
|
57
|
-
// The one place a path segment becomes a Caller id again.
|
|
58
|
-
// it would address a session no other call can name.
|
|
31
|
+
// The one place a path segment becomes a Caller id again.
|
|
59
32
|
if (actual === "") {
|
|
60
33
|
ok = false;
|
|
61
34
|
break;
|
|
@@ -64,11 +37,8 @@ function planeApp(routes) {
|
|
|
64
37
|
session = decodeURIComponent(actual);
|
|
65
38
|
}
|
|
66
39
|
catch {
|
|
67
|
-
// `%zz` and friends: not an id any client could have produced, so this path matches
|
|
68
|
-
//
|
|
69
|
-
// guards the handlers, so letting it throw would leave the boundary entirely — no CORS
|
|
70
|
-
// headers, no log line, and a rejected promise for an embedder mounting this handler
|
|
71
|
-
// directly. The query-parameter form this replaced decoded leniently and could not.
|
|
40
|
+
// `%zz` and friends: not an id any client could have produced, so this path matches nothing and falls
|
|
41
|
+
// through to the plane's own 404.
|
|
72
42
|
ok = false;
|
|
73
43
|
break;
|
|
74
44
|
}
|
|
@@ -83,32 +53,15 @@ function planeApp(routes) {
|
|
|
83
53
|
}
|
|
84
54
|
return hits;
|
|
85
55
|
};
|
|
86
|
-
// Per PATH, stating what it actually serves — omitting a method it does serve has the browser
|
|
87
|
-
// refuse a call that would have worked. `HEAD` is that case: every GET route answers it.
|
|
88
|
-
const allowMethods = (hits, requested) => {
|
|
89
|
-
const methods = new Set(hits.flatMap((h) => (h.route.method ? [h.route.method] : [])));
|
|
90
|
-
if (methods.has("GET"))
|
|
91
|
-
methods.add("HEAD");
|
|
92
|
-
// The requested method is always allowed, even where this path does not serve it: preflight is a
|
|
93
|
-
// gate applied BEFORE the request exists, so refusing there means the real request is never sent
|
|
94
|
-
// and the client sees an opaque network error. Allowing it lets the plane's own 404/405 arrive,
|
|
95
|
-
// with these headers and an explanation.
|
|
96
|
-
if (requested)
|
|
97
|
-
methods.add(requested.toUpperCase());
|
|
98
|
-
return [...methods, "OPTIONS"].join(", ");
|
|
99
|
-
};
|
|
100
56
|
return async (req) => {
|
|
101
|
-
const
|
|
57
|
+
const url = new URL(req.url);
|
|
58
|
+
const path = url.pathname;
|
|
102
59
|
const hits = match(path);
|
|
103
60
|
const answer = async () => {
|
|
104
|
-
// A preflight carries no token — that is its purpose — so it is answered before auth, and for
|
|
105
|
-
// ANY path under the prefix: gating it would stop the request the 404 below is waiting for.
|
|
106
|
-
if (req.method === "OPTIONS")
|
|
107
|
-
return new Response(null, { status: 204 });
|
|
108
61
|
const hit = hits.find((h) => h.route.method === req.method) ??
|
|
109
62
|
(req.method === "HEAD" ? hits.find((h) => h.route.method === "GET") : undefined);
|
|
110
63
|
if (hit)
|
|
111
|
-
return await hit.route.handler(req, hit.session ?? "");
|
|
64
|
+
return await hit.route.handler(req, url, hit.session ?? "");
|
|
112
65
|
// 404 vs 405 as in the host router: a client reads 404 as "this serve predates the route".
|
|
113
66
|
if (hits.length > 0)
|
|
114
67
|
return text("method not allowed\n", 405);
|
|
@@ -121,32 +74,27 @@ function planeApp(routes) {
|
|
|
121
74
|
res = req.method === "HEAD" ? withoutBody(answered) : answered;
|
|
122
75
|
}
|
|
123
76
|
catch (error) {
|
|
124
|
-
// The plane's own totality boundary: a rejecting handler (`commands()` on an unreadable
|
|
125
|
-
//
|
|
77
|
+
// The plane's own totality boundary: a rejecting handler (`commands()` on an unreadable definition) must still
|
|
78
|
+
// answer with the headers; the message stays internal.
|
|
126
79
|
log.error(`[control] ${req.method} ${path} failed: ${String(error)}`);
|
|
127
80
|
res = text("internal error\n", 500);
|
|
128
81
|
}
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
res.headers.set("access-control-allow-headers", "authorization, content-type");
|
|
132
|
-
res.headers.set("access-control-allow-methods", allowMethods(hits, req.headers.get("access-control-request-method")));
|
|
82
|
+
// CORS is NOT set here: the host router owns that verdict for every path it publishes, this prefix included
|
|
83
|
+
// (channels/serve.ts). A second writer of those headers is a second policy to keep in sync.
|
|
133
84
|
return res;
|
|
134
85
|
};
|
|
135
86
|
}
|
|
136
|
-
// ONE constant for every Prompt-bearing wire surface (imported from the invoke channel — the two
|
|
137
|
-
//
|
|
87
|
+
// ONE constant for every Prompt-bearing wire surface (imported from the invoke channel — the two caps cannot drift
|
|
88
|
+
// apart).
|
|
138
89
|
const ACTION_BODY_LIMIT = MAX_BODY_BYTES;
|
|
139
90
|
/**
|
|
140
|
-
* Parse-don't-validate at the wire: a remote client can send any JSON, and the hub's inner layers
|
|
141
|
-
*
|
|
142
|
-
* `run_command_failed`). Returns the typed action, or undefined for anything malformed — which
|
|
143
|
-
* answers protocol-level `invalid_command`, same responsibility as the hub's unknown-type default.
|
|
91
|
+
* Parse-don't-validate at the wire: a remote client can send any JSON, and the hub's inner layers trust action shapes
|
|
92
|
+
* (a malformed `steer` would surface as an ENGINE failure misclassified as `run_command_failed`).
|
|
144
93
|
*/
|
|
145
94
|
function parseWireAction(raw) {
|
|
146
|
-
// COMPILE-TIME drift guard, variant level: this switch hand-mirrors the SessionAction union, and a
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// line first, forcing the decision of how the wire carries it.
|
|
95
|
+
// COMPILE-TIME drift guard, variant level: this switch hand-mirrors the SessionAction union, and a new variant
|
|
96
|
+
// added in session.ts would otherwise compile clean while the wire answers it `invalid_command` — silently breaking
|
|
97
|
+
// local/remote isomorphism.
|
|
150
98
|
const _actionDriftGuard = {
|
|
151
99
|
steer: true,
|
|
152
100
|
follow_up: true,
|
|
@@ -167,12 +115,12 @@ function parseWireAction(raw) {
|
|
|
167
115
|
if (typeof p.text !== "string")
|
|
168
116
|
return false;
|
|
169
117
|
const images = p.images;
|
|
170
|
-
// Element-level: `images: [42]` reaching the engine would resurface exactly the misclassified
|
|
171
|
-
//
|
|
118
|
+
// Element-level: `images: [42]` reaching the engine would resurface exactly the misclassified failure this parser
|
|
119
|
+
// exists to prevent (ImageRef shape from src/session.ts's Prompt).
|
|
172
120
|
return images === undefined || (Array.isArray(images) && images.every(imageOk));
|
|
173
121
|
};
|
|
174
|
-
// REBUILD, never pass raw through: "typed out" must be construction, not assertion — a passed-
|
|
175
|
-
//
|
|
122
|
+
// REBUILD, never pass raw through: "typed out" must be construction, not assertion — a passed-through object would
|
|
123
|
+
// carry arbitrary extra keys into the engine.
|
|
176
124
|
const rebuildPrompt = (p) => {
|
|
177
125
|
const images = p.images;
|
|
178
126
|
return {
|
|
@@ -181,12 +129,9 @@ function parseWireAction(raw) {
|
|
|
181
129
|
};
|
|
182
130
|
};
|
|
183
131
|
// COMPILE-TIME drift guard: whitelist reconstruction silently strips any field it does not know.
|
|
184
|
-
// A new Prompt field must break THIS line (non-empty Exclude → {} unassignable), not vanish on
|
|
185
|
-
// the wire while the client believes it was sent.
|
|
186
132
|
const _promptDriftGuard = {};
|
|
187
133
|
void _promptDriftGuard;
|
|
188
|
-
// Same guard one level down: the image whitelist ({data, mimeType}) must break when ImageRef
|
|
189
|
-
// grows a field — top-level coverage alone would let element fields vanish silently.
|
|
134
|
+
// Same guard one level down: the image whitelist ({data, mimeType}) must break when ImageRef grows a field.
|
|
190
135
|
const _imageDriftGuard = {};
|
|
191
136
|
void _imageDriftGuard;
|
|
192
137
|
switch (c.type) {
|
|
@@ -204,12 +149,8 @@ function parseWireAction(raw) {
|
|
|
204
149
|
}
|
|
205
150
|
}
|
|
206
151
|
/**
|
|
207
|
-
* Parse a PATCH body into a session update — or into the REASON it is not one, because the two
|
|
208
|
-
*
|
|
209
|
-
* field (a newer client talking to an older serve, or a typo), and the client's move is to drop it:
|
|
210
|
-
* `unsupported_capability`, the same code the in-process path answers, so the two planes stay
|
|
211
|
-
* isomorphic. A wrong VALUE TYPE is a malformed payload: `invalid_command`. Neither is dropped
|
|
212
|
-
* silently, which would answer `ok: true` for a patch that set nothing.
|
|
152
|
+
* Parse a PATCH body into a session update — or into the REASON it is not one, because the two reasons are different
|
|
153
|
+
* answers to the client.
|
|
213
154
|
*/
|
|
214
155
|
function parseWireUpdate(raw) {
|
|
215
156
|
if (typeof raw !== "object" || raw === null) {
|
|
@@ -224,8 +165,8 @@ function parseWireUpdate(raw) {
|
|
|
224
165
|
};
|
|
225
166
|
}
|
|
226
167
|
const patch = {};
|
|
227
|
-
// The field list is the CONTRACT's (`UPDATE_FIELDS`), not a copy: a field added to SessionUpdate
|
|
228
|
-
//
|
|
168
|
+
// The field list is the CONTRACT's (`UPDATE_FIELDS`), not a copy: a field added to SessionUpdate travels here
|
|
169
|
+
// without anyone remembering to, and one removed cannot linger.
|
|
229
170
|
for (const field of UPDATE_FIELDS) {
|
|
230
171
|
const value = c[field];
|
|
231
172
|
if (value === undefined)
|
|
@@ -237,63 +178,36 @@ function parseWireUpdate(raw) {
|
|
|
237
178
|
}
|
|
238
179
|
return { patch };
|
|
239
180
|
}
|
|
181
|
+
/** Create the control plane as a mountable prefix owner. */
|
|
182
|
+
export function createControlPlane(control) {
|
|
183
|
+
return mountControlPlane(controlPlaneRoutes(control));
|
|
184
|
+
}
|
|
240
185
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* stream are sub-resources, and the things that HAPPEN to a run are posted to `…/actions`.
|
|
244
|
-
* The plane OWNS the prefix: it answers its own 404/405/preflight and puts CORS headers on every
|
|
245
|
-
* reply — see {@link planeApp}.
|
|
186
|
+
* Mount a plane route table as a {@link PrefixMount} — the plane owns a PREFIX, while a route table is a set of paths,
|
|
187
|
+
* at most one segment of which is a session id.
|
|
246
188
|
*/
|
|
247
|
-
export function createControlPlane(control, options) {
|
|
248
|
-
return mountControlPlane(controlPlaneRoutes(control, options));
|
|
249
|
-
}
|
|
250
|
-
/** Mount a plane route table as a {@link PrefixMount} — the plane owns a PREFIX, while a route
|
|
251
|
-
* table is a set of paths, at most one segment of which is a session id. */
|
|
252
189
|
export function mountControlPlane(routes) {
|
|
253
190
|
return { prefix: CONTROL_PREFIX, handler: planeApp(routes) };
|
|
254
191
|
}
|
|
255
|
-
|
|
256
|
-
* The plane's route table. Exported so the conformance sweeps derive their route list from what is
|
|
257
|
-
* actually mounted, rather than from a hand-kept copy that cannot notice a new route.
|
|
258
|
-
*
|
|
259
|
-
* The shape mirrors the contract: a collection, a resource, its sub-resources, and one action
|
|
260
|
-
* endpoint. What a session IS gets `GET`; what a session HAS gets `PATCH` (properties, last-wins);
|
|
261
|
-
* what happens TO a run gets `POST …/actions` (not a property — an event in time). `PUT` on a
|
|
262
|
-
* session id is the fork, and it is a PUT because a fork is idempotent by construction: the id is
|
|
263
|
-
* the caller's, the body says where the history came from, and repeating it changes nothing.
|
|
264
|
-
*/
|
|
265
|
-
export function controlPlaneRoutes(control, options) {
|
|
266
|
-
const { token } = options;
|
|
267
|
-
if (!token)
|
|
268
|
-
throw new Error("createControlPlane: a bearer token is required (empty tokens are not a mode)");
|
|
192
|
+
export function controlPlaneRoutes(control) {
|
|
269
193
|
const epoch = crypto.randomUUID();
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* query parameters), and the session the PATH named — `""` on the routes that have no id in
|
|
277
|
-
* them, which those handlers never read. */
|
|
278
|
-
const guard = (handler) => (req, session) => {
|
|
279
|
-
if (!authed(req))
|
|
280
|
-
return text("unauthorized\n", 401);
|
|
281
|
-
return handler(req, new URL(req.url), session);
|
|
282
|
-
};
|
|
283
|
-
/** Read a JSON body under the shared cap. Answers the Response to send on failure, so a route can
|
|
284
|
-
* `if ("error" in read) return read.error`. */
|
|
194
|
+
/**
|
|
195
|
+
* Read a JSON body under the shared cap.
|
|
196
|
+
*
|
|
197
|
+
* No content-type gate here: this plane only ever rides the host router, which applies it to every route that
|
|
198
|
+
* authenticates nobody — this whole prefix included (channels/serve.ts).
|
|
199
|
+
*/
|
|
285
200
|
const readJson = async (req) => {
|
|
286
201
|
const body = await readBodyCapped(req, ACTION_BODY_LIMIT);
|
|
287
|
-
// The 413 names the ceiling: the docs promise images on this plane, and an unexplained
|
|
288
|
-
//
|
|
289
|
-
// constant — a hardcoded "1 MiB" would lie the day the cap changes.
|
|
202
|
+
// The 413 names the ceiling: the docs promise images on this plane, and an unexplained rejection would send a
|
|
203
|
+
// client author hunting everywhere but the cap.
|
|
290
204
|
if ("tooLarge" in body) {
|
|
291
205
|
return {
|
|
292
206
|
error: text(`body too large (limit ${MAX_BODY_BYTES >> 20} MiB — images count base64-inflated)\n`, 413),
|
|
293
207
|
};
|
|
294
208
|
}
|
|
295
|
-
// An empty body is an empty object: `POST …/actions` always carries one, but `PATCH` with
|
|
296
|
-
//
|
|
209
|
+
// An empty body is an empty object: `POST …/actions` always carries one, but `PATCH` with nothing to set is a
|
|
210
|
+
// legal no-op and a client should not have to send `{}` to say so.
|
|
297
211
|
if (body.text.trim() === "")
|
|
298
212
|
return { value: {} };
|
|
299
213
|
try {
|
|
@@ -304,79 +218,57 @@ export function controlPlaneRoutes(control, options) {
|
|
|
304
218
|
}
|
|
305
219
|
};
|
|
306
220
|
return {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
"GET /control/capabilities": guard(() => json(control.capabilities())),
|
|
312
|
-
"GET /control/commands": guard(async () => json(await control.commands())),
|
|
313
|
-
// The DEPLOYMENT's conversation list — and the one read that may fail: `[]` is what an empty
|
|
314
|
-
// deployment answers, so a store that cannot be enumerated gets a coded non-2xx instead. 503 +
|
|
315
|
-
// the code, because the alternative (#309's lesson) is a client that can only classify a bare
|
|
316
|
-
// 500 as "the endpoint is unreachable" and burns its reconnect budget on a condition
|
|
317
|
-
// reconnecting cannot fix.
|
|
318
|
-
"GET /control/sessions": guard(async () => {
|
|
221
|
+
"GET /control/capabilities": () => json(control.capabilities()),
|
|
222
|
+
"GET /control/commands": async () => json(await control.commands()),
|
|
223
|
+
// The DEPLOYMENT's conversation list — and the one read that may fail.
|
|
224
|
+
"GET /control/sessions": async () => {
|
|
319
225
|
try {
|
|
320
226
|
return json(await control.sessions.list());
|
|
321
227
|
}
|
|
322
228
|
catch (error) {
|
|
323
|
-
// ONLY a store fault becomes the retryable code.
|
|
324
|
-
// bug, and answering `retryable: true` for it has a client poll forever on something no
|
|
325
|
-
// retry can fix — so it goes back to the plane's totality boundary, which logs it and
|
|
326
|
-
// answers 500. An IO error carries an ERRNO `code` (EACCES, ENOTDIR); that is the shape of a
|
|
327
|
-
// condition the operator can act on.
|
|
328
|
-
//
|
|
329
|
-
// The errno SHAPE, not merely "has a string code": Node's own argument-validation errors are
|
|
330
|
-
// TypeErrors carrying `ERR_INVALID_ARG_TYPE`/`ERR_OUT_OF_RANGE`, so a bug in row building
|
|
331
|
-
// would otherwise be handed to a client as retryable — the exact case this excludes. Read
|
|
332
|
-
// through `?.` because a thrown null must reach the boundary as itself, not as a TypeError
|
|
333
|
-
// from this line that replaces it in the log.
|
|
229
|
+
// ONLY a store fault becomes the retryable code.
|
|
334
230
|
const code = error?.code;
|
|
335
231
|
if (typeof code !== "string" || !/^E[A-Z]+$/.test(code))
|
|
336
232
|
throw error;
|
|
337
|
-
// Logged as well as answered: the catch would otherwise be the one place a store fault is
|
|
338
|
-
//
|
|
233
|
+
// Logged as well as answered: the catch would otherwise be the one place a store fault is invisible on the
|
|
234
|
+
// server, since it preempts the boundary that does the logging.
|
|
339
235
|
log.error(`[control] GET /control/sessions failed: ${String(error)}`);
|
|
340
236
|
return json({ code: SESSIONS_UNAVAILABLE_CODE, message: String(error), retryable: true }, 503);
|
|
341
237
|
}
|
|
342
|
-
}
|
|
238
|
+
},
|
|
343
239
|
// PUT, because a fork is idempotent: this id, holding the history that was at `from`@`at`.
|
|
344
|
-
|
|
345
|
-
// conflict, not an overwrite.
|
|
346
|
-
[`PUT /control/sessions/${SESSION_SEGMENT}`]: guard(async (req, _url, session) => {
|
|
240
|
+
[`PUT /control/sessions/${SESSION_SEGMENT}`]: async (req, _url, session) => {
|
|
347
241
|
const read = await readJson(req);
|
|
348
242
|
if ("error" in read)
|
|
349
243
|
return read.error;
|
|
350
|
-
// `JSON.parse("null")` is null, and a body is whatever the client sent: reaching into it
|
|
351
|
-
//
|
|
244
|
+
// `JSON.parse("null")` is null, and a body is whatever the client sent: reaching into it unguarded turns a
|
|
245
|
+
// malformed request into a 500 the client cannot act on.
|
|
352
246
|
const body = read.value;
|
|
353
247
|
if (typeof body?.from !== "string" || typeof body.at !== "string") {
|
|
354
248
|
return text("expected { from: string, at: string }\n", 400);
|
|
355
249
|
}
|
|
356
250
|
return json(await control.sessions.fork({ from: body.from, at: body.at, into: session }));
|
|
357
|
-
}
|
|
358
|
-
[`GET /control/sessions/${SESSION_SEGMENT}`]:
|
|
251
|
+
},
|
|
252
|
+
[`GET /control/sessions/${SESSION_SEGMENT}`]: async (_req, _url, session) => json(await control.sessions.get(session).state()),
|
|
359
253
|
// PATCH, because these are session PROPERTIES: last-wins, durable, applied by the next turn.
|
|
360
|
-
[`PATCH /control/sessions/${SESSION_SEGMENT}`]:
|
|
254
|
+
[`PATCH /control/sessions/${SESSION_SEGMENT}`]: async (req, _url, session) => {
|
|
361
255
|
const read = await readJson(req);
|
|
362
256
|
if ("error" in read)
|
|
363
257
|
return read.error;
|
|
364
258
|
const parsed = parseWireUpdate(read.value);
|
|
365
|
-
// A protocol-level answer carrying the SAME code the hub would have used
|
|
366
|
-
// be where a client loses the difference between "drop that field" and "fix that value".
|
|
259
|
+
// A protocol-level answer carrying the SAME code the hub would have used.
|
|
367
260
|
if (!("patch" in parsed)) {
|
|
368
261
|
return json({ ok: false, error: { ...parsed, retryable: false } });
|
|
369
262
|
}
|
|
370
263
|
return json(await control.sessions.get(session).update(parsed.patch));
|
|
371
|
-
}
|
|
372
|
-
[`DELETE /control/sessions/${SESSION_SEGMENT}`]:
|
|
373
|
-
[`GET /control/sessions/${SESSION_SEGMENT}/entries`]:
|
|
264
|
+
},
|
|
265
|
+
[`DELETE /control/sessions/${SESSION_SEGMENT}`]: async (_req, _url, session) => json(await control.sessions.get(session).delete()),
|
|
266
|
+
[`GET /control/sessions/${SESSION_SEGMENT}/entries`]: async (_req, url, session) => {
|
|
374
267
|
const since = url.searchParams.get("since") ?? undefined;
|
|
375
268
|
return json(await control.sessions.get(session).entries(since !== undefined ? { since } : undefined));
|
|
376
|
-
}
|
|
377
|
-
// The run actions.
|
|
378
|
-
|
|
379
|
-
[`POST /control/sessions/${SESSION_SEGMENT}/actions`]: guard(async (req, _url, session) => {
|
|
269
|
+
},
|
|
270
|
+
// The run actions.
|
|
271
|
+
[`POST /control/sessions/${SESSION_SEGMENT}/actions`]: async (req, _url, session) => {
|
|
380
272
|
const read = await readJson(req);
|
|
381
273
|
if ("error" in read)
|
|
382
274
|
return read.error;
|
|
@@ -388,8 +280,8 @@ export function controlPlaneRoutes(control, options) {
|
|
|
388
280
|
});
|
|
389
281
|
}
|
|
390
282
|
const s = control.sessions.get(session);
|
|
391
|
-
// The result rides HTTP 200 either way: `ok: false` is a protocol-level answer (rejected
|
|
392
|
-
//
|
|
283
|
+
// The result rides HTTP 200 either way: `ok: false` is a protocol-level answer (rejected before acceptance),
|
|
284
|
+
// not a transport failure.
|
|
393
285
|
switch (action.type) {
|
|
394
286
|
case "steer":
|
|
395
287
|
return json(await s.steer(action.prompt));
|
|
@@ -400,8 +292,8 @@ export function controlPlaneRoutes(control, options) {
|
|
|
400
292
|
case "compact":
|
|
401
293
|
return json(await s.compact(action.instructions !== undefined ? { instructions: action.instructions } : {}));
|
|
402
294
|
}
|
|
403
|
-
}
|
|
404
|
-
[`GET /control/sessions/${SESSION_SEGMENT}/events`]:
|
|
295
|
+
},
|
|
296
|
+
[`GET /control/sessions/${SESSION_SEGMENT}/events`]: (_req, _url, session) => {
|
|
405
297
|
let seq = 0;
|
|
406
298
|
return sseResponse(control.sessions.get(session).events(), (event) => ({
|
|
407
299
|
sessionId: session,
|
|
@@ -409,6 +301,6 @@ export function controlPlaneRoutes(control, options) {
|
|
|
409
301
|
seq: seq++,
|
|
410
302
|
event,
|
|
411
303
|
}));
|
|
412
|
-
}
|
|
304
|
+
},
|
|
413
305
|
};
|
|
414
306
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineChannel` — a channel file's authoring surface, and the third member of the `defineX` family
|
|
3
|
+
* (with `defineTool` / `defineRoutine`). Its whole job is the secrets: a channel is the one code
|
|
4
|
+
* input that is ALL credentials (a bot token, a signing secret), and before this it read them from
|
|
5
|
+
* `process.env` at module scope, which meant a CUSTOM channel could not tell `deploy` what to carry —
|
|
6
|
+
* the deploy could only guess from a table of first-party names.
|
|
7
|
+
*
|
|
8
|
+
* The declaration attaches to the module WITHOUT adding a third module form to the Channel contract
|
|
9
|
+
* (channel.ts §7 fixes two: a function is a route channel, an object with `connect` is a long
|
|
10
|
+
* connection). The builder runs at import and returns one of those two; the declared names ride along
|
|
11
|
+
* as a property that the discovery reads and everything else ignores.
|
|
12
|
+
*
|
|
13
|
+
* Values are read at import, so a rotated credential needs a restart — the same lifetime an adapter
|
|
14
|
+
* configured with a literal already has, and the reason there is no per-request accessor here.
|
|
15
|
+
*
|
|
16
|
+
* KNOWN GAP: every declared name is REQUIRED (the gate refuses an empty value), so an
|
|
17
|
+
* OPTIONAL credential has no expression here — declare it and an agent that does not use it cannot
|
|
18
|
+
* start; leave it out and `deploy` does not carry it. First-party channels have the third answer in
|
|
19
|
+
* the scaffold table (`ChannelEnv.required`), which is why feishu/lark still read their optional
|
|
20
|
+
* `*_ENCRYPT_KEY` from the environment. Closing it means a second entry shape here
|
|
21
|
+
* (`{ name, required: false }`) carried through `DeclaredSecret`, `missingSecrets` and the deploy
|
|
22
|
+
* runbook's required flag — a contract change, not a local one.
|
|
23
|
+
*/
|
|
24
|
+
import type { ChannelModule, LongConnectionChannelModule } from "../channel.ts";
|
|
25
|
+
/** A channel module carrying what it declared it needs. Read by discovery; ignored by the server. */
|
|
26
|
+
type DeclaringChannelModule<M> = M & {
|
|
27
|
+
secrets?: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
export interface DefineChannelOptions<S extends readonly string[], M> {
|
|
30
|
+
/**
|
|
31
|
+
* Env vars this channel needs (`["TELEGRAM_BOT_TOKEN"]`), typed into `channel`. `deploy` carries
|
|
32
|
+
* them to the host and `dev`/`start` refuse to serve while one is unset, naming this file.
|
|
33
|
+
*/
|
|
34
|
+
secrets?: S;
|
|
35
|
+
/** Build the channel from those values — usually one call to an adapter (`telegramChannel({...})`). */
|
|
36
|
+
channel: (secrets: Record<S[number], string>) => M;
|
|
37
|
+
}
|
|
38
|
+
export declare function defineChannel<const S extends readonly string[] = readonly [], M extends ChannelModule | LongConnectionChannelModule = ChannelModule>(options: DefineChannelOptions<S, M>): DeclaringChannelModule<M>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { secretValues } from "../declared-secrets.js";
|
|
2
|
+
// `S` defaults to the EMPTY tuple for the same reason as `defineTool`: a channel that declares
|
|
3
|
+
// nothing must not type-check `secrets.ANYTHING`.
|
|
4
|
+
export function defineChannel(options) {
|
|
5
|
+
const built = options.channel(secretValues(options.secrets));
|
|
6
|
+
// Object.assign on a FUNCTION module keeps it a function (the route-channel form); on the
|
|
7
|
+
// long-connection object it adds one field. Either way the shape the server sees is unchanged.
|
|
8
|
+
return Object.assign(built, options.secrets ? { secrets: options.secrets } : {});
|
|
9
|
+
}
|