@akagilnc/pi-workflow-roles 0.1.3556 → 0.1.3565
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/acp-host/description.js +21 -0
- package/dist/{grok → acp-host}/mcp-relay.mjs +5 -5
- package/dist/{grok → acp-host}/production-host.js +111 -108
- package/dist/host-descriptions.js +34 -0
- package/dist/public-cli/config.js +23 -10
- package/dist/public-cli/main.js +89 -37
- package/package.json +1 -1
- package/scripts/build-package.mjs +8 -8
- package/src/acp-host/description.ts +43 -0
- package/src/{grok → acp-host}/mcp-relay.mjs +5 -5
- package/src/{grok → acp-host}/production-host.ts +30 -37
- package/src/{grok → acp-host}/role-envelope.ts +23 -23
- package/src/{grok → acp-host}/role-turn-host.ts +46 -81
- package/src/{grok → acp-host}/session-identity.ts +7 -7
- package/src/host-contracts.ts +5 -3
- package/src/host-descriptions.ts +51 -0
- package/src/host-transition-prior-native.ts +12 -24
- package/src/pi/role-turn-host.ts +2 -1
- package/src/public-cli/cli.ts +10 -9
- package/src/public-cli/config.ts +24 -11
- package/src/public-cli/load-production-acp-host.ts +52 -0
- package/src/public-cli/post-admission.ts +1 -1
- package/src/public-cli/load-production-grok-host.ts +0 -35
|
@@ -3,9 +3,10 @@ import { createInterface } from "node:readline";
|
|
|
3
3
|
|
|
4
4
|
import type { RoleTurnHost, RoleTurnKnownFailure, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
|
|
5
5
|
import { renderAgentStartMaterials } from "../agent-start-materials.ts";
|
|
6
|
+
import type { AcpHostDescription } from "./description.ts";
|
|
6
7
|
|
|
7
|
-
/** ACP v1 surface used by the
|
|
8
|
-
export interface
|
|
8
|
+
/** ACP v1 surface used by the generic ACP adapter. Protocol details stay in this module. */
|
|
9
|
+
export interface AcpConnection {
|
|
9
10
|
request(method: string, params: Readonly<Record<string, unknown>>): Promise<Readonly<Record<string, unknown>>>;
|
|
10
11
|
notify(method: string, params: Readonly<Record<string, unknown>>): void;
|
|
11
12
|
/** Subscribe to agent→client notifications (session/update stream, etc.). */
|
|
@@ -15,7 +16,7 @@ export interface GrokAcpConnection {
|
|
|
15
16
|
|
|
16
17
|
/** The shared envelope, prepared before session/new (systemPrompt delivery) and
|
|
17
18
|
* able to observe the host's real builtin tool surface once it arrives post-session. */
|
|
18
|
-
export type
|
|
19
|
+
export type AcpPreparedTurn = Readonly<{
|
|
19
20
|
mcpServers: readonly Readonly<Record<string, unknown>>[];
|
|
20
21
|
/**
|
|
21
22
|
* Structured system-prompt authority. `body` is the provider-facing prompt
|
|
@@ -45,24 +46,26 @@ export type GrokPreparedTurn = Readonly<{
|
|
|
45
46
|
}>;
|
|
46
47
|
|
|
47
48
|
/** Fold structured system-prompt authority into the provider-visible ACP override. */
|
|
48
|
-
export function
|
|
49
|
+
export function renderAcpSystemPromptOverride(authority: {
|
|
49
50
|
readonly body: string;
|
|
50
51
|
readonly materials: readonly unknown[];
|
|
51
52
|
}): string {
|
|
52
53
|
return renderAgentStartMaterials(authority.body, authority.materials);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
export type
|
|
56
|
+
export type AcpSessionIdentityAuthority = Readonly<{
|
|
56
57
|
load(principal: RoleTurnRequest["principal"]): Promise<string | undefined>;
|
|
57
58
|
bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
|
|
58
59
|
/** Durable principal session path for layout ownership / isAvailable — not a rebuild source (#617 DK-4). */
|
|
59
60
|
resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
|
|
60
61
|
}>;
|
|
61
62
|
|
|
62
|
-
export type
|
|
63
|
-
sessionIdentity:
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
export type AcpRoleTurnHostConfig = Readonly<{
|
|
64
|
+
sessionIdentity: AcpSessionIdentityAuthority;
|
|
65
|
+
/** Whether a bound resume reuses the native session or mints a fresh one. */
|
|
66
|
+
boundResume: AcpHostDescription["boundResume"];
|
|
67
|
+
connect(request: RoleTurnRequest): Promise<AcpConnection>;
|
|
68
|
+
prepare(request: RoleTurnRequest): Promise<AcpPreparedTurn>;
|
|
66
69
|
}>;
|
|
67
70
|
|
|
68
71
|
function failure(cause: "activation" | "session" | "output", name: string, code: string, details?: Readonly<Record<string, unknown>>): RoleTurnResult {
|
|
@@ -81,7 +84,7 @@ function failure(cause: "activation" | "session" | "output", name: string, code:
|
|
|
81
84
|
type RpcReply = { readonly id?: unknown; readonly method?: unknown; readonly params?: unknown; readonly result?: unknown; readonly error?: unknown };
|
|
82
85
|
|
|
83
86
|
function hostAbortedError(): Error & { readonly code: "host-aborted" } {
|
|
84
|
-
return Object.assign(new Error("
|
|
87
|
+
return Object.assign(new Error("ACP host aborted"), { code: "host-aborted" as const });
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
function acpError(code: string, message: string, cause?: unknown): Error & { readonly code: string } {
|
|
@@ -89,23 +92,14 @@ function acpError(code: string, message: string, cause?: unknown): Error & { rea
|
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
/** One ACP JSON-RPC stdio process. Natural close/SIGTERM are its only lifecycle exits. */
|
|
92
|
-
export function
|
|
95
|
+
export function connectAcpStdio(options: {
|
|
93
96
|
readonly binary: string;
|
|
97
|
+
readonly args: readonly string[];
|
|
94
98
|
readonly cwd: string;
|
|
95
99
|
readonly env: NodeJS.ProcessEnv;
|
|
96
|
-
readonly model?: string;
|
|
97
|
-
readonly toolset?: string;
|
|
98
100
|
readonly onNotification?: (method: string, params: Readonly<Record<string, unknown>>) => void;
|
|
99
|
-
}): Promise<
|
|
100
|
-
const
|
|
101
|
-
"agent",
|
|
102
|
-
...(options.model === undefined ? [] : ["--model", options.model]),
|
|
103
|
-
"stdio",
|
|
104
|
-
];
|
|
105
|
-
const env = options.toolset === undefined
|
|
106
|
-
? options.env
|
|
107
|
-
: { ...options.env, GROK_CONFIG: JSON.stringify({ toolset: options.toolset }) };
|
|
108
|
-
const child = spawn(options.binary, args, { cwd: options.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
101
|
+
}): Promise<AcpConnection> {
|
|
102
|
+
const child = spawn(options.binary, [...options.args], { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
109
103
|
const pending = new Map<number, { resolve(value: Readonly<Record<string, unknown>>): void; reject(error: Error): void }>();
|
|
110
104
|
const notificationHandlers: Array<(method: string, params: Readonly<Record<string, unknown>>) => void> = [];
|
|
111
105
|
if (options.onNotification !== undefined) notificationHandlers.push(options.onNotification);
|
|
@@ -128,12 +122,12 @@ export function connectGrokAcpStdio(options: {
|
|
|
128
122
|
child.kill("SIGTERM");
|
|
129
123
|
};
|
|
130
124
|
child.stderr.setEncoding("utf8").on("data", (chunk: string) => { stderr += chunk; });
|
|
131
|
-
child.on("error", (error) => settleClosed(acpError("acp-process-error", `
|
|
125
|
+
child.on("error", (error) => settleClosed(acpError("acp-process-error", `ACP process error: ${error.message}`, error)));
|
|
132
126
|
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
133
127
|
let message: RpcReply;
|
|
134
128
|
try { message = JSON.parse(line) as RpcReply; }
|
|
135
129
|
catch (error) {
|
|
136
|
-
terminate(acpError("acp-invalid-json", `Invalid
|
|
130
|
+
terminate(acpError("acp-invalid-json", `Invalid ACP JSON: ${String(error)}`, error));
|
|
137
131
|
return;
|
|
138
132
|
}
|
|
139
133
|
if (typeof message.method === "string") {
|
|
@@ -142,14 +136,14 @@ export function connectGrokAcpStdio(options: {
|
|
|
142
136
|
for (const handler of notificationHandlers) handler(message.method, params);
|
|
143
137
|
if (typeof message.id === "number") {
|
|
144
138
|
if (message.method !== "session/request_permission") {
|
|
145
|
-
terminate(acpError("acp-unsupported-client-request", `Unsupported
|
|
139
|
+
terminate(acpError("acp-unsupported-client-request", `Unsupported ACP client request: ${message.method}`));
|
|
146
140
|
return;
|
|
147
141
|
}
|
|
148
142
|
const choices = Array.isArray(params.options) ? params.options : [];
|
|
149
143
|
const selected = choices.find((value) =>
|
|
150
144
|
typeof value === "object" && value !== null && (value as { kind?: unknown }).kind === "allow_once") as { optionId?: unknown } | undefined;
|
|
151
145
|
if (typeof selected?.optionId !== "string") {
|
|
152
|
-
terminate(acpError("acp-permission-missing-allow-once", "
|
|
146
|
+
terminate(acpError("acp-permission-missing-allow-once", "ACP permission request omitted allow_once"));
|
|
153
147
|
return;
|
|
154
148
|
}
|
|
155
149
|
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "selected", optionId: selected.optionId } } })}\n`);
|
|
@@ -160,13 +154,13 @@ export function connectGrokAcpStdio(options: {
|
|
|
160
154
|
const waiter = pending.get(message.id);
|
|
161
155
|
if (waiter === undefined) return;
|
|
162
156
|
pending.delete(message.id);
|
|
163
|
-
if (message.error !== undefined) waiter.reject(acpError("acp-upstream-error", `
|
|
157
|
+
if (message.error !== undefined) waiter.reject(acpError("acp-upstream-error", `ACP error: ${JSON.stringify(message.error)}`));
|
|
164
158
|
else waiter.resolve((message.result ?? {}) as Readonly<Record<string, unknown>>);
|
|
165
159
|
});
|
|
166
|
-
child.on("close", (code) => settleClosed(acpError("acp-closed", `
|
|
160
|
+
child.on("close", (code) => settleClosed(acpError("acp-closed", `ACP closed (${String(code)}): ${stderr}`)));
|
|
167
161
|
return Promise.resolve({
|
|
168
162
|
request(method, params) {
|
|
169
|
-
if (closed) return Promise.reject(terminalError ?? acpError("acp-connection-closed", "
|
|
163
|
+
if (closed) return Promise.reject(terminalError ?? acpError("acp-connection-closed", "ACP connection is closed"));
|
|
170
164
|
const id = ++nextId;
|
|
171
165
|
return new Promise((resolve, reject) => {
|
|
172
166
|
pending.set(id, { resolve, reject });
|
|
@@ -175,12 +169,12 @@ export function connectGrokAcpStdio(options: {
|
|
|
175
169
|
const waiter = pending.get(id);
|
|
176
170
|
if (waiter === undefined) return;
|
|
177
171
|
pending.delete(id);
|
|
178
|
-
waiter.reject(acpError("acp-write-failed", `
|
|
172
|
+
waiter.reject(acpError("acp-write-failed", `ACP write failed: ${error.message}`, error));
|
|
179
173
|
});
|
|
180
174
|
});
|
|
181
175
|
},
|
|
182
176
|
notify(method, params) {
|
|
183
|
-
if (closed) throw terminalError ?? acpError("acp-connection-closed", "
|
|
177
|
+
if (closed) throw terminalError ?? acpError("acp-connection-closed", "ACP connection is closed");
|
|
184
178
|
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
185
179
|
},
|
|
186
180
|
onNotification(handler) {
|
|
@@ -188,7 +182,7 @@ export function connectGrokAcpStdio(options: {
|
|
|
188
182
|
},
|
|
189
183
|
async close() {
|
|
190
184
|
if (closed) return;
|
|
191
|
-
settleClosed(acpError("acp-connection-closed", "
|
|
185
|
+
settleClosed(acpError("acp-connection-closed", "ACP connection is closed"));
|
|
192
186
|
child.stdin.end();
|
|
193
187
|
child.kill("SIGTERM");
|
|
194
188
|
await new Promise<void>((resolve) => child.once("close", () => resolve()));
|
|
@@ -197,23 +191,23 @@ export function connectGrokAcpStdio(options: {
|
|
|
197
191
|
}
|
|
198
192
|
|
|
199
193
|
/**
|
|
200
|
-
* Main-session
|
|
194
|
+
* Main-session ACP adapter. The injected composition callbacks are the shared
|
|
201
195
|
* envelope boundary: this module owns ACP lifecycle, never role policy.
|
|
202
196
|
*/
|
|
203
|
-
export function
|
|
197
|
+
export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHost {
|
|
204
198
|
let serial = Promise.resolve();
|
|
205
199
|
return {
|
|
206
200
|
executeTurn(request) {
|
|
207
201
|
const execution = serial.then(async (): Promise<RoleTurnResult> => {
|
|
208
202
|
const continuation = request.continuation;
|
|
209
203
|
const prepared = await config.prepare(request);
|
|
210
|
-
let connection:
|
|
204
|
+
let connection: AcpConnection | undefined;
|
|
211
205
|
let sessionId: string | undefined;
|
|
212
206
|
let accepted = false;
|
|
213
207
|
try {
|
|
214
208
|
// AK injection proof is prepared MCP composition (envelope).
|
|
215
209
|
if (prepared.mcpServers.length === 0) {
|
|
216
|
-
return failure("activation", "
|
|
210
|
+
return failure("activation", "UncontrolledAcpSession", "ak-config-missing");
|
|
217
211
|
}
|
|
218
212
|
connection = await config.connect(request);
|
|
219
213
|
const initialized = await connection.request("initialize", {
|
|
@@ -227,60 +221,47 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
|
|
|
227
221
|
const availableModels = Array.isArray(modelState?.availableModels) ? modelState.availableModels : undefined;
|
|
228
222
|
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
|
|
229
223
|
typeof entry === "object" && entry !== null && (entry as { modelId?: unknown }).modelId === request.model?.model)) {
|
|
230
|
-
return failure("activation", "
|
|
224
|
+
return failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
231
225
|
provider: request.model.provider,
|
|
232
226
|
model: request.model.model,
|
|
233
227
|
});
|
|
234
228
|
}
|
|
235
229
|
|
|
236
|
-
// #617 DK-7: hand
|
|
230
|
+
// #617 DK-7 / #732: hand the projected prior-native session paths once,
|
|
231
|
+
// whichever record family they came from; the host reads the files itself.
|
|
237
232
|
const priorNativePaths =
|
|
238
233
|
continuation.kind === "resume"
|
|
239
|
-
|
|
240
|
-
? request.hostTransition.priorNativePaths
|
|
234
|
+
? request.hostTransition?.priorNativePaths
|
|
241
235
|
: undefined;
|
|
242
|
-
if (continuation.kind === "resume") {
|
|
236
|
+
if (continuation.kind === "resume" && config.boundResume === "session/load") {
|
|
237
|
+
// Same-host resume reuses the native ACP session via session/load.
|
|
243
238
|
const boundSessionId = await config.sessionIdentity.load(request.principal);
|
|
244
239
|
if (boundSessionId !== undefined && boundSessionId !== "") {
|
|
245
|
-
// Same-host Grok resume reuses native ACP session via session/load.
|
|
246
240
|
const loaded = await connection.request("session/load", {
|
|
247
241
|
sessionId: boundSessionId,
|
|
248
242
|
cwd: request.cwd,
|
|
249
243
|
mcpServers: prepared.mcpServers,
|
|
250
|
-
_meta: { systemPromptOverride:
|
|
244
|
+
_meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
|
|
251
245
|
});
|
|
252
246
|
sessionId = typeof loaded.sessionId === "string" && loaded.sessionId !== ""
|
|
253
247
|
? loaded.sessionId
|
|
254
248
|
: boundSessionId;
|
|
255
|
-
} else {
|
|
256
|
-
// Unbound resume (cross-host or lost binding): session/new + bind.
|
|
257
|
-
const session = await connection.request(
|
|
258
|
-
"session/new",
|
|
259
|
-
{
|
|
260
|
-
cwd: request.cwd,
|
|
261
|
-
mcpServers: prepared.mcpServers,
|
|
262
|
-
_meta: { systemPromptOverride: renderGrokSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
|
|
263
|
-
},
|
|
264
|
-
);
|
|
265
|
-
sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
|
|
266
|
-
if (sessionId === undefined || sessionId === "") {
|
|
267
|
-
return failure("session", "GrokAcpSessionFailure", "session-id-missing");
|
|
268
|
-
}
|
|
269
|
-
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
270
249
|
}
|
|
271
|
-
}
|
|
272
|
-
|
|
250
|
+
}
|
|
251
|
+
if (sessionId === undefined) {
|
|
252
|
+
// Initial run, unbound resume (cross-host / lost binding), or a host
|
|
253
|
+
// whose bound resume is session/new: mint the session and bind it.
|
|
273
254
|
const session = await connection.request(
|
|
274
255
|
"session/new",
|
|
275
256
|
{
|
|
276
257
|
cwd: request.cwd,
|
|
277
258
|
mcpServers: prepared.mcpServers,
|
|
278
|
-
_meta: { systemPromptOverride:
|
|
259
|
+
_meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
|
|
279
260
|
},
|
|
280
261
|
);
|
|
281
262
|
sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
|
|
282
263
|
if (sessionId === undefined || sessionId === "") {
|
|
283
|
-
return failure("session", "
|
|
264
|
+
return failure("session", "AcpSessionFailure", "session-id-missing");
|
|
284
265
|
}
|
|
285
266
|
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
286
267
|
}
|
|
@@ -353,7 +334,7 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
|
|
|
353
334
|
throw error;
|
|
354
335
|
}
|
|
355
336
|
if (result.stopReason === "refusal") {
|
|
356
|
-
return failure("output", "
|
|
337
|
+
return failure("output", "AcpRefusal", "refusal", { sessionId });
|
|
357
338
|
}
|
|
358
339
|
// session/prompt resolution is the sole typed round boundary before seal
|
|
359
340
|
// when the turn ends without host abort; abort path closes above.
|
|
@@ -370,7 +351,7 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
|
|
|
370
351
|
}
|
|
371
352
|
prompt = `The prior terminal submission was rejected (${closure.retry.code}). Resubmit it as the sole terminal tool call. Rejected call ids: ${closure.retry.toolCallIds.join(", ") || "none"}.`;
|
|
372
353
|
}
|
|
373
|
-
return failure("output", "
|
|
354
|
+
return failure("output", "AcpRoundLimit", "round-retry-limit", { sessionId });
|
|
374
355
|
} finally {
|
|
375
356
|
if (connection !== undefined) {
|
|
376
357
|
if (sessionId !== undefined && !accepted) {
|
|
@@ -389,19 +370,3 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
|
|
|
389
370
|
},
|
|
390
371
|
};
|
|
391
372
|
}
|
|
392
|
-
|
|
393
|
-
const PRIVATE_COMPAT_ENV = Object.fromEntries(
|
|
394
|
-
["CLAUDE", "CURSOR", "CODEX"].flatMap((vendor) =>
|
|
395
|
-
["SKILLS", "RULES", "AGENTS", "MCPS", "HOOKS", "SESSIONS"].map((kind) =>
|
|
396
|
-
[`GROK_${vendor}_${kind}_ENABLED`, "false"] as const)),
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
/** Child environment shared by ACP agent processes. */
|
|
400
|
-
export function controlledGrokChildEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
401
|
-
return {
|
|
402
|
-
...base,
|
|
403
|
-
...PRIVATE_COMPAT_ENV,
|
|
404
|
-
GROK_MEMORY: "0",
|
|
405
|
-
GROK_SUBAGENTS: "0",
|
|
406
|
-
};
|
|
407
|
-
}
|
|
@@ -2,15 +2,15 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import type { DurablePrincipal, DurablePrincipalAuthority } from "../host-contracts.ts";
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
/** ACP binding filename written only by the grok host on initial bind. */
|
|
8
|
-
export const GROK_ACP_SESSION_BINDING = "grok-acp-session.json";
|
|
5
|
+
import type { AcpSessionIdentityAuthority } from "./role-turn-host.ts";
|
|
9
6
|
|
|
10
7
|
/** Durable ACP binding stored beside the host-owned session principal. */
|
|
11
|
-
export function
|
|
8
|
+
export function createAcpSessionIdentityAuthority(
|
|
9
|
+
authority: DurablePrincipalAuthority,
|
|
10
|
+
sessionBindingFile: string,
|
|
11
|
+
): AcpSessionIdentityAuthority {
|
|
12
12
|
const bindingPath = (principal: DurablePrincipal): string =>
|
|
13
|
-
join(authority.decode(principal).sessionDirectory,
|
|
13
|
+
join(authority.decode(principal).sessionDirectory, sessionBindingFile);
|
|
14
14
|
return {
|
|
15
15
|
resolveSessionFile(principal) {
|
|
16
16
|
return authority.decode(principal).sessionFile;
|
|
@@ -19,7 +19,7 @@ export function createGrokSessionIdentityAuthority(authority: DurablePrincipalAu
|
|
|
19
19
|
try {
|
|
20
20
|
const value: unknown = JSON.parse(await readFile(bindingPath(principal), "utf8"));
|
|
21
21
|
if (typeof value !== "object" || value === null || typeof (value as { sessionId?: unknown }).sessionId !== "string") {
|
|
22
|
-
throw new Error("durable
|
|
22
|
+
throw new Error("durable ACP session binding is invalid");
|
|
23
23
|
}
|
|
24
24
|
return (value as { sessionId: string }).sessionId;
|
|
25
25
|
} catch (error) {
|
package/src/host-contracts.ts
CHANGED
|
@@ -140,12 +140,14 @@ export type RoleTurnModelConfig = {
|
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
142
|
* Typed cross-host resume handoff (#617 DK-4).
|
|
143
|
-
* Present only when post-admission projects a real switch
|
|
144
|
-
*
|
|
143
|
+
* Present only when post-admission projects a real host switch.
|
|
144
|
+
* priorNativeKind names the record family the paths belong to — Pi's own
|
|
145
|
+
* session file, or sitian run records (ADR 0077) — so a consuming adapter
|
|
146
|
+
* reads the handoff without knowing which host wrote it.
|
|
145
147
|
* Target host reads those files itself; projector never copies bytes.
|
|
146
148
|
*/
|
|
147
149
|
export type RoleTurnHostTransition = {
|
|
148
|
-
readonly
|
|
150
|
+
readonly priorNativeKind: "pi-native" | "sitian";
|
|
149
151
|
readonly priorNativePaths: readonly string[];
|
|
150
152
|
};
|
|
151
153
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaged host description table (#729 / #731).
|
|
3
|
+
* Key = seat-table `host` value; the row is the generic ACP factory's input.
|
|
4
|
+
* pi is the in-process default, not a row.
|
|
5
|
+
* Unregistered names fail closed (#510); this table does not fallback.
|
|
6
|
+
*/
|
|
7
|
+
import type { AcpHostDescription } from "./acp-host/description.ts";
|
|
8
|
+
|
|
9
|
+
/** Grok CLI reads vendor-private compat surfaces unless each is disabled by name. */
|
|
10
|
+
const PRIVATE_COMPAT_ENV = Object.fromEntries(
|
|
11
|
+
["CLAUDE", "CURSOR", "CODEX"].flatMap((vendor) =>
|
|
12
|
+
["SKILLS", "RULES", "AGENTS", "MCPS", "HOOKS", "SESSIONS"].map((kind) =>
|
|
13
|
+
[`GROK_${vendor}_${kind}_ENABLED`, "false"] as const)),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_ROLE_TURN_HOST = "pi" as const;
|
|
17
|
+
|
|
18
|
+
export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = Object.freeze({
|
|
19
|
+
/** Operator home `~/.grok`, native session/load resume, `agent [--model X] stdio`. */
|
|
20
|
+
"grok-build": Object.freeze({
|
|
21
|
+
binaryFromHome: Object.freeze([".grok", "bin", "grok"]),
|
|
22
|
+
argv: Object.freeze({
|
|
23
|
+
prefix: Object.freeze(["agent"]),
|
|
24
|
+
suffix: Object.freeze(["stdio"]),
|
|
25
|
+
modelFlag: "--model",
|
|
26
|
+
}),
|
|
27
|
+
boundResume: "session/load",
|
|
28
|
+
sessionBindingFile: "grok-acp-session.json",
|
|
29
|
+
childEnv: Object.freeze({
|
|
30
|
+
...PRIVATE_COMPAT_ENV,
|
|
31
|
+
GROK_MEMORY: "0",
|
|
32
|
+
GROK_SUBAGENTS: "0",
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export function lookupHostDescription(host: string): AcpHostDescription | undefined {
|
|
38
|
+
return Object.hasOwn(HOST_DESCRIPTIONS, host) ? HOST_DESCRIPTIONS[host] : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function packagedExternalHostNames(): readonly string[] {
|
|
42
|
+
return Object.keys(HOST_DESCRIPTIONS);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Legal persistent/invocation host: default pi, or a table key. */
|
|
46
|
+
export function assertRegisteredHostName(host: string): string {
|
|
47
|
+
if (host === DEFAULT_ROLE_TURN_HOST || lookupHostDescription(host) !== undefined) {
|
|
48
|
+
return host;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`unregistered host: ${host}`);
|
|
51
|
+
}
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single authority for #617 DK-4 cross-host prior-native projection.
|
|
3
|
-
*
|
|
3
|
+
* Classifies the prior volume into the two record families that exist
|
|
4
|
+
* (Pi native session file / sitian run records), never by host name.
|
|
4
5
|
*/
|
|
5
6
|
import { access, readdir } from "node:fs/promises";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
8
|
|
|
8
9
|
import type { RoleTurnHostTransition } from "./host-contracts.ts";
|
|
9
10
|
|
|
10
|
-
const KNOWN_ROLE_TURN_HOSTS = ["pi", "grok-build"] as const;
|
|
11
|
-
type KnownRoleTurnHost = (typeof KNOWN_ROLE_TURN_HOSTS)[number];
|
|
12
|
-
|
|
13
|
-
function isKnownRoleTurnHost(value: string): value is KnownRoleTurnHost {
|
|
14
|
-
return (KNOWN_ROLE_TURN_HOSTS as readonly string[]).includes(value);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
function isEnoent(error: unknown): boolean {
|
|
18
12
|
return typeof error === "object" && error !== null && (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
19
13
|
}
|
|
@@ -59,13 +53,12 @@ async function listSitianRecordPaths(sessionParent: string): Promise<string[]> {
|
|
|
59
53
|
}
|
|
60
54
|
|
|
61
55
|
/**
|
|
62
|
-
* Project one hostTransition only for a real switch
|
|
63
|
-
*
|
|
64
|
-
* yields a typed switch (empty path list).
|
|
56
|
+
* Project one hostTransition only for a real host switch. Empty native volume
|
|
57
|
+
* still yields a typed switch (empty path list).
|
|
65
58
|
*
|
|
66
|
-
* Pi
|
|
67
|
-
*
|
|
68
|
-
* stay in the operator
|
|
59
|
+
* Pi wrote its own session.jsonl; every other host's run volume is the sitian
|
|
60
|
+
* record set on the live run (ADR 0077 `record-scope-phase-two`, #717) — the CLI's
|
|
61
|
+
* own journals stay in the operator home and are never copied here.
|
|
69
62
|
*/
|
|
70
63
|
export async function projectHostTransitionPriorNative(input: {
|
|
71
64
|
readonly previousHost: string;
|
|
@@ -73,20 +66,15 @@ export async function projectHostTransitionPriorNative(input: {
|
|
|
73
66
|
readonly piSessionFile: string;
|
|
74
67
|
}): Promise<RoleTurnHostTransition | undefined> {
|
|
75
68
|
if (input.previousHost === input.liveHost) return undefined;
|
|
76
|
-
if (!isKnownRoleTurnHost(input.previousHost) || !isKnownRoleTurnHost(input.liveHost)) {
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
69
|
if (input.previousHost === "pi") {
|
|
80
|
-
const paths = await listPiNativeRecordPaths(input.piSessionFile);
|
|
81
70
|
return {
|
|
82
|
-
|
|
83
|
-
priorNativePaths:
|
|
71
|
+
priorNativeKind: "pi-native",
|
|
72
|
+
priorNativePaths: await listPiNativeRecordPaths(input.piSessionFile),
|
|
84
73
|
};
|
|
85
74
|
}
|
|
86
|
-
//
|
|
87
|
-
const paths = await listSitianRecordPaths(input.piSessionFile);
|
|
75
|
+
// Sitian path handoff only — do not read bytes.
|
|
88
76
|
return {
|
|
89
|
-
|
|
90
|
-
priorNativePaths:
|
|
77
|
+
priorNativeKind: "sitian",
|
|
78
|
+
priorNativePaths: await listSitianRecordPaths(input.piSessionFile),
|
|
91
79
|
};
|
|
92
80
|
}
|
package/src/pi/role-turn-host.ts
CHANGED
|
@@ -406,9 +406,10 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
406
406
|
return {
|
|
407
407
|
async executeTurn(request: RoleTurnRequest): Promise<RoleTurnResult> {
|
|
408
408
|
// #617 DK-7: Pi argv gets projected native paths once; never record bytes.
|
|
409
|
+
// Pi already owns its own session file, so only sitian prior volume rides in.
|
|
409
410
|
let turnRequest = request;
|
|
410
411
|
const paths =
|
|
411
|
-
request.hostTransition?.
|
|
412
|
+
request.hostTransition?.priorNativeKind === "sitian"
|
|
412
413
|
? request.hostTransition.priorNativePaths
|
|
413
414
|
: undefined;
|
|
414
415
|
if (
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -38,8 +38,9 @@ import { seatModelOnly } from "./registry.ts";
|
|
|
38
38
|
import { CliUsageError } from "./cli-errors.ts";
|
|
39
39
|
import type { CliIo } from "./cli-io.ts";
|
|
40
40
|
import type { PostAdmissionEnv } from "./post-admission.ts";
|
|
41
|
-
import type { RoleTurnHost } from "../host-contracts.ts";
|
|
42
|
-
import {
|
|
41
|
+
import type { RoleTurnHost, RoleTurnRequest } from "../host-contracts.ts";
|
|
42
|
+
import { packagedExternalHostNames } from "../host-descriptions.ts";
|
|
43
|
+
import { loadProductionAcpHostFactory } from "./load-production-acp-host.ts";
|
|
43
44
|
import {
|
|
44
45
|
createPiRoleTurnHost,
|
|
45
46
|
appendPiSessionCustomEntry,
|
|
@@ -321,19 +322,19 @@ function resolveRoleTurnHost(
|
|
|
321
322
|
recordLaunchedRolePackageIdentity,
|
|
322
323
|
observeLaunchedRolePackageIdentity,
|
|
323
324
|
});
|
|
324
|
-
// Composition-root
|
|
325
|
-
const adapters = env.hostAdapters ?? [
|
|
325
|
+
// Composition-root adapter table: pi (in-process default) + one ACP adapter per description-table key.
|
|
326
|
+
const adapters: readonly NamedRoleTurnHostAdapter[] = env.hostAdapters ?? [
|
|
326
327
|
{ name: "pi", create: () => ({ ok: true as const, host: piHost }) },
|
|
327
|
-
{
|
|
328
|
-
name
|
|
328
|
+
...packagedExternalHostNames().map((name) => ({
|
|
329
|
+
name,
|
|
329
330
|
create: () => {
|
|
330
331
|
// Factory loads outside the public bin static graph (ADR 0052 peer-free discovery).
|
|
331
332
|
let hostPromise: Promise<RoleTurnHost> | undefined;
|
|
332
333
|
return {
|
|
333
334
|
ok: true as const,
|
|
334
335
|
host: {
|
|
335
|
-
executeTurn: async (request) => {
|
|
336
|
-
hostPromise ??=
|
|
336
|
+
executeTurn: async (request: RoleTurnRequest) => {
|
|
337
|
+
hostPromise ??= loadProductionAcpHostFactory(env.packageRoot, name).then((create) =>
|
|
337
338
|
create({
|
|
338
339
|
packageRoot: env.packageRoot,
|
|
339
340
|
principalAuthority: options.principalAuthority,
|
|
@@ -344,7 +345,7 @@ function resolveRoleTurnHost(
|
|
|
344
345
|
},
|
|
345
346
|
};
|
|
346
347
|
},
|
|
347
|
-
},
|
|
348
|
+
})),
|
|
348
349
|
];
|
|
349
350
|
const hostName = options.seat.host;
|
|
350
351
|
const adapter = adapters.find((candidate) => candidate.name === hostName);
|
package/src/public-cli/config.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
|
|
7
|
+
import { assertRegisteredHostName, DEFAULT_ROLE_TURN_HOST } from "../host-descriptions.ts";
|
|
7
8
|
import { assertLegalEngineName } from "../package-resources/engine-material.ts";
|
|
8
9
|
import { resolveConfiguredProvinceOfficer } from "../institutional-resolution.ts";
|
|
9
10
|
import {
|
|
@@ -218,7 +219,8 @@ export function setPersistentSeatHost(
|
|
|
218
219
|
}
|
|
219
220
|
return { ...config, seats: { ...config.seats, [seat]: rest } };
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
+
const registered = assertRegisteredHostName(host);
|
|
223
|
+
return { ...config, seats: { ...config.seats, [seat]: { ...previous, host: registered } } };
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
/**
|
|
@@ -295,7 +297,7 @@ export function setAutoResumeLimit(
|
|
|
295
297
|
* Config-parse seam: persistent call axes belong to PUBLIC_CALLABLE_ROLES;
|
|
296
298
|
* engine names need only path-safety syntax (no closed material catalog;
|
|
297
299
|
* #376 / #378 / #391 / ADR 0069). Syntax authority = assertLegalEngineName
|
|
298
|
-
* (no injected duplicate).
|
|
300
|
+
* (no injected duplicate). Persistent host must be pi or a description-table key (#510 / #731).
|
|
299
301
|
*/
|
|
300
302
|
export function validatePublicCliConfigAxes(
|
|
301
303
|
config: PublicCliConfig,
|
|
@@ -303,14 +305,25 @@ export function validatePublicCliConfigAxes(
|
|
|
303
305
|
): void {
|
|
304
306
|
for (const seat of Object.keys(config.seats) as PublicConfigurableSeat[]) {
|
|
305
307
|
const row = config.seats[seat];
|
|
306
|
-
if (row?.engine
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
308
|
+
if (row?.engine !== undefined) {
|
|
309
|
+
try {
|
|
310
|
+
assertLegalEngineName(row.engine);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`config seat ${seat} engine is illegal: ${row.engine}`,
|
|
314
|
+
{ cause: error },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (row?.host !== undefined) {
|
|
319
|
+
try {
|
|
320
|
+
assertRegisteredHostName(row.host);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`config seat ${seat} host is unregistered: ${row.host}`,
|
|
324
|
+
{ cause: error },
|
|
325
|
+
);
|
|
326
|
+
}
|
|
314
327
|
}
|
|
315
328
|
}
|
|
316
329
|
}
|
|
@@ -550,7 +563,7 @@ function attachHostAxis(
|
|
|
550
563
|
if (invocation?.host !== undefined) return { ...seat, host: invocation.host, hostSource: "invocation" };
|
|
551
564
|
const persistent = config.seats[seat.seat]?.host;
|
|
552
565
|
if (persistent !== undefined) return { ...seat, host: persistent, hostSource: "persistent" };
|
|
553
|
-
return { ...seat, host:
|
|
566
|
+
return { ...seat, host: DEFAULT_ROLE_TURN_HOST, hostSource: "default" };
|
|
554
567
|
}
|
|
555
568
|
|
|
556
569
|
function attachEngineAxis(
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred loader for the packaged generic ACP RoleTurnHost factory.
|
|
3
|
+
*
|
|
4
|
+
* Public ak-role bin must not statically value-import the production host (or its
|
|
5
|
+
* role-runtime / pi-coding-agent edges). Specifier is runtime-constructed so
|
|
6
|
+
* esbuild leaves this import external (ADR 0052 discovery stays peer-free).
|
|
7
|
+
*
|
|
8
|
+
* Host selection is a description-table lookup: the row for the seat's host key
|
|
9
|
+
* is the factory input. There is no host-name fork here.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
|
|
15
|
+
import type { AcpHostDescription } from "../acp-host/description.ts";
|
|
16
|
+
import { lookupHostDescription } from "../host-descriptions.ts";
|
|
17
|
+
import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
|
|
18
|
+
|
|
19
|
+
export type ProductionAcpHostFactory = (options: {
|
|
20
|
+
packageRoot: string;
|
|
21
|
+
principalAuthority: DurablePrincipalAuthority;
|
|
22
|
+
}) => RoleTurnHost;
|
|
23
|
+
|
|
24
|
+
type GenericAcpHostFactory = (options: {
|
|
25
|
+
packageRoot: string;
|
|
26
|
+
principalAuthority: DurablePrincipalAuthority;
|
|
27
|
+
description: AcpHostDescription;
|
|
28
|
+
}) => RoleTurnHost;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the production ACP host factory for one registered host key without a
|
|
32
|
+
* static graph edge into the public CLI bundle.
|
|
33
|
+
*/
|
|
34
|
+
export async function loadProductionAcpHostFactory(
|
|
35
|
+
packageRoot: string,
|
|
36
|
+
host: string,
|
|
37
|
+
): Promise<ProductionAcpHostFactory> {
|
|
38
|
+
const description = lookupHostDescription(host);
|
|
39
|
+
if (description === undefined) {
|
|
40
|
+
throw new Error(`unregistered host: ${host}`);
|
|
41
|
+
}
|
|
42
|
+
const built = join(packageRoot, "dist/acp-host/production-host.js");
|
|
43
|
+
const source = join(packageRoot, "src/acp-host/production-host.ts");
|
|
44
|
+
// Prefer the built artifact (plain node); fall back to source under tsx tests.
|
|
45
|
+
const target = existsSync(built) ? built : source;
|
|
46
|
+
const href = pathToFileURL(target).href;
|
|
47
|
+
const mod = (await import(href)) as {
|
|
48
|
+
createProductionAcpRoleTurnHost: GenericAcpHostFactory;
|
|
49
|
+
};
|
|
50
|
+
const create = mod.createProductionAcpRoleTurnHost;
|
|
51
|
+
return (options) => create({ ...options, description });
|
|
52
|
+
}
|