@fastagent-sh/fastagent 0.19.0 → 0.20.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 (131) hide show
  1. package/dist/atomic-write.d.ts +11 -7
  2. package/dist/atomic-write.js +16 -11
  3. package/dist/channels/agentcore-limits.d.ts +9 -0
  4. package/dist/channels/agentcore-limits.js +9 -0
  5. package/dist/channels/agentcore-service.d.ts +12 -11
  6. package/dist/channels/agentcore-service.js +14 -18
  7. package/dist/channels/agentcore.d.ts +11 -19
  8. package/dist/channels/agentcore.js +14 -7
  9. package/dist/channels/control.d.ts +50 -12
  10. package/dist/channels/control.js +239 -96
  11. package/dist/channels/discover.d.ts +26 -4
  12. package/dist/channels/discover.js +12 -23
  13. package/dist/channels/feishu/feishu-api.js +5 -8
  14. package/dist/channels/feishu/feishu.js +35 -18
  15. package/dist/channels/kit/attachment-path.d.ts +12 -0
  16. package/dist/channels/kit/attachment-path.js +43 -0
  17. package/dist/channels/kit/context-buffer.d.ts +9 -0
  18. package/dist/channels/kit/context-buffer.js +11 -0
  19. package/dist/channels/kit/signature.d.ts +12 -0
  20. package/dist/channels/kit/signature.js +17 -0
  21. package/dist/channels/kit/stop-command.js +3 -3
  22. package/dist/channels/kit/tasks.d.ts +1 -1
  23. package/dist/channels/kit/tasks.js +10 -4
  24. package/dist/channels/kit/turn-store.d.ts +19 -3
  25. package/dist/channels/kit/turn-store.js +29 -2
  26. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  27. package/dist/channels/slack/invoke-turn.js +5 -1
  28. package/dist/channels/slack/onboard.js +5 -5
  29. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  30. package/dist/channels/slack/onboarding-state.js +13 -20
  31. package/dist/channels/slack/register-webhook.js +1 -1
  32. package/dist/channels/slack/scaffold/slack-send.ts +2 -17
  33. package/dist/channels/slack/slack-api.js +22 -54
  34. package/dist/channels/slack/slack.js +13 -13
  35. package/dist/channels/telegram/parse.d.ts +21 -1
  36. package/dist/channels/telegram/parse.js +65 -11
  37. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  38. package/dist/channels/telegram/telegram-api.js +5 -6
  39. package/dist/channels/telegram/telegram.d.ts +2 -2
  40. package/dist/channels/telegram/telegram.js +57 -69
  41. package/dist/cli/add-slack.js +6 -5
  42. package/dist/cli/commands/add.js +1 -5
  43. package/dist/cli/commands/attach.d.ts +2 -4
  44. package/dist/cli/commands/attach.js +17 -15
  45. package/dist/cli/commands/deploy.d.ts +50 -5
  46. package/dist/cli/commands/deploy.js +183 -134
  47. package/dist/cli/commands/dev.js +4 -31
  48. package/dist/cli/commands/info.js +1 -2
  49. package/dist/cli/commands/init.js +1 -2
  50. package/dist/cli/commands/start.js +31 -41
  51. package/dist/cli/serve.d.ts +12 -21
  52. package/dist/cli/serve.js +20 -25
  53. package/dist/cli/shared.d.ts +40 -10
  54. package/dist/cli/shared.js +51 -4
  55. package/dist/deploy/agentcore/logs.d.ts +2 -2
  56. package/dist/deploy/agentcore/logs.js +2 -2
  57. package/dist/deploy/agentcore/plan.d.ts +12 -10
  58. package/dist/deploy/agentcore/plan.js +39 -31
  59. package/dist/deploy/agentcore/run.d.ts +5 -4
  60. package/dist/deploy/agentcore/run.js +13 -36
  61. package/dist/deploy/channel-ingress.d.ts +73 -0
  62. package/dist/deploy/channel-ingress.js +101 -0
  63. package/dist/deploy/docker/plan.d.ts +3 -7
  64. package/dist/deploy/docker/plan.js +4 -15
  65. package/dist/deploy/docker/run.d.ts +18 -1
  66. package/dist/deploy/docker/run.js +10 -0
  67. package/dist/deploy/fly/plan.d.ts +4 -5
  68. package/dist/deploy/fly/plan.js +7 -22
  69. package/dist/deploy/fly/run.d.ts +9 -23
  70. package/dist/deploy/fly/run.js +32 -68
  71. package/dist/deploy/preflight.d.ts +6 -7
  72. package/dist/deploy/preflight.js +26 -22
  73. package/dist/deploy/railway/plan.d.ts +4 -5
  74. package/dist/deploy/railway/plan.js +11 -26
  75. package/dist/deploy/railway/run.d.ts +9 -9
  76. package/dist/deploy/railway/run.js +40 -40
  77. package/dist/deploy/runner.js +22 -2
  78. package/dist/deploy/secrets.d.ts +18 -9
  79. package/dist/deploy/secrets.js +64 -12
  80. package/dist/dev-supervisor.js +3 -4
  81. package/dist/engines/pi/agent-session-factory.js +20 -39
  82. package/dist/engines/pi/auth.js +43 -43
  83. package/dist/engines/pi/config.d.ts +1 -1
  84. package/dist/engines/pi/create.d.ts +14 -2
  85. package/dist/engines/pi/create.js +2 -2
  86. package/dist/engines/pi/definition.d.ts +1 -1
  87. package/dist/engines/pi/definition.js +3 -4
  88. package/dist/engines/pi/login.js +32 -16
  89. package/dist/engines/pi/open.d.ts +1 -2
  90. package/dist/engines/pi/open.js +1 -1
  91. package/dist/engines/pi/search-tools.js +1 -1
  92. package/dist/engines/pi/service.d.ts +1 -1
  93. package/dist/engines/pi/service.js +8 -0
  94. package/dist/engines/pi/session-builder.js +6 -33
  95. package/dist/engines/pi/session-control.d.ts +19 -6
  96. package/dist/engines/pi/session-control.js +565 -470
  97. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  98. package/dist/engines/pi/session-inheritance.js +95 -76
  99. package/dist/engines/pi/session-markers.d.ts +48 -0
  100. package/dist/engines/pi/session-markers.js +59 -0
  101. package/dist/engines/pi/session-settings.d.ts +4 -4
  102. package/dist/engines/pi/session-settings.js +7 -4
  103. package/dist/engines/pi/session-store.d.ts +83 -13
  104. package/dist/engines/pi/session-store.js +408 -55
  105. package/dist/engines/pi/tool-context.d.ts +21 -8
  106. package/dist/engines/pi/tool-context.js +29 -4
  107. package/dist/engines/pi/tool.js +2 -2
  108. package/dist/env.js +1 -2
  109. package/dist/loader.d.ts +49 -7
  110. package/dist/loader.js +79 -18
  111. package/dist/paths.d.ts +19 -0
  112. package/dist/paths.js +27 -2
  113. package/dist/scaffold/add-channel.js +2 -2
  114. package/dist/scaffold/init.js +7 -2
  115. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  116. package/dist/schedule/discover.js +3 -15
  117. package/dist/schedule/wake-alarm.d.ts +13 -7
  118. package/dist/schedule/wake-alarm.js +88 -46
  119. package/dist/schedule/wakeups.d.ts +1 -1
  120. package/dist/schedule/wakeups.js +10 -7
  121. package/dist/service.d.ts +1 -2
  122. package/dist/service.js +27 -5
  123. package/dist/session-remote.d.ts +7 -2
  124. package/dist/session-remote.js +162 -92
  125. package/dist/session.d.ts +226 -92
  126. package/dist/session.js +61 -23
  127. package/dist/telegram.d.ts +1 -1
  128. package/dist/telegram.js +1 -1
  129. package/dist/tunnel.d.ts +18 -3
  130. package/dist/tunnel.js +32 -40
  131. package/package.json +1 -1
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Write a file so a reader sees the whole thing or nothing: same-directory temp, then rename.
3
3
  *
4
- * The temp name is fixed (`<path>.tmp`). That is safe here because no two processes write one state
5
- * root: a deployment runs one container, and `dev`'s supervisor respawns its worker only after the
6
- * old one has EXITED (dev-supervisor.ts) and these writes are synchronous, so an exited process
7
- * has none in flight. The fixed name is also the seam several channel tests use to inject a write
8
- * failure, by occupying that path with a directory. A unique name would trade that seam for a race
9
- * this codebase does not have; revisit it if a second writer ever becomes real.
4
+ * The temp name is fixed (`<path>.tmp`). It holds wherever one process writes one state root: a
5
+ * deployment runs one container, `dev`'s supervisor respawns its worker only after the old one has
6
+ * EXITED (dev-supervisor.ts), and these writes are synchronous, so an exited process has none in
7
+ * flight. Slack's onboarding state is the one file with two writers `add slack`, and the
8
+ * config-token rotation inside `--tunnel` webhook registration so a second terminal running
9
+ * `add slack --replace-config` can overlap a live `dev --tunnel`. Kept fixed anyway: that window is
10
+ * a single write at tunnel startup, its repair is the `add slack --replace-config` the registration
11
+ * failure already prints, and the fixed name is the seam several channel tests use to inject a write
12
+ * failure by occupying that path with a directory. Revisit for a writer that is neither rare nor
13
+ * self-repairing.
10
14
  *
11
15
  * `mode` is applied to the temp first, so the content is never briefly world-readable.
12
16
  *
@@ -16,4 +20,4 @@
16
20
  * It runs on the temp, before the rename — the final path is then never observable with the wrong
17
21
  * permissions, which a chmod after the rename cannot promise.
18
22
  */
19
- export declare function writeFileAtomic(path: string, data: string, mode?: number): void;
23
+ export declare function writeFileAtomic(path: string, data: string | Buffer, mode?: number): void;
@@ -1,22 +1,27 @@
1
1
  /**
2
- * One spelling of "a reader sees the whole file or none of it" for SYNCHRONOUS writes, after four
3
- * copies of it drifted apart: two identical, two with different temp names and different permission
4
- * handling.
2
+ * One spelling of "a reader sees the whole file or none of it", after five copies of it drifted
3
+ * apart: two identical, three with different temp names and different permission handling.
5
4
  *
6
- * Slack's onboarding state stays on its own async path this is deliberately not an async API, and
7
- * converting that caller is a separate question from de-duplicating these four.
5
+ * Synchronous, and there is no async sibling: every caller either sits on a path where a KB-sized
6
+ * write must complete BEFORE a transport ACK (channel state an ACKed delivery is not redelivered)
7
+ * or on a CLI/startup path where the cost is not observable. An async spelling would buy one of them
8
+ * nothing and cost this module a second set of rules to keep true.
8
9
  */
9
10
  import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
11
  import { dirname } from "node:path";
11
12
  /**
12
13
  * Write a file so a reader sees the whole thing or nothing: same-directory temp, then rename.
13
14
  *
14
- * The temp name is fixed (`<path>.tmp`). That is safe here because no two processes write one state
15
- * root: a deployment runs one container, and `dev`'s supervisor respawns its worker only after the
16
- * old one has EXITED (dev-supervisor.ts) and these writes are synchronous, so an exited process
17
- * has none in flight. The fixed name is also the seam several channel tests use to inject a write
18
- * failure, by occupying that path with a directory. A unique name would trade that seam for a race
19
- * this codebase does not have; revisit it if a second writer ever becomes real.
15
+ * The temp name is fixed (`<path>.tmp`). It holds wherever one process writes one state root: a
16
+ * deployment runs one container, `dev`'s supervisor respawns its worker only after the old one has
17
+ * EXITED (dev-supervisor.ts), and these writes are synchronous, so an exited process has none in
18
+ * flight. Slack's onboarding state is the one file with two writers `add slack`, and the
19
+ * config-token rotation inside `--tunnel` webhook registration so a second terminal running
20
+ * `add slack --replace-config` can overlap a live `dev --tunnel`. Kept fixed anyway: that window is
21
+ * a single write at tunnel startup, its repair is the `add slack --replace-config` the registration
22
+ * failure already prints, and the fixed name is the seam several channel tests use to inject a write
23
+ * failure by occupying that path with a directory. Revisit for a writer that is neither rare nor
24
+ * self-repairing.
20
25
  *
21
26
  * `mode` is applied to the temp first, so the content is never briefly world-readable.
22
27
  *
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The HOST's body ceilings, and the one place they are computed.
3
+ *
4
+ * Lambda Function URLs cap a request at 6 MB, so the forwarder cannot deliver more than that no
5
+ * matter what the adapter accepts; the body arrives base64-encoded (×4/3) inside a JSON envelope, so
6
+ * the ORIGINAL body ceiling is smaller still. This is a real capability difference from a resident
7
+ * host — the GitHub channel's own contract is 25 MiB — which is why `deploy agentcore` states it at
8
+ * plan time rather than letting an oversized payload surface as an opaque 502.
9
+ */
1
10
  /**
2
11
  * Maximum original webhook body after reserving JSON-envelope overhead and base64 expansion.
3
12
  * Enforced by both the public forwarder and the runtime adapter.
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The HOST's body ceilings, and the one place they are computed.
3
+ *
4
+ * Lambda Function URLs cap a request at 6 MB, so the forwarder cannot deliver more than that no
5
+ * matter what the adapter accepts; the body arrives base64-encoded (×4/3) inside a JSON envelope, so
6
+ * the ORIGINAL body ceiling is smaller still. This is a real capability difference from a resident
7
+ * host — the GitHub channel's own contract is 25 MiB — which is why `deploy agentcore` states it at
8
+ * plan time rather than letting an oversized payload surface as an opaque 502.
9
+ */
1
10
  /** AWS Lambda Function URLs accept request payloads up to 6 MB. */
2
11
  const FUNCTION_URL_REQUEST_LIMIT = 6 * 1000 * 1000;
3
12
  /**
@@ -18,7 +18,7 @@ import type { Agent } from "../agent.ts";
18
18
  import type { Routes } from "../channel.ts";
19
19
  import type { LoadedSchedule } from "../schedule/schedule.ts";
20
20
  import { type AgentService, type MountableAgent } from "../service.ts";
21
- import { type RouteSurface } from "./agentcore.ts";
21
+ import { type AgentcoreAdapterOptions } from "./agentcore.ts";
22
22
  export interface MountAgentcoreServiceOptions {
23
23
  /** Wrap the opened agent before anything binds to it (the CLI's turn trace). */
24
24
  wrapAgent?: (agent: Agent) => Agent;
@@ -34,21 +34,22 @@ export interface MountAgentcoreServiceOptions {
34
34
  export declare function isAgentcoreRuntime(): boolean;
35
35
  export declare function mountAgentcoreService(opened: MountableAgent, options?: MountAgentcoreServiceOptions): Promise<AgentService>;
36
36
  /**
37
- * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
38
- * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
39
- * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
40
- * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
41
- * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
37
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) the deployed container's
38
+ * ONLY reachable surface (channels/agentcore.ts). Wired by `start` when `FASTAGENT_AGENTCORE=1` (set
39
+ * by the generated deploy artifacts, never by hand).
40
+ *
41
+ * The adapter IS the surface: the agent's channels live in a table INSIDE the envelope dispatch, a
42
+ * separate namespace from these two paths, so a channel route named `/invocations` is reached
43
+ * through the Function URL as itself and cannot shadow anything.
42
44
  */
43
- export declare function mountAgentcore(routes: Routes, options: {
45
+ export declare function mountAgentcore(options: {
44
46
  agent: Agent;
45
47
  stateRoot: string;
46
48
  schedules: readonly LoadedSchedule[];
47
49
  onStateReady?: () => void;
48
50
  /** Cancels the adapter's process-global registrations on close. */
49
51
  signal?: AbortSignal;
50
- /** The serving path's LAZY channel surface: constructed by the adapter on the first envelope
51
- * AFTER the state-snapshot restore, never at boot (channels/agentcore.ts). When absent,
52
- * `routes` is the dispatch target — for wirings whose state root is already authoritative. */
53
- lazyChannels?: () => Promise<RouteSurface>;
52
+ /** The channel surface, constructed on the first envelope AFTER the state-snapshot restore — never
53
+ * at boot, where the mount is pre-restore (channels/agentcore.ts). */
54
+ channels: AgentcoreAdapterOptions["channels"];
54
55
  }): Routes;
@@ -4,7 +4,7 @@ import { assertNoControlPlaneCollision, mountSessionControl, routesFor, startSch
4
4
  import { UnknownScheduleError, agentcoreRoutes } from "./agentcore.js";
5
5
  import { createStateSync } from "./agentcore-state.js";
6
6
  import { activeWork } from "./busy.js";
7
- import { routeKeysConflict, router } from "./serve.js";
7
+ import { router } from "./serve.js";
8
8
  /** Is this process running inside the AgentCore Runtime? Set by the generated deploy artifacts. */
9
9
  export function isAgentcoreRuntime() {
10
10
  return process.env.FASTAGENT_AGENTCORE === "1";
@@ -32,13 +32,13 @@ export async function mountAgentcoreService(opened, options = {}) {
32
32
  };
33
33
  // The adapter registers process-global listeners; this is what takes them down on close.
34
34
  const closed = new AbortController();
35
- const adapterRoutes = mountAgentcore({}, {
35
+ const adapterRoutes = mountAgentcore({
36
36
  signal: closed.signal,
37
37
  agent,
38
38
  stateRoot,
39
39
  schedules: scheduled.schedules,
40
40
  onStateReady: options.onStateReady,
41
- lazyChannels,
41
+ channels: lazyChannels,
42
42
  });
43
43
  const handler = router(adapterRoutes, withControl.mounts);
44
44
  log.info(`[fastagent] agentcore: serving POST /invocations + GET /ping (FASTAGENT_AGENTCORE=1)`);
@@ -70,16 +70,18 @@ export async function mountAgentcoreService(opened, options = {}) {
70
70
  };
71
71
  }
72
72
  /**
73
- * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
74
- * the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
75
- * `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
76
- * on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
77
- * the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
73
+ * Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) the deployed container's
74
+ * ONLY reachable surface (channels/agentcore.ts). Wired by `start` when `FASTAGENT_AGENTCORE=1` (set
75
+ * by the generated deploy artifacts, never by hand).
76
+ *
77
+ * The adapter IS the surface: the agent's channels live in a table INSIDE the envelope dispatch, a
78
+ * separate namespace from these two paths, so a channel route named `/invocations` is reached
79
+ * through the Function URL as itself and cannot shadow anything.
78
80
  */
79
- export function mountAgentcore(routes, options) {
80
- const { agent, stateRoot, schedules, onStateReady, lazyChannels, signal } = options;
81
- const mounted = agentcoreRoutes({
82
- routes: lazyChannels ?? { routes },
81
+ export function mountAgentcore(options) {
82
+ const { agent, stateRoot, schedules, onStateReady, channels, signal } = options;
83
+ return agentcoreRoutes({
84
+ channels,
83
85
  agent,
84
86
  stateRoot,
85
87
  isBusy: () => activeWork() > 0,
@@ -102,10 +104,4 @@ export function mountAgentcore(routes, options) {
102
104
  return fireScheduleOnce({ agent, stateRoot, schedule, slot });
103
105
  },
104
106
  });
105
- const collisions = Object.keys(routes).filter((key) => Object.keys(mounted).some((adapterKey) => routeKeysConflict(key, adapterKey)));
106
- if (collisions.length > 0) {
107
- throw new Error(`channel route(s) ${collisions.map((key) => `"${key}"`).join(", ")} collide with the AgentCore adapter ` +
108
- `(/invocations, /ping) — rename the channel route`);
109
- }
110
- return { ...routes, ...mounted };
111
107
  }
@@ -3,14 +3,6 @@ import type { StateSync, StateUrls } from "./agentcore-state.ts";
3
3
  import type { Routes } from "../channel.ts";
4
4
  import { type PrefixMount } from "../channels/serve.ts";
5
5
  import type { ScheduleFireOutcome } from "../schedule/scheduler.ts";
6
- /**
7
- * The HOST's webhook body limit, and the one place it is computed. Lambda Function URLs cap a request
8
- * at 6 MB, so the forwarder cannot deliver more than that no matter what the adapter accepts; the
9
- * body arrives base64-encoded (×4/3) inside a JSON envelope, so the ORIGINAL body ceiling is smaller
10
- * still. This is a real capability difference from a resident host — the GitHub channel's own
11
- * contract is 25 MiB — so `deploy agentcore` says so at plan time rather than letting an oversized
12
- * payload surface as an opaque 502.
13
- */
14
6
  /** What the forwarder Lambda / EventBridge deliver in the `/invocations` payload. Every kind may
15
7
  * carry `wake` — the forwarder's self-resolved public URL, which the adapter persists so the wake
16
8
  * ALARM sink (schedule/wake-alarm.ts) can call back without the URL being baked anywhere. */
@@ -79,13 +71,14 @@ export interface RouteSurface {
79
71
  mounts?: readonly PrefixMount[];
80
72
  }
81
73
  export interface AgentcoreAdapterOptions {
82
- /** The serving routes a direct deployment would mount (channels or the builtin invoke + health).
83
- * The serving path passes a LAZY factory: channel construction loads channel state and replays
84
- * durable turn intent, so on AgentCore it must not run until the state root is authoritative —
85
- * which happens at the first envelope's `stateSync.ready()` (the restore URLs only an envelope
86
- * carries), never at boot, where the mount is pre-restore (empty after every version update).
87
- * An eager `Routes` value remains supported for wirings whose state root is already durable. */
88
- routes: RouteSurface | (() => Promise<RouteSurface> | RouteSurface);
74
+ /** The serving routes a direct deployment would mount (channels or the builtin invoke + health),
75
+ * built LAZILY: channel construction loads channel state and replays durable turn intent, so on
76
+ * AgentCore it must not run until the state root is authoritative — which happens at the first
77
+ * envelope's `stateSync.ready()` (the restore URLs only an envelope carries), never at boot, where
78
+ * the mount is pre-restore (empty after every version update). A factory, not a value: there is no
79
+ * moment during construction at which the right answer is knowable. May answer synchronously the
80
+ * resolution chain normalizes it either way. */
81
+ channels: () => Promise<RouteSurface> | RouteSurface;
89
82
  agent: Agent;
90
83
  /** Where the forwarder URL from envelopes is persisted for the wake-alarm sink (the state root). */
91
84
  stateRoot: string;
@@ -109,10 +102,9 @@ export interface AgentcoreAdapterOptions {
109
102
  onStateReady?: () => void;
110
103
  }
111
104
  /**
112
- * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }`. The caller merges it
113
- * over its routes (collision-checked at the mount site, serve.ts) the inner routes stay mounted
114
- * too, which is harmless (AgentCore routes only /invocations and /ping into the container) and keeps
115
- * a local `curl` debug surface.
105
+ * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }` the whole of what the
106
+ * platform routes into the container. The agent's own channels are not beside these: they are a table
107
+ * inside the envelope dispatch below, reached only by unwrapping a forwarder envelope.
116
108
  */
117
109
  export declare function agentcoreRoutes(options: AgentcoreAdapterOptions): Routes;
118
110
  /** Thrown by the mount-site `fire` binding when the envelope names a schedule this workspace does
@@ -48,14 +48,13 @@ function secretMatches(actual, expected) {
48
48
  return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
49
49
  }
50
50
  /**
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.
51
+ * Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }` the whole of what the
52
+ * platform routes into the container. The agent's own channels are not beside these: they are a table
53
+ * inside the envelope dispatch below, reached only by unwrapping a forwarder envelope.
55
54
  */
56
55
  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
56
+ const { channels, agent, stateRoot, isBusy, fire, stateSync, ingressSecret, onStateReady } = options;
57
+ // Lazy channel construction (see AgentcoreAdapterOptions.channels) — resolved ONCE per process, on
59
58
  // the first trusted envelope after the state root is authoritative, and the outcome is cached
60
59
  // EITHER WAY. Success: the same resident channels a direct host keeps. Failure too: construction
61
60
  // is an ACTIVATION with side effects — loadChannels builds every healthy channel (starting its
@@ -71,7 +70,7 @@ export function agentcoreRoutes(options) {
71
70
  // The factory runs INSIDE the chain: a synchronous throw must land in the cached rejection,
72
71
  // not escape before `dispatchP` is assigned (which would silently re-run the activation).
73
72
  dispatchP = Promise.resolve()
74
- .then(() => (typeof routes === "function" ? routes() : routes))
73
+ .then(channels)
75
74
  .then((surface) => router(surface.routes, surface.mounts));
76
75
  dispatchP.catch(() => { }); // observed here so the CACHED rejection is never "unhandled"
77
76
  }
@@ -301,6 +300,14 @@ export function agentcoreRoutes(options) {
301
300
  };
302
301
  // Settle-then-snapshot: when the envelope leaves nothing in flight, its writes are final now (a
303
302
  // background turn instead reports through the idle edge above).
303
+ //
304
+ // A public `invoke` is the one kind this MISSES, measurably: it answers with an unconsumed SSE
305
+ // stream, so the turn runs after this line and its records wait for the next envelope's snapshot.
306
+ // Counting it as in-flight work would close that and cost more than it buys — the stream drains at
307
+ // the CLIENT's pace, so one parked reader pins /ping at HealthyBusy and defeats the idle reclaim,
308
+ // which is the quota-exhaustion failure the ping contract below warns about. The kinds that carry
309
+ // conversations (webhook, schedule-fire, wake-poke) all run their turn inside the request or
310
+ // through `beginWork`, so they land on one of the two edges; `invoke` is the direct/debug door.
304
311
  const invocations = async (req) => {
305
312
  const response = await handleInvocation(req);
306
313
  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 — the Phase 3 transport (design §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 {};