@fastagent-sh/fastagent 0.19.0 → 0.21.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 +5 -1
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- 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 +2 -2
- package/dist/channels/telegram/telegram.js +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- 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 +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- 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 +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Secrets go in one-per-`variable set --stdin` (value on stdin, never argv/process listing — Railway has
|
|
23
23
|
* no bulk stdin import like Fly's `secrets import`). Auth needs an ACCOUNT credential (login or
|
|
24
|
-
* `
|
|
24
|
+
* `RAILWAY_API_TOKEN`), not the project-scoped `RAILWAY_TOKEN`: `init` creates a project that a project
|
|
25
|
+
* token can't predate.
|
|
25
26
|
*/
|
|
26
|
-
import type
|
|
27
|
-
import type {
|
|
27
|
+
import { type Registrars } from "../channel-ingress.ts";
|
|
28
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
28
29
|
import type { CliRunner } from "../runner.ts";
|
|
29
30
|
export interface RailwayRunPlan {
|
|
30
31
|
/** Names both the project (`railway init --name`) and the service (`railway add --service`). Railway
|
|
@@ -38,8 +39,8 @@ export interface RailwayRunPlan {
|
|
|
38
39
|
secrets: Record<string, string>;
|
|
39
40
|
/** Required secret names with NO local value — the run gates on these before any side effect. */
|
|
40
41
|
missingSecrets: string[];
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
43
|
+
channels: readonly DeclaredChannel[];
|
|
43
44
|
/** Opt-in (CLI `--into-linked`) to provision INTO the project this directory is already linked to. Off
|
|
44
45
|
* by default so `--run` only creates on an unlinked dir and never deploys into a pre-existing (possibly
|
|
45
46
|
* unrelated/production) project; the flag is the operator's explicit "yes, this project". */
|
|
@@ -84,9 +85,9 @@ export declare function parseDomainUrl(stdout: string): string | undefined;
|
|
|
84
85
|
*/
|
|
85
86
|
export declare function parseHasVolume(stdout: string, mountPath: string): boolean;
|
|
86
87
|
/**
|
|
87
|
-
* Run the deploy through `railway`. `log` reports progress; the injected
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* Run the deploy through `railway`. `log` reports progress; the injected {@link Registrars} perform
|
|
89
|
+
* the post-deploy webhook steps from the builder machine (Slack's control credential never travels to
|
|
90
|
+
* the host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
91
|
+
* fail-visible.
|
|
91
92
|
*/
|
|
92
|
-
export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void,
|
|
93
|
+
export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void, registrars: Registrars): Promise<RailwayRunOutcome>;
|
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent deploy railway --run` — drive the Railway CLI to completion. The middle of the deploy the
|
|
3
|
+
* plain runbook hands to the operator; `--run` executes it so a coding agent runs ONE command.
|
|
4
|
+
*
|
|
5
|
+
* Railway's model forces differences from the Fly runner (fly/run.ts), all validated against CLI 5.15.0:
|
|
6
|
+
*
|
|
7
|
+
* - **`--run` PROVISIONS a project and only runs on an UNLINKED directory** — so it can never deploy
|
|
8
|
+
* into a project it didn't create. `railway init` isn't check-then-act (it ALWAYS makes a new project)
|
|
9
|
+
* and `railway status` exits 0 even when unlinked (linkedness is read from stdout: JSON when linked,
|
|
10
|
+
* empty when not). We do NOT track ownership: a pre-existing link is refused (Railway has no globally
|
|
11
|
+
* unique name to give free identity like Fly's app name, and synthesizing one — a machine-local marker
|
|
12
|
+
* — was a large, bug-prone premature optimization). The only "yes, this project" signal is the operator's
|
|
13
|
+
* explicit `--into-linked`, which provisions INTO the linked project; a routine redeploy is just
|
|
14
|
+
* `railway up`. The volume is check-then-act, so `--into-linked` (or a create that failed at volume-add) still gets it.
|
|
15
|
+
* - **Every command needs `--service` explicitly** to stay non-interactive (a bare command prompts to
|
|
16
|
+
* pick a service). `railway volume add` is the exception — it has NO `--service` flag and rides the
|
|
17
|
+
* linked service, so the service must be created (and thus linked) first.
|
|
18
|
+
* - **The public URL is minted, not deterministic** — bare `railway domain --json` returns the service
|
|
19
|
+
* domain (minting one if absent), read for the webhook. We avoid the newer `domain list` subcommand:
|
|
20
|
+
* on a CLI without it, `railway domain list` registers a bogus custom domain named "list" (destructive).
|
|
21
|
+
*
|
|
22
|
+
* Secrets go in one-per-`variable set --stdin` (value on stdin, never argv/process listing — Railway has
|
|
23
|
+
* no bulk stdin import like Fly's `secrets import`). Auth needs an ACCOUNT credential (login or
|
|
24
|
+
* `RAILWAY_API_TOKEN`), not the project-scoped `RAILWAY_TOKEN`: `init` creates a project that a project
|
|
25
|
+
* token can't predate.
|
|
26
|
+
*/
|
|
27
|
+
import { registerWebhooks } from "../channel-ingress.js";
|
|
2
28
|
/**
|
|
3
29
|
* Whether `railway status --json` shows a linked project: non-empty stdout. Unlinked prints its message
|
|
4
30
|
* to stderr and leaves stdout EMPTY (the exit code is 0 either way, so it can't be the signal). ANY
|
|
@@ -62,12 +88,12 @@ export function parseHasVolume(stdout, mountPath) {
|
|
|
62
88
|
return jsonStrings(stdout).includes(mountPath);
|
|
63
89
|
}
|
|
64
90
|
/**
|
|
65
|
-
* Run the deploy through `railway`. `log` reports progress; the injected
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
91
|
+
* Run the deploy through `railway`. `log` reports progress; the injected {@link Registrars} perform
|
|
92
|
+
* the post-deploy webhook steps from the builder machine (Slack's control credential never travels to
|
|
93
|
+
* the host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
94
|
+
* fail-visible.
|
|
69
95
|
*/
|
|
70
|
-
export async function deployRailwayRun(plan, railway, log,
|
|
96
|
+
export async function deployRailwayRun(plan, railway, log, registrars) {
|
|
71
97
|
const gate = (g) => ({ ok: false, gate: g });
|
|
72
98
|
// Every --service below targets plan.name — the name this tool gives BOTH the project and the service
|
|
73
99
|
// (`init --name` + `add --service`). On a fresh create they match; on `--into-linked` into a hand-made
|
|
@@ -75,10 +101,10 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram, reg
|
|
|
75
101
|
// and it's ordered before the volume (which has no --service), so the mismatch fails visibly with no
|
|
76
102
|
// side effect. (Pre-checking the name means walking status's nested multi-service shape — not worth it.)
|
|
77
103
|
const svc = ["--service", plan.name];
|
|
78
|
-
// 1. Auth needs an ACCOUNT credential (browser login or
|
|
79
|
-
// predate the project `init` creates. `whoami` succeeds with either.
|
|
104
|
+
// 1. Auth needs an ACCOUNT credential (browser login or RAILWAY_API_TOKEN) — the project-scoped
|
|
105
|
+
// RAILWAY_TOKEN can't predate the project `init` creates. `whoami` succeeds with either.
|
|
80
106
|
if ((await railway(["whoami"], { capture: true })).code !== 0) {
|
|
81
|
-
return gate("not logged in to Railway — run `railway login`, or set
|
|
107
|
+
return gate("not logged in to Railway — run `railway login`, or set RAILWAY_API_TOKEN (an account token), then re-run");
|
|
82
108
|
}
|
|
83
109
|
// 2. Gate missing required secret VALUES before any side effect (no half-created infra).
|
|
84
110
|
if (plan.missingSecrets.length > 0) {
|
|
@@ -173,40 +199,15 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram, reg
|
|
|
173
199
|
if (!url) {
|
|
174
200
|
return gate("couldn't read a domain from `railway domain` — run `railway domain` manually, then set any webhook");
|
|
175
201
|
}
|
|
176
|
-
// 7. Post-deploy webhook —
|
|
177
|
-
//
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
|
|
186
|
-
}
|
|
187
|
-
if (plan.channels.includes("slack")) {
|
|
188
|
-
if (registerSlack) {
|
|
189
|
-
log("registering slack event URL…");
|
|
190
|
-
reg.track("slack", await registerSlack(url));
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
log(`slack: set Event Subscriptions → Request URL → ${url}/slack`);
|
|
194
|
-
reg.track("slack", "manual");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
for (const kind of ["feishu", "lark"]) {
|
|
198
|
-
if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
|
|
199
|
-
continue;
|
|
200
|
-
if (registerFeishu) {
|
|
201
|
-
log(`registering ${kind} event URL…`);
|
|
202
|
-
reg.track(kind, await registerFeishu(url, kind));
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
log(`${kind}: set the event Request URL in the developer console (Events & Callbacks) → ${url}/${kind} (the service must be running when you save)`);
|
|
206
|
-
reg.track(kind, "manual"); // no registrar wired — the console step above is the operator's
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
const registrationGateMsg = reg.gate();
|
|
202
|
+
// 7. Post-deploy webhook — which channels, in what words, and the gate policy are all the shared
|
|
203
|
+
// kernel's; Railway contributes the minted URL and how to retry.
|
|
204
|
+
const registrationGateMsg = await registerWebhooks({
|
|
205
|
+
baseUrl: url,
|
|
206
|
+
channels: plan.channels,
|
|
207
|
+
registrars,
|
|
208
|
+
log,
|
|
209
|
+
retryHint: "re-run with --into-linked to retry registration",
|
|
210
|
+
});
|
|
210
211
|
if (registrationGateMsg)
|
|
211
212
|
return gate(registrationGateMsg);
|
|
212
213
|
return { ok: true, url };
|
package/dist/deploy/runner.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* binary and the command sequence differ. Tests inject a fake recorder; production spawns the real CLI.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { log } from "../log.js";
|
|
7
8
|
/**
|
|
8
9
|
* Production {@link CliRunner}: spawn `bin` in `cwd` (the workspace, so a build/upload context is the
|
|
9
10
|
* agent). stderr is always inherited to the terminal; stdout is inherited unless `capture`. A spawn
|
|
@@ -22,11 +23,30 @@ export function spawnRunner(bin, cwd) {
|
|
|
22
23
|
});
|
|
23
24
|
let out = "";
|
|
24
25
|
let err = "";
|
|
26
|
+
let stdinError;
|
|
25
27
|
child.stdout?.on("data", (d) => (out += String(d)));
|
|
26
28
|
child.stderr?.on("data", (d) => (err += String(d)));
|
|
27
|
-
if (opts?.input)
|
|
29
|
+
if (opts?.input) {
|
|
30
|
+
// A host CLI that rejects before reading (bad auth, a refused command) closes stdin under us:
|
|
31
|
+
// the write then emits `error` on a stream with no listener, which Node turns into an uncaught
|
|
32
|
+
// exception — crashing the deploy and losing the gate message the exit code was about to carry.
|
|
33
|
+
// HELD, not dropped: the exit code says it only when the CLI exits non-zero (see below).
|
|
34
|
+
child.stdin?.on("error", (error) => (stdinError ??= error));
|
|
28
35
|
child.stdin?.end(opts.input);
|
|
29
|
-
|
|
36
|
+
}
|
|
37
|
+
child.on("close", (code) => {
|
|
38
|
+
// A CLI that exits 0 having refused part of its stdin took TRUNCATED input — an auth seed runs
|
|
39
|
+
// well past the 64KB pipe buffer, so "success" here would deploy a half-written secret and say
|
|
40
|
+
// nothing. The exit code is what callers read, so the failure has to reach them as one.
|
|
41
|
+
const truncated = stdinError !== undefined && (code ?? 1) === 0;
|
|
42
|
+
if (truncated) {
|
|
43
|
+
// `error`, not `warn`: this line is the ONLY evidence of the failure — the caller's gate says
|
|
44
|
+
// "see the output above", and above it the host CLI printed success. A log level that mutes
|
|
45
|
+
// it (CI often sets `error`) leaves a stopped deploy with no diagnosable cause.
|
|
46
|
+
log.error(`[fastagent] ${bin} exited 0 without reading all of its input — ${stdinError?.message}`);
|
|
47
|
+
}
|
|
48
|
+
res({ code: truncated ? 1 : (code ?? 1), stdout: out, stderr: opts?.captureStderr ? err : undefined });
|
|
49
|
+
});
|
|
30
50
|
child.on("error", () => res({ code: 127, stdout: "", stderr: opts?.captureStderr ? "" : undefined })); // ENOENT
|
|
31
51
|
});
|
|
32
52
|
}
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* The secret set a deployed agent needs, computed from the definition — host-neutral. Required values
|
|
3
|
-
* gate every target; optional channel values travel only when configured. Only the SET command differs
|
|
4
|
-
* (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
|
|
5
|
-
*/
|
|
6
|
-
import { type ChannelKind } from "../scaffold/add-channel.ts";
|
|
1
|
+
import type { DeclaredChannel } from "../channels/discover.ts";
|
|
7
2
|
/**
|
|
8
3
|
* Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none?
|
|
9
4
|
* Positive match on the UPPER_SNAKE env-var naming shape, NOT a negative exclude of today's sentinel
|
|
@@ -17,7 +12,7 @@ export declare function isEnvKey(source: string | undefined): source is string;
|
|
|
17
12
|
* deployment prerequisites. An OAuth/stored login has no env key here — it carries as
|
|
18
13
|
* `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
|
|
19
14
|
*/
|
|
20
|
-
export declare function deploymentSecrets(modelAuth: string | undefined, channels:
|
|
15
|
+
export declare function deploymentSecrets(modelAuth: string | undefined, channels: readonly DeclaredChannel[], extraSecrets?: string[]): {
|
|
21
16
|
name: string;
|
|
22
17
|
hint: string;
|
|
23
18
|
required: boolean;
|
|
@@ -42,8 +37,7 @@ export declare function assembleSecrets(input: {
|
|
|
42
37
|
* no value to carry and no gate to raise — see {@link modelCredentialCarry}. */
|
|
43
38
|
modelKeyInDefinition?: boolean;
|
|
44
39
|
authFile: Buffer | undefined;
|
|
45
|
-
channels:
|
|
46
|
-
longConnectionChannels?: string[];
|
|
40
|
+
channels: readonly DeclaredChannel[];
|
|
47
41
|
/** Extra secret env-var names from `fastagent.config` deploy.secrets — carried like channel secrets. */
|
|
48
42
|
extraSecrets?: string[];
|
|
49
43
|
env: NodeJS.ProcessEnv;
|
|
@@ -52,3 +46,18 @@ export declare function assembleSecrets(input: {
|
|
|
52
46
|
missingSecrets: string[];
|
|
53
47
|
needsModelCredential: boolean;
|
|
54
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
|
|
51
|
+
* FASTAGENT_AUTH_SEED materialization (the read side of {@link assembleSecrets}'s carry). ABSENT-ONLY
|
|
52
|
+
* by design: a present file (a refreshed volume copy) is never overwritten by the stale seed, so a box
|
|
53
|
+
* that ran its own OAuth refresh is not rolled back.
|
|
54
|
+
*/
|
|
55
|
+
export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
|
|
58
|
+
* continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
|
|
59
|
+
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
60
|
+
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
61
|
+
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
62
|
+
*/
|
|
63
|
+
export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
|
package/dist/deploy/secrets.js
CHANGED
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
* The secret set a deployed agent needs, computed from the definition — host-neutral. Required values
|
|
3
3
|
* gate every target; optional channel values travel only when configured. Only the SET command differs
|
|
4
4
|
* (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
|
|
5
|
+
*
|
|
6
|
+
* Both DIRECTIONS of the credential carry live here: the deploy-time assembly below, and the boot-time
|
|
7
|
+
* seed read at the bottom. They were split across a host driver (`fly/run.ts`), which left `start`
|
|
8
|
+
* — a serving path that deploys nothing, on Fly or anywhere — importing from it to boot a container.
|
|
5
9
|
*/
|
|
6
|
-
import {
|
|
10
|
+
import { CONTROL_TOKEN_ENV } from "../channels/control.js";
|
|
11
|
+
import { CHANNEL_KINDS, channelSetup } from "../scaffold/add-channel.js";
|
|
12
|
+
/** The declared channels this tool has setup metadata for. A custom channel carries its own secrets;
|
|
13
|
+
* nothing here can name them, and guessing would print a runbook line no one can act on. */
|
|
14
|
+
function firstPartyChannels(channels) {
|
|
15
|
+
return channels.flatMap((channel) => CHANNEL_KINDS.includes(channel.name)
|
|
16
|
+
? [{ kind: channel.name, ingress: channel.ingress }]
|
|
17
|
+
: []);
|
|
18
|
+
}
|
|
7
19
|
/**
|
|
8
20
|
* Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none?
|
|
9
21
|
* Positive match on the UPPER_SNAKE env-var naming shape, NOT a negative exclude of today's sentinel
|
|
@@ -19,20 +31,30 @@ export function isEnvKey(source) {
|
|
|
19
31
|
* deployment prerequisites. An OAuth/stored login has no env key here — it carries as
|
|
20
32
|
* `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
|
|
21
33
|
*/
|
|
22
|
-
export function deploymentSecrets(modelAuth, channels, extraSecrets = []
|
|
34
|
+
export function deploymentSecrets(modelAuth, channels, extraSecrets = []) {
|
|
23
35
|
const secrets = [];
|
|
24
36
|
if (isEnvKey(modelAuth))
|
|
25
37
|
secrets.push({ name: modelAuth, hint: "your model provider key", required: true });
|
|
26
|
-
for (const kind of channels) {
|
|
27
|
-
const
|
|
28
|
-
for (const e of channelSetup(kind, setupMode).env) {
|
|
38
|
+
for (const { kind, ingress } of firstPartyChannels(channels)) {
|
|
39
|
+
for (const e of channelSetup(kind, ingress === "long-connection" ? "websocket" : "webhook").env) {
|
|
29
40
|
secrets.push({ name: e.name, hint: e.hint, required: e.required });
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
// Dedup: a name already covered by the model key / a channel secret must not appear twice in the runbook.
|
|
33
44
|
for (const name of extraSecrets) {
|
|
34
45
|
if (!secrets.some((s) => s.name === name)) {
|
|
35
|
-
|
|
46
|
+
const control = name === CONTROL_TOKEN_ENV;
|
|
47
|
+
secrets.push({
|
|
48
|
+
name,
|
|
49
|
+
hint: control
|
|
50
|
+
? "the /control/* bearer token — mint one (uuidgen) and give the same value to callers"
|
|
51
|
+
: "declared in fastagent.config deploy.secrets",
|
|
52
|
+
// OPTIONAL, unlike every other extra: unset, the box mints a per-boot token and still serves,
|
|
53
|
+
// and every host with a shell can read it back out of control.json. Gating would stop deploys
|
|
54
|
+
// that work today to enforce a convenience — the pre-flight warning is where that argument
|
|
55
|
+
// belongs.
|
|
56
|
+
required: !control,
|
|
57
|
+
});
|
|
36
58
|
}
|
|
37
59
|
}
|
|
38
60
|
return secrets;
|
|
@@ -74,9 +96,8 @@ export function assembleSecrets(input) {
|
|
|
74
96
|
else {
|
|
75
97
|
needsModelCredential = true; // no env key, no auth.json — `fastagent login` remediation
|
|
76
98
|
}
|
|
77
|
-
for (const kind of input.channels) {
|
|
78
|
-
const
|
|
79
|
-
for (const e of channelSetup(kind, setupMode).env) {
|
|
99
|
+
for (const { kind, ingress } of firstPartyChannels(input.channels)) {
|
|
100
|
+
for (const e of channelSetup(kind, ingress === "long-connection" ? "websocket" : "webhook").env) {
|
|
80
101
|
const v = input.env[e.name];
|
|
81
102
|
if (v)
|
|
82
103
|
secrets[e.name] = v; // optional channel values travel when configured
|
|
@@ -85,31 +106,45 @@ export function assembleSecrets(input) {
|
|
|
85
106
|
}
|
|
86
107
|
}
|
|
87
108
|
}
|
|
88
|
-
// Slack bot-token rotation is an all-or-nothing credential bundle. Its fields remain optional so a
|
|
89
|
-
// manually configured long-lived token works, but a partial bundle must gate before the container
|
|
90
|
-
// reaches slackChannel construction.
|
|
91
|
-
if (input.channels.includes("slack")) {
|
|
92
|
-
const rotation = [
|
|
93
|
-
"SLACK_BOT_REFRESH_TOKEN",
|
|
94
|
-
"SLACK_BOT_TOKEN_EXPIRES_AT",
|
|
95
|
-
"SLACK_CLIENT_ID",
|
|
96
|
-
"SLACK_CLIENT_SECRET",
|
|
97
|
-
];
|
|
98
|
-
if (rotation.some((name) => !!input.env[name])) {
|
|
99
|
-
for (const name of rotation) {
|
|
100
|
-
if (!input.env[name] && !missingSecrets.includes(name))
|
|
101
|
-
missingSecrets.push(name);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
109
|
for (const name of input.extraSecrets ?? []) {
|
|
106
110
|
if (name in secrets || missingSecrets.includes(name))
|
|
107
111
|
continue; // already covered by model/channel — no dup
|
|
108
112
|
const v = input.env[name];
|
|
109
113
|
if (v)
|
|
110
114
|
secrets[name] = v;
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
// The control token is CARRIED, never gated — see {@link deploymentSecrets}: unset, the box mints
|
|
116
|
+
// one and serves; every other extra is declared as needed, so its absence is a stop.
|
|
117
|
+
else if (name !== CONTROL_TOKEN_ENV)
|
|
118
|
+
missingSecrets.push(name);
|
|
113
119
|
}
|
|
114
120
|
return { secrets, missingSecrets, needsModelCredential };
|
|
115
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
|
|
124
|
+
* FASTAGENT_AUTH_SEED materialization (the read side of {@link assembleSecrets}'s carry). ABSENT-ONLY
|
|
125
|
+
* by design: a present file (a refreshed volume copy) is never overwritten by the stale seed, so a box
|
|
126
|
+
* that ran its own OAuth refresh is not rolled back.
|
|
127
|
+
*/
|
|
128
|
+
export function authSeedBytes(seed, fileExists) {
|
|
129
|
+
return !seed || fileExists ? undefined : Buffer.from(seed, "base64");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
|
|
133
|
+
* continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
|
|
134
|
+
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
135
|
+
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
136
|
+
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
137
|
+
*/
|
|
138
|
+
export function collectAuthSeed(env) {
|
|
139
|
+
const first = env.FASTAGENT_AUTH_SEED;
|
|
140
|
+
if (!first)
|
|
141
|
+
return undefined;
|
|
142
|
+
let seed = first;
|
|
143
|
+
for (let i = 2;; i++) {
|
|
144
|
+
const part = env[`FASTAGENT_AUTH_SEED_${i}`];
|
|
145
|
+
if (!part)
|
|
146
|
+
break;
|
|
147
|
+
seed += part;
|
|
148
|
+
}
|
|
149
|
+
return seed;
|
|
150
|
+
}
|
package/dist/dev-supervisor.js
CHANGED
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import { relative, sep } from "node:path";
|
|
15
15
|
import { watch as watchTree } from "chokidar";
|
|
16
|
-
import { AGENT_CONFIG_NAMES, AGENT_MODELS_FILE, resolveStateRoot } from "./paths.js";
|
|
17
|
-
import { isUnderDir } from "./paths.js";
|
|
16
|
+
import { AGENT_CONFIG_NAMES, AGENT_MODELS_FILE, resolveStateRoot, isUnderDir, } from "./paths.js";
|
|
18
17
|
import { dotEnvPath } from "./env.js";
|
|
19
18
|
import { log } from "./log.js";
|
|
20
19
|
import { installProxyFetch } from "./proxy.js";
|
|
21
20
|
import { openExternalUrl } from "./open-url.js";
|
|
21
|
+
import { declaredChannels } from "./channels/discover.js";
|
|
22
22
|
import { announceWebhooks, startCloudflareTunnel } from "./tunnel.js";
|
|
23
23
|
/** What the dev watcher restarts on (agent-dir-relative): the process-bound code inputs only. */
|
|
24
24
|
/**
|
|
@@ -108,9 +108,8 @@ export async function runDevSupervisor(placement, options = {}) {
|
|
|
108
108
|
void startCloudflareTunnel(m.port).then((t) => {
|
|
109
109
|
if (t) {
|
|
110
110
|
tunnel = t;
|
|
111
|
-
void announceWebhooks(placement.agentDir, t.url, {
|
|
111
|
+
void announceWebhooks(placement.agentDir, t.url, declaredChannels(m.routeChannels ?? []), {
|
|
112
112
|
openUrl: openExternalUrl,
|
|
113
|
-
routeChannels: m.routeChannels,
|
|
114
113
|
stateRoot: resolveStateRoot(placement.agentDir),
|
|
115
114
|
});
|
|
116
115
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type
|
|
3
|
-
import { type AgentSessionServices, type CreateAgentSessionServicesOptions, type ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { type AgentSessionServices, type CreateAgentSessionServicesOptions, type ModelRuntime, type SessionManager, createAgentSessionFromServices } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import type { PiAgentSessionFactory } from "./invoke-session.ts";
|
|
5
4
|
import type { PiSessionRecordStore } from "./session-store.ts";
|
|
6
5
|
import { type MountedTool } from "./tool.ts";
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { type AnyModel } from "./models.ts";
|
|
7
|
+
interface PiSessionDefinition {
|
|
8
|
+
systemPrompt?: string;
|
|
9
|
+
skills: Skill[];
|
|
10
|
+
}
|
|
9
11
|
export interface PiAgentSessionFactoryOptions {
|
|
10
12
|
/** Where conversations live. Continuity = same store + same session id. */
|
|
11
13
|
sessions: PiSessionRecordStore;
|
|
@@ -23,16 +25,8 @@ export interface PiAgentSessionFactoryOptions {
|
|
|
23
25
|
}>;
|
|
24
26
|
thinkingLevel?: ThinkingLevel;
|
|
25
27
|
tools?: MountedTool[];
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
skills?: Skill[];
|
|
29
|
-
/** Per-turn source for the prompt+skills PAIR; supersedes the two above. This is what keeps
|
|
30
|
-
* "the directory is the agent, LIVE" true on a shared `services`: the ResourceLoader is built
|
|
31
|
-
* once, but what it serves is re-read here. */
|
|
32
|
-
live?: () => Promise<{
|
|
33
|
-
systemPrompt?: string;
|
|
34
|
-
skills?: Skill[];
|
|
35
|
-
}>;
|
|
28
|
+
/** Read once per binding; prompt and skills come from the same definition read. */
|
|
29
|
+
readDefinition: () => PiSessionDefinition | Promise<PiSessionDefinition>;
|
|
36
30
|
/** The agent's working directory — what fastagent-defined tools see as `cwd`. */
|
|
37
31
|
cwd: string;
|
|
38
32
|
/**
|
|
@@ -66,9 +60,35 @@ export interface PiAgentSessionFactoryOptions {
|
|
|
66
60
|
extensionPaths?: string[];
|
|
67
61
|
/** Built-ins omitted by an explicit lower-level tool list. */
|
|
68
62
|
excludedToolNames?: readonly string[];
|
|
69
|
-
/** Filesystem/process environment: definition loading, and the turn context for tools that read one. */
|
|
70
|
-
env: ExecutionEnv;
|
|
71
63
|
}
|
|
64
|
+
export interface BindPiSessionOptions {
|
|
65
|
+
services: AgentSessionServices;
|
|
66
|
+
sessionManager: SessionManager;
|
|
67
|
+
/** Chat only: pi's runtime hands the resumed/new session its start event. */
|
|
68
|
+
sessionStartEvent?: Parameters<typeof createAgentSessionFromServices>[0]["sessionStartEvent"];
|
|
69
|
+
model: AnyModel;
|
|
70
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
71
|
+
tools: MountedTool[];
|
|
72
|
+
/** The agent's working directory — what fastagent-defined tools see as `cwd`. */
|
|
73
|
+
cwd: string;
|
|
74
|
+
/** Built-ins omitted by an explicit lower-level tool list. */
|
|
75
|
+
excludedToolNames?: readonly string[];
|
|
76
|
+
/** The CALLER's session id — what a tool asking which conversation it is in hears. Defaults to
|
|
77
|
+
* pi's own id, which is right where the caller has none (chat). */
|
|
78
|
+
sessionId?: string;
|
|
79
|
+
/** Record each discovered activation on the session, so the next bind restores it. Off for chat:
|
|
80
|
+
* pi's own session has nowhere to put one, so a resumed chat re-discovers. */
|
|
81
|
+
recordActivations: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Bind ONE pi session to a record: the definition's tools as pi definitions over one turn context,
|
|
85
|
+
* pi's own tool copies kept off, and deferral applied. The per-invoke factory and the resident chat
|
|
86
|
+
* runtime both bind here — they differ in what they hand in (a shared vs a per-session `services`,
|
|
87
|
+
* record-resolved vs configured settings) and in whether a discovered activation has a record to
|
|
88
|
+
* land in, and in nothing else. Two copies of this drifted once (the tool adapter and the deferral
|
|
89
|
+
* narrowing each existed twice, identical but for those parameters).
|
|
90
|
+
*/
|
|
91
|
+
export declare function bindPiSession(options: BindPiSessionOptions): ReturnType<typeof createAgentSessionFromServices>;
|
|
72
92
|
/**
|
|
73
93
|
* Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and
|
|
74
94
|
* carries on with the rest — sound for a TUI that shows them, silent for a server that never looks.
|