@fastagent-sh/fastagent 0.22.0 → 0.23.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/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/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/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 +9 -6
- 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/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 +4 -4
- package/dist/cli/shared.js +24 -30
- 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 +74 -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 +5 -2
- package/dist/engines/pi/agent-models.d.ts +70 -0
- package/dist/engines/pi/agent-models.js +93 -0
- package/dist/engines/pi/auth.d.ts +56 -3
- package/dist/engines/pi/auth.js +63 -16
- package/dist/engines/pi/config.d.ts +0 -25
- package/dist/engines/pi/config.js +3 -37
- package/dist/engines/pi/create.d.ts +22 -16
- package/dist/engines/pi/create.js +28 -28
- 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 +26 -0
- package/dist/engines/pi/login.d.ts +20 -13
- package/dist/engines/pi/login.js +19 -12
- package/dist/engines/pi/models.d.ts +115 -36
- package/dist/engines/pi/models.js +218 -48
- package/dist/engines/pi/open.d.ts +42 -11
- package/dist/engines/pi/open.js +53 -21
- 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 +28 -58
- package/dist/engines/pi/session-control.js +14 -3
- package/dist/engines/pi/turn-kit.js +11 -5
- 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 +39 -6
- package/package.json +2 -1
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { webhookPaths } from "../../../deploy/channel-ingress.js";
|
|
5
|
-
import { composeHasTunnelService, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../../deploy/docker/plan.js";
|
|
5
|
+
import { DOCKER_COMPOSE_FILE, composeHasTunnelService, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../../deploy/docker/plan.js";
|
|
6
6
|
import { deployDockerRun } from "../../../deploy/docker/run.js";
|
|
7
7
|
import { spawnRunner } from "../../../deploy/runner.js";
|
|
8
8
|
import { openExternalUrl } from "../../../open-url.js";
|
|
9
9
|
import { SECRETS_DIRNAME, SECRET_FILE_MODE, exists, readTextIfExists, resolveStateRoot, } from "../../../paths.js";
|
|
10
10
|
import { dotEnvPath } from "../../../env.js";
|
|
11
11
|
import { announceWebhooks } from "../../../tunnel.js";
|
|
12
|
+
import { assembleSecrets } from "../../../deploy/secrets.js";
|
|
13
|
+
import { processShell } from "../../../deploy/box-shell.js";
|
|
12
14
|
import { failStartup } from "../../fail.js";
|
|
13
|
-
import {
|
|
15
|
+
import { isInteractive } from "../../shared.js";
|
|
16
|
+
import { boxLoginStep } from "./shared.js";
|
|
14
17
|
export const dockerHost = {
|
|
15
|
-
isOurs: (path, content) => path.endsWith(
|
|
18
|
+
isOurs: (path, content) => path.endsWith(DOCKER_COMPOSE_FILE) && isGeneratedCompose(content),
|
|
19
|
+
artifact: DOCKER_COMPOSE_FILE,
|
|
20
|
+
shell: async ({ agentDir, workspace }) => composeShell(`${basename(agentDir)}/${DOCKER_COMPOSE_FILE}`, workspace),
|
|
16
21
|
async deploy(ctx) {
|
|
17
22
|
const { opts, agentDir, workspace, channels, webhookChannels, pre, write } = ctx;
|
|
18
|
-
const { modelAuth,
|
|
23
|
+
const { modelAuth, boxLogin, container, port, declaredSecrets, values, valueFile } = pre;
|
|
19
24
|
// The generated Compose names `<agent>/.secrets/.env` unconditionally, so it has to be there — Compose refuses
|
|
20
25
|
// an `env_file` entry pointing at a missing path, and this floor predates `required: false` (Compose 2.24).
|
|
21
26
|
// Creating it empty is honest: a deployment that declares nothing declares it in an empty file.
|
|
@@ -51,7 +56,7 @@ export const dockerHost = {
|
|
|
51
56
|
const dockerPlan = (tunnel) => planDockerDeploy({
|
|
52
57
|
projectName,
|
|
53
58
|
port,
|
|
54
|
-
|
|
59
|
+
boxLogin,
|
|
55
60
|
channels,
|
|
56
61
|
tunnel,
|
|
57
62
|
secrets: pre.secrets,
|
|
@@ -84,8 +89,8 @@ export const dockerHost = {
|
|
|
84
89
|
port,
|
|
85
90
|
requireTunnel: requestedTunnel,
|
|
86
91
|
modelAuth,
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
boxLogin,
|
|
93
|
+
input: opts.input !== false && isInteractive(),
|
|
89
94
|
channels,
|
|
90
95
|
declaredSecrets,
|
|
91
96
|
values,
|
|
@@ -100,21 +105,23 @@ export const dockerHost = {
|
|
|
100
105
|
return;
|
|
101
106
|
},
|
|
102
107
|
};
|
|
103
|
-
/**
|
|
104
|
-
* `deploy docker --run`: carry local credentials into Compose's child environment, then reconcile the user-owned local
|
|
105
|
-
* topology.
|
|
106
|
-
*/
|
|
108
|
+
/** `deploy docker --run`: reconcile the user-owned local topology, then log the box in if it must. */
|
|
107
109
|
async function runDeployDocker(params) {
|
|
108
110
|
const { agentDir, workspace, composeFile, port, requireTunnel, channels } = params;
|
|
109
|
-
const { secrets, missingSecrets
|
|
111
|
+
const { secrets, missingSecrets } = assembleSecrets({
|
|
112
|
+
modelAuth: params.modelAuth,
|
|
113
|
+
declared: params.declaredSecrets,
|
|
114
|
+
values: params.values,
|
|
115
|
+
});
|
|
110
116
|
const outcome = await deployDockerRun({
|
|
111
117
|
composeFile,
|
|
112
118
|
port,
|
|
113
119
|
secrets,
|
|
114
120
|
missingSecrets,
|
|
115
121
|
valueFile: params.valueFile,
|
|
116
|
-
needsModelCredential,
|
|
117
122
|
requireTunnel,
|
|
123
|
+
channels,
|
|
124
|
+
...boxLoginStep("docker", params, () => composeShell(composeFile, workspace)),
|
|
118
125
|
announce: (tunnelUrl) => announceWebhooks(agentDir, tunnelUrl, channels, {
|
|
119
126
|
openUrl: openExternalUrl,
|
|
120
127
|
stateRoot: resolveStateRoot(agentDir),
|
|
@@ -144,3 +151,7 @@ async function runDeployDocker(params) {
|
|
|
144
151
|
`default webhook path(s): ${paths.join(", ")} (or your remapped channel routes)`);
|
|
145
152
|
}
|
|
146
153
|
}
|
|
154
|
+
/** `docker compose exec` into the running agent service. */
|
|
155
|
+
function composeShell(composeFile, workspace) {
|
|
156
|
+
return processShell("docker", (command) => ["compose", "-f", composeFile, "exec", "-T", "agent", "sh", "-c", command], workspace);
|
|
157
|
+
}
|
|
@@ -5,13 +5,21 @@ import { deployFlyRun } from "../../../deploy/fly/run.js";
|
|
|
5
5
|
import { spawnRunner } from "../../../deploy/runner.js";
|
|
6
6
|
import { readTextIfExists } from "../../../paths.js";
|
|
7
7
|
import { residencyFor } from "../../../deploy/residency.js";
|
|
8
|
+
import { assembleSecrets } from "../../../deploy/secrets.js";
|
|
9
|
+
import { processShell } from "../../../deploy/box-shell.js";
|
|
8
10
|
import { failStartup } from "../../fail.js";
|
|
9
|
-
import {
|
|
11
|
+
import { isInteractive } from "../../shared.js";
|
|
12
|
+
import { boxLoginStep, registrarsFor } from "./shared.js";
|
|
10
13
|
export const flyHost = {
|
|
11
14
|
isOurs: (path, content) => path.endsWith("fly.toml") && isGeneratedFlyToml(content),
|
|
15
|
+
artifact: "fly.toml",
|
|
16
|
+
async shell({ agentDir, workspace }) {
|
|
17
|
+
const flyToml = await readTextIfExists(join(agentDir, "fly.toml"));
|
|
18
|
+
return flyShell((flyToml && parseFlyAppName(flyToml)) ?? toFlyAppName(basename(workspace)), workspace);
|
|
19
|
+
},
|
|
12
20
|
async deploy(ctx) {
|
|
13
21
|
const { opts, agentDir, workspace, channels, pre, write } = ctx;
|
|
14
|
-
const { hasCron, modelAuth,
|
|
22
|
+
const { hasCron, modelAuth, boxLogin, container, port, declaredSecrets, values, valueFile } = pre;
|
|
15
23
|
// Two consistent modes.
|
|
16
24
|
const flyTomlPath = join(agentDir, "fly.toml");
|
|
17
25
|
const flyToml = await readTextIfExists(flyTomlPath).catch(failStartup);
|
|
@@ -43,7 +51,7 @@ export const flyHost = {
|
|
|
43
51
|
const plan = planFlyDeploy({
|
|
44
52
|
appName,
|
|
45
53
|
port,
|
|
46
|
-
|
|
54
|
+
boxLogin,
|
|
47
55
|
channels,
|
|
48
56
|
secrets: pre.secrets,
|
|
49
57
|
hasCron,
|
|
@@ -57,8 +65,8 @@ export const flyHost = {
|
|
|
57
65
|
agentPrefix: container.agentPrefix,
|
|
58
66
|
appName,
|
|
59
67
|
modelAuth,
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
boxLogin,
|
|
69
|
+
input: opts.input !== false && isInteractive(),
|
|
62
70
|
channels,
|
|
63
71
|
declaredSecrets,
|
|
64
72
|
values,
|
|
@@ -76,8 +84,11 @@ async function runDeployFly(params) {
|
|
|
76
84
|
if ((await fly(["version"], { capture: true })).code === 127) {
|
|
77
85
|
failStartup(new Error(`flyctl not found — install it: https://fly.io/docs/flyctl/install, then re-run`));
|
|
78
86
|
}
|
|
79
|
-
const { secrets, missingSecrets
|
|
80
|
-
|
|
87
|
+
const { secrets, missingSecrets } = assembleSecrets({
|
|
88
|
+
modelAuth: params.modelAuth,
|
|
89
|
+
declared: params.declaredSecrets,
|
|
90
|
+
values: params.values,
|
|
91
|
+
});
|
|
81
92
|
const outcome = await deployFlyRun({
|
|
82
93
|
appName,
|
|
83
94
|
secrets,
|
|
@@ -86,8 +97,20 @@ async function runDeployFly(params) {
|
|
|
86
97
|
channels,
|
|
87
98
|
flyConfig: `${agentPrefix}fly.toml`,
|
|
88
99
|
dockerfile: `${agentPrefix}Dockerfile`,
|
|
100
|
+
...boxLoginStep("fly", params, () => flyShell(appName, workspace)),
|
|
89
101
|
}, fly, (m) => console.error(`[fastagent] ${m}`), registrarsFor(agentDir));
|
|
90
102
|
if (!outcome.ok)
|
|
91
103
|
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
92
104
|
console.error(`[fastagent] deployed → https://${appName}.fly.dev`);
|
|
93
105
|
}
|
|
106
|
+
/** `fly ssh console` into the app's machine, woken first: a suspended machine has no shell to open. */
|
|
107
|
+
function flyShell(app, workspace) {
|
|
108
|
+
return {
|
|
109
|
+
...processShell("fly", (command) => ["ssh", "console", "--app", app, "--quiet", "--command", `sh -c '${command}'`], workspace),
|
|
110
|
+
// `fly ssh` fails with "no started VMs" while the machine is suspended, and a request is what resumes it. The
|
|
111
|
+
// answer is not the point: a machine that stays down makes the shell fail right after, with Fly's own reason.
|
|
112
|
+
wake: async () => {
|
|
113
|
+
await fetch(`https://${app}.fly.dev/health`, { signal: AbortSignal.timeout(30_000) }).catch(() => undefined);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -6,17 +6,22 @@ import { basename } from "node:path";
|
|
|
6
6
|
import { dockerfilePathVar, isGeneratedRailwayJson, planRailwayDeploy, toRailwayName, } from "../../../deploy/railway/plan.js";
|
|
7
7
|
import { deployRailwayRun } from "../../../deploy/railway/run.js";
|
|
8
8
|
import { spawnRunner } from "../../../deploy/runner.js";
|
|
9
|
+
import { assembleSecrets } from "../../../deploy/secrets.js";
|
|
10
|
+
import { processShell } from "../../../deploy/box-shell.js";
|
|
9
11
|
import { failStartup } from "../../fail.js";
|
|
10
|
-
import {
|
|
12
|
+
import { isInteractive } from "../../shared.js";
|
|
13
|
+
import { boxLoginStep, registrarsFor } from "./shared.js";
|
|
11
14
|
export const railwayHost = {
|
|
12
15
|
isOurs: (path, content) => path.endsWith("railway.json") && isGeneratedRailwayJson(content),
|
|
16
|
+
artifact: "railway.json",
|
|
17
|
+
shell: async ({ workspace }) => railwayShell(toRailwayName(basename(workspace)), workspace),
|
|
13
18
|
async deploy(ctx) {
|
|
14
19
|
const { opts, agentDir, workspace, pre, channels, write } = ctx;
|
|
15
|
-
const { hasCron, modelAuth,
|
|
20
|
+
const { hasCron, modelAuth, boxLogin, container, declaredSecrets, values, valueFile } = pre;
|
|
16
21
|
const serviceName = toRailwayName(basename(workspace));
|
|
17
22
|
const plan = planRailwayDeploy({
|
|
18
23
|
serviceName,
|
|
19
|
-
|
|
24
|
+
boxLogin,
|
|
20
25
|
channels,
|
|
21
26
|
secrets: pre.secrets,
|
|
22
27
|
hasCron,
|
|
@@ -36,8 +41,8 @@ export const railwayHost = {
|
|
|
36
41
|
workspace,
|
|
37
42
|
name: serviceName,
|
|
38
43
|
modelAuth,
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
boxLogin,
|
|
45
|
+
input: opts.input !== false && isInteractive(),
|
|
41
46
|
channels,
|
|
42
47
|
declaredSecrets,
|
|
43
48
|
values,
|
|
@@ -58,8 +63,11 @@ async function runDeployRailway(params) {
|
|
|
58
63
|
if ((await railway(["--version"], { capture: true })).code === 127) {
|
|
59
64
|
failStartup(new Error(`railway CLI not found — install it: https://docs.railway.com/guides/cli, then re-run`));
|
|
60
65
|
}
|
|
61
|
-
const { secrets, missingSecrets
|
|
62
|
-
|
|
66
|
+
const { secrets, missingSecrets } = assembleSecrets({
|
|
67
|
+
modelAuth: params.modelAuth,
|
|
68
|
+
declared: params.declaredSecrets,
|
|
69
|
+
values: params.values,
|
|
70
|
+
});
|
|
63
71
|
const outcome = await deployRailwayRun({
|
|
64
72
|
name,
|
|
65
73
|
mountPath: "/data",
|
|
@@ -69,8 +77,14 @@ async function runDeployRailway(params) {
|
|
|
69
77
|
channels,
|
|
70
78
|
intoLinked,
|
|
71
79
|
dockerfilePath,
|
|
80
|
+
...boxLoginStep("railway", params, () => railwayShell(name, workspace)),
|
|
72
81
|
}, railway, (m) => console.error(`[fastagent] ${m}`), registrarsFor(agentDir));
|
|
73
82
|
if (!outcome.ok)
|
|
74
83
|
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
75
84
|
console.error(`[fastagent] deployed → ${outcome.url}`);
|
|
76
85
|
}
|
|
86
|
+
/** `railway ssh` into the service, in the project and environment this directory is linked to. */
|
|
87
|
+
function railwayShell(service, workspace) {
|
|
88
|
+
// One quoted word: the command reaches the box as ONE line its shell parses, the way OpenSSH hands it over.
|
|
89
|
+
return processShell("railway", (command) => ["ssh", "--service", service, "--", `sh -c '${command}'`], workspace);
|
|
90
|
+
}
|
|
@@ -2,7 +2,9 @@ import type { DeclaredChannel } from "../../../channels/discover.ts";
|
|
|
2
2
|
import type { Registrars } from "../../../deploy/channel-ingress.ts";
|
|
3
3
|
import type { DeployPreflight } from "../../../deploy/preflight.ts";
|
|
4
4
|
import type { FastagentConfig } from "../../../engines/pi/config.ts";
|
|
5
|
-
import type
|
|
5
|
+
import { type ResolvedPlacement } from "../../../paths.ts";
|
|
6
|
+
import { type BoxLoginStep, type BoxShell } from "../../../deploy/box-shell.ts";
|
|
7
|
+
import type { DeployHost } from "../../../deploy/hosts.ts";
|
|
6
8
|
export interface DeployOptions {
|
|
7
9
|
run?: boolean;
|
|
8
10
|
tunnel?: boolean;
|
|
@@ -39,6 +41,10 @@ interface DeployContext {
|
|
|
39
41
|
export interface HostDeploy {
|
|
40
42
|
/** Did this host generate the file at `path`? */
|
|
41
43
|
isOurs(path: string, content: string): boolean;
|
|
44
|
+
/** The file in the agent dir whose presence says this workspace deploys to the host. */
|
|
45
|
+
artifact: string;
|
|
46
|
+
/** The shell into this workspace's running box (`fastagent login --deployment`). */
|
|
47
|
+
shell(placement: ResolvedPlacement): Promise<BoxShell>;
|
|
42
48
|
/**
|
|
43
49
|
* Plan the artifacts from the pre-flight facts and what is on disk, write them, then either drive the host CLI
|
|
44
50
|
* (`--run`) or print the runbook.
|
|
@@ -48,23 +54,18 @@ export interface HostDeploy {
|
|
|
48
54
|
/** The registrars every host driver gets. */
|
|
49
55
|
export declare function registrarsFor(agentDir: string): Registrars;
|
|
50
56
|
/**
|
|
51
|
-
* The `--run`
|
|
52
|
-
*
|
|
57
|
+
* The login `--run` starts on the box it just deployed, before any webhook is pointed at it (a host driver's `boxLogin`): for
|
|
58
|
+
* the model's provider, keeping a credential the box already holds (a redeploy), and asking only when a person can
|
|
59
|
+
* answer. None when the model's credential travels as a variable.
|
|
53
60
|
*/
|
|
54
|
-
export declare function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
secrets: Record<string, string>;
|
|
63
|
-
missingSecrets: string[];
|
|
64
|
-
needsModelCredential: boolean;
|
|
65
|
-
}>;
|
|
66
|
-
/** Gate a `--run` that has no model credential to carry. */
|
|
67
|
-
export declare function gateOnModelCredential(needsModelCredential: boolean): void;
|
|
61
|
+
export declare function boxLoginStep<Args extends unknown[]>(host: DeployHost, params: ResolvedPlacement & {
|
|
62
|
+
boxLogin: string | undefined;
|
|
63
|
+
input: boolean;
|
|
64
|
+
},
|
|
65
|
+
/** The shell, from whatever the driver learns only after deploying (AgentCore's runtime ARN). */
|
|
66
|
+
shell: (...args: Args) => BoxShell): {
|
|
67
|
+
boxLogin?: BoxLoginStep<Args>;
|
|
68
|
+
};
|
|
68
69
|
/** One artifact's verdict under the ownership rule: the bytes to write, or `undefined` to keep what is there. */
|
|
69
70
|
type ArtifactStep = {
|
|
70
71
|
abs: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What every host's deploy command shares: the context the dispatcher hands a host
|
|
3
|
-
* ownership rule
|
|
2
|
+
* What every host's deploy command shares: the context the dispatcher hands a host and the artifact writer with its
|
|
3
|
+
* ownership rule.
|
|
4
4
|
*/
|
|
5
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
@@ -10,9 +10,9 @@ import { registerSlackWebhook } from "../../../channels/slack/register-webhook.j
|
|
|
10
10
|
import { registerTelegramWebhook } from "../../../channels/telegram/register-webhook.js";
|
|
11
11
|
import { isGeneratedDockerfile, isGeneratedDockerignore } from "../../../deploy/container.js";
|
|
12
12
|
import { RELEASE_FILE } from "../../../deploy/workspace.js";
|
|
13
|
-
import { assembleSecrets } from "../../../deploy/secrets.js";
|
|
14
13
|
import { exists, resolveStateRoot } from "../../../paths.js";
|
|
15
|
-
import {
|
|
14
|
+
import { deploymentLoginCommand } from "../../../deploy/box-shell.js";
|
|
15
|
+
import { loginOnBox } from "../../box-login.js";
|
|
16
16
|
/** The registrars every host driver gets. */
|
|
17
17
|
export function registrarsFor(agentDir) {
|
|
18
18
|
const attempts = DEPLOY_REGISTRATION_ATTEMPTS;
|
|
@@ -23,24 +23,23 @@ export function registrarsFor(agentDir) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* The `--run`
|
|
27
|
-
*
|
|
26
|
+
* The login `--run` starts on the box it just deployed, before any webhook is pointed at it (a host driver's `boxLogin`): for
|
|
27
|
+
* the model's provider, keeping a credential the box already holds (a redeploy), and asking only when a person can
|
|
28
|
+
* answer. None when the model's credential travels as a variable.
|
|
28
29
|
*/
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return;
|
|
43
|
-
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
30
|
+
export function boxLoginStep(host, params,
|
|
31
|
+
/** The shell, from whatever the driver learns only after deploying (AgentCore's runtime ARN). */
|
|
32
|
+
shell) {
|
|
33
|
+
const provider = params.boxLogin;
|
|
34
|
+
if (provider === undefined)
|
|
35
|
+
return {};
|
|
36
|
+
const placement = { agentDir: params.agentDir, workspace: params.workspace };
|
|
37
|
+
return {
|
|
38
|
+
boxLogin: {
|
|
39
|
+
command: deploymentLoginCommand(host, provider),
|
|
40
|
+
run: (...args) => loginOnBox({ host, shell: shell(...args), placement, provider, ifMissing: true, input: params.input }),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
44
43
|
}
|
|
45
44
|
/**
|
|
46
45
|
* Decide what happens to each generated artifact under `target`, under ONE ownership rule — reading only, so a
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import { destroyAgentcoreDeployment } from "../../deploy/agentcore/destroy.js";
|
|
4
4
|
import { agentcoreName } from "../../deploy/agentcore/plan.js";
|
|
5
|
-
import {
|
|
5
|
+
import { awsRunner } from "../../deploy/runner.js";
|
|
6
6
|
import { enterAgentEnv } from "../../env.js";
|
|
7
7
|
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
8
8
|
export async function runDestroy(host, dirArg, opts) {
|
|
@@ -15,7 +15,7 @@ export async function runDestroy(host, dirArg, opts) {
|
|
|
15
15
|
const placement = placementOrExit(resolve(dirArg));
|
|
16
16
|
enterAgentEnv(placement.agentDir); // AWS_PROFILE/region/proxy may be definition-local, as on deploy
|
|
17
17
|
const name = agentcoreName(basename(placement.workspace));
|
|
18
|
-
const outcome = await destroyAgentcoreDeployment({ name, run: opts.run === true },
|
|
18
|
+
const outcome = await destroyAgentcoreDeployment({ name, run: opts.run === true }, awsRunner(placement.workspace), (message) => console.error(`[fastagent] destroy: ${message}`));
|
|
19
19
|
if (!outcome.ok) {
|
|
20
20
|
// THE WHOLE PICTURE, before the gate: a half-finished teardown is exactly when an operator needs to know
|
|
21
21
|
// what is out there, what is already gone, and what the retry still has to reach.
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { enterAgentEnv } from "../../env.js";
|
|
4
4
|
import { inspectChannels } from "../../channels/discover.js";
|
|
5
|
-
import { loadConfig, providerOf,
|
|
6
|
-
import {
|
|
5
|
+
import { loadConfig, providerOf, resolveModel, resolveModelSpec } from "../../engines/pi/config.js";
|
|
6
|
+
import { machineModels } from "../../engines/pi/models.js";
|
|
7
|
+
import { agentModels } from "../../engines/pi/agent-models.js";
|
|
7
8
|
import { resolveSessionsDir, resolveStateRoot } from "../../paths.js";
|
|
8
9
|
import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
|
|
9
10
|
import { loadAgentDefinition } from "../../engines/pi/definition.js";
|
|
@@ -74,13 +75,15 @@ export async function runInfo(dirArg, opts) {
|
|
|
74
75
|
const sessionsDir = resolveSessionsDir(agentDir);
|
|
75
76
|
// Both layers: "what is this agent's state" is the question `info` answers, and a credential it runs on can live in
|
|
76
77
|
// a file the agent dir does not contain.
|
|
77
|
-
const
|
|
78
|
+
const models = agentModels(agentDir);
|
|
79
|
+
const { auth } = models;
|
|
78
80
|
// RESOLVE the spec, do not just echo it: a spec is only real once its provider/model exist in the agent's own
|
|
79
81
|
// surface (built-ins + its models.json), which is exactly what a custom endpoint changes.
|
|
80
82
|
const modelError = modelSpec
|
|
81
|
-
? await
|
|
82
|
-
.
|
|
83
|
-
|
|
83
|
+
? await models
|
|
84
|
+
.runtime()
|
|
85
|
+
.then((runtime) => {
|
|
86
|
+
resolveModel(runtime, modelSpec);
|
|
84
87
|
return undefined;
|
|
85
88
|
})
|
|
86
89
|
.catch((error) => error.message)
|
|
@@ -6,12 +6,12 @@ import { failStartup } from "../fail.js";
|
|
|
6
6
|
import { enterAgentCommand, reportAuth } from "../shared.js";
|
|
7
7
|
export async function runInvoke(message, dirArg, opts) {
|
|
8
8
|
const placement = await enterAgentCommand(dirArg, opts);
|
|
9
|
-
const { agent, modelSpec,
|
|
9
|
+
const { agent, modelSpec, models } = await createPiAgentFromDir(placement.agentDir, {
|
|
10
10
|
model: opts.model,
|
|
11
11
|
}).catch(failStartup);
|
|
12
12
|
// BOTH directories, like dev/start: which agent ran, and what it worked on.
|
|
13
13
|
console.error(`[fastagent] invoke: ${placement.agentDir} (workspace ${placement.workspace}, ${modelSpec})`);
|
|
14
|
-
await reportAuth(
|
|
14
|
+
await reportAuth(models, modelSpec);
|
|
15
15
|
// Fresh session per invoke (one-shot, no resume). runInvokeStream maps events→IO: reply→stdout,
|
|
16
16
|
// tool/failure→stderr, exit 1 iff the turn failed (so CI can gate on it).
|
|
17
17
|
const exitCode = await runInvokeStream(agent.invoke({ session: randomUUID() }, { text: message }), (text) => process.stdout.write(text), (line) => console.error(line));
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export interface LoginOptions {
|
|
2
|
-
/** `-g`: store in the user-global file
|
|
2
|
+
/** `-g`: store in the user-global file, which an agent reads for a provider it has no other credential for. */
|
|
3
3
|
global?: boolean;
|
|
4
4
|
/** false ⇔ `--no-input`. */
|
|
5
5
|
input?: boolean;
|
|
6
|
+
/** `--deployment [host]`: log in this workspace's deployment (`true` when no host was named). */
|
|
7
|
+
deployment?: string | boolean;
|
|
8
|
+
/** `--stdio`: the box's half of `--deployment` — the flow speaks the relay wire on stdin/stdout. */
|
|
9
|
+
stdio?: boolean;
|
|
10
|
+
/** `--if-missing` (with `--stdio`): keep a credential already stored for the provider. */
|
|
11
|
+
ifMissing?: boolean;
|
|
6
12
|
}
|
|
7
13
|
export declare function runLogin(provider: string | undefined, opts: LoginOptions): Promise<void>;
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `fastagent login [provider]`: authenticate a model provider into the project-level auth file
|
|
3
|
-
* (`<agentDir>/.secrets/auth.json`) by default, or `FASTAGENT_AUTH_PATH`.
|
|
3
|
+
* (`<agentDir>/.secrets/auth.json`) by default, or `FASTAGENT_AUTH_PATH`. `--deployment` runs the same login on this
|
|
4
|
+
* workspace's deployed box instead (box-login.ts); `--stdio` is the box's half of that.
|
|
4
5
|
*/
|
|
5
|
-
import {
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { enterAgentEnv } from "../../env.js";
|
|
8
|
-
import { resolveAuthPath } from "../../engines/pi/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { GLOBAL_AUTH_PATH, resolveAuthPath } from "../../engines/pi/auth.js";
|
|
10
|
+
import { agentModels } from "../../engines/pi/agent-models.js";
|
|
11
|
+
import { DEPLOY_HOSTS } from "../../deploy/hosts.js";
|
|
12
|
+
import { findAgentDir, globalHome, placementDeadEnd } from "../../paths.js";
|
|
11
13
|
import { LoginCancelled, loginFlow } from "../../engines/pi/login.js";
|
|
12
|
-
import {
|
|
14
|
+
import { environmentAuthSource } from "../../engines/pi/models.js";
|
|
15
|
+
import { loginOnBox } from "../box-login.js";
|
|
16
|
+
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
17
|
+
import { stdioLoginIO } from "../login-relay.js";
|
|
13
18
|
import { isInteractive, terminalLoginIO } from "../shared.js";
|
|
19
|
+
import { HOSTS } from "./deploy.js";
|
|
14
20
|
export async function runLogin(provider, opts) {
|
|
21
|
+
if (opts.stdio)
|
|
22
|
+
return runStdioLogin(provider, opts);
|
|
23
|
+
if (opts.deployment !== undefined)
|
|
24
|
+
return runDeploymentLogin(provider, opts);
|
|
15
25
|
const cwd = process.cwd();
|
|
16
26
|
const agentDir = findAgentDir(cwd);
|
|
17
27
|
// "Outside an agent" must mean exactly that.
|
|
@@ -19,9 +29,12 @@ export async function runLogin(provider, opts) {
|
|
|
19
29
|
placementOrExit(cwd);
|
|
20
30
|
// Outside any agent the target is the user-global machinery home — handed over explicitly, so the path resolvers
|
|
21
31
|
// need no "is this $HOME?" special case to infer it.
|
|
22
|
-
const loginDir = agentDir ??
|
|
32
|
+
const loginDir = agentDir ?? globalHome();
|
|
23
33
|
// FASTAGENT_AUTH_PATH and a proxy may both be configured in the project .env, and the OAuth token exchange must go
|
|
24
34
|
// through that proxy (region-locked providers).
|
|
35
|
+
// Before the agent's `.env` joins it: the shadowing check below is about what EVERY agent sees, and that is the
|
|
36
|
+
// environment the shell hands down, not one agent's value file.
|
|
37
|
+
const shellEnv = { ...process.env };
|
|
25
38
|
enterAgentEnv(loginDir);
|
|
26
39
|
// `-g` names the global file outright. Otherwise: FASTAGENT_AUTH_PATH > default — the one owner. The
|
|
27
40
|
// store built here is deliberately UNLAYERED: reading falls back to the global file, but writing must land
|
|
@@ -32,9 +45,9 @@ export async function runLogin(provider, opts) {
|
|
|
32
45
|
// lands somewhere no agent will read.
|
|
33
46
|
if (!agentDir && !opts.global && !process.env.FASTAGENT_AUTH_PATH) {
|
|
34
47
|
console.error(`[fastagent] no agent here (no fastagent.config.ts, here or one level inside) — ` +
|
|
35
|
-
`logging in GLOBALLY (${authPath}).
|
|
36
|
-
|
|
37
|
-
`one its own account instead.`);
|
|
48
|
+
`logging in GLOBALLY (${authPath}). An agent on this machine uses this file for a provider it has no ` +
|
|
49
|
+
`credential of its own for (no entry in its .secrets/auth.json, no apiKey in its models.json, no env ` +
|
|
50
|
+
`variable), so this is usually what you want; \`cd\` into an agent to give that one its own account instead.`);
|
|
38
51
|
}
|
|
39
52
|
// login is inherently interactive — loginFlow renders provider/method menus and opens a browser (or prompts for a
|
|
40
53
|
// key).
|
|
@@ -57,5 +70,105 @@ export async function runLogin(provider, opts) {
|
|
|
57
70
|
failStartup(error);
|
|
58
71
|
});
|
|
59
72
|
console.error(`[fastagent] logged in to ${result.provider} (${result.method}) — saved to ${authPath}`);
|
|
73
|
+
// The environment outranks the global file for every agent, so a global login it shadows would sit unused while
|
|
74
|
+
// agents run on, say, an API key billed per request, with only a startup line to show it.
|
|
75
|
+
const shadowedBy = authPath === GLOBAL_AUTH_PATH ? await environmentAuthSource(result.provider, shellEnv) : undefined;
|
|
76
|
+
if (shadowedBy !== undefined) {
|
|
77
|
+
console.error(`[fastagent] warning: ${result.provider} is also authenticated by ${shadowedBy} in your shell environment, which ` +
|
|
78
|
+
`agents use before the global file, so this login is not used while it is set. Unset it to use this login.`);
|
|
79
|
+
}
|
|
60
80
|
process.exit(0); // the undici proxy agent's keep-alive sockets would otherwise hold the event loop open
|
|
61
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* `--deployment [host]`: the host is named, or the one whose generated artifact is in the agent dir. Several is a
|
|
84
|
+
* question only the operator can answer, so it is asked back rather than guessed.
|
|
85
|
+
*/
|
|
86
|
+
async function runDeploymentLogin(provider, opts) {
|
|
87
|
+
if (opts.global)
|
|
88
|
+
failUsage("--deployment logs the deployment in; -g names this machine's global file — pick one");
|
|
89
|
+
const placement = placementOrExit(process.cwd());
|
|
90
|
+
// The host's CLI must reach the account/region/proxy the deploy used, and those may be definition-local.
|
|
91
|
+
enterAgentEnv(placement.agentDir);
|
|
92
|
+
let host;
|
|
93
|
+
if (typeof opts.deployment === "string") {
|
|
94
|
+
if (!DEPLOY_HOSTS.includes(opts.deployment)) {
|
|
95
|
+
failUsage(`--deployment takes a host (${DEPLOY_HOSTS.join(", ")}), not "${opts.deployment}" — to name a provider too: ` +
|
|
96
|
+
`fastagent login <provider> --deployment <host>`);
|
|
97
|
+
}
|
|
98
|
+
host = opts.deployment;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const found = DEPLOY_HOSTS.filter((h) => existsSync(join(placement.agentDir, HOSTS[h].artifact)));
|
|
102
|
+
if (found.length !== 1) {
|
|
103
|
+
failUsage(found.length === 0
|
|
104
|
+
? `no deployment artifacts in ${placement.agentDir} — name the host: --deployment <${DEPLOY_HOSTS.join("|")}>`
|
|
105
|
+
: `this agent deploys to ${found.join(" and ")} — name one: --deployment <${found.join("|")}>`);
|
|
106
|
+
}
|
|
107
|
+
host = found[0];
|
|
108
|
+
}
|
|
109
|
+
const input = opts.input !== false && isInteractive();
|
|
110
|
+
if (!input) {
|
|
111
|
+
failStartup(new Error(`login is interactive (it shows a menu and opens a browser) — run it in a terminal`));
|
|
112
|
+
}
|
|
113
|
+
const failed = await loginOnBox({
|
|
114
|
+
host,
|
|
115
|
+
shell: await HOSTS[host].shell(placement).catch(failStartup),
|
|
116
|
+
placement,
|
|
117
|
+
...(provider ? { provider } : {}),
|
|
118
|
+
input,
|
|
119
|
+
});
|
|
120
|
+
if (failed)
|
|
121
|
+
failStartup(new Error(failed));
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `--stdio`: the box's half of `--deployment`. Runs where the server runs; everything it would show or ask travels as
|
|
126
|
+
* the relay wire, and it ends with exactly one result line. Never started by hand: the credential path is the one the
|
|
127
|
+
* caller exported (container.ts `boxLoginCommand`).
|
|
128
|
+
*/
|
|
129
|
+
async function runStdioLogin(provider, opts) {
|
|
130
|
+
const wire = stdioLoginIO(process.stdin, process.stdout);
|
|
131
|
+
const result = await stdioLogin(wire.io, provider, opts).catch((error) => error instanceof LoginCancelled
|
|
132
|
+
? { ok: false, reason: "cancelled", message: "cancelled" }
|
|
133
|
+
: { ok: false, reason: "failed", message: error.message });
|
|
134
|
+
wire.result(result);
|
|
135
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
136
|
+
// Exit once the result line is flushed: an undici keep-alive socket would otherwise hold the process open.
|
|
137
|
+
process.stdout.write("", () => process.exit());
|
|
138
|
+
}
|
|
139
|
+
async function stdioLogin(io, provider, opts) {
|
|
140
|
+
// `boxLoginCommand` starts this in the deployed agent directory; anywhere else there is no box to log in.
|
|
141
|
+
const agentDir = findAgentDir(process.cwd());
|
|
142
|
+
if (!agentDir)
|
|
143
|
+
throw new Error(`login --stdio: ${process.cwd()} is not an agent directory`);
|
|
144
|
+
// A deployed box has no value file to read: its variables reach this process only as the host's shell hands them
|
|
145
|
+
// down (the container's for `docker compose exec`, the platform's over fly/railway ssh), and this installs the egress
|
|
146
|
+
// proxy they name. AgentCore's command shell hands down none of the runtime's, so a proxy carried there in
|
|
147
|
+
// FASTAGENT_ENV does not reach the login (docs/deploy.md).
|
|
148
|
+
enterAgentEnv(agentDir);
|
|
149
|
+
// The model environment the serving runtime reads (createPiAgentFromDir), so "logged in" means what the server will
|
|
150
|
+
// use.
|
|
151
|
+
const models = agentModels(agentDir);
|
|
152
|
+
const { auth } = models;
|
|
153
|
+
if (opts.ifMissing && provider) {
|
|
154
|
+
// The server's own answer (`authStatus`), the one its startup report prints. Known ceiling: nothing is asked of
|
|
155
|
+
// the provider beyond a due refresh, so a revoked grant whose access token has not expired yet, or a revoked API
|
|
156
|
+
// key, reads as held and the first turn fails with the provider's error. Asking would spend a real model call on
|
|
157
|
+
// every redeploy, which the readiness checks deliberately never do.
|
|
158
|
+
const held = await models.authStatus(provider);
|
|
159
|
+
if (held.source !== undefined)
|
|
160
|
+
return { ok: true, provider, kept: held.source };
|
|
161
|
+
if (opts.input === false) {
|
|
162
|
+
const what = held.stored
|
|
163
|
+
? `the stored ${provider} ${held.stored} credential is expired or unusable`
|
|
164
|
+
: `no ${provider} credential`;
|
|
165
|
+
const because = held.error === undefined ? "" : ` (${held.error})`;
|
|
166
|
+
return { ok: false, reason: "missing", message: `${what}${because} in ${held.path ?? auth.path}` };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (opts.input === false) {
|
|
170
|
+
return { ok: false, reason: "missing", message: `no ${provider ?? "model"} credential in ${auth.path}` };
|
|
171
|
+
}
|
|
172
|
+
const result = await loginFlow(io, { authPath: auth.path, ...(provider ? { provider } : {}), agentDir });
|
|
173
|
+
return { ok: true, provider: result.provider, method: result.method, path: auth.path };
|
|
174
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import { agentcoreName } from "../../deploy/agentcore/plan.js";
|
|
4
4
|
import { tailAgentcoreLogs } from "../../deploy/agentcore/logs.js";
|
|
5
|
-
import {
|
|
5
|
+
import { awsRunner } from "../../deploy/runner.js";
|
|
6
6
|
import { enterAgentEnv } from "../../env.js";
|
|
7
7
|
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
8
8
|
export async function runLogs(host, dirArg, opts) {
|
|
@@ -20,7 +20,7 @@ export async function runLogs(host, dirArg, opts) {
|
|
|
20
20
|
source: source,
|
|
21
21
|
since: opts.since,
|
|
22
22
|
follow: opts.follow === true,
|
|
23
|
-
},
|
|
23
|
+
}, awsRunner(placement.workspace), (message) => console.error(`[fastagent] logs: ${message}`));
|
|
24
24
|
if (!outcome.ok)
|
|
25
25
|
failStartup(new Error(`logs stopped: ${outcome.gate}`));
|
|
26
26
|
}
|