@tryinget/pi-agent-registry 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -14
- package/docs/engineering.local.md +16 -7
- package/docs/project/2026-08-27-agent-registry.md +86 -12
- package/extensions/pi-agent-registry.ts +3 -0
- package/extensions/standing-agent-spawn.ts +85 -0
- package/package.json +8 -6
- package/src/visible-launch-admission.ts +69 -0
- package/src/visible-launch-bootstrap.ts +100 -0
- package/src/visible-launch-compose.ts +273 -0
- package/src/visible-launch-contract.ts +165 -0
- package/src/visible-launch-inputs.ts +57 -0
- package/src/visible-launch-receipt.ts +276 -0
- package/src/visible-launch-transport.ts +77 -0
- package/src/visible-launch.ts +482 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// summary: resolve only approved explicit entrypoints from installed local package manifests, never caller paths.
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { sha256Hex } from "./dispatch-receipt.ts";
|
|
6
|
+
|
|
7
|
+
export interface TrustedVisibleLaunchBootstrap {
|
|
8
|
+
extensions: string[];
|
|
9
|
+
bindings: { package: string; entry: string; manifestSha256: string; entrySha256: string }[];
|
|
10
|
+
/** Re-observe package manifests and entry bytes at the admission/observation boundaries. */
|
|
11
|
+
verify(): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
export type TrustedVisibleLaunchBootstrapResolver = (
|
|
14
|
+
model: string,
|
|
15
|
+
) => Promise<TrustedVisibleLaunchBootstrap | undefined>;
|
|
16
|
+
|
|
17
|
+
// Provider-extension aliases are deliberately NOT inferred. An owner-approved provider bootstrap
|
|
18
|
+
// must extend this policy, not inherit ambient extensions or accept PI_SUBAGENT_EXTENSIONS paths.
|
|
19
|
+
// Installed Pi 0.84.4 natively supplies zai/glm-5.3; no ambient provider extension is needed.
|
|
20
|
+
// Operator models.json/auth remain configured inputs, not a whole-runtime integrity claim.
|
|
21
|
+
const BUILTIN_PROVIDERS = new Set(["anthropic", "openai", "openai-codex", "google", "zai"]);
|
|
22
|
+
const APPROVED = [
|
|
23
|
+
["@tryinget/pi-peer-messaging", "extensions/intercom.ts"],
|
|
24
|
+
["@tryinget/pi-little-helpers", "extensions/session-presence.ts"],
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export const resolveTrustedVisibleLaunchBootstrap: TrustedVisibleLaunchBootstrapResolver = async (
|
|
28
|
+
model,
|
|
29
|
+
) => {
|
|
30
|
+
if (!BUILTIN_PROVIDERS.has(model.split("/")[0]) || !model.includes("/")) return undefined;
|
|
31
|
+
const configured = process.env.PI_CODING_AGENT_DIR;
|
|
32
|
+
const agentDir = configured
|
|
33
|
+
? resolve(configured === "~" ? homedir() : configured)
|
|
34
|
+
: join(homedir(), ".pi", "agent");
|
|
35
|
+
try {
|
|
36
|
+
const settings = JSON.parse(await readFile(join(agentDir, "settings.json"), "utf8"));
|
|
37
|
+
if (!Array.isArray(settings.packages)) return undefined;
|
|
38
|
+
const bindings: TrustedVisibleLaunchBootstrap["bindings"] = [];
|
|
39
|
+
const captured: { path: string; sha256: string }[] = [];
|
|
40
|
+
for (const [name, entry] of APPROVED) {
|
|
41
|
+
const matches: { root: string; manifest: Buffer }[] = [];
|
|
42
|
+
for (const installed of settings.packages) {
|
|
43
|
+
const source = typeof installed === "string" ? installed : installed?.source;
|
|
44
|
+
if (
|
|
45
|
+
typeof source !== "string" ||
|
|
46
|
+
!(isAbsolute(source) || /^\.\.?\//u.test(source) || source.startsWith("~/"))
|
|
47
|
+
)
|
|
48
|
+
continue;
|
|
49
|
+
const root = await realpath(
|
|
50
|
+
source.startsWith("~/") ? join(homedir(), source.slice(2)) : resolve(agentDir, source),
|
|
51
|
+
).catch(() => undefined);
|
|
52
|
+
if (!root) continue;
|
|
53
|
+
const manifest = await readFile(join(root, "package.json")).catch(() => undefined);
|
|
54
|
+
if (!manifest) continue;
|
|
55
|
+
const parsed = JSON.parse(manifest.toString("utf8"));
|
|
56
|
+
if (parsed.name !== name) continue;
|
|
57
|
+
if (
|
|
58
|
+
!Array.isArray(parsed.pi?.extensions) ||
|
|
59
|
+
!parsed.pi.extensions.some((p: unknown) => p === entry || p === `./${entry}`)
|
|
60
|
+
)
|
|
61
|
+
return undefined;
|
|
62
|
+
// Complex/glob settings filters have no approval proof in this slice.
|
|
63
|
+
if (
|
|
64
|
+
typeof installed === "object" &&
|
|
65
|
+
(installed.autoload === false || installed.extensions !== undefined)
|
|
66
|
+
)
|
|
67
|
+
return undefined;
|
|
68
|
+
matches.push({ root, manifest });
|
|
69
|
+
}
|
|
70
|
+
if (matches.length !== 1) return undefined;
|
|
71
|
+
const { root, manifest } = matches[0];
|
|
72
|
+
const path = await realpath(join(root, entry));
|
|
73
|
+
if (relative(root, path).startsWith("..") || !(await stat(path)).isFile()) return undefined;
|
|
74
|
+
const entrySha256 = sha256Hex(await readFile(path));
|
|
75
|
+
const manifestSha256 = sha256Hex(manifest);
|
|
76
|
+
bindings.push({ package: name, entry: path, manifestSha256, entrySha256 });
|
|
77
|
+
captured.push(
|
|
78
|
+
{ path: join(root, "package.json"), sha256: manifestSha256 },
|
|
79
|
+
{ path, sha256: entrySha256 },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
extensions: bindings.map((b) => b.entry),
|
|
84
|
+
bindings,
|
|
85
|
+
async verify() {
|
|
86
|
+
try {
|
|
87
|
+
return (
|
|
88
|
+
await Promise.all(
|
|
89
|
+
captured.map(async (file) => sha256Hex(await readFile(file.path)) === file.sha256),
|
|
90
|
+
)
|
|
91
|
+
).every(Boolean);
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: pure Fleet Phase-3 composition: standing-agent child argv, boot prompt with ACK protocol, and title.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing how a visible standing agent is composed from its manifest, or the ACK/boot prompt literals.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import type { AgentManifest } from "./manifest.ts";
|
|
9
|
+
import type { ResolvedAgentLaunch } from "./registry.ts";
|
|
10
|
+
import {
|
|
11
|
+
READ_ONLY_VISIBLE_LAUNCH_TOOLS,
|
|
12
|
+
VISIBLE_LAUNCH_ACK_TOOL,
|
|
13
|
+
VISIBLE_LAUNCH_SYSTEM_PROMPT_ARGV_LIMIT,
|
|
14
|
+
type VisibleLaunchReportBack,
|
|
15
|
+
} from "./visible-launch-contract.ts";
|
|
16
|
+
|
|
17
|
+
const EXACT_SESSION_ID_PATTERN =
|
|
18
|
+
/^session-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
19
|
+
|
|
20
|
+
const AMBIGUOUS_PARENT_PEER_TARGETS = new Set([
|
|
21
|
+
"active",
|
|
22
|
+
"controller",
|
|
23
|
+
"current",
|
|
24
|
+
"here",
|
|
25
|
+
"me",
|
|
26
|
+
"parent",
|
|
27
|
+
"self",
|
|
28
|
+
"this",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export type ParentTargetCheck =
|
|
32
|
+
| { ok: true; target: string }
|
|
33
|
+
| { ok: false; reason: "missing" | "ambiguous" | "not_exact_session_id"; target?: string };
|
|
34
|
+
|
|
35
|
+
/** Exact controller session id required for intercom report-back (peer-messaging target contract). */
|
|
36
|
+
export function checkParentPeerTarget(value: string | undefined): ParentTargetCheck {
|
|
37
|
+
const target = value?.trim();
|
|
38
|
+
if (!target) return { ok: false, reason: "missing" };
|
|
39
|
+
if (AMBIGUOUS_PARENT_PEER_TARGETS.has(target.toLowerCase())) {
|
|
40
|
+
return { ok: false, reason: "ambiguous", target };
|
|
41
|
+
}
|
|
42
|
+
if (!EXACT_SESSION_ID_PATTERN.test(target)) {
|
|
43
|
+
return { ok: false, reason: "not_exact_session_id", target };
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, target };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createStandingAgentRunId(): string {
|
|
49
|
+
return `standingagent-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Standing-agent tab/window title base. */
|
|
53
|
+
export function standingAgentTitle(manifest: AgentManifest): string {
|
|
54
|
+
const label = manifest.display_name ?? manifest.name;
|
|
55
|
+
return `Standing: ${label} (${manifest.name})`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Boot/report-back instructions mirroring the peer PEER_ACK/PEER_FINAL protocol literals. */
|
|
59
|
+
export function standingAgentBootInstructions({
|
|
60
|
+
reportBack,
|
|
61
|
+
parentPeerTarget,
|
|
62
|
+
runId,
|
|
63
|
+
agentName,
|
|
64
|
+
}: {
|
|
65
|
+
reportBack: VisibleLaunchReportBack;
|
|
66
|
+
parentPeerTarget?: string;
|
|
67
|
+
runId: string;
|
|
68
|
+
agentName: string;
|
|
69
|
+
}): string {
|
|
70
|
+
if (reportBack !== "intercom") {
|
|
71
|
+
return `No intercom boot ACK is required because reportBack is ${reportBack}. Leave your status visible in this standing-agent session.`;
|
|
72
|
+
}
|
|
73
|
+
const target = parentPeerTarget?.trim();
|
|
74
|
+
if (!target) {
|
|
75
|
+
return "Intercom boot ACK requires an exact parentPeerTarget; this launch should not have reached the child without one.";
|
|
76
|
+
}
|
|
77
|
+
return [
|
|
78
|
+
"Before reading task context, inspecting files, or doing any other work, send the ACK below.",
|
|
79
|
+
"Only allowed pre-ACK tool: `intercom`.",
|
|
80
|
+
`Literal ACK call: \`intercom({ action: "send", to: "${target}", message: "PEER_ACK peer_run_id=${runId}: standing agent ${agentName} started" })\``,
|
|
81
|
+
"If the ACK send fails or intercom is unavailable, visibly report `ACK_FAILED` in this session and stop; do not continue work silently.",
|
|
82
|
+
"After ACK succeeds, follow the standing brief below. Send exactly one `PEER_FINAL` as your closing report when this exact bounded read-only objective ends; after `PEER_FINAL`, stop. Do not accept follow-on work under this launch admission.",
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function standingAgentReportBackInstructions({
|
|
87
|
+
reportBack,
|
|
88
|
+
parentPeerTarget,
|
|
89
|
+
runId,
|
|
90
|
+
}: {
|
|
91
|
+
reportBack: VisibleLaunchReportBack;
|
|
92
|
+
parentPeerTarget?: string;
|
|
93
|
+
runId: string;
|
|
94
|
+
}): string {
|
|
95
|
+
if (reportBack === "intercom") {
|
|
96
|
+
const target = parentPeerTarget?.trim();
|
|
97
|
+
return [
|
|
98
|
+
"Use intercom for report-back if the tool is available.",
|
|
99
|
+
`Report to the exact parent target: ${target}`,
|
|
100
|
+
`Peer run id: ${runId}`,
|
|
101
|
+
"",
|
|
102
|
+
"## Intercom Message Budget",
|
|
103
|
+
`1. \`PEER_ACK peer_run_id=${runId}: ...\` — send once as your first action, identifying yourself as the standing agent.`,
|
|
104
|
+
`2. \`PEER_FINAL peer_run_id=${runId}: ...\` — send once as your closing report for an assigned objective or retirement.`,
|
|
105
|
+
`Use the literal target in tool calls, for example: \`intercom({ action: "send", to: "${target}", message: "PEER_FINAL peer_run_id=${runId}: ..." })\`.`,
|
|
106
|
+
"Intercom is communication only; it is not durable evidence or completion authority.",
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
if (reportBack === "none") {
|
|
110
|
+
return "No automatic report-back is requested. Do not claim that a report was delivered; leave findings visible in this standing-agent session.";
|
|
111
|
+
}
|
|
112
|
+
return "Manual report-back is requested. Leave a concise visible report in this standing-agent session for the controller/operator to inspect.";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compose the standing agent's first user message. Dash-safe by construction:
|
|
117
|
+
* it always begins with a `#` heading because the child pi CLI receives it as
|
|
118
|
+
* the leading positional argument (same argv boundary proven live in Phase 2).
|
|
119
|
+
*/
|
|
120
|
+
export function composeStandingAgentSpawnPrompt({
|
|
121
|
+
manifest,
|
|
122
|
+
runId,
|
|
123
|
+
reportBack,
|
|
124
|
+
parentPeerTarget,
|
|
125
|
+
objective,
|
|
126
|
+
task,
|
|
127
|
+
cwd,
|
|
128
|
+
}: {
|
|
129
|
+
manifest: AgentManifest;
|
|
130
|
+
runId: string;
|
|
131
|
+
reportBack: VisibleLaunchReportBack;
|
|
132
|
+
parentPeerTarget?: string;
|
|
133
|
+
objective: string;
|
|
134
|
+
task: number;
|
|
135
|
+
cwd: string;
|
|
136
|
+
}): string {
|
|
137
|
+
const brief = objective.trim();
|
|
138
|
+
return [
|
|
139
|
+
`# Standing agent launch: ${manifest.name} (Fleet Phase 3, clean visible session)`,
|
|
140
|
+
"",
|
|
141
|
+
`You are the standing agent \`${manifest.name}\`, now launched visibly in your own clean Pi session. Your system prompt is your persona and advisory operating territory; this message is your launch brief, not a replacement identity. You are parallel cognition, not parallel authority.`,
|
|
142
|
+
"",
|
|
143
|
+
"## BOOT PROTOCOL / FIRST ACTION REQUIRED",
|
|
144
|
+
standingAgentBootInstructions({
|
|
145
|
+
reportBack,
|
|
146
|
+
parentPeerTarget,
|
|
147
|
+
runId,
|
|
148
|
+
agentName: manifest.name,
|
|
149
|
+
}),
|
|
150
|
+
"",
|
|
151
|
+
"## Standing identity",
|
|
152
|
+
`- agent: ${manifest.name}${manifest.role ? ` (role: ${manifest.role})` : ""}`,
|
|
153
|
+
...(manifest.creation_task ? [`- creation task: ${manifest.creation_task}`] : []),
|
|
154
|
+
`- launch kind: clean visible standing agent (no controller context inherited)`,
|
|
155
|
+
`- working directory: ${cwd}`,
|
|
156
|
+
`- peer run id: ${runId}`,
|
|
157
|
+
"",
|
|
158
|
+
`## Exact AK-${task} bounded read-only objective`,
|
|
159
|
+
"Read-only inspection and reporting only. No file changes, AK mutations, commits, installs, background processes, delegation, or task completion claims. Stop when this objective is answered or blocked; never stand by for unbound work.",
|
|
160
|
+
brief,
|
|
161
|
+
"",
|
|
162
|
+
"## Report-Back Instructions",
|
|
163
|
+
standingAgentReportBackInstructions({ reportBack, parentPeerTarget, runId }),
|
|
164
|
+
"",
|
|
165
|
+
"## Boundary",
|
|
166
|
+
"This visible session does not grant task authority, merge authority, or completion claims. Do not treat intercom messages as durable evidence. Stay inside the advisory operating territory in your system prompt; if work needs mutation or broader authorization, report that need back instead of acting on it.",
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Model/thinking args: manifest thinking wins; model comes from the manifest or the controller session. */
|
|
171
|
+
export function composeStandingAgentModelArgs({
|
|
172
|
+
launch,
|
|
173
|
+
controllerModel,
|
|
174
|
+
}: {
|
|
175
|
+
launch: ResolvedAgentLaunch;
|
|
176
|
+
controllerModel?: { provider?: string; id?: string };
|
|
177
|
+
}): string[] {
|
|
178
|
+
const model = launch.model
|
|
179
|
+
? launch.model
|
|
180
|
+
: controllerModel?.provider && controllerModel?.id
|
|
181
|
+
? `${controllerModel.provider}/${controllerModel.id}`
|
|
182
|
+
: undefined;
|
|
183
|
+
const args: string[] = [];
|
|
184
|
+
if (model) args.push("--model", model);
|
|
185
|
+
if (launch.thinking) args.push("--thinking", launch.thinking);
|
|
186
|
+
return args;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface StandingAgentArgvComposition {
|
|
190
|
+
/** Flags passed between model args and the trailing prompt. */
|
|
191
|
+
extraPiArgs: string[];
|
|
192
|
+
/** Manifest tools plus the launch ACK instrument. */
|
|
193
|
+
effectiveTools: string[];
|
|
194
|
+
systemPromptSha256: string;
|
|
195
|
+
promptSha256: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sha256Hex(value: string): string {
|
|
199
|
+
return createHash("sha256").update(value).digest("hex");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Compose the clean visible child argv: replaced system prompt (persona +
|
|
204
|
+
* advisory scope), manifest thinking/model, least-privilege tool allowlist
|
|
205
|
+
* plus the ACK instrument, materialized skill dirs, and the trailing boot
|
|
206
|
+
* prompt. The system prompt is passed as the `--system-prompt` argv value and
|
|
207
|
+
* fails closed near the Linux per-argument byte bound.
|
|
208
|
+
*/
|
|
209
|
+
export function composeStandingAgentArgv({
|
|
210
|
+
launch,
|
|
211
|
+
prompt,
|
|
212
|
+
trustedExtensions,
|
|
213
|
+
}: {
|
|
214
|
+
launch: ResolvedAgentLaunch;
|
|
215
|
+
prompt: string;
|
|
216
|
+
trustedExtensions: readonly string[];
|
|
217
|
+
}): StandingAgentArgvComposition {
|
|
218
|
+
const declaredTools = launch.tools
|
|
219
|
+
.split(",")
|
|
220
|
+
.map((tool) => tool.trim())
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
const effectiveTools = [...declaredTools, VISIBLE_LAUNCH_ACK_TOOL];
|
|
223
|
+
const extraPiArgs: string[] = [
|
|
224
|
+
"--offline",
|
|
225
|
+
"--no-extensions",
|
|
226
|
+
"--no-skills",
|
|
227
|
+
"--no-prompt-templates",
|
|
228
|
+
"--system-prompt",
|
|
229
|
+
launch.systemPrompt,
|
|
230
|
+
"--tools",
|
|
231
|
+
effectiveTools.join(","),
|
|
232
|
+
];
|
|
233
|
+
for (const extension of trustedExtensions) extraPiArgs.push("--extension", extension);
|
|
234
|
+
for (const skillDir of launch.skillDirs) {
|
|
235
|
+
extraPiArgs.push("--skill", skillDir);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
extraPiArgs,
|
|
239
|
+
effectiveTools,
|
|
240
|
+
systemPromptSha256: sha256Hex(launch.systemPrompt),
|
|
241
|
+
promptSha256: sha256Hex(prompt),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Size gate for the composed system prompt as one argv value. */
|
|
246
|
+
export function systemPromptWithinArgvBound(systemPrompt: string): boolean {
|
|
247
|
+
// A small margin below MAX_ARG_STRLEN for NUL-termination accounting.
|
|
248
|
+
return (
|
|
249
|
+
!systemPrompt.includes("\0") &&
|
|
250
|
+
Buffer.from(systemPrompt, "utf8").toString("utf8") === systemPrompt &&
|
|
251
|
+
Buffer.byteLength(systemPrompt, "utf8") < VISIBLE_LAUNCH_SYSTEM_PROMPT_ARGV_LIMIT - 1
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Shape-only argv projection for receipts: flag names with values redacted to digests/counts. */
|
|
256
|
+
export function redactArgvForReceipt(extraPiArgs: string[]): string[] {
|
|
257
|
+
const projection: string[] = [];
|
|
258
|
+
for (let index = 0; index < extraPiArgs.length; index += 1) {
|
|
259
|
+
const flag = extraPiArgs[index];
|
|
260
|
+
projection.push(flag);
|
|
261
|
+
if (!["--offline", "--no-extensions", "--no-skills", "--no-prompt-templates"].includes(flag))
|
|
262
|
+
index += 1;
|
|
263
|
+
}
|
|
264
|
+
return projection;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Declared-tool gate helper shared by the pipeline and tests. */
|
|
268
|
+
export function manifestToolsAreLaunchEligible(manifest: AgentManifest): boolean {
|
|
269
|
+
return (
|
|
270
|
+
manifest.tools.length > 0 &&
|
|
271
|
+
manifest.tools.every((tool) => READ_ONLY_VISIBLE_LAUNCH_TOOLS.includes(tool))
|
|
272
|
+
);
|
|
273
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: Fleet Phase-3 clean visible standing-agent launch contract constants and types.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing the standing_agent_spawn Phase-3 gates, ACK instrument policy, or receipt/evidence semantics.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import type { AkAuthorizationFailureCode } from "./dispatch-authorization.ts";
|
|
8
|
+
import type { AgentRegistry } from "./registry.ts";
|
|
9
|
+
import type { TrustedVisibleLaunchBootstrapResolver } from "./visible-launch-bootstrap.ts";
|
|
10
|
+
import type {
|
|
11
|
+
VisibleLaunchReceipt,
|
|
12
|
+
writeImmutableVisibleLaunchReceipt,
|
|
13
|
+
} from "./visible-launch-receipt.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fleet Phase 3 adds exactly one bounded surface: launching one registered
|
|
17
|
+
* standing agent as a CLEAN VISIBLE Pi TUI session in a Ghostty tab/window,
|
|
18
|
+
* composed from the fleet manifest, ACKing the controller through intercom,
|
|
19
|
+
* and recorded as one write-once launch receipt. It is an operator-surface
|
|
20
|
+
* capability, not ASC execution and not task authority: dispatch_agent keeps
|
|
21
|
+
* owning exact-task read-only dispatch, and AK keeps owning task/evidence
|
|
22
|
+
* truth. The launch does not authenticate the caller as any AK claimant.
|
|
23
|
+
*/
|
|
24
|
+
export const VISIBLE_LAUNCH_PHASE = "fleet_phase_3" as const;
|
|
25
|
+
|
|
26
|
+
export const VISIBLE_LAUNCH_RECEIPT_SCHEMA = "pi-agent-registry.visible-launch-receipt/1" as const;
|
|
27
|
+
|
|
28
|
+
export const VISIBLE_LAUNCH_EVIDENCE_CHECK_TYPE = "standing-agent-visible-launch" as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Declared-tool gate for a visible standing-agent launch. Mirrors Phase-2
|
|
32
|
+
* read-only posture: `bash` is admitted only as the fleet's established
|
|
33
|
+
* read-only exploration instrument; a visible standing agent with mutation
|
|
34
|
+
* tools is a later fleet phase, not this contract.
|
|
35
|
+
*/
|
|
36
|
+
export const READ_ONLY_VISIBLE_LAUNCH_TOOLS: readonly string[] = ["read", "bash"];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Report-back instrument added by the launch envelope. It is not part of the
|
|
40
|
+
* agent's declared manifest toolset (which stays the agent's own authority
|
|
41
|
+
* surface); the receipt records declared and effective tools separately so
|
|
42
|
+
* the ACK instrument never silently widens agent authority.
|
|
43
|
+
*/
|
|
44
|
+
export const VISIBLE_LAUNCH_ACK_TOOL = "intercom" as const;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Provenance marker exported into every visibly launched standing-agent
|
|
48
|
+
* child. `standing_agent_spawn` refuses to run inside a session that already
|
|
49
|
+
* carries it, keeping visible standing-agent launches one level deep.
|
|
50
|
+
*/
|
|
51
|
+
export const VISIBLE_LAUNCH_CHILD_PROVENANCE_ENV =
|
|
52
|
+
"PI_PROVENANCE_STANDING_AGENT_VISIBLE_LAUNCH" as const;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Linux MAX_ARG_STRLEN (32 pages) bounds one argv entry; a composed system
|
|
56
|
+
* prompt at or above this bound cannot be passed as `--system-prompt` argv
|
|
57
|
+
* and fails closed instead of silently truncating the agent persona.
|
|
58
|
+
*/
|
|
59
|
+
export const VISIBLE_LAUNCH_SYSTEM_PROMPT_ARGV_LIMIT = 131_072;
|
|
60
|
+
|
|
61
|
+
export type VisibleLaunchReportBack = "intercom" | "manual" | "none";
|
|
62
|
+
|
|
63
|
+
export type VisibleLaunchFailureReason =
|
|
64
|
+
| AkAuthorizationFailureCode
|
|
65
|
+
| "parent_repo_unobservable"
|
|
66
|
+
| "bootstrap_unavailable"
|
|
67
|
+
| "manifest_extensions_unapproved"
|
|
68
|
+
| "launch_already_reserved"
|
|
69
|
+
| "reservation_failed"
|
|
70
|
+
| "cancelled"
|
|
71
|
+
| "invalid_argv"
|
|
72
|
+
| "invalid_request"
|
|
73
|
+
| "invalid_parent_peer_target"
|
|
74
|
+
| "recursive_launch"
|
|
75
|
+
| "visible_transport_unavailable"
|
|
76
|
+
| "unknown_agent"
|
|
77
|
+
| "agent_not_read_only"
|
|
78
|
+
| "agent_repo_dirty"
|
|
79
|
+
| "agent_repo_drift"
|
|
80
|
+
| "agent_resolution_failed"
|
|
81
|
+
| "system_prompt_too_large"
|
|
82
|
+
| "launch_failed"
|
|
83
|
+
| "launch_indeterminate"
|
|
84
|
+
| "receipt_write_failed";
|
|
85
|
+
|
|
86
|
+
export interface StandingAgentSpawnRequest {
|
|
87
|
+
/** Registered standing-agent name (agent.json `name`). */
|
|
88
|
+
agent: string;
|
|
89
|
+
/** Exact claimed AK task in the origin repository. */
|
|
90
|
+
task: number;
|
|
91
|
+
/** Nonblank bounded read-only objective; never a standby session. */
|
|
92
|
+
objective: string;
|
|
93
|
+
/** Report-back mode; intercom (default) requires an exact parent session id. */
|
|
94
|
+
reportBack?: VisibleLaunchReportBack;
|
|
95
|
+
/** Exact controller session id receiving PEER_ACK/PEER_FINAL. */
|
|
96
|
+
parentPeerTarget?: string;
|
|
97
|
+
/** Working directory for the child (default: the origin repository). */
|
|
98
|
+
cwd?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Structural shape of the little-helpers visible transport seam. */
|
|
102
|
+
export interface VisibleLaunchTransport {
|
|
103
|
+
launchPiQuestSession: (request: {
|
|
104
|
+
pi: unknown;
|
|
105
|
+
ctx: { model?: unknown; cwd?: string };
|
|
106
|
+
options?: Record<string, unknown>;
|
|
107
|
+
defaultPiBin: string;
|
|
108
|
+
prompt: string;
|
|
109
|
+
titlePrompt: string;
|
|
110
|
+
cwd: string;
|
|
111
|
+
titlePrefix?: string;
|
|
112
|
+
modelArgs?: string[];
|
|
113
|
+
extraPiArgs?: string[];
|
|
114
|
+
childProvenanceEnv?: Record<string, string>;
|
|
115
|
+
signal?: AbortSignal;
|
|
116
|
+
}) => Promise<{
|
|
117
|
+
ok: boolean;
|
|
118
|
+
effectDisposition: string;
|
|
119
|
+
launchMode: string;
|
|
120
|
+
sessionMode: string;
|
|
121
|
+
cwd: string;
|
|
122
|
+
titleBase: string;
|
|
123
|
+
promptSummary: string;
|
|
124
|
+
launchNote?: string;
|
|
125
|
+
failure?: string;
|
|
126
|
+
}>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface StandingAgentSpawnDeps {
|
|
130
|
+
registry: AgentRegistry;
|
|
131
|
+
pi: unknown;
|
|
132
|
+
/** Test-only transport substitution; null forces unavailable. Never a tool parameter. */
|
|
133
|
+
transport?: VisibleLaunchTransport | null;
|
|
134
|
+
receiptsDir?: string;
|
|
135
|
+
akBinary?: string;
|
|
136
|
+
/** Trusted runtime dependency, not caller-supplied extension paths. */
|
|
137
|
+
resolveTrustedBootstrap?: TrustedVisibleLaunchBootstrapResolver;
|
|
138
|
+
/** Test seam for publication failures after transport admission. */
|
|
139
|
+
writeReceipt?: typeof writeImmutableVisibleLaunchReceipt;
|
|
140
|
+
}
|
|
141
|
+
export interface VisibleLaunchCtx {
|
|
142
|
+
cwd: string;
|
|
143
|
+
model?: { provider?: string; id?: string };
|
|
144
|
+
}
|
|
145
|
+
export interface StandingAgentSpawnSuccess {
|
|
146
|
+
ok: true;
|
|
147
|
+
phase: typeof VISIBLE_LAUNCH_PHASE;
|
|
148
|
+
admission: "transport_admitted";
|
|
149
|
+
receipt: VisibleLaunchReceipt;
|
|
150
|
+
receiptPath: string;
|
|
151
|
+
runId: string;
|
|
152
|
+
launchMode: string;
|
|
153
|
+
}
|
|
154
|
+
export interface StandingAgentSpawnFailure {
|
|
155
|
+
ok: false;
|
|
156
|
+
phase: typeof VISIBLE_LAUNCH_PHASE;
|
|
157
|
+
reason: VisibleLaunchFailureReason;
|
|
158
|
+
message: string;
|
|
159
|
+
effectDisposition: "confirmed_no_effects" | "settled" | "effect_indeterminate";
|
|
160
|
+
spawnAttempted: boolean;
|
|
161
|
+
runId?: string;
|
|
162
|
+
receipt?: VisibleLaunchReceipt;
|
|
163
|
+
receiptPath?: string;
|
|
164
|
+
}
|
|
165
|
+
export type StandingAgentSpawnOutcome = StandingAgentSpawnSuccess | StandingAgentSpawnFailure;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// summary: compare cached, freshly parsed, committed and worktree persona inputs without altering Phase 2.
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { canonicalJsonString } from "./dispatch-receipt.ts";
|
|
5
|
+
import { knownEcProfiles } from "./ec-profiles.ts";
|
|
6
|
+
import type { FleetGitSnapshot } from "./fleet-git-snapshot.ts";
|
|
7
|
+
import {
|
|
8
|
+
type AgentManifest,
|
|
9
|
+
loadAgentManifest,
|
|
10
|
+
readAgentSystemPrompt,
|
|
11
|
+
validateAgentManifest,
|
|
12
|
+
} from "./manifest.ts";
|
|
13
|
+
import { type AgentRegistry, type ResolvedAgentLaunch, renderScopeSection } from "./registry.ts";
|
|
14
|
+
|
|
15
|
+
export async function verifyVisibleLaunchInputs(
|
|
16
|
+
manifest: AgentManifest,
|
|
17
|
+
registry: AgentRegistry,
|
|
18
|
+
snapshot: FleetGitSnapshot,
|
|
19
|
+
launch?: ResolvedAgentLaunch,
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
try {
|
|
22
|
+
const committed = await snapshot.readFile("agent.json", 64 * 1024);
|
|
23
|
+
const persona = await snapshot.readFile(manifest.system_prompt_file, 512 * 1024);
|
|
24
|
+
if (!committed || !persona) return false;
|
|
25
|
+
const options = { ecProfiles: knownEcProfiles(registry.ec) };
|
|
26
|
+
const parsed = validateAgentManifest(
|
|
27
|
+
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(committed.bytes)),
|
|
28
|
+
snapshot.root,
|
|
29
|
+
join(snapshot.root, "agent.json"),
|
|
30
|
+
options,
|
|
31
|
+
);
|
|
32
|
+
const fresh = await loadAgentManifest(snapshot.root, options);
|
|
33
|
+
const normalized = canonicalJsonString(parsed);
|
|
34
|
+
if (
|
|
35
|
+
normalized !== canonicalJsonString(manifest) ||
|
|
36
|
+
normalized !== canonicalJsonString(fresh) ||
|
|
37
|
+
normalized !== canonicalJsonString(registry.get(manifest.name))
|
|
38
|
+
)
|
|
39
|
+
return false;
|
|
40
|
+
if (!(await readFile(parsed.manifestPath)).equals(committed.bytes)) return false;
|
|
41
|
+
const prompt = await readAgentSystemPrompt(parsed);
|
|
42
|
+
if (!Buffer.from(prompt, "utf8").equals(persona.bytes)) return false;
|
|
43
|
+
const scope = renderScopeSection(parsed);
|
|
44
|
+
const composed = scope ? `${prompt.replace(/\s+$/u, "")}\n\n---\n\n${scope}` : prompt;
|
|
45
|
+
return (
|
|
46
|
+
!launch ||
|
|
47
|
+
(launch.systemPrompt === composed &&
|
|
48
|
+
launch.name === parsed.name &&
|
|
49
|
+
launch.tools === parsed.tools.join(",") &&
|
|
50
|
+
launch.model === parsed.defaults.model &&
|
|
51
|
+
launch.thinking === parsed.defaults.thinking &&
|
|
52
|
+
launch.extensions.length === 0)
|
|
53
|
+
);
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|