@fastagent-sh/fastagent 0.22.1 → 0.24.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/dist/agent.d.ts +10 -0
- package/dist/channels/agentcore-protocol.d.ts +2 -1
- package/dist/channels/agentcore-service.js +10 -18
- package/dist/channels/agentcore.js +6 -3
- package/dist/channels/control.js +23 -0
- package/dist/channels/kit/preview-kit.d.ts +6 -1
- package/dist/channels/kit/preview-kit.js +21 -4
- package/dist/channels/slack/config-api.d.ts +2 -2
- package/dist/channels/slack/config-api.js +2 -2
- package/dist/channels/slack/onboard.js +2 -2
- package/dist/channels/slack/preview.js +2 -1
- package/dist/channels/slack/register-webhook.js +2 -2
- package/dist/channels/slack/slack-api.js +4 -4
- package/dist/cli/add-feishu.js +4 -2
- package/dist/cli/auth-view.d.ts +15 -6
- package/dist/cli/auth-view.js +24 -8
- package/dist/cli/box-login.d.ts +33 -0
- package/dist/cli/box-login.js +162 -0
- package/dist/cli/commands/deploy/agentcore.js +48 -11
- package/dist/cli/commands/deploy/docker.js +24 -13
- package/dist/cli/commands/deploy/fly.js +30 -7
- package/dist/cli/commands/deploy/railway.js +21 -7
- package/dist/cli/commands/deploy/shared.d.ts +18 -17
- package/dist/cli/commands/deploy/shared.js +20 -21
- package/dist/cli/commands/destroy.js +2 -2
- package/dist/cli/commands/info.js +15 -12
- package/dist/cli/commands/invoke.js +2 -2
- package/dist/cli/commands/login.d.ts +7 -1
- package/dist/cli/commands/login.js +123 -10
- package/dist/cli/commands/logs.js +2 -2
- package/dist/cli/commands/models.d.ts +4 -1
- package/dist/cli/commands/models.js +37 -4
- package/dist/cli/commands/routine-run.js +2 -2
- package/dist/cli/commands/start.d.ts +0 -5
- package/dist/cli/commands/start.js +3 -13
- package/dist/cli/invoke-stream.js +14 -0
- package/dist/cli/kernel.d.ts +2 -0
- package/dist/cli/kernel.js +1 -1
- package/dist/cli/login-relay.d.ts +38 -0
- package/dist/cli/login-relay.js +121 -0
- package/dist/cli/program.js +46 -8
- package/dist/cli/shared.d.ts +6 -5
- package/dist/cli/shared.js +29 -36
- package/dist/deploy/agentcore/plan.d.ts +10 -11
- package/dist/deploy/agentcore/plan.js +8 -14
- package/dist/deploy/agentcore/run.d.ts +8 -1
- package/dist/deploy/agentcore/run.js +20 -14
- package/dist/deploy/agentcore/shell.d.ts +25 -0
- package/dist/deploy/agentcore/shell.js +202 -0
- package/dist/deploy/box-shell.d.ts +43 -0
- package/dist/deploy/box-shell.js +28 -0
- package/dist/deploy/build-context.js +16 -7
- package/dist/deploy/channel-ingress.d.ts +20 -1
- package/dist/deploy/channel-ingress.js +34 -7
- package/dist/deploy/container.d.ts +14 -0
- package/dist/deploy/container.js +45 -1
- package/dist/deploy/docker/plan.d.ts +7 -3
- package/dist/deploy/docker/plan.js +9 -14
- package/dist/deploy/docker/run.d.ts +14 -7
- package/dist/deploy/docker/run.js +58 -35
- package/dist/deploy/fly/plan.d.ts +6 -3
- package/dist/deploy/fly/plan.js +6 -8
- package/dist/deploy/fly/run.d.ts +8 -2
- package/dist/deploy/fly/run.js +15 -25
- package/dist/deploy/preflight.d.ts +9 -7
- package/dist/deploy/preflight.js +92 -20
- package/dist/deploy/railway/plan.d.ts +6 -3
- package/dist/deploy/railway/plan.js +7 -9
- package/dist/deploy/railway/run.d.ts +10 -6
- package/dist/deploy/railway/run.js +29 -13
- package/dist/deploy/runner.d.ts +16 -0
- package/dist/deploy/runner.js +31 -0
- package/dist/deploy/secrets.d.ts +4 -13
- package/dist/deploy/secrets.js +8 -27
- package/dist/dev-supervisor.js +7 -4
- package/dist/engines/pi/agent-models.d.ts +83 -0
- package/dist/engines/pi/agent-models.js +120 -0
- package/dist/engines/pi/agent-session-factory.d.ts +13 -13
- package/dist/engines/pi/agent-session-factory.js +132 -159
- package/dist/engines/pi/auth.d.ts +56 -3
- package/dist/engines/pi/auth.js +68 -109
- package/dist/engines/pi/config.d.ts +0 -25
- package/dist/engines/pi/config.js +3 -37
- package/dist/engines/pi/create.d.ts +34 -21
- package/dist/engines/pi/create.js +90 -57
- package/dist/engines/pi/entry-images.d.ts +14 -0
- package/dist/engines/pi/entry-images.js +23 -0
- package/dist/engines/pi/invoke-session.js +88 -20
- package/dist/engines/pi/locked-file.d.ts +9 -0
- package/dist/engines/pi/locked-file.js +102 -0
- package/dist/engines/pi/login.d.ts +27 -14
- package/dist/engines/pi/login.js +41 -18
- package/dist/engines/pi/machine.d.ts +23 -0
- package/dist/engines/pi/machine.js +24 -2
- package/dist/engines/pi/models.d.ts +120 -36
- package/dist/engines/pi/models.js +316 -66
- package/dist/engines/pi/open.d.ts +65 -20
- package/dist/engines/pi/open.js +88 -34
- package/dist/engines/pi/report.d.ts +3 -0
- package/dist/engines/pi/report.js +11 -0
- package/dist/engines/pi/service.d.ts +2 -2
- package/dist/engines/pi/service.js +1 -0
- package/dist/engines/pi/session-builder.d.ts +3 -2
- package/dist/engines/pi/session-builder.js +30 -76
- package/dist/engines/pi/session-control.d.ts +4 -4
- package/dist/engines/pi/session-control.js +86 -43
- package/dist/engines/pi/session-settings.d.ts +9 -0
- package/dist/engines/pi/session-settings.js +17 -0
- package/dist/engines/pi/session-store.d.ts +2 -2
- package/dist/engines/pi/tool-context.d.ts +3 -5
- package/dist/engines/pi/tool-context.js +4 -16
- package/dist/engines/pi/tool.d.ts +32 -21
- package/dist/engines/pi/tool.js +21 -25
- package/dist/engines/pi/turn-kit.d.ts +18 -9
- package/dist/engines/pi/turn-kit.js +115 -32
- package/dist/open-url.d.ts +15 -1
- package/dist/open-url.js +35 -3
- package/dist/paths.d.ts +4 -2
- package/dist/paths.js +6 -2
- package/dist/pi.d.ts +5 -3
- package/dist/pi.js +3 -2
- package/dist/service.d.ts +5 -8
- package/dist/session-remote.js +21 -0
- package/dist/session.d.ts +88 -15
- package/dist/session.js +5 -1
- package/package.json +6 -5
- package/dist/engines/pi/search-tools.d.ts +0 -4
- package/dist/engines/pi/search-tools.js +0 -109
package/dist/cli/shared.js
CHANGED
|
@@ -6,14 +6,15 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import { relative, resolve } from "node:path";
|
|
7
7
|
import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
|
|
8
8
|
import { buildModelPickerOptions } from "./models-view.js";
|
|
9
|
-
import {
|
|
10
|
-
import { isValidPort, listModels, loadConfig, providerOf, resolveAuthLayers, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
|
|
9
|
+
import { isValidPort, listModels, loadConfig, providerOf, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
|
|
11
10
|
import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
|
|
12
11
|
import { readMachine, withMachine } from "../engines/pi/machine.js";
|
|
13
|
-
import {
|
|
12
|
+
import { providerAuthStatuses } from "../engines/pi/models.js";
|
|
13
|
+
import { agentModels } from "../engines/pi/agent-models.js";
|
|
14
14
|
import { formatAuthReport } from "./auth-view.js";
|
|
15
15
|
import { CODING_TOOL_NAMES } from "../engines/pi/create.js";
|
|
16
|
-
import { reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
|
|
16
|
+
import { describeIndirectTools, reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
|
|
17
|
+
import { isDeployedWorkspace } from "../paths.js";
|
|
17
18
|
import { log } from "../log.js";
|
|
18
19
|
import { enterAgentEnv } from "../env.js";
|
|
19
20
|
import { openExternalUrl } from "../open-url.js";
|
|
@@ -23,7 +24,7 @@ import { failStartup, failUsage, placementOrExit } from "./fail.js";
|
|
|
23
24
|
export async function enterAgentCommand(dirArg, opts) {
|
|
24
25
|
const placement = placementOrExit(resolve(dirArg));
|
|
25
26
|
enterAgentEnv(placement.agentDir);
|
|
26
|
-
await resolveFirstRunModel(placement
|
|
27
|
+
await resolveFirstRunModel(placement, opts);
|
|
27
28
|
return placement;
|
|
28
29
|
}
|
|
29
30
|
/** The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). */
|
|
@@ -37,7 +38,7 @@ export async function reportAssembly(a, extras = {}) {
|
|
|
37
38
|
for (const [label, value] of extras.beforeModel ?? [])
|
|
38
39
|
reportLine(label, value);
|
|
39
40
|
reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
|
|
40
|
-
await reportAuth(a.
|
|
41
|
+
await reportAuth(a.models, a.modelSpec);
|
|
41
42
|
reportLine("context", a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
|
|
42
43
|
if (a.definition.persona)
|
|
43
44
|
reportLine("persona", "persona.md");
|
|
@@ -47,9 +48,8 @@ export async function reportAssembly(a, extras = {}) {
|
|
|
47
48
|
reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
|
|
48
49
|
if (a.toolNames.length > 0)
|
|
49
50
|
reportLine("tools", a.toolNames.join(", "));
|
|
50
|
-
if (a.
|
|
51
|
-
reportLine("
|
|
52
|
-
}
|
|
51
|
+
if (a.indirectTools.length > 0)
|
|
52
|
+
reportLine("indirect", describeIndirectTools(a.indirectTools));
|
|
53
53
|
reportToolCollisions(a.toolCollisions);
|
|
54
54
|
for (const [label, value] of extras.afterTools ?? [])
|
|
55
55
|
reportLine(label, value);
|
|
@@ -87,26 +87,18 @@ export function parseBind(value) {
|
|
|
87
87
|
return bindAddress(trimmed); // a name never travels past this point — see bind.ts
|
|
88
88
|
}
|
|
89
89
|
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. */
|
|
90
|
-
export async function reportAuth(
|
|
90
|
+
export async function reportAuth(models, modelSpec) {
|
|
91
91
|
const provider = providerOf(modelSpec);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
// Name the layer the credential actually came from: "which file do I edit" is the question this line answers, and
|
|
103
|
-
// a global credential lives in a file the agent dir does not contain. With NEITHER layer holding it, the answer is
|
|
104
|
-
// the primary — that is the file the `fastagent login` this report recommends writes.
|
|
105
|
-
const found = fallbackAuthPath !== undefined && inFallback ? fallbackAuthPath : authPath;
|
|
106
|
-
// Only when nothing satisfies auth does the stored credential matter: it tells "nothing stored" from "stored but
|
|
107
|
-
// unusable".
|
|
108
|
-
const stored = source === undefined ? (inPrimary ?? inFallback) : undefined;
|
|
109
|
-
const report = formatAuthReport(provider, found, source, stored);
|
|
92
|
+
// No files: the opener was handed a credential store, which is its caller's to describe.
|
|
93
|
+
if (models.auth === undefined) {
|
|
94
|
+
log.info(`[fastagent] auth: the caller's credential store (${provider})`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// The agent's own model environment, the one its turns run on and a deployed box's `login --if-missing` asks: the
|
|
98
|
+
// line cannot name a file, or a source, other than the ones the runtime uses.
|
|
99
|
+
const modelId = modelSpec.slice(provider.length + 1);
|
|
100
|
+
const status = await models.authStatus(provider, modelId).catch(failStartup);
|
|
101
|
+
const report = formatAuthReport({ provider, path: models.auth.path, ...status, deployed: isDeployedWorkspace() });
|
|
110
102
|
log.info(`[fastagent] ${report.line}`);
|
|
111
103
|
if (report.warn)
|
|
112
104
|
log.warn(`[fastagent] ${report.warn}`);
|
|
@@ -114,7 +106,7 @@ export async function reportAuth(agentDir, modelSpec, auth) {
|
|
|
114
106
|
/**
|
|
115
107
|
* First-run model resolution for every assembly command (dev/start/invoke/fire/chat/deploy): ONE funnel, no dead ends.
|
|
116
108
|
*/
|
|
117
|
-
async function resolveFirstRunModel(agentDir, options = {}) {
|
|
109
|
+
async function resolveFirstRunModel({ agentDir, workspace }, options = {}) {
|
|
118
110
|
const { config, path: configPath } = await loadConfig(agentDir).catch(failStartup);
|
|
119
111
|
if (resolveModelSpec(options.model, config))
|
|
120
112
|
return; // already set (flag > FASTAGENT_MODEL > config)
|
|
@@ -122,11 +114,11 @@ async function resolveFirstRunModel(agentDir, options = {}) {
|
|
|
122
114
|
return; // --no-input: never prompt (clig) — the opener raises the clear error
|
|
123
115
|
if (!isInteractive())
|
|
124
116
|
return; // CI/deploy: the opener throws the actionable missing-model error
|
|
125
|
-
const
|
|
117
|
+
const environment = agentModels(agentDir, {}, { cwd: workspace });
|
|
126
118
|
// The picker lists the AGENT's surface: built-ins plus whatever its models.json declares, so a self-hosted endpoint
|
|
127
119
|
// is pickable on first run instead of being invisible until hand-set.
|
|
128
|
-
const models = await
|
|
129
|
-
const chosen = await pickWithCredentials(models, auth.path, agentDir);
|
|
120
|
+
const models = await environment.runtime().catch(failStartup);
|
|
121
|
+
const chosen = await pickWithCredentials(models, environment.auth.path, agentDir);
|
|
130
122
|
if (chosen === undefined)
|
|
131
123
|
return; // cancelled (or auth probe failed): the caller raises its clear missing-model error
|
|
132
124
|
process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
|
|
@@ -197,10 +189,10 @@ export function terminalLoginIO() {
|
|
|
197
189
|
}
|
|
198
190
|
/** Best-effort persist the picked model so the next run does not prompt. */
|
|
199
191
|
async function persistModelChoice(agentDir, configPath, spec) {
|
|
200
|
-
const hint = () => console.error(
|
|
192
|
+
const hint = (why = "") => console.error(
|
|
201
193
|
// The pick lives in THIS process's environment only, so it serves this run and nothing that outlives it: a
|
|
202
194
|
// deployment resolves in the environment being deployed, where this machine's variables do not exist.
|
|
203
|
-
`[fastagent] picked ${spec} — set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
|
|
195
|
+
`[fastagent] picked ${spec} — set \`model: ${JSON.stringify(spec)}\` in your config to persist${why}`);
|
|
204
196
|
if (!configPath)
|
|
205
197
|
return hint();
|
|
206
198
|
try {
|
|
@@ -210,7 +202,8 @@ async function persistModelChoice(agentDir, configPath, spec) {
|
|
|
210
202
|
await writeFile(configPath, replaced);
|
|
211
203
|
console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(agentDir, configPath)}`);
|
|
212
204
|
}
|
|
213
|
-
catch {
|
|
214
|
-
|
|
205
|
+
catch (error) {
|
|
206
|
+
// The run goes on with the pick either way; the reason the file was not written is what the author needs to fix.
|
|
207
|
+
hint(` (could not write ${configPath}: ${error.message})`);
|
|
215
208
|
}
|
|
216
209
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
2
2
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
3
|
-
import {
|
|
3
|
+
import type { DeploymentSecret } from "../secrets.ts";
|
|
4
4
|
/** The one schedule fact the plan needs (from loadRoutines) — name + cron + tz. */
|
|
5
5
|
export interface ScheduleFact {
|
|
6
6
|
name: string;
|
|
@@ -10,8 +10,6 @@ export interface ScheduleFact {
|
|
|
10
10
|
export interface AgentcorePlanInput extends ContainerInput {
|
|
11
11
|
/** Base name (dir basename) — shapes the runtime name, stack name, ECR repo, session id. */
|
|
12
12
|
name: string;
|
|
13
|
-
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
14
|
-
modelAuth: string | undefined;
|
|
15
13
|
/**
|
|
16
14
|
* Every declared channel and its ingress — the source of the webhook steps, and whether the
|
|
17
15
|
* forwarder is needed at all (ANY webhook channel requires it, customs included).
|
|
@@ -30,6 +28,12 @@ export interface AgentcorePlanInput extends ContainerInput {
|
|
|
30
28
|
* {@link DEFAULT_IDLE_TIMEOUT_SECONDS}.
|
|
31
29
|
*/
|
|
32
30
|
idleTimeoutSeconds?: number;
|
|
31
|
+
/**
|
|
32
|
+
* The provider the runtime logs in to itself (`fastagent login --deployment`) after every deploy and every 14-day
|
|
33
|
+
* idle reset: its storage, and
|
|
34
|
+
* the login with it, resets each time.
|
|
35
|
+
*/
|
|
36
|
+
boxLogin?: string;
|
|
33
37
|
}
|
|
34
38
|
export interface AgentcorePlan {
|
|
35
39
|
/** template + forwarder + Dockerfile/.dockerignore — written by the CLI (kept unless --force). */
|
|
@@ -69,18 +73,13 @@ export declare function wakeAlarmPrefix(name: string): string;
|
|
|
69
73
|
/** Account-suffixed artifact bucket for S3's global namespace. */
|
|
70
74
|
export declare function deploymentBucketName(name: string, account: string): string;
|
|
71
75
|
/**
|
|
72
|
-
* AgentCore env values max 2048 chars — a
|
|
73
|
-
* `<NAME>` + `_2`… (collectChunked reassembles at boot)
|
|
74
|
-
* value file's variables.
|
|
76
|
+
* AgentCore env values max 2048 chars — a value file's variables easily exceed it, so the carrier is CHUNKED across
|
|
77
|
+
* `<NAME>` + `_2`… (collectChunked reassembles at boot).
|
|
75
78
|
*/
|
|
76
79
|
export declare const CARRIER_CHUNK_SIZE = 2000;
|
|
77
80
|
export declare const CARRIER_MAX_CHUNKS = 4;
|
|
78
|
-
/** The template's
|
|
81
|
+
/** The template's chunked carrier: CloudFormation parameter prefix → container env-var name. */
|
|
79
82
|
export declare const CARRIERS: readonly [{
|
|
80
|
-
readonly param: "FastagentAuthSeed";
|
|
81
|
-
readonly env: "FASTAGENT_AUTH_SEED";
|
|
82
|
-
readonly what: "base64 auth.json";
|
|
83
|
-
}, {
|
|
84
83
|
readonly param: "FastagentEnv";
|
|
85
84
|
readonly env: "FASTAGENT_ENV";
|
|
86
85
|
readonly what: "base64 JSON of the value file's variables";
|
|
@@ -5,7 +5,7 @@ import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
|
|
|
5
5
|
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
6
6
|
import { webhookKinds, webhookRunbook } from "../channel-ingress.js";
|
|
7
7
|
import { containerArtifacts } from "../container.js";
|
|
8
|
-
import {
|
|
8
|
+
import { deploymentLoginCommand } from "../box-shell.js";
|
|
9
9
|
function agentcoreTopology(input) {
|
|
10
10
|
return { webhooks: input.channels.some((channel) => channel.ingress === "webhook") };
|
|
11
11
|
}
|
|
@@ -33,15 +33,13 @@ export function deploymentBucketName(name, account) {
|
|
|
33
33
|
return `fa-${name}-${account}`;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* AgentCore env values max 2048 chars — a
|
|
37
|
-
* `<NAME>` + `_2`… (collectChunked reassembles at boot)
|
|
38
|
-
* value file's variables.
|
|
36
|
+
* AgentCore env values max 2048 chars — a value file's variables easily exceed it, so the carrier is CHUNKED across
|
|
37
|
+
* `<NAME>` + `_2`… (collectChunked reassembles at boot).
|
|
39
38
|
*/
|
|
40
39
|
export const CARRIER_CHUNK_SIZE = 2000;
|
|
41
40
|
export const CARRIER_MAX_CHUNKS = 4;
|
|
42
|
-
/** The template's
|
|
41
|
+
/** The template's chunked carrier: CloudFormation parameter prefix → container env-var name. */
|
|
43
42
|
export const CARRIERS = [
|
|
44
|
-
{ param: "FastagentAuthSeed", env: "FASTAGENT_AUTH_SEED", what: "base64 auth.json" },
|
|
45
43
|
{ param: "FastagentEnv", env: "FASTAGENT_ENV", what: "base64 JSON of the value file's variables" },
|
|
46
44
|
];
|
|
47
45
|
/** Chunk `i` (0-based) of a carrier: its parameter and env-var names. */
|
|
@@ -216,7 +214,7 @@ function template(input, translated, topology) {
|
|
|
216
214
|
` FASTAGENT_STATE_DIR: ${MOUNT}/.state`,
|
|
217
215
|
` FASTAGENT_SECRETS_DIR: ${SECRETS_DIR}`,
|
|
218
216
|
];
|
|
219
|
-
//
|
|
217
|
+
// The carrier is chunked (env values max 2048 chars — see CARRIER_CHUNK_SIZE): N parameters, each riding its
|
|
220
218
|
// own env var; `start` reassembles them. ONE parameter set for every variable, so the template does not change
|
|
221
219
|
// when the value file gains a name.
|
|
222
220
|
for (const carrier of CARRIERS) {
|
|
@@ -381,12 +379,8 @@ export function planAgentcoreDeploy(input) {
|
|
|
381
379
|
const wakeSecretHint = " FastagentWakeSecret=<any random string>";
|
|
382
380
|
runbook.push(`# FastagentWakeSecret: the wake-alarm shared secret — any random string (\`--run\` mints one)`);
|
|
383
381
|
runbook.push(`aws cloudformation deploy --stack-name ${stack} --template-file ${prefix}${TEMPLATE_FILE} \\`, ` --capabilities CAPABILITY_IAM \\`, ` --parameter-overrides ImageUri=${image} ForwarderBucket=${bucketHint} ForwarderS3Key=forwarder/<hash>.zip${secrets.length > 0 ? " FastagentEnv=<base64 JSON>" : ""}${wakeSecretHint}`, ``, `# 4. Read the outputs (the runtime ARN + callback URL; it serves webhooks only when configured):`, `aws cloudformation describe-stacks --stack-name ${stack} --query "Stacks[0].Outputs"`, ``, `# 5. Tail the Runtime's application stdout/stderr (same fastagent messages + log level as locally).`, `# Discovery resolves the per-endpoint log group from the stack's RuntimeArn:`, `fastagent logs agentcore --follow`, `# The ingress transport is a separate Lambda and therefore a separate log source:`, `fastagent logs agentcore --source forwarder --follow`, ``, `# 6. Set a log retention period on EVERY group this stack writes to. Each is created by the service that`, `# writes it, not by this template, and CloudWatch keeps log data indefinitely by default — which is`, `# forever-billed storage, and the logs are where WHY a scheduled turn failed is recorded. Logs are the`, `# ONE state path this host does not reclaim on its own.`, `aws logs put-retention-policy --log-group-name <the group \`fastagent logs agentcore\` resolves> --retention-in-days 14`, `# The forwarder is a separate Lambda, so its group is separate and defaults the same way:`, `aws logs put-retention-policy --log-group-name ${forwarderLogGroup(input.name)} --retention-in-days 14`);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (!isEnvKey(input.modelAuth)) {
|
|
387
|
-
runbook.push(``, input.modelAuth === undefined
|
|
388
|
-
? `# Model auth: none found at the local auth path — set FASTAGENT_AUTH_PATH, or \`--run\` carries it`
|
|
389
|
-
: `# 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.`);
|
|
382
|
+
if (input.boxLogin) {
|
|
383
|
+
runbook.push(``, `# Model auth: after EVERY deploy and after 14 idle days (each storage reset wipes the last login),`, `# log the runtime in to ${input.boxLogin} from this workspace — before pointing any webhook at it:`, `${deploymentLoginCommand("agentcore", input.boxLogin)}`);
|
|
390
384
|
}
|
|
391
385
|
// Post-deploy webhook registration — the shared channel-ingress steps, pointed at the forwarder's Function URL
|
|
392
386
|
// (read from the stack outputs).
|
|
@@ -403,6 +397,6 @@ export function planAgentcoreDeploy(input) {
|
|
|
403
397
|
}
|
|
404
398
|
runbook.push(``, `# Wake-ups: the agent's own follow-up turns (the wake tool) are EventBridge-backed — each pending wake-up is mirrored`, `# (via the forwarder, authenticated by FastagentWakeSecret) into a self-deleting one-shot`, `# schedule (fa-${name}-wk-*) that wakes the container at the right instant. Reliable even when`, `# the compute is reclaimed. The forwarder supplies the callback URL; use the deployment's`, `# fixed runtime session id for programmatic calls too.`);
|
|
405
399
|
runbook.push(``, `# Invoke the agent programmatically (reuse the deployment session; conversations remain separate):`, `aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}" \\`, ` --payload '{"kind":"invoke","session":"cli","text":"hello"}' --cli-binary-format raw-in-base64-out /dev/stdout`);
|
|
406
|
-
runbook.push(``, `# Stop the fixed runtime session after redeploy so the next call uses the new image.`, `# \`--run\` does this automatically. In-flight turns are interrupted.`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`, ``, `# Redeploy = \`fastagent deploy agentcore\` (a new definition needs a fresh release manifest) + step 1b`, `# (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is managed SessionStorage. It keeps base/ (including unfinished work), .state/`, `# and .secrets/ across compute stop/resume — an idle-reclaimed agent resumes with its memory. AWS`, `# RESETS it on every runtime version update (i.e. every deploy) and after 14 idle days, so a deploy`, `# replaces the state along with the image: sessions, channel state and pending wake-ups start blank
|
|
400
|
+
runbook.push(``, `# Stop the fixed runtime session after redeploy so the next call uses the new image.`, `# \`--run\` does this automatically. In-flight turns are interrupted.`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`, ``, `# Redeploy = \`fastagent deploy agentcore\` (a new definition needs a fresh release manifest) + step 1b`, `# (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is managed SessionStorage. It keeps base/ (including unfinished work), .state/`, `# and .secrets/ across compute stop/resume — an idle-reclaimed agent resumes with its memory. AWS`, `# RESETS it on every runtime version update (i.e. every deploy) and after 14 idle days, so a deploy`, `# replaces the state along with the image: sessions, channel state and pending wake-ups start blank.`, `# A provider API key in the value file travels with every deploy; a login on the runtime survives neither reset.`, `# Cross-deploy memory needs EFS or S3 Files, which are VPC-only (a NAT gateway for model/channel`, `# egress, ~$33/mo standing): use \`deploy fly\` or \`deploy railway\` for a real volume instead.`, `# Keep one runtime writer per workspace; use the fixed runtime session id printed above for every entry point.`);
|
|
407
401
|
return { artifacts, runbook, untranslatableSchedules: untranslatable, topology };
|
|
408
402
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. */
|
|
2
|
+
import type { BoxLoginStep } from "../box-shell.ts";
|
|
1
3
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
2
4
|
import { type Registrars } from "../channel-ingress.ts";
|
|
3
5
|
import type { CliRunner } from "../runner.ts";
|
|
@@ -16,7 +18,7 @@ export interface AgentcoreRunPlan {
|
|
|
16
18
|
* region`.
|
|
17
19
|
*/
|
|
18
20
|
region?: string;
|
|
19
|
-
/** Env-var name → value: the value file's carried variables
|
|
21
|
+
/** Env-var name → value: the value file's carried variables. */
|
|
20
22
|
secrets: Record<string, string>;
|
|
21
23
|
/** Declared names the value file supplies no value for — the run gates on these before any side effect. */
|
|
22
24
|
missingSecrets: string[];
|
|
@@ -29,6 +31,11 @@ export interface AgentcoreRunPlan {
|
|
|
29
31
|
* forwarder (and its artifact bucket parameters) exists.
|
|
30
32
|
*/
|
|
31
33
|
topology: AgentcoreTopology;
|
|
34
|
+
/**
|
|
35
|
+
* Log the runtime in (`fastagent login --deployment`) once the probe verified it and before any webhook points at
|
|
36
|
+
* it. Resolves a gate line, or undefined.
|
|
37
|
+
*/
|
|
38
|
+
boxLogin?: BoxLoginStep<[runtimeArn: string]>;
|
|
32
39
|
}
|
|
33
40
|
export type AgentcoreRunOutcome = {
|
|
34
41
|
ok: true;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
/** `fastagent deploy agentcore --run` — drive the AWS CLI + Docker to completion. */
|
|
2
1
|
import { RESERVED_PATHS } from "../../channels/agentcore-protocol.js";
|
|
3
|
-
import { registerWebhooks } from "../channel-ingress.js";
|
|
2
|
+
import { loginGate, registerWebhooks } from "../channel-ingress.js";
|
|
4
3
|
import { awsCli, awsJson } from "./aws-cli.js";
|
|
5
4
|
import { createHash } from "node:crypto";
|
|
6
5
|
import { Buffer } from "node:buffer";
|
|
@@ -97,11 +96,10 @@ const MINTED_PARAMS = {
|
|
|
97
96
|
FASTAGENT_INGRESS_SECRET: "FastagentIngressSecret",
|
|
98
97
|
FASTAGENT_WAKE_SECRET: "FastagentWakeSecret",
|
|
99
98
|
};
|
|
100
|
-
/** What each carrier holds for this deploy:
|
|
99
|
+
/** What each carrier holds for this deploy: every variable but the minted ones, as one encoded object. */
|
|
101
100
|
function carrierValues(secrets) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
return { FASTAGENT_AUTH_SEED: seed, FASTAGENT_ENV: encodeCarriedEnv(carried) };
|
|
101
|
+
const carried = Object.fromEntries(Object.entries(secrets).filter(([name]) => !(name in MINTED_PARAMS)));
|
|
102
|
+
return { FASTAGENT_ENV: encodeCarriedEnv(carried) };
|
|
105
103
|
}
|
|
106
104
|
/** The `--parameter-overrides file://` payload: a JSON array of "Key=Value" strings. */
|
|
107
105
|
export function paramsFileContent(imageUri, secrets, forwarder) {
|
|
@@ -170,10 +168,6 @@ probe = {}) {
|
|
|
170
168
|
// 3b.
|
|
171
169
|
const capacity = CARRIER_CHUNK_SIZE * CARRIER_MAX_CHUNKS;
|
|
172
170
|
const carried = carrierValues(plan.secrets);
|
|
173
|
-
if (carried.FASTAGENT_AUTH_SEED.length > capacity) {
|
|
174
|
-
return gate(`your auth.json is too large to carry (${carried.FASTAGENT_AUTH_SEED.length} chars base64 > ${capacity}) — ` +
|
|
175
|
-
`slim it (keep only the model's credential), or set a provider API key in .env instead`);
|
|
176
|
-
}
|
|
177
171
|
if (carried.FASTAGENT_ENV.length > capacity) {
|
|
178
172
|
return gate(`the variables in ${plan.valueFile} are too large to carry (${carried.FASTAGENT_ENV.length} chars encoded > ` +
|
|
179
173
|
`${capacity}) — move what the deployment does not need out of that file`);
|
|
@@ -224,6 +218,8 @@ probe = {}) {
|
|
|
224
218
|
], (stdout) => stdout.trim() || undefined);
|
|
225
219
|
const stackStatus = await readStackStatus();
|
|
226
220
|
const status = "ok" in stackStatus ? stackStatus.ok : "";
|
|
221
|
+
// A stack that held state: an earlier deploy served from it, and may have registered webhooks at its forwarder.
|
|
222
|
+
const redeploy = status !== "" && !EMPTY_STACK_STATUSES.has(status);
|
|
227
223
|
// The only answers the build can invalidate: one still in flight (a first create rolling back is exactly what step 7
|
|
228
224
|
// exists for, and minutes of arm64 build are long enough for it to settle), and one we never got.
|
|
229
225
|
const settling = status.endsWith("_IN_PROGRESS") || "unreadable" in stackStatus;
|
|
@@ -233,11 +229,9 @@ probe = {}) {
|
|
|
233
229
|
log(`warn: could not read stack ${stack} (${stackStatus.unreadable}) — if it exists, this deploy resets its ` +
|
|
234
230
|
`managed SessionStorage (${MOUNT}) and a failed first create will not be cleared`);
|
|
235
231
|
}
|
|
236
|
-
else if (
|
|
232
|
+
else if (redeploy) {
|
|
237
233
|
log(`warn: this is a REDEPLOY and AWS resets managed SessionStorage (${MOUNT}) on every runtime version update — ` +
|
|
238
|
-
`sessions, channel state and pending wake-ups start blank` +
|
|
239
|
-
// Only the carried auth.json is re-seeded; a provider API key deployment has no such step to blame.
|
|
240
|
-
`${carried.FASTAGENT_AUTH_SEED ? ", and the model credential is re-seeded from FASTAGENT_AUTH_SEED" : ""}. ` +
|
|
234
|
+
`sessions, channel state and pending wake-ups start blank. ` +
|
|
241
235
|
`Cross-deploy memory needs a real volume: \`deploy fly\` or \`deploy railway\`.`);
|
|
242
236
|
}
|
|
243
237
|
// 4.
|
|
@@ -394,6 +388,18 @@ probe = {}) {
|
|
|
394
388
|
return gate(verdict.gate);
|
|
395
389
|
log("runtime verified (workspace ready, channels constructed)");
|
|
396
390
|
}
|
|
391
|
+
const notLoggedIn = await plan.boxLogin?.run(runtimeArn);
|
|
392
|
+
if (notLoggedIn) {
|
|
393
|
+
return gate(loginGate({
|
|
394
|
+
notLoggedIn,
|
|
395
|
+
channels: plan.channels,
|
|
396
|
+
log,
|
|
397
|
+
// The forwarder URL outlives a redeploy, so the webhooks an earlier deploy registered already point at it.
|
|
398
|
+
...(redeploy
|
|
399
|
+
? { afterLogin: "they work again: the forwarder URL is unchanged, so nothing needs pointing" }
|
|
400
|
+
: { baseUrl: url, afterLogin: "point the channels by hand (a redeploy would wipe that login again)" }),
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
397
403
|
// 9.
|
|
398
404
|
const registrationGateMsg = url
|
|
399
405
|
? await registerWebhooks({
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BoxChannel, BoxShell } from "../box-shell.ts";
|
|
2
|
+
import type { CliRunner } from "../runner.ts";
|
|
3
|
+
export interface AwsCredentials {
|
|
4
|
+
accessKeyId: string;
|
|
5
|
+
secretAccessKey: string;
|
|
6
|
+
sessionToken?: string;
|
|
7
|
+
}
|
|
8
|
+
/** A presigned `wss://` URL for one shell on one runtime session: the SDK's `connectShellPresigned`. */
|
|
9
|
+
export declare function presignShellUrl(input: {
|
|
10
|
+
runtimeArn: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
shellId: string;
|
|
13
|
+
credentials: AwsCredentials;
|
|
14
|
+
now?: Date;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
/** What a status frame says: `undefined` for the connection confirmation, else how the shell exited. */
|
|
17
|
+
export declare function shellExit(status: string): string | undefined;
|
|
18
|
+
/** Open `command` in a new shell on `url`: stdin/stdout as streams, the other channels folded in. */
|
|
19
|
+
export declare function openShellChannel(url: string, command: string): BoxChannel;
|
|
20
|
+
/**
|
|
21
|
+
* The shell `login --deployment` opens on an AgentCore runtime, on the session its server runs in. `wake` first sends
|
|
22
|
+
* that session an IAM `probe`: the runtime prepares its workspace only on an invocation, and after a deploy or an idle
|
|
23
|
+
* reset nothing may have invoked it yet, which would leave the login no agent directory to run in.
|
|
24
|
+
*/
|
|
25
|
+
export declare function agentcoreShell(runtimeArn: string, sessionId: string, aws: CliRunner): BoxShell;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shell into the AgentCore runtime, for `fastagent login --deployment agentcore`: `InvokeAgentRuntimeCommandShell`,
|
|
3
|
+
* a WebSocket to an interactive shell inside the running session. Opened on the fixed ingress session, so the shell
|
|
4
|
+
* sees the same `/mnt/data` the server does.
|
|
5
|
+
*
|
|
6
|
+
* Authenticated with the same AWS credentials `deploy agentcore` uses, read from the AWS CLI
|
|
7
|
+
* (`aws configure export-credentials`), so profiles, SSO and `aws login` all work with nothing else installed. The
|
|
8
|
+
* URL is presigned (SigV4 in the query string, the SDK's `connectShellPresigned`), which needs no custom upgrade
|
|
9
|
+
* header.
|
|
10
|
+
*
|
|
11
|
+
* The wire (the TypeScript SDK's `shell/protocol.ts`, the Kubernetes v5 channel protocol): every binary frame is one
|
|
12
|
+
* channel byte then payload — 0 stdin, 1 stdout, 2 stderr, 3 status JSON, 5 heartbeat, 0xFF close. The shell is a
|
|
13
|
+
* terminal, so the command switches it to raw, no echo (answers are not echoed back, and a long line is not cut by
|
|
14
|
+
* the terminal's line editor) before handing it to `exec`.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
17
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { PassThrough, Writable } from "node:stream";
|
|
21
|
+
import { SignatureV4 } from "@smithy/signature-v4";
|
|
22
|
+
import { WebSocket, ping } from "undici";
|
|
23
|
+
import { awsCli, awsJson } from "./aws-cli.js";
|
|
24
|
+
import { MOUNT } from "./plan.js";
|
|
25
|
+
const STDIN = 0x00;
|
|
26
|
+
const STDOUT = 0x01;
|
|
27
|
+
const STDERR = 0x02;
|
|
28
|
+
const STATUS = 0x03;
|
|
29
|
+
const HEARTBEAT = 0x05;
|
|
30
|
+
/** The service closes a connection whose frame is larger (code 1009). */
|
|
31
|
+
const MAX_PAYLOAD = 64 * 1024 - 1;
|
|
32
|
+
/** The SDK's interval; a connection with no ping for ~60s is dropped by the service. */
|
|
33
|
+
const KEEPALIVE_MS = 30_000;
|
|
34
|
+
/** The longest a presigned shell URL may live (the SDK refuses more). */
|
|
35
|
+
const PRESIGN_SECONDS = 300;
|
|
36
|
+
/** The AWS CLI's own credentials, the same ones every `deploy agentcore` call runs with. */
|
|
37
|
+
async function awsCliCredentials(aws) {
|
|
38
|
+
const read = await awsCli(aws).read(["configure", "export-credentials", "--format", "process"], awsJson((parsed) => {
|
|
39
|
+
const p = parsed;
|
|
40
|
+
if (typeof p?.AccessKeyId !== "string" || typeof p.SecretAccessKey !== "string")
|
|
41
|
+
return undefined;
|
|
42
|
+
return {
|
|
43
|
+
accessKeyId: p.AccessKeyId,
|
|
44
|
+
secretAccessKey: p.SecretAccessKey,
|
|
45
|
+
...(typeof p.SessionToken === "string" ? { sessionToken: p.SessionToken } : {}),
|
|
46
|
+
};
|
|
47
|
+
}));
|
|
48
|
+
if ("ok" in read)
|
|
49
|
+
return read.ok;
|
|
50
|
+
throw new Error(`could not read AWS credentials from the AWS CLI (${"absent" in read ? "none configured" : read.unreadable})`);
|
|
51
|
+
}
|
|
52
|
+
/** SHA-256 / HMAC-SHA-256 over node:crypto, in the shape SigV4's signer takes. */
|
|
53
|
+
class Sha256 {
|
|
54
|
+
hash;
|
|
55
|
+
constructor(secret) {
|
|
56
|
+
const key = secret instanceof ArrayBuffer ? Buffer.from(secret) : secret;
|
|
57
|
+
this.hash = key === undefined ? createHash("sha256") : createHmac("sha256", key);
|
|
58
|
+
}
|
|
59
|
+
update(data) {
|
|
60
|
+
this.hash.update(data instanceof ArrayBuffer ? Buffer.from(data) : data);
|
|
61
|
+
}
|
|
62
|
+
async digest() {
|
|
63
|
+
return new Uint8Array(this.hash.digest());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The runtime's region, from its ARN (`arn:<partition>:bedrock-agentcore:<region>:<account>:runtime/<id>`). */
|
|
67
|
+
function regionOf(runtimeArn) {
|
|
68
|
+
const region = runtimeArn.split(":")[3];
|
|
69
|
+
if (!region)
|
|
70
|
+
throw new Error(`not an AgentCore runtime ARN: ${runtimeArn}`);
|
|
71
|
+
return region;
|
|
72
|
+
}
|
|
73
|
+
/** A presigned `wss://` URL for one shell on one runtime session: the SDK's `connectShellPresigned`. */
|
|
74
|
+
export async function presignShellUrl(input) {
|
|
75
|
+
const region = regionOf(input.runtimeArn);
|
|
76
|
+
const hostname = `bedrock-agentcore.${region}.amazonaws.com${region.startsWith("cn-") ? ".cn" : ""}`;
|
|
77
|
+
const url = new URL(`https://${hostname}/runtimes/${encodeURIComponent(input.runtimeArn)}/ws/shells`);
|
|
78
|
+
url.searchParams.set("shellId", input.shellId);
|
|
79
|
+
url.searchParams.set("X-Amzn-Bedrock-AgentCore-Runtime-Session-Id", input.sessionId);
|
|
80
|
+
const signed = await new SignatureV4({
|
|
81
|
+
credentials: input.credentials,
|
|
82
|
+
region,
|
|
83
|
+
service: "bedrock-agentcore",
|
|
84
|
+
sha256: Sha256,
|
|
85
|
+
}).presign({
|
|
86
|
+
method: "GET",
|
|
87
|
+
protocol: "https:",
|
|
88
|
+
hostname,
|
|
89
|
+
path: url.pathname,
|
|
90
|
+
query: Object.fromEntries(url.searchParams),
|
|
91
|
+
headers: { host: hostname },
|
|
92
|
+
}, { expiresIn: PRESIGN_SECONDS, ...(input.now ? { signingDate: input.now } : {}) });
|
|
93
|
+
const query = Object.entries(signed.query ?? {})
|
|
94
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`)
|
|
95
|
+
.join("&");
|
|
96
|
+
return `wss://${hostname}${signed.path}?${query}`;
|
|
97
|
+
}
|
|
98
|
+
/** What a status frame says: `undefined` for the connection confirmation, else how the shell exited. */
|
|
99
|
+
export function shellExit(status) {
|
|
100
|
+
const s = JSON.parse(status);
|
|
101
|
+
if (s.metadata?.shellId)
|
|
102
|
+
return undefined;
|
|
103
|
+
return s.status === "Success" ? "exit 0" : `exit ${s.details?.causes?.[0]?.message ?? "(unknown)"}`;
|
|
104
|
+
}
|
|
105
|
+
/** Open `command` in a new shell on `url`: stdin/stdout as streams, the other channels folded in. */
|
|
106
|
+
export function openShellChannel(url, command) {
|
|
107
|
+
const output = new PassThrough();
|
|
108
|
+
const ws = new WebSocket(url);
|
|
109
|
+
ws.binaryType = "arraybuffer";
|
|
110
|
+
const frame = (channel, payload = Buffer.alloc(0)) => ws.send(Buffer.concat([Buffer.from([channel]), payload]));
|
|
111
|
+
const stdin = (bytes) => {
|
|
112
|
+
for (let at = 0; at < bytes.length; at += MAX_PAYLOAD)
|
|
113
|
+
frame(STDIN, bytes.subarray(at, at + MAX_PAYLOAD));
|
|
114
|
+
};
|
|
115
|
+
let exit;
|
|
116
|
+
let failure;
|
|
117
|
+
let keepalive;
|
|
118
|
+
const opened = new Promise((resolve) => {
|
|
119
|
+
ws.addEventListener("open", () => resolve(true), { once: true });
|
|
120
|
+
ws.addEventListener("close", () => resolve(false), { once: true });
|
|
121
|
+
});
|
|
122
|
+
ws.addEventListener("open", () => {
|
|
123
|
+
stdin(Buffer.from(`stty raw -echo 2>/dev/null; exec sh -c '${command}'\n`));
|
|
124
|
+
keepalive = setInterval(() => {
|
|
125
|
+
ping(ws);
|
|
126
|
+
frame(HEARTBEAT);
|
|
127
|
+
}, KEEPALIVE_MS);
|
|
128
|
+
});
|
|
129
|
+
ws.addEventListener("message", (event) => {
|
|
130
|
+
const data = Buffer.from(event.data);
|
|
131
|
+
const payload = data.subarray(1);
|
|
132
|
+
if (data[0] === STDOUT)
|
|
133
|
+
output.write(payload);
|
|
134
|
+
else if (data[0] === STDERR)
|
|
135
|
+
process.stderr.write(payload);
|
|
136
|
+
else if (data[0] === STATUS)
|
|
137
|
+
exit = shellExit(payload.toString("utf8")) ?? exit;
|
|
138
|
+
});
|
|
139
|
+
ws.addEventListener("error", (event) => {
|
|
140
|
+
failure = event.message || "connection failed";
|
|
141
|
+
});
|
|
142
|
+
const closed = new Promise((resolve) => {
|
|
143
|
+
ws.addEventListener("close", async (event) => {
|
|
144
|
+
clearInterval(keepalive);
|
|
145
|
+
output.end();
|
|
146
|
+
if (!(await opened)) {
|
|
147
|
+
resolve({
|
|
148
|
+
opened: false,
|
|
149
|
+
error: `could not open a shell on the AgentCore runtime (${failure ?? `closed ${event.code}`}) — it needs ` +
|
|
150
|
+
`bedrock-agentcore:InvokeAgentRuntimeCommandShell, and a runtime created or redeployed after 2026-06-05`,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
resolve({ opened: true, how: exit ?? `WebSocket closed ${event.code}${event.reason ? ` ${event.reason}` : ""}` });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
const input = new Writable({
|
|
158
|
+
write(chunk, _encoding, done) {
|
|
159
|
+
void opened.then((open) => {
|
|
160
|
+
if (open && ws.readyState === WebSocket.OPEN)
|
|
161
|
+
stdin(chunk);
|
|
162
|
+
done();
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
return { output, input, closed };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The shell `login --deployment` opens on an AgentCore runtime, on the session its server runs in. `wake` first sends
|
|
170
|
+
* that session an IAM `probe`: the runtime prepares its workspace only on an invocation, and after a deploy or an idle
|
|
171
|
+
* reset nothing may have invoked it yet, which would leave the login no agent directory to run in.
|
|
172
|
+
*/
|
|
173
|
+
export function agentcoreShell(runtimeArn, sessionId, aws) {
|
|
174
|
+
return {
|
|
175
|
+
storage: MOUNT,
|
|
176
|
+
async wake() {
|
|
177
|
+
const dir = await mkdtemp(join(tmpdir(), "fastagent-agentcore-wake-"));
|
|
178
|
+
try {
|
|
179
|
+
const reply = join(dir, "reply.json");
|
|
180
|
+
const args = ["bedrock-agentcore", "invoke-agent-runtime", "--agent-runtime-arn", runtimeArn];
|
|
181
|
+
args.push("--runtime-session-id", sessionId, "--payload", '{"kind":"probe"}');
|
|
182
|
+
args.push("--cli-binary-format", "raw-in-base64-out", reply);
|
|
183
|
+
const sent = await awsCli(aws).present(args);
|
|
184
|
+
if (!("ok" in sent)) {
|
|
185
|
+
const why = "absent" in sent ? "the runtime is not there" : sent.unreadable;
|
|
186
|
+
throw new Error(`could not open the AgentCore runtime's workspace (${why})`);
|
|
187
|
+
}
|
|
188
|
+
const answer = await readFile(reply, "utf8");
|
|
189
|
+
if (!/"ok"\s*:\s*true/.test(answer)) {
|
|
190
|
+
throw new Error(`the AgentCore runtime did not open its workspace: ${answer.trim().slice(0, 300)}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await rm(dir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
async open(command) {
|
|
198
|
+
const credentials = await awsCliCredentials(aws);
|
|
199
|
+
return openShellChannel(await presignShellUrl({ runtimeArn, sessionId, shellId: randomUUID(), credentials }), command);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
/** One open session on the box, running one command. */
|
|
3
|
+
export interface BoxChannel {
|
|
4
|
+
/** What the command writes to its stdout. Ends when the session does. */
|
|
5
|
+
output: Readable;
|
|
6
|
+
/** The command's stdin. */
|
|
7
|
+
input: Writable;
|
|
8
|
+
/** How the session ended, or why it never opened. */
|
|
9
|
+
closed: Promise<{
|
|
10
|
+
opened: true;
|
|
11
|
+
how: string;
|
|
12
|
+
} | {
|
|
13
|
+
opened: false;
|
|
14
|
+
error: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface BoxShell {
|
|
18
|
+
/** Run `command` (one POSIX shell line) on the box. Its stderr goes to this process's stderr. */
|
|
19
|
+
open(command: string): Promise<BoxChannel>;
|
|
20
|
+
/** Make sure there is a running machine to open the shell on (a host that suspends idle machines). */
|
|
21
|
+
wake?(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* The box's storage root, for a shell that does not carry the server's environment (AgentCore's). Unset: the
|
|
24
|
+
* shell sees `FASTAGENT_STORAGE_DIR` as the server does, or the image's default.
|
|
25
|
+
*/
|
|
26
|
+
storage?: string;
|
|
27
|
+
}
|
|
28
|
+
/** A host CLI that runs a command on the box itself: `bin ...args(command)`, run from `cwd`. */
|
|
29
|
+
export declare function processShell(bin: string, args: (command: string) => string[], cwd: string): BoxShell;
|
|
30
|
+
/**
|
|
31
|
+
* The command that logs a deployment in, as every message and runbook names it: with the provider when it is known,
|
|
32
|
+
* because the model needs THAT one, and a menu invites another.
|
|
33
|
+
*/
|
|
34
|
+
export declare function deploymentLoginCommand(host: string, provider?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* The login a host driver runs on the box it just deployed, before any webhook points at it. `run` resolves a gate line,
|
|
37
|
+
* or undefined once the box authenticates; `command` is how a person runs the same login, for the manual route.
|
|
38
|
+
* ONE value, so a driver cannot have the step without its command.
|
|
39
|
+
*/
|
|
40
|
+
export interface BoxLoginStep<Args extends unknown[] = []> {
|
|
41
|
+
command: string;
|
|
42
|
+
run: (...args: Args) => Promise<string | undefined>;
|
|
43
|
+
}
|