@fastagent-sh/fastagent 0.18.0 → 0.20.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 +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +23 -0
- package/dist/atomic-write.js +53 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +55 -0
- package/dist/channels/agentcore-service.js +107 -0
- package/dist/channels/agentcore.d.ts +22 -20
- package/dist/channels/agentcore.js +20 -11
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +55 -7
- package/dist/channels/control.js +312 -67
- package/dist/channels/discover.d.ts +59 -0
- package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +6 -9
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +41 -24
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
- package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
- package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
- package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +2 -2
- package/dist/channels/slack/invoke-turn.js +13 -6
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +23 -55
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +20 -20
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +67 -13
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +3 -3
- package/dist/channels/telegram/telegram.js +60 -72
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +25 -40
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -5
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +80 -121
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +22 -70
- package/dist/cli/serve.js +66 -257
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +53 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +11 -6
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +295 -0
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +5 -5
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +67 -42
- package/dist/engines/pi/create.js +142 -72
- package/dist/engines/pi/definition.d.ts +42 -8
- package/dist/engines/pi/definition.js +114 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +39 -20
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +10 -8
- package/dist/engines/pi/open.js +21 -11
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +27 -0
- package/dist/engines/pi/session-builder.js +87 -118
- package/dist/engines/pi/session-control.d.ts +28 -14
- package/dist/engines/pi/session-control.js +570 -438
- package/dist/engines/pi/session-inheritance.d.ts +30 -0
- package/dist/engines/pi/session-inheritance.js +261 -0
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +15 -3
- package/dist/engines/pi/session-settings.js +22 -1
- package/dist/engines/pi/session-store.d.ts +140 -0
- package/dist/engines/pi/session-store.js +666 -0
- package/dist/engines/pi/tool-context.d.ts +36 -13
- package/dist/engines/pi/tool-context.js +51 -4
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +3 -3
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/env.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +37 -2
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +2 -2
- package/dist/schedule/wakeups.js +11 -8
- package/dist/service.d.ts +157 -0
- package/dist/service.js +376 -0
- package/dist/session-remote.d.ts +8 -3
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +229 -93
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +14 -5
- package/dist/engines/pi/channel.d.ts +0 -37
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { createHash } from "node:crypto";
|
|
31
31
|
import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
|
|
32
32
|
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
33
|
+
import { webhookKinds, webhookRunbook } from "../channel-ingress.js";
|
|
33
34
|
import { containerArtifacts } from "../container.js";
|
|
34
35
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
35
36
|
/** SessionStorage mount = FASTAGENT_STATE_DIR (AgentCore requires exactly `/mnt/<one-level>`). It is
|
|
@@ -215,15 +216,18 @@ function yamlSingleQuote(value) {
|
|
|
215
216
|
return `'${value.replace(/'/g, "''")}'`;
|
|
216
217
|
}
|
|
217
218
|
/**
|
|
218
|
-
* The forwarder Lambda source — the ONLY string both the
|
|
219
|
+
* The forwarder Lambda source — the ONLY string both the deployment package and the readable
|
|
219
220
|
* `lambda/forwarder.js` artifact are generated from (one source, no drift). Zero-dependency: the
|
|
220
|
-
* Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE:
|
|
221
|
-
*
|
|
221
|
+
* Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: the package's entry lands as
|
|
222
|
+
* `index.js` ({@link zipSingleFile}), where ESM `import` is a syntax error. Two
|
|
222
223
|
* event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
|
|
223
224
|
* channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
|
|
224
225
|
* Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
|
|
225
|
-
* (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
|
|
226
|
-
*
|
|
226
|
+
* (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
|
|
227
|
+
*
|
|
228
|
+
* It ships as an S3 object rather than inline `ZipFile`, so there is no 4096-byte ceiling on it — a
|
|
229
|
+
* constraint this comment asserted for long after the template stopped inlining, which is the kind
|
|
230
|
+
* of stale rule that stops the next person making a correct change.
|
|
227
231
|
*/
|
|
228
232
|
export function forwarderSource() {
|
|
229
233
|
return `// Generated by \`fastagent deploy agentcore\` — the deployment's only ingress.
|
|
@@ -250,6 +254,15 @@ let ownUrl; // self-resolved once per cold start; rides on every envelope for th
|
|
|
250
254
|
const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => \`%\${c.charCodeAt(0).toString(16).toUpperCase()}\`);
|
|
251
255
|
const hmac = (key, data) => crypto.createHmac("sha256", key).update(data).digest();
|
|
252
256
|
|
|
257
|
+
// Every shared-secret gate on this public URL goes through here. Constant-time, and an unset
|
|
258
|
+
// expected secret NEVER matches — otherwise a topology that did not configure one would accept a
|
|
259
|
+
// request that sent none. Non-string input is coerced to "" first: Buffer.from(8) allocates eight
|
|
260
|
+
// bytes rather than throwing, which would compare a number against a secret by length alone.
|
|
261
|
+
const secretEq = (given, expected) => {
|
|
262
|
+
const a = Buffer.from(typeof given === "string" ? given : ""), b = Buffer.from(expected || "");
|
|
263
|
+
return b.length > 0 && a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
264
|
+
};
|
|
265
|
+
|
|
253
266
|
function presign(method, seconds) {
|
|
254
267
|
const bucket = process.env.STATE_BUCKET, key = process.env.STATE_KEY, region = process.env.AWS_REGION;
|
|
255
268
|
const host = \`\${bucket}.s3.\${region}.amazonaws.com\`;
|
|
@@ -375,11 +388,7 @@ exports.handler = async (event, ctx) => {
|
|
|
375
388
|
// The container may settle long after the webhook Lambda (and its credentials) expired.
|
|
376
389
|
if (event.rawPath === "/__fastagent/state-urls") {
|
|
377
390
|
const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
|
|
378
|
-
|
|
379
|
-
const expected = Buffer.from(process.env.STATE_REFRESH_SECRET || "");
|
|
380
|
-
if (!expected.length || actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
|
|
381
|
-
return { statusCode: 403, body: "forbidden\\n" };
|
|
382
|
-
}
|
|
391
|
+
if (!secretEq(req.auth, process.env.STATE_REFRESH_SECRET)) return { statusCode: 403, body: "forbidden\\n" };
|
|
383
392
|
return {
|
|
384
393
|
statusCode: 200,
|
|
385
394
|
headers: { "content-type": "application/json" },
|
|
@@ -389,7 +398,7 @@ exports.handler = async (event, ctx) => {
|
|
|
389
398
|
// The container's wake-alarm callback (reserved path, shared secret) — handled HERE, never forwarded.
|
|
390
399
|
if (event.rawPath === "/__fastagent/wake-alarm") {
|
|
391
400
|
const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
|
|
392
|
-
if (!
|
|
401
|
+
if (!secretEq(req.secret, process.env.WAKE_SECRET)) return { statusCode: 403, body: "forbidden\\n" };
|
|
393
402
|
const failed = await syncAlarms(req.alarms || [], ctx);
|
|
394
403
|
// Partial failure IS failure: the container retries the whole (idempotent) set until every
|
|
395
404
|
// pending wake really has its alarm.
|
|
@@ -405,7 +414,7 @@ exports.handler = async (event, ctx) => {
|
|
|
405
414
|
// public traffic) are probeable too.
|
|
406
415
|
if (event.rawPath === "/__fastagent/probe") {
|
|
407
416
|
const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
|
|
408
|
-
if (!
|
|
417
|
+
if (!secretEq(req.auth, process.env.INGRESS_SECRET)) return { statusCode: 403, body: "forbidden\\n" };
|
|
409
418
|
const r = await invoke({ kind: "probe" });
|
|
410
419
|
if (r.status !== 200) {
|
|
411
420
|
console.log(\`probe transport error \${r.status}: \${r.body}\`);
|
|
@@ -465,7 +474,7 @@ function template(input, translated) {
|
|
|
465
474
|
// a schedule turn can outlive both its original presigned URL and the Lambda credentials that signed
|
|
466
475
|
// it. Schedule-only URLs reject every non-reserved HTTP path before invoking AgentCore, so they do
|
|
467
476
|
// not expose a webhook/data plane (and start never mounts the builtin /invoke under AgentCore).
|
|
468
|
-
const needsForwarder = input.
|
|
477
|
+
const needsForwarder = input.channels.some((channel) => channel.ingress === "webhook") || translated.length > 0 || input.selfSchedule;
|
|
469
478
|
const needsFunctionUrl = needsForwarder;
|
|
470
479
|
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
471
480
|
const forwarderFnArn = `!Sub arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:fastagent-${input.name}-forwarder`;
|
|
@@ -604,7 +613,7 @@ function template(input, translated) {
|
|
|
604
613
|
` Action: lambda:GetFunctionUrlConfig`,
|
|
605
614
|
` Resource: ${forwarderFnArn}`,
|
|
606
615
|
]
|
|
607
|
-
: []), ``, ` Forwarder:`, ` Type: AWS::Lambda::Function`, ` Properties:`, ` FunctionName: fastagent-${input.name}-forwarder`, ` Runtime: nodejs22.x`, ` Handler: index.handler`, ` # Webhook ACKs are fast, but schedule-fire holds the connection for the WHOLE agent turn`, ` # (claim-before-run means a timeout never double-fires; the turn also continues and is`, ` # audited container-side). EventBridge→Lambda is async, so the long timeout costs nothing.`, ` Timeout: 900`, ` MemorySize: 256`, ` Role: !GetAtt ForwarderRole.Arn`, ` Environment:`, ` Variables:`, ` RUNTIME_ARN: !GetAtt Runtime.AgentRuntimeArn`, ` INGRESS_SESSION_ID: ${ingressSessionId(input.name)}`, ...(needsFunctionUrl ? [` STATE_REFRESH_SECRET: !Ref FastagentIngressSecret`] : []), ...(input.
|
|
616
|
+
: []), ``, ` Forwarder:`, ` Type: AWS::Lambda::Function`, ` Properties:`, ` FunctionName: fastagent-${input.name}-forwarder`, ` Runtime: nodejs22.x`, ` Handler: index.handler`, ` # Webhook ACKs are fast, but schedule-fire holds the connection for the WHOLE agent turn`, ` # (claim-before-run means a timeout never double-fires; the turn also continues and is`, ` # audited container-side). EventBridge→Lambda is async, so the long timeout costs nothing.`, ` Timeout: 900`, ` MemorySize: 256`, ` Role: !GetAtt ForwarderRole.Arn`, ` Environment:`, ` Variables:`, ` RUNTIME_ARN: !GetAtt Runtime.AgentRuntimeArn`, ` INGRESS_SESSION_ID: ${ingressSessionId(input.name)}`, ...(needsFunctionUrl ? [` STATE_REFRESH_SECRET: !Ref FastagentIngressSecret`] : []), ...(input.channels.some((channel) => channel.ingress === "webhook") ? [` WEBHOOKS_ENABLED: "1"`] : []), ...(input.selfSchedule
|
|
608
617
|
? [
|
|
609
618
|
` WAKE_SECRET: !Ref FastagentWakeSecret`,
|
|
610
619
|
` WAKE_ROLE_ARN: !GetAtt WakeSchedulerRole.Arn`,
|
|
@@ -671,7 +680,7 @@ export function planAgentcoreDeploy(input) {
|
|
|
671
680
|
}
|
|
672
681
|
// Every forwarder needs its authenticated Function URL to refresh S3 snapshot capabilities during
|
|
673
682
|
// a long turn. Without route channels, ordinary HTTP paths are rejected before AgentCore is invoked.
|
|
674
|
-
const needsForwarder = input.
|
|
683
|
+
const needsForwarder = input.channels.some((channel) => channel.ingress === "webhook") || translated.length > 0 || input.selfSchedule;
|
|
675
684
|
const needsFunctionUrl = needsForwarder;
|
|
676
685
|
const artifacts = [
|
|
677
686
|
{ path: `${prefix}${TEMPLATE_FILE}`, content: template(input, translated) },
|
|
@@ -743,22 +752,21 @@ export function planAgentcoreDeploy(input) {
|
|
|
743
752
|
? `# Model auth: none found at the local auth path — pass --auth-path <file>, or \`--run\` carries it`
|
|
744
753
|
: `# Model auth: your local auth is "${input.modelAuth}" — the plan can't read its value; \`--run\` carries it`, `# as the FastagentAuthSeed parameter (base64 of auth.json), materialized on first boot.`);
|
|
745
754
|
}
|
|
746
|
-
// Post-deploy webhook registration —
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
post.push(`# Set the ${kind === "feishu" ? "Feishu" : "Lark"} event Request URL (developer console → Events & Callbacks):`, `# Request URL = <ForwarderUrl>/${kind} (the stack must be deployed when you save — the console`, `# sends a challenge, which rides through the forwarder to the channel and back verbatim).`);
|
|
755
|
+
// Post-deploy webhook registration — the shared channel-ingress steps, pointed at the forwarder's
|
|
756
|
+
// Function URL (read from the stack outputs). `--run` REFUSES a long-connection channel here, but
|
|
757
|
+
// generate-only only warns and still prints this runbook, so the steps are filtered on ingress like
|
|
758
|
+
// every other host's rather than on the CLI having gated.
|
|
759
|
+
const post = webhookRunbook(`<ForwarderUrl>`, channels);
|
|
760
|
+
// AgentCore asides the shared steps cannot carry: nothing else routes through a forwarder, and
|
|
761
|
+
// nothing else has a compute ceiling.
|
|
762
|
+
// Named rather than positional: the asides land after ALL the steps, so "the console" has to say
|
|
763
|
+
// WHICH console. One line for both kinds — an agent that declares feishu AND lark reads the same
|
|
764
|
+
// fact twice otherwise and looks for a second step that does not exist.
|
|
765
|
+
if (webhookKinds(channels).some((kind) => kind === "feishu" || kind === "lark")) {
|
|
766
|
+
post.push(`# NOTE: the Feishu/Lark console's challenge rides through the forwarder to the channel and back`, `# verbatim, so the stack must be deployed when you save the Request URL.`);
|
|
767
|
+
}
|
|
768
|
+
if (channels.some((channel) => channel.name === "github")) {
|
|
769
|
+
post.push(`# NOTE: github turns are fire-and-forget with no replay — a compute reclaimed mid-review drops it`, `# (the ping's HealthyBusy + time_of_last_update holds the session while turns run, but the 8 h compute ceiling is hard).`);
|
|
762
770
|
}
|
|
763
771
|
if (post.length > 0)
|
|
764
772
|
runbook.push(``, ...post);
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* a caller-provided temp parameters file (`file://…`, mode 0600, deleted by the caller) — the write
|
|
15
15
|
* is injected to keep this module pure and the security-sensitive wiring testable.
|
|
16
16
|
*/
|
|
17
|
-
import type {
|
|
18
|
-
import type
|
|
17
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
18
|
+
import { type Registrars } from "../channel-ingress.ts";
|
|
19
19
|
import type { CliRunner } from "../runner.ts";
|
|
20
20
|
export interface AgentcoreRunPlan {
|
|
21
21
|
/** The base name — stack `fastagent-<name>`, ECR repo `fastagent/<name>`. */
|
|
@@ -35,7 +35,8 @@ export interface AgentcoreRunPlan {
|
|
|
35
35
|
secrets: Record<string, string>;
|
|
36
36
|
/** Required secret names with NO local value — gated before any side effect. */
|
|
37
37
|
missingSecrets: string[];
|
|
38
|
-
|
|
38
|
+
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
39
|
+
channels: readonly DeclaredChannel[];
|
|
39
40
|
/** Whether the topology includes the forwarder Lambda (route channels / schedules / selfSchedule).
|
|
40
41
|
* It owns the state snapshot's presigned URLs, so its absence means an invoke-only deployment
|
|
41
42
|
* with no cross-deploy state to keep. */
|
|
@@ -70,7 +71,7 @@ export declare function parseCheckpointReply(stdout: string): CheckpointReply |
|
|
|
70
71
|
* post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
|
|
71
72
|
* gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
|
|
72
73
|
*/
|
|
73
|
-
export declare function deployAgentcoreRun(plan: AgentcoreRunPlan, aws: CliRunner, docker: CliRunner, log: (msg: string) => void, writeSecretFile: (content: string) => Promise<string>, writeForwarderZip: (bytes: Uint8Array) => Promise<string>,
|
|
74
|
+
export declare function deployAgentcoreRun(plan: AgentcoreRunPlan, aws: CliRunner, docker: CliRunner, log: (msg: string) => void, writeSecretFile: (content: string) => Promise<string>, writeForwarderZip: (bytes: Uint8Array) => Promise<string>, registrars: Registrars,
|
|
74
75
|
/** Injected in tests; the probe itself stays inside the run so no deploy can skip it. */
|
|
75
76
|
probe?: {
|
|
76
77
|
fetchImpl?: typeof fetch;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registerWebhooks } from "../channel-ingress.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { Buffer } from "node:buffer";
|
|
4
4
|
import { AUTH_SEED_CHUNK_SIZE, AUTH_SEED_MAX_CHUNKS, cfnParamName, forwarderSource, ingressSessionId, stateBucketName, } from "./plan.js";
|
|
@@ -123,7 +123,7 @@ export function parseCheckpointReply(stdout) {
|
|
|
123
123
|
* post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
|
|
124
124
|
* gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
|
|
125
125
|
*/
|
|
126
|
-
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip,
|
|
126
|
+
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registrars,
|
|
127
127
|
/** Injected in tests; the probe itself stays inside the run so no deploy can skip it. */
|
|
128
128
|
probe = {}) {
|
|
129
129
|
const gate = (g) => ({ ok: false, gate: g });
|
|
@@ -457,40 +457,17 @@ probe = {}) {
|
|
|
457
457
|
}
|
|
458
458
|
// 9. Post-deploy webhook registration — same registrar seam as every host, pointed at the
|
|
459
459
|
// forwarder's Function URL. Gate policy is the shared registration-gate kernel.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
log
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (registerSlack) {
|
|
472
|
-
log("registering slack event URL…");
|
|
473
|
-
reg.track("slack", await registerSlack(url));
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
log(`slack: set Event Subscriptions → Request URL → ${url}/slack`);
|
|
477
|
-
reg.track("slack", "manual");
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
for (const kind of ["feishu", "lark"]) {
|
|
481
|
-
if (!plan.channels.includes(kind))
|
|
482
|
-
continue;
|
|
483
|
-
if (registerFeishu) {
|
|
484
|
-
log(`registering ${kind} event URL…`);
|
|
485
|
-
reg.track(kind, await registerFeishu(url, kind));
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
log(`${kind}: set the event Request URL (developer console → Events & Callbacks) → ${url}/${kind}`);
|
|
489
|
-
reg.track(kind, "manual");
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
const registrationGateMsg = reg.gate();
|
|
460
|
+
// No long-connection channels reach here: `deploy.ts` gates them before the driver runs (AgentCore
|
|
461
|
+
// has no resident process to hold a connection), so the deploy's channels are all webhook ones.
|
|
462
|
+
const registrationGateMsg = url
|
|
463
|
+
? await registerWebhooks({
|
|
464
|
+
baseUrl: url,
|
|
465
|
+
channels: plan.channels,
|
|
466
|
+
registrars,
|
|
467
|
+
log,
|
|
468
|
+
retryHint: "re-run to retry registration (steps already done are skipped)",
|
|
469
|
+
})
|
|
470
|
+
: undefined;
|
|
494
471
|
if (registrationGateMsg)
|
|
495
472
|
return gate(registrationGateMsg);
|
|
496
473
|
return { ok: true, runtimeArn, url };
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
*/
|
|
23
|
+
import type { DeclaredChannel } from "../channels/discover.ts";
|
|
24
|
+
import type { RegistrationOutcome } from "../channels/registration.ts";
|
|
25
|
+
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
|
+
export interface Registrars {
|
|
29
|
+
telegram: (baseUrl: string) => Promise<RegistrationOutcome>;
|
|
30
|
+
slack?: (baseUrl: string) => Promise<RegistrationOutcome>;
|
|
31
|
+
feishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool
|
|
35
|
+
* knows how to instruct. ONE answer for every host and every path — the runbooks, the `--run`
|
|
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.
|
|
38
|
+
*/
|
|
39
|
+
export declare function webhookKinds(channels: readonly DeclaredChannel[]): ChannelKind[];
|
|
40
|
+
/** Default route keys for {@link webhookKinds}, in the same order (the docker plan's ingress note). */
|
|
41
|
+
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
|
+
/**
|
|
46
|
+
* Point every channel that has a webhook at `baseUrl`, reporting what each one ended as. All channels
|
|
47
|
+
* are attempted — one failure does not skip the rest — and a channel whose registrar the caller did
|
|
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).
|
|
51
|
+
*/
|
|
52
|
+
export declare function pointChannelsAt(input: {
|
|
53
|
+
baseUrl: string;
|
|
54
|
+
channels: readonly DeclaredChannel[];
|
|
55
|
+
registrars: Registrars;
|
|
56
|
+
log: (msg: string) => void;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
kind: ChannelKind;
|
|
59
|
+
outcome: RegistrationOutcome;
|
|
60
|
+
}[]>;
|
|
61
|
+
/**
|
|
62
|
+
* {@link pointChannelsAt} plus the shared gate policy, for a command that EXITS: an exit 0 claims the
|
|
63
|
+
* deployment is reachable, so a failed registration has to become a non-zero one. Returns the gate
|
|
64
|
+
* message, or undefined when nothing gates.
|
|
65
|
+
*/
|
|
66
|
+
export declare function registerWebhooks(input: {
|
|
67
|
+
baseUrl: string;
|
|
68
|
+
channels: readonly DeclaredChannel[];
|
|
69
|
+
registrars: Registrars;
|
|
70
|
+
log: (msg: string) => void;
|
|
71
|
+
/** How THIS host retries — the only per-host words in the gate. */
|
|
72
|
+
retryHint: string;
|
|
73
|
+
}): Promise<string | undefined>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { registrationGate } from "./registration-gate.js";
|
|
2
|
+
const feishuCloud = (kind, label) => ({
|
|
3
|
+
path: `/${kind}`,
|
|
4
|
+
register: (r, baseUrl) => r.feishu?.(baseUrl, kind),
|
|
5
|
+
manual: (baseUrl) => `${kind}: set the event Request URL in the developer console (Events & Callbacks) → ${baseUrl}/${kind} (the app must be running when you save)`,
|
|
6
|
+
runbook: (baseUrl) => [
|
|
7
|
+
`# Set the ${label} event Request URL (developer console → Events & Callbacks). Default route`,
|
|
8
|
+
`# POST /${kind}; the app must be RUNNING when you save (the console verifies with a challenge):`,
|
|
9
|
+
`# Request URL = ${baseUrl}/${kind}`,
|
|
10
|
+
],
|
|
11
|
+
});
|
|
12
|
+
/** Declaration order — the order every runbook and every driver reports in. */
|
|
13
|
+
const INGRESS = {
|
|
14
|
+
telegram: {
|
|
15
|
+
path: "/telegram",
|
|
16
|
+
register: (r, baseUrl) => r.telegram(baseUrl),
|
|
17
|
+
manual: (baseUrl) => `telegram: set the webhook → ${baseUrl}/telegram (secret_token = TELEGRAM_SECRET_TOKEN)`,
|
|
18
|
+
runbook: (baseUrl) => [
|
|
19
|
+
`# Register the Telegram webhook. The path assumes the default route (POST /telegram); if you`,
|
|
20
|
+
`# remapped it in channels/telegram.ts, use your path. secret_token MUST equal TELEGRAM_SECRET_TOKEN:`,
|
|
21
|
+
`curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \\`,
|
|
22
|
+
` -d url=${baseUrl}/telegram -d secret_token=<TELEGRAM_SECRET_TOKEN>`,
|
|
23
|
+
],
|
|
24
|
+
},
|
|
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
|
+
slack: {
|
|
37
|
+
path: "/slack",
|
|
38
|
+
register: (r, baseUrl) => r.slack?.(baseUrl),
|
|
39
|
+
manual: (baseUrl) => `slack: set Event Subscriptions → Request URL → ${baseUrl}/slack`,
|
|
40
|
+
runbook: (baseUrl) => [
|
|
41
|
+
`# Set Slack Event Subscriptions → Request URL (default route POST /slack; the running service`,
|
|
42
|
+
`# answers Slack's challenge), and match scopes/subscriptions to channels/slack.ts groupBehavior:`,
|
|
43
|
+
`# Request URL = ${baseUrl}/slack`,
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
feishu: feishuCloud("feishu", "Feishu"),
|
|
47
|
+
lark: feishuCloud("lark", "Lark"),
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool
|
|
51
|
+
* knows how to instruct. ONE answer for every host and every path — the runbooks, the `--run`
|
|
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.
|
|
54
|
+
*/
|
|
55
|
+
export function webhookKinds(channels) {
|
|
56
|
+
const declared = new Set(channels.filter((c) => c.ingress === "webhook").map((c) => c.name));
|
|
57
|
+
return Object.keys(INGRESS).filter((kind) => declared.has(kind));
|
|
58
|
+
}
|
|
59
|
+
/** Default route keys for {@link webhookKinds}, in the same order (the docker plan's ingress note). */
|
|
60
|
+
export function webhookPaths(channels) {
|
|
61
|
+
return webhookKinds(channels).map((kind) => INGRESS[kind].path);
|
|
62
|
+
}
|
|
63
|
+
/** The runbook block for every channel that needs a URL set by hand, `baseUrl` spelled the host's way
|
|
64
|
+
* (a literal `https://app.fly.dev`, or a placeholder like `<your-domain>` the operator fills in). */
|
|
65
|
+
export function webhookRunbook(baseUrl, channels) {
|
|
66
|
+
return webhookKinds(channels).flatMap((kind) => INGRESS[kind].runbook(baseUrl));
|
|
67
|
+
}
|
|
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
|
+
*/
|
|
75
|
+
export async function pointChannelsAt(input) {
|
|
76
|
+
const outcomes = [];
|
|
77
|
+
for (const kind of webhookKinds(input.channels)) {
|
|
78
|
+
const ingress = INGRESS[kind];
|
|
79
|
+
// Calling IS the question: a channel whose registrar the caller did not wire returns undefined.
|
|
80
|
+
const running = ingress.register?.(input.registrars, input.baseUrl);
|
|
81
|
+
if (!running) {
|
|
82
|
+
input.log(ingress.manual(input.baseUrl));
|
|
83
|
+
outcomes.push({ kind, outcome: "manual" }); // a human's step — re-surfaced after registrar output
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
input.log(`registering ${kind} webhook…`);
|
|
87
|
+
outcomes.push({ kind, outcome: await running });
|
|
88
|
+
}
|
|
89
|
+
return outcomes;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* {@link pointChannelsAt} plus the shared gate policy, for a command that EXITS: an exit 0 claims the
|
|
93
|
+
* deployment is reachable, so a failed registration has to become a non-zero one. Returns the gate
|
|
94
|
+
* message, or undefined when nothing gates.
|
|
95
|
+
*/
|
|
96
|
+
export async function registerWebhooks(input) {
|
|
97
|
+
const reg = registrationGate(input.log, input.retryHint);
|
|
98
|
+
for (const { kind, outcome } of await pointChannelsAt(input))
|
|
99
|
+
reg.track(kind, outcome);
|
|
100
|
+
return reg.gate();
|
|
101
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* process, its loopback port, exact environment-variable names, and one persistent state volume.
|
|
5
5
|
* `--tunnel` can add an ephemeral Cloudflare Quick Tunnel service; durable ingress remains operator-owned.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
8
8
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
9
9
|
export interface DockerPlanInput extends ContainerInput {
|
|
10
10
|
/** Stable Compose project name, sanitized by {@link toDockerProjectName}. */
|
|
@@ -13,10 +13,8 @@ export interface DockerPlanInput extends ContainerInput {
|
|
|
13
13
|
port: number;
|
|
14
14
|
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
15
15
|
modelAuth: string | undefined;
|
|
16
|
-
/**
|
|
17
|
-
channels:
|
|
18
|
-
/** All long-connection channel basenames, including custom channels. */
|
|
19
|
-
longConnectionChannels?: string[];
|
|
16
|
+
/** Every declared channel and its ingress — the source of the secret list and the ingress note. */
|
|
17
|
+
channels: readonly DeclaredChannel[];
|
|
20
18
|
/** Generate an optional Cloudflare Quick Tunnel service in Compose. Generation only; `--run` starts it. */
|
|
21
19
|
tunnel: boolean;
|
|
22
20
|
/** Extra environment-variable names declared in config.deploy.secrets. */
|
|
@@ -39,7 +37,5 @@ export declare function isGeneratedCompose(content: string): boolean;
|
|
|
39
37
|
export declare function composeHasTunnelService(content: string): boolean;
|
|
40
38
|
/** Compose project names are lowercase [a-z0-9_-] and must start with an alphanumeric character. */
|
|
41
39
|
export declare function toDockerProjectName(directoryName: string): string;
|
|
42
|
-
/** Default first-party webhook paths. Workspace glue may remap them, so guidance labels them defaults. */
|
|
43
|
-
export declare function dockerWebhookPaths(channels: ChannelKind[]): string[];
|
|
44
40
|
/** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
|
|
45
41
|
export declare function planDockerDeploy(input: DockerPlanInput): DockerPlan;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { webhookPaths } from "../channel-ingress.js";
|
|
1
2
|
import { containerArtifacts } from "../container.js";
|
|
2
3
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
4
|
const MOUNT = "/data";
|
|
@@ -24,23 +25,12 @@ export function toDockerProjectName(directoryName) {
|
|
|
24
25
|
.replace(/[-_]+$/g, "");
|
|
25
26
|
return `fastagent-${slug || "agent"}`;
|
|
26
27
|
}
|
|
27
|
-
/** Default first-party webhook paths. Workspace glue may remap them, so guidance labels them defaults. */
|
|
28
|
-
export function dockerWebhookPaths(channels) {
|
|
29
|
-
const path = {
|
|
30
|
-
github: "/webhook",
|
|
31
|
-
telegram: "/telegram",
|
|
32
|
-
slack: "/slack",
|
|
33
|
-
feishu: "/feishu",
|
|
34
|
-
lark: "/lark",
|
|
35
|
-
};
|
|
36
|
-
return channels.map((kind) => path[kind]);
|
|
37
|
-
}
|
|
38
28
|
/** `${NAME:-}` without making JavaScript treat it as interpolation. */
|
|
39
29
|
function composeInterpolation(name) {
|
|
40
30
|
return `\${${name}:-}`;
|
|
41
31
|
}
|
|
42
32
|
function composeYaml(input) {
|
|
43
|
-
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets
|
|
33
|
+
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
44
34
|
// Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
|
|
45
35
|
// it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
|
|
46
36
|
const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
|
|
@@ -100,11 +90,10 @@ export function planDockerDeploy(input) {
|
|
|
100
90
|
const composePath = `${input.agentPrefix}${DOCKER_COMPOSE_FILE}`;
|
|
101
91
|
const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
|
|
102
92
|
const compose = `docker compose -f ${composePath}`;
|
|
103
|
-
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets
|
|
93
|
+
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
104
94
|
const required = secrets.filter((secret) => secret.required);
|
|
105
95
|
const optional = secrets.filter((secret) => !secret.required);
|
|
106
|
-
const
|
|
107
|
-
const paths = dockerWebhookPaths(routeChannels);
|
|
96
|
+
const paths = webhookPaths(input.channels);
|
|
108
97
|
const runbook = [
|
|
109
98
|
`# Run FastAgent in local Docker. ${composePath} / Dockerfile(.dockerignore) are generated above.`,
|
|
110
99
|
`# Existing artifacts are authoritative and kept; edit Dockerfile or ${composePath} freely.`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RegistrationOutcome } from "../../channels/registration.ts";
|
|
1
2
|
import type { CliRunner } from "../runner.ts";
|
|
2
3
|
export interface DockerRunPlan {
|
|
3
4
|
/** Compose file relative to the runner cwd (the workspace root). */
|
|
@@ -10,6 +11,10 @@ export interface DockerRunPlan {
|
|
|
10
11
|
missingSecrets: string[];
|
|
11
12
|
/** Neither an env-key credential nor a readable auth.json is available. */
|
|
12
13
|
needsModelCredential: boolean;
|
|
14
|
+
/** Register the deployment's webhooks against the tunnel URL, reporting what each registrar
|
|
15
|
+
* answered. REQUIRED, not optional: an absent one would delete this whole step in silence, which
|
|
16
|
+
* is the failure this driver's gate exists to end. A topology with no tunnel never calls it. */
|
|
17
|
+
announce: DockerAnnounce;
|
|
13
18
|
/** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
|
|
14
19
|
requireTunnel: boolean;
|
|
15
20
|
}
|
|
@@ -17,10 +22,21 @@ export type DockerRunOutcome = {
|
|
|
17
22
|
ok: true;
|
|
18
23
|
url?: string;
|
|
19
24
|
tunnelUrl?: string;
|
|
20
|
-
}
|
|
25
|
+
}
|
|
26
|
+
/** `url`/`tunnelUrl` travel with a gate too: Compose is up, so the operator still needs to know
|
|
27
|
+
* where it is and what to re-run. */
|
|
28
|
+
| {
|
|
21
29
|
ok: false;
|
|
22
30
|
gate: string;
|
|
31
|
+
url?: string;
|
|
32
|
+
tunnelUrl?: string;
|
|
23
33
|
};
|
|
34
|
+
/** Register the deployment's webhooks against its public URL, reporting what each registrar
|
|
35
|
+
* answered. Injected so the driver stays free of channel specifics — and so a test can fail one. */
|
|
36
|
+
type DockerAnnounce = (baseUrl: string) => Promise<{
|
|
37
|
+
kind: string;
|
|
38
|
+
outcome: RegistrationOutcome;
|
|
39
|
+
}[]>;
|
|
24
40
|
export type DockerHealthProbe = (healthUrl: string) => Promise<boolean>;
|
|
25
41
|
export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string, env: NodeJS.ProcessEnv) => Promise<string | undefined>;
|
|
26
42
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
@@ -38,3 +54,4 @@ export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile:
|
|
|
38
54
|
* service is still success (an operator-owned sidecar/reverse proxy may be its only ingress).
|
|
39
55
|
*/
|
|
40
56
|
export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, tunnelUrlProbe?: DockerTunnelUrlProbe): Promise<DockerRunOutcome>;
|
|
57
|
+
export {};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { waitForHealth } from "../../channels/wait-health.js";
|
|
7
7
|
import { parseTunnelUrl } from "../../tunnel.js";
|
|
8
|
+
import { registrationGate } from "../registration-gate.js";
|
|
8
9
|
import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
|
|
9
10
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
10
11
|
export function localUrlFromComposePort(stdout) {
|
|
@@ -122,5 +123,14 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
122
123
|
if (!tunnelUrl) {
|
|
123
124
|
return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
124
125
|
}
|
|
126
|
+
// Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer
|
|
127
|
+
// that owns the outcome, so it is the layer that can gate on one. While it sat above, docker was
|
|
128
|
+
// the one target whose `--run` could exit 0 with a webhook that never registered.
|
|
129
|
+
const reg = registrationGate(log, `re-run this deploy to retry registration (Compose is already up)`);
|
|
130
|
+
for (const { kind, outcome } of await plan.announce(tunnelUrl))
|
|
131
|
+
reg.track(kind, outcome);
|
|
132
|
+
const blocked = reg.gate();
|
|
133
|
+
if (blocked)
|
|
134
|
+
return { ok: false, gate: blocked, url, tunnelUrl };
|
|
125
135
|
return { ok: true, url, tunnelUrl };
|
|
126
136
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* snapshot is discarded replays on the next start (the Telegram L1 turn store) — at-least-once, the
|
|
17
17
|
* documented floor. State on the /data volume survives stop/suspend on the same machine.
|
|
18
18
|
*/
|
|
19
|
-
import type {
|
|
19
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
20
20
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
21
21
|
export interface FlyPlanInput extends ContainerInput {
|
|
22
22
|
/** Fly app name — globally unique, lowercase; the CLI sanitizes it from the dir basename. */
|
|
@@ -28,10 +28,9 @@ export interface FlyPlanInput extends ContainerInput {
|
|
|
28
28
|
* `"OAuth"`/`"stored credential"` (a local login the server can't use), or undefined (unconfigured).
|
|
29
29
|
*/
|
|
30
30
|
modelAuth: string | undefined;
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
longConnectionChannels?: string[];
|
|
31
|
+
/** Every declared channel and its ingress — the source of the secret list, the webhook steps, and
|
|
32
|
+
* whether a machine must stay up for an outbound connection. */
|
|
33
|
+
channels: readonly DeclaredChannel[];
|
|
35
34
|
/** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
|
|
36
35
|
extraSecrets?: string[];
|
|
37
36
|
/** `auto_stop_machines` — `"suspend"` (default, fast resume) or `"stop"` (cold start). CLI `--stop`. */
|