@fastagent-sh/fastagent 0.17.0 → 0.18.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 +11 -0
- package/dist/channels/agentcore.d.ts +16 -2
- package/dist/channels/agentcore.js +66 -4
- package/dist/channels/feishu/card.d.ts +20 -9
- package/dist/channels/feishu/card.js +27 -13
- package/dist/channels/feishu/feishu-api.d.ts +13 -2
- package/dist/channels/feishu/feishu.js +121 -17
- package/dist/channels/feishu/invoke-turn.d.ts +12 -2
- package/dist/channels/feishu/invoke-turn.js +159 -14
- package/dist/channels/feishu/normalize.js +97 -32
- package/dist/channels/feishu/parse.js +6 -0
- package/dist/channels/feishu/preview.d.ts +4 -3
- package/dist/channels/feishu/preview.js +77 -23
- package/dist/channels/feishu/scaffold/feishu-send.ts +9 -6
- package/dist/channels/http.js +15 -2
- package/dist/channels/invoke-turn-kit.d.ts +5 -2
- package/dist/channels/invoke-turn-kit.js +6 -2
- package/dist/channels/lark/scaffold/lark-send.ts +9 -6
- package/dist/channels/slack/invoke-turn.js +1 -1
- package/dist/channels/slack/slack.js +1 -5
- package/dist/channels/state.d.ts +0 -10
- package/dist/channels/state.js +2 -19
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/thread-participants.d.ts +7 -0
- package/dist/channels/thread-participants.js +3 -0
- package/dist/cli/commands/deploy.js +13 -5
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +21 -1
- package/dist/cli/commands/invoke.js +1 -1
- package/dist/cli/commands/start.js +40 -6
- package/dist/cli/serve.d.ts +4 -0
- package/dist/cli/serve.js +2 -2
- package/dist/cli/shared.d.ts +4 -2
- package/dist/cli/shared.js +12 -5
- package/dist/collect.d.ts +30 -4
- package/dist/collect.js +39 -6
- package/dist/deploy/agentcore/plan.js +17 -0
- package/dist/deploy/agentcore/run.d.ts +7 -1
- package/dist/deploy/agentcore/run.js +93 -8
- package/dist/deploy/preflight.d.ts +8 -2
- package/dist/deploy/preflight.js +55 -10
- package/dist/deploy/secrets.d.ts +3 -0
- package/dist/deploy/secrets.js +6 -0
- package/dist/dev-supervisor.js +8 -2
- package/dist/engines/pi/create.d.ts +2 -1
- package/dist/engines/pi/create.js +12 -7
- package/dist/engines/pi/harness.d.ts +6 -3
- package/dist/engines/pi/harness.js +4 -3
- package/dist/engines/pi/invoke-session.d.ts +32 -0
- package/dist/engines/pi/invoke-session.js +171 -0
- package/dist/engines/pi/invoke.d.ts +6 -27
- package/dist/engines/pi/invoke.js +49 -208
- package/dist/engines/pi/models.d.ts +45 -11
- package/dist/engines/pi/models.js +55 -8
- package/dist/engines/pi/session-builder.js +4 -2
- package/dist/engines/pi/session-control.d.ts +2 -1
- package/dist/engines/pi/sessions.d.ts +17 -1
- package/dist/engines/pi/sessions.js +292 -10
- package/dist/engines/pi/turn-kit.d.ts +56 -0
- package/dist/engines/pi/turn-kit.js +161 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.js +6 -0
- package/dist/pi.d.ts +3 -2
- package/dist/pi.js +1 -1
- package/dist/scaffold/templates/fastagent.config.mjs +2 -0
- package/dist/session-remote.js +10 -2
- package/package.json +1 -1
|
@@ -396,6 +396,23 @@ exports.handler = async (event, ctx) => {
|
|
|
396
396
|
if (failed > 0) return { statusCode: 500, body: \`\${failed} alarm(s) failed\\n\` };
|
|
397
397
|
return { statusCode: 200, body: "ok\\n" };
|
|
398
398
|
}
|
|
399
|
+
// The deploy driver's probe (reserved path, ingress secret): wake the runtime through the SAME
|
|
400
|
+
// trusted envelope pipeline (state URLs included — a direct InvokeAgentRuntime call could not mint
|
|
401
|
+
// them, and would make the runtime construct against a pre-restore mount) and pass its structured
|
|
402
|
+
// transport-200 verdict back VERBATIM. The ordinary webhook path below folds a non-200 transport
|
|
403
|
+
// into an opaque 502, which would strip exactly the diagnostics the probe exists to carry — and it
|
|
404
|
+
// sits BEFORE the WEBHOOKS_ENABLED gate so schedule-only topologies (whose URLs refuse ordinary
|
|
405
|
+
// public traffic) are probeable too.
|
|
406
|
+
if (event.rawPath === "/__fastagent/probe") {
|
|
407
|
+
const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
|
|
408
|
+
if (!process.env.INGRESS_SECRET || req.auth !== process.env.INGRESS_SECRET) return { statusCode: 403, body: "forbidden\\n" };
|
|
409
|
+
const r = await invoke({ kind: "probe" });
|
|
410
|
+
if (r.status !== 200) {
|
|
411
|
+
console.log(\`probe transport error \${r.status}: \${r.body}\`);
|
|
412
|
+
return { statusCode: 502, body: "upstream error\\n" };
|
|
413
|
+
}
|
|
414
|
+
return { statusCode: 200, headers: { "content-type": "application/json" }, body: r.body.toString() };
|
|
415
|
+
}
|
|
399
416
|
// Enforce the advertised ORIGINAL-body ceiling before base64 adds another 4/3 inside the runtime
|
|
400
417
|
// envelope. This also leaves deterministic room for headers/query/JSON under Lambda's 6 MB cap.
|
|
401
418
|
const webhookBytes = event.body === undefined ? 0
|
|
@@ -70,4 +70,10 @@ export declare function parseCheckpointReply(stdout: string): CheckpointReply |
|
|
|
70
70
|
* post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
|
|
71
71
|
* gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
|
|
72
72
|
*/
|
|
73
|
-
export declare function deployAgentcoreRun(plan: AgentcoreRunPlan, aws: CliRunner, docker: CliRunner, log: (msg: string) => void, writeSecretFile: (content: string) => Promise<string>, writeForwarderZip: (bytes: Uint8Array) => Promise<string>, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>, registerSlack?: (baseUrl: string) => Promise<RegistrationOutcome
|
|
73
|
+
export declare function deployAgentcoreRun(plan: AgentcoreRunPlan, aws: CliRunner, docker: CliRunner, log: (msg: string) => void, writeSecretFile: (content: string) => Promise<string>, writeForwarderZip: (bytes: Uint8Array) => Promise<string>, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>, registerSlack?: (baseUrl: string) => Promise<RegistrationOutcome>,
|
|
74
|
+
/** Injected in tests; the probe itself stays inside the run so no deploy can skip it. */
|
|
75
|
+
probe?: {
|
|
76
|
+
fetchImpl?: typeof fetch;
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
intervalMs?: number;
|
|
79
|
+
}): Promise<AgentcoreRunOutcome>;
|
|
@@ -3,6 +3,66 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { Buffer } from "node:buffer";
|
|
4
4
|
import { AUTH_SEED_CHUNK_SIZE, AUTH_SEED_MAX_CHUNKS, cfnParamName, forwarderSource, ingressSessionId, stateBucketName, } from "./plan.js";
|
|
5
5
|
import { zipSingleFile } from "./zip.js";
|
|
6
|
+
/** How long the post-deploy probe waits for the fresh session (image pull + microVM boot + snapshot
|
|
7
|
+
* restore + channel construction) before gating with the last answer. */
|
|
8
|
+
const PROBE_TIMEOUT_MS = 120_000;
|
|
9
|
+
const PROBE_INTERVAL_MS = 3_000;
|
|
10
|
+
/**
|
|
11
|
+
* Drive the forwarder's reserved `/__fastagent/probe` path until it answers, and read the runtime's
|
|
12
|
+
* STRUCTURED verdict. The path answers on every forwarder topology (a schedule-only URL refuses
|
|
13
|
+
* ordinary public traffic, so a plain `GET /health` would 404 there), and the verdict rides a
|
|
14
|
+
* transport-200 JSON body `{ ok, error? }` — the ordinary webhook relay folds a non-200 transport
|
|
15
|
+
* into an opaque 502, which would strip the very diagnostics this probe exists to carry.
|
|
16
|
+
*
|
|
17
|
+
* Outcome policy: `ok:true` verifies the deploy; `ok:false` gates IMMEDIATELY with the runtime's own
|
|
18
|
+
* error text (construction rejections are cached per session, so polling cannot change the answer);
|
|
19
|
+
* anything else (unroutable URL, forwarder 4xx/5xx, malformed body) is retried to the deadline —
|
|
20
|
+
* that budget's job is absorbing cold-start provisioning — and then gates with the last answer seen.
|
|
21
|
+
*/
|
|
22
|
+
async function probeRuntime(probeUrl, auth, fetchImpl, timeoutMs = PROBE_TIMEOUT_MS, intervalMs = PROBE_INTERVAL_MS) {
|
|
23
|
+
const deadline = Date.now() + timeoutMs;
|
|
24
|
+
let last;
|
|
25
|
+
for (;;) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetchImpl(probeUrl, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json" },
|
|
30
|
+
body: JSON.stringify({ auth }),
|
|
31
|
+
signal: AbortSignal.timeout(65_000),
|
|
32
|
+
});
|
|
33
|
+
const bodyText = await res.text();
|
|
34
|
+
if (res.status === 200) {
|
|
35
|
+
let verdict;
|
|
36
|
+
try {
|
|
37
|
+
verdict = JSON.parse(bodyText);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* malformed — fall through to retry with it as the last answer */
|
|
41
|
+
}
|
|
42
|
+
if (verdict?.ok === true)
|
|
43
|
+
return { ok: true };
|
|
44
|
+
if (verdict?.ok === false) {
|
|
45
|
+
const error = typeof verdict.error === "string" ? verdict.error : "unknown error";
|
|
46
|
+
return { ok: false, gate: `the deployed runtime failed its probe: ${error} — fix and re-run` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const firstLine = bodyText.trim().split("\n")[0] ?? "";
|
|
50
|
+
last = `${res.status}${firstLine ? ` ${firstLine}` : ""}`;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* not routable yet (Function URL DNS, cold start) — keep polling until the deadline */
|
|
54
|
+
}
|
|
55
|
+
if (Date.now() >= deadline) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
gate: last
|
|
59
|
+
? `the forwarder probe never verified the deployment (last answer: ${last}) — check the runtime logs and re-run`
|
|
60
|
+
: "the forwarder URL never answered the probe — check the Function URL / runtime logs and re-run",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
6
66
|
/** Stack outputs (`describe-stacks --query "Stacks[0].Outputs"`) → { OutputKey: OutputValue }. */
|
|
7
67
|
export function parseStackOutputs(stdout) {
|
|
8
68
|
try {
|
|
@@ -63,7 +123,9 @@ export function parseCheckpointReply(stdout) {
|
|
|
63
123
|
* post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
|
|
64
124
|
* gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
|
|
65
125
|
*/
|
|
66
|
-
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registerTelegram, registerFeishu, registerSlack
|
|
126
|
+
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registerTelegram, registerFeishu, registerSlack,
|
|
127
|
+
/** Injected in tests; the probe itself stays inside the run so no deploy can skip it. */
|
|
128
|
+
probe = {}) {
|
|
67
129
|
const gate = (g) => ({ ok: false, gate: g });
|
|
68
130
|
const stack = `fastagent-${plan.name}`;
|
|
69
131
|
const repo = `fastagent/${plan.name}`;
|
|
@@ -359,19 +421,42 @@ export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile
|
|
|
359
421
|
log("note: no ingress session to stop (first deploy, or already reclaimed)");
|
|
360
422
|
}
|
|
361
423
|
else {
|
|
362
|
-
|
|
363
|
-
|
|
424
|
+
// A GATE, not a warning: the probe below reaches the SAME fixed session id, so a session
|
|
425
|
+
// still running the previous image would answer it and the deploy would claim to have
|
|
426
|
+
// verified a serving path it never touched. Unable to guarantee the session is fresh =
|
|
427
|
+
// unable to verify = stop.
|
|
364
428
|
const firstLine = stderr.trim().split("\n")[0];
|
|
365
|
-
|
|
366
|
-
|
|
429
|
+
return gate(`could not stop the ingress session — it may still be serving the PREVIOUS image, so the ` +
|
|
430
|
+
`deploy cannot verify the new one${firstLine ? ` (${firstLine})` : ""}. ` +
|
|
431
|
+
`Stop it manually (aws ${stopCommand.join(" ")}) and re-run`);
|
|
367
432
|
}
|
|
368
433
|
}
|
|
369
434
|
}
|
|
435
|
+
// 8c. Every forwarder topology MUST carry the ForwarderUrl output — schedule-only and
|
|
436
|
+
// selfSchedule-only deployments included, since the probe below is their only construction
|
|
437
|
+
// check (there is no boot-time failStartup on this host). A missing output means an edited
|
|
438
|
+
// template; skipping the probe silently would let such a deploy report success unverified.
|
|
439
|
+
// Only a pure-invoke deployment (no forwarder) legitimately has no URL and nothing to probe.
|
|
440
|
+
// `channels.length` is belt-and-braces: the planner derives needsForwarder FROM the channel
|
|
441
|
+
// list, but this gate must not silently trust that invariant across callers.
|
|
442
|
+
if ((plan.needsForwarder || plan.channels.length > 0) && !url) {
|
|
443
|
+
return gate("this deployment needs the forwarder but the stack has no ForwarderUrl output — regenerate the " +
|
|
444
|
+
"template with --force");
|
|
445
|
+
}
|
|
446
|
+
// 8d. Warm + verify the NEW serving path end to end, BEFORE registration: the probe wakes a fresh
|
|
447
|
+
// session on the new image through the forwarder's reserved path, which restores the state
|
|
448
|
+
// snapshot and constructs the channels — construction is deferred to exactly that moment
|
|
449
|
+
// (channels/agentcore.ts), so this is where a bad credential, a broken channels/ module, or an
|
|
450
|
+
// unrestorable snapshot surfaces AT DEPLOY TIME with the runtime's own error text.
|
|
451
|
+
if (url) {
|
|
452
|
+
log("probing the deployed runtime (state restore + channel construction)…");
|
|
453
|
+
const verdict = await probeRuntime(`${url}/__fastagent/probe`, plan.secrets.FASTAGENT_INGRESS_SECRET ?? "", probe.fetchImpl ?? fetch, probe.timeoutMs, probe.intervalMs);
|
|
454
|
+
if (!verdict.ok)
|
|
455
|
+
return gate(verdict.gate);
|
|
456
|
+
log("runtime verified (state restored, channels constructed)");
|
|
457
|
+
}
|
|
370
458
|
// 9. Post-deploy webhook registration — same registrar seam as every host, pointed at the
|
|
371
459
|
// forwarder's Function URL. Gate policy is the shared registration-gate kernel.
|
|
372
|
-
if (plan.channels.length > 0 && !url) {
|
|
373
|
-
return gate("channels are declared but the stack has no ForwarderUrl output — regenerate the template with --force");
|
|
374
|
-
}
|
|
375
460
|
const reg = registrationGate(log, "re-run to retry registration (steps already done are skipped)");
|
|
376
461
|
if (url) {
|
|
377
462
|
if (plan.channels.includes("telegram")) {
|
|
@@ -19,9 +19,15 @@ interface DeployFacts {
|
|
|
19
19
|
* has no external wake-up, so the deployment must keep one machine running: the fly plan forces
|
|
20
20
|
* `min_machines_running=1`, the railway runbook forbids App Sleeping. */
|
|
21
21
|
hasTimeTriggers: boolean;
|
|
22
|
-
/** What satisfies model auth locally
|
|
23
|
-
*
|
|
22
|
+
/** What satisfies model auth locally — an env-var name, an OAuth/stored label, or undefined. Drives the
|
|
23
|
+
* runbook's secret guidance and `--run`'s credential carry. For a models.json endpoint keyed from the
|
|
24
|
+
* environment this is the VARIABLE NAME (see {@link modelCredentialCarry}), not the display label, so
|
|
25
|
+
* the value carries like any provider key. */
|
|
24
26
|
modelAuth: string | undefined;
|
|
27
|
+
/** The definition itself carries the model key (a models.json literal `apiKey`, or a `!command` run on
|
|
28
|
+
* the host), so there is nothing for `--run` to carry AND nothing to gate: `fastagent login` cannot
|
|
29
|
+
* serve a custom provider, so gating on it would strand a correctly configured agent. */
|
|
30
|
+
modelKeyInDefinition: boolean;
|
|
25
31
|
/** The project-level auth file `--run` reads to carry the credential (probed with the same path). */
|
|
26
32
|
authPath: string;
|
|
27
33
|
/** Container facts shared by the plan and the generated Dockerfile — ONE source, so they can't drift. */
|
package/dist/deploy/preflight.js
CHANGED
|
@@ -17,12 +17,13 @@ import { resolveAuthPath } from "../engines/pi/config.js";
|
|
|
17
17
|
import { resolveSecretsDir, resolveStateRoot } from "../paths.js";
|
|
18
18
|
import { inspectChannels } from "../engines/pi/channel.js";
|
|
19
19
|
import { discoverScheduleFiles } from "../schedule/discover.js";
|
|
20
|
-
import {
|
|
20
|
+
import { createPiModelRuntime, modelCredentialCarry, probeAuthSource } from "../engines/pi/models.js";
|
|
21
21
|
import { CHANNEL_KINDS } from "../scaffold/add-channel.js";
|
|
22
22
|
import { exists } from "../paths.js";
|
|
23
23
|
import { detectRuntime, readPackageJson } from "../runtime.js";
|
|
24
24
|
import { fastagentVersion } from "../version.js";
|
|
25
25
|
import { isGeneratedDockerfile, isGeneratedDockerignore } from "./container.js";
|
|
26
|
+
import { isEnvKey } from "./secrets.js";
|
|
26
27
|
/**
|
|
27
28
|
* "Would docker's packer drop this path?" — built from a `.dockerignore`'s text via the `ignore`
|
|
28
29
|
* matcher (the same library the workspace ignore files use), so `!` negation and last-match-wins are
|
|
@@ -126,9 +127,25 @@ export async function preflightDeploy(input) {
|
|
|
126
127
|
});
|
|
127
128
|
}
|
|
128
129
|
// Probe auth from the SAME project-level file the opener/login use — not the global default, which would
|
|
129
|
-
// miss a `fastagent login` credential and falsely report "none configured".
|
|
130
|
+
// miss a `fastagent login` credential and falsely report "none configured". Through the AGENT's model
|
|
131
|
+
// surface too (its models.json travels into the image), so a custom endpoint is not read as an unknown
|
|
132
|
+
// provider — this probe feeds the gate that decides whether `--run` may proceed.
|
|
130
133
|
const authPath = resolveAuthPath(agentDir, authPathFlag);
|
|
131
|
-
const
|
|
134
|
+
const models = await createPiModelRuntime({ agentDir, authPath });
|
|
135
|
+
let modelAuth = modelSpec ? await probeAuthSource(models, modelSpec) : undefined;
|
|
136
|
+
let modelKeyInDefinition = false;
|
|
137
|
+
// probeAuthSource answers "is it authenticated here", which is not the deploy question ("how does the
|
|
138
|
+
// credential REACH the host"). It reports every models.json endpoint as "configured API key" — not an
|
|
139
|
+
// env-var name — so without this the gate below sees no credential and stops the deploy with two
|
|
140
|
+
// remedies that are both wrong for such an agent: `fastagent login` cannot serve a custom provider,
|
|
141
|
+
// and the key is already in the environment.
|
|
142
|
+
if (modelSpec && !isEnvKey(modelAuth)) {
|
|
143
|
+
const carry = modelCredentialCarry(models, modelSpec);
|
|
144
|
+
if (carry.envVar)
|
|
145
|
+
modelAuth = carry.envVar;
|
|
146
|
+
else
|
|
147
|
+
modelKeyInDefinition = carry.inDefinition;
|
|
148
|
+
}
|
|
132
149
|
// Container facts (shared by every host) + the warnings that follow. The facts describe the AGENT —
|
|
133
150
|
// its package.json/runtime/lockfile drive the image's install step — never the workspace's (the bake
|
|
134
151
|
// ships the whole tree, but the workspace's own manifest belongs to its own deploy).
|
|
@@ -195,12 +212,17 @@ export async function preflightDeploy(input) {
|
|
|
195
212
|
const rel = relative(workspace, p);
|
|
196
213
|
return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? undefined : rel.split(sep).join("/");
|
|
197
214
|
};
|
|
198
|
-
// The secrets DIR is the unit,
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
215
|
+
// The secrets DIR is the unit of RESPONSIBILITY, but never the unit of the leak QUESTION below: the
|
|
216
|
+
// generated ignore excludes the dir's CONTENTS (`**/.secrets/**`) so its two value-free tracked
|
|
217
|
+
// scaffolds can be re-included, and a directory-level question reads that correct file as "not
|
|
218
|
+
// excluded" — the generator's own default output gated its own deploy (field-hit: a fresh
|
|
219
|
+
// kit-layout workspace without --force; --force skips checking our own file, which is why the
|
|
220
|
+
// combination stayed invisible). What leaks is a FILE, so files are what the gate asks about — see
|
|
221
|
+
// secretDirFiles below, which enumerates what is actually inside (an atomic-write temp beside
|
|
222
|
+
// auth.json, a second key file, an editor backup of `.env`: the dir-as-unit worry, covered per
|
|
223
|
+
// file). The auth path adds an entry only when an override puts it OUTSIDE that dir. An external
|
|
224
|
+
// secrets dir (the deployed posture: a mounted volume) is outside the context — nothing to check,
|
|
225
|
+
// nothing to exclude.
|
|
204
226
|
const secretsRel = inContext(resolveSecretsDir(agentDir));
|
|
205
227
|
const authRel = inContext(authPath);
|
|
206
228
|
const authElsewhere = authRel !== undefined && (secretsRel === undefined || !authRel.startsWith(`${secretsRel}/`));
|
|
@@ -246,7 +268,29 @@ export async function preflightDeploy(input) {
|
|
|
246
268
|
.map((n) => join(relDir, n).split(sep).join("/"));
|
|
247
269
|
};
|
|
248
270
|
const envFiles = (await Promise.all([...new Set(["", agentPrefix])].map(dotEnvFiles))).flat();
|
|
249
|
-
|
|
271
|
+
// Everything ACTUALLY inside the secrets dir, minus the two tracked scaffolds the image ships on
|
|
272
|
+
// purpose (they carry no values; the generated ignore re-includes them by name). Existence is the
|
|
273
|
+
// enumeration itself — readdir lists exactly what could be baked — and a hand-written ignore that
|
|
274
|
+
// misses the dir now gates NAMING the leaking file, a better diagnostic than pointing at a
|
|
275
|
+
// directory. Recurses: a subdirectory inside .secrets is unusual but its files leak all the same.
|
|
276
|
+
const secretDirFiles = async (dirRel) => {
|
|
277
|
+
const entries = await readdir(join(workspace, dirRel), { withFileTypes: true }).catch(() => []);
|
|
278
|
+
const files = [];
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
if (entry.name === ".gitignore" || entry.name === ".env.example")
|
|
281
|
+
continue;
|
|
282
|
+
if (entry.isDirectory())
|
|
283
|
+
files.push(...(await secretDirFiles(`${dirRel}/${entry.name}`)));
|
|
284
|
+
else
|
|
285
|
+
files.push(`${dirRel}/${entry.name}`);
|
|
286
|
+
}
|
|
287
|
+
return files;
|
|
288
|
+
};
|
|
289
|
+
const leakCandidates = [
|
|
290
|
+
...(secretsRel ? await secretDirFiles(secretsRel) : []),
|
|
291
|
+
...(await present(authElsewhere && authRel !== undefined ? [authRel] : [])),
|
|
292
|
+
...envFiles,
|
|
293
|
+
];
|
|
250
294
|
// Same existence rule: a node_modules that is not there cannot be uploaded.
|
|
251
295
|
const depDirs = await present([...new Set([`${agentPrefix}node_modules`, "node_modules"])]);
|
|
252
296
|
const machineryPaths = [...secretPaths, ...(stateRel ? [stateRel] : [])];
|
|
@@ -383,6 +427,7 @@ export async function preflightDeploy(input) {
|
|
|
383
427
|
longConnectionChannels,
|
|
384
428
|
hasTimeTriggers,
|
|
385
429
|
modelAuth,
|
|
430
|
+
modelKeyInDefinition,
|
|
386
431
|
authPath,
|
|
387
432
|
container,
|
|
388
433
|
port,
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -38,6 +38,9 @@ export declare function deploymentSecrets(modelAuth: string | undefined, channel
|
|
|
38
38
|
*/
|
|
39
39
|
export declare function assembleSecrets(input: {
|
|
40
40
|
modelAuth: string | undefined;
|
|
41
|
+
/** The definition carries the model key itself (a models.json literal `apiKey` / `!command`): there is
|
|
42
|
+
* no value to carry and no gate to raise — see {@link modelCredentialCarry}. */
|
|
43
|
+
modelKeyInDefinition?: boolean;
|
|
41
44
|
authFile: Buffer | undefined;
|
|
42
45
|
channels: ChannelKind[];
|
|
43
46
|
longConnectionChannels?: string[];
|
package/dist/deploy/secrets.js
CHANGED
|
@@ -65,6 +65,12 @@ export function assembleSecrets(input) {
|
|
|
65
65
|
else if (input.authFile) {
|
|
66
66
|
secrets.FASTAGENT_AUTH_SEED = input.authFile.toString("base64");
|
|
67
67
|
}
|
|
68
|
+
else if (input.modelKeyInDefinition) {
|
|
69
|
+
// The definition authenticates itself (models.json literal key, or a command run on the host), so it
|
|
70
|
+
// travels in the image with everything else. Gating here would be the worst kind of wrong: both
|
|
71
|
+
// remedies we print are impossible for such an agent — `fastagent login` has no flow for a custom
|
|
72
|
+
// provider, and there is no provider env key to set.
|
|
73
|
+
}
|
|
68
74
|
else {
|
|
69
75
|
needsModelCredential = true; // no env key, no auth.json — `fastagent login` remediation
|
|
70
76
|
}
|
package/dist/dev-supervisor.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
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, resolveStateRoot } from "./paths.js";
|
|
16
|
+
import { AGENT_CONFIG_NAMES, AGENT_MODELS_FILE, resolveStateRoot } from "./paths.js";
|
|
17
17
|
import { isUnderDir } from "./engines/pi/definition.js";
|
|
18
18
|
import { dotEnvPath } from "./env.js";
|
|
19
19
|
import { log } from "./log.js";
|
|
@@ -21,7 +21,7 @@ import { installProxyFetch } from "./proxy.js";
|
|
|
21
21
|
import { openExternalUrl } from "./open-url.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
|
-
const WATCHED_HINT = "tools/, channels/, schedules/, package.json, fastagent.config.*, .secrets/.env";
|
|
24
|
+
const WATCHED_HINT = "tools/, channels/, schedules/, package.json, fastagent.config.*, models.json, .secrets/.env";
|
|
25
25
|
/**
|
|
26
26
|
* chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. When
|
|
27
27
|
* the agent sits INSIDE the workspace that means the surrounding tree never triggers a restart at all;
|
|
@@ -49,6 +49,12 @@ export function devWatchIgnored(root, envFile) {
|
|
|
49
49
|
return false;
|
|
50
50
|
if (rel === "package.json")
|
|
51
51
|
return false;
|
|
52
|
+
// models.json is read ONCE per worker (the model hub is built during assembly), so an edit needs a
|
|
53
|
+
// restart like any other code input. It is also the one input whose breakage KILLS the worker — a
|
|
54
|
+
// malformed file fails assembly — so leaving it unwatched would strand the author: the fix that
|
|
55
|
+
// repairs it would not be the edit that restarts.
|
|
56
|
+
if (rel === AGENT_MODELS_FILE)
|
|
57
|
+
return false;
|
|
52
58
|
const segments = rel.split(sep);
|
|
53
59
|
if (segments[0] === "tools" || segments[0] === "channels" || segments[0] === "schedules")
|
|
54
60
|
return false;
|
|
@@ -7,7 +7,8 @@ import { type AnyModel, piHarnessFactory } from "./harness.ts";
|
|
|
7
7
|
import { type PiSessionStore } from "./sessions.ts";
|
|
8
8
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
9
9
|
import { type ToolCollision, type MountedTool } from "./tool.ts";
|
|
10
|
-
import { type
|
|
10
|
+
import { type SessionObserver } from "./invoke.ts";
|
|
11
|
+
import { type Lease } from "./turn-kit.ts";
|
|
11
12
|
/** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
|
|
12
13
|
* turn as the harness tool context — hence no argument here. */
|
|
13
14
|
export declare function piDefaultTools(): MountedTool[];
|
|
@@ -18,12 +18,13 @@ import { defaultAuthPath, resolveModel } from "./config.js";
|
|
|
18
18
|
import { resolveSecretsDir } from "../../paths.js";
|
|
19
19
|
import { loadAgentDefinition } from "./definition.js";
|
|
20
20
|
import { DEFAULT_THINKING_LEVEL, piHarnessFactory } from "./harness.js";
|
|
21
|
-
import { createPiModels } from "./models.js";
|
|
21
|
+
import { createPiModelRuntime, createPiModels } from "./models.js";
|
|
22
22
|
import { reportFindingsIfChanged } from "./report.js";
|
|
23
23
|
import { inMemorySessionStore } from "./sessions.js";
|
|
24
24
|
import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
|
|
25
25
|
import { withSearchTool } from "./search-tools.js";
|
|
26
|
-
import { createPiAgentFromHarness
|
|
26
|
+
import { createPiAgentFromHarness } from "./invoke.js";
|
|
27
|
+
import { inProcessLease } from "./turn-kit.js";
|
|
27
28
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
28
29
|
//
|
|
29
30
|
// The full pi toolset is the default for fidelity: authors vibe in local pi with it, so serving with
|
|
@@ -164,7 +165,7 @@ export function assembleSystemPrompt(options) {
|
|
|
164
165
|
* directory) and route through here.
|
|
165
166
|
*/
|
|
166
167
|
function buildPiAgent(opts) {
|
|
167
|
-
const models = createPiModels({ providers: opts.providers, authPath: opts.authPath });
|
|
168
|
+
const models = opts.models ?? createPiModels({ providers: opts.providers, authPath: opts.authPath });
|
|
168
169
|
const env = opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() });
|
|
169
170
|
// Materialized here (not defaulted inside createPiAgentFromHarness) so the exposed parts carry
|
|
170
171
|
// the SAME lease instance the agent runs under — boundary mutations must contend on it.
|
|
@@ -241,13 +242,17 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
241
242
|
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
242
243
|
// it; a caller's own search_tools wins).
|
|
243
244
|
const tools = withSearchTool(options.tools ?? piDefaultTools());
|
|
245
|
+
// Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
|
|
246
|
+
// passes an explicit authPath, so this only affects direct L2 callers).
|
|
247
|
+
const authPath = options.authPath ?? defaultAuthPath(resolveSecretsDir(dir));
|
|
244
248
|
const agent = buildPiAgent({
|
|
245
249
|
model: options.model,
|
|
246
250
|
thinkingLevel: options.thinkingLevel,
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
authPath: options.
|
|
251
|
+
// THE directory rung's model surface: built-ins + the agent's own models.json (custom endpoints,
|
|
252
|
+
// which are definition data and travel with the artifact) + any injected Provider instance. This
|
|
253
|
+
// is what makes `dev`/`start`/`invoke` and an embedded L2 caller resolve the same specs.
|
|
254
|
+
models: await createPiModelRuntime({ agentDir: dir, authPath, providers: options.providers }),
|
|
255
|
+
authPath,
|
|
251
256
|
// The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills
|
|
252
257
|
// edits (the author's, or the agent's own self-modification) take effect on the next turn with
|
|
253
258
|
// no process restart — restarts are reserved for code (tools/channels/config, module cache).
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
10
|
import type { ExecutionEnv, ExecutionToolContext, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
11
11
|
import type { Model, Models } from "@earendil-works/pi-ai";
|
|
12
|
-
import { type PiSessionStore } from "./sessions.ts";
|
|
12
|
+
import { type PiSessionStore, type SessionInheritance } from "./sessions.ts";
|
|
13
13
|
import { type MountedTool } from "./tool.ts";
|
|
14
14
|
import { type OverrideEntryLike } from "./session-settings.ts";
|
|
15
15
|
/**
|
|
@@ -38,7 +38,9 @@ export type AnyModel = Model<any>;
|
|
|
38
38
|
* pi's env-backed default tools read (pi 0.83). Custom tools are context-FREE and stay assignable — a
|
|
39
39
|
* four-parameter `execute` satisfies the five-parameter one, so `defineTool` is untouched by this. */
|
|
40
40
|
type PiHarness = AgentHarness<ExecutionToolContext>;
|
|
41
|
-
export type PiHarnessFactory = (session: string
|
|
41
|
+
export type PiHarnessFactory = (session: string,
|
|
42
|
+
/** Where a NEW session starts from (sessions.ts) — an existing session ignores it. */
|
|
43
|
+
inherit?: SessionInheritance) => PiHarness | Promise<PiHarness>;
|
|
42
44
|
export interface PiHarnessFactoryOptions {
|
|
43
45
|
/** Session persistence. Continuity = same backing store + same session id. */
|
|
44
46
|
sessions: PiSessionStore;
|
|
@@ -107,6 +109,7 @@ export declare function resolveHarnessOverrides(entries: OverrideEntryLike[], mo
|
|
|
107
109
|
thinkingLevel: ThinkingLevel;
|
|
108
110
|
};
|
|
109
111
|
export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: MountedTool[], sessionId: string): string[] | undefined;
|
|
110
|
-
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing →
|
|
112
|
+
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing →
|
|
113
|
+
* create, seeded from `inherit` when the scope names a parent. */
|
|
111
114
|
export declare function piHarnessFactory(options: PiHarnessFactoryOptions): PiHarnessFactory;
|
|
112
115
|
export {};
|
|
@@ -114,10 +114,11 @@ export function resolveHarnessActiveToolNames(recorded, tools, sessionId) {
|
|
|
114
114
|
}
|
|
115
115
|
return [...new Set([...initial, ...known])];
|
|
116
116
|
}
|
|
117
|
-
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing →
|
|
117
|
+
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing →
|
|
118
|
+
* create, seeded from `inherit` when the scope names a parent. */
|
|
118
119
|
export function piHarnessFactory(options) {
|
|
119
|
-
return async (sessionId) => {
|
|
120
|
-
const session = await options.sessions.openOrCreate(sessionId);
|
|
120
|
+
return async (sessionId, inherit) => {
|
|
121
|
+
const session = await options.sessions.openOrCreate(sessionId, inherit);
|
|
121
122
|
// One extra entry walk per invoke to collect the activation deltas — negligible against the model
|
|
122
123
|
// call, same trade as L2's per-invoke definition re-read. The walk is over the ACTIVE PATH, not
|
|
123
124
|
// the flat journal: `navigate` moves the leaf, so the tree can hold an abandoned branch whose
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L0 over pi-coding-agent's `AgentSession`, in the `per-invoke` state locality
|
|
3
|
+
* ([conformance-levels.md](../../../docs/design/conformance-levels.md) §2, top-right cell): build a
|
|
4
|
+
* session per invoke over the SAME durable jsonl, run one turn, dispose.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists next to {@link createPiAgentFromHarness}: pi 0.84 replaced `AgentHarness` with an
|
|
7
|
+
* unimplemented lane-based skeleton, and pi does not consume that class itself — its TUI, RPC and SDK
|
|
8
|
+
* all run on `AgentSession`. This is the executable proof that the SPEC's four Agent-side MUSTs hold
|
|
9
|
+
* on the class pi actually maintains (test/conformance-session.test.ts).
|
|
10
|
+
*
|
|
11
|
+
* SCOPE, deliberately narrow: the concurrency floor, the event stream, and cancellation. The
|
|
12
|
+
* observation plane (SessionObserver / RunControls / the rich `SessionEvent` vocabulary), the tool
|
|
13
|
+
* activation bridge, auto-compaction and session inheritance are NOT wired.
|
|
14
|
+
*
|
|
15
|
+
* WHICH L0 SERVES: {@link createPiAgentFromHarness}, still — this one is reachable only from its
|
|
16
|
+
* conformance test (deliberately absent from `src/pi.ts`), because a serving path needs the pieces
|
|
17
|
+
* above. Its one known debt is {@link toAgentEvent}: translating pi events straight to SPEC
|
|
18
|
+
* `AgentEvent`s is the second parallel translation `docs/design/session-control.md` §6 forbids. It
|
|
19
|
+
* retires the moment this L0 grows the observation plane — the rich `SessionEvent` layer comes back
|
|
20
|
+
* with it, and the harness L0 goes away.
|
|
21
|
+
*/
|
|
22
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { type Agent } from "../../agent.ts";
|
|
24
|
+
import { type Lease } from "./turn-kit.ts";
|
|
25
|
+
/** Open-or-create the session behind `sessionId` and bind an `AgentSession` to it, per invoke. */
|
|
26
|
+
export type PiAgentSessionFactory = (sessionId: string) => Promise<AgentSession>;
|
|
27
|
+
export interface CreatePiAgentFromSessionOptions {
|
|
28
|
+
sessionFactory: PiAgentSessionFactory;
|
|
29
|
+
/** Single-writer lease. Defaults to the in-process per-session fail-fast lease. */
|
|
30
|
+
lease?: Lease;
|
|
31
|
+
}
|
|
32
|
+
export declare function createPiAgentFromSession(options: CreatePiAgentFromSessionOptions): Agent;
|