@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.
- package/README.md +9 -4
- package/dist/atomic-write.d.ts +4 -5
- package/dist/atomic-write.js +4 -5
- package/dist/authoring.d.ts +32 -15
- package/dist/authoring.js +147 -127
- package/dist/channels/agentcore-protocol.d.ts +8 -3
- package/dist/channels/agentcore-protocol.js +3 -1
- package/dist/channels/agentcore-service.d.ts +1 -0
- package/dist/channels/agentcore-service.js +14 -4
- package/dist/channels/agentcore.d.ts +9 -0
- package/dist/channels/agentcore.js +29 -4
- package/dist/channels/context.d.ts +2 -0
- package/dist/channels/context.js +5 -1
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.js +3 -5
- package/dist/channels/feishu/scaffold/channel.ts +32 -34
- package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
- package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
- package/dist/channels/feishu/setup-mode.d.ts +2 -12
- package/dist/channels/feishu/setup-mode.js +2 -22
- package/dist/channels/feishu/setup.d.ts +12 -0
- package/dist/channels/feishu/setup.js +197 -0
- package/dist/channels/kit/abortable.d.ts +8 -0
- package/dist/channels/kit/abortable.js +22 -0
- package/dist/channels/lark/lark.d.ts +0 -3
- package/dist/channels/lark/lark.js +0 -5
- package/dist/channels/lark/scaffold/channel.ts +33 -35
- package/dist/channels/lark/scaffold/lark-send.ts +5 -5
- package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
- package/dist/channels/lark/setup.d.ts +9 -0
- package/dist/channels/lark/setup.js +57 -0
- package/dist/channels/setup.d.ts +112 -0
- package/dist/channels/setup.js +31 -0
- package/dist/channels/slack/history.js +1 -1
- package/dist/channels/slack/manifest.d.ts +17 -7
- package/dist/channels/slack/manifest.js +12 -16
- package/dist/channels/slack/scaffold/channel.ts +44 -30
- package/dist/channels/slack/scaffold/slack-send.ts +8 -8
- package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
- package/dist/channels/slack/setup.d.ts +7 -0
- package/dist/channels/slack/setup.js +119 -0
- package/dist/channels/slack/slack-api.d.ts +2 -0
- package/dist/channels/slack/slack-api.js +4 -0
- package/dist/channels/slack/slack.d.ts +21 -2
- package/dist/channels/slack/slack.js +398 -338
- package/dist/channels/slack/socket.d.ts +57 -0
- package/dist/channels/slack/socket.js +305 -0
- package/dist/channels/telegram/polling.d.ts +27 -0
- package/dist/channels/telegram/polling.js +105 -0
- package/dist/channels/telegram/scaffold/channel.ts +52 -32
- package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
- package/dist/channels/telegram/setup.d.ts +2 -0
- package/dist/channels/telegram/setup.js +42 -0
- package/dist/channels/telegram/telegram-api.d.ts +27 -1
- package/dist/channels/telegram/telegram-api.js +7 -6
- package/dist/channels/telegram/telegram.d.ts +11 -3
- package/dist/channels/telegram/telegram.js +184 -157
- package/dist/channels/webhook-evidence.d.ts +11 -0
- package/dist/channels/webhook-evidence.js +71 -0
- package/dist/channels/webhooks.d.ts +37 -0
- package/dist/channels/webhooks.js +70 -0
- package/dist/child-env.d.ts +16 -0
- package/dist/child-env.js +30 -0
- package/dist/cli/box-login.js +4 -1
- package/dist/cli/channel-app.d.ts +3 -0
- package/dist/cli/channel-app.js +35 -0
- package/dist/cli/channel-setup.d.ts +38 -0
- package/dist/cli/channel-setup.js +195 -0
- package/dist/cli/commands/add.d.ts +6 -14
- package/dist/cli/commands/add.js +45 -303
- package/dist/cli/commands/auth.js +8 -2
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +16 -0
- package/dist/cli/commands/channels.d.ts +19 -0
- package/dist/cli/commands/channels.js +230 -0
- package/dist/cli/commands/content.d.ts +7 -3
- package/dist/cli/commands/content.js +16 -10
- package/dist/cli/commands/deploy/agentcore.js +55 -23
- package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
- package/dist/cli/commands/deploy/channel-apps.js +30 -0
- package/dist/cli/commands/deploy/docker.js +32 -65
- package/dist/cli/commands/deploy/fly.js +28 -12
- package/dist/cli/commands/deploy/railway.js +38 -13
- package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
- package/dist/cli/commands/deploy/secrets-step.js +66 -0
- package/dist/cli/commands/deploy/shared.d.ts +16 -5
- package/dist/cli/commands/deploy/shared.js +25 -8
- package/dist/cli/commands/deploy.js +53 -60
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +15 -3
- package/dist/cli/commands/env.js +1 -1
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +60 -25
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.js +20 -7
- package/dist/cli/commands/login.d.ts +0 -7
- package/dist/cli/commands/login.js +22 -10
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.js +14 -6
- package/dist/cli/commands/models.js +3 -2
- package/dist/cli/commands/schedules.d.ts +4 -2
- package/dist/cli/commands/schedules.js +14 -10
- package/dist/cli/commands/secrets.d.ts +14 -0
- package/dist/cli/commands/secrets.js +213 -0
- package/dist/cli/commands/start.d.ts +14 -14
- package/dist/cli/commands/start.js +46 -60
- package/dist/cli/content-view.d.ts +4 -6
- package/dist/cli/content-view.js +5 -11
- package/dist/cli/first-run-channels.d.ts +3 -0
- package/dist/cli/first-run-channels.js +63 -0
- package/dist/cli/instance.d.ts +41 -0
- package/dist/cli/instance.js +155 -0
- package/dist/cli/invoke-stream.d.ts +5 -2
- package/dist/cli/invoke-stream.js +7 -2
- package/dist/cli/program.js +282 -110
- package/dist/cli/serve.d.ts +4 -5
- package/dist/cli/serve.js +4 -7
- package/dist/cli/shared.d.ts +11 -2
- package/dist/cli/shared.js +32 -19
- package/dist/cli/ticket.js +50 -6
- package/dist/content/declare.d.ts +31 -37
- package/dist/content/declare.js +59 -60
- package/dist/content/file.d.ts +23 -12
- package/dist/content/file.js +48 -35
- package/dist/content/git.d.ts +0 -10
- package/dist/content/git.js +5 -39
- package/dist/content/resolve.d.ts +18 -22
- package/dist/content/resolve.js +32 -66
- package/dist/content/source.d.ts +7 -8
- package/dist/content/source.js +10 -28
- package/dist/credentials-page.d.ts +26 -0
- package/dist/credentials-page.js +185 -0
- package/dist/declared-secrets.d.ts +10 -3
- package/dist/declared-secrets.js +8 -4
- package/dist/deploy/agentcore/destroy.d.ts +4 -1
- package/dist/deploy/agentcore/destroy.js +25 -1
- package/dist/deploy/agentcore/forwarder.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +7 -21
- package/dist/deploy/agentcore/plan.js +47 -49
- package/dist/deploy/agentcore/run.d.ts +18 -10
- package/dist/deploy/agentcore/run.js +93 -70
- package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
- package/dist/deploy/agentcore/runtime-secrets.js +108 -0
- package/dist/deploy/agentcore/shell.js +12 -19
- package/dist/deploy/agentcore/sigv4.d.ts +7 -0
- package/dist/deploy/agentcore/sigv4.js +15 -0
- package/dist/deploy/agentcore/slr.d.ts +18 -0
- package/dist/deploy/agentcore/slr.js +27 -0
- package/dist/deploy/agentcore/store.d.ts +7 -0
- package/dist/deploy/agentcore/store.js +115 -0
- package/dist/deploy/api.d.ts +14 -14
- package/dist/deploy/api.js +26 -24
- package/dist/deploy/box-shell.d.ts +3 -1
- package/dist/deploy/build-context.d.ts +4 -4
- package/dist/deploy/build-context.js +11 -15
- package/dist/deploy/channel-ingress.d.ts +35 -9
- package/dist/deploy/channel-ingress.js +119 -22
- package/dist/deploy/container.d.ts +35 -10
- package/dist/deploy/container.js +79 -37
- package/dist/deploy/delivered-text.d.ts +13 -0
- package/dist/deploy/delivered-text.js +86 -0
- package/dist/deploy/docker/plan.d.ts +7 -8
- package/dist/deploy/docker/plan.js +31 -52
- package/dist/deploy/docker/run.d.ts +5 -43
- package/dist/deploy/docker/run.js +9 -84
- package/dist/deploy/file-store.d.ts +2 -0
- package/dist/deploy/file-store.js +17 -0
- package/dist/deploy/fly/plan.d.ts +3 -2
- package/dist/deploy/fly/plan.js +20 -9
- package/dist/deploy/fly/run.d.ts +18 -8
- package/dist/deploy/fly/run.js +24 -42
- package/dist/deploy/fly/store.d.ts +12 -0
- package/dist/deploy/fly/store.js +83 -0
- package/dist/deploy/hosts.d.ts +1 -2
- package/dist/deploy/hosts.js +1 -2
- package/dist/deploy/preflight.d.ts +18 -5
- package/dist/deploy/preflight.js +135 -63
- package/dist/deploy/railway/ignore.d.ts +8 -0
- package/dist/deploy/railway/ignore.js +57 -0
- package/dist/deploy/railway/plan.d.ts +1 -2
- package/dist/deploy/railway/plan.js +8 -6
- package/dist/deploy/railway/run.d.ts +21 -15
- package/dist/deploy/railway/run.js +32 -78
- package/dist/deploy/railway/store.d.ts +29 -0
- package/dist/deploy/railway/store.js +143 -0
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +4 -3
- package/dist/deploy/secret-store.d.ts +63 -0
- package/dist/deploy/secret-store.js +98 -0
- package/dist/deploy/secrets.d.ts +29 -46
- package/dist/deploy/secrets.js +49 -122
- package/dist/deploy/workspace.d.ts +6 -12
- package/dist/deploy/workspace.js +18 -94
- package/dist/dev-supervisor.js +6 -8
- package/dist/environment/declare.js +3 -2
- package/dist/environment/mise.d.ts +3 -1
- package/dist/environment/mise.js +4 -2
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/harnesses/models/catalog.d.ts +1 -0
- package/dist/harnesses/models/catalog.js +1 -2
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +9 -5
- package/dist/harnesses/models/host.d.ts +0 -7
- package/dist/harnesses/models/host.js +10 -1
- package/dist/harnesses/models/login.d.ts +0 -8
- package/dist/harnesses/models/login.js +5 -4
- package/dist/harnesses/models/models.d.ts +7 -2
- package/dist/harnesses/models/models.js +28 -4
- package/dist/harnesses/pi-durable/agent.js +59 -38
- package/dist/harnesses/pi-durable/build.d.ts +12 -0
- package/dist/harnesses/pi-durable/build.js +270 -0
- package/dist/harnesses/pi-durable/built.d.ts +24 -0
- package/dist/harnesses/pi-durable/built.js +136 -0
- package/dist/harnesses/pi-durable/config.d.ts +81 -0
- package/dist/harnesses/pi-durable/config.js +266 -0
- package/dist/harnesses/pi-durable/define.d.ts +63 -13
- package/dist/harnesses/pi-durable/define.js +23 -2
- package/dist/harnesses/pi-durable/definition.d.ts +72 -0
- package/dist/harnesses/pi-durable/definition.js +143 -0
- package/dist/harnesses/pi-durable/events.d.ts +4 -3
- package/dist/harnesses/pi-durable/events.js +179 -136
- package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
- package/dist/harnesses/pi-durable/execution-env.js +11 -0
- package/dist/harnesses/pi-durable/mcp.js +1 -3
- package/dist/harnesses/pi-durable/open.d.ts +35 -37
- package/dist/harnesses/pi-durable/open.js +91 -153
- package/dist/harnesses/pi-durable/reads.d.ts +0 -1
- package/dist/harnesses/pi-durable/reads.js +1 -3
- package/dist/harnesses/pi-durable/registry.d.ts +4 -6
- package/dist/harnesses/pi-durable/registry.js +29 -26
- package/dist/harnesses/pi-durable/release.d.ts +2 -2
- package/dist/harnesses/pi-durable/release.js +10 -7
- package/dist/harnesses/pi-durable/serve.d.ts +91 -15
- package/dist/harnesses/pi-durable/serve.js +109 -47
- package/dist/harnesses/pi-durable/sessions.js +15 -12
- package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
- package/dist/harnesses/pi-durable/text-sources.js +55 -0
- package/dist/harnesses/pi-durable/text.d.ts +16 -4
- package/dist/harnesses/pi-durable/text.js +115 -59
- package/dist/harnesses/pi-durable/wake.js +8 -8
- package/dist/http/client.js +9 -3
- package/dist/http/contract.d.ts +3 -3
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -1
- package/dist/instance-name.d.ts +7 -0
- package/dist/instance-name.js +26 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/mcp/client.js +11 -2
- package/dist/mcp/config.d.ts +70 -18
- package/dist/mcp/config.js +136 -53
- package/dist/mcp/host.d.ts +5 -2
- package/dist/mcp/host.js +14 -6
- package/dist/mcp/sign-in.js +2 -2
- package/dist/mcp-api.d.ts +14 -0
- package/dist/paths.d.ts +22 -8
- package/dist/paths.js +44 -11
- package/dist/runtime-websocket.d.ts +14 -0
- package/dist/runtime-websocket.js +39 -0
- package/dist/scaffold/add-channel.d.ts +9 -16
- package/dist/scaffold/add-channel.js +13 -83
- package/dist/scaffold/templates/env.example +4 -5
- package/dist/scaffold/templates/fastagent.config.ts +20 -14
- package/dist/scaffold/templates/gitignore +9 -6
- package/dist/scaffold/templates.d.ts +6 -2
- package/dist/scaffold/templates.js +7 -2
- package/dist/scaffold/vendor-skill.js +4 -2
- package/dist/service.d.ts +44 -6
- package/dist/service.js +110 -10
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +1 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/strip-edges.d.ts +10 -0
- package/dist/strip-edges.js +22 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +0 -1
- package/dist/tunnel.js +2 -8
- package/dist/value-file.d.ts +11 -6
- package/dist/value-file.js +21 -9
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +6 -5
- package/dist/channels/lark/credentials-page.d.ts +0 -25
- package/dist/channels/lark/credentials-page.js +0 -206
- package/dist/channels/lark/onboard.d.ts +0 -52
- package/dist/channels/lark/onboard.js +0 -86
- package/dist/channels/slack/config-api.d.ts +0 -69
- package/dist/channels/slack/config-api.js +0 -162
- package/dist/channels/slack/onboard.d.ts +0 -40
- package/dist/channels/slack/onboard.js +0 -136
- package/dist/channels/slack/onboarding-state.d.ts +0 -31
- package/dist/channels/slack/onboarding-state.js +0 -64
- package/dist/channels/slack/register-webhook.d.ts +0 -12
- package/dist/channels/slack/register-webhook.js +0 -67
- package/dist/channels/slack/setup-server.d.ts +0 -32
- package/dist/channels/slack/setup-server.js +0 -246
- package/dist/cli/add-feishu.d.ts +0 -41
- package/dist/cli/add-feishu.js +0 -391
- package/dist/cli/add-slack.d.ts +0 -18
- package/dist/cli/add-slack.js +0 -216
- package/dist/harnesses/pi-durable/parked.d.ts +0 -18
- 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;
|
|
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
|
-
|
|
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" })) {
|
package/dist/atomic-write.d.ts
CHANGED
|
@@ -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.
|
|
7
|
-
*
|
|
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
|
|
15
|
-
*
|
|
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
|
*/
|
package/dist/atomic-write.js
CHANGED
|
@@ -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.
|
|
11
|
-
*
|
|
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
|
|
19
|
-
*
|
|
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
|
*/
|
package/dist/authoring.d.ts
CHANGED
|
@@ -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`,
|
|
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
|
|
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.
|
|
58
|
-
*
|
|
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`:
|
|
74
|
-
* `
|
|
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
|
|
80
|
-
*
|
|
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 {
|
|
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 {
|
|
16
|
-
import { contextFileText, loadContent,
|
|
17
|
-
import {
|
|
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`
|
|
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
|
|
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
|
|
47
|
-
let maps = {
|
|
48
|
-
for (const addition of
|
|
49
|
-
maps = withAddition(
|
|
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,
|
|
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.
|
|
61
|
-
writeFileAtomic(join(agentDir, CONTEXT_FILE), contextFileText({ content: maps.
|
|
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.
|
|
64
|
-
writeFileAtomic(
|
|
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(
|
|
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.
|
|
77
|
-
*
|
|
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
|
|
81
|
-
|
|
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`:
|
|
86
|
-
* `
|
|
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
|
|
91
|
-
const declaration = await
|
|
92
|
-
const
|
|
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
|
-
|
|
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
|
|
104
|
-
*
|
|
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
|
|
109
|
-
const { removed,
|
|
110
|
-
const found = entryNamed(
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
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 (
|
|
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(
|
|
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
|
|
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(
|
|
138
|
-
const { name, entry,
|
|
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.
|
|
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
|
-
|
|
148
|
-
content:
|
|
149
|
-
|
|
150
|
-
};
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
167
|
-
|
|
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
|
|
182
|
-
const
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
192
|
-
* taken is asked of both
|
|
193
|
-
* creates neither file; what `edit` returns
|
|
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
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
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
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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).
|
|
68
|
-
*
|
|
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;
|