@rosetears/aili-pi 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/THIRD_PARTY_NOTICES.md +12 -12
- package/docs/persistent-agents.md +114 -0
- package/extensions/matrix/index.ts +24 -8
- package/extensions/zentui/config.ts +13 -13
- package/extensions/zentui/format.ts +15 -1
- package/extensions/zentui/gradient.ts +22 -13
- package/extensions/zentui/tool-execution.ts +7 -4
- package/manifests/adapter-evidence.json +53 -20
- package/manifests/capabilities.json +3 -3
- package/manifests/live-verification.json +18 -19
- package/manifests/provenance.json +12 -12
- package/manifests/roles.json +270 -57
- package/manifests/sbom.json +1 -128
- package/manifests/skill-compatibility.json +57 -61
- package/manifests/subagent-provenance.json +8 -15
- package/package.json +3 -3
- package/roles/agent-evaluator.md +8 -3
- package/roles/ai-regression-scout.md +8 -3
- package/roles/browser-qa-runner.md +8 -3
- package/roles/code-reviewer.md +8 -3
- package/roles/code-scout.md +8 -3
- package/roles/convergence-reviewer.md +8 -3
- package/roles/doc-researcher.md +8 -3
- package/roles/e2e-artifact-runner.md +8 -3
- package/roles/general.md +49 -0
- package/roles/implementer.md +8 -3
- package/roles/opensource-sanitizer.md +8 -3
- package/roles/plan-auditor.md +8 -3
- package/roles/pr-test-analyzer.md +8 -3
- package/roles/security-auditor.md +8 -3
- package/roles/silent-failure-reviewer.md +8 -3
- package/roles/spec-miner.md +8 -3
- package/roles/test-coverage-reviewer.md +8 -3
- package/roles/test-engineer.md +8 -3
- package/roles/web-performance-auditor.md +8 -3
- package/roles/web-researcher.md +8 -3
- package/scripts/sync-roles.ts +186 -24
- package/src/runtime/doctor.ts +38 -10
- package/src/runtime/global-resources.ts +5 -1
- package/src/runtime/index.ts +2 -2
- package/src/runtime/persistent-agents/hub.ts +429 -0
- package/src/runtime/persistent-agents/model-selection.ts +363 -0
- package/src/runtime/persistent-agents/output-delivery.ts +356 -0
- package/src/runtime/persistent-agents/permission.ts +223 -0
- package/src/runtime/persistent-agents/policy.ts +311 -0
- package/src/runtime/persistent-agents/production.ts +569 -0
- package/src/runtime/persistent-agents/runtime.ts +164 -0
- package/src/runtime/persistent-agents/sandbox.ts +68 -0
- package/src/runtime/persistent-agents/scheduler.ts +190 -0
- package/src/runtime/persistent-agents/session-factory.ts +184 -0
- package/src/runtime/persistent-agents/storage.ts +775 -0
- package/src/runtime/persistent-agents/task-coordinator.ts +460 -0
- package/src/runtime/persistent-agents/task-schema.ts +141 -0
- package/src/runtime/persistent-agents/types.ts +134 -0
- package/src/runtime/persistent-agents/workspace.ts +335 -0
- package/src/runtime/registry.ts +28 -32
- package/src/runtime/roles.ts +211 -18
- package/src/runtime/rose-context.ts +1 -1
- package/templates/APPEND_SYSTEM.md +1 -1
- package/themes/rose-cyberdeck.json +5 -9
- package/upstream/opencode-global-agents.lock.json +1 -1
- package/src/runtime/subagents.ts +0 -249
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
5
|
+
import { BUNDLED_ROLE_SELECTORS } from "../roles.js";
|
|
6
|
+
import type { CoordinatorJournal } from "./storage.js";
|
|
7
|
+
import { assertNoCredentialMaterial } from "./permission.js";
|
|
8
|
+
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
export type ModelLayer = "one-shot" | "instance" | "project-role" | "user-role" | "profile" | "parent-fallback";
|
|
11
|
+
export type ModelThinking = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
12
|
+
|
|
13
|
+
export interface ModelOverride {
|
|
14
|
+
model: string;
|
|
15
|
+
thinking?: ModelThinking;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ModelOverrideConfig {
|
|
19
|
+
schemaVersion: 1;
|
|
20
|
+
roles: Record<string, ModelOverride>;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LoadedModelConfigs {
|
|
25
|
+
global: ModelOverrideConfig;
|
|
26
|
+
project?: ModelOverrideConfig;
|
|
27
|
+
diagnostics: string[];
|
|
28
|
+
globalBytes: string;
|
|
29
|
+
projectBytes?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const THINKING = new Set<ModelThinking>(["off", "minimal", "low", "medium", "high", "xhigh"]);
|
|
33
|
+
const SELECTORS = new Set<string>(BUNDLED_ROLE_SELECTORS as readonly string[]);
|
|
34
|
+
|
|
35
|
+
export function defaultGlobalModelConfigPath(home = homedir()): string {
|
|
36
|
+
return join(home, ".pi", "agent", "aili", "model-overrides.json");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function defaultProjectModelConfigPath(projectRoot: string): string {
|
|
40
|
+
return resolve(projectRoot, ".pi", "aili", "model-overrides.json");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function emptyConfig(): ModelOverrideConfig {
|
|
44
|
+
return { schemaVersion: 1, roles: {} };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateModelIdentifier(model: string): { provider: string; model: string; canonical: string } {
|
|
48
|
+
const normalized = model.trim();
|
|
49
|
+
const slash = normalized.indexOf("/");
|
|
50
|
+
if (slash <= 0 || slash === normalized.length - 1 || /[\s\0\r\n]/.test(normalized)) {
|
|
51
|
+
throw new Error(`model must use canonical provider/model form: ${model}`);
|
|
52
|
+
}
|
|
53
|
+
return { provider: normalized.slice(0, slash), model: normalized.slice(slash + 1), canonical: normalized };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateOverride(value: unknown, label: string): ModelOverride {
|
|
57
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
58
|
+
const record = value as Record<string, unknown>;
|
|
59
|
+
const unknown = Object.keys(record).filter((key) => key !== "model" && key !== "thinking");
|
|
60
|
+
if (unknown.length > 0) throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
|
|
61
|
+
if (typeof record.model !== "string") throw new Error(`${label}.model is required`);
|
|
62
|
+
const model = validateModelIdentifier(record.model).canonical;
|
|
63
|
+
if (record.thinking !== undefined && !THINKING.has(record.thinking as ModelThinking)) throw new Error(`${label}.thinking is invalid`);
|
|
64
|
+
return { model, thinking: record.thinking as ModelThinking | undefined };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseModelOverrideConfig(bytes: string, scope: string): ModelOverrideConfig {
|
|
68
|
+
let raw: unknown;
|
|
69
|
+
try {
|
|
70
|
+
raw = JSON.parse(bytes);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(`${scope} model config is malformed JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${scope} model config must be an object`);
|
|
75
|
+
const record = raw as Record<string, unknown>;
|
|
76
|
+
const unknown = Object.keys(record).filter((key) => !["schemaVersion", "roles", "metadata"].includes(key));
|
|
77
|
+
if (unknown.length > 0) throw new Error(`${scope} model config contains unknown fields: ${unknown.join(", ")}`);
|
|
78
|
+
if (record.schemaVersion !== 1) throw new Error(`${scope} model config schemaVersion must be 1`);
|
|
79
|
+
if (!record.roles || typeof record.roles !== "object" || Array.isArray(record.roles)) throw new Error(`${scope} model config roles must be an object`);
|
|
80
|
+
const roles: Record<string, ModelOverride> = {};
|
|
81
|
+
for (const [selector, value] of Object.entries(record.roles as Record<string, unknown>)) {
|
|
82
|
+
if (!SELECTORS.has(selector)) throw new Error(`${scope} model config has unknown selector: ${selector}`);
|
|
83
|
+
roles[selector] = validateOverride(value, `${scope}.roles.${selector}`);
|
|
84
|
+
}
|
|
85
|
+
if (record.metadata !== undefined && (!record.metadata || typeof record.metadata !== "object" || Array.isArray(record.metadata))) throw new Error(`${scope} model config metadata must be an object`);
|
|
86
|
+
return { schemaVersion: 1, roles, metadata: record.metadata as Record<string, unknown> | undefined };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readConfig(path: string, scope: string): Promise<{ config: ModelOverrideConfig; bytes: string }> {
|
|
90
|
+
try {
|
|
91
|
+
const bytes = await readFile(path, "utf8");
|
|
92
|
+
return { config: parseModelOverrideConfig(bytes, scope), bytes };
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { config: emptyConfig(), bytes: "" };
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ModelConfigStoreOptions {
|
|
100
|
+
globalPath: string;
|
|
101
|
+
projectPath: string;
|
|
102
|
+
beforeRename?: (scope: "global" | "project", temporaryPath: string, targetPath: string) => void | Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class ModelConfigStore {
|
|
106
|
+
constructor(private readonly options: ModelConfigStoreOptions) {}
|
|
107
|
+
|
|
108
|
+
async load(projectTrusted: boolean): Promise<LoadedModelConfigs> {
|
|
109
|
+
const global = await readConfig(this.options.globalPath, "global");
|
|
110
|
+
if (!projectTrusted) {
|
|
111
|
+
return {
|
|
112
|
+
global: global.config,
|
|
113
|
+
diagnostics: ["project model config ignored because project trust is inactive"],
|
|
114
|
+
globalBytes: global.bytes,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const project = await readConfig(this.options.projectPath, "project");
|
|
118
|
+
return {
|
|
119
|
+
global: global.config,
|
|
120
|
+
project: project.config,
|
|
121
|
+
diagnostics: [],
|
|
122
|
+
globalBytes: global.bytes,
|
|
123
|
+
projectBytes: project.bytes,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async setRole(scope: "global" | "project", selector: string, override: ModelOverride | undefined, projectTrusted: boolean): Promise<ModelOverrideConfig> {
|
|
128
|
+
if (!SELECTORS.has(selector)) throw new Error(`unknown bundled selector: ${selector}`);
|
|
129
|
+
if (scope === "project" && !projectTrusted) throw new Error("project model config write requires active project trust");
|
|
130
|
+
if (override) {
|
|
131
|
+
await assertNoCredentialMaterial(override, "model override");
|
|
132
|
+
validateOverride(override, "model override");
|
|
133
|
+
}
|
|
134
|
+
const path = scope === "global" ? this.options.globalPath : this.options.projectPath;
|
|
135
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
136
|
+
const lockPath = `${path}.lock`;
|
|
137
|
+
let lock;
|
|
138
|
+
try {
|
|
139
|
+
lock = await open(lockPath, "wx", 0o600);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new Error(`${scope} model config lock unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
142
|
+
}
|
|
143
|
+
let temporary: string | undefined;
|
|
144
|
+
try {
|
|
145
|
+
const loaded = await readConfig(path, scope);
|
|
146
|
+
const next: ModelOverrideConfig = {
|
|
147
|
+
...loaded.config,
|
|
148
|
+
roles: { ...loaded.config.roles },
|
|
149
|
+
};
|
|
150
|
+
if (override) next.roles[selector] = validateOverride(override, "model override");
|
|
151
|
+
else delete next.roles[selector];
|
|
152
|
+
temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
153
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
154
|
+
try {
|
|
155
|
+
await handle.writeFile(`${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
156
|
+
await handle.sync();
|
|
157
|
+
} finally {
|
|
158
|
+
await handle.close();
|
|
159
|
+
}
|
|
160
|
+
await this.options.beforeRename?.(scope, temporary, path);
|
|
161
|
+
await rename(temporary, path);
|
|
162
|
+
temporary = undefined;
|
|
163
|
+
return next;
|
|
164
|
+
} finally {
|
|
165
|
+
if (temporary) await rm(temporary, { force: true }).catch(() => undefined);
|
|
166
|
+
await lock.close().catch(() => undefined);
|
|
167
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface CatalogModel {
|
|
173
|
+
provider: string;
|
|
174
|
+
model: string;
|
|
175
|
+
available: boolean;
|
|
176
|
+
authenticated: boolean;
|
|
177
|
+
thinkingLevels?: ModelThinking[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface ModelCatalog {
|
|
181
|
+
resolve(model: string): Promise<CatalogModel | undefined>;
|
|
182
|
+
resolveParentFallback(): Promise<CatalogModel | undefined>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export class OfficialPiModelCatalog implements ModelCatalog {
|
|
186
|
+
constructor(
|
|
187
|
+
private readonly runtime: ModelRuntime,
|
|
188
|
+
private readonly parentModel: { provider: string; id: string } | undefined,
|
|
189
|
+
private readonly thinkingLevels: (model: { provider: string; id: string }) => ModelThinking[] | undefined = () => undefined,
|
|
190
|
+
) {}
|
|
191
|
+
|
|
192
|
+
async resolve(model: string): Promise<CatalogModel | undefined> {
|
|
193
|
+
const parsed = validateModelIdentifier(model);
|
|
194
|
+
const found = this.runtime.getModel(parsed.provider, parsed.model);
|
|
195
|
+
if (!found) return undefined;
|
|
196
|
+
const available = this.runtime.getAvailableSnapshot().some((candidate) => candidate.provider === found.provider && candidate.id === found.id);
|
|
197
|
+
return {
|
|
198
|
+
provider: found.provider,
|
|
199
|
+
model: found.id,
|
|
200
|
+
available,
|
|
201
|
+
authenticated: this.runtime.hasConfiguredAuth(found.provider),
|
|
202
|
+
thinkingLevels: this.thinkingLevels({ provider: found.provider, id: found.id }),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async resolveParentFallback(): Promise<CatalogModel | undefined> {
|
|
207
|
+
if (!this.parentModel) return undefined;
|
|
208
|
+
return await this.resolve(`${this.parentModel.provider}/${this.parentModel.id}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ResolveModelInput {
|
|
213
|
+
selector: string;
|
|
214
|
+
agentId: string;
|
|
215
|
+
oneShot?: ModelOverride;
|
|
216
|
+
instance?: ModelOverride;
|
|
217
|
+
projectRole?: ModelOverride;
|
|
218
|
+
projectTrusted: boolean;
|
|
219
|
+
userRole?: ModelOverride;
|
|
220
|
+
profile?: ModelOverride;
|
|
221
|
+
parentThinking?: ModelThinking;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface ResolvedModelChoice {
|
|
225
|
+
provider: string;
|
|
226
|
+
model: string;
|
|
227
|
+
canonical: string;
|
|
228
|
+
layer: ModelLayer;
|
|
229
|
+
thinking: ModelThinking;
|
|
230
|
+
persistent: boolean;
|
|
231
|
+
oneShot: boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export class ModelSelectionError extends Error {
|
|
235
|
+
constructor(readonly layer: ModelLayer, message: string) {
|
|
236
|
+
super(`${layer} model selection failed: ${message}`);
|
|
237
|
+
this.name = "ModelSelectionError";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function resolveModelChoice(input: ResolveModelInput, catalog: ModelCatalog): Promise<ResolvedModelChoice> {
|
|
242
|
+
const layers: Array<{ layer: ModelLayer; value?: ModelOverride; persistent: boolean }> = [
|
|
243
|
+
{ layer: "one-shot", value: input.oneShot, persistent: false },
|
|
244
|
+
{ layer: "instance", value: input.instance, persistent: true },
|
|
245
|
+
{ layer: "project-role", value: input.projectTrusted ? input.projectRole : undefined, persistent: true },
|
|
246
|
+
{ layer: "user-role", value: input.userRole, persistent: true },
|
|
247
|
+
{ layer: "profile", value: input.profile, persistent: true },
|
|
248
|
+
];
|
|
249
|
+
const selected = layers.find((candidate) => candidate.value);
|
|
250
|
+
const layer = selected?.layer ?? "parent-fallback";
|
|
251
|
+
const requested = selected?.value;
|
|
252
|
+
let candidate: CatalogModel | undefined;
|
|
253
|
+
if (requested) {
|
|
254
|
+
validateModelIdentifier(requested.model);
|
|
255
|
+
candidate = await catalog.resolve(requested.model);
|
|
256
|
+
} else {
|
|
257
|
+
candidate = await catalog.resolveParentFallback();
|
|
258
|
+
}
|
|
259
|
+
if (!candidate) throw new ModelSelectionError(layer, requested ? `unknown explicit model ${requested.model}; lower layers were not considered` : "official parent/fallback model is unavailable");
|
|
260
|
+
const canonical = `${candidate.provider}/${candidate.model}`;
|
|
261
|
+
if (!candidate.available) throw new ModelSelectionError(layer, `${canonical} is unavailable; lower layers were not considered`);
|
|
262
|
+
if (!candidate.authenticated) throw new ModelSelectionError(layer, `${canonical} is unauthenticated; lower layers were not considered`);
|
|
263
|
+
const thinking = requested?.thinking ?? input.parentThinking ?? "medium";
|
|
264
|
+
if (candidate.thinkingLevels && !candidate.thinkingLevels.includes(thinking)) {
|
|
265
|
+
throw new ModelSelectionError(layer, `${canonical} is incompatible with thinking=${thinking}; lower layers were not considered`);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
provider: candidate.provider,
|
|
269
|
+
model: candidate.model,
|
|
270
|
+
canonical,
|
|
271
|
+
layer,
|
|
272
|
+
thinking,
|
|
273
|
+
persistent: selected?.persistent ?? false,
|
|
274
|
+
oneShot: layer === "one-shot",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function resolveAgentModel(options: {
|
|
279
|
+
input: Omit<ResolveModelInput, "instance" | "projectRole" | "userRole">;
|
|
280
|
+
journal: CoordinatorJournal;
|
|
281
|
+
configs: LoadedModelConfigs;
|
|
282
|
+
catalog: ModelCatalog;
|
|
283
|
+
}): Promise<ResolvedModelChoice> {
|
|
284
|
+
const instance = options.journal.getState().models[options.input.agentId] as unknown as ModelOverride | undefined;
|
|
285
|
+
return await resolveModelChoice({
|
|
286
|
+
...options.input,
|
|
287
|
+
instance,
|
|
288
|
+
projectRole: options.configs.project?.roles[options.input.selector],
|
|
289
|
+
userRole: options.configs.global.roles[options.input.selector],
|
|
290
|
+
}, options.catalog);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface ModelChangeConfirmation {
|
|
294
|
+
hasUI: boolean;
|
|
295
|
+
confirm(packet: { scope: "instance" | "global" | "project"; target: string; oldValue?: ModelOverride; newValue?: ModelOverride }): Promise<"confirm" | "deny" | "dismiss">;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export class ModelConfigurationService {
|
|
299
|
+
constructor(
|
|
300
|
+
private readonly store: ModelConfigStore,
|
|
301
|
+
private readonly journal: CoordinatorJournal,
|
|
302
|
+
private readonly validateUsable?: (override: ModelOverride) => Promise<void>,
|
|
303
|
+
) {}
|
|
304
|
+
|
|
305
|
+
async userSetRole(scope: "global" | "project", selector: string, override: ModelOverride | undefined, projectTrusted: boolean): Promise<void> {
|
|
306
|
+
if (override) await this.validateUsable?.(override);
|
|
307
|
+
await this.store.setRole(scope, selector, override, projectTrusted);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async userSetInstance(agentId: string, override: ModelOverride | undefined): Promise<void> {
|
|
311
|
+
if (!this.journal.getState().agents[agentId]) throw new Error(`${agentId}: unknown active Agent`);
|
|
312
|
+
if (override) {
|
|
313
|
+
await assertNoCredentialMaterial(override, "instance model override");
|
|
314
|
+
validateOverride(override, "instance model override");
|
|
315
|
+
await this.validateUsable?.(override);
|
|
316
|
+
await this.journal.append({ kind: "model.put", agentId, payload: { ...override, source: "direct-user" } });
|
|
317
|
+
} else {
|
|
318
|
+
await this.journal.append({ kind: "model.clear", agentId, payload: { source: "direct-user" } });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async requestRoleChange(
|
|
323
|
+
scope: "global" | "project",
|
|
324
|
+
selector: string,
|
|
325
|
+
override: ModelOverride | undefined,
|
|
326
|
+
projectTrusted: boolean,
|
|
327
|
+
confirmation: ModelChangeConfirmation,
|
|
328
|
+
): Promise<{ status: "changed" | "denied" }> {
|
|
329
|
+
if (scope === "project" && !projectTrusted) throw new Error("project model config write requires active project trust");
|
|
330
|
+
const configs = await this.store.load(projectTrusted);
|
|
331
|
+
const oldValue = (scope === "global" ? configs.global : configs.project)?.roles[selector];
|
|
332
|
+
if (!confirmation.hasUI) return { status: "denied" };
|
|
333
|
+
if (override) {
|
|
334
|
+
await assertNoCredentialMaterial(override, "model-facing role override request");
|
|
335
|
+
validateOverride(override, "model-facing role override request");
|
|
336
|
+
await this.validateUsable?.(override);
|
|
337
|
+
}
|
|
338
|
+
const decision = await confirmation.confirm({ scope, target: selector, oldValue, newValue: override });
|
|
339
|
+
if (decision !== "confirm") return { status: "denied" };
|
|
340
|
+
await this.store.setRole(scope, selector, override, projectTrusted);
|
|
341
|
+
return { status: "changed" };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async requestInstanceChange(
|
|
345
|
+
agentId: string,
|
|
346
|
+
override: ModelOverride | undefined,
|
|
347
|
+
confirmation: ModelChangeConfirmation,
|
|
348
|
+
): Promise<{ status: "changed" | "denied" }> {
|
|
349
|
+
const agent = this.journal.getState().agents[agentId];
|
|
350
|
+
if (!agent) throw new Error(`${agentId}: unknown active Agent`);
|
|
351
|
+
const oldValue = this.journal.getState().models[agentId] as unknown as ModelOverride | undefined;
|
|
352
|
+
if (!confirmation.hasUI) return { status: "denied" };
|
|
353
|
+
if (override) {
|
|
354
|
+
await assertNoCredentialMaterial(override, "model-facing instance override request");
|
|
355
|
+
validateOverride(override, "model-facing instance override request");
|
|
356
|
+
await this.validateUsable?.(override);
|
|
357
|
+
}
|
|
358
|
+
const decision = await confirmation.confirm({ scope: "instance", target: agentId, oldValue, newValue: override });
|
|
359
|
+
if (decision !== "confirm") return { status: "denied" };
|
|
360
|
+
await this.userSetInstance(agentId, override);
|
|
361
|
+
return { status: "changed" };
|
|
362
|
+
}
|
|
363
|
+
}
|