@fastagent-sh/fastagent 0.21.1 → 0.22.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 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -40
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +209 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +153 -228
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- package/dist/schedule/schedule.js +0 -23
package/dist/deploy/secrets.js
CHANGED
|
@@ -1,150 +1,164 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* gate every target; optional channel values travel only when configured. Only the SET command differs
|
|
4
|
-
* (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
|
|
2
|
+
* The environment a deployed agent runs with — host-neutral.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The value file (`.secrets/.env`) IS the deployed environment: every variable in it travels, on every host. What the
|
|
5
|
+
* definition declares (`defineTool`/`defineChannel`/`defineRoutine({ secrets })`, plus the model's env key) only says
|
|
6
|
+
* which of those must have a value, so a missing one stops a deploy before its first side effect instead of a boot.
|
|
9
7
|
*/
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function firstPartyChannels(channels) {
|
|
15
|
-
return channels.flatMap((channel) => CHANNEL_KINDS.includes(channel.name)
|
|
16
|
-
? [{ kind: channel.name, ingress: channel.ingress }]
|
|
17
|
-
: []);
|
|
8
|
+
import { dedupeSecrets } from "../declared-secrets.js";
|
|
9
|
+
/** Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none? */
|
|
10
|
+
export function isEnvKey(source) {
|
|
11
|
+
return source !== undefined && /^[A-Z][A-Z0-9_]*$/.test(source);
|
|
18
12
|
}
|
|
19
13
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* labels: a new non-env `AuthResult.source` (e.g. `keychain`) then degrades to guidance, never a fake
|
|
23
|
-
* `keychain=<value>` secret — the two modules don't couple through an exhaustive string list.
|
|
14
|
+
* Names a deployment sets on the box itself. The value file's copy of one describes THIS machine (a local state path,
|
|
15
|
+
* a port) or rides another carrier (the model rides the release manifest), so it never travels.
|
|
24
16
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
const DEPLOY_OWNED = new Set([
|
|
18
|
+
"PORT",
|
|
19
|
+
"FASTAGENT_AGENT",
|
|
20
|
+
"FASTAGENT_MODEL",
|
|
21
|
+
"FASTAGENT_STATE_DIR",
|
|
22
|
+
"FASTAGENT_SECRETS_DIR",
|
|
23
|
+
"FASTAGENT_AUTH_PATH",
|
|
24
|
+
"FASTAGENT_RELEASE_FILE",
|
|
25
|
+
"FASTAGENT_STORAGE_DIR",
|
|
26
|
+
"FASTAGENT_AGENTCORE",
|
|
27
|
+
"FASTAGENT_INGRESS_SECRET",
|
|
28
|
+
"FASTAGENT_WAKE_SECRET",
|
|
29
|
+
"FASTAGENT_DEV_WORKER",
|
|
30
|
+
]);
|
|
31
|
+
/** Is `name` one the deployment sets itself (see {@link DEPLOY_OWNED}), including the chunked carriers? */
|
|
32
|
+
function isDeployOwned(name) {
|
|
33
|
+
return DEPLOY_OWNED.has(name) || /^FASTAGENT_(AUTH_SEED|ENV)(_\d+)?$/.test(name);
|
|
34
|
+
}
|
|
35
|
+
/** The value-file variables that travel: everything with a value that the deployment does not set itself. */
|
|
36
|
+
function carriedValues(values) {
|
|
37
|
+
const carried = {};
|
|
38
|
+
for (const [name, value] of values)
|
|
39
|
+
if (value && !isDeployOwned(name))
|
|
40
|
+
carried[name] = value;
|
|
41
|
+
return carried;
|
|
27
42
|
}
|
|
28
43
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* deployment prerequisites. An OAuth/stored login has no env key here — it carries as
|
|
32
|
-
* `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
|
|
44
|
+
* What a runbook lists: the names that must have a value (the model's env key, every declaration — hinted by the file
|
|
45
|
+
* that declared it) and then every other variable the value file carries.
|
|
33
46
|
*/
|
|
34
|
-
export function deploymentSecrets(modelAuth,
|
|
47
|
+
export function deploymentSecrets(modelAuth, declared, values, valueFile) {
|
|
35
48
|
const secrets = [];
|
|
49
|
+
const add = (name, hint) => {
|
|
50
|
+
if (!secrets.some((s) => s.name === name))
|
|
51
|
+
secrets.push({ name, hint });
|
|
52
|
+
};
|
|
36
53
|
if (isEnvKey(modelAuth))
|
|
37
|
-
|
|
38
|
-
for (const {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// Dedup: a name already covered by the model key / a channel secret must not appear twice in the runbook.
|
|
44
|
-
for (const name of extraSecrets) {
|
|
45
|
-
if (!secrets.some((s) => s.name === name)) {
|
|
46
|
-
const control = name === CONTROL_TOKEN_ENV;
|
|
47
|
-
secrets.push({
|
|
48
|
-
name,
|
|
49
|
-
hint: control
|
|
50
|
-
? "the /control/* bearer token — mint one (uuidgen) and give the same value to callers"
|
|
51
|
-
: "declared in fastagent.config deploy.secrets",
|
|
52
|
-
// OPTIONAL, unlike every other extra: unset, the box mints a per-boot token and still serves,
|
|
53
|
-
// and every host with a shell can read it back out of control.json. Gating would stop deploys
|
|
54
|
-
// that work today to enforce a convenience — the pre-flight warning is where that argument
|
|
55
|
-
// belongs.
|
|
56
|
-
required: !control,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
54
|
+
add(modelAuth, "your model provider key");
|
|
55
|
+
for (const { name, source } of dedupeSecrets(declared))
|
|
56
|
+
add(name, `required by ${source}`);
|
|
57
|
+
for (const name of Object.keys(carriedValues(values)))
|
|
58
|
+
add(name, `from ${valueFile}`);
|
|
60
59
|
return secrets;
|
|
61
60
|
}
|
|
62
61
|
/**
|
|
63
|
-
* Assemble the
|
|
64
|
-
*
|
|
65
|
-
* the security-sensitive key wiring is testable once. The model credential travels one of two ways: an
|
|
66
|
-
* env-key auth as its own secret (value from `env`), OR an OAuth/stored login (no plaintext key) as
|
|
67
|
-
* `FASTAGENT_AUTH_SEED` (base64 auth.json) which `start` materializes on first boot. `needsModelCredential`
|
|
68
|
-
* (neither present) is a DISTINCT signal: its remediation is `fastagent login`, not the `.env` one that
|
|
69
|
-
* `missingSecrets` (real secret NAMES with no value) carries.
|
|
62
|
+
* Assemble the VALUES a `--run` deploy sets on the host: every variable the value file carries, plus the local
|
|
63
|
+
* credential. Never minted here.
|
|
70
64
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* required value enters `missingSecrets`; an absent optional value is simply omitted.
|
|
65
|
+
* `values` is the selected value file, read as data — **the environment running `deploy` is not a source**. A
|
|
66
|
+
* deployment must be reproducible from what it carries, and a variable that happens to be exported on the builder is
|
|
67
|
+
* written down nowhere. CI supplies values by writing the file before running the command.
|
|
75
68
|
*/
|
|
76
69
|
export function assembleSecrets(input) {
|
|
77
|
-
const secrets =
|
|
70
|
+
const secrets = carriedValues(input.values);
|
|
78
71
|
const missingSecrets = [];
|
|
79
72
|
let needsModelCredential = false;
|
|
80
73
|
if (isEnvKey(input.modelAuth)) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
secrets[input.modelAuth] = v;
|
|
84
|
-
else
|
|
85
|
-
missingSecrets.push(input.modelAuth); // an env-key name with no value — `.env` remediation fits
|
|
74
|
+
if (!secrets[input.modelAuth])
|
|
75
|
+
missingSecrets.push(input.modelAuth); // `.env` remediation fits
|
|
86
76
|
}
|
|
87
77
|
else if (input.authFile) {
|
|
88
78
|
secrets.FASTAGENT_AUTH_SEED = input.authFile.toString("base64");
|
|
89
79
|
}
|
|
90
80
|
else if (input.modelKeyInDefinition) {
|
|
91
|
-
// The definition authenticates itself (models.json literal key, or a command run on the host), so it
|
|
92
|
-
//
|
|
93
|
-
// remedies we print are impossible for such an agent — `fastagent login` has no flow for a custom
|
|
94
|
-
// provider, and there is no provider env key to set.
|
|
81
|
+
// The definition authenticates itself (models.json literal key, or a command run on the host), so it travels in
|
|
82
|
+
// the image with everything else.
|
|
95
83
|
}
|
|
96
84
|
else {
|
|
97
85
|
needsModelCredential = true; // no env key, no auth.json — `fastagent login` remediation
|
|
98
86
|
}
|
|
99
|
-
for (const {
|
|
100
|
-
|
|
101
|
-
const v = input.env[e.name];
|
|
102
|
-
if (v)
|
|
103
|
-
secrets[e.name] = v; // optional channel values travel when configured
|
|
104
|
-
else if (e.required) {
|
|
105
|
-
missingSecrets.push(e.name); // operator-provided (in .env); a human-shared secret can't be minted
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
for (const name of input.extraSecrets ?? []) {
|
|
110
|
-
if (name in secrets || missingSecrets.includes(name))
|
|
111
|
-
continue; // already covered by model/channel — no dup
|
|
112
|
-
const v = input.env[name];
|
|
113
|
-
if (v)
|
|
114
|
-
secrets[name] = v;
|
|
115
|
-
// The control token is CARRIED, never gated — see {@link deploymentSecrets}: unset, the box mints
|
|
116
|
-
// one and serves; every other extra is declared as needed, so its absence is a stop.
|
|
117
|
-
else if (name !== CONTROL_TOKEN_ENV)
|
|
87
|
+
for (const { name } of dedupeSecrets(input.declared ?? [])) {
|
|
88
|
+
if (!secrets[name] && !missingSecrets.includes(name))
|
|
118
89
|
missingSecrets.push(name);
|
|
119
90
|
}
|
|
120
91
|
return { secrets, missingSecrets, needsModelCredential };
|
|
121
92
|
}
|
|
122
93
|
/**
|
|
123
|
-
* The
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* that ran its own OAuth refresh is not rolled back.
|
|
94
|
+
* The ONE refusal for declared names the deployed environment does not supply a value for — every host reaches it
|
|
95
|
+
* before its first side effect. Host-neutral because the reason is: the value file IS the deployed environment, so a
|
|
96
|
+
* name missing from it is missing from the deployment, whatever platform receives it.
|
|
127
97
|
*/
|
|
98
|
+
export function missingValuesGate(missing, valueFile) {
|
|
99
|
+
if (missing.length === 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
return (`no value for: ${missing.join(", ")} — the deployed environment is declared by ${valueFile}, and this deploy ` +
|
|
102
|
+
`reads only that file (exporting the variable here does not reach the deployment). Add them there and re-run`);
|
|
103
|
+
}
|
|
104
|
+
/** The bytes to seed to the auth file, or undefined to leave it alone. */
|
|
128
105
|
export function authSeedBytes(seed, fileExists) {
|
|
129
106
|
return !seed || fileExists ? undefined : Buffer.from(seed, "base64");
|
|
130
107
|
}
|
|
131
108
|
/**
|
|
132
|
-
* Collect
|
|
133
|
-
*
|
|
134
|
-
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
135
|
-
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
136
|
-
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
109
|
+
* Collect a (possibly CHUNKED) carrier from the environment: `<name>` plus numbered continuations (`_2`, `_3`, …)
|
|
110
|
+
* concatenated in order. A host whose env values have a length cap splits a long value this way.
|
|
137
111
|
*/
|
|
138
|
-
|
|
139
|
-
const first = env
|
|
112
|
+
function collectChunked(env, name) {
|
|
113
|
+
const first = env[name];
|
|
140
114
|
if (!first)
|
|
141
115
|
return undefined;
|
|
142
|
-
let
|
|
116
|
+
let value = first;
|
|
143
117
|
for (let i = 2;; i++) {
|
|
144
|
-
const part = env[
|
|
118
|
+
const part = env[`${name}_${i}`];
|
|
145
119
|
if (!part)
|
|
146
120
|
break;
|
|
147
|
-
|
|
121
|
+
value += part;
|
|
148
122
|
}
|
|
149
|
-
return
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
/** `FASTAGENT_AUTH_SEED` (+ `_2`, …): the base64 auth.json a deploy carries. */
|
|
126
|
+
export function collectAuthSeed(env) {
|
|
127
|
+
return collectChunked(env, "FASTAGENT_AUTH_SEED");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `FASTAGENT_ENV` (+ `_2`, …): the carried variables as ONE base64 JSON object, for a host whose deployment artifact
|
|
131
|
+
* must not depend on which names the value file holds (AgentCore's template is committed and gated on drift). The
|
|
132
|
+
* inverse of {@link encodeCarriedEnv}.
|
|
133
|
+
*/
|
|
134
|
+
function collectCarriedEnv(env) {
|
|
135
|
+
const encoded = collectChunked(env, "FASTAGENT_ENV");
|
|
136
|
+
if (encoded === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
139
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
140
|
+
throw new Error("FASTAGENT_ENV is not an object of variables — redeploy to regenerate it");
|
|
141
|
+
}
|
|
142
|
+
const carried = {};
|
|
143
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
144
|
+
if (typeof value !== "string")
|
|
145
|
+
throw new Error(`FASTAGENT_ENV: ${name} is not a string — redeploy to regenerate it`);
|
|
146
|
+
carried[name] = value;
|
|
147
|
+
}
|
|
148
|
+
return carried;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Expand `FASTAGENT_ENV` into the process environment — what `start` does first on a host that carries the value file
|
|
152
|
+
* that way. The platform's own variables still win, as they do over a value file anywhere.
|
|
153
|
+
*/
|
|
154
|
+
export function applyCarriedEnv(env = process.env) {
|
|
155
|
+
const carried = collectCarriedEnv(env);
|
|
156
|
+
if (carried)
|
|
157
|
+
for (const [name, value] of Object.entries(carried))
|
|
158
|
+
if (!(name in env))
|
|
159
|
+
env[name] = value;
|
|
160
|
+
}
|
|
161
|
+
/** The carried variables as the base64 JSON {@link collectCarriedEnv} reads back; empty when nothing travels. */
|
|
162
|
+
export function encodeCarriedEnv(carried) {
|
|
163
|
+
return Object.keys(carried).length === 0 ? "" : Buffer.from(JSON.stringify(carried), "utf8").toString("base64");
|
|
150
164
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const RELEASE_FILE = "fastagent.release.json";
|
|
2
|
+
export interface DeploymentRelease {
|
|
3
|
+
version: 1;
|
|
4
|
+
id: string;
|
|
5
|
+
agent: string;
|
|
6
|
+
/**
|
|
7
|
+
* The model this release resolved, when the deployed environment's value file named it (`config.model` needs
|
|
8
|
+
* nothing here — the config ships in the image too). NON-CREDENTIAL configuration only: the manifest travels
|
|
9
|
+
* inside the image, which is readable by anyone who can pull it, and it is rebuilt on every deploy — both are the
|
|
10
|
+
* opposite of what a credential needs (see docs/design/configuration.md §8).
|
|
11
|
+
*/
|
|
12
|
+
model?: string;
|
|
13
|
+
}
|
|
14
|
+
/** The manifest names a directory the container joins onto the workspace root, so the spelling is
|
|
15
|
+
* constrained. `deploy` asks BEFORE generating artifacts — a name `init` accepts but this rejects
|
|
16
|
+
* is the author's directory name, not a corrupt manifest. */
|
|
17
|
+
export declare function isReleaseAgentName(name: string): boolean;
|
|
18
|
+
export declare function parseDeploymentRelease(raw: string): DeploymentRelease;
|
|
19
|
+
/**
|
|
20
|
+
* Project a release's own declarations into the environment it was resolved FOR — the receiving half of the model
|
|
21
|
+
* chain, run by `prepareStartWorkspace` BEFORE the workspace's `.env` is read (so either source outranks a value
|
|
22
|
+
* edited on the box).
|
|
23
|
+
*
|
|
24
|
+
* `||=`: a variable the platform already holds still wins, because the deployment declares only the half of this
|
|
25
|
+
* environment it can. The log names whichever source actually won — the remedies differ, and a redeploy does
|
|
26
|
+
* nothing at all to a platform-set variable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyReleaseEnv(release: DeploymentRelease, env?: NodeJS.ProcessEnv): void;
|
|
29
|
+
/** A `provider/modelId` spec. The id itself may carry `/` and `~` (`baseten/zai-org/GLM-5.3`, openrouter aliases);
|
|
30
|
+
* resolution only ever splits on the first slash. */
|
|
31
|
+
export declare function isModelSpec(value: string): boolean;
|
|
32
|
+
/** Call while holding the workspace lease. No deployment replaces existing work outside the definition. */
|
|
33
|
+
export declare function applyDeploymentRelease(source: string, root: string, release: DeploymentRelease): Promise<string>;
|
|
34
|
+
/** An unmounted directory must never look like a new disk. */
|
|
35
|
+
export declare function assertStorageMounted(root: string, paths?: string[]): Promise<void>;
|
|
36
|
+
/** `waitSeconds` covers a restart overlapping the old process's exit; a test that wants the refusal
|
|
37
|
+
* passes a short one rather than waiting out the deployment default. */
|
|
38
|
+
export declare function leaseDeployment(metadata: string, waitSeconds?: number): Promise<() => Promise<void>>;
|
|
39
|
+
/**
|
|
40
|
+
* Prepare the deployed workspace and return its path.
|
|
41
|
+
*
|
|
42
|
+
* The lease is NOT returned: it stays held for the process lifetime, because channel activation can
|
|
43
|
+
* start background writers that `close()` does not drain. The kernel drops it when the process exits.
|
|
44
|
+
*/
|
|
45
|
+
export declare function prepareDeployment(source: string, root: string, release: DeploymentRelease): Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Install the deployed agent's dependencies into the storage when it has no installed CLI yet (a fresh volume), or
|
|
48
|
+
* when an earlier install died part-way — its `installing` marker is still there, so the next boot redoes it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function installAgentDependencies(root: string, agentDir: string): Promise<void>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/** Deployment-owned initialization; the workspace itself belongs to the running agent. */
|
|
2
|
+
import { cp, lstat, mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { closeSync, openSync } from "node:fs";
|
|
6
|
+
import { once } from "node:events";
|
|
7
|
+
import { writeFileAtomic } from "../atomic-write.js";
|
|
8
|
+
import { log } from "../log.js";
|
|
9
|
+
import { exists } from "../paths.js";
|
|
10
|
+
import { detectRuntime, readPackageJson } from "../runtime.js";
|
|
11
|
+
export const RELEASE_FILE = "fastagent.release.json";
|
|
12
|
+
/** The deployment's own bookkeeping on the storage: the lease, the release journal, the install marker. */
|
|
13
|
+
const deploymentMeta = (root) => join(root, ".deployment");
|
|
14
|
+
/** The manifest names a directory the container joins onto the workspace root, so the spelling is
|
|
15
|
+
* constrained. `deploy` asks BEFORE generating artifacts — a name `init` accepts but this rejects
|
|
16
|
+
* is the author's directory name, not a corrupt manifest. */
|
|
17
|
+
export function isReleaseAgentName(name) {
|
|
18
|
+
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
19
|
+
}
|
|
20
|
+
export function parseDeploymentRelease(raw) {
|
|
21
|
+
const r = JSON.parse(raw);
|
|
22
|
+
if (r?.version !== 1 ||
|
|
23
|
+
typeof r.id !== "string" ||
|
|
24
|
+
!r.id ||
|
|
25
|
+
typeof r.agent !== "string" ||
|
|
26
|
+
!isReleaseAgentName(r.agent) ||
|
|
27
|
+
(r.model !== undefined && !isModelSpec(r.model))) {
|
|
28
|
+
throw new Error("invalid deployment release manifest");
|
|
29
|
+
}
|
|
30
|
+
return r;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Project a release's own declarations into the environment it was resolved FOR — the receiving half of the model
|
|
34
|
+
* chain, run by `prepareStartWorkspace` BEFORE the workspace's `.env` is read (so either source outranks a value
|
|
35
|
+
* edited on the box).
|
|
36
|
+
*
|
|
37
|
+
* `||=`: a variable the platform already holds still wins, because the deployment declares only the half of this
|
|
38
|
+
* environment it can. The log names whichever source actually won — the remedies differ, and a redeploy does
|
|
39
|
+
* nothing at all to a platform-set variable.
|
|
40
|
+
*/
|
|
41
|
+
export function applyReleaseEnv(release, env = process.env) {
|
|
42
|
+
const fromPlatform = env.FASTAGENT_MODEL;
|
|
43
|
+
if (release.model && !fromPlatform)
|
|
44
|
+
env.FASTAGENT_MODEL = release.model;
|
|
45
|
+
const effective = env.FASTAGENT_MODEL;
|
|
46
|
+
if (!effective)
|
|
47
|
+
return;
|
|
48
|
+
log.info(`[fastagent] model ${effective} — from ${fromPlatform
|
|
49
|
+
? `a FASTAGENT_MODEL already set in this environment${release.model ? ", which outranks the release manifest" : ""}`
|
|
50
|
+
: "the release manifest (redeploy to change it)"}; editing FASTAGENT_MODEL in this workspace's .env does not override it`);
|
|
51
|
+
}
|
|
52
|
+
/** A `provider/modelId` spec. The id itself may carry `/` and `~` (`baseten/zai-org/GLM-5.3`, openrouter aliases);
|
|
53
|
+
* resolution only ever splits on the first slash. */
|
|
54
|
+
export function isModelSpec(value) {
|
|
55
|
+
return /^\S+\/\S+$/.test(value);
|
|
56
|
+
}
|
|
57
|
+
/** A completed staging tree is published before its journal; readers start only after recovery. */
|
|
58
|
+
async function finishRelease(root, pending) {
|
|
59
|
+
const meta = deploymentMeta(root), staged = join(meta, "staged"), previous = join(meta, "previous");
|
|
60
|
+
const target = pending.initial ? join(root, "base") : join(root, "base", pending.release.agent);
|
|
61
|
+
if (await exists(staged)) {
|
|
62
|
+
if (await exists(target)) {
|
|
63
|
+
if (pending.initial || (await exists(previous)))
|
|
64
|
+
throw new Error(`deployment recovery conflict at ${target}`);
|
|
65
|
+
await rename(target, previous);
|
|
66
|
+
}
|
|
67
|
+
await rename(staged, target);
|
|
68
|
+
}
|
|
69
|
+
else if (!(await exists(target))) {
|
|
70
|
+
throw new Error(`deployment recovery has neither staged nor active content: ${target}`);
|
|
71
|
+
}
|
|
72
|
+
writeFileAtomic(join(meta, "applied.json"), JSON.stringify(pending.release));
|
|
73
|
+
await rm(previous, { recursive: true, force: true });
|
|
74
|
+
await rm(join(meta, "pending.json"));
|
|
75
|
+
}
|
|
76
|
+
/** Call while holding the workspace lease. No deployment replaces existing work outside the definition. */
|
|
77
|
+
export async function applyDeploymentRelease(source, root, release) {
|
|
78
|
+
const meta = deploymentMeta(root), staged = join(meta, "staged"), pendingPath = join(meta, "pending.json");
|
|
79
|
+
await mkdir(meta, { recursive: true });
|
|
80
|
+
if (await exists(pendingPath)) {
|
|
81
|
+
const pending = JSON.parse(await readFile(pendingPath, "utf8"));
|
|
82
|
+
pending.release = parseDeploymentRelease(JSON.stringify(pending.release));
|
|
83
|
+
if (typeof pending.initial !== "boolean")
|
|
84
|
+
throw new Error("invalid pending deployment journal");
|
|
85
|
+
await finishRelease(root, pending);
|
|
86
|
+
}
|
|
87
|
+
const appliedPath = join(meta, "applied.json");
|
|
88
|
+
const applied = (await exists(appliedPath)) ? parseDeploymentRelease(await readFile(appliedPath, "utf8")) : undefined;
|
|
89
|
+
const workspace = join(root, "base");
|
|
90
|
+
if (applied?.agent !== undefined && applied.agent !== release.agent) {
|
|
91
|
+
throw new Error(`deployment selects ${release.agent}, but this workspace belongs to ${applied.agent}`);
|
|
92
|
+
}
|
|
93
|
+
// Logged on BOTH paths: rebuilding an image without regenerating the manifest keeps the id, and a
|
|
94
|
+
// deployment that silently kept the old definition looks identical to one that took the new one.
|
|
95
|
+
if (applied?.id === release.id) {
|
|
96
|
+
log.info(`[fastagent] release ${release.id} is already applied — keeping the workspace's definition`);
|
|
97
|
+
return workspace;
|
|
98
|
+
}
|
|
99
|
+
const initial = applied === undefined;
|
|
100
|
+
if (initial && (await exists(workspace)))
|
|
101
|
+
throw new Error(`refusing to initialize over an existing unowned workspace: ${workspace}`);
|
|
102
|
+
const definition = join(source, release.agent);
|
|
103
|
+
if (!(await exists(definition)) || !(await lstat(definition)).isDirectory())
|
|
104
|
+
throw new Error(`the release must contain its agent directory: ${definition}`);
|
|
105
|
+
log.info(initial
|
|
106
|
+
? `[fastagent] seeding the workspace from release ${release.id}`
|
|
107
|
+
: `[fastagent] publishing release ${release.id} over base/${release.agent}`);
|
|
108
|
+
await rm(staged, { recursive: true, force: true });
|
|
109
|
+
await cp(initial ? source : join(source, release.agent), staged, { recursive: true, verbatimSymlinks: true });
|
|
110
|
+
const pending = { release, initial };
|
|
111
|
+
writeFileAtomic(pendingPath, JSON.stringify(pending));
|
|
112
|
+
await finishRelease(root, pending);
|
|
113
|
+
return workspace;
|
|
114
|
+
}
|
|
115
|
+
async function mountedPaths() {
|
|
116
|
+
const mounts = await readFile("/proc/self/mountinfo", "utf8");
|
|
117
|
+
return mounts
|
|
118
|
+
.split("\n")
|
|
119
|
+
.map((line) => line
|
|
120
|
+
.split(" ")[4]
|
|
121
|
+
?.replace(/\\([0-7]{3})/g, (_, octal) => String.fromCharCode(Number.parseInt(octal, 8))))
|
|
122
|
+
.filter((path) => path !== undefined);
|
|
123
|
+
}
|
|
124
|
+
/** An unmounted directory must never look like a new disk. */
|
|
125
|
+
export async function assertStorageMounted(root, paths) {
|
|
126
|
+
if (!(paths ?? (await mountedPaths())).includes(resolve(root)))
|
|
127
|
+
throw new Error(`persistent storage is not mounted at ${root}`);
|
|
128
|
+
}
|
|
129
|
+
/** `waitSeconds` covers a restart overlapping the old process's exit; a test that wants the refusal
|
|
130
|
+
* passes a short one rather than waiting out the deployment default. */
|
|
131
|
+
export async function leaseDeployment(metadata, waitSeconds = 35) {
|
|
132
|
+
// flock locks the shared open-file description. The parent's raw fd survives GC and closes at exit.
|
|
133
|
+
// Never unlink this inode: another starter may already have it open.
|
|
134
|
+
const fd = openSync(join(metadata, "lock"), "a", 0o600);
|
|
135
|
+
try {
|
|
136
|
+
const child = spawn("flock", ["--exclusive", "--wait", String(waitSeconds), "3"], {
|
|
137
|
+
stdio: ["ignore", "ignore", "pipe", fd],
|
|
138
|
+
});
|
|
139
|
+
let stderr = "";
|
|
140
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
141
|
+
stderr += chunk;
|
|
142
|
+
});
|
|
143
|
+
const [code, signal] = await once(child, "close");
|
|
144
|
+
if (code !== 0)
|
|
145
|
+
throw new Error(`could not acquire workspace lease at ${metadata}: flock ${signal ?? code} ${stderr.trim()}`);
|
|
146
|
+
return async () => {
|
|
147
|
+
closeSync(fd);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
closeSync(fd);
|
|
152
|
+
// A custom base image without util-linux fails every boot; the bare spawn error names neither
|
|
153
|
+
// the binary nor why a lease needs one.
|
|
154
|
+
if (error.code === "ENOENT")
|
|
155
|
+
throw new Error(`the deployed workspace lease needs the \`flock\` binary (util-linux): ${String(error)}`);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Prepare the deployed workspace and return its path.
|
|
161
|
+
*
|
|
162
|
+
* The lease is NOT returned: it stays held for the process lifetime, because channel activation can
|
|
163
|
+
* start background writers that `close()` does not drain. The kernel drops it when the process exits.
|
|
164
|
+
*/
|
|
165
|
+
export async function prepareDeployment(source, root, release) {
|
|
166
|
+
await assertStorageMounted(root);
|
|
167
|
+
const meta = deploymentMeta(root);
|
|
168
|
+
await mkdir(meta, { recursive: true });
|
|
169
|
+
const unlock = await leaseDeployment(meta);
|
|
170
|
+
try {
|
|
171
|
+
return await applyDeploymentRelease(source, root, release);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
await unlock();
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Install the deployed agent's dependencies into the storage when it has no installed CLI yet (a fresh volume), or
|
|
180
|
+
* when an earlier install died part-way — its `installing` marker is still there, so the next boot redoes it.
|
|
181
|
+
*/
|
|
182
|
+
export async function installAgentDependencies(root, agentDir) {
|
|
183
|
+
const installing = join(deploymentMeta(root), "installing");
|
|
184
|
+
// A failed install can leave the CLI link in place before its dependencies are complete.
|
|
185
|
+
if ((await exists(installing)) || !(await exists(join(agentDir, "node_modules/.bin/fastagent")))) {
|
|
186
|
+
const { runtime, hasLockfile } = detectRuntime(agentDir, await readPackageJson(agentDir));
|
|
187
|
+
const args = runtime === "bun" ? ["install", ...(hasLockfile ? ["--frozen-lockfile"] : [])] : [hasLockfile ? "ci" : "install"];
|
|
188
|
+
writeFileAtomic(installing, "");
|
|
189
|
+
log.info(`[fastagent] installing the agent's dependencies (${runtime} ${args.join(" ")})…`);
|
|
190
|
+
// stdio inherited: a five-minute install with no output reads as a hang, and the default 1 MB capture would
|
|
191
|
+
// kill a noisy one outright.
|
|
192
|
+
const [code, signal] = (await once(spawn(runtime === "bun" ? "bun" : "npm", args, { cwd: agentDir, stdio: "inherit" }), "exit"));
|
|
193
|
+
if (code !== 0)
|
|
194
|
+
throw new Error(`dependency install failed (${signal ?? `exit ${code}`})`);
|
|
195
|
+
await rm(installing);
|
|
196
|
+
}
|
|
197
|
+
}
|
package/dist/dev-supervisor.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { type ResolvedPlacement } from "./paths.ts";
|
|
2
|
-
/**
|
|
3
|
-
* chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. When
|
|
4
|
-
* the agent sits INSIDE the workspace that means the surrounding tree never triggers a restart at all;
|
|
5
|
-
* when the agent IS the workspace the root is that tree, and the allowlist below is what keeps the
|
|
6
|
-
* author's own files out of scope. Ignoring a directory prunes the whole subtree, so everything outside it —
|
|
7
|
-
* `.state/` machine state, node_modules, .git, and any file/dir the agent writes as work product —
|
|
8
|
-
* costs no watchers and triggers no restarts. Helper code imported from OUTSIDE tools//channels/ is
|
|
9
|
-
* out of scope by design (keep it under tools/, or restart manually) — the startup log names the set.
|
|
10
|
-
*/
|
|
2
|
+
/** chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. */
|
|
11
3
|
export declare function devWatchIgnored(root: string, envFile: string): (path: string) => boolean;
|
|
12
4
|
/** Spawn the dev worker and restart it on agent-dir edits; supervise its lifecycle until the process exits. */
|
|
13
5
|
export declare function runDevSupervisor(placement: ResolvedPlacement, options?: {
|