@fastagent-sh/fastagent 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* the platform reclaim the microVM (that idle-to-zero IS the point of this deployment).
|
|
27
27
|
*/
|
|
28
28
|
import { Buffer } from "node:buffer";
|
|
29
|
-
import {
|
|
29
|
+
import { ENVELOPE_KINDS } from "./agentcore-protocol.js";
|
|
30
30
|
import { beginWork, onIdle } from "./busy.js";
|
|
31
31
|
import { router } from "../channels/serve.js";
|
|
32
32
|
import { log } from "../log.js";
|
|
@@ -34,49 +34,84 @@ import { rememberWakeAlarmUrl } from "../schedule/wake-alarm.js";
|
|
|
34
34
|
import { readBodyCapped } from "./body.js";
|
|
35
35
|
import { createInvokeHandler } from "./http.js";
|
|
36
36
|
import { text } from "./respond.js";
|
|
37
|
+
import { secretEquals } from "./secret.js";
|
|
37
38
|
import { MAX_ENVELOPE_BYTES, MAX_WEBHOOK_BODY_BYTES } from "./agentcore-limits.js";
|
|
38
39
|
const unsnapshottedWarning = "[agentcore] this envelope carried no state-snapshot URLs — the state root is LOCAL ONLY and the " +
|
|
39
40
|
"platform erases it on the next deploy (redeploy with a current fastagent to restore durability)";
|
|
40
41
|
const jsonHeaders = { "content-type": "application/json" };
|
|
41
42
|
const json = (body, status) => new Response(`${JSON.stringify(body)}\n`, { status, headers: jsonHeaders });
|
|
42
|
-
/** Compare an untrusted envelope secret without leaking a matching-prefix timing signal. */
|
|
43
|
-
function secretMatches(actual, expected) {
|
|
44
|
-
if (typeof actual !== "string" || expected === undefined)
|
|
45
|
-
return false;
|
|
46
|
-
const actualBytes = Buffer.from(actual);
|
|
47
|
-
const expectedBytes = Buffer.from(expected);
|
|
48
|
-
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
49
|
-
}
|
|
50
43
|
/**
|
|
51
|
-
* Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* a local `curl` debug surface.
|
|
44
|
+
* Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }` — the whole of what the
|
|
45
|
+
* platform routes into the container. The agent's own channels are not beside these: they are a table
|
|
46
|
+
* inside the envelope dispatch below, reached only by unwrapping a forwarder envelope.
|
|
55
47
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The process's BOOT, deferred to ingress. A resident host restores its state, wires its sinks and
|
|
50
|
+
* constructs its channels at start-up; here nothing before the first trusted envelope is
|
|
51
|
+
* authoritative (the mount is pre-restore, and the snapshot URLs only an envelope carries), so those
|
|
52
|
+
* steps run against the first one and are asserted again on every later one. Two things happen ONCE
|
|
53
|
+
* per process, in this order: the post-restore hook (the wake-alarm reconcile, which at boot would
|
|
54
|
+
* read the mount the platform just wiped), and channel construction — whose outcome is cached EITHER
|
|
55
|
+
* WAY. Success: the same resident channels a direct host keeps. Failure too: construction is an
|
|
56
|
+
* ACTIVATION with side effects — loadChannels builds every healthy channel (starting its queues and
|
|
57
|
+
* replaying durable turn intent) before reporting another module's failure — and there is no cleanup
|
|
58
|
+
* contract to unwind it, so re-running it per envelope could replay the same recovered turn
|
|
59
|
+
* concurrently. The first rejection is the process's answer: every later envelope fails with the same
|
|
60
|
+
* message (visible each time), the retry boundary is a fresh session (which scale-to-zero provides
|
|
61
|
+
* naturally), and the deploy driver's probe catches deterministic failures at deploy time.
|
|
62
|
+
*/
|
|
63
|
+
function createActivation(deps) {
|
|
64
|
+
const { stateSync, stateRoot, onStateReady } = deps;
|
|
65
|
+
let warnedUnsnapshotted = false;
|
|
66
|
+
let stateReadyFired = false;
|
|
68
67
|
let dispatchP;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
return {
|
|
69
|
+
async restore(envelope) {
|
|
70
|
+
if (stateSync) {
|
|
71
|
+
if (envelope.state && typeof envelope.state.getUrl === "string" && typeof envelope.state.putUrl === "string") {
|
|
72
|
+
stateSync.use(envelope.state);
|
|
73
|
+
}
|
|
74
|
+
else if (envelope.kind !== "invoke" && !stateSync.configured() && !warnedUnsnapshotted) {
|
|
75
|
+
// webhook/schedule-fire/wake-poke/probe reach us ONLY through the forwarder, which mints the
|
|
76
|
+
// pair. Missing = a broken/stale topology whose state dies at the next deploy: say so, loudly,
|
|
77
|
+
// once per process (a direct `invoke` legitimately has none — its session storage is its own).
|
|
78
|
+
warnedUnsnapshotted = true;
|
|
79
|
+
log.warn(unsnapshottedWarning);
|
|
80
|
+
}
|
|
81
|
+
await stateSync.ready();
|
|
82
|
+
}
|
|
83
|
+
// The forwarder rides its public URL along on every envelope — persist it (write-if-changed) so
|
|
84
|
+
// the wake-alarm sink can call back. AFTER the restore: a stale snapshot copy must not win over
|
|
85
|
+
// the URL this deployment is actually reachable at. A bad persist must not fail the turn.
|
|
86
|
+
if (typeof envelope.wake?.url === "string") {
|
|
87
|
+
try {
|
|
88
|
+
rememberWakeAlarmUrl(stateRoot, envelope.wake.url);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
log.error(`[agentcore] could not persist the wake-alarm URL: ${String(e)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (onStateReady && !stateReadyFired) {
|
|
95
|
+
stateReadyFired = true;
|
|
96
|
+
onStateReady();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
channels() {
|
|
100
|
+
if (!dispatchP) {
|
|
101
|
+
// The factory runs INSIDE the chain: a synchronous throw must land in the cached rejection,
|
|
102
|
+
// not escape before `dispatchP` is assigned (which would silently re-run the activation).
|
|
103
|
+
dispatchP = Promise.resolve()
|
|
104
|
+
.then(deps.channels)
|
|
105
|
+
.then((surface) => router(surface.routes, surface.mounts));
|
|
106
|
+
dispatchP.catch(() => { }); // observed here so the CACHED rejection is never "unhandled"
|
|
107
|
+
}
|
|
108
|
+
return dispatchP;
|
|
109
|
+
},
|
|
79
110
|
};
|
|
111
|
+
}
|
|
112
|
+
export function agentcoreRoutes(options) {
|
|
113
|
+
const { channels, agent, stateRoot, isBusy, fire, stateSync, ingressSecret, onStateReady } = options;
|
|
114
|
+
const activation = createActivation({ stateSync, stateRoot, onStateReady, channels });
|
|
80
115
|
const invokeHandler = createInvokeHandler(agent);
|
|
81
116
|
// Snapshot on the 0-in-flight edge: webhook channels ACK fast and finish the turn in the
|
|
82
117
|
// background, so "the request returned" is NOT when the state root settles.
|
|
@@ -84,8 +119,6 @@ export function agentcoreRoutes(options) {
|
|
|
84
119
|
const off = onIdle(() => stateSync.save());
|
|
85
120
|
options.signal?.addEventListener("abort", off, { once: true });
|
|
86
121
|
}
|
|
87
|
-
let warnedUnsnapshotted = false;
|
|
88
|
-
let stateReadyFired = false;
|
|
89
122
|
const handleInvocation = async (req) => {
|
|
90
123
|
const body = await readBodyCapped(req, MAX_ENVELOPE_BYTES);
|
|
91
124
|
if ("tooLarge" in body)
|
|
@@ -98,7 +131,7 @@ export function agentcoreRoutes(options) {
|
|
|
98
131
|
return text("invalid json\n", 400);
|
|
99
132
|
}
|
|
100
133
|
if (envelope === null || typeof envelope !== "object" || typeof envelope.kind !== "string") {
|
|
101
|
-
return text(
|
|
134
|
+
return text(`need { "kind": ${ENVELOPE_KINDS.map((k) => `"${k}"`).join(" | ")}, ... }\n`, 400);
|
|
102
135
|
}
|
|
103
136
|
// AUTHENTICATION BOUNDARY. `InvokeAgentRuntime` is an ordinary IAM action, so "reached this
|
|
104
137
|
// handler" proves nothing about the sender. Only an envelope carrying the shared secret is the
|
|
@@ -106,7 +139,7 @@ export function agentcoreRoutes(options) {
|
|
|
106
139
|
// and may NOT carry internal fields — riding a `state` or `wake` URL on a public invoke would
|
|
107
140
|
// redirect the state snapshot (auth.json) or the alarm callback (the wake secret) to the caller.
|
|
108
141
|
// Internal fields are DROPPED rather than rejected: a public caller has no business knowing them.
|
|
109
|
-
const trusted =
|
|
142
|
+
const trusted = secretEquals(envelope.auth, ingressSecret);
|
|
110
143
|
if (!trusted) {
|
|
111
144
|
if (envelope.kind !== "invoke") {
|
|
112
145
|
log.warn(`[agentcore] rejected an unauthenticated "${envelope.kind}" envelope`);
|
|
@@ -115,67 +148,34 @@ export function agentcoreRoutes(options) {
|
|
|
115
148
|
envelope.wake = undefined;
|
|
116
149
|
envelope.state = undefined;
|
|
117
150
|
}
|
|
118
|
-
// The forwarder rides its public URL along on every envelope — persist it (write-if-changed) so
|
|
119
|
-
// the wake-alarm sink can call back. Written AFTER the restore below: a stale snapshot copy must
|
|
120
|
-
// not win over the URL this deployment is actually reachable at. A bad persist must not fail the turn.
|
|
121
|
-
const rememberUrl = () => {
|
|
122
|
-
if (typeof envelope.wake?.url !== "string")
|
|
123
|
-
return;
|
|
124
|
-
try {
|
|
125
|
-
rememberWakeAlarmUrl(stateRoot, envelope.wake.url);
|
|
126
|
-
}
|
|
127
|
-
catch (e) {
|
|
128
|
-
log.error(`[agentcore] could not persist the wake-alarm URL: ${String(e)}`);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
151
|
// Cross-deploy state: the platform wipes /mnt/state on every version update, so the durable copy
|
|
132
|
-
// must be pulled back BEFORE anything reads it.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (envelope.state && typeof envelope.state.getUrl === "string" && typeof envelope.state.putUrl === "string") {
|
|
136
|
-
stateSync.use(envelope.state);
|
|
137
|
-
}
|
|
138
|
-
else if (envelope.kind !== "invoke" && !stateSync.configured() && !warnedUnsnapshotted) {
|
|
139
|
-
// webhook/schedule-fire/wake-poke/probe reach us ONLY through the forwarder, which mints the
|
|
140
|
-
// pair. Missing = a broken/stale topology whose state dies at the next deploy: say so, loudly,
|
|
141
|
-
// once per process (a direct `invoke` legitimately has none — its session storage is its own).
|
|
142
|
-
warnedUnsnapshotted = true;
|
|
143
|
-
log.warn(unsnapshottedWarning);
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
await stateSync.ready();
|
|
147
|
-
}
|
|
148
|
-
catch (e) {
|
|
149
|
-
log.error(`[agentcore] state restore failed: ${String(e)}`);
|
|
150
|
-
// The probe is the deploy driver's verification channel: its diagnostics must survive the
|
|
151
|
-
// forwarder, which folds a non-200 transport into an opaque 502 — so for it the failure
|
|
152
|
-
// rides a transport-200 structured verdict; every other kind keeps the plain 503.
|
|
153
|
-
if (envelope.kind === "probe")
|
|
154
|
-
return json({ ok: false, error: `state restore failed: ${String(e)}` }, 200);
|
|
155
|
-
return text(`state restore failed: ${String(e)}\n`, 503);
|
|
156
|
-
}
|
|
152
|
+
// must be pulled back BEFORE anything reads it.
|
|
153
|
+
try {
|
|
154
|
+
await activation.restore(envelope);
|
|
157
155
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
156
|
+
catch (e) {
|
|
157
|
+
log.error(`[agentcore] state restore failed: ${String(e)}`);
|
|
158
|
+
// The probe is the deploy driver's verification channel: its diagnostics must survive the
|
|
159
|
+
// forwarder, which folds a non-200 transport into an opaque 502 — so for it the failure
|
|
160
|
+
// rides a transport-200 structured verdict; every other kind keeps the plain 503.
|
|
161
|
+
if (envelope.kind === "probe")
|
|
162
|
+
return json({ ok: false, error: `state restore failed: ${String(e)}` }, 200);
|
|
163
|
+
return text(`state restore failed: ${String(e)}\n`, 503);
|
|
164
164
|
}
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
165
|
+
// The channels come up on the first trusted ingress after the state root became authoritative —
|
|
166
|
+
// whichever kind carries it, so a cold start woken by a schedule fire or an alarm poke still
|
|
167
|
+
// replays checkpointed turn intent. Two deliberate exceptions: `checkpoint` must push state even
|
|
168
|
+
// when a channel is broken, and a public `invoke` runs in its own isolated storage — constructing
|
|
169
|
+
// against THAT root would cache pre-restore emptiness for the ingress session. Failure policy is
|
|
170
|
+
// per kind below: webhook and wake-poke fail their request (503), the probe reports it
|
|
171
|
+
// structurally, and a schedule fire proceeds — cron does not consume channels, and letting an
|
|
172
|
+
// unrelated channel misconfiguration silence the clock would turn one fault into two (the error
|
|
173
|
+
// is logged here either way).
|
|
174
174
|
let constructionError;
|
|
175
175
|
let dispatch;
|
|
176
176
|
if (trusted && envelope.kind !== "checkpoint" && envelope.kind !== "invoke") {
|
|
177
177
|
try {
|
|
178
|
-
dispatch = await
|
|
178
|
+
dispatch = await activation.channels();
|
|
179
179
|
}
|
|
180
180
|
catch (e) {
|
|
181
181
|
constructionError = String(e);
|
|
@@ -301,6 +301,14 @@ export function agentcoreRoutes(options) {
|
|
|
301
301
|
};
|
|
302
302
|
// Settle-then-snapshot: when the envelope leaves nothing in flight, its writes are final now (a
|
|
303
303
|
// background turn instead reports through the idle edge above).
|
|
304
|
+
//
|
|
305
|
+
// A public `invoke` is the one kind this MISSES, measurably: it answers with an unconsumed SSE
|
|
306
|
+
// stream, so the turn runs after this line and its records wait for the next envelope's snapshot.
|
|
307
|
+
// Counting it as in-flight work would close that and cost more than it buys — the stream drains at
|
|
308
|
+
// the CLIENT's pace, so one parked reader pins /ping at HealthyBusy and defeats the idle reclaim,
|
|
309
|
+
// which is the quota-exhaustion failure the ping contract below warns about. The kinds that carry
|
|
310
|
+
// conversations (webhook, schedule-fire, wake-poke) all run their turn inside the request or
|
|
311
|
+
// through `beginWork`, so they land on one of the two edges; `invoke` is the direct/debug door.
|
|
304
312
|
const invocations = async (req) => {
|
|
305
313
|
const response = await handleInvocation(req);
|
|
306
314
|
if (stateSync && !isBusy())
|
|
@@ -1,7 +1,36 @@
|
|
|
1
|
-
|
|
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
|
+
*/
|
|
2
21
|
import type { Agent } from "../agent.ts";
|
|
3
|
-
import type
|
|
22
|
+
import { type SessionControl, type SessionEvent } from "../session.ts";
|
|
4
23
|
import { type PrefixMount } from "./serve.ts";
|
|
24
|
+
/** A plane handler: the request, plus the session id the path named (`""` where the path has none). */
|
|
25
|
+
type PlaneHandler = (req: Request, session: string) => Response | Promise<Response>;
|
|
26
|
+
/** The plane's route table: `"<METHOD> <path>"` → handler, where at most one path segment is
|
|
27
|
+
* {@link SESSION_SEGMENT}. */
|
|
28
|
+
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";
|
|
5
34
|
/** The SSE payload: one control-plane event in its transport envelope. */
|
|
6
35
|
export interface WireEvent {
|
|
7
36
|
sessionId: string;
|
|
@@ -23,16 +52,25 @@ export interface ControlPlaneOptions {
|
|
|
23
52
|
agent?: Agent;
|
|
24
53
|
}
|
|
25
54
|
/**
|
|
26
|
-
* Create the control plane as a mountable prefix owner
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* The plane OWNS
|
|
30
|
-
*
|
|
55
|
+
* Create the control plane as a mountable prefix owner — a RESTful surface over
|
|
56
|
+
* {@link CONTROL_PREFIX}: sessions are a collection, a session is a resource, its history and event
|
|
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}.
|
|
31
60
|
*/
|
|
32
61
|
export declare function createControlPlane(control: SessionControl, options: ControlPlaneOptions): PrefixMount;
|
|
33
62
|
/** Mount a plane route table as a {@link PrefixMount} — the plane owns a PREFIX, while a route
|
|
34
|
-
* table is a set of
|
|
35
|
-
export declare function mountControlPlane(routes:
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
|
|
63
|
+
* table is a set of paths, at most one segment of which is a session id. */
|
|
64
|
+
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;
|
|
76
|
+
export {};
|