@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.
@@ -0,0 +1,276 @@
1
+ // ---
2
+ // summary: write-once Fleet Phase-3 visible standing-agent launch receipts with canonical digests.
3
+ // read_when:
4
+ // - changing visible-launch receipt identity, immutability mechanics, or verification semantics.
5
+ // ---
6
+
7
+ import { randomUUID } from "node:crypto";
8
+ import * as fs from "node:fs";
9
+ import { mkdir, realpath } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import type { AkTaskSnapshot } from "./dispatch-contract.ts";
13
+ import { canonicalJsonString, sha256Hex } from "./dispatch-receipt.ts";
14
+ import type { TrustedVisibleLaunchBootstrap } from "./visible-launch-bootstrap.ts";
15
+ import { VISIBLE_LAUNCH_RECEIPT_SCHEMA } from "./visible-launch-contract.ts";
16
+
17
+ /** Minimal registry-owned agent-dir resolution for the receipts home (honors PI_CODING_AGENT_DIR). */
18
+ function resolvePiAgentDir(): string {
19
+ const configured = process.env.PI_CODING_AGENT_DIR?.trim();
20
+ if (configured) {
21
+ return configured === "~" ? homedir() : configured;
22
+ }
23
+ return join(homedir(), ".pi", "agent");
24
+ }
25
+
26
+ export const VISIBLE_LAUNCH_RECEIPTS_DIR_ENV = "PI_AGENT_REGISTRY_VISIBLE_LAUNCH_RECEIPTS_DIR";
27
+
28
+ export class VisibleLaunchReceiptError extends Error {
29
+ constructor(message: string) {
30
+ super(message);
31
+ this.name = "VisibleLaunchReceiptError";
32
+ }
33
+ }
34
+
35
+ export interface VisibleLaunchReceipt {
36
+ schema: typeof VISIBLE_LAUNCH_RECEIPT_SCHEMA;
37
+ phase: "fleet_phase_3";
38
+ agent: {
39
+ name: string;
40
+ role?: string;
41
+ creation_task?: string;
42
+ declaredTools: string[];
43
+ effectiveTools: string[];
44
+ thinking: string;
45
+ model: string | null;
46
+ skillProfile?: string;
47
+ loadedSkills: string[];
48
+ manifestSha256: string;
49
+ manifestBlobOid: string;
50
+ systemPromptSha256: string;
51
+ systemPromptBlobOid: string;
52
+ composedSystemPromptSha256: string;
53
+ agentRepo: {
54
+ commit: string;
55
+ treeOid: string;
56
+ statusSha256: string;
57
+ };
58
+ };
59
+ task: AkTaskSnapshot;
60
+ bootstrap: TrustedVisibleLaunchBootstrap["bindings"];
61
+ observation: {
62
+ agentRevisionStable: boolean;
63
+ originRevisionStable: boolean;
64
+ bootstrapStable: boolean;
65
+ inputsStable: boolean;
66
+ parentRepoRoot: string;
67
+ parentCommit: string;
68
+ parentStatusSha256: string;
69
+ boundary: string;
70
+ };
71
+ launch: {
72
+ runId: string;
73
+ sessionMode: "clean";
74
+ objective: string;
75
+ mutationPolicy: "read_only";
76
+ composedArgvSha256: string;
77
+ admission: "transport_admitted" | "not_admitted" | "unproven";
78
+ sessionStarted: "unproven";
79
+ ack: "unproven";
80
+ taskCompletion: "unproven";
81
+ objectiveSha256: string;
82
+ reportBack: "intercom" | "manual" | "none";
83
+ parentPeerTarget?: string;
84
+ cwd: string;
85
+ title: string;
86
+ promptSha256: string;
87
+ argvFlags: string[];
88
+ argvCount: number;
89
+ skillDirCount: number;
90
+ };
91
+ transport: {
92
+ owner: "pi-little-helpers";
93
+ launchMode: string;
94
+ effectDisposition: string;
95
+ ok: boolean;
96
+ failure?: string;
97
+ launchNote?: string;
98
+ };
99
+ recordedAt: string;
100
+ receiptSha256: string;
101
+ }
102
+
103
+ /** Digest of the receipt over its canonical form without the `receiptSha256` field. */
104
+ export function computeVisibleLaunchReceiptSha256(
105
+ receipt: Omit<VisibleLaunchReceipt, "receiptSha256">,
106
+ ): string {
107
+ const { receiptSha256: _omitted, ...rest } = receipt as VisibleLaunchReceipt;
108
+ return sha256Hex(canonicalJsonString(rest));
109
+ }
110
+
111
+ export function validateVisibleLaunchIdentity(agent: string, runId: string): void {
112
+ if (
113
+ !/^[a-z][a-z0-9-]{0,63}$/u.test(agent) ||
114
+ !/^standingagent-[a-z0-9]{1,32}-[a-f0-9]{8}$/u.test(runId)
115
+ ) {
116
+ throw new VisibleLaunchReceiptError("Invalid visible launch agent/run identity");
117
+ }
118
+ }
119
+
120
+ export function visibleLaunchReceiptFileName(agent: string, stamp: string, nonce: string): string {
121
+ if (
122
+ !/^[a-z][a-z0-9-]{0,63}$/u.test(agent) ||
123
+ !/^\d{8}T\d{6}Z$/u.test(stamp) ||
124
+ !/^[a-f0-9]{8}$/u.test(nonce)
125
+ ) {
126
+ throw new VisibleLaunchReceiptError("Unsafe visible launch receipt filename");
127
+ }
128
+ return `visible-${agent}.${stamp}.${nonce}.launch-receipt.json`;
129
+ }
130
+
131
+ /** UTC compact stamp (yyyymmddThhmmssZ) for receipt file names. */
132
+ export function visibleLaunchStamp(date = new Date()): string {
133
+ return `${date.toISOString().replaceAll(/[-:]/gu, "").split(".")[0]}Z`;
134
+ }
135
+
136
+ export function resolveVisibleLaunchReceiptsDir(explicit?: string): string {
137
+ const configured = explicit ?? process.env[VISIBLE_LAUNCH_RECEIPTS_DIR_ENV]?.trim();
138
+ if (configured) {
139
+ return configured;
140
+ }
141
+ return join(resolvePiAgentDir(), "visible-launch-receipts");
142
+ }
143
+
144
+ export interface WrittenVisibleLaunchReceipt {
145
+ receipt: VisibleLaunchReceipt;
146
+ receiptPath: string;
147
+ receiptSha256: string;
148
+ bytes: number;
149
+ }
150
+
151
+ /** Build one receipt input from explicit pipeline facts (schema/phase included). */
152
+ export function buildVisibleLaunchReceiptInput(facts: {
153
+ agent: VisibleLaunchReceipt["agent"];
154
+ task: VisibleLaunchReceipt["task"];
155
+ bootstrap: VisibleLaunchReceipt["bootstrap"];
156
+ observation: VisibleLaunchReceipt["observation"];
157
+ launch: VisibleLaunchReceipt["launch"];
158
+ transport: VisibleLaunchReceipt["transport"];
159
+ recordedAt: string;
160
+ }): Omit<VisibleLaunchReceipt, "receiptSha256"> {
161
+ return {
162
+ schema: VISIBLE_LAUNCH_RECEIPT_SCHEMA,
163
+ phase: "fleet_phase_3",
164
+ agent: facts.agent,
165
+ task: facts.task,
166
+ bootstrap: facts.bootstrap,
167
+ observation: facts.observation,
168
+ launch: facts.launch,
169
+ transport: facts.transport,
170
+ recordedAt: facts.recordedAt,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Publish one immutable append-only launch receipt: canonical bytes, private
176
+ * temporary file, hard-link publication (O_EXCL-equivalent), read-only final
177
+ * mode, and a verified re-read. Pair reservations precede transport and are
178
+ * separate from receipts: an unresolved reservation blocks all automatic retries.
179
+ */
180
+ export async function writeImmutableVisibleLaunchReceipt(
181
+ receiptInput: Omit<VisibleLaunchReceipt, "receiptSha256">,
182
+ options?: { dir?: string },
183
+ ): Promise<WrittenVisibleLaunchReceipt> {
184
+ validateVisibleLaunchIdentity(receiptInput.agent.name, receiptInput.launch.runId);
185
+ const receipt: VisibleLaunchReceipt = {
186
+ ...receiptInput,
187
+ receiptSha256: computeVisibleLaunchReceiptSha256(receiptInput),
188
+ };
189
+ const bytes = `${JSON.stringify(receipt, null, 2)}\n`;
190
+ const configuredDir = resolveVisibleLaunchReceiptsDir(options?.dir);
191
+ const dir = await realpath(configuredDir).catch(async () => {
192
+ await mkdir(configuredDir, { recursive: true });
193
+ return realpath(configuredDir);
194
+ });
195
+ const receiptPath = join(
196
+ dir,
197
+ visibleLaunchReceiptFileName(
198
+ receipt.agent.name,
199
+ visibleLaunchStamp(new Date(receipt.recordedAt)),
200
+ randomUUID().slice(0, 8),
201
+ ),
202
+ );
203
+ if (fs.existsSync(receiptPath)) {
204
+ throw new VisibleLaunchReceiptError(
205
+ `visible launch receipt name collision for ${receipt.launch.runId}`,
206
+ );
207
+ }
208
+ const temporaryPath = `${receiptPath}.${process.pid}.${randomUUID()}.tmp`;
209
+ let descriptor: number | undefined;
210
+ try {
211
+ descriptor = fs.openSync(
212
+ temporaryPath,
213
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
214
+ 0o600,
215
+ );
216
+ fs.writeFileSync(descriptor, bytes, "utf8");
217
+ fs.fsyncSync(descriptor);
218
+ fs.closeSync(descriptor);
219
+ descriptor = undefined;
220
+ fs.linkSync(temporaryPath, receiptPath);
221
+ fs.unlinkSync(temporaryPath);
222
+ fs.chmodSync(receiptPath, 0o400);
223
+ const dirDescriptor = fs.openSync(dir, fs.constants.O_RDONLY);
224
+ try {
225
+ fs.fsyncSync(dirDescriptor);
226
+ } finally {
227
+ fs.closeSync(dirDescriptor);
228
+ }
229
+ } finally {
230
+ if (descriptor !== undefined) fs.closeSync(descriptor);
231
+ try {
232
+ fs.unlinkSync(temporaryPath);
233
+ } catch {
234
+ // Publication or earlier cleanup already removed the private temporary file.
235
+ }
236
+ }
237
+ const verified = await readVisibleLaunchReceipt(receiptPath);
238
+ if (!verified || verified.receiptSha256 !== receipt.receiptSha256) {
239
+ throw new VisibleLaunchReceiptError("published visible launch receipt failed verification");
240
+ }
241
+ return {
242
+ receipt: verified,
243
+ receiptPath,
244
+ receiptSha256: verified.receiptSha256,
245
+ bytes: Buffer.byteLength(bytes, "utf8"),
246
+ };
247
+ }
248
+
249
+ export async function readVisibleLaunchReceipt(
250
+ receiptPath: string,
251
+ ): Promise<VisibleLaunchReceipt | undefined> {
252
+ let raw: string;
253
+ try {
254
+ raw = await fs.promises.readFile(receiptPath, "utf8");
255
+ } catch {
256
+ return undefined;
257
+ }
258
+ let parsed: VisibleLaunchReceipt;
259
+ try {
260
+ parsed = JSON.parse(raw) as VisibleLaunchReceipt;
261
+ } catch {
262
+ return undefined;
263
+ }
264
+ if (!parsed || parsed.schema !== VISIBLE_LAUNCH_RECEIPT_SCHEMA) {
265
+ return undefined;
266
+ }
267
+ try {
268
+ validateVisibleLaunchIdentity(parsed.agent.name, parsed.launch.runId);
269
+ } catch {
270
+ return undefined;
271
+ }
272
+ if (computeVisibleLaunchReceiptSha256(parsed) !== parsed.receiptSha256) {
273
+ return undefined;
274
+ }
275
+ return parsed;
276
+ }
@@ -0,0 +1,77 @@
1
+ // ---
2
+ // summary: capability-checked loader for the little-helpers visible Ghostty launch transport.
3
+ // read_when:
4
+ // - changing how pi-agent-registry consumes the shared visible-launch core.
5
+ // ---
6
+
7
+ import { authorizeExactTask, readAkTask } from "./dispatch-authorization.ts";
8
+ import type { AkTaskSnapshot } from "./dispatch-contract.ts";
9
+ import { canonicalJsonString } from "./dispatch-receipt.ts";
10
+ import type { VisibleLaunchTransport } from "./visible-launch-contract.ts";
11
+
12
+ export function hasVisibleLaunchTransportCapability(mod: Record<string, unknown>): boolean {
13
+ return (
14
+ mod.STANDING_AGENT_TRANSPORT_VERSION === 1 &&
15
+ mod.STANDING_AGENT_DISPATCH_GUARD_VERSION === 1 &&
16
+ typeof mod.launchPiQuestSession === "function"
17
+ );
18
+ }
19
+
20
+ let cached: VisibleLaunchTransport | undefined | "unloaded" = "unloaded";
21
+
22
+ /**
23
+ * Load the little-helpers visible-launch core (`launchPiQuestSession`). The
24
+ * registry declares the workspace link; an installed/published
25
+ * pi-little-helpers that predates the `./sidequest-launch` export resolves to
26
+ * `undefined` and every Phase-3 launch fails closed with
27
+ * `visible_transport_unavailable` (confirmed_no_effects). Ghostty/transport
28
+ * mechanics stay little-helpers-owned; this loader checks version-1 final-guard capability too.
29
+ */
30
+ export async function loadVisibleLaunchTransport(): Promise<VisibleLaunchTransport | undefined> {
31
+ if (cached !== "unloaded") {
32
+ return cached;
33
+ }
34
+ try {
35
+ const mod = (await import("@tryinget/pi-little-helpers/sidequest-launch")) as Record<
36
+ string,
37
+ unknown
38
+ >;
39
+ cached = hasVisibleLaunchTransportCapability(mod)
40
+ ? (mod as unknown as VisibleLaunchTransport)
41
+ : undefined;
42
+ } catch {
43
+ cached = undefined;
44
+ }
45
+ return cached;
46
+ }
47
+
48
+ /** Test-only transport cache reset. */
49
+ export function resetVisibleLaunchTransportCache(): void {
50
+ cached = "unloaded";
51
+ }
52
+
53
+ /** Read-only owner gate consumed by shared transport AFTER its FIFO/identity waits.
54
+ * The original finite lease bounds dispatch even if time passes while this read resolves.
55
+ * This is a final bounded observation, not atomic lifetime authority or claimant authentication.
56
+ */
57
+ export function createVisibleLaunchDispatchGuard(
58
+ task: AkTaskSnapshot,
59
+ parentRoot: string,
60
+ akBinary?: string,
61
+ ) {
62
+ const expected = canonicalJsonString(task);
63
+ const dispatchDeadlineMs = Date.parse(task.lease_expires_at ?? "");
64
+ return {
65
+ dispatchDeadlineMs,
66
+ async beforeDispatch(): Promise<boolean> {
67
+ try {
68
+ const latest = await readAkTask(task.id, { akBinary });
69
+ return (
70
+ canonicalJsonString(latest) === expected && authorizeExactTask(latest, parentRoot).ok
71
+ );
72
+ } catch {
73
+ return false;
74
+ }
75
+ },
76
+ };
77
+ }