@fastagent-sh/fastagent 0.29.0 → 0.30.1
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/kernel.js +3 -0
- 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.d.ts +7 -0
- package/dist/cli/ticket.js +106 -30
- 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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** The gap the target has, as the line a person (or the agent relaying one) acts on. */
|
|
2
|
+
function gapLine(host, missing, held, commands) {
|
|
3
|
+
const names = missing.map((need) => `${need.name} (${need.hint})`).join("; ");
|
|
4
|
+
const fromFile = missing.filter((need) => held.includes(need.name));
|
|
5
|
+
const rest = missing.filter((need) => !held.includes(need.name));
|
|
6
|
+
const ways = [
|
|
7
|
+
...(fromFile.length > 0
|
|
8
|
+
? [
|
|
9
|
+
`\`${commands.push}\` sets ${fromFile.length === 1 ? "the one" : "the ones"} a value file holds (${fromFile.map((n) => n.name).join(", ")})`,
|
|
10
|
+
]
|
|
11
|
+
: []),
|
|
12
|
+
...(rest.length > 0
|
|
13
|
+
? [
|
|
14
|
+
`a person types ${fromFile.length > 0 ? "the others" : rest.length === 1 ? "it" : "them"} in a terminal, one command each: ${rest.map((need) => `\`${commands.set(need.name)}\``).join(", ")}`,
|
|
15
|
+
]
|
|
16
|
+
: []),
|
|
17
|
+
`\`${commands.push} --from-env\` takes them from the environment of a secret manager or a CI job (\`op run\`, \`doppler run\`)`,
|
|
18
|
+
];
|
|
19
|
+
return `${host} has no value for: ${names}. ${ways.join("; ")}. Then re-run. A value is never asked for in a conversation`;
|
|
20
|
+
}
|
|
21
|
+
/** Resolves a gate line when the target still lacks a required name afterwards, undefined once it holds them all. */
|
|
22
|
+
export async function provideSecrets(input) {
|
|
23
|
+
const { host, store, needs, known, source, asker, commands, mint = [] } = input;
|
|
24
|
+
try {
|
|
25
|
+
const have = await store.names();
|
|
26
|
+
const missing = needs.filter((need) => !have.has(need.name));
|
|
27
|
+
const ours = new Set(mint.map((made) => made.name));
|
|
28
|
+
const filed = (name) => {
|
|
29
|
+
const value = source?.values.get(name);
|
|
30
|
+
return value !== undefined && value.trim() !== "" ? value : undefined;
|
|
31
|
+
};
|
|
32
|
+
// What FastAgent made is not the person's to be asked about: the file records it, so a target that lacks it (a
|
|
33
|
+
// deployment made again) gets that value, and one the file lacks too gets a new one.
|
|
34
|
+
const minted = {};
|
|
35
|
+
const restored = [];
|
|
36
|
+
for (const { name, make } of mint) {
|
|
37
|
+
if (have.has(name))
|
|
38
|
+
continue;
|
|
39
|
+
const kept = filed(name);
|
|
40
|
+
if (kept === undefined)
|
|
41
|
+
minted[name] = make();
|
|
42
|
+
else {
|
|
43
|
+
minted[name] = kept;
|
|
44
|
+
restored.push(name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const offered = [...(source?.values ?? [])]
|
|
48
|
+
.filter(([name, value]) => value.trim() !== "" && known.includes(name) && !have.has(name) && !ours.has(name))
|
|
49
|
+
.map(([name]) => name);
|
|
50
|
+
if (missing.length === 0 && offered.length === 0 && Object.keys(minted).length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const finishMinted = async () => {
|
|
53
|
+
if (restored.length > 0) {
|
|
54
|
+
input.inform?.(`set ${restored.join(", ")} on ${host} from ${source?.file}, where an earlier deploy kept it`);
|
|
55
|
+
}
|
|
56
|
+
const made = Object.fromEntries(Object.entries(minted).filter(([name]) => !restored.includes(name)));
|
|
57
|
+
if (Object.keys(made).length > 0)
|
|
58
|
+
await input.onMinted?.(made);
|
|
59
|
+
};
|
|
60
|
+
const held = offered.filter((name) => missing.some((need) => need.name === name));
|
|
61
|
+
if (asker === undefined) {
|
|
62
|
+
const lacking = missing.filter((need) => !(need.name in minted));
|
|
63
|
+
if (lacking.length > 0)
|
|
64
|
+
return gapLine(host, lacking, held, commands);
|
|
65
|
+
// Names a file holds and the target lacks, which nothing requires: said, since nobody was asked to set them.
|
|
66
|
+
if (offered.length > 0 && source !== undefined) {
|
|
67
|
+
input.say?.(`${host} lacks ${offered.join(", ")}, which ${source.file} holds; \`${commands.push}\` sets ${offered.length === 1 ? "it" : "them"}`);
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(minted).length > 0) {
|
|
70
|
+
await store.write(minted);
|
|
71
|
+
await finishMinted();
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const write = { ...minted };
|
|
76
|
+
if (offered.length > 0 && source !== undefined) {
|
|
77
|
+
const required = new Set(missing.map((need) => need.name));
|
|
78
|
+
const list = offered.map((name) => (required.has(name) ? name : `${name} (optional)`)).join(", ");
|
|
79
|
+
if (await asker.confirm(`${host} lacks ${list}, which ${source.file} holds. Set them on ${host} from it?`)) {
|
|
80
|
+
for (const name of offered)
|
|
81
|
+
write[name] = source.values.get(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const need of missing) {
|
|
85
|
+
if (need.name in write)
|
|
86
|
+
continue;
|
|
87
|
+
write[need.name] = await asker.secret(need);
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(write).length > 0)
|
|
90
|
+
await store.write(write);
|
|
91
|
+
if (Object.keys(minted).length > 0)
|
|
92
|
+
await finishMinted();
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return `${host}'s values could not be read or set: ${error instanceof Error ? error.message : String(error)}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The environment a deployed agent runs with — host-neutral.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* definition declares (`defineTool`/`defineChannel`, the `variables` of `context.json`, plus the model's env key)
|
|
6
|
-
* which
|
|
4
|
+
* Docker, Fly and Railway keep a deployment's values in a store of their own and are never handed a file (secret-store.ts):
|
|
5
|
+
* what the definition declares (`defineTool`/`defineChannel`, the `variables` of `context.json`, plus the model's env key)
|
|
6
|
+
* says which names must have a value there. AgentCore's store is a Secrets Manager secret (agentcore/store.ts).
|
|
7
7
|
*/
|
|
8
8
|
import { type DeclaredSecret } from "../declared-secrets.ts";
|
|
9
9
|
/** Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none? */
|
|
10
10
|
export declare function isEnvKey(source: string | undefined): source is string;
|
|
11
|
-
/** Is `name` one the deployment sets itself (see {@link DEPLOY_OWNED})
|
|
11
|
+
/** Is `name` one the deployment sets itself (see {@link DEPLOY_OWNED})? */
|
|
12
12
|
export declare function isDeployOwned(name: string): boolean;
|
|
13
13
|
/** One variable a runbook tells the operator to set, and where to look for its value. */
|
|
14
14
|
export interface DeploymentSecret {
|
|
@@ -16,52 +16,35 @@ export interface DeploymentSecret {
|
|
|
16
16
|
hint: string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* What a runbook lists: the names
|
|
20
|
-
* that declared it)
|
|
19
|
+
* What a runbook lists: the names the deployment declares (the model's env key, every declaration — hinted by the file
|
|
20
|
+
* that declared it).
|
|
21
21
|
*/
|
|
22
|
-
export declare function deploymentSecrets(modelAuth: string | undefined, declared: readonly DeclaredSecret[]
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/** Every name the definition declared it needs — what must have a value. */
|
|
36
|
-
declared?: readonly DeclaredSecret[];
|
|
37
|
-
/** The selected value file's contents (`loadEnvValues`). The ONLY source of operator-supplied values. */
|
|
38
|
-
values: ReadonlyMap<string, string>;
|
|
39
|
-
}): {
|
|
40
|
-
secrets: Record<string, string>;
|
|
41
|
-
missingSecrets: string[];
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* The stop for a host CLI signed out with nobody to sign it in. A token goes in the shell's (or CI's) environment,
|
|
45
|
-
* never in the production value file: everything there travels to the box, which runs an agent with a shell.
|
|
46
|
-
*/
|
|
47
|
-
export declare function hostSignedOutGate(host: string, login: string, token: string, forAgent: string): string;
|
|
22
|
+
export declare function deploymentSecrets(modelAuth: string | undefined, declared: readonly DeclaredSecret[]): DeploymentSecret[];
|
|
23
|
+
/** One name a deployment cannot start without, and where to find out what it is for. */
|
|
24
|
+
export interface SecretNeed {
|
|
25
|
+
name: string;
|
|
26
|
+
hint: string;
|
|
27
|
+
}
|
|
28
|
+
/** What the model's credential asks of the target (preflight.ts's `modelKey`). */
|
|
29
|
+
export interface ModelKey {
|
|
30
|
+
/** The variables the provider reads its key from. */
|
|
31
|
+
variables: readonly string[];
|
|
32
|
+
/** One of them must be set: the value file holds no key and the provider has no login to run on the box. */
|
|
33
|
+
required: boolean;
|
|
34
|
+
}
|
|
48
35
|
/**
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
51
|
-
*
|
|
36
|
+
* The names the target must hold a value for: the key of a model with no other way in, the model's key when the value
|
|
37
|
+
* file holds one, and every declaration that does not say the code runs without it, each hinted by the file that
|
|
38
|
+
* declared it. A name some declaration requires is required.
|
|
52
39
|
*/
|
|
53
|
-
export declare function
|
|
40
|
+
export declare function secretNeeds(modelAuth: string | undefined, declared: readonly DeclaredSecret[], modelKey?: ModelKey): SecretNeed[];
|
|
54
41
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* value, so planning with this one would deploy a different channel than the one planned for: an export in this shell
|
|
58
|
-
* is the usual cause.
|
|
42
|
+
* Every name the definition declares, optional ones and the model's keys included: what a source may supply, and what
|
|
43
|
+
* `secrets list` shows.
|
|
59
44
|
*/
|
|
60
|
-
export declare function
|
|
45
|
+
export declare function declaredNames(modelAuth: string | undefined, declared: readonly DeclaredSecret[], modelKey?: ModelKey): string[];
|
|
61
46
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
47
|
+
* The stop for a host CLI signed out with nobody to sign it in. A token goes in the shell's (or CI's) environment,
|
|
48
|
+
* never in the production value file, which holds the deployment's values and not the CLI's.
|
|
64
49
|
*/
|
|
65
|
-
export declare function
|
|
66
|
-
/** The carried variables as the base64 JSON {@link collectCarriedEnv} reads back; empty when nothing travels. */
|
|
67
|
-
export declare function encodeCarriedEnv(carried: Readonly<Record<string, string>>): string;
|
|
50
|
+
export declare function hostSignedOutGate(host: string, login: string, token: string, forAgent: string): string;
|
package/dist/deploy/secrets.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The environment a deployed agent runs with — host-neutral.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* definition declares (`defineTool`/`defineChannel`, the `variables` of `context.json`, plus the model's env key)
|
|
6
|
-
* which
|
|
4
|
+
* Docker, Fly and Railway keep a deployment's values in a store of their own and are never handed a file (secret-store.ts):
|
|
5
|
+
* what the definition declares (`defineTool`/`defineChannel`, the `variables` of `context.json`, plus the model's env key)
|
|
6
|
+
* says which names must have a value there. AgentCore's store is a Secrets Manager secret (agentcore/store.ts).
|
|
7
7
|
*/
|
|
8
8
|
import { dedupeSecrets } from "../declared-secrets.js";
|
|
9
9
|
import { DEV_SERVE_ENV } from "../serving-command.js";
|
|
@@ -12,42 +12,34 @@ import { AGENT_ENVIRONMENT_ENV } from "../paths.js";
|
|
|
12
12
|
export function isEnvKey(source) {
|
|
13
13
|
return source !== undefined && /^[A-Z][A-Z0-9_]*$/.test(source);
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Names a deployment sets on the box itself. The value file's copy of one describes THIS machine (a local state path,
|
|
17
|
-
* a port) or rides another carrier (the model rides the release manifest), so it never travels.
|
|
18
|
-
*/
|
|
15
|
+
/** Names a deployment sets on the box itself: a value for one describes THIS machine (a local state path, a port). */
|
|
19
16
|
const DEPLOY_OWNED = new Set([
|
|
20
17
|
"PORT",
|
|
21
18
|
"FASTAGENT_MODEL",
|
|
19
|
+
"FASTAGENT_CONFIG",
|
|
20
|
+
"FASTAGENT_ROOT",
|
|
21
|
+
"FASTAGENT_WORKDIR",
|
|
22
22
|
"FASTAGENT_STATE_DIR",
|
|
23
23
|
"FASTAGENT_SECRETS_DIR",
|
|
24
24
|
"FASTAGENT_AUTH_PATH",
|
|
25
25
|
"FASTAGENT_RELEASE_FILE",
|
|
26
|
-
"FASTAGENT_STORAGE_DIR",
|
|
27
26
|
"FASTAGENT_AGENTCORE",
|
|
27
|
+
"FASTAGENT_SECRET_ARN",
|
|
28
28
|
"FASTAGENT_INGRESS_SECRET",
|
|
29
29
|
"FASTAGENT_WAKE_SECRET",
|
|
30
30
|
"FASTAGENT_DEV_WORKER",
|
|
31
31
|
DEV_SERVE_ENV,
|
|
32
32
|
AGENT_ENVIRONMENT_ENV,
|
|
33
33
|
]);
|
|
34
|
-
/** Is `name` one the deployment sets itself (see {@link DEPLOY_OWNED})
|
|
34
|
+
/** Is `name` one the deployment sets itself (see {@link DEPLOY_OWNED})? */
|
|
35
35
|
export function isDeployOwned(name) {
|
|
36
|
-
return DEPLOY_OWNED.has(name)
|
|
37
|
-
}
|
|
38
|
-
/** The value-file variables that travel: everything with a value that the deployment does not set itself. */
|
|
39
|
-
function carriedValues(values) {
|
|
40
|
-
const carried = {};
|
|
41
|
-
for (const [name, value] of values)
|
|
42
|
-
if (value && !isDeployOwned(name))
|
|
43
|
-
carried[name] = value;
|
|
44
|
-
return carried;
|
|
36
|
+
return DEPLOY_OWNED.has(name);
|
|
45
37
|
}
|
|
46
38
|
/**
|
|
47
|
-
* What a runbook lists: the names
|
|
48
|
-
* that declared it)
|
|
39
|
+
* What a runbook lists: the names the deployment declares (the model's env key, every declaration — hinted by the file
|
|
40
|
+
* that declared it).
|
|
49
41
|
*/
|
|
50
|
-
export function deploymentSecrets(modelAuth, declared
|
|
42
|
+
export function deploymentSecrets(modelAuth, declared) {
|
|
51
43
|
const secrets = [];
|
|
52
44
|
const add = (name, hint) => {
|
|
53
45
|
if (!secrets.some((s) => s.name === name))
|
|
@@ -55,118 +47,53 @@ export function deploymentSecrets(modelAuth, declared, values, valueFile) {
|
|
|
55
47
|
};
|
|
56
48
|
if (isEnvKey(modelAuth))
|
|
57
49
|
add(modelAuth, "your model provider key");
|
|
58
|
-
for (const { name
|
|
59
|
-
|
|
50
|
+
for (const { name } of dedupeSecrets(declared)) {
|
|
51
|
+
// Hinted by a declaration that requires it, when one does: a name some declaration requires is required.
|
|
52
|
+
const declarations = declared.filter((s) => s.name === name);
|
|
53
|
+
const { declaredIn, description, optional } = declarations.find((s) => !s.optional) ?? declarations[0];
|
|
54
|
+
add(name, `${optional ? "optional, read by" : "required by"} ${declaredIn}${description ? `: ${description}` : ""}`);
|
|
60
55
|
}
|
|
61
|
-
for (const name of Object.keys(carriedValues(values)))
|
|
62
|
-
add(name, `from ${valueFile}`);
|
|
63
56
|
return secrets;
|
|
64
57
|
}
|
|
65
58
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* deployment must be reproducible from what it carries, and a variable that happens to be exported on the builder is
|
|
70
|
-
* written down nowhere. CI supplies values by writing the file before running the command.
|
|
71
|
-
*
|
|
72
|
-
* No credentials file travels: a model that does not authenticate through a variable (or its own definition) logs in
|
|
73
|
-
* on the deployment itself (`fastagent login --deployment`), so the box is the only holder of its grant.
|
|
59
|
+
* The names the target must hold a value for: the key of a model with no other way in, the model's key when the value
|
|
60
|
+
* file holds one, and every declaration that does not say the code runs without it, each hinted by the file that
|
|
61
|
+
* declared it. A name some declaration requires is required.
|
|
74
62
|
*/
|
|
75
|
-
export function
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!secrets[name] && !missingSecrets.includes(name))
|
|
82
|
-
missingSecrets.push(name);
|
|
63
|
+
export function secretNeeds(modelAuth, declared, modelKey) {
|
|
64
|
+
const needs = [];
|
|
65
|
+
if (isEnvKey(modelAuth))
|
|
66
|
+
needs.push({ name: modelAuth, hint: "your model provider key" });
|
|
67
|
+
else if (modelKey?.required && modelKey.variables[0] !== undefined) {
|
|
68
|
+
needs.push({ name: modelKey.variables[0], hint: "your model provider key: it has no login to run on the box" });
|
|
83
69
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*/
|
|
90
|
-
export function hostSignedOutGate(host, login, token, forAgent) {
|
|
91
|
-
return (`not logged in to ${host} — run \`${login}\` in a terminal (it opens a browser), or set ${token} in this ` +
|
|
92
|
-
`shell's environment, never in .secrets/production/.env (everything there travels to the box); then re-run. ` +
|
|
93
|
-
`From a coding agent, \`${forAgent}\` hands the sign-in to its user`);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* The ONE refusal for declared names the deployed environment does not supply a value for — every host reaches it
|
|
97
|
-
* before its first side effect. Host-neutral because the reason is: the value file IS the deployed environment, so a
|
|
98
|
-
* name missing from it is missing from the deployment, whatever platform receives it.
|
|
99
|
-
*/
|
|
100
|
-
export function missingValuesGate(missing, valueFile) {
|
|
101
|
-
if (missing.length === 0)
|
|
102
|
-
return undefined;
|
|
103
|
-
return (`no value for: ${missing.join(", ")} — the deployed environment is declared by ${valueFile}, and this deploy ` +
|
|
104
|
-
`reads only that file (exporting the variable here does not reach the deployment). Add them there and re-run`);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* The refusal for a setting that shapes what `deploy` plans (a channel's ingress, read when `deploy` imports the
|
|
108
|
-
* channel here) and differs between this process and the value file. The box imports the channel with the file's
|
|
109
|
-
* value, so planning with this one would deploy a different channel than the one planned for: an export in this shell
|
|
110
|
-
* is the usual cause.
|
|
111
|
-
*/
|
|
112
|
-
export function divergentSettingsGate(names, env, values, valueFile) {
|
|
113
|
-
const differing = names.filter((name) => (env[name] ?? "") !== (values.get(name) ?? ""));
|
|
114
|
-
if (differing.length === 0)
|
|
115
|
-
return undefined;
|
|
116
|
-
const said = differing.map((name) => `${name} is "${env[name] ?? ""}" here and "${values.get(name) ?? ""}" in ${valueFile}`);
|
|
117
|
-
return (`${said.join("; ")} — the deployment receives only ${valueFile}, and this deploy would plan for a different ` +
|
|
118
|
-
`channel than the one it starts. Set it in ${valueFile} (or unset it here) and re-run`);
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Collect a (possibly CHUNKED) carrier from the environment: `<name>` plus numbered continuations (`_2`, `_3`, …)
|
|
122
|
-
* concatenated in order. A host whose env values have a length cap splits a long value this way.
|
|
123
|
-
*/
|
|
124
|
-
function collectChunked(env, name) {
|
|
125
|
-
const first = env[name];
|
|
126
|
-
if (!first)
|
|
127
|
-
return undefined;
|
|
128
|
-
let value = first;
|
|
129
|
-
for (let i = 2;; i++) {
|
|
130
|
-
const part = env[`${name}_${i}`];
|
|
131
|
-
if (!part)
|
|
132
|
-
break;
|
|
133
|
-
value += part;
|
|
70
|
+
for (const { name } of dedupeSecrets(declared.filter((secret) => !secret.optional))) {
|
|
71
|
+
if (needs.some((need) => need.name === name))
|
|
72
|
+
continue;
|
|
73
|
+
const { declaredIn, description } = declared.find((secret) => secret.name === name && !secret.optional);
|
|
74
|
+
needs.push({ name, hint: `required by ${declaredIn}${description ? `: ${description}` : ""}` });
|
|
134
75
|
}
|
|
135
|
-
return
|
|
76
|
+
return needs;
|
|
136
77
|
}
|
|
137
78
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* inverse of {@link encodeCarriedEnv}.
|
|
79
|
+
* Every name the definition declares, optional ones and the model's keys included: what a source may supply, and what
|
|
80
|
+
* `secrets list` shows.
|
|
141
81
|
*/
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const carried = {};
|
|
151
|
-
for (const [name, value] of Object.entries(parsed)) {
|
|
152
|
-
if (typeof value !== "string")
|
|
153
|
-
throw new Error(`FASTAGENT_ENV: ${name} is not a string — redeploy to regenerate it`);
|
|
154
|
-
carried[name] = value;
|
|
155
|
-
}
|
|
156
|
-
return carried;
|
|
82
|
+
export function declaredNames(modelAuth, declared, modelKey) {
|
|
83
|
+
return [
|
|
84
|
+
...new Set([
|
|
85
|
+
...(isEnvKey(modelAuth) ? [modelAuth] : []),
|
|
86
|
+
...(modelKey?.variables ?? []),
|
|
87
|
+
...declared.map(({ name }) => name),
|
|
88
|
+
]),
|
|
89
|
+
];
|
|
157
90
|
}
|
|
158
91
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
92
|
+
* The stop for a host CLI signed out with nobody to sign it in. A token goes in the shell's (or CI's) environment,
|
|
93
|
+
* never in the production value file, which holds the deployment's values and not the CLI's.
|
|
161
94
|
*/
|
|
162
|
-
export function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!(name in env))
|
|
167
|
-
env[name] = value;
|
|
168
|
-
}
|
|
169
|
-
/** The carried variables as the base64 JSON {@link collectCarriedEnv} reads back; empty when nothing travels. */
|
|
170
|
-
export function encodeCarriedEnv(carried) {
|
|
171
|
-
return Object.keys(carried).length === 0 ? "" : Buffer.from(JSON.stringify(carried), "utf8").toString("base64");
|
|
95
|
+
export function hostSignedOutGate(host, login, token, forAgent) {
|
|
96
|
+
return (`not logged in to ${host} — run \`${login}\` in a terminal (it opens a browser), or set ${token} in this ` +
|
|
97
|
+
`shell's environment, not in .secrets/production/.env (that file holds the deployment's values); then re-run. ` +
|
|
98
|
+
`From a coding agent, \`${forAgent}\` hands the sign-in to its user`);
|
|
172
99
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export declare const RELEASE_FILE = "fastagent.release.json";
|
|
2
|
-
/** Where the storage holds the deployed definition, the agent's working directory on the host. */
|
|
3
|
-
export declare const DEPLOYED_DEFINITION_DIR = "definition";
|
|
4
2
|
export interface DeploymentRelease {
|
|
5
3
|
version: 1;
|
|
6
4
|
id: string;
|
|
@@ -32,24 +30,20 @@ export declare function applyReleaseEnv(release: DeploymentRelease, env?: NodeJS
|
|
|
32
30
|
* resolution only ever splits on the first slash. */
|
|
33
31
|
export declare function isModelSpec(value: string): boolean;
|
|
34
32
|
/**
|
|
35
|
-
* Call while holding the deployment lease.
|
|
36
|
-
*
|
|
33
|
+
* Call while holding the deployment lease. Records `release` as the one started on the storage at `root`, refusing a
|
|
34
|
+
* storage another agent's deployments started on: its state is that agent's.
|
|
37
35
|
*/
|
|
38
|
-
export declare function
|
|
36
|
+
export declare function claimDeploymentStorage(root: string, release: DeploymentRelease): Promise<void>;
|
|
39
37
|
/** An unmounted directory must never look like a new disk. */
|
|
40
38
|
export declare function assertStorageMounted(root: string, paths?: string[]): Promise<void>;
|
|
41
39
|
/** `waitSeconds` covers a restart overlapping the old process's exit; a test that wants the refusal
|
|
42
40
|
* passes a short one rather than waiting out the deployment default. */
|
|
43
41
|
export declare function leaseDeployment(metadata: string, waitSeconds?: number): Promise<() => Promise<void>>;
|
|
44
42
|
/**
|
|
45
|
-
*
|
|
43
|
+
* Take the deployment's storage at `root`: it must be a mount, one process holds it (the lease), and it is the
|
|
44
|
+
* storage of the agent `release` names.
|
|
46
45
|
*
|
|
47
46
|
* The lease is NOT returned: it stays held for the process lifetime, because channel activation can
|
|
48
47
|
* start background writers that `close()` does not drain. The kernel drops it when the process exits.
|
|
49
48
|
*/
|
|
50
|
-
export declare function prepareDeployment(
|
|
51
|
-
/**
|
|
52
|
-
* Install the deployed agent's dependencies into the storage when it has no installed CLI yet (a fresh volume), or
|
|
53
|
-
* when an earlier install died part-way — its `installing` marker is still there, so the next boot redoes it.
|
|
54
|
-
*/
|
|
55
|
-
export declare function installAgentDependencies(root: string, agentDir: string): Promise<void>;
|
|
49
|
+
export declare function prepareDeployment(root: string, release: DeploymentRelease): Promise<void>;
|
package/dist/deploy/workspace.js
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deployment-owned initialization of a host's storage: the lease, the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Deployment-owned initialization of a host's storage: the lease, and the record of which agent the storage is for.
|
|
3
|
+
* The program is the image's, read where it is built; the storage is the instance's root, its working directory with
|
|
4
|
+
* its state, credentials and content (`.state/`, `.secrets/`, `content/`), which outlives every release.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
7
7
|
import { join, resolve } from "node:path";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { closeSync, openSync } from "node:fs";
|
|
10
10
|
import { once } from "node:events";
|
|
11
11
|
import { writeFileAtomic } from "../atomic-write.js";
|
|
12
12
|
import { log } from "../log.js";
|
|
13
|
-
import {
|
|
14
|
-
import { detectRuntime, readPackageJson } from "../runtime.js";
|
|
13
|
+
import { exists } from "../paths.js";
|
|
15
14
|
export const RELEASE_FILE = "fastagent.release.json";
|
|
16
|
-
/**
|
|
17
|
-
export const DEPLOYED_DEFINITION_DIR = "definition";
|
|
18
|
-
/** The deployment's own bookkeeping on the storage: the lease, the release journal, the install marker. */
|
|
15
|
+
/** The deployment's own bookkeeping on the storage: the lease, and the release last started on it. */
|
|
19
16
|
const deploymentMeta = (root) => join(root, ".deployment");
|
|
20
17
|
/** The manifest names the agent the storage belongs to, and that name is also a content entry's name rule (one path
|
|
21
18
|
* segment). `deploy` asks BEFORE generating artifacts — a name `init` accepts but this rejects is the author's
|
|
@@ -60,74 +57,20 @@ export function applyReleaseEnv(release, env = process.env) {
|
|
|
60
57
|
export function isModelSpec(value) {
|
|
61
58
|
return /^\S+\/\S+$/.test(value);
|
|
62
59
|
}
|
|
63
|
-
/** A completed staging tree is published before its journal; readers start only after recovery. */
|
|
64
|
-
async function finishRelease(root, release) {
|
|
65
|
-
const meta = deploymentMeta(root), staged = join(meta, "staged"), previous = join(meta, "previous");
|
|
66
|
-
const target = join(root, DEPLOYED_DEFINITION_DIR);
|
|
67
|
-
if (await exists(staged)) {
|
|
68
|
-
if (await exists(target)) {
|
|
69
|
-
if (await exists(previous))
|
|
70
|
-
throw new Error(`deployment recovery conflict at ${target}`);
|
|
71
|
-
await rename(target, previous);
|
|
72
|
-
}
|
|
73
|
-
await rename(staged, target);
|
|
74
|
-
}
|
|
75
|
-
else if (!(await exists(target))) {
|
|
76
|
-
throw new Error(`deployment recovery has neither staged nor active content: ${target}`);
|
|
77
|
-
}
|
|
78
|
-
writeFileAtomic(join(meta, "applied.json"), JSON.stringify(release));
|
|
79
|
-
await rm(previous, { recursive: true, force: true });
|
|
80
|
-
await rm(join(meta, "pending.json"));
|
|
81
|
-
}
|
|
82
60
|
/**
|
|
83
|
-
* Call while holding the deployment lease.
|
|
84
|
-
*
|
|
61
|
+
* Call while holding the deployment lease. Records `release` as the one started on the storage at `root`, refusing a
|
|
62
|
+
* storage another agent's deployments started on: its state is that agent's.
|
|
85
63
|
*/
|
|
86
|
-
export async function
|
|
87
|
-
const meta = deploymentMeta(root)
|
|
88
|
-
await mkdir(meta, { recursive: true });
|
|
89
|
-
await mkdir(join(root, CONTENT_DIRNAME), { recursive: true });
|
|
90
|
-
// A storage an earlier FastAgent laid out holds the agent's whole workspace there, which this one would leave
|
|
91
|
-
// behind unread: said, never silently abandoned.
|
|
92
|
-
const formerWorkspace = join(root, "base");
|
|
93
|
-
if (await exists(formerWorkspace)) {
|
|
94
|
-
throw new Error(`${formerWorkspace} holds a workspace from an earlier FastAgent, which no longer copies one to the host — ` +
|
|
95
|
-
`move out what you need and delete it, or deploy onto fresh storage`);
|
|
96
|
-
}
|
|
97
|
-
if (await exists(pendingPath)) {
|
|
98
|
-
const pending = parseDeploymentRelease(await readFile(pendingPath, "utf8"));
|
|
99
|
-
await finishRelease(root, pending);
|
|
100
|
-
}
|
|
64
|
+
export async function claimDeploymentStorage(root, release) {
|
|
65
|
+
const meta = deploymentMeta(root);
|
|
101
66
|
const appliedPath = join(meta, "applied.json");
|
|
102
67
|
const applied = (await exists(appliedPath)) ? parseDeploymentRelease(await readFile(appliedPath, "utf8")) : undefined;
|
|
103
|
-
|
|
104
|
-
if (applied?.agent !== undefined && applied.agent !== release.agent) {
|
|
68
|
+
if (applied !== undefined && applied.agent !== release.agent) {
|
|
105
69
|
throw new Error(`deployment selects ${release.agent}, but this storage belongs to ${applied.agent}`);
|
|
106
70
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
log.info(`[fastagent] release ${release.id} is already applied — keeping the deployed definition`);
|
|
111
|
-
return definition;
|
|
112
|
-
}
|
|
113
|
-
if (applied === undefined && (await exists(definition))) {
|
|
114
|
-
throw new Error(`refusing to initialize over an existing unowned definition: ${definition}`);
|
|
115
|
-
}
|
|
116
|
-
if (!(await exists(source)) || !(await lstat(source)).isDirectory()) {
|
|
117
|
-
throw new Error(`the release must contain its agent directory: ${source}`);
|
|
118
|
-
}
|
|
119
|
-
log.info(`[fastagent] publishing release ${release.id} as ${DEPLOYED_DEFINITION_DIR}/`);
|
|
120
|
-
await rm(staged, { recursive: true, force: true });
|
|
121
|
-
await cp(source, staged, { recursive: true, verbatimSymlinks: true });
|
|
122
|
-
// Where the agent reaches each content entry, on every instance: here, the storage's, which no release replaces.
|
|
123
|
-
await symlink(join("..", CONTENT_DIRNAME), join(staged, CONTENT_DIRNAME)).catch((error) => {
|
|
124
|
-
if (error.code !== "EEXIST")
|
|
125
|
-
throw error;
|
|
126
|
-
throw new Error(`the release holds ${CONTENT_DIRNAME}/, which a deploy keeps out of the image — rebuild it`);
|
|
127
|
-
});
|
|
128
|
-
writeFileAtomic(pendingPath, JSON.stringify(release));
|
|
129
|
-
await finishRelease(root, release);
|
|
130
|
-
return definition;
|
|
71
|
+
log.info(`[fastagent] release ${release.id} on ${root}`);
|
|
72
|
+
if (applied?.id !== release.id)
|
|
73
|
+
writeFileAtomic(appliedPath, JSON.stringify(release));
|
|
131
74
|
}
|
|
132
75
|
async function mountedPaths() {
|
|
133
76
|
const mounts = await readFile("/proc/self/mountinfo", "utf8");
|
|
@@ -174,41 +117,22 @@ export async function leaseDeployment(metadata, waitSeconds = 35) {
|
|
|
174
117
|
}
|
|
175
118
|
}
|
|
176
119
|
/**
|
|
177
|
-
*
|
|
120
|
+
* Take the deployment's storage at `root`: it must be a mount, one process holds it (the lease), and it is the
|
|
121
|
+
* storage of the agent `release` names.
|
|
178
122
|
*
|
|
179
123
|
* The lease is NOT returned: it stays held for the process lifetime, because channel activation can
|
|
180
124
|
* start background writers that `close()` does not drain. The kernel drops it when the process exits.
|
|
181
125
|
*/
|
|
182
|
-
export async function prepareDeployment(
|
|
126
|
+
export async function prepareDeployment(root, release) {
|
|
183
127
|
await assertStorageMounted(root);
|
|
184
128
|
const meta = deploymentMeta(root);
|
|
185
129
|
await mkdir(meta, { recursive: true });
|
|
186
130
|
const unlock = await leaseDeployment(meta);
|
|
187
131
|
try {
|
|
188
|
-
|
|
132
|
+
await claimDeploymentStorage(root, release);
|
|
189
133
|
}
|
|
190
134
|
catch (error) {
|
|
191
135
|
await unlock();
|
|
192
136
|
throw error;
|
|
193
137
|
}
|
|
194
138
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Install the deployed agent's dependencies into the storage when it has no installed CLI yet (a fresh volume), or
|
|
197
|
-
* when an earlier install died part-way — its `installing` marker is still there, so the next boot redoes it.
|
|
198
|
-
*/
|
|
199
|
-
export async function installAgentDependencies(root, agentDir) {
|
|
200
|
-
const installing = join(deploymentMeta(root), "installing");
|
|
201
|
-
// A failed install can leave the CLI link in place before its dependencies are complete.
|
|
202
|
-
if ((await exists(installing)) || !(await exists(join(agentDir, "node_modules/.bin/fastagent")))) {
|
|
203
|
-
const { runtime, hasLockfile } = detectRuntime(agentDir, await readPackageJson(agentDir));
|
|
204
|
-
const args = runtime === "bun" ? ["install", ...(hasLockfile ? ["--frozen-lockfile"] : [])] : [hasLockfile ? "ci" : "install"];
|
|
205
|
-
writeFileAtomic(installing, "");
|
|
206
|
-
log.info(`[fastagent] installing the agent's dependencies (${runtime} ${args.join(" ")})…`);
|
|
207
|
-
// stdio inherited: a five-minute install with no output reads as a hang, and the default 1 MB capture would
|
|
208
|
-
// kill a noisy one outright.
|
|
209
|
-
const [code, signal] = (await once(spawn(runtime === "bun" ? "bun" : "npm", args, { cwd: agentDir, stdio: "inherit" }), "exit"));
|
|
210
|
-
if (code !== 0)
|
|
211
|
-
throw new Error(`dependency install failed (${signal ?? `exit ${code}`})`);
|
|
212
|
-
await rm(installing);
|
|
213
|
-
}
|
|
214
|
-
}
|