@akagilnc/pi-workflow-roles 0.1.2312 → 0.1.2322
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/evidence-child-executor.js +33 -3
- package/dist/public-cli/config.js +491 -0
- package/dist/public-cli/main.js +103 -21
- package/dist/public-cli/registry.js +121 -0
- package/package.json +1 -1
- package/src/evidence-child-executor.ts +58 -3
- package/src/gatekeeper-role.ts +5 -1
- package/src/public-cli/cli.ts +43 -7
- package/src/public-cli/config.ts +122 -27
- package/src/public-cli/option-definitions.ts +2 -0
- package/src/public-cli/registry.ts +25 -2
|
@@ -11,6 +11,7 @@ import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_E
|
|
|
11
11
|
import { createEngineDetourToolDefinition } from "./engine-detour-tool.js";
|
|
12
12
|
import { engineNameFromEnv } from "./engine-detour.js";
|
|
13
13
|
import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, } from "./package-resources/engine-material.js";
|
|
14
|
+
import { formatModelSpec, loadPublicCliConfig, resolveMenxiaOfficerModelSelection, } from "./public-cli/config.js";
|
|
14
15
|
import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
|
|
15
16
|
import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.js";
|
|
16
17
|
import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.js";
|
|
@@ -71,7 +72,8 @@ export async function withInProcessScratch(options, run) {
|
|
|
71
72
|
*/
|
|
72
73
|
export async function createInheritedRuntime(options) {
|
|
73
74
|
const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
|
|
74
|
-
|
|
75
|
+
// Explicit override wins; never silent-fallback to parent when override is set (#453).
|
|
76
|
+
const activeModel = options.model ?? options.context.model;
|
|
75
77
|
if (activeModel === undefined)
|
|
76
78
|
throw new Error(`${options.label} requires an active model`);
|
|
77
79
|
const dispatch = await prepareComplianceDispatch(activeModel, options.context, options.label);
|
|
@@ -243,9 +245,11 @@ export async function createInheritedRuntime(options) {
|
|
|
243
245
|
})();
|
|
244
246
|
return wrapped;
|
|
245
247
|
};
|
|
248
|
+
// Provider id must match activeModel.provider so ModelRuntime auth lookup
|
|
249
|
+
// finds this registration when a menxia override changes provider (#453).
|
|
246
250
|
const provider = options.idleRetry === true || options.runCompletion !== undefined
|
|
247
251
|
? {
|
|
248
|
-
id:
|
|
252
|
+
id: activeModel.provider,
|
|
249
253
|
name: parentProvider?.name ?? options.label,
|
|
250
254
|
auth: {
|
|
251
255
|
apiKey: {
|
|
@@ -572,6 +576,28 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
|
|
|
572
576
|
}
|
|
573
577
|
});
|
|
574
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Resolve province child model from public-cli persistent config (#453).
|
|
581
|
+
* Own override > gatekeeper override > unset (inherit parent). Availability
|
|
582
|
+
* failures throw — createInheritedRuntime must not silent-fallback to parent.
|
|
583
|
+
*/
|
|
584
|
+
async function resolveMenxiaSeatModelOptions(context, seat, roleLabel) {
|
|
585
|
+
const selection = resolveMenxiaOfficerModelSelection(await loadPublicCliConfig(), seat);
|
|
586
|
+
if (selection === undefined)
|
|
587
|
+
return {};
|
|
588
|
+
const find = context.modelRegistry.find?.bind(context.modelRegistry);
|
|
589
|
+
if (typeof find !== "function") {
|
|
590
|
+
throw new Error(`${roleLabel} model registry cannot resolve models`);
|
|
591
|
+
}
|
|
592
|
+
const model = find(selection.provider, selection.model);
|
|
593
|
+
if (model === undefined) {
|
|
594
|
+
throw new Error(`${roleLabel} model is unavailable: ${formatModelSpec(selection)}`);
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
model,
|
|
598
|
+
...(selection.thinking === undefined ? {} : { thinkingLevel: selection.thinking }),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
575
601
|
/**
|
|
576
602
|
* Auditor lifecycle via the shared in-process helper.
|
|
577
603
|
* Adapter keeps role label / soul / decision tool / result projection only.
|
|
@@ -581,10 +607,14 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
|
|
|
581
607
|
export async function executeAuditorChild(options) {
|
|
582
608
|
const { createRecordSession } = await import("./archivist-record-entry.js");
|
|
583
609
|
return withInProcessScratch({ prefix: "ak-auditor-role-" }, async (scratch) => {
|
|
610
|
+
const menxia = options.menxiaSeat === undefined
|
|
611
|
+
? {}
|
|
612
|
+
: await resolveMenxiaSeatModelOptions(options.context, options.menxiaSeat, options.roleLabel);
|
|
584
613
|
const inherited = await createInheritedRuntime({
|
|
585
614
|
context: options.context,
|
|
586
615
|
label: options.roleLabel,
|
|
587
616
|
idleRetry: true,
|
|
617
|
+
...(menxia.model === undefined ? {} : { model: menxia.model }),
|
|
588
618
|
...(options.runCompletion === undefined
|
|
589
619
|
? {}
|
|
590
620
|
: { runCompletion: options.runCompletion, injectedSystemPrompt: options.systemPrompt }),
|
|
@@ -640,7 +670,7 @@ export async function executeAuditorChild(options) {
|
|
|
640
670
|
cwd,
|
|
641
671
|
agentDir: scratch,
|
|
642
672
|
model: inherited.model,
|
|
643
|
-
thinkingLevel: options.context.thinkingLevel ?? "off",
|
|
673
|
+
thinkingLevel: menxia.thinkingLevel ?? options.context.thinkingLevel ?? "off",
|
|
644
674
|
modelRuntime: inherited.runtime,
|
|
645
675
|
systemPrompt: options.systemPrompt,
|
|
646
676
|
customTools: [{ ...options.dossierTool, label: options.roleLabel }, tool],
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent and effective per-seat model/thinking configuration for ak-role.
|
|
3
|
+
*/
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { assertLegalEngineName } from "../package-resources/engine-material.js";
|
|
8
|
+
import { AUTOMATIC_CONFIGURABLE_SEATS, PUBLIC_CALLABLE_ROLES, PUBLIC_CONFIGURABLE_SEATS, isAutomaticConfigurableSeat, isPublicCallableRole, isPublicConfigurableSeat, publicStartupCandidates, } from "./registry.js";
|
|
9
|
+
/** Province officers that may carry a persistent model override (#453). */
|
|
10
|
+
export const MENXIA_OFFICER_SEATS = [
|
|
11
|
+
"gatekeeper",
|
|
12
|
+
"inspector",
|
|
13
|
+
"notary",
|
|
14
|
+
];
|
|
15
|
+
export function isMenxiaOfficerSeat(value) {
|
|
16
|
+
return MENXIA_OFFICER_SEATS.includes(value);
|
|
17
|
+
}
|
|
18
|
+
const THINKING_LEVELS = new Set([
|
|
19
|
+
"off",
|
|
20
|
+
"minimal",
|
|
21
|
+
"low",
|
|
22
|
+
"medium",
|
|
23
|
+
"high",
|
|
24
|
+
"xhigh",
|
|
25
|
+
"max",
|
|
26
|
+
]);
|
|
27
|
+
export function publicCliConfigPath(home = homedir()) {
|
|
28
|
+
return join(home, ".ak-roles", "public-cli.json");
|
|
29
|
+
}
|
|
30
|
+
export async function loadPublicCliConfig(home = homedir()) {
|
|
31
|
+
const path = publicCliConfigPath(home);
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(path, "utf8");
|
|
34
|
+
return parsePublicCliConfig(JSON.parse(raw));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof Error &&
|
|
38
|
+
"code" in error &&
|
|
39
|
+
error.code === "ENOENT") {
|
|
40
|
+
return { seats: {} };
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function savePublicCliConfig(config, home = homedir()) {
|
|
46
|
+
const path = publicCliConfigPath(home);
|
|
47
|
+
await mkdir(dirname(path), { recursive: true });
|
|
48
|
+
const normalized = parsePublicCliConfig(config);
|
|
49
|
+
await writeFile(path, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
50
|
+
}
|
|
51
|
+
export function setPersistentSeatConfig(config, seat, selection) {
|
|
52
|
+
const previous = config.seats[seat];
|
|
53
|
+
return {
|
|
54
|
+
// Spread preserves sibling top-level keys such as autoResumeLimit (#422):
|
|
55
|
+
// a seat write must never silently drop them.
|
|
56
|
+
...config,
|
|
57
|
+
seats: {
|
|
58
|
+
...config.seats,
|
|
59
|
+
[seat]: {
|
|
60
|
+
...selection,
|
|
61
|
+
// Model rewrite preserves a previously configured engine axis.
|
|
62
|
+
...(previous?.engine === undefined ? {} : { engine: previous.engine }),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Clear a menxia officer's persistent model override (#453).
|
|
69
|
+
* Scope is MenxiaOfficerSeat only — non-province seats have no destructive clear seam.
|
|
70
|
+
* Only notary may retain an engine-only residual so direct notary activation keeps
|
|
71
|
+
* its labor engine while model resolution returns to startup / province inheritance.
|
|
72
|
+
* gatekeeper/inspector drop the whole row. Already-absent seats are a no-op.
|
|
73
|
+
*/
|
|
74
|
+
export function clearPersistentSeatConfig(config, seat) {
|
|
75
|
+
const previous = config.seats[seat];
|
|
76
|
+
if (previous === undefined)
|
|
77
|
+
return config;
|
|
78
|
+
// Engine-only residual ownership is notary-only — never widen to other officers.
|
|
79
|
+
if (seat === "notary" && previous.engine !== undefined) {
|
|
80
|
+
return {
|
|
81
|
+
...config,
|
|
82
|
+
seats: {
|
|
83
|
+
...config.seats,
|
|
84
|
+
notary: { engine: previous.engine },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const { [seat]: _dropped, ...seats } = config.seats;
|
|
89
|
+
return { ...config, seats };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Province-only model selection (#453): officer persistent > gatekeeper persistent
|
|
93
|
+
* > unset (caller inherits parent session). Never consults startup candidates —
|
|
94
|
+
* direct `ak-role notary` keeps resolveEffectiveSeat; only province reads this.
|
|
95
|
+
* Engine-only residual is not a model override.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveMenxiaOfficerModelSelection(config, officer) {
|
|
98
|
+
const ownModel = seatModelOnly(config.seats[officer]);
|
|
99
|
+
if (ownModel !== undefined)
|
|
100
|
+
return ownModel;
|
|
101
|
+
if (officer === "gatekeeper")
|
|
102
|
+
return undefined;
|
|
103
|
+
return seatModelOnly(config.seats.gatekeeper);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Seats that own the labor-engine axis (#391: PUBLIC_CALLABLE_ROLES only).
|
|
107
|
+
* Navigator is configurable for model but has no independent activation path.
|
|
108
|
+
*/
|
|
109
|
+
export function isEngineAxisSeat(seat) {
|
|
110
|
+
return isPublicCallableRole(seat);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Set or clear persistent engine on a callable role seat (#356 / #378 / #391 / #453).
|
|
114
|
+
* First engine still requires an existing seat row (model, or notary engine residual).
|
|
115
|
+
* Clearing engine from a notary engine-only residual drops the empty row; clearing
|
|
116
|
+
* engine from a model+engine row leaves model-only. Seat type is PublicCallableRole
|
|
117
|
+
* (navigator excluded at the type boundary).
|
|
118
|
+
*/
|
|
119
|
+
export function setPersistentSeatEngine(config, seat, engine) {
|
|
120
|
+
const previous = config.seats[seat];
|
|
121
|
+
if (previous === undefined) {
|
|
122
|
+
throw new Error(`config seat ${seat} has no persistent model; set provider/model[:thinking] before engine`);
|
|
123
|
+
}
|
|
124
|
+
if (engine === undefined) {
|
|
125
|
+
const { engine: _dropped, ...modelOnly } = previous;
|
|
126
|
+
// Engine-only residual with engine cleared → drop the empty row.
|
|
127
|
+
if (seatModelOnly(modelOnly) === undefined) {
|
|
128
|
+
const { [seat]: _row, ...seats } = config.seats;
|
|
129
|
+
return { ...config, seats };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
...config,
|
|
133
|
+
seats: {
|
|
134
|
+
...config.seats,
|
|
135
|
+
[seat]: modelOnly,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Engine-name path-safety syntax is owned solely by assertLegalEngineName
|
|
140
|
+
// (call-request + config-parse seams). Setter is pure seat mutation.
|
|
141
|
+
return {
|
|
142
|
+
...config,
|
|
143
|
+
seats: {
|
|
144
|
+
...config.seats,
|
|
145
|
+
[seat]: { ...previous, engine },
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* #422 value domain authority for the auto-resume ceiling: non-negative integer,
|
|
151
|
+
* no package-local upper bound (ADR 0035). `0` is legal and means auto-resume is
|
|
152
|
+
* disabled (a single dispatch, no in-place retry). Negative numbers, fractions,
|
|
153
|
+
* NaN, Infinity and non-number types are rejected loudly — never silently coerced.
|
|
154
|
+
*/
|
|
155
|
+
export function parseAutoResumeLimit(value) {
|
|
156
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
157
|
+
throw new Error(`auto-resume limit must be a non-negative integer, got ${JSON.stringify(value)}`);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* #422: set the persistent autoResumeLimit top-level key. Pure mutation that
|
|
163
|
+
* preserves all sibling keys (seats included).
|
|
164
|
+
*/
|
|
165
|
+
export function setAutoResumeLimit(config, limit) {
|
|
166
|
+
return { ...config, autoResumeLimit: parseAutoResumeLimit(limit) };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Strip optional engine so activation model argv never sees the engine axis.
|
|
170
|
+
* Engine-only residual (model cleared) yields undefined — not a model override.
|
|
171
|
+
*/
|
|
172
|
+
export function seatModelOnly(seat) {
|
|
173
|
+
if (seat?.provider === undefined || seat.model === undefined)
|
|
174
|
+
return undefined;
|
|
175
|
+
return seat.thinking === undefined
|
|
176
|
+
? { provider: seat.provider, model: seat.model }
|
|
177
|
+
: { provider: seat.provider, model: seat.model, thinking: seat.thinking };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Config-parse seam: engine axis is PUBLIC_CALLABLE_ROLES; engine names need only
|
|
181
|
+
* path-safety syntax (no closed material catalog; #376 / #378 / #391 / ADR 0069).
|
|
182
|
+
* Disk-handwritten navigator.engine is rejected (no independent activation → silent
|
|
183
|
+
* ineffective would violate failure honesty). Call after load / before dispatch (#356).
|
|
184
|
+
* Syntax authority = assertLegalEngineName (no injected duplicate).
|
|
185
|
+
*/
|
|
186
|
+
export function validatePublicCliConfigEngines(config, _packageRoot) {
|
|
187
|
+
for (const seat of Object.keys(config.seats)) {
|
|
188
|
+
const row = config.seats[seat];
|
|
189
|
+
if (row?.engine === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
if (!isEngineAxisSeat(seat)) {
|
|
192
|
+
throw new Error(`config seat ${seat} cannot persist engine: no independent activation path; storing would be silently ineffective`);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
assertLegalEngineName(row.engine);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw new Error(`config seat ${seat} engine is illegal: ${row.engine}`, { cause: error });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export function parseModelSpec(spec, fallbackThinking) {
|
|
203
|
+
const trimmed = spec.trim();
|
|
204
|
+
if (!trimmed) {
|
|
205
|
+
throw new Error("model specification must be non-empty");
|
|
206
|
+
}
|
|
207
|
+
const thinkingSplit = trimmed.lastIndexOf(":");
|
|
208
|
+
let modelPart = trimmed;
|
|
209
|
+
let thinking = fallbackThinking;
|
|
210
|
+
// #346: no colon → bare provider/model is legal. Colon present (any index,
|
|
211
|
+
// including 0) → suffix must be a typed PublicThinkingLevel (empty/unknown
|
|
212
|
+
// stay format rejects; never swallow ":…" into the model name).
|
|
213
|
+
if (thinkingSplit !== -1) {
|
|
214
|
+
const maybeThinking = trimmed.slice(thinkingSplit + 1);
|
|
215
|
+
if (!THINKING_LEVELS.has(maybeThinking)) {
|
|
216
|
+
throw new Error(`model specification must be provider/model[:thinking], got ${spec}`);
|
|
217
|
+
}
|
|
218
|
+
thinking = maybeThinking;
|
|
219
|
+
modelPart = trimmed.slice(0, thinkingSplit);
|
|
220
|
+
}
|
|
221
|
+
const slash = modelPart.indexOf("/");
|
|
222
|
+
if (slash <= 0 || slash === modelPart.length - 1) {
|
|
223
|
+
throw new Error(`model specification must be provider/model[:thinking], got ${spec}`);
|
|
224
|
+
}
|
|
225
|
+
const provider = modelPart.slice(0, slash);
|
|
226
|
+
const model = modelPart.slice(slash + 1);
|
|
227
|
+
// #346/#384: bare provider/model is legal — do not invent thinking.
|
|
228
|
+
return thinking === undefined
|
|
229
|
+
? { provider, model }
|
|
230
|
+
: { provider, model, thinking };
|
|
231
|
+
}
|
|
232
|
+
export function formatModelSpec(selection) {
|
|
233
|
+
const base = `${selection.provider}/${selection.model}`;
|
|
234
|
+
return selection.thinking === undefined ? base : `${base}:${selection.thinking}`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Single source: seat model selection → Pi argv at the public CLI execution seam.
|
|
238
|
+
* Bare provider/model omits --thinking so pi/model defaults apply; suffix passes through.
|
|
239
|
+
*/
|
|
240
|
+
export function buildSeatModelCliArgs(model) {
|
|
241
|
+
if (model === undefined)
|
|
242
|
+
return [];
|
|
243
|
+
return [
|
|
244
|
+
"--provider",
|
|
245
|
+
model.provider,
|
|
246
|
+
"--model",
|
|
247
|
+
model.model,
|
|
248
|
+
...(model.thinking === undefined ? [] : ["--thinking", model.thinking]),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
function parsePublicCliConfig(value) {
|
|
252
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
253
|
+
throw new Error("public CLI config must be an object");
|
|
254
|
+
}
|
|
255
|
+
const record = value;
|
|
256
|
+
// #422 round-trip preservation: the sibling top-level key must survive every
|
|
257
|
+
// parse→save cycle; an unknown-key drop would silently erase it on any write.
|
|
258
|
+
let autoResumeLimit;
|
|
259
|
+
if (record.autoResumeLimit !== undefined) {
|
|
260
|
+
autoResumeLimit = parseAutoResumeLimit(record.autoResumeLimit);
|
|
261
|
+
}
|
|
262
|
+
if (record.seats === undefined) {
|
|
263
|
+
return {
|
|
264
|
+
seats: {},
|
|
265
|
+
...(autoResumeLimit === undefined ? {} : { autoResumeLimit }),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (record.seats === null ||
|
|
269
|
+
typeof record.seats !== "object" ||
|
|
270
|
+
Array.isArray(record.seats)) {
|
|
271
|
+
throw new Error("public CLI config.seats must be an object");
|
|
272
|
+
}
|
|
273
|
+
const seats = {};
|
|
274
|
+
for (const [key, raw] of Object.entries(record.seats)) {
|
|
275
|
+
if (!PUBLIC_CONFIGURABLE_SEATS.includes(key)) {
|
|
276
|
+
throw new Error(`unknown configurable seat in config: ${key}`);
|
|
277
|
+
}
|
|
278
|
+
seats[key] = parseSeatModelConfig(raw, key);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
seats,
|
|
282
|
+
...(autoResumeLimit === undefined ? {} : { autoResumeLimit }),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function parseSeatModelConfig(value, seat) {
|
|
286
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
287
|
+
throw new Error(`config seat ${seat} must be an object`);
|
|
288
|
+
}
|
|
289
|
+
const raw = value;
|
|
290
|
+
const hasProvider = raw.provider !== undefined;
|
|
291
|
+
const hasModel = raw.model !== undefined;
|
|
292
|
+
if (hasProvider !== hasModel) {
|
|
293
|
+
throw new Error(`config seat ${seat} requires both provider and model`);
|
|
294
|
+
}
|
|
295
|
+
if (raw.engine !== undefined && typeof raw.engine !== "string") {
|
|
296
|
+
// Shape only: engine must be a string field. Path-safety syntax is deferred to
|
|
297
|
+
// validatePublicCliConfigEngines → assertLegalEngineName (single authority).
|
|
298
|
+
throw new Error(`config seat ${seat} engine must be a string`);
|
|
299
|
+
}
|
|
300
|
+
// #453: engine-only residual is legal only for notary after model clear.
|
|
301
|
+
// All other seats keep the baseline provider/model required contract.
|
|
302
|
+
if (!hasProvider) {
|
|
303
|
+
if (seat === "notary" && typeof raw.engine === "string") {
|
|
304
|
+
if (raw.thinking !== undefined) {
|
|
305
|
+
throw new Error(`config seat ${seat} thinking requires provider/model`);
|
|
306
|
+
}
|
|
307
|
+
return { engine: raw.engine };
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`config seat ${seat} requires provider`);
|
|
310
|
+
}
|
|
311
|
+
if (typeof raw.provider !== "string" || raw.provider.trim() === "") {
|
|
312
|
+
throw new Error(`config seat ${seat} requires provider`);
|
|
313
|
+
}
|
|
314
|
+
if (typeof raw.model !== "string" || raw.model.trim() === "") {
|
|
315
|
+
throw new Error(`config seat ${seat} requires model`);
|
|
316
|
+
}
|
|
317
|
+
// #384: thinking is optional on persistent seats; when present it must be typed.
|
|
318
|
+
if (raw.thinking !== undefined) {
|
|
319
|
+
if (typeof raw.thinking !== "string" ||
|
|
320
|
+
!THINKING_LEVELS.has(raw.thinking)) {
|
|
321
|
+
throw new Error(`config seat ${seat} requires a valid thinking level`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const parsed = {
|
|
325
|
+
provider: raw.provider,
|
|
326
|
+
model: raw.model,
|
|
327
|
+
...(raw.thinking === undefined
|
|
328
|
+
? {}
|
|
329
|
+
: { thinking: raw.thinking }),
|
|
330
|
+
...(raw.engine === undefined ? {} : { engine: raw.engine }),
|
|
331
|
+
};
|
|
332
|
+
return parsed;
|
|
333
|
+
}
|
|
334
|
+
export function providerConfigured(credentials, provider) {
|
|
335
|
+
if (provider === "openai-codex")
|
|
336
|
+
return credentials["openai-codex"] === true;
|
|
337
|
+
if (provider === "xai")
|
|
338
|
+
return credentials.xai === true;
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Typed provider-credential absence for the public seat providers ak-role owns.
|
|
343
|
+
* Presence is read from auth.json shape (CredentialProviders), never from stderr prose.
|
|
344
|
+
* Returns undefined for unknown/custom providers (not this package's credential map).
|
|
345
|
+
*/
|
|
346
|
+
export function missingPublicProviderCredential(provider, credentials) {
|
|
347
|
+
if (provider !== "openai-codex" && provider !== "xai")
|
|
348
|
+
return false;
|
|
349
|
+
return !providerConfigured(credentials, provider);
|
|
350
|
+
}
|
|
351
|
+
function pickStartupCandidate(seat, credentials) {
|
|
352
|
+
for (const candidate of publicStartupCandidates(seat)) {
|
|
353
|
+
if (providerConfigured(credentials, candidate.provider)) {
|
|
354
|
+
return { ...candidate };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
function attachEngineAxis(seat, config, invocation) {
|
|
360
|
+
// #391: engine axis is PUBLIC_CALLABLE_ROLES only (single isEngineAxisSeat predicate).
|
|
361
|
+
if (!isEngineAxisSeat(seat.seat)) {
|
|
362
|
+
return {
|
|
363
|
+
...seat,
|
|
364
|
+
engineSource: "unconfigured",
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const persistentEngine = config.seats[seat.seat]?.engine;
|
|
368
|
+
if (invocation?.engine !== undefined) {
|
|
369
|
+
return {
|
|
370
|
+
...seat,
|
|
371
|
+
engine: invocation.engine,
|
|
372
|
+
engineSource: "invocation",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (persistentEngine !== undefined) {
|
|
376
|
+
return {
|
|
377
|
+
...seat,
|
|
378
|
+
engine: persistentEngine,
|
|
379
|
+
engineSource: "persistent",
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
...seat,
|
|
384
|
+
engineSource: "unconfigured",
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function resolveBaseSeat(config, seat, credentials) {
|
|
388
|
+
const automatic = isAutomaticConfigurableSeat(seat);
|
|
389
|
+
// Engine-only residual is not a persistent model source (#453).
|
|
390
|
+
const persistentModel = seatModelOnly(config.seats[seat]);
|
|
391
|
+
if (persistentModel !== undefined) {
|
|
392
|
+
return {
|
|
393
|
+
seat,
|
|
394
|
+
automatic,
|
|
395
|
+
source: "persistent",
|
|
396
|
+
selection: persistentModel,
|
|
397
|
+
engineSource: "unconfigured",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const startup = pickStartupCandidate(seat, credentials);
|
|
401
|
+
if (startup !== undefined) {
|
|
402
|
+
return {
|
|
403
|
+
seat,
|
|
404
|
+
automatic,
|
|
405
|
+
source: "startup",
|
|
406
|
+
selection: startup,
|
|
407
|
+
engineSource: "unconfigured",
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return { seat, automatic, source: "unconfigured", engineSource: "unconfigured" };
|
|
411
|
+
}
|
|
412
|
+
export function resolveEffectiveSeat(config, seat, credentials, invocation) {
|
|
413
|
+
const automatic = isAutomaticConfigurableSeat(seat);
|
|
414
|
+
const hasModelInvocation = invocation !== undefined &&
|
|
415
|
+
(invocation.model !== undefined || invocation.thinking !== undefined);
|
|
416
|
+
let modelSeat;
|
|
417
|
+
if (!hasModelInvocation || invocation === undefined) {
|
|
418
|
+
modelSeat = resolveBaseSeat(config, seat, credentials);
|
|
419
|
+
}
|
|
420
|
+
else if (invocation.model !== undefined) {
|
|
421
|
+
const spec = invocation.model.includes(":") || invocation.thinking === undefined
|
|
422
|
+
? invocation.model
|
|
423
|
+
: `${invocation.model}:${invocation.thinking}`;
|
|
424
|
+
modelSeat = {
|
|
425
|
+
seat,
|
|
426
|
+
automatic,
|
|
427
|
+
source: "invocation",
|
|
428
|
+
selection: parseModelSpec(spec),
|
|
429
|
+
engineSource: "unconfigured",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
const base = resolveBaseSeat(config, seat, credentials);
|
|
434
|
+
if (base.selection === undefined || invocation.thinking === undefined) {
|
|
435
|
+
modelSeat = {
|
|
436
|
+
seat,
|
|
437
|
+
automatic,
|
|
438
|
+
source: "unconfigured",
|
|
439
|
+
engineSource: "unconfigured",
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
modelSeat = {
|
|
444
|
+
seat,
|
|
445
|
+
automatic,
|
|
446
|
+
source: "invocation",
|
|
447
|
+
selection: { ...base.selection, thinking: invocation.thinking },
|
|
448
|
+
engineSource: "unconfigured",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return attachEngineAxis(modelSeat, config, invocation);
|
|
453
|
+
}
|
|
454
|
+
export function effectiveSeatConfigurations(config, credentials, invocation) {
|
|
455
|
+
return PUBLIC_CONFIGURABLE_SEATS.map((seat) => resolveEffectiveSeat(config, seat, credentials, invocation));
|
|
456
|
+
}
|
|
457
|
+
/** Callable roles first (package order), then automatic configurable seats. */
|
|
458
|
+
export function listRolesForDisplay(config, credentials, invocation) {
|
|
459
|
+
const all = effectiveSeatConfigurations(config, credentials, invocation);
|
|
460
|
+
const callable = PUBLIC_CALLABLE_ROLES.map((role) => all.find((entry) => entry.seat === role));
|
|
461
|
+
const automatic = AUTOMATIC_CONFIGURABLE_SEATS.map((seat) => all.find((entry) => entry.seat === seat));
|
|
462
|
+
return [...callable, ...automatic];
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Read configured credential presence from a Pi auth.json document.
|
|
466
|
+
* Only presence of a provider key matters for startup selection (#11).
|
|
467
|
+
*/
|
|
468
|
+
export function credentialProvidersFromAuthData(data) {
|
|
469
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
470
|
+
return { "openai-codex": false, xai: false };
|
|
471
|
+
}
|
|
472
|
+
const record = data;
|
|
473
|
+
return {
|
|
474
|
+
"openai-codex": Object.prototype.hasOwnProperty.call(record, "openai-codex"),
|
|
475
|
+
xai: Object.prototype.hasOwnProperty.call(record, "xai"),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
export async function loadCredentialProviders(agentDir) {
|
|
479
|
+
try {
|
|
480
|
+
const raw = await readFile(join(agentDir, "auth.json"), "utf8");
|
|
481
|
+
return credentialProvidersFromAuthData(JSON.parse(raw));
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (error instanceof Error &&
|
|
485
|
+
"code" in error &&
|
|
486
|
+
error.code === "ENOENT") {
|
|
487
|
+
return { "openai-codex": false, xai: false };
|
|
488
|
+
}
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
}
|