@fastagent-sh/fastagent 0.29.0 → 0.30.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 (310) hide show
  1. package/README.md +9 -4
  2. package/dist/atomic-write.d.ts +4 -5
  3. package/dist/atomic-write.js +4 -5
  4. package/dist/authoring.d.ts +32 -15
  5. package/dist/authoring.js +147 -127
  6. package/dist/channels/agentcore-protocol.d.ts +8 -3
  7. package/dist/channels/agentcore-protocol.js +3 -1
  8. package/dist/channels/agentcore-service.d.ts +1 -0
  9. package/dist/channels/agentcore-service.js +14 -4
  10. package/dist/channels/agentcore.d.ts +9 -0
  11. package/dist/channels/agentcore.js +29 -4
  12. package/dist/channels/context.d.ts +2 -0
  13. package/dist/channels/context.js +5 -1
  14. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -3
  16. package/dist/channels/feishu/feishu.js +10 -10
  17. package/dist/channels/feishu/register-app.js +3 -5
  18. package/dist/channels/feishu/scaffold/channel.ts +32 -34
  19. package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
  20. package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
  21. package/dist/channels/feishu/setup-mode.d.ts +2 -12
  22. package/dist/channels/feishu/setup-mode.js +2 -22
  23. package/dist/channels/feishu/setup.d.ts +12 -0
  24. package/dist/channels/feishu/setup.js +197 -0
  25. package/dist/channels/kit/abortable.d.ts +8 -0
  26. package/dist/channels/kit/abortable.js +22 -0
  27. package/dist/channels/lark/lark.d.ts +0 -3
  28. package/dist/channels/lark/lark.js +0 -5
  29. package/dist/channels/lark/scaffold/channel.ts +33 -35
  30. package/dist/channels/lark/scaffold/lark-send.ts +5 -5
  31. package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
  32. package/dist/channels/lark/setup.d.ts +9 -0
  33. package/dist/channels/lark/setup.js +57 -0
  34. package/dist/channels/setup.d.ts +112 -0
  35. package/dist/channels/setup.js +31 -0
  36. package/dist/channels/slack/history.js +1 -1
  37. package/dist/channels/slack/manifest.d.ts +17 -7
  38. package/dist/channels/slack/manifest.js +12 -16
  39. package/dist/channels/slack/scaffold/channel.ts +44 -30
  40. package/dist/channels/slack/scaffold/slack-send.ts +8 -8
  41. package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
  42. package/dist/channels/slack/setup.d.ts +7 -0
  43. package/dist/channels/slack/setup.js +119 -0
  44. package/dist/channels/slack/slack-api.d.ts +2 -0
  45. package/dist/channels/slack/slack-api.js +4 -0
  46. package/dist/channels/slack/slack.d.ts +21 -2
  47. package/dist/channels/slack/slack.js +398 -338
  48. package/dist/channels/slack/socket.d.ts +57 -0
  49. package/dist/channels/slack/socket.js +305 -0
  50. package/dist/channels/telegram/polling.d.ts +27 -0
  51. package/dist/channels/telegram/polling.js +105 -0
  52. package/dist/channels/telegram/scaffold/channel.ts +52 -32
  53. package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
  54. package/dist/channels/telegram/setup.d.ts +2 -0
  55. package/dist/channels/telegram/setup.js +42 -0
  56. package/dist/channels/telegram/telegram-api.d.ts +27 -1
  57. package/dist/channels/telegram/telegram-api.js +7 -6
  58. package/dist/channels/telegram/telegram.d.ts +11 -3
  59. package/dist/channels/telegram/telegram.js +184 -157
  60. package/dist/channels/webhook-evidence.d.ts +11 -0
  61. package/dist/channels/webhook-evidence.js +71 -0
  62. package/dist/channels/webhooks.d.ts +37 -0
  63. package/dist/channels/webhooks.js +70 -0
  64. package/dist/child-env.d.ts +16 -0
  65. package/dist/child-env.js +30 -0
  66. package/dist/cli/box-login.js +4 -1
  67. package/dist/cli/channel-app.d.ts +3 -0
  68. package/dist/cli/channel-app.js +35 -0
  69. package/dist/cli/channel-setup.d.ts +38 -0
  70. package/dist/cli/channel-setup.js +195 -0
  71. package/dist/cli/commands/add.d.ts +6 -14
  72. package/dist/cli/commands/add.js +45 -303
  73. package/dist/cli/commands/auth.js +8 -2
  74. package/dist/cli/commands/build.d.ts +5 -0
  75. package/dist/cli/commands/build.js +16 -0
  76. package/dist/cli/commands/channels.d.ts +19 -0
  77. package/dist/cli/commands/channels.js +230 -0
  78. package/dist/cli/commands/content.d.ts +7 -3
  79. package/dist/cli/commands/content.js +16 -10
  80. package/dist/cli/commands/deploy/agentcore.js +55 -23
  81. package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
  82. package/dist/cli/commands/deploy/channel-apps.js +30 -0
  83. package/dist/cli/commands/deploy/docker.js +32 -65
  84. package/dist/cli/commands/deploy/fly.js +28 -12
  85. package/dist/cli/commands/deploy/railway.js +38 -13
  86. package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
  87. package/dist/cli/commands/deploy/secrets-step.js +66 -0
  88. package/dist/cli/commands/deploy/shared.d.ts +16 -5
  89. package/dist/cli/commands/deploy/shared.js +25 -8
  90. package/dist/cli/commands/deploy.js +53 -60
  91. package/dist/cli/commands/dev.d.ts +2 -0
  92. package/dist/cli/commands/dev.js +15 -3
  93. package/dist/cli/commands/env.js +1 -1
  94. package/dist/cli/commands/info.d.ts +2 -0
  95. package/dist/cli/commands/info.js +60 -25
  96. package/dist/cli/commands/init.js +3 -1
  97. package/dist/cli/commands/invoke.d.ts +2 -0
  98. package/dist/cli/commands/invoke.js +20 -7
  99. package/dist/cli/commands/login.d.ts +0 -7
  100. package/dist/cli/commands/login.js +22 -10
  101. package/dist/cli/commands/logout.js +1 -1
  102. package/dist/cli/commands/mcp.js +14 -6
  103. package/dist/cli/commands/models.js +3 -2
  104. package/dist/cli/commands/schedules.d.ts +4 -2
  105. package/dist/cli/commands/schedules.js +14 -10
  106. package/dist/cli/commands/secrets.d.ts +14 -0
  107. package/dist/cli/commands/secrets.js +213 -0
  108. package/dist/cli/commands/start.d.ts +14 -14
  109. package/dist/cli/commands/start.js +46 -60
  110. package/dist/cli/content-view.d.ts +4 -6
  111. package/dist/cli/content-view.js +5 -11
  112. package/dist/cli/first-run-channels.d.ts +3 -0
  113. package/dist/cli/first-run-channels.js +63 -0
  114. package/dist/cli/instance.d.ts +41 -0
  115. package/dist/cli/instance.js +155 -0
  116. package/dist/cli/invoke-stream.d.ts +5 -2
  117. package/dist/cli/invoke-stream.js +7 -2
  118. package/dist/cli/program.js +282 -110
  119. package/dist/cli/serve.d.ts +4 -5
  120. package/dist/cli/serve.js +4 -7
  121. package/dist/cli/shared.d.ts +11 -2
  122. package/dist/cli/shared.js +32 -19
  123. package/dist/cli/ticket.js +50 -6
  124. package/dist/content/declare.d.ts +31 -37
  125. package/dist/content/declare.js +59 -60
  126. package/dist/content/file.d.ts +23 -12
  127. package/dist/content/file.js +48 -35
  128. package/dist/content/git.d.ts +0 -10
  129. package/dist/content/git.js +5 -39
  130. package/dist/content/resolve.d.ts +18 -22
  131. package/dist/content/resolve.js +32 -66
  132. package/dist/content/source.d.ts +7 -8
  133. package/dist/content/source.js +10 -28
  134. package/dist/credentials-page.d.ts +26 -0
  135. package/dist/credentials-page.js +185 -0
  136. package/dist/declared-secrets.d.ts +10 -3
  137. package/dist/declared-secrets.js +8 -4
  138. package/dist/deploy/agentcore/destroy.d.ts +4 -1
  139. package/dist/deploy/agentcore/destroy.js +25 -1
  140. package/dist/deploy/agentcore/forwarder.js +1 -1
  141. package/dist/deploy/agentcore/plan.d.ts +7 -21
  142. package/dist/deploy/agentcore/plan.js +47 -49
  143. package/dist/deploy/agentcore/run.d.ts +18 -10
  144. package/dist/deploy/agentcore/run.js +93 -70
  145. package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
  146. package/dist/deploy/agentcore/runtime-secrets.js +108 -0
  147. package/dist/deploy/agentcore/shell.js +12 -19
  148. package/dist/deploy/agentcore/sigv4.d.ts +7 -0
  149. package/dist/deploy/agentcore/sigv4.js +15 -0
  150. package/dist/deploy/agentcore/slr.d.ts +18 -0
  151. package/dist/deploy/agentcore/slr.js +27 -0
  152. package/dist/deploy/agentcore/store.d.ts +7 -0
  153. package/dist/deploy/agentcore/store.js +115 -0
  154. package/dist/deploy/api.d.ts +14 -14
  155. package/dist/deploy/api.js +26 -24
  156. package/dist/deploy/box-shell.d.ts +3 -1
  157. package/dist/deploy/build-context.d.ts +4 -4
  158. package/dist/deploy/build-context.js +11 -15
  159. package/dist/deploy/channel-ingress.d.ts +35 -9
  160. package/dist/deploy/channel-ingress.js +119 -22
  161. package/dist/deploy/container.d.ts +35 -10
  162. package/dist/deploy/container.js +79 -37
  163. package/dist/deploy/delivered-text.d.ts +13 -0
  164. package/dist/deploy/delivered-text.js +86 -0
  165. package/dist/deploy/docker/plan.d.ts +7 -8
  166. package/dist/deploy/docker/plan.js +31 -52
  167. package/dist/deploy/docker/run.d.ts +5 -43
  168. package/dist/deploy/docker/run.js +9 -84
  169. package/dist/deploy/file-store.d.ts +2 -0
  170. package/dist/deploy/file-store.js +17 -0
  171. package/dist/deploy/fly/plan.d.ts +3 -2
  172. package/dist/deploy/fly/plan.js +20 -9
  173. package/dist/deploy/fly/run.d.ts +18 -8
  174. package/dist/deploy/fly/run.js +24 -42
  175. package/dist/deploy/fly/store.d.ts +12 -0
  176. package/dist/deploy/fly/store.js +83 -0
  177. package/dist/deploy/hosts.d.ts +1 -2
  178. package/dist/deploy/hosts.js +1 -2
  179. package/dist/deploy/preflight.d.ts +18 -5
  180. package/dist/deploy/preflight.js +135 -63
  181. package/dist/deploy/railway/ignore.d.ts +8 -0
  182. package/dist/deploy/railway/ignore.js +57 -0
  183. package/dist/deploy/railway/plan.d.ts +1 -2
  184. package/dist/deploy/railway/plan.js +8 -6
  185. package/dist/deploy/railway/run.d.ts +21 -15
  186. package/dist/deploy/railway/run.js +32 -78
  187. package/dist/deploy/railway/store.d.ts +29 -0
  188. package/dist/deploy/railway/store.js +143 -0
  189. package/dist/deploy/runner.d.ts +5 -2
  190. package/dist/deploy/runner.js +4 -3
  191. package/dist/deploy/secret-store.d.ts +63 -0
  192. package/dist/deploy/secret-store.js +98 -0
  193. package/dist/deploy/secrets.d.ts +29 -46
  194. package/dist/deploy/secrets.js +49 -122
  195. package/dist/deploy/workspace.d.ts +6 -12
  196. package/dist/deploy/workspace.js +18 -94
  197. package/dist/dev-supervisor.js +6 -8
  198. package/dist/environment/declare.js +3 -2
  199. package/dist/environment/mise.d.ts +3 -1
  200. package/dist/environment/mise.js +4 -2
  201. package/dist/feishu.d.ts +1 -1
  202. package/dist/feishu.js +1 -1
  203. package/dist/harnesses/models/catalog.d.ts +1 -0
  204. package/dist/harnesses/models/catalog.js +1 -2
  205. package/dist/harnesses/models/environment.d.ts +7 -2
  206. package/dist/harnesses/models/environment.js +9 -5
  207. package/dist/harnesses/models/host.d.ts +0 -7
  208. package/dist/harnesses/models/host.js +10 -1
  209. package/dist/harnesses/models/login.d.ts +0 -8
  210. package/dist/harnesses/models/login.js +5 -4
  211. package/dist/harnesses/models/models.d.ts +7 -2
  212. package/dist/harnesses/models/models.js +28 -4
  213. package/dist/harnesses/pi-durable/agent.js +59 -38
  214. package/dist/harnesses/pi-durable/build.d.ts +12 -0
  215. package/dist/harnesses/pi-durable/build.js +270 -0
  216. package/dist/harnesses/pi-durable/built.d.ts +24 -0
  217. package/dist/harnesses/pi-durable/built.js +136 -0
  218. package/dist/harnesses/pi-durable/config.d.ts +81 -0
  219. package/dist/harnesses/pi-durable/config.js +266 -0
  220. package/dist/harnesses/pi-durable/define.d.ts +63 -13
  221. package/dist/harnesses/pi-durable/define.js +23 -2
  222. package/dist/harnesses/pi-durable/definition.d.ts +72 -0
  223. package/dist/harnesses/pi-durable/definition.js +143 -0
  224. package/dist/harnesses/pi-durable/events.d.ts +4 -3
  225. package/dist/harnesses/pi-durable/events.js +179 -136
  226. package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
  227. package/dist/harnesses/pi-durable/execution-env.js +11 -0
  228. package/dist/harnesses/pi-durable/mcp.js +1 -3
  229. package/dist/harnesses/pi-durable/open.d.ts +35 -37
  230. package/dist/harnesses/pi-durable/open.js +91 -153
  231. package/dist/harnesses/pi-durable/reads.d.ts +0 -1
  232. package/dist/harnesses/pi-durable/reads.js +1 -3
  233. package/dist/harnesses/pi-durable/registry.d.ts +4 -6
  234. package/dist/harnesses/pi-durable/registry.js +29 -26
  235. package/dist/harnesses/pi-durable/release.d.ts +2 -2
  236. package/dist/harnesses/pi-durable/release.js +10 -7
  237. package/dist/harnesses/pi-durable/serve.d.ts +91 -15
  238. package/dist/harnesses/pi-durable/serve.js +109 -47
  239. package/dist/harnesses/pi-durable/sessions.js +15 -12
  240. package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
  241. package/dist/harnesses/pi-durable/text-sources.js +55 -0
  242. package/dist/harnesses/pi-durable/text.d.ts +16 -4
  243. package/dist/harnesses/pi-durable/text.js +115 -59
  244. package/dist/harnesses/pi-durable/wake.js +8 -8
  245. package/dist/http/client.js +9 -3
  246. package/dist/http/contract.d.ts +3 -3
  247. package/dist/http/server.d.ts +1 -1
  248. package/dist/http/server.js +1 -1
  249. package/dist/index.d.ts +8 -5
  250. package/dist/index.js +4 -1
  251. package/dist/instance-name.d.ts +7 -0
  252. package/dist/instance-name.js +26 -0
  253. package/dist/lark.d.ts +1 -1
  254. package/dist/lark.js +1 -1
  255. package/dist/mcp/client.js +11 -2
  256. package/dist/mcp/config.d.ts +70 -18
  257. package/dist/mcp/config.js +136 -53
  258. package/dist/mcp/host.d.ts +5 -2
  259. package/dist/mcp/host.js +14 -6
  260. package/dist/mcp/sign-in.js +2 -2
  261. package/dist/mcp-api.d.ts +14 -0
  262. package/dist/paths.d.ts +22 -8
  263. package/dist/paths.js +44 -11
  264. package/dist/runtime-websocket.d.ts +14 -0
  265. package/dist/runtime-websocket.js +39 -0
  266. package/dist/scaffold/add-channel.d.ts +9 -16
  267. package/dist/scaffold/add-channel.js +13 -83
  268. package/dist/scaffold/templates/env.example +4 -5
  269. package/dist/scaffold/templates/fastagent.config.ts +20 -14
  270. package/dist/scaffold/templates/gitignore +9 -6
  271. package/dist/scaffold/templates.d.ts +6 -2
  272. package/dist/scaffold/templates.js +7 -2
  273. package/dist/scaffold/vendor-skill.js +4 -2
  274. package/dist/service.d.ts +44 -6
  275. package/dist/service.js +110 -10
  276. package/dist/serving-command.d.ts +10 -0
  277. package/dist/serving-command.js +1 -0
  278. package/dist/slack.d.ts +1 -1
  279. package/dist/slack.js +1 -1
  280. package/dist/strip-edges.d.ts +10 -0
  281. package/dist/strip-edges.js +22 -0
  282. package/dist/telegram.d.ts +1 -1
  283. package/dist/telegram.js +1 -1
  284. package/dist/tunnel.d.ts +0 -1
  285. package/dist/tunnel.js +2 -8
  286. package/dist/value-file.d.ts +11 -6
  287. package/dist/value-file.js +21 -9
  288. package/dist/version.d.ts +7 -1
  289. package/dist/version.js +9 -3
  290. package/package.json +6 -5
  291. package/dist/channels/lark/credentials-page.d.ts +0 -25
  292. package/dist/channels/lark/credentials-page.js +0 -206
  293. package/dist/channels/lark/onboard.d.ts +0 -52
  294. package/dist/channels/lark/onboard.js +0 -86
  295. package/dist/channels/slack/config-api.d.ts +0 -69
  296. package/dist/channels/slack/config-api.js +0 -162
  297. package/dist/channels/slack/onboard.d.ts +0 -40
  298. package/dist/channels/slack/onboard.js +0 -136
  299. package/dist/channels/slack/onboarding-state.d.ts +0 -31
  300. package/dist/channels/slack/onboarding-state.js +0 -64
  301. package/dist/channels/slack/register-webhook.d.ts +0 -12
  302. package/dist/channels/slack/register-webhook.js +0 -67
  303. package/dist/channels/slack/setup-server.d.ts +0 -32
  304. package/dist/channels/slack/setup-server.js +0 -246
  305. package/dist/cli/add-feishu.d.ts +0 -41
  306. package/dist/cli/add-feishu.js +0 -391
  307. package/dist/cli/add-slack.d.ts +0 -18
  308. package/dist/cli/add-slack.js +0 -216
  309. package/dist/harnesses/pi-durable/parked.d.ts +0 -18
  310. package/dist/harnesses/pi-durable/parked.js +0 -27
package/README.md CHANGED
@@ -42,7 +42,7 @@ FastAgent is the missing bridge from local agent directory to live service.
42
42
  - **Durable by default.** Every conversation lives in the instance's storage, and an accepted message is answered after a restart. Built on the open-source [pi](https://github.com/earendil-works/pi) durable harness.
43
43
  - **Models, tools & skills.** Any model provider (OpenAI, Anthropic, Google, …) via OAuth or API key, plus your own providers in code; typed tools in `src/tools/` (TypeBox-validated); Agent Skills loaded on demand.
44
44
  - **App embedding — your stack, we plug in.** Open the agent in your own process and call it like any function, behind your auth, with your database and infra; mount its channels in your Next / Astro / Hono / Bun / Node app with one handler. FastAgent composes with your app, never owns it.
45
- - **Deploy anywhere.** No application build step — the directory is the deployable unit. `fastagent deploy docker|fly|railway|agentcore` generates the container + target config and a runbook (`--run` drives it to completion). Local Docker gets user-owned Compose + durable state; optional `--tunnel` adds an ephemeral Quick Tunnel service for webhook channels; AWS Bedrock AgentCore gets a one-stack CloudFormation topology (webhooks via a forwarder Lambda, schedules via EventBridge). Durable ingress remains yours.
45
+ - **Deploy anywhere.** No application build step — the directory is the deployable unit. `fastagent deploy docker|fly|railway|agentcore` generates the container + target config and a runbook (`--run` drives it to completion). Local Docker gets user-owned Compose + durable state; AWS Bedrock AgentCore gets a one-stack CloudFormation topology (webhooks via a forwarder Lambda, schedules via EventBridge). Durable ingress remains yours.
46
46
 
47
47
  ## Design philosophy
48
48
 
@@ -107,9 +107,14 @@ There is no FastAgent build step: the agent folder is the deployable unit.
107
107
  ## Embed in an app
108
108
 
109
109
  ```ts
110
- import { openAgent } from "@fastagent-sh/fastagent";
111
-
112
- const agent = await openAgent("./agent", { model: "openai/gpt-5.5" });
110
+ import { baseEnv, openAgent } from "@fastagent-sh/fastagent";
111
+ import support from "@acme/support-agent"; // the default export of a package `fastagent build` made
112
+
113
+ const agent = await openAgent(support, {
114
+ root: "/var/lib/myapp/support",
115
+ env: baseEnv(process.env),
116
+ model: "openai/gpt-5.5",
117
+ });
113
118
 
114
119
  const session = await agent.sessions.create();
115
120
  for await (const event of agent.messages.stream({ sessionId: session.id, content: "hi" })) {
@@ -3,16 +3,15 @@
3
3
  *
4
4
  * The temp name is FIXED (`<path>.tmp`), which rests on one process writing one state root: a deployment runs one
5
5
  * container, `dev`'s supervisor respawns its worker only after the old one has exited, and these writes are
6
- * synchronous. Slack's onboarding state is the documented exception (`add slack` plus the config-token rotation at
7
- * `--tunnel` startup); it stays fixed because that window is one rare, self-repairing write, and because the fixed
8
- * name is the seam channel tests use to inject a write failure. Revisit before adding a writer that is neither.
6
+ * synchronous. The fixed name is also the seam channel tests use to inject a write failure. Revisit before adding a
7
+ * writer that is not one process's synchronous write.
9
8
  *
10
9
  * `mode` is applied to the temp first, so the content is never briefly world-readable, and the `chmod` is not
11
10
  * redundant with it: `writeFileSync` honours `mode` only when it CREATES the file, so a temp left behind by a crashed
12
11
  * writer would keep its old, possibly looser permissions.
13
12
  *
14
- * `sharedWriters` is for a file SEVERAL processes may write at once (the generated `.fastagent/` files, which every
15
- * command that reads the definition writes with the same bytes): each writes its own temp, and the last rename wins. A fixed temp there would
13
+ * `sharedWriters` is for a file SEVERAL processes may write at once (the model catalog's lists, which every process
14
+ * that refreshes them writes): each writes its own temp, and the last rename wins. A fixed temp there would
16
15
  * let one writer's rename take the other's file and fail it with ENOENT. No test exercises that race: it needs two
17
16
  * processes renaming within the same moment, which a test cannot arrange without being flaky.
18
17
  */
@@ -7,16 +7,15 @@ import { dirname } from "node:path";
7
7
  *
8
8
  * The temp name is FIXED (`<path>.tmp`), which rests on one process writing one state root: a deployment runs one
9
9
  * container, `dev`'s supervisor respawns its worker only after the old one has exited, and these writes are
10
- * synchronous. Slack's onboarding state is the documented exception (`add slack` plus the config-token rotation at
11
- * `--tunnel` startup); it stays fixed because that window is one rare, self-repairing write, and because the fixed
12
- * name is the seam channel tests use to inject a write failure. Revisit before adding a writer that is neither.
10
+ * synchronous. The fixed name is also the seam channel tests use to inject a write failure. Revisit before adding a
11
+ * writer that is not one process's synchronous write.
13
12
  *
14
13
  * `mode` is applied to the temp first, so the content is never briefly world-readable, and the `chmod` is not
15
14
  * redundant with it: `writeFileSync` honours `mode` only when it CREATES the file, so a temp left behind by a crashed
16
15
  * writer would keep its old, possibly looser permissions.
17
16
  *
18
- * `sharedWriters` is for a file SEVERAL processes may write at once (the generated `.fastagent/` files, which every
19
- * command that reads the definition writes with the same bytes): each writes its own temp, and the last rename wins. A fixed temp there would
17
+ * `sharedWriters` is for a file SEVERAL processes may write at once (the model catalog's lists, which every process
18
+ * that refreshes them writes): each writes its own temp, and the last rename wins. A fixed temp there would
20
19
  * let one writer's rename take the other's file and fail it with ENOENT. No test exercises that race: it needs two
21
20
  * processes renaming within the same moment, which a test cannot arrange without being flaky.
22
21
  */
@@ -1,4 +1,3 @@
1
- import { type BoundContent } from "./content/declare.ts";
2
1
  import type { DeclaredEnvironment } from "./environment/declare.ts";
3
2
  import { type ResolvedContent } from "./content/resolve.ts";
4
3
  import type { ContentAddition } from "./content/source.ts";
@@ -10,8 +9,7 @@ export declare class ContentNameError extends Error {
10
9
  name: string;
11
10
  }
12
11
  export interface InitAgentOptions {
13
- /** What the agent works on: repositories into `context.json`, this machine's directories into
14
- * `context.local.json`. */
12
+ /** What the agent works on: repositories for its program into `context.json`, the rest into `fastagent.json`. */
15
13
  content?: ContentAddition[];
16
14
  /**
17
15
  * Install the agent's dependencies, after the scaffold and before its first commit, so the lockfile is in that
@@ -32,13 +30,20 @@ export interface CreatedAgent {
32
30
  */
33
31
  repository: string;
34
32
  }
33
+ /** Where an agent's content is declared and kept: which instance configuration is edited, and where clones go. */
34
+ export interface ContentScope {
35
+ /** The instance configuration to edit, absolute. Default: `fastagent.json` in the agent directory. */
36
+ config?: string;
37
+ /** The instance's working directory, where a clone is made. Default: the agent directory. */
38
+ workdir?: string;
39
+ }
35
40
  /**
36
41
  * Create an agent in `dir` as `fastagent init` does, which must be new or empty: the whole scaffold, and a git
37
42
  * repository whose first commit is what was written, since the agent changes itself and version control is how its
38
43
  * author goes back to a version that worked. It installs no dependencies; the agent opens without them.
39
44
  *
40
45
  * `content` gives it any number of entries, each a directory of this machine or a GitHub repository. They are all
41
- * checked before anything is written, and one that is refused leaves no directory. Two entries may bind the same
46
+ * checked before anything is written, and one that is refused leaves no directory. Two entries may be the same
42
47
  * directory, or one inside the other; the agent then reaches it by both names.
43
48
  */
44
49
  export declare function createAgent(dir: string, options?: {
@@ -54,12 +59,11 @@ export declare function initAgent(dir: string, options?: InitAgentOptions): Prom
54
59
  content: ResolvedContent[];
55
60
  }>;
56
61
  /**
57
- * The on-disk declaration resolved for this machine, not an open agent's startup snapshot. Missing local directories
58
- * are listed separately; an agent opened here is not told of them.
62
+ * The on-disk declaration resolved for this machine, not an open agent's startup snapshot. An entry that cannot be
63
+ * resolved (a directory that is gone) refuses, naming it.
59
64
  */
60
- export declare function listContent(agentDir: string): Promise<{
65
+ export declare function listContent(agentDir: string, scope?: ContentScope): Promise<{
61
66
  content: ResolvedContent[];
62
- absent: BoundContent[];
63
67
  }>;
64
68
  /**
65
69
  * The edited declaration resolved for this machine; an already-open agent keeps its startup snapshot until reopened.
@@ -70,17 +74,30 @@ export interface ContentEdit {
70
74
  notes: string[];
71
75
  }
72
76
  /**
73
- * Add a content entry to the agent in `agentDir`: its repository to `context.json`, its directory on this machine to
74
- * `context.local.json`, or both. A name that cannot be one, or that the agent already has (ignoring case), is a
75
- * {@link ContentNameError}.
77
+ * Add a content entry to the agent in `agentDir`: to the instance configuration (`fastagent.json`, or the file
78
+ * `scope.config` names), or with `program` to the program's `context.json`. A name that cannot be one, or that the
79
+ * agent already has in either (ignoring case), is a {@link ContentNameError}.
76
80
  */
77
- export declare function addContent(agentDir: string, addition: ContentAddition): Promise<ContentEdit>;
81
+ export declare function addContent(agentDir: string, addition: ContentAddition, scope?: ContentScope): Promise<ContentEdit>;
78
82
  /**
79
- * Remove the agent's content entry named `name` (ignoring case), from `context.json` and from this machine's
80
- * `context.local.json`. A clone at `content/<name>` is left as it is, and said: it may hold the agent's work, and
83
+ * Remove the agent's content entry named `name` (ignoring case), from whichever of `context.json` and the instance
84
+ * configuration holds it. A clone at `content/<name>` is left as it is, and said: it may hold the agent's work, and
81
85
  * fastagent never deletes a directory in `content/`. A name the agent does not have is a {@link ContentNameError}.
82
86
  */
83
- export declare function removeContent(agentDir: string, name: string): Promise<ContentEdit>;
87
+ export declare function removeContent(agentDir: string, name: string, scope?: ContentScope): Promise<ContentEdit>;
88
+ /**
89
+ * Set what the instance configuration says of the channel `channel` (`channels.<channel>` in `fastagent.json` in
90
+ * `agentDir`, or the file `scope.config` names): `entry` over what it holds, the rest of the file kept as it is. Under
91
+ * the lock content edits take, since they write the same file.
92
+ */
93
+ export declare function setChannelConfig(agentDir: string, channel: string, entry: {
94
+ ingress?: "webhook" | "connection";
95
+ app?: {
96
+ id: string;
97
+ team?: string;
98
+ url?: string;
99
+ };
100
+ }, scope?: Pick<ContentScope, "config">): Promise<void>;
84
101
  /**
85
102
  * Replace the environment `context.json` declares with what `change` makes of it as it is now, under the lock content
86
103
  * edits take, keeping the rest of the file as it is; and return what was written.
package/dist/authoring.js CHANGED
@@ -7,16 +7,15 @@
7
7
  */
8
8
  import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
9
9
  import { rm } from "node:fs/promises";
10
- import { join, resolve } from "node:path";
10
+ import { dirname, isAbsolute, join, resolve } from "node:path";
11
11
  import { writeFileAtomic } from "./atomic-write.js";
12
- import { GitNotInstalled, gitFor } from "./git.js";
13
- import { CONTEXT_FILE, LOCAL_CONTEXT_FILE, contentEntryPath, resolveAgentDir } from "./paths.js";
12
+ import { CONTEXT_FILE, INSTANCE_CONFIG_FILE, contentEntryPath, resolveAgentDir } from "./paths.js";
14
13
  import { initRepository, scaffoldAgent } from "./scaffold/init.js";
15
- import { canonicalBinding, canonicalEntry, declareBindings, declareContent, isContentName, } from "./content/declare.js";
16
- import { contextFileText, loadContent, localContextFileText, parseContextFile, parseLocalContextFile, } from "./content/file.js";
17
- import { checkBinding, contentAbsentHere, resolveContent } from "./content/resolve.js";
14
+ import { canonicalEntry, declareContent, isContentName, joinContent, } from "./content/declare.js";
15
+ import { contextFileText, loadContent, parseContextFile } from "./content/file.js";
16
+ import { checkDirectory, resolveContent } from "./content/resolve.js";
18
17
  import { withLockedFile } from "./locked-file.js";
19
- /** `context.json` is part of the definition, read by whoever reads the agent. */
18
+ /** `context.json` and `fastagent.json` are part of an agent's text: read by whoever reads the agent. */
20
19
  const CONTEXT_FILE_MODE = 0o644;
21
20
  /**
22
21
  * A refusal about a content entry's NAME: one that cannot name an entry, one the agent already has, one it does not
@@ -25,13 +24,24 @@ const CONTEXT_FILE_MODE = 0o644;
25
24
  export class ContentNameError extends Error {
26
25
  name = "ContentNameError";
27
26
  }
27
+ const placesOf = (dir, scope = {}) => {
28
+ const config = scope.config ?? join(dir, INSTANCE_CONFIG_FILE);
29
+ for (const [name, path] of [
30
+ ["config", scope.config],
31
+ ["workdir", scope.workdir],
32
+ ]) {
33
+ if (path !== undefined && !isAbsolute(path))
34
+ throw new Error(`${name} must be an absolute path, and is "${path}"`);
35
+ }
36
+ return { dir, workdir: scope.workdir ?? dir, config };
37
+ };
28
38
  /**
29
39
  * Create an agent in `dir` as `fastagent init` does, which must be new or empty: the whole scaffold, and a git
30
40
  * repository whose first commit is what was written, since the agent changes itself and version control is how its
31
41
  * author goes back to a version that worked. It installs no dependencies; the agent opens without them.
32
42
  *
33
43
  * `content` gives it any number of entries, each a directory of this machine or a GitHub repository. They are all
34
- * checked before anything is written, and one that is refused leaves no directory. Two entries may bind the same
44
+ * checked before anything is written, and one that is refused leaves no directory. Two entries may be the same
35
45
  * directory, or one inside the other; the agent then reaches it by both names.
36
46
  */
37
47
  export async function createAgent(dir, options = {}) {
@@ -43,25 +53,25 @@ export async function createAgent(dir, options = {}) {
43
53
  */
44
54
  export async function initAgent(dir, options = {}) {
45
55
  const agentDir = resolve(dir);
46
- const additions = options.content ?? [];
47
- let maps = { content: {}, bindings: {} };
48
- for (const addition of additions)
49
- maps = withAddition(agentDir, maps, addition).maps;
56
+ const places = placesOf(agentDir);
57
+ let maps = { program: {}, instance: {} };
58
+ for (const addition of options.content ?? [])
59
+ maps = withAddition(places, maps, addition).maps;
50
60
  const { created, undo } = await scaffoldAgent(agentDir);
51
61
  const removeAll = async (error) => {
52
62
  await rm(join(agentDir, CONTEXT_FILE), { force: true });
53
- await rm(join(agentDir, LOCAL_CONTEXT_FILE), { force: true });
63
+ await rm(join(agentDir, INSTANCE_CONFIG_FILE), { force: true });
54
64
  await undo();
55
65
  throw error;
56
66
  };
57
67
  // The content was checked above, so a refusal here is one that check could not foresee (the disk changed in
58
68
  // between); the scaffold goes with it, or a retry would find "already an agent" declaring no content.
59
69
  try {
60
- if (Object.keys(maps.content).length > 0) {
61
- writeFileAtomic(join(agentDir, CONTEXT_FILE), contextFileText({ content: maps.content }), CONTEXT_FILE_MODE);
70
+ if (Object.keys(maps.program).length > 0) {
71
+ writeFileAtomic(join(agentDir, CONTEXT_FILE), contextFileText({ content: maps.program }), CONTEXT_FILE_MODE);
62
72
  }
63
- if (Object.keys(maps.bindings).length > 0) {
64
- writeFileAtomic(join(agentDir, LOCAL_CONTEXT_FILE), localContextFileText(maps.bindings), CONTEXT_FILE_MODE);
73
+ if (Object.keys(maps.instance).length > 0) {
74
+ writeFileAtomic(places.config, instanceFileText({}, maps.instance), CONTEXT_FILE_MODE);
65
75
  }
66
76
  }
67
77
  catch (error) {
@@ -69,166 +79,176 @@ export async function initAgent(dir, options = {}) {
69
79
  }
70
80
  if (options.install)
71
81
  await options.install(agentDir).catch(removeAll);
72
- const content = resolveContent(agentDir, declarationOf(agentDir, maps));
82
+ const content = resolveContent(declarationOf(places, maps), places);
73
83
  return { dir: agentDir, created, content, repository: await initRepository(agentDir) };
74
84
  }
75
85
  /**
76
- * The on-disk declaration resolved for this machine, not an open agent's startup snapshot. Missing local directories
77
- * are listed separately; an agent opened here is not told of them.
86
+ * The on-disk declaration resolved for this machine, not an open agent's startup snapshot. An entry that cannot be
87
+ * resolved (a directory that is gone) refuses, naming it.
78
88
  */
79
- export async function listContent(agentDir) {
80
- const dir = resolveAgentDir(agentDir);
81
- const declaration = loadContent(dir);
82
- return { content: resolveContent(dir, declaration), absent: contentAbsentHere(declaration) };
89
+ export async function listContent(agentDir, scope) {
90
+ const places = placesOf(resolveAgentDir(agentDir), scope);
91
+ return { content: resolveContent(loadContent(places.dir, instanceSourceOf(places)), places) };
83
92
  }
84
93
  /**
85
- * Add a content entry to the agent in `agentDir`: its repository to `context.json`, its directory on this machine to
86
- * `context.local.json`, or both. A name that cannot be one, or that the agent already has (ignoring case), is a
87
- * {@link ContentNameError}.
94
+ * Add a content entry to the agent in `agentDir`: to the instance configuration (`fastagent.json`, or the file
95
+ * `scope.config` names), or with `program` to the program's `context.json`. A name that cannot be one, or that the
96
+ * agent already has in either (ignoring case), is a {@link ContentNameError}.
88
97
  */
89
- export async function addContent(agentDir, addition) {
90
- const dir = resolveAgentDir(agentDir);
91
- const declaration = await editContext(dir, (shared, local) => {
92
- const { maps } = withAddition(dir, mapsOf(shared, local), addition);
93
- return {
94
- result: declarationOf(dir, maps),
95
- ...(addition.entry !== undefined ? { shared: contextFileText({ ...shared, content: maps.content }) } : {}),
96
- ...(addition.binding !== undefined ? { local: localContextFileText(maps.bindings) } : {}),
97
- };
98
+ export async function addContent(agentDir, addition, scope) {
99
+ const places = placesOf(resolveAgentDir(agentDir), scope);
100
+ const declaration = await editContent(places, (maps) => {
101
+ const next = withAddition(places, maps, addition).maps;
102
+ return { result: declarationOf(places, next), next };
98
103
  });
99
- const notes = addition.binding !== undefined ? unignoredLocalFile(dir) : [];
100
- return { name: addition.name, content: resolveContent(dir, declaration), notes };
104
+ return { name: addition.name, content: resolveContent(declaration, places), notes: [] };
101
105
  }
102
106
  /**
103
- * Remove the agent's content entry named `name` (ignoring case), from `context.json` and from this machine's
104
- * `context.local.json`. A clone at `content/<name>` is left as it is, and said: it may hold the agent's work, and
107
+ * Remove the agent's content entry named `name` (ignoring case), from whichever of `context.json` and the instance
108
+ * configuration holds it. A clone at `content/<name>` is left as it is, and said: it may hold the agent's work, and
105
109
  * fastagent never deletes a directory in `content/`. A name the agent does not have is a {@link ContentNameError}.
106
110
  */
107
- export async function removeContent(agentDir, name) {
108
- const dir = resolveAgentDir(agentDir);
109
- const { removed, wasDeclared, declaration } = await editContext(dir, (shared, local) => {
110
- const found = entryNamed(mapsOf(shared, local), name);
111
- const maps = {
112
- content: without(shared.content, found.name),
113
- bindings: without(local.content, found.name),
114
- };
115
- const declaration = declarationOf(dir, maps);
116
- // What is left is resolved before it is written, so an entry that cannot be refuses the edit rather than failing
117
- // it once it is made. The removed one is not asked: removing a broken entry is how it is mended.
118
- resolveContent(dir, declaration);
119
- return {
120
- result: { removed: found.name, wasDeclared: found.shared, declaration },
121
- ...(found.shared ? { shared: contextFileText({ ...shared, content: maps.content }) } : {}),
122
- ...(found.local ? { local: localContextFileText(maps.bindings) } : {}),
111
+ export async function removeContent(agentDir, name, scope) {
112
+ const places = placesOf(resolveAgentDir(agentDir), scope);
113
+ const { removed, wasClone, declaration } = await editContent(places, (maps) => {
114
+ const found = entryNamed(maps, name);
115
+ const next = {
116
+ program: without(maps.program, found),
117
+ instance: without(maps.instance, found),
123
118
  };
119
+ const declaration = declarationOf(places, next);
120
+ // What is left is resolved before it is written, so an entry that cannot refuses the edit rather than failing it
121
+ // once it is made. The removed one is not asked: removing a broken entry is how it is mended.
122
+ resolveContent(declaration, places);
123
+ const was = declarationOf(places, maps).find((entry) => entry.name === found);
124
+ return { result: { removed: found, wasClone: was?.kind === "github", declaration }, next };
124
125
  });
125
126
  const notes = [];
126
- if (wasDeclared && lstatSync(contentEntryPath(dir, removed), { throwIfNoEntry: false })) {
127
+ if (wasClone && lstatSync(contentEntryPath(places.workdir, removed), { throwIfNoEntry: false })) {
127
128
  notes.push(`content/${removed} is left as it is: it may hold the agent's work — delete it once nothing in it is needed`);
128
129
  }
129
- return { name: removed, content: resolveContent(dir, declaration), notes };
130
+ return { name: removed, content: resolveContent(declaration, places), notes };
130
131
  }
131
132
  /**
132
133
  * `maps` with `addition` among them, checked as a whole before anything is written: its name can name an entry and is
133
134
  * not taken (ignoring case), the entry is valid, a repository to clone has nothing at `content/<name>` yet, a
134
- * directory to bind can be bound, and every other entry still resolves (one that does not refuses the edit, not fails
135
+ * directory can be the content, and every other entry still resolves (one that does not refuses the edit, not fails
135
136
  * it once made).
136
137
  */
137
- function withAddition(agentDir, maps, addition) {
138
- const { name, entry, binding } = addition;
139
- if (entry === undefined && binding === undefined)
140
- throw new Error(`content "${name}": nothing to add`);
138
+ function withAddition(places, maps, addition) {
139
+ const { name, entry, program } = addition;
141
140
  if (!isContentName(name)) {
142
141
  throw new ContentNameError(`"${name}" cannot name content (one path segment of letters, digits, "-" and "_")`);
143
142
  }
144
- const taken = [...Object.keys(maps.content), ...Object.keys(maps.bindings)].find((key) => key.toLowerCase() === name.toLowerCase());
143
+ const taken = [...Object.keys(maps.program), ...Object.keys(maps.instance)].find((key) => key.toLowerCase() === name.toLowerCase());
145
144
  if (taken)
146
145
  throw new ContentNameError(`this agent already has content named "${taken}"`);
147
- const next = {
148
- content: entry !== undefined ? { ...maps.content, [name]: canonicalEntry(entry) } : maps.content,
149
- bindings: binding !== undefined ? { ...maps.bindings, [name]: canonicalBinding(binding) } : maps.bindings,
150
- };
151
- const declaration = declarationOf(agentDir, next);
152
- if (binding === undefined) {
153
- if (lstatSync(contentEntryPath(agentDir, name), { throwIfNoEntry: false })) {
154
- throw new Error(`content/${name} already exists in ${agentDir}: move it away first`);
146
+ if (program === true && !("github" in entry)) {
147
+ throw new Error(`content "${name}": ${CONTEXT_FILE} is shared, so it declares only repositories every instance reaches`);
148
+ }
149
+ const written = { ...(program === true ? maps.program : maps.instance), [name]: canonicalEntry(entry) };
150
+ const next = program === true ? { ...maps, program: written } : { ...maps, instance: written };
151
+ const declaration = declarationOf(places, next);
152
+ const declared = declaration.find((d) => d.name === name);
153
+ if (declared.kind === "github") {
154
+ if (lstatSync(contentEntryPath(places.workdir, name), { throwIfNoEntry: false })) {
155
+ throw new Error(`content/${name} already exists in ${places.workdir}: move it away first`);
155
156
  }
156
157
  }
157
158
  else {
158
- const bound = declaration.bound.find((b) => b.name === name);
159
- const repository = declaration.declared.find((d) => d.name === name);
160
- checkBinding(agentDir, name, bound.path, repository);
159
+ checkDirectory(places, name, declared.path);
161
160
  }
162
- resolveContent(agentDir, declaration);
161
+ resolveContent(declaration, places);
163
162
  return { maps: next };
164
163
  }
165
164
  /** `maps` declared: what every reader of the files would read from them. */
166
- function declarationOf(agentDir, maps) {
167
- const declared = declareContent(maps.content);
168
- return { declared, bound: declareBindings(maps.bindings, declared, agentDir) };
169
- }
170
- function mapsOf(shared, local) {
171
- return { content: shared.content, bindings: local.content };
165
+ function declarationOf(places, maps) {
166
+ return joinContent(declareContent(maps.program, { origin: "program", file: CONTEXT_FILE }), declareContent(maps.instance, { origin: "instance", file: places.config, base: dirname(places.config) }));
172
167
  }
173
168
  function without(map, name) {
174
169
  return Object.fromEntries(Object.entries(map).filter(([key]) => key !== name));
175
170
  }
176
- /**
177
- * Where the entry `name` matches ignoring case is written: its name, and which of the two files hold it. One the agent
178
- * does not have is a {@link ContentNameError}.
179
- */
171
+ /** Where the entry `name` matches ignoring case is written: its name. One the agent does not have is a {@link ContentNameError}. */
180
172
  function entryNamed(maps, name) {
181
- const matches = (key) => key.toLowerCase() === name.toLowerCase();
182
- const shared = Object.keys(maps.content).find(matches);
183
- const local = Object.keys(maps.bindings).find(matches);
184
- const found = shared ?? local;
173
+ const names = [...Object.keys(maps.program), ...Object.keys(maps.instance)];
174
+ const found = names.find((key) => key.toLowerCase() === name.toLowerCase());
185
175
  if (found !== undefined)
186
- return { name: found, shared: shared !== undefined, local: local !== undefined };
187
- const names = [...new Set([...Object.keys(maps.content), ...Object.keys(maps.bindings)])];
188
- throw new ContentNameError(`no content named "${name}" (this agent has: ${names.join(", ") || "none"})`);
176
+ return found;
177
+ throw new ContentNameError(`no content named "${name}" (this agent has: ${[...new Set(names)].join(", ") || "none"})`);
178
+ }
179
+ /** The instance configuration's content, as `loadContent` reads it: from the file, or none when there is none. */
180
+ function instanceSourceOf(places) {
181
+ const text = textIfExists(places.config);
182
+ if (text === undefined)
183
+ return undefined;
184
+ return { values: parseInstanceFile(text, places.config), label: places.config, directory: dirname(places.config) };
185
+ }
186
+ /** `text`, the contents of the instance configuration at `path`, as the object it is. */
187
+ function parseInstanceFile(text, path) {
188
+ let raw;
189
+ try {
190
+ raw = JSON.parse(text);
191
+ }
192
+ catch (error) {
193
+ throw new Error(`${path} is not valid JSON: ${error.message}`);
194
+ }
195
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
196
+ throw new Error(`${path} must hold a JSON object`);
197
+ return raw;
198
+ }
199
+ /** The instance configuration as written: `file` as it is, with `content` replaced (left out when empty). */
200
+ function instanceFileText(file, content) {
201
+ const { content: _replaced, ...rest } = file;
202
+ return `${JSON.stringify(Object.keys(content).length > 0 ? { ...rest, content } : rest, null, 2)}\n`;
189
203
  }
190
204
  /**
191
- * Run `edit` over both context files under ONE lock, `context.json`'s, which env edits take too: whether a name is
192
- * taken is asked of both files, so two edits that write different files must still queue behind each other. The lock
193
- * creates neither file; what `edit` returns for a file is written under it, `context.local.json` first.
205
+ * Run `edit` over both files that hold content under ONE lock, `context.json`'s, which env edits take too: whether a
206
+ * name is taken is asked of both, so two edits that write different files must still queue behind each other. The lock
207
+ * creates neither file; what `edit` returns is written under it, the instance configuration first. A file that holds
208
+ * the same content as before is not rewritten.
194
209
  */
195
- async function editContext(dir, edit) {
196
- const sharedPath = join(dir, CONTEXT_FILE);
197
- const localPath = join(dir, LOCAL_CONTEXT_FILE);
198
- return withLockedFile(sharedPath, async (sharedText) => {
199
- const shared = sharedText === undefined ? { content: {}, declared: [] } : parseContextFile(sharedText, sharedPath);
200
- const localText = textIfExists(localPath);
201
- const local = localText === undefined
202
- ? { content: {}, bound: [] }
203
- : parseLocalContextFile(localText, localPath, shared.declared, dir);
204
- const out = edit(shared, local);
205
- if (out.local !== undefined)
206
- writeFileAtomic(localPath, out.local, fileMode(localPath));
207
- return { result: out.result, next: out.shared };
208
- }, { mode: fileMode(sharedPath), create: false });
210
+ async function editContent(places, edit) {
211
+ const programPath = join(places.dir, CONTEXT_FILE);
212
+ return withLockedFile(programPath, async (programText) => {
213
+ const program = programText === undefined ? { content: {}, declared: [] } : parseContextFile(programText, programPath);
214
+ const instanceText = textIfExists(places.config);
215
+ const instance = instanceText === undefined ? {} : parseInstanceFile(instanceText, places.config);
216
+ const maps = {
217
+ program: program.content,
218
+ instance: (instance.content ?? {}),
219
+ };
220
+ const out = edit(maps);
221
+ if (!sameEntries(out.next.instance, maps.instance)) {
222
+ writeFileAtomic(places.config, instanceFileText(instance, out.next.instance), fileMode(places.config));
223
+ }
224
+ return {
225
+ result: out.result,
226
+ ...(sameEntries(out.next.program, maps.program)
227
+ ? {}
228
+ : { next: contextFileText({ ...program, content: out.next.program }) }),
229
+ };
230
+ }, { mode: fileMode(programPath), create: false });
209
231
  }
232
+ const sameEntries = (a, b) => JSON.stringify(a) === JSON.stringify(b);
210
233
  function textIfExists(path) {
211
234
  return existsSync(path) ? readFileSync(path, "utf8") : undefined;
212
235
  }
213
236
  /**
214
- * What to say when git would commit `context.local.json`: it names directories of this machine, so committed it would
215
- * point every other checkout of the agent at paths it does not have. `init` ignores it; an agent made before that may
216
- * not. Asked only of an agent in a git repository, and never failing an edit that has already been made.
237
+ * Set what the instance configuration says of the channel `channel` (`channels.<channel>` in `fastagent.json` in
238
+ * `agentDir`, or the file `scope.config` names): `entry` over what it holds, the rest of the file kept as it is. Under
239
+ * the lock content edits take, since they write the same file.
217
240
  */
218
- function unignoredLocalFile(dir) {
219
- const git = gitFor("to check that context.local.json stays out of git");
220
- try {
221
- if (git.answer(["rev-parse", "--is-inside-work-tree"], dir) !== "true")
222
- return [];
223
- if (git.answer(["check-ignore", "-q", LOCAL_CONTEXT_FILE], dir) !== undefined)
224
- return [];
225
- }
226
- catch (error) {
227
- if (error instanceof GitNotInstalled)
228
- return [];
229
- throw error;
230
- }
231
- return [`${LOCAL_CONTEXT_FILE} holds directories of this machine and git does not ignore it: add it to .gitignore`];
241
+ export async function setChannelConfig(agentDir, channel, entry, scope) {
242
+ const places = placesOf(resolveAgentDir(agentDir), scope);
243
+ const programPath = join(places.dir, CONTEXT_FILE);
244
+ await withLockedFile(programPath, async () => {
245
+ const text = textIfExists(places.config);
246
+ const file = text === undefined ? {} : parseInstanceFile(text, places.config);
247
+ const channels = (file.channels ?? {});
248
+ const next = { ...file, channels: { ...channels, [channel]: { ...channels[channel], ...entry } } };
249
+ writeFileAtomic(places.config, `${JSON.stringify(next, null, 2)}\n`, fileMode(places.config));
250
+ return { result: undefined };
251
+ }, { mode: fileMode(programPath), create: false });
232
252
  }
233
253
  /**
234
254
  * Replace the environment `context.json` declares with what `change` makes of it as it is now, under the lock content
@@ -13,7 +13,9 @@ export declare const ENVELOPE_KINDS: readonly ["webhook", "schedule-fire", "wake
13
13
  *
14
14
  * WHAT IT ACTUALLY GUARDS, in one line: only the forwarder may tell the container where the forwarder is. That URL
15
15
  * (`envelope.wake.url`) is where the container later POSTs its wake alarms, carrying `FASTAGENT_WAKE_SECRET` — so a
16
- * principal who could set it would redirect that callback and collect the secret along with every pending wake-up.
16
+ * principal who could set it would redirect that callback and collect the secret along with every pending wake-up. It
17
+ * is also where the container points its webhook channels (their platform's credentials sent to the platform, and a
18
+ * URL the platform would then deliver every message to).
17
19
  *
18
20
  * The other thing it gates — the kinds other than `api` — is DEPTH, not a boundary. Every one of them is weaker than
19
21
  * the `api` the same caller may already send: `schedule-fire` runs a prompt the definition wrote down,
@@ -40,6 +42,7 @@ export interface CarriedRequest {
40
42
  export type AgentcoreEnvelope = {
41
43
  /** Shared secret proving this envelope came from the forwarder (FASTAGENT_INGRESS_SECRET). */
42
44
  auth?: string;
45
+ /** Where the forwarder is: the wake-alarm callback, and where the platforms reach the webhook channels. */
43
46
  wake?: {
44
47
  url: string;
45
48
  };
@@ -64,8 +67,10 @@ export type AgentcoreEnvelope = {
64
67
  }
65
68
  /**
66
69
  * The deploy driver's post-deploy verification (relayed by the forwarder's reserved probe path, which answers on
67
- * EVERY forwarder topology — schedule-only URLs refuse ordinary public traffic). Also accepted from the IAM door,
68
- * where it only opens the deployed definition (`login --deployment` needs one) and does not construct the channels.
70
+ * EVERY forwarder topology — schedule-only URLs refuse ordinary public traffic). It answers `{ ok, webhooks }`: the
71
+ * outcome of each webhook channel the container points itself, `pending` until it ends. Also accepted from the IAM
72
+ * door, where it only opens the deployed definition (`login --deployment` needs one) and does not construct the
73
+ * channels.
69
74
  */
70
75
  | {
71
76
  kind: "probe";
@@ -15,7 +15,9 @@ export const ENVELOPE_KINDS = ["webhook", "schedule-fire", "wake-poke", "probe"]
15
15
  *
16
16
  * WHAT IT ACTUALLY GUARDS, in one line: only the forwarder may tell the container where the forwarder is. That URL
17
17
  * (`envelope.wake.url`) is where the container later POSTs its wake alarms, carrying `FASTAGENT_WAKE_SECRET` — so a
18
- * principal who could set it would redirect that callback and collect the secret along with every pending wake-up.
18
+ * principal who could set it would redirect that callback and collect the secret along with every pending wake-up. It
19
+ * is also where the container points its webhook channels (their platform's credentials sent to the platform, and a
20
+ * URL the platform would then deliver every message to).
19
21
  *
20
22
  * The other thing it gates — the kinds other than `api` — is DEPTH, not a boundary. Every one of them is weaker than
21
23
  * the `api` the same caller may already send: `schedule-fire` runs a prompt the definition wrote down,
@@ -24,4 +24,5 @@ export declare function mountAgentcore(options: {
24
24
  onStateReady?: () => void;
25
25
  /** The channel surface, initialized once by the adapter on trusted ingress. */
26
26
  channels: AgentcoreAdapterOptions["channels"];
27
+ webhooks?: AgentcoreAdapterOptions["webhooks"];
27
28
  }): Routes;