@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.
Files changed (212) hide show
  1. package/README.md +5 -1
  2. package/dist/atomic-write.d.ts +11 -7
  3. package/dist/atomic-write.js +16 -11
  4. package/dist/channels/agentcore-limits.d.ts +9 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-protocol.d.ts +112 -0
  7. package/dist/channels/agentcore-protocol.js +22 -0
  8. package/dist/channels/agentcore-service.d.ts +12 -15
  9. package/dist/channels/agentcore-service.js +15 -24
  10. package/dist/channels/agentcore-state.d.ts +5 -11
  11. package/dist/channels/agentcore-state.js +4 -1
  12. package/dist/channels/agentcore.d.ts +9 -83
  13. package/dist/channels/agentcore.js +101 -93
  14. package/dist/channels/control.d.ts +50 -12
  15. package/dist/channels/control.js +251 -157
  16. package/dist/channels/discover.d.ts +26 -5
  17. package/dist/channels/discover.js +13 -26
  18. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  19. package/dist/channels/feishu/context-buffer.js +0 -38
  20. package/dist/channels/feishu/crypto.d.ts +0 -2
  21. package/dist/channels/feishu/crypto.js +3 -8
  22. package/dist/channels/feishu/feishu-api.js +7 -12
  23. package/dist/channels/feishu/feishu.js +86 -128
  24. package/dist/channels/feishu/parse.d.ts +4 -3
  25. package/dist/channels/feishu/parse.js +5 -4
  26. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  27. package/dist/channels/feishu/register-webhook.js +43 -57
  28. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  29. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  30. package/dist/channels/feishu/setup-mode.js +26 -0
  31. package/dist/channels/feishu/shared-api.d.ts +10 -0
  32. package/dist/channels/feishu/shared-api.js +38 -0
  33. package/dist/channels/http.d.ts +0 -8
  34. package/dist/channels/http.js +4 -56
  35. package/dist/channels/kit/attachment-path.d.ts +12 -0
  36. package/dist/channels/kit/attachment-path.js +43 -0
  37. package/dist/channels/kit/context-buffer.d.ts +9 -0
  38. package/dist/channels/kit/context-buffer.js +11 -0
  39. package/dist/channels/kit/signature.d.ts +12 -0
  40. package/dist/channels/kit/signature.js +17 -0
  41. package/dist/channels/kit/stop-command.js +3 -3
  42. package/dist/channels/kit/tasks.d.ts +1 -1
  43. package/dist/channels/kit/tasks.js +10 -4
  44. package/dist/channels/kit/turn-runner.d.ts +59 -0
  45. package/dist/channels/kit/turn-runner.js +84 -0
  46. package/dist/channels/kit/turn-store.d.ts +19 -3
  47. package/dist/channels/kit/turn-store.js +29 -2
  48. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  49. package/dist/channels/registration.d.ts +36 -1
  50. package/dist/channels/registration.js +57 -1
  51. package/dist/channels/secret.d.ts +1 -0
  52. package/dist/channels/secret.js +16 -0
  53. package/dist/channels/slack/config-api.d.ts +35 -3
  54. package/dist/channels/slack/config-api.js +51 -9
  55. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  56. package/dist/channels/slack/invoke-turn.js +5 -1
  57. package/dist/channels/slack/manifest.js +5 -1
  58. package/dist/channels/slack/onboard.d.ts +5 -5
  59. package/dist/channels/slack/onboard.js +44 -23
  60. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  61. package/dist/channels/slack/onboarding-state.js +13 -20
  62. package/dist/channels/slack/register-webhook.d.ts +3 -2
  63. package/dist/channels/slack/register-webhook.js +35 -15
  64. package/dist/channels/slack/scaffold/channel.ts +3 -10
  65. package/dist/channels/slack/scaffold/slack-send.ts +18 -141
  66. package/dist/channels/slack/shared-api.d.ts +10 -0
  67. package/dist/channels/slack/shared-api.js +34 -0
  68. package/dist/channels/slack/slack-api.d.ts +20 -2
  69. package/dist/channels/slack/slack-api.js +100 -73
  70. package/dist/channels/slack/slack.d.ts +0 -10
  71. package/dist/channels/slack/slack.js +68 -107
  72. package/dist/channels/sse.d.ts +4 -0
  73. package/dist/channels/sse.js +66 -0
  74. package/dist/channels/telegram/parse.d.ts +21 -1
  75. package/dist/channels/telegram/parse.js +65 -11
  76. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  77. package/dist/channels/telegram/register-webhook.js +44 -42
  78. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  79. package/dist/channels/telegram/telegram-api.js +5 -6
  80. package/dist/channels/telegram/telegram.d.ts +2 -2
  81. package/dist/channels/telegram/telegram.js +97 -204
  82. package/dist/channels/wait-health.js +7 -4
  83. package/dist/cli/add-feishu.js +3 -10
  84. package/dist/cli/add-slack.js +13 -25
  85. package/dist/cli/commands/add.d.ts +0 -1
  86. package/dist/cli/commands/add.js +22 -35
  87. package/dist/cli/commands/attach.d.ts +2 -4
  88. package/dist/cli/commands/attach.js +17 -15
  89. package/dist/cli/commands/chat.js +6 -12
  90. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  91. package/dist/cli/commands/deploy/agentcore.js +178 -0
  92. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  93. package/dist/cli/commands/deploy/docker.js +119 -0
  94. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  95. package/dist/cli/commands/deploy/fly.js +131 -0
  96. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  97. package/dist/cli/commands/deploy/railway.js +71 -0
  98. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  99. package/dist/cli/commands/deploy/shared.js +124 -0
  100. package/dist/cli/commands/deploy.d.ts +38 -25
  101. package/dist/cli/commands/deploy.js +101 -631
  102. package/dist/cli/commands/dev.js +20 -76
  103. package/dist/cli/commands/fire.js +6 -12
  104. package/dist/cli/commands/info.js +1 -1
  105. package/dist/cli/commands/init.js +1 -2
  106. package/dist/cli/commands/invoke.js +4 -11
  107. package/dist/cli/commands/schedule.js +1 -1
  108. package/dist/cli/commands/start.js +41 -80
  109. package/dist/cli/commands/tool.js +3 -8
  110. package/dist/cli/kernel.d.ts +0 -2
  111. package/dist/cli/kernel.js +0 -2
  112. package/dist/cli/program.js +7 -12
  113. package/dist/cli/serve.d.ts +42 -34
  114. package/dist/cli/serve.js +107 -28
  115. package/dist/cli/shared.d.ts +52 -27
  116. package/dist/cli/shared.js +69 -6
  117. package/dist/deploy/agentcore/forwarder.js +250 -0
  118. package/dist/deploy/agentcore/logs.d.ts +2 -2
  119. package/dist/deploy/agentcore/logs.js +2 -2
  120. package/dist/deploy/agentcore/plan.d.ts +34 -11
  121. package/dist/deploy/agentcore/plan.js +52 -259
  122. package/dist/deploy/agentcore/run.d.ts +9 -24
  123. package/dist/deploy/agentcore/run.js +35 -43
  124. package/dist/deploy/channel-ingress.d.ts +73 -0
  125. package/dist/deploy/channel-ingress.js +101 -0
  126. package/dist/deploy/docker/plan.d.ts +3 -7
  127. package/dist/deploy/docker/plan.js +4 -15
  128. package/dist/deploy/docker/run.d.ts +35 -4
  129. package/dist/deploy/docker/run.js +40 -8
  130. package/dist/deploy/fly/plan.d.ts +4 -5
  131. package/dist/deploy/fly/plan.js +14 -22
  132. package/dist/deploy/fly/run.d.ts +36 -23
  133. package/dist/deploy/fly/run.js +129 -83
  134. package/dist/deploy/hosts.d.ts +5 -0
  135. package/dist/deploy/hosts.js +4 -0
  136. package/dist/deploy/preflight.d.ts +6 -7
  137. package/dist/deploy/preflight.js +27 -23
  138. package/dist/deploy/railway/plan.d.ts +9 -5
  139. package/dist/deploy/railway/plan.js +18 -26
  140. package/dist/deploy/railway/run.d.ts +11 -10
  141. package/dist/deploy/railway/run.js +44 -43
  142. package/dist/deploy/runner.js +22 -2
  143. package/dist/deploy/secrets.d.ts +18 -9
  144. package/dist/deploy/secrets.js +63 -28
  145. package/dist/dev-supervisor.js +3 -4
  146. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  147. package/dist/engines/pi/agent-session-factory.js +111 -106
  148. package/dist/engines/pi/auth.js +43 -43
  149. package/dist/engines/pi/config.d.ts +2 -2
  150. package/dist/engines/pi/create.d.ts +22 -27
  151. package/dist/engines/pi/create.js +59 -74
  152. package/dist/engines/pi/definition.d.ts +1 -1
  153. package/dist/engines/pi/definition.js +11 -12
  154. package/dist/engines/pi/invoke-session.js +13 -30
  155. package/dist/engines/pi/login.js +32 -16
  156. package/dist/engines/pi/open.d.ts +3 -4
  157. package/dist/engines/pi/open.js +32 -42
  158. package/dist/engines/pi/retry-event.d.ts +6 -0
  159. package/dist/engines/pi/retry-event.js +15 -0
  160. package/dist/engines/pi/search-tools.js +1 -1
  161. package/dist/engines/pi/service.d.ts +1 -1
  162. package/dist/engines/pi/service.js +8 -0
  163. package/dist/engines/pi/session-builder.js +26 -140
  164. package/dist/engines/pi/session-control.d.ts +27 -22
  165. package/dist/engines/pi/session-control.js +551 -490
  166. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  167. package/dist/engines/pi/session-inheritance.js +95 -76
  168. package/dist/engines/pi/session-markers.d.ts +48 -0
  169. package/dist/engines/pi/session-markers.js +59 -0
  170. package/dist/engines/pi/session-settings.d.ts +5 -5
  171. package/dist/engines/pi/session-settings.js +8 -5
  172. package/dist/engines/pi/session-store.d.ts +91 -26
  173. package/dist/engines/pi/session-store.js +413 -82
  174. package/dist/engines/pi/tool-context.d.ts +24 -11
  175. package/dist/engines/pi/tool-context.js +29 -4
  176. package/dist/engines/pi/tool.d.ts +6 -9
  177. package/dist/engines/pi/tool.js +3 -2
  178. package/dist/env.js +1 -2
  179. package/dist/feishu.d.ts +1 -0
  180. package/dist/feishu.js +1 -0
  181. package/dist/lark.d.ts +1 -0
  182. package/dist/lark.js +1 -0
  183. package/dist/loader.d.ts +51 -7
  184. package/dist/loader.js +84 -18
  185. package/dist/log.d.ts +9 -17
  186. package/dist/log.js +25 -30
  187. package/dist/paths.d.ts +26 -3
  188. package/dist/paths.js +43 -5
  189. package/dist/scaffold/add-channel.d.ts +6 -1
  190. package/dist/scaffold/add-channel.js +50 -67
  191. package/dist/scaffold/init.js +7 -2
  192. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  193. package/dist/schedule/discover.js +3 -15
  194. package/dist/schedule/wake-alarm.d.ts +14 -19
  195. package/dist/schedule/wake-alarm.js +89 -48
  196. package/dist/schedule/wakeups.d.ts +1 -1
  197. package/dist/schedule/wakeups.js +10 -7
  198. package/dist/service.d.ts +24 -24
  199. package/dist/service.js +33 -77
  200. package/dist/session-remote.d.ts +12 -7
  201. package/dist/session-remote.js +185 -118
  202. package/dist/session.d.ts +227 -93
  203. package/dist/session.js +61 -23
  204. package/dist/slack.d.ts +2 -0
  205. package/dist/slack.js +1 -0
  206. package/dist/telegram.d.ts +1 -1
  207. package/dist/telegram.js +1 -1
  208. package/dist/tunnel.d.ts +34 -9
  209. package/dist/tunnel.js +83 -50
  210. package/package.json +5 -4
  211. package/dist/channels/slack/bot-auth.d.ts +0 -15
  212. 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 { timingSafeEqual } from "node:crypto";
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" }`. The caller merges it
52
- * over its routes (collision-checked at the mount site, serve.ts) the inner routes stay mounted
53
- * too, which is harmless (AgentCore routes only /invocations and /ping into the container) and keeps
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
- export function agentcoreRoutes(options) {
57
- const { routes, agent, stateRoot, isBusy, fire, stateSync, ingressSecret, onStateReady } = options;
58
- // Lazy channel construction (see AgentcoreAdapterOptions.routes) resolved ONCE per process, on
59
- // the first trusted envelope after the state root is authoritative, and the outcome is cached
60
- // EITHER WAY. Success: the same resident channels a direct host keeps. Failure too: construction
61
- // is an ACTIVATION with side effects loadChannels builds every healthy channel (starting its
62
- // queues and replaying durable turn intent) before reporting another module's failureand there
63
- // is no cleanup contract to unwind it, so re-running it per envelope could replay the same
64
- // recovered turn concurrently. The first rejection is therefore the process's answer: every later
65
- // envelope fails with the same message (visible each time), the retry boundary is a fresh session
66
- // (which scale-to-zero provides naturally), and the deploy driver's probe catches deterministic
67
- // failures at deploy time.
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 constructionwhose 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
- const resolveDispatch = () => {
70
- if (!dispatchP) {
71
- // The factory runs INSIDE the chain: a synchronous throw must land in the cached rejection,
72
- // not escape before `dispatchP` is assigned (which would silently re-run the activation).
73
- dispatchP = Promise.resolve()
74
- .then(() => (typeof routes === "function" ? routes() : routes))
75
- .then((surface) => router(surface.routes, surface.mounts));
76
- dispatchP.catch(() => { }); // observed here so the CACHED rejection is never "unhandled"
77
- }
78
- return dispatchP;
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('need { "kind": "webhook" | "schedule-fire" | "invoke" | "wake-poke" | "checkpoint" | "probe", ... }\n', 400);
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 = secretMatches(envelope.auth, ingressSecret);
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. A failed restore fails the request — serving an
133
- // empty agent (and then snapshotting that emptiness over the good copy) is the worse outcome.
134
- if (stateSync) {
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
- rememberUrl();
159
- // The state root is authoritative only now — anything that must READ it at startup (the wake-alarm
160
- // reconcile) runs here, once, rather than at boot against a mount the platform just wiped.
161
- if (onStateReady && !stateReadyFired) {
162
- stateReadyFired = true;
163
- onStateReady();
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
- // PROCESS INITIALIZATION, kind-independent: the (lazy) channels are constructed on the first
166
- // trusted ingress after the state root became authoritative whichever kind carries it, so a
167
- // cold start woken by a schedule fire or an alarm poke still replays checkpointed turn intent.
168
- // Two deliberate exceptions: `checkpoint` must push state even when a channel is broken, and a
169
- // public `invoke` runs in its own isolated storage constructing against THAT root would cache
170
- // pre-restore emptiness for the ingress session. Failure policy is per kind below: webhook and
171
- // wake-poke fail their request (503), the probe reports it structurally, and a schedule fire
172
- // proceeds cron does not consume channels, and letting an unrelated channel misconfiguration
173
- // silence the clock would turn one fault into two (the error is logged here either way).
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 resolveDispatch();
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
- import { type SessionControl, type SessionEvent } from "../session.ts";
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 { ChannelHandler } from "../channel.ts";
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: `GET
27
- * /control/capabilities|commands|state|entries|events` + `POST /control/dispatch`, all
28
- * bearer-authenticated. `events` streams SSE (`data: <WireEvent>` lines).
29
- * The plane OWNS {@link CONTROL_PREFIX}: it is mounted as one sub-application, answers its own
30
- * 404/405/preflight, and puts CORS headers on every reply — see {@link planeApp}.
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 literal paths. */
35
- export declare function mountControlPlane(routes: Record<string, ChannelHandler>): PrefixMount;
36
- /** The plane's route table. Exported so the conformance sweeps derive their route list from what is
37
- * actually mounted, rather than from a hand-kept copy that cannot notice a new route. */
38
- export declare function controlPlaneRoutes(control: SessionControl, options: ControlPlaneOptions): Record<string, ChannelHandler>;
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 {};