@akagilnc/pi-workflow-roles 0.1.3999 → 0.1.4021
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/production-host.js +10 -0
- package/dist/headless-host/description.js +319 -21
- package/dist/headless-host/production-host.js +462 -52
- package/dist/host-descriptions.js +19 -6
- package/dist/public-cli/main.js +10 -0
- package/package.json +1 -1
- package/src/headless-host/description.ts +381 -20
- package/src/headless-host/production-host.ts +5 -5
- package/src/headless-host/role-turn-host.ts +290 -27
- package/src/host-descriptions.ts +20 -7
|
@@ -46,15 +46,19 @@ export const HOST_DESCRIPTIONS = Object.freeze({
|
|
|
46
46
|
}),
|
|
47
47
|
});
|
|
48
48
|
/**
|
|
49
|
-
* Headless CLI family (#645). Claude is the first row;
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
49
|
+
* Headless CLI family (#645 / #646). Claude print-mode is the first row;
|
|
50
|
+
* codex exec (#646) adds another. Protocol-specific argv/parse live in
|
|
51
|
+
* headless-host helpers (#752 per-host impl).
|
|
52
|
+
* Claude fixedArgs: print mode, isolation without `--bare` (OAuth stays), full
|
|
53
|
+
* permissions. stream-json + verbose: live host events for sitian records
|
|
54
|
+
* (#811); result is last line. `--setting-sources` empty = load no
|
|
55
|
+
* user/project/local CLAUDE.md/hooks/skills (role envelope is delivered via
|
|
56
|
+
* `--system-prompt` wholesale replace). `--strict-mcp-config` with no
|
|
57
|
+
* `--mcp-config` drops operator MCP + claude.ai connectors.
|
|
55
58
|
*/
|
|
56
59
|
export const HEADLESS_HOST_DESCRIPTIONS = Object.freeze({
|
|
57
60
|
"claude": Object.freeze({
|
|
61
|
+
protocol: "claude-print",
|
|
58
62
|
binaryFromHome: Object.freeze([".local", "bin", "claude"]),
|
|
59
63
|
sessionBindingFile: "claude-headless-session.json",
|
|
60
64
|
fixedArgs: Object.freeze([
|
|
@@ -78,6 +82,15 @@ export const HEADLESS_HOST_DESCRIPTIONS = Object.freeze({
|
|
|
78
82
|
sessionIdFlag: "--session-id",
|
|
79
83
|
resumeFlag: "--resume",
|
|
80
84
|
}),
|
|
85
|
+
/**
|
|
86
|
+
* Codex headless (#646). Binary under operator home; auth stays in CODEX_HOME.
|
|
87
|
+
* Argv/parse/schema-close are codex-exec helpers — not Claude flag mapping.
|
|
88
|
+
*/
|
|
89
|
+
"codex": Object.freeze({
|
|
90
|
+
protocol: "codex-exec",
|
|
91
|
+
binaryFromHome: Object.freeze([".local", "bin", "codex"]),
|
|
92
|
+
sessionBindingFile: "codex-headless-session.json",
|
|
93
|
+
}),
|
|
81
94
|
});
|
|
82
95
|
export function lookupHostDescription(host) {
|
|
83
96
|
return Object.hasOwn(HOST_DESCRIPTIONS, host) ? HOST_DESCRIPTIONS[host] : undefined;
|
package/dist/public-cli/main.js
CHANGED
|
@@ -15162,6 +15162,7 @@ var init_host_descriptions = __esm({
|
|
|
15162
15162
|
});
|
|
15163
15163
|
HEADLESS_HOST_DESCRIPTIONS = Object.freeze({
|
|
15164
15164
|
"claude": Object.freeze({
|
|
15165
|
+
protocol: "claude-print",
|
|
15165
15166
|
binaryFromHome: Object.freeze([".local", "bin", "claude"]),
|
|
15166
15167
|
sessionBindingFile: "claude-headless-session.json",
|
|
15167
15168
|
fixedArgs: Object.freeze([
|
|
@@ -15187,6 +15188,15 @@ var init_host_descriptions = __esm({
|
|
|
15187
15188
|
mcpConfigFlag: "--mcp-config",
|
|
15188
15189
|
sessionIdFlag: "--session-id",
|
|
15189
15190
|
resumeFlag: "--resume"
|
|
15191
|
+
}),
|
|
15192
|
+
/**
|
|
15193
|
+
* Codex headless (#646). Binary under operator home; auth stays in CODEX_HOME.
|
|
15194
|
+
* Argv/parse/schema-close are codex-exec helpers — not Claude flag mapping.
|
|
15195
|
+
*/
|
|
15196
|
+
"codex": Object.freeze({
|
|
15197
|
+
protocol: "codex-exec",
|
|
15198
|
+
binaryFromHome: Object.freeze([".local", "bin", "codex"]),
|
|
15199
|
+
sessionBindingFile: "codex-headless-session.json"
|
|
15190
15200
|
})
|
|
15191
15201
|
});
|
|
15192
15202
|
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Headless CLI host descriptions (#645 / #646 / #752).
|
|
3
|
+
* Shared lifecycle owns spawn/bind/close; each protocol owns argv + parse shape.
|
|
4
|
+
* Claude print-mode and codex exec differ enough that #752 host-specific
|
|
5
|
+
* assembly lives here as sibling helpers — not a third unified abstraction.
|
|
6
6
|
*/
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
type HeadlessHostBase = Readonly<{
|
|
10
10
|
/** Binary path segments relative to the operator home. */
|
|
11
11
|
binaryFromHome: readonly string[];
|
|
12
12
|
/** Durable session-id binding filename beside the session principal. */
|
|
13
13
|
sessionBindingFile: string;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
/** Claude Code print-mode (#645). */
|
|
17
|
+
export type ClaudePrintHostDescription = HeadlessHostBase & Readonly<{
|
|
18
|
+
protocol: "claude-print";
|
|
14
19
|
/**
|
|
15
20
|
* Host-native print-mode flags that never change per turn (no prompt).
|
|
16
21
|
* Model / effort / system-prompt / schema / session / resume / mcp-config
|
|
@@ -38,6 +43,29 @@ export type HeadlessHostDescription = Readonly<{
|
|
|
38
43
|
resumeFlag: string;
|
|
39
44
|
}>;
|
|
40
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Codex `exec` / `exec resume` (#646).
|
|
48
|
+
* Protocol differences (JSONL, resume subcommand, schema file, `-c` MCP) stay
|
|
49
|
+
* in codex-specific helpers — description only carries identity + binary path.
|
|
50
|
+
*/
|
|
51
|
+
export type CodexExecHostDescription = HeadlessHostBase & Readonly<{
|
|
52
|
+
protocol: "codex-exec";
|
|
53
|
+
}>;
|
|
54
|
+
|
|
55
|
+
export type HeadlessHostDescription = ClaudePrintHostDescription | CodexExecHostDescription;
|
|
56
|
+
|
|
57
|
+
export function isClaudePrintDescription(
|
|
58
|
+
description: HeadlessHostDescription,
|
|
59
|
+
): description is ClaudePrintHostDescription {
|
|
60
|
+
return description.protocol === "claude-print";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isCodexExecDescription(
|
|
64
|
+
description: HeadlessHostDescription,
|
|
65
|
+
): description is CodexExecHostDescription {
|
|
66
|
+
return description.protocol === "codex-exec";
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
/** Absolute agent binary for one operator home. */
|
|
42
70
|
export function resolveHeadlessBinary(
|
|
43
71
|
description: HeadlessHostDescription,
|
|
@@ -47,11 +75,11 @@ export function resolveHeadlessBinary(
|
|
|
47
75
|
}
|
|
48
76
|
|
|
49
77
|
/**
|
|
50
|
-
* Build one
|
|
78
|
+
* Build one Claude print-mode argv for a single process turn.
|
|
51
79
|
* Shape: `<promptFlag> <prompt> <fixedArgs…> <system/schema/mcp/model/effort/session…>`.
|
|
52
80
|
*/
|
|
53
81
|
export function headlessTurnArgs(options: {
|
|
54
|
-
readonly description:
|
|
82
|
+
readonly description: ClaudePrintHostDescription;
|
|
55
83
|
readonly prompt: string;
|
|
56
84
|
/** Absolute path written by the adapter; paired with `systemPromptFlag`. */
|
|
57
85
|
readonly systemPromptPath: string;
|
|
@@ -90,6 +118,350 @@ export function headlessTurnArgs(options: {
|
|
|
90
118
|
return args;
|
|
91
119
|
}
|
|
92
120
|
|
|
121
|
+
/**
|
|
122
|
+
* TOML string literal for `codex -c key=<value>` (values are TOML-parsed).
|
|
123
|
+
* Double-quoted form; escapes backslash and quote only.
|
|
124
|
+
*/
|
|
125
|
+
function codexTomlString(value: string): string {
|
|
126
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** TOML array of strings for `-c key=["a","b"]`. */
|
|
130
|
+
function codexTomlStringArray(values: readonly string[]): string {
|
|
131
|
+
return `[${values.map(codexTomlString).join(",")}]`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** TOML inline table of string→string for `-c key={a="b"}`. */
|
|
135
|
+
function codexTomlStringTable(entries: Readonly<Record<string, string>>): string {
|
|
136
|
+
const parts = Object.entries(entries).map(
|
|
137
|
+
([key, value]) => `${key}=${codexTomlString(value)}`,
|
|
138
|
+
);
|
|
139
|
+
return `{${parts.join(",")}}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Free-form JSON leaf for Type.Unknown under Codex strict transport.
|
|
144
|
+
* Strict rejects bare untyped nodes and root-level additionalProperties:true;
|
|
145
|
+
* a $defs anyOf of JSON values (with nested additionalProperties as $ref)
|
|
146
|
+
* is accepted and keeps array/object receipts expressible (navigator candidates).
|
|
147
|
+
* Package code still does not validate or reject the receipt against schema.
|
|
148
|
+
*/
|
|
149
|
+
const CODEX_JSON_VALUE_DEF = "codexJsonValue";
|
|
150
|
+
const CODEX_JSON_VALUE_REF = `#/$defs/${CODEX_JSON_VALUE_DEF}`;
|
|
151
|
+
const CODEX_JSON_VALUE_SCHEMA = Object.freeze({
|
|
152
|
+
anyOf: Object.freeze([
|
|
153
|
+
Object.freeze({ type: "string" }),
|
|
154
|
+
Object.freeze({ type: "number" }),
|
|
155
|
+
Object.freeze({ type: "boolean" }),
|
|
156
|
+
Object.freeze({ type: "null" }),
|
|
157
|
+
Object.freeze({ type: "array", items: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }) }),
|
|
158
|
+
Object.freeze({
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: Object.freeze({}),
|
|
161
|
+
required: Object.freeze([] as string[]),
|
|
162
|
+
additionalProperties: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }),
|
|
163
|
+
}),
|
|
164
|
+
]),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Derive a Codex/OpenAI-strict transport schema from the package open schema.
|
|
169
|
+
* Legal open schema is untouched; this is a host-only transmission projection
|
|
170
|
+
* (#646 / 0057 法意 / 0054 strict): every object closes, every property is
|
|
171
|
+
* required, and only originally-optional fields become a null union (official
|
|
172
|
+
* guidance: emulate optional via type|null). Originally-required fields stay
|
|
173
|
+
* non-nullable. Nested open-tool anyOf wrappers are flattened so every branch
|
|
174
|
+
* carries `type`. Type.Unknown / description-only leaves become a free JSON
|
|
175
|
+
* $ref. Package code still does not validate or reject the receipt.
|
|
176
|
+
*/
|
|
177
|
+
export function closeJsonSchemaForCodex(
|
|
178
|
+
schema: Readonly<Record<string, unknown>>,
|
|
179
|
+
): Record<string, unknown> {
|
|
180
|
+
const closed = closeSchemaNode(schema) as Record<string, unknown>;
|
|
181
|
+
const existingDefs = isPlainObject(closed.$defs)
|
|
182
|
+
? (closed.$defs as Record<string, unknown>)
|
|
183
|
+
: {};
|
|
184
|
+
return {
|
|
185
|
+
...closed,
|
|
186
|
+
$defs: {
|
|
187
|
+
...existingDefs,
|
|
188
|
+
[CODEX_JSON_VALUE_DEF]: CODEX_JSON_VALUE_SCHEMA,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Shared plain-object guard for headless host schema/event reduction. */
|
|
194
|
+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
195
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Pure null leaf only — composite type|null is stripped in-leaf, not dropped whole. */
|
|
199
|
+
function isPureNullTypeSchema(value: unknown): boolean {
|
|
200
|
+
if (!isPlainObject(value)) return false;
|
|
201
|
+
if (value.type === "null") return true;
|
|
202
|
+
if (Array.isArray(value.type) && value.type.length > 0 && value.type.every((t) => t === "null")) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Within a leaf, remove null members from type arrays.
|
|
210
|
+
* Required edges keep the non-null type(s); optional edges re-add null once.
|
|
211
|
+
*/
|
|
212
|
+
function stripNullFromLeafType(leaf: unknown): unknown {
|
|
213
|
+
if (!isPlainObject(leaf) || !Array.isArray(leaf.type)) return leaf;
|
|
214
|
+
const nonNull = leaf.type.filter((t) => t !== "null");
|
|
215
|
+
if (nonNull.length === leaf.type.length) return leaf;
|
|
216
|
+
if (nonNull.length === 0) return { ...leaf, type: "null" };
|
|
217
|
+
if (nonNull.length === 1) return { ...leaf, type: nonNull[0] };
|
|
218
|
+
return { ...leaf, type: nonNull };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Flatten nested anyOf wrappers into concrete leaf schemas.
|
|
223
|
+
* Open-tool unions often wrap leaves in description-only anyOf shells that
|
|
224
|
+
* lack `type`; strict structured output rejects those intermediate nodes.
|
|
225
|
+
*/
|
|
226
|
+
function flattenUnionLeaves(schema: unknown): unknown[] {
|
|
227
|
+
if (!isPlainObject(schema)) return [schema];
|
|
228
|
+
if (Array.isArray(schema.anyOf)) {
|
|
229
|
+
return schema.anyOf.flatMap(flattenUnionLeaves);
|
|
230
|
+
}
|
|
231
|
+
return [schema];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Drop pure-null leaves after in-leaf null stripping; optional edges re-add null once. */
|
|
235
|
+
function nonNullLeaves(schema: unknown): unknown[] {
|
|
236
|
+
return flattenUnionLeaves(schema)
|
|
237
|
+
.map(stripNullFromLeafType)
|
|
238
|
+
.filter((leaf) => !isPureNullTypeSchema(leaf));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Property edge under strict transport.
|
|
243
|
+
* Required → closed non-null leaf(s). Optional → closed leaf(s) + null.
|
|
244
|
+
*/
|
|
245
|
+
function closePropertySchema(schema: unknown, optional: boolean): unknown {
|
|
246
|
+
const leaves = nonNullLeaves(schema).map(closeSchemaNode);
|
|
247
|
+
if (leaves.length === 0) return { type: "null" };
|
|
248
|
+
if (!optional) {
|
|
249
|
+
return leaves.length === 1 ? leaves[0] : { anyOf: leaves };
|
|
250
|
+
}
|
|
251
|
+
return { anyOf: [...leaves, { type: "null" }] };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Strict generators require every schema node to declare `type` (or a $ref).
|
|
256
|
+
* Type.Unknown / description-only leaves → free JSON $ref (not string): array
|
|
257
|
+
* and object receipts stay expressible under --output-schema.
|
|
258
|
+
*/
|
|
259
|
+
function ensureTypedLeaf(schema: Record<string, unknown>): Record<string, unknown> {
|
|
260
|
+
if (schema.type !== undefined) return schema;
|
|
261
|
+
if (typeof schema.$ref === "string") return schema;
|
|
262
|
+
if (isPlainObject(schema.properties) || schema.additionalProperties !== undefined) {
|
|
263
|
+
return { ...schema, type: "object" };
|
|
264
|
+
}
|
|
265
|
+
if (schema.items !== undefined) {
|
|
266
|
+
return { ...schema, type: "array" };
|
|
267
|
+
}
|
|
268
|
+
if (schema.const !== undefined) {
|
|
269
|
+
const value = schema.const;
|
|
270
|
+
if (value === null) return { ...schema, type: "null" };
|
|
271
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
272
|
+
return { ...schema, type: typeof value };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
276
|
+
const sample = schema.enum.find((item) => item !== null);
|
|
277
|
+
if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
|
|
278
|
+
return { ...schema, type: typeof sample };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Free JSON via $defs. $ref must stand alone (strict rejects sibling keys).
|
|
282
|
+
return { $ref: CODEX_JSON_VALUE_REF };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function originalRequiredNames(node: Record<string, unknown>): ReadonlySet<string> {
|
|
286
|
+
if (!Array.isArray(node.required)) return new Set();
|
|
287
|
+
return new Set(node.required.filter((item): item is string => typeof item === "string"));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function closeSchemaNode(node: unknown): unknown {
|
|
291
|
+
if (!isPlainObject(node)) return node;
|
|
292
|
+
|
|
293
|
+
// Already a ref (free-JSON leaf or pre-existing) — do not retype.
|
|
294
|
+
if (typeof node.$ref === "string") return node;
|
|
295
|
+
|
|
296
|
+
// Union node: flatten then close each concrete leaf (do not keep untyped shells).
|
|
297
|
+
if (Array.isArray(node.anyOf)) {
|
|
298
|
+
const leaves = nonNullLeaves(node).map(closeSchemaNode);
|
|
299
|
+
if (leaves.length === 0) return { type: "null" };
|
|
300
|
+
if (leaves.length === 1) return leaves[0];
|
|
301
|
+
return { anyOf: leaves };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let out: Record<string, unknown> = { ...node };
|
|
305
|
+
|
|
306
|
+
if (node.items !== undefined) {
|
|
307
|
+
out.items = closeSchemaNode(node.items);
|
|
308
|
+
}
|
|
309
|
+
if (isPlainObject(node.$defs)) {
|
|
310
|
+
out.$defs = Object.fromEntries(
|
|
311
|
+
Object.entries(node.$defs).map(([key, value]) => [key, closeSchemaNode(value)]),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const hasProperties = isPlainObject(node.properties);
|
|
316
|
+
const isObjectType =
|
|
317
|
+
node.type === "object"
|
|
318
|
+
|| (Array.isArray(node.type) && node.type.includes("object"))
|
|
319
|
+
|| hasProperties
|
|
320
|
+
|| node.additionalProperties !== undefined;
|
|
321
|
+
|
|
322
|
+
if (hasProperties) {
|
|
323
|
+
const props = node.properties as Record<string, unknown>;
|
|
324
|
+
const wasRequired = originalRequiredNames(node);
|
|
325
|
+
const closedProps: Record<string, unknown> = {};
|
|
326
|
+
const required: string[] = [];
|
|
327
|
+
for (const [name, propSchema] of Object.entries(props)) {
|
|
328
|
+
required.push(name);
|
|
329
|
+
closedProps[name] = closePropertySchema(propSchema, !wasRequired.has(name));
|
|
330
|
+
}
|
|
331
|
+
out.properties = closedProps;
|
|
332
|
+
out.required = required;
|
|
333
|
+
out.additionalProperties = false;
|
|
334
|
+
if (out.type === undefined) out.type = "object";
|
|
335
|
+
// Object nodes must not also carry residual anyOf from the open copy.
|
|
336
|
+
delete out.anyOf;
|
|
337
|
+
} else if (isObjectType) {
|
|
338
|
+
out.additionalProperties = false;
|
|
339
|
+
if (!Array.isArray(out.required)) out.required = [];
|
|
340
|
+
if (out.type === undefined) out.type = "object";
|
|
341
|
+
} else {
|
|
342
|
+
out = ensureTypedLeaf(out);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Project shared-envelope MCP rows into `codex -c mcp_servers.<name>.*` argv pairs.
|
|
350
|
+
* Dot-path + TOML values per official config-advanced; spawn argv (no shell).
|
|
351
|
+
*/
|
|
352
|
+
function stringEnvironment(value: unknown): Record<string, string> | undefined {
|
|
353
|
+
const entries = Array.isArray(value)
|
|
354
|
+
? value.flatMap((item) => {
|
|
355
|
+
if (!isPlainObject(item) || typeof item.name !== "string" || typeof item.value !== "string") return [];
|
|
356
|
+
return [[item.name, item.value] as const];
|
|
357
|
+
})
|
|
358
|
+
: isPlainObject(value)
|
|
359
|
+
? Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string")
|
|
360
|
+
: [];
|
|
361
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function codexMcpConfigArgs(
|
|
365
|
+
mcpServers: readonly Readonly<Record<string, unknown>>[],
|
|
366
|
+
): string[] {
|
|
367
|
+
const args: string[] = [];
|
|
368
|
+
for (const row of mcpServers) {
|
|
369
|
+
const name = typeof row.name === "string" ? row.name : undefined;
|
|
370
|
+
const command = typeof row.command === "string" ? row.command : undefined;
|
|
371
|
+
if (name === undefined || name === "" || command === undefined || command === "") continue;
|
|
372
|
+
const prefix = `mcp_servers.${name}`;
|
|
373
|
+
// required=true: fail the exec if the AK relay cannot handshake (official:
|
|
374
|
+
// required MCP init failure exits with error). Without it, a silent drop
|
|
375
|
+
// would hide a broken intermediate-tool channel (#646 须真跑证②).
|
|
376
|
+
args.push("-c", `${prefix}.command=${codexTomlString(command)}`);
|
|
377
|
+
args.push("-c", `${prefix}.required=true`);
|
|
378
|
+
if (Array.isArray(row.args) && row.args.every((item): item is string => typeof item === "string")) {
|
|
379
|
+
args.push("-c", `${prefix}.args=${codexTomlStringArray(row.args)}`);
|
|
380
|
+
}
|
|
381
|
+
const env = stringEnvironment(row.env);
|
|
382
|
+
if (env !== undefined) {
|
|
383
|
+
args.push("-c", `${prefix}.env=${codexTomlStringTable(env)}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return args;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Build one `codex exec` / `codex exec resume` argv (#646).
|
|
391
|
+
* New: `exec --approve-for-me … prompt`.
|
|
392
|
+
* Resume: `exec --approve-for-me resume <thread_id> … prompt`.
|
|
393
|
+
* Approval stays on the parent command so new and resumed turns use Codex's
|
|
394
|
+
* automatic review with its workspace-write sandbox.
|
|
395
|
+
*/
|
|
396
|
+
export function codexTurnArgs(options: {
|
|
397
|
+
readonly prompt: string;
|
|
398
|
+
/** Absolute path for `-c model_instructions_file=…`. */
|
|
399
|
+
readonly systemPromptPath: string;
|
|
400
|
+
/** Absolute path for `--output-schema` (closed transport schema). */
|
|
401
|
+
readonly outputSchemaPath: string;
|
|
402
|
+
readonly mcpServers: readonly Readonly<Record<string, unknown>>[];
|
|
403
|
+
readonly model?: string;
|
|
404
|
+
readonly effort?: string;
|
|
405
|
+
/**
|
|
406
|
+
* New turn: host mints thread_id (captured from JSONL).
|
|
407
|
+
* Resume: package-bound thread_id via `exec resume <id>`.
|
|
408
|
+
*/
|
|
409
|
+
readonly session: { readonly kind: "new" } | { readonly kind: "resume"; readonly id: string };
|
|
410
|
+
/** When cwd is not a git work tree, pass `--skip-git-repo-check`. */
|
|
411
|
+
readonly skipGitRepoCheck?: boolean;
|
|
412
|
+
/**
|
|
413
|
+
* Extra writable roots under workspace-write (absolute paths).
|
|
414
|
+
* Git worktrees need the common dir writable for index.lock / commit
|
|
415
|
+
* (official `sandbox_workspace_write.writable_roots` / `--add-dir`).
|
|
416
|
+
*/
|
|
417
|
+
readonly writableRoots?: readonly string[];
|
|
418
|
+
}): string[] {
|
|
419
|
+
// Parent-command flag must precede the resume subcommand.
|
|
420
|
+
const args: string[] = ["exec", "--approve-for-me"];
|
|
421
|
+
if (options.session.kind === "resume") {
|
|
422
|
+
args.push("resume", options.session.id);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// JSONL event stream: thread_id + final agent_message + turn.completed/failed.
|
|
426
|
+
args.push("--json");
|
|
427
|
+
// Operator config/MCP off; auth still uses CODEX_HOME (official).
|
|
428
|
+
// Project/system config and AGENTS.md have no official suppression switch.
|
|
429
|
+
args.push("--ignore-user-config", "--ignore-rules");
|
|
430
|
+
const roots = (options.writableRoots ?? []).filter((root) => root !== "");
|
|
431
|
+
if (roots.length > 0) {
|
|
432
|
+
// Resume has no --add-dir; the config key keeps extra roots available on both paths.
|
|
433
|
+
args.push(
|
|
434
|
+
"-c",
|
|
435
|
+
`sandbox_workspace_write.writable_roots=${codexTomlStringArray(roots)}`,
|
|
436
|
+
);
|
|
437
|
+
if (options.session.kind === "new") {
|
|
438
|
+
for (const root of roots) {
|
|
439
|
+
args.push("--add-dir", root);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
args.push("-c", `model_instructions_file=${codexTomlString(options.systemPromptPath)}`);
|
|
445
|
+
args.push("--output-schema", options.outputSchemaPath);
|
|
446
|
+
args.push(...codexMcpConfigArgs(options.mcpServers));
|
|
447
|
+
|
|
448
|
+
if (options.model !== undefined && options.model !== "") {
|
|
449
|
+
args.push("-m", options.model);
|
|
450
|
+
}
|
|
451
|
+
if (options.effort !== undefined && options.effort !== "") {
|
|
452
|
+
args.push("-c", `model_reasoning_effort=${codexTomlString(options.effort)}`);
|
|
453
|
+
}
|
|
454
|
+
if (options.skipGitRepoCheck === true) {
|
|
455
|
+
args.push("--skip-git-repo-check");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Prompt last as positional. `--` stops option parsing so a leading `-`
|
|
459
|
+
// (markdown lists, pasted flags, rulings) is not eaten by clap (codex tip:
|
|
460
|
+
// "use '-- -s'"). Same for new and resume — both end here.
|
|
461
|
+
args.push("--", options.prompt);
|
|
462
|
+
return args;
|
|
463
|
+
}
|
|
464
|
+
|
|
93
465
|
/**
|
|
94
466
|
* Project shared-envelope MCP server rows into Claude `--mcp-config` JSON.
|
|
95
467
|
* Env stays a plain object (Claude CLI shape); ACP rows use `{name,value}[]`.
|
|
@@ -104,19 +476,8 @@ export function headlessMcpConfigDocument(
|
|
|
104
476
|
if (name === undefined || name === "" || command === undefined || command === "") continue;
|
|
105
477
|
const entry: Record<string, unknown> = { command };
|
|
106
478
|
if (Array.isArray(row.args)) entry.args = row.args;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
for (const item of row.env) {
|
|
110
|
-
if (typeof item !== "object" || item === null) continue;
|
|
111
|
-
const record = item as { name?: unknown; value?: unknown };
|
|
112
|
-
if (typeof record.name === "string" && typeof record.value === "string") {
|
|
113
|
-
env[record.name] = record.value;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (Object.keys(env).length > 0) entry.env = env;
|
|
117
|
-
} else if (typeof row.env === "object" && row.env !== null && !Array.isArray(row.env)) {
|
|
118
|
-
entry.env = row.env;
|
|
119
|
-
}
|
|
479
|
+
const env = stringEnvironment(row.env);
|
|
480
|
+
if (env !== undefined) entry.env = env;
|
|
120
481
|
servers[name] = entry;
|
|
121
482
|
}
|
|
122
483
|
return Object.freeze({ mcpServers: Object.freeze(servers) });
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Production composition for the generic headless CLI RoleTurnHost (#645).
|
|
2
|
+
* Production composition for the generic headless CLI RoleTurnHost (#645 / #646).
|
|
3
3
|
* Agent subprocesses inherit the operator home and credentials in place.
|
|
4
4
|
* No HOME rewrite, no isolated home, no credential parameters — CLI owns auth.
|
|
5
5
|
* Sitian records on the run are the dossier; host private sessions stay private.
|
|
6
6
|
*
|
|
7
|
-
* Intermediate AK tools ride the shared envelope MCP relay
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (
|
|
7
|
+
* Intermediate AK tools ride the shared envelope MCP relay (Claude `--mcp-config`,
|
|
8
|
+
* codex `-c mcp_servers.*`). The terminating receipt is the host-native schema
|
|
9
|
+
* channel only (#750 submission-tool-is-schema-channel) — terminating tool is not
|
|
10
|
+
* listed on MCP (Claude `--json-schema`, codex `--output-schema` closed projection).
|
|
11
11
|
*/
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
13
|
|