@fastagent-sh/fastagent 0.21.0 → 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 -43
- 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
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A minimal, DETERMINISTIC single-file ZIP writer — just enough to package the forwarder Lambda.
|
|
3
|
-
*
|
|
4
|
-
* WHY NOT A DEPENDENCY: this runs in the deploy path of a tool whose whole promise is "no build
|
|
5
|
-
* step"; pulling an archiver in for ~40 lines of well-specified format would be the wrong trade.
|
|
6
|
-
* WHY NOT `zip(1)`: it is absent on plenty of machines (and on Windows), and its output embeds the
|
|
7
|
-
* current time — the forwarder's S3 key is CONTENT-HASHED, so identical source must produce
|
|
8
|
-
* identical bytes or every deploy would look like a code change to CloudFormation.
|
|
9
|
-
*
|
|
10
|
-
* STORE (no compression) on purpose: the payload is a few KB of JavaScript, and the format's stored
|
|
11
|
-
* form is trivially verifiable. Fixed 1980-01-01 timestamps keep the output byte-stable.
|
|
12
|
-
*/
|
|
1
|
+
/** A minimal, DETERMINISTIC single-file ZIP writer — just enough to package the forwarder Lambda. */
|
|
13
2
|
import { Buffer } from "node:buffer";
|
|
14
|
-
/**
|
|
15
|
-
* Build a ZIP archive containing exactly one stored entry. Byte-identical for identical inputs.
|
|
16
|
-
*/
|
|
3
|
+
/** Build a ZIP archive containing exactly one stored entry. */
|
|
17
4
|
export declare function zipSingleFile(name: string, content: Buffer): Buffer;
|
|
@@ -1,23 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A minimal, DETERMINISTIC single-file ZIP writer — just enough to package the forwarder Lambda.
|
|
3
|
-
*
|
|
4
|
-
* WHY NOT A DEPENDENCY: this runs in the deploy path of a tool whose whole promise is "no build
|
|
5
|
-
* step"; pulling an archiver in for ~40 lines of well-specified format would be the wrong trade.
|
|
6
|
-
* WHY NOT `zip(1)`: it is absent on plenty of machines (and on Windows), and its output embeds the
|
|
7
|
-
* current time — the forwarder's S3 key is CONTENT-HASHED, so identical source must produce
|
|
8
|
-
* identical bytes or every deploy would look like a code change to CloudFormation.
|
|
9
|
-
*
|
|
10
|
-
* STORE (no compression) on purpose: the payload is a few KB of JavaScript, and the format's stored
|
|
11
|
-
* form is trivially verifiable. Fixed 1980-01-01 timestamps keep the output byte-stable.
|
|
12
|
-
*/
|
|
1
|
+
/** A minimal, DETERMINISTIC single-file ZIP writer — just enough to package the forwarder Lambda. */
|
|
13
2
|
import { Buffer } from "node:buffer";
|
|
14
3
|
import { crc32 } from "node:zlib";
|
|
15
4
|
/** MS-DOS epoch (1980-01-01 00:00:00) — the earliest the format can express, and a constant. */
|
|
16
5
|
const DOS_DATE = 0x0021;
|
|
17
6
|
const DOS_TIME = 0x0000;
|
|
18
|
-
/**
|
|
19
|
-
* Build a ZIP archive containing exactly one stored entry. Byte-identical for identical inputs.
|
|
20
|
-
*/
|
|
7
|
+
/** Build a ZIP archive containing exactly one stored entry. */
|
|
21
8
|
export function zipSingleFile(name, content) {
|
|
22
9
|
const nameBytes = Buffer.from(name, "utf8");
|
|
23
10
|
const sum = crc32(content);
|
|
@@ -49,8 +36,7 @@ export function zipSingleFile(name, content) {
|
|
|
49
36
|
centralHeader.writeUInt16LE(0, 32); // file comment length
|
|
50
37
|
centralHeader.writeUInt16LE(0, 34); // disk number start
|
|
51
38
|
centralHeader.writeUInt16LE(0, 36); // internal attributes
|
|
52
|
-
// External attributes: regular file, rw-r--r-- in the high 16 bits.
|
|
53
|
-
// operators are 32-bit SIGNED — the shifted value is negative without it.
|
|
39
|
+
// External attributes: regular file, rw-r--r-- in the high 16 bits.
|
|
54
40
|
centralHeader.writeUInt32LE((0o100644 << 16) >>> 0, 38);
|
|
55
41
|
centralHeader.writeUInt32LE(0, 42); // local header offset (single entry: always 0)
|
|
56
42
|
const centralSize = centralHeader.byteLength + nameBytes.byteLength;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DeployReport } from "./preflight.ts";
|
|
2
|
+
/** Workspace-relative paths the build context may hold and the image must not. */
|
|
3
|
+
export interface BuildContextPaths {
|
|
4
|
+
/** The resolved machinery dirs the generated ignore excludes by path (ContainerInput.machineryPaths). */
|
|
5
|
+
machineryPaths: string[];
|
|
6
|
+
/** Credential files that exist right now — a kept ignore file that lets one through bakes it. */
|
|
7
|
+
leakCandidates: string[];
|
|
8
|
+
/** The state root, when it exists inside the context. */
|
|
9
|
+
stateShips?: string;
|
|
10
|
+
/** The node_modules dirs that exist, the agent's and the workspace's. */
|
|
11
|
+
depDirs: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function buildContextPaths(workspace: string, agentDir: string, agentPrefix: string, authPath: string): Promise<BuildContextPaths>;
|
|
14
|
+
/** Interrogate BOTH ignore files deploy emits, when the author kept them. */
|
|
15
|
+
export declare function checkKeptIgnoreFiles(ctx: {
|
|
16
|
+
workspace: string;
|
|
17
|
+
agentDir: string;
|
|
18
|
+
agentPrefix: string;
|
|
19
|
+
force: boolean;
|
|
20
|
+
paths: BuildContextPaths;
|
|
21
|
+
}, report: DeployReport): Promise<void>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a deploy's BUILD CONTEXT (the workspace) holds that must not ship, and whether an ignore file the author kept
|
|
3
|
+
* still keeps it out. A kept workspace-root `.dockerignore` silently replaces the generated one's protections, so the
|
|
4
|
+
* pre-flight asks it the same questions the generated one answers by construction.
|
|
5
|
+
*/
|
|
6
|
+
import { readdir } from "node:fs/promises";
|
|
7
|
+
import { basename, isAbsolute, join, relative, sep } from "node:path";
|
|
8
|
+
import ignore from "ignore";
|
|
9
|
+
import { exists, readTextIfExists, resolveSecretsDir, resolveStateRoot } from "../paths.js";
|
|
10
|
+
import { isGeneratedDockerignore } from "./container.js";
|
|
11
|
+
/**
|
|
12
|
+
* "Would docker's packer drop this path?" — built from a `.dockerignore`'s text via the `ignore` matcher (the same
|
|
13
|
+
* library the workspace ignore files use), so `!` negation and last-match-wins are the library's problem, not ours.
|
|
14
|
+
*/
|
|
15
|
+
function dockerignoreMatcher(text) {
|
|
16
|
+
const anchored = text
|
|
17
|
+
.split("\n")
|
|
18
|
+
.map((raw) => {
|
|
19
|
+
const line = raw.trim();
|
|
20
|
+
if (line === "" || line.startsWith("#"))
|
|
21
|
+
return line;
|
|
22
|
+
const negated = line.startsWith("!");
|
|
23
|
+
const pattern = negated ? line.slice(1) : line;
|
|
24
|
+
if (pattern.startsWith("/") || pattern.startsWith("**/"))
|
|
25
|
+
return line;
|
|
26
|
+
return `${negated ? "!" : ""}/${pattern}`;
|
|
27
|
+
})
|
|
28
|
+
.join("\n");
|
|
29
|
+
const matcher = ignore({ ignorecase: false }).add(anchored);
|
|
30
|
+
return (path) => matcher.ignores(path);
|
|
31
|
+
}
|
|
32
|
+
export async function buildContextPaths(workspace, agentDir, agentPrefix, authPath) {
|
|
33
|
+
const inContext = (p) => {
|
|
34
|
+
const rel = relative(workspace, p);
|
|
35
|
+
return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? undefined : rel.split(sep).join("/");
|
|
36
|
+
};
|
|
37
|
+
// The secrets DIR is the unit of RESPONSIBILITY, but never the unit of the leak QUESTION below.
|
|
38
|
+
const secretsRel = inContext(resolveSecretsDir(agentDir));
|
|
39
|
+
const authRel = inContext(authPath);
|
|
40
|
+
const authElsewhere = authRel !== undefined && (secretsRel === undefined || !authRel.startsWith(`${secretsRel}/`));
|
|
41
|
+
const secretPaths = [...(secretsRel ? [secretsRel] : []), ...(authElsewhere ? [authRel] : [])];
|
|
42
|
+
// ONE rule for every checked path: a file that is not there cannot be baked, so gating on it would be a refusal
|
|
43
|
+
// about a spelling rather than about what would ship (an agent that has never run `login` has no auth.json).
|
|
44
|
+
const present = async (rels) => {
|
|
45
|
+
const found = [];
|
|
46
|
+
for (const rel of rels)
|
|
47
|
+
if (await exists(join(workspace, rel)))
|
|
48
|
+
found.push(rel);
|
|
49
|
+
return found;
|
|
50
|
+
};
|
|
51
|
+
// State gets the same treatment (a custom in-tree FASTAGENT_STATE_DIR is invisible to the name-based `**/.state`),
|
|
52
|
+
// at warn level.
|
|
53
|
+
const stateRel = inContext(resolveStateRoot(agentDir));
|
|
54
|
+
// Existence gates the WARNING, never the generated exclude (same split as secretPaths vs leakCandidates).
|
|
55
|
+
const stateShips = stateRel !== undefined && (await exists(join(workspace, stateRel))) ? stateRel : undefined;
|
|
56
|
+
// The `.env` family at the two levels fastagent is RESPONSIBLE for: the agent dir and the workspace root.
|
|
57
|
+
const dotEnvFiles = async (relDir) => {
|
|
58
|
+
const names = await readdir(join(workspace, relDir || ".")).catch(() => []);
|
|
59
|
+
// POSIX separators, like every other context-relative path here (`inContext`).
|
|
60
|
+
return names
|
|
61
|
+
.filter((n) => (n === ".env" || n.startsWith(".env.")) && n !== ".env.example")
|
|
62
|
+
.map((n) => join(relDir, n).split(sep).join("/"));
|
|
63
|
+
};
|
|
64
|
+
const envFiles = (await Promise.all([...new Set(["", agentPrefix])].map(dotEnvFiles))).flat();
|
|
65
|
+
// Everything ACTUALLY inside the secrets dir, minus the two tracked scaffolds the image ships on purpose (they
|
|
66
|
+
// carry no values; the generated ignore re-includes them by name).
|
|
67
|
+
const secretDirFiles = async (dirRel) => {
|
|
68
|
+
const entries = await readdir(join(workspace, dirRel), { withFileTypes: true }).catch(() => []);
|
|
69
|
+
const files = [];
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (entry.name === ".gitignore" || entry.name === ".env.example")
|
|
72
|
+
continue;
|
|
73
|
+
if (entry.isDirectory())
|
|
74
|
+
files.push(...(await secretDirFiles(`${dirRel}/${entry.name}`)));
|
|
75
|
+
else
|
|
76
|
+
files.push(`${dirRel}/${entry.name}`);
|
|
77
|
+
}
|
|
78
|
+
return files;
|
|
79
|
+
};
|
|
80
|
+
const leakCandidates = [
|
|
81
|
+
...(secretsRel ? await secretDirFiles(secretsRel) : []),
|
|
82
|
+
...(await present(authElsewhere && authRel !== undefined ? [authRel] : [])),
|
|
83
|
+
...envFiles,
|
|
84
|
+
];
|
|
85
|
+
// Same existence rule: a node_modules that is not there cannot be uploaded.
|
|
86
|
+
const depDirs = await present([...new Set([`${agentPrefix}node_modules`, "node_modules"])]);
|
|
87
|
+
const machineryPaths = [...secretPaths, ...(stateRel ? [stateRel] : [])];
|
|
88
|
+
return { machineryPaths, leakCandidates, stateShips, depDirs };
|
|
89
|
+
}
|
|
90
|
+
/** Interrogate BOTH ignore files deploy emits, when the author kept them. */
|
|
91
|
+
export async function checkKeptIgnoreFiles(ctx, report) {
|
|
92
|
+
const { workspace, agentDir, agentPrefix, force } = ctx;
|
|
93
|
+
const { leakCandidates, stateShips, depDirs } = ctx.paths;
|
|
94
|
+
for (const rel of [".dockerignore", `${agentPrefix}Dockerfile.dockerignore`]) {
|
|
95
|
+
const kept = await readTextIfExists(join(workspace, rel));
|
|
96
|
+
if (kept === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
// One WE generated is regenerated by this very run under --force, so checking the stale content on disk would
|
|
99
|
+
// gate a deploy on a file about to be replaced.
|
|
100
|
+
const keptIsOurs = isGeneratedDockerignore(kept);
|
|
101
|
+
if (force && keptIsOurs)
|
|
102
|
+
continue;
|
|
103
|
+
const remedy = (lines) => keptIsOurs
|
|
104
|
+
? `Re-run with --force to regenerate it.`
|
|
105
|
+
: `Add ${lines.map((p) => `\`${p}\``).join(" and ")} before deploying (the same lines the generated ${rel} writes).`;
|
|
106
|
+
const excluded = dockerignoreMatcher(kept);
|
|
107
|
+
// Asked as a DIRECTORY (trailing slash), which is what it is.
|
|
108
|
+
if (excluded(`${basename(agentDir)}/`)) {
|
|
109
|
+
const text = `your ${rel} (kept) excludes \`${basename(agentDir)}\` — the build context would ship WITHOUT the ` +
|
|
110
|
+
`agent entirely (the deployed box has no persona/config and crash-loops). Remove that rule ` +
|
|
111
|
+
`before deploying.`;
|
|
112
|
+
report.issue(text);
|
|
113
|
+
}
|
|
114
|
+
// Resolved paths, not spellings: dockerignore patterns are root-anchored (unlike .gitignore), so a bare
|
|
115
|
+
// `.secrets` line does not cover `fastagent/.secrets`.
|
|
116
|
+
const leaks = leakCandidates.filter((p) => !excluded(p));
|
|
117
|
+
if (leaks.length > 0) {
|
|
118
|
+
const text = `your ${rel} (kept) does not exclude ${leaks.map((p) => `\`${p}\``).join(", ")} — the build ` +
|
|
119
|
+
`context would BAKE SECRETS INTO THE IMAGE. ${remedy(leaks.map((p) => `/${p}`))}`;
|
|
120
|
+
report.issue(text);
|
|
121
|
+
}
|
|
122
|
+
if (stateShips && !excluded(`${stateShips}/sessions`)) {
|
|
123
|
+
report.warn(`your ${rel} (kept) does not exclude \`${stateShips}\` — the build machine's sessions/channel state would ship in the image. ${remedy([`/${stateShips}`])}`);
|
|
124
|
+
}
|
|
125
|
+
// Both the agent's own node_modules and the workspace's.
|
|
126
|
+
const unexcludedDeps = depDirs.filter((p) => !excluded(`${p}/.package-lock.json`));
|
|
127
|
+
if (unexcludedDeps.length > 0) {
|
|
128
|
+
report.warn(`your ${rel} does not exclude ${unexcludedDeps.map((p) => `\`${p}\``).join(" or ")} — the ` +
|
|
129
|
+
`build machine's deps (native binaries for YOUR OS) would be uploaded and clobber the image's ` +
|
|
130
|
+
`freshly-installed ones. ${remedy(unexcludedDeps.map((p) => `/${p}`))}`);
|
|
131
|
+
}
|
|
132
|
+
if (excluded(".git/HEAD")) {
|
|
133
|
+
report.note(`your ${rel} excludes .git — the baked copy ships WITHOUT history/remote, so the agent ` +
|
|
134
|
+
`cannot pull/commit/push it; it must \`git clone\` its repo in the workspace instead (or remove the .git line).`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -1,54 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HOW A RUNNING CHANNEL IS REACHED — the URL-neutral half of "point this channel at the agent": its
|
|
3
|
-
* default route, whether anything can set that URL end-to-end, and the words an operator needs when
|
|
4
|
-
* nothing can. Only the base URL varies, which is the argument every function here takes — a deploy
|
|
5
|
-
* host's (`<app>.fly.dev`, a minted Railway domain, a Function URL) or `dev --tunnel`'s Quick Tunnel.
|
|
6
|
-
* It lives under `deploy/` because that is where its answers are mostly consumed and where the gate
|
|
7
|
-
* policy it composes with lives; the serving path reads the same answers through `tunnel.ts`.
|
|
8
|
-
*
|
|
9
|
-
* It exists because that knowledge belongs to the CHANNEL and was written per HOST: three runbook
|
|
10
|
-
* plans, three `--run` drivers, a path table in the docker plan and the `--tunnel` announcer each
|
|
11
|
-
* hand-wrote the same five-branch if-chain. A rule spelled seven times drifts, and it did — the
|
|
12
|
-
* long-connection exception reached only the feishu/lark branches, so a long-connection Telegram
|
|
13
|
-
* deploy printed `setWebhook` in its runbook, which makes `getUpdates` return 409 and stops the
|
|
14
|
-
* channel the operator just deployed.
|
|
15
|
-
*
|
|
16
|
-
* Every function here takes the {@link DeclaredChannel} list and filters it ITSELF. Taking a
|
|
17
|
-
* pre-filtered list would put the rule back at the call sites, which is where it drifted from: the
|
|
18
|
-
* `--tunnel` announcer trusted its caller that way and had a default that passed every channel,
|
|
19
|
-
* long-connection ones included.
|
|
20
|
-
*
|
|
21
|
-
* A host adds its base URL and its own asides; it does not restate which channels have a webhook.
|
|
22
|
-
*/
|
|
1
|
+
/** HOW A RUNNING CHANNEL IS REACHED — the URL-neutral half of "point this channel at the agent". */
|
|
23
2
|
import type { DeclaredChannel } from "../channels/discover.ts";
|
|
24
3
|
import type { RegistrationOutcome } from "../channels/registration.ts";
|
|
25
4
|
import type { ChannelKind } from "../scaffold/add-channel.ts";
|
|
26
|
-
/** The registrars a host can drive. `telegram` is always available (fastagent holds the token and the
|
|
27
|
-
* URL); the others are optional so a caller without their credentials falls back to a manual step. */
|
|
28
5
|
export interface Registrars {
|
|
29
6
|
telegram: (baseUrl: string) => Promise<RegistrationOutcome>;
|
|
30
7
|
slack?: (baseUrl: string) => Promise<RegistrationOutcome>;
|
|
31
8
|
feishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>;
|
|
32
9
|
}
|
|
33
10
|
/**
|
|
34
|
-
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool
|
|
35
|
-
*
|
|
36
|
-
* drivers, the docker ingress note, `--tunnel`. A custom channel is skipped here and reported by the
|
|
37
|
-
* pre-flight instead: its URL is its author's to set.
|
|
11
|
+
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool knows how to
|
|
12
|
+
* instruct.
|
|
38
13
|
*/
|
|
39
14
|
export declare function webhookKinds(channels: readonly DeclaredChannel[]): ChannelKind[];
|
|
40
15
|
/** Default route keys for {@link webhookKinds}, in the same order (the docker plan's ingress note). */
|
|
41
16
|
export declare function webhookPaths(channels: readonly DeclaredChannel[]): string[];
|
|
42
|
-
/** The runbook block for every channel that needs a URL set by hand, `baseUrl` spelled the host's way
|
|
43
|
-
* (a literal `https://app.fly.dev`, or a placeholder like `<your-domain>` the operator fills in). */
|
|
44
|
-
export declare function webhookRunbook(baseUrl: string, channels: readonly DeclaredChannel[]): string[];
|
|
45
17
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* not wire reports `manual` with the operator's instruction. The two callers differ in what they do
|
|
49
|
-
* with the outcomes, not in how they are produced: a deploy gates on them, a long-running serve
|
|
50
|
-
* cannot ({@link registerWebhooks} is the gating half).
|
|
18
|
+
* The runbook block for every channel that needs a URL set by hand, `baseUrl` spelled the host's way (a literal
|
|
19
|
+
* `https://app.fly.dev`, or a placeholder like `<your-domain>` the operator fills in).
|
|
51
20
|
*/
|
|
21
|
+
export declare function webhookRunbook(baseUrl: string, channels: readonly DeclaredChannel[]): string[];
|
|
22
|
+
/** Point every channel that has a webhook at `baseUrl`, reporting what each one ended as. */
|
|
52
23
|
export declare function pointChannelsAt(input: {
|
|
53
24
|
baseUrl: string;
|
|
54
25
|
channels: readonly DeclaredChannel[];
|
|
@@ -58,11 +29,27 @@ export declare function pointChannelsAt(input: {
|
|
|
58
29
|
kind: ChannelKind;
|
|
59
30
|
outcome: RegistrationOutcome;
|
|
60
31
|
}[]>;
|
|
32
|
+
/** Poll a public `/health` until it answers 200; false on timeout. Injected by tests. */
|
|
33
|
+
export type PublicHealthProbe = (healthUrl: string) => Promise<boolean>;
|
|
61
34
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
35
|
+
* The readiness floor a host clears BEFORE {@link registerWebhooks}: `setWebhook` does not verify that anything
|
|
36
|
+
* answers the URL, so a deploy whose app crash-loops would otherwise point a live channel at a dead address and
|
|
37
|
+
* report success. The channels that DO verify (Slack/Feishu challenge) would fail here too, but as "registration
|
|
38
|
+
* failed" — a diagnosis that hides the actual cause.
|
|
39
|
+
*
|
|
40
|
+
* Asked ONLY when this deployment actually has a webhook to point. A definition with none (schedules only, the
|
|
41
|
+
* built-in `POST /invoke`, or long-connection channels) needs no inbound reachability at all, and demanding it would
|
|
42
|
+
* invent a failure for a deploy run from a network that cannot reach the platform's edge.
|
|
65
43
|
*/
|
|
44
|
+
export declare function publicHealthGate(input: {
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
channels: readonly DeclaredChannel[];
|
|
47
|
+
log: (msg: string) => void;
|
|
48
|
+
/** How THIS host is inspected and re-run — the only per-host words in the gate. */
|
|
49
|
+
inspectHint: string;
|
|
50
|
+
probe?: PublicHealthProbe;
|
|
51
|
+
}): Promise<string | undefined>;
|
|
52
|
+
/** {@link pointChannelsAt} plus the shared gate policy, for a command that EXITS. */
|
|
66
53
|
export declare function registerWebhooks(input: {
|
|
67
54
|
baseUrl: string;
|
|
68
55
|
channels: readonly DeclaredChannel[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registrationGate } from "./registration-gate.js";
|
|
2
|
+
import { waitForHealth } from "../channels/wait-health.js";
|
|
2
3
|
const feishuCloud = (kind, label) => ({
|
|
3
4
|
path: `/${kind}`,
|
|
4
5
|
register: (r, baseUrl) => r.feishu?.(baseUrl, kind),
|
|
@@ -22,17 +23,6 @@ const INGRESS = {
|
|
|
22
23
|
` -d url=${baseUrl}/telegram -d secret_token=<TELEGRAM_SECRET_TOKEN>`,
|
|
23
24
|
],
|
|
24
25
|
},
|
|
25
|
-
github: {
|
|
26
|
-
path: "/webhook",
|
|
27
|
-
// The env-var name is the part a first-time operator cannot guess, and this line is all `--tunnel`
|
|
28
|
-
// prints — there is no runbook beside it to carry the detail.
|
|
29
|
-
manual: (baseUrl) => `github: set the webhook in the repo (Settings → Webhooks) → ${baseUrl}/webhook (content type application/json, secret = GITHUB_WEBHOOK_SECRET)`,
|
|
30
|
-
runbook: (baseUrl) => [
|
|
31
|
-
`# Set the GitHub webhook (repo Settings → Webhooks). Default route POST /webhook; if you remapped`,
|
|
32
|
-
`# it in channels/github.ts, use your path:`,
|
|
33
|
-
`# Payload URL = ${baseUrl}/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`,
|
|
34
|
-
],
|
|
35
|
-
},
|
|
36
26
|
slack: {
|
|
37
27
|
path: "/slack",
|
|
38
28
|
register: (r, baseUrl) => r.slack?.(baseUrl),
|
|
@@ -47,10 +37,8 @@ const INGRESS = {
|
|
|
47
37
|
lark: feishuCloud("lark", "Lark"),
|
|
48
38
|
};
|
|
49
39
|
/**
|
|
50
|
-
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool
|
|
51
|
-
*
|
|
52
|
-
* drivers, the docker ingress note, `--tunnel`. A custom channel is skipped here and reported by the
|
|
53
|
-
* pre-flight instead: its URL is its author's to set.
|
|
40
|
+
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool knows how to
|
|
41
|
+
* instruct.
|
|
54
42
|
*/
|
|
55
43
|
export function webhookKinds(channels) {
|
|
56
44
|
const declared = new Set(channels.filter((c) => c.ingress === "webhook").map((c) => c.name));
|
|
@@ -60,24 +48,20 @@ export function webhookKinds(channels) {
|
|
|
60
48
|
export function webhookPaths(channels) {
|
|
61
49
|
return webhookKinds(channels).map((kind) => INGRESS[kind].path);
|
|
62
50
|
}
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
51
|
+
/**
|
|
52
|
+
* The runbook block for every channel that needs a URL set by hand, `baseUrl` spelled the host's way (a literal
|
|
53
|
+
* `https://app.fly.dev`, or a placeholder like `<your-domain>` the operator fills in).
|
|
54
|
+
*/
|
|
65
55
|
export function webhookRunbook(baseUrl, channels) {
|
|
66
56
|
return webhookKinds(channels).flatMap((kind) => INGRESS[kind].runbook(baseUrl));
|
|
67
57
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Point every channel that has a webhook at `baseUrl`, reporting what each one ended as. All channels
|
|
70
|
-
* are attempted — one failure does not skip the rest — and a channel whose registrar the caller did
|
|
71
|
-
* not wire reports `manual` with the operator's instruction. The two callers differ in what they do
|
|
72
|
-
* with the outcomes, not in how they are produced: a deploy gates on them, a long-running serve
|
|
73
|
-
* cannot ({@link registerWebhooks} is the gating half).
|
|
74
|
-
*/
|
|
58
|
+
/** Point every channel that has a webhook at `baseUrl`, reporting what each one ended as. */
|
|
75
59
|
export async function pointChannelsAt(input) {
|
|
76
60
|
const outcomes = [];
|
|
77
61
|
for (const kind of webhookKinds(input.channels)) {
|
|
78
62
|
const ingress = INGRESS[kind];
|
|
79
63
|
// Calling IS the question: a channel whose registrar the caller did not wire returns undefined.
|
|
80
|
-
const running = ingress.register
|
|
64
|
+
const running = ingress.register(input.registrars, input.baseUrl);
|
|
81
65
|
if (!running) {
|
|
82
66
|
input.log(ingress.manual(input.baseUrl));
|
|
83
67
|
outcomes.push({ kind, outcome: "manual" }); // a human's step — re-surfaced after registrar output
|
|
@@ -89,10 +73,55 @@ export async function pointChannelsAt(input) {
|
|
|
89
73
|
return outcomes;
|
|
90
74
|
}
|
|
91
75
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* message, or undefined when nothing gates.
|
|
76
|
+
* How long a just-deployed public URL gets to answer `/health`: the FIRST boot seeds the whole workspace onto the
|
|
77
|
+
* volume before it binds a port, so this budget covers a copy on slow host storage, not a listen.
|
|
95
78
|
*/
|
|
79
|
+
const PUBLIC_HEALTH_TIMEOUT_MS = 180_000;
|
|
80
|
+
/** How often the wait announces itself, so a crash-looping app is not three silent minutes. */
|
|
81
|
+
const WAIT_NOTICE_MS = 15_000;
|
|
82
|
+
/**
|
|
83
|
+
* The readiness floor a host clears BEFORE {@link registerWebhooks}: `setWebhook` does not verify that anything
|
|
84
|
+
* answers the URL, so a deploy whose app crash-loops would otherwise point a live channel at a dead address and
|
|
85
|
+
* report success. The channels that DO verify (Slack/Feishu challenge) would fail here too, but as "registration
|
|
86
|
+
* failed" — a diagnosis that hides the actual cause.
|
|
87
|
+
*
|
|
88
|
+
* Asked ONLY when this deployment actually has a webhook to point. A definition with none (schedules only, the
|
|
89
|
+
* built-in `POST /invoke`, or long-connection channels) needs no inbound reachability at all, and demanding it would
|
|
90
|
+
* invent a failure for a deploy run from a network that cannot reach the platform's edge.
|
|
91
|
+
*/
|
|
92
|
+
export async function publicHealthGate(input) {
|
|
93
|
+
if (webhookKinds(input.channels).length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
const healthUrl = `${input.baseUrl}/health`;
|
|
96
|
+
input.log(`waiting for ${healthUrl} (up to ${PUBLIC_HEALTH_TIMEOUT_MS / 1000}s)…`);
|
|
97
|
+
const probe = input.probe ?? ((url) => waitForHealth(url, PUBLIC_HEALTH_TIMEOUT_MS, 500, announce(input.log)));
|
|
98
|
+
if (await probe(healthUrl))
|
|
99
|
+
return undefined;
|
|
100
|
+
// The infrastructure is up and only the webhook is missing, so hand over the manual route rather than leaving
|
|
101
|
+
// `re-run` as the only way out (a Fly re-run repeats the remote build).
|
|
102
|
+
for (const line of webhookRunbook(input.baseUrl, input.channels))
|
|
103
|
+
input.log(line);
|
|
104
|
+
return (`the deployed agent did not become healthy at ${healthUrl}, so no webhook was registered — ${input.inspectHint}. ` +
|
|
105
|
+
`To point the channels by hand instead, use the lines above.`);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Keep the wait visible. This occupies `waitForHealth`'s liveness slot without answering it: neither Fly nor Railway
|
|
109
|
+
* has a liveness question cheaper than the wait it would shorten (each is a CLI round trip, unlike docker's local
|
|
110
|
+
* `compose ps`), so the hook reports progress and never cuts the budget short.
|
|
111
|
+
*/
|
|
112
|
+
function announce(log) {
|
|
113
|
+
const started = Date.now();
|
|
114
|
+
let last = started;
|
|
115
|
+
return async () => {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
if (now - last >= WAIT_NOTICE_MS) {
|
|
118
|
+
last = now;
|
|
119
|
+
log(`still waiting for /health (${Math.round((now - started) / 1000)}s)…`);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** {@link pointChannelsAt} plus the shared gate policy, for a command that EXITS. */
|
|
96
125
|
export async function registerWebhooks(input) {
|
|
97
126
|
const reg = registrationGate(input.log, input.retryHint);
|
|
98
127
|
for (const { kind, outcome } of await pointChannelsAt(input))
|
|
@@ -2,25 +2,19 @@ export interface Artifact {
|
|
|
2
2
|
path: string;
|
|
3
3
|
content: string;
|
|
4
4
|
}
|
|
5
|
-
/** Fixed first-line prefix of a fastagent-generated Dockerfile
|
|
6
|
-
* `head` below) and the "is this ours?" check (cli deploy's kept-Dockerfile apt warn) share, so they
|
|
7
|
-
* can't drift into detecting a marker the generator no longer writes. */
|
|
5
|
+
/** Fixed first-line prefix of a fastagent-generated Dockerfile. */
|
|
8
6
|
export declare const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
|
|
9
|
-
/** The ONE predicate for "did fastagent generate this Dockerfile?"
|
|
10
|
-
* marker, so a hand-written one (or a file where the marker was dropped to take ownership) reads false.
|
|
11
|
-
* Both the "keep vs flag-as-stale" decision and the apt warning share this; a split would let them
|
|
12
|
-
* disagree on the same file (e.g. a marker not at byte 0). */
|
|
7
|
+
/** The ONE predicate for "did fastagent generate this Dockerfile?". */
|
|
13
8
|
export declare function isGeneratedDockerfile(content: string): boolean;
|
|
14
|
-
/** Did fastagent generate this `.dockerignore`? (Same ownership rule as {@link isGeneratedDockerfile}.) */
|
|
15
9
|
export declare function isGeneratedDockerignore(content: string): boolean;
|
|
16
10
|
export interface ContainerInput {
|
|
11
|
+
/** Stable for one release; generated once by the deploy preflight, never on container restart. */
|
|
12
|
+
releaseId: string;
|
|
17
13
|
/** Whether the agent has a package.json (a code agent); else a pure markdown/skills agent. */
|
|
18
14
|
hasPackageJson: boolean;
|
|
19
|
-
/** The package manager the generated image targets
|
|
20
|
-
* `oven/bun` base + `bun install` + `bun run fastagent`; otherwise npm on `node:22-slim`. */
|
|
15
|
+
/** The package manager the generated image targets. */
|
|
21
16
|
runtime: "node" | "bun";
|
|
22
|
-
/** Whether the runtime's lockfile exists (package-lock.json for npm, bun.lock/bun.lockb for bun)
|
|
23
|
-
* frozen/ci install needs it; without it the build resolves versions at build time (not reproducible). */
|
|
17
|
+
/** Whether the runtime's lockfile exists (package-lock.json for npm, bun.lock/bun.lockb for bun). */
|
|
24
18
|
hasLockfile: boolean;
|
|
25
19
|
/** For `runtime: "bun"`, the `FROM oven/bun:<tag>` version (from packageManager `bun@x`), else `"1"`. */
|
|
26
20
|
bunVersion?: string;
|
|
@@ -28,30 +22,22 @@ export interface ContainerInput {
|
|
|
28
22
|
version: string;
|
|
29
23
|
/** Extra apt packages (fastagent.config deploy.apt) baked in for the agent's tools — git, ripgrep, …. */
|
|
30
24
|
apt?: string[];
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
25
|
+
/**
|
|
26
|
+
* The model this deployment resolved, recorded in the release manifest — set only when the deployed environment's
|
|
27
|
+
* VALUE FILE named it (a `config.model` needs nothing: the config ships too). The manifest is the carrier because
|
|
28
|
+
* it is rebuilt and rewritten on every deploy, so the answer cannot go stale, and nothing on the way in can
|
|
29
|
+
* interpolate the operator's shell.
|
|
30
|
+
*/
|
|
31
|
+
modelSpec?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Where deploy's artifacts and the agent's own files sit, relative to the BUILD CONTEXT (which is always the
|
|
34
|
+
* workspace).
|
|
35
|
+
*/
|
|
35
36
|
agentPrefix: string;
|
|
36
|
-
/** Workspace-relative machinery paths the NAME-based excludes below would miss
|
|
37
|
-
* `FASTAGENT_SECRETS_DIR`/`FASTAGENT_STATE_DIR` pointed at an in-tree directory not called
|
|
38
|
-
* `.secrets`/`.state`. Preflight resolves them; excluding by PATH is what keeps `COPY . .` from
|
|
39
|
-
* baking a credential (or the build machine's sessions). */
|
|
37
|
+
/** Workspace-relative machinery paths the NAME-based excludes below would miss. */
|
|
40
38
|
machineryPaths?: string[];
|
|
41
|
-
/** Whether the baked workspace ships a `.git` (preflight fact).
|
|
42
|
-
* merged "git" into `apt` (the write-back loop needs history + binary together); the plans word
|
|
43
|
-
* their runbook's freshness/write-back guidance from the same fact. */
|
|
39
|
+
/** Whether the baked workspace ships a `.git` (preflight fact). */
|
|
44
40
|
shipsGit?: boolean;
|
|
45
41
|
}
|
|
46
|
-
/**
|
|
47
|
-
* The Dockerfile + ignore artifacts — spread into any host's artifact list. Everything is prefixed with
|
|
48
|
-
* {@link ContainerInput.agentPrefix}, so a NESTED agent's Dockerfile lands under `fastagent/` (never
|
|
49
|
-
* colliding with one the workspace already owns) while an agent that IS the workspace lands it at the root, where it IS
|
|
50
|
-
* the agent's own file. The ignore ships in TWO forms because context packing is host-CLI-owned and
|
|
51
|
-
* inconsistent: (1) a ROOT `.dockerignore` — the only form flyctl/railway's own context packers
|
|
52
|
-
* reliably read (kept if the workspace already has one — preflight then checks the machinery/secret
|
|
53
|
-
* excludes it must carry) — and (2) a per-Dockerfile `Dockerfile.dockerignore` for plain docker/buildx
|
|
54
|
-
* builds. When the agent sits inside the workspace that root file is the ONE write deploy makes outside the agent dir, and
|
|
55
|
-
* only at deploy time — without it the host CLI's packer would bake `.secrets/` into the image.
|
|
56
|
-
*/
|
|
42
|
+
/** The Dockerfile + ignore artifacts — spread into any host's artifact list. */
|
|
57
43
|
export declare function containerArtifacts(input: ContainerInput): Artifact[];
|