@kal-elsam/kairo-runtime 0.1.5 → 0.2.1
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 +23 -0
- package/package.json +1 -1
- package/src/cli.js +54 -1
- package/src/global/action-planner.js +55 -3
- package/src/global/ink/orchestrator-app.js +36 -11
- package/src/global/ink/orchestrator-state.js +74 -3
- package/src/global/intelligence/backends/custom-http.js +175 -0
- package/src/global/intelligence/backends/ollama.js +164 -0
- package/src/global/intelligence/backends/openrouter.js +198 -0
- package/src/global/intelligence/context-compiler.js +338 -0
- package/src/global/intelligence/custom-url.js +82 -0
- package/src/global/intelligence/http.js +63 -0
- package/src/global/intelligence/index.js +38 -0
- package/src/global/intelligence/orchestrate.js +189 -0
- package/src/global/intelligence/registry.js +77 -0
- package/src/global/intelligence/router.js +191 -0
- package/src/global/intelligence/types.js +99 -0
- package/src/global/intelligence-cli.js +323 -0
- package/src/global/orchestrator.js +11 -0
- package/src/global/profile.js +153 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const BACKEND_IDS = {
|
|
2
|
+
OLLAMA: "ollama",
|
|
3
|
+
OPENROUTER: "openrouter",
|
|
4
|
+
CUSTOM: "custom"
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const COST_CLASSES = {
|
|
8
|
+
FREE: "free",
|
|
9
|
+
LOCAL: "local",
|
|
10
|
+
PAID: "paid",
|
|
11
|
+
UNKNOWN: "unknown"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const PRIVACY_CLASSES = {
|
|
15
|
+
LOCAL: "local",
|
|
16
|
+
CLOUD: "cloud",
|
|
17
|
+
UNKNOWN: "unknown"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const ROUTING_MODES = {
|
|
21
|
+
DIAGNOSTICS: "diagnostics",
|
|
22
|
+
LOCAL: "local",
|
|
23
|
+
CLOUD_CONSENT: "cloud_consent",
|
|
24
|
+
USER_OVERRIDE: "user_override"
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const OPENROUTER_FREE_MODEL = "openrouter/free";
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434";
|
|
30
|
+
|
|
31
|
+
export function createModelDescriptor({
|
|
32
|
+
provider,
|
|
33
|
+
modelId,
|
|
34
|
+
local = false,
|
|
35
|
+
costClass = COST_CLASSES.UNKNOWN,
|
|
36
|
+
privacyClass = PRIVACY_CLASSES.UNKNOWN,
|
|
37
|
+
contextLimit = null,
|
|
38
|
+
tools = false,
|
|
39
|
+
reasoning = false,
|
|
40
|
+
rateLimits = null,
|
|
41
|
+
opaque = false
|
|
42
|
+
}) {
|
|
43
|
+
return {
|
|
44
|
+
provider,
|
|
45
|
+
modelId,
|
|
46
|
+
local,
|
|
47
|
+
costClass,
|
|
48
|
+
privacyClass,
|
|
49
|
+
contextLimit,
|
|
50
|
+
tools,
|
|
51
|
+
reasoning,
|
|
52
|
+
rateLimits,
|
|
53
|
+
opaque
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createRoutingDecision({
|
|
58
|
+
backendId,
|
|
59
|
+
model,
|
|
60
|
+
reason,
|
|
61
|
+
estimatedTokens = null,
|
|
62
|
+
privacyImpact = PRIVACY_CLASSES.UNKNOWN,
|
|
63
|
+
fallback = null,
|
|
64
|
+
mode = ROUTING_MODES.DIAGNOSTICS,
|
|
65
|
+
requiresCloudConsent = false,
|
|
66
|
+
canInvoke = false
|
|
67
|
+
}) {
|
|
68
|
+
return {
|
|
69
|
+
backendId,
|
|
70
|
+
model,
|
|
71
|
+
reason,
|
|
72
|
+
estimatedTokens,
|
|
73
|
+
privacyImpact,
|
|
74
|
+
fallback,
|
|
75
|
+
mode,
|
|
76
|
+
requiresCloudConsent,
|
|
77
|
+
canInvoke
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createUsageTelemetry({
|
|
82
|
+
inputTokens = null,
|
|
83
|
+
outputTokens = null,
|
|
84
|
+
cachedTokens = null,
|
|
85
|
+
estimatedCost = null,
|
|
86
|
+
model = null,
|
|
87
|
+
backendId = null,
|
|
88
|
+
fallbackUsed = false
|
|
89
|
+
} = {}) {
|
|
90
|
+
return {
|
|
91
|
+
inputTokens,
|
|
92
|
+
outputTokens,
|
|
93
|
+
cachedTokens,
|
|
94
|
+
estimatedCost,
|
|
95
|
+
model,
|
|
96
|
+
backendId,
|
|
97
|
+
fallbackUsed
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { resolveHomeDir } from "./paths.js";
|
|
2
|
+
import { resolveProfile, buildProfileJson } from "./profile.js";
|
|
3
|
+
import {
|
|
4
|
+
compileContextPack,
|
|
5
|
+
inspectIntelligenceBackends,
|
|
6
|
+
resolveRoutingDecision,
|
|
7
|
+
runIntelligenceRequest,
|
|
8
|
+
summarizeIntelligenceBackends
|
|
9
|
+
} from "./intelligence/index.js";
|
|
10
|
+
import { BRAND } from "./brand/index.js";
|
|
11
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
12
|
+
|
|
13
|
+
export async function runIntelligenceCli(options, packageManifest) {
|
|
14
|
+
const action = options.intelligenceAction ?? "status";
|
|
15
|
+
const homeDir = resolveHomeDir();
|
|
16
|
+
const workspaceRoot = options.cwd;
|
|
17
|
+
const { profile, sources } = await resolveProfile({ homeDir, workspaceRoot });
|
|
18
|
+
|
|
19
|
+
switch (action) {
|
|
20
|
+
case "status":
|
|
21
|
+
return printIntelligenceStatus({
|
|
22
|
+
homeDir,
|
|
23
|
+
workspaceRoot,
|
|
24
|
+
profile,
|
|
25
|
+
sources,
|
|
26
|
+
json: options.json,
|
|
27
|
+
env: process.env
|
|
28
|
+
});
|
|
29
|
+
case "models":
|
|
30
|
+
return printIntelligenceModels({
|
|
31
|
+
profile,
|
|
32
|
+
json: options.json,
|
|
33
|
+
env: process.env
|
|
34
|
+
});
|
|
35
|
+
case "context":
|
|
36
|
+
return printIntelligenceContext({
|
|
37
|
+
workspaceRoot,
|
|
38
|
+
profile,
|
|
39
|
+
task: options.intelligenceTask,
|
|
40
|
+
relevantPaths: options.intelligencePaths ?? [],
|
|
41
|
+
includePrivate: options.includePrivate,
|
|
42
|
+
confirmed: options.yes || options.confirm,
|
|
43
|
+
json: options.json
|
|
44
|
+
});
|
|
45
|
+
case "route":
|
|
46
|
+
return printIntelligenceRoute({
|
|
47
|
+
workspaceRoot,
|
|
48
|
+
profile,
|
|
49
|
+
task: options.intelligenceTask,
|
|
50
|
+
cloudConsent: options.cloudConsent,
|
|
51
|
+
json: options.json,
|
|
52
|
+
env: process.env
|
|
53
|
+
});
|
|
54
|
+
case "ask":
|
|
55
|
+
return runIntelligenceAsk({
|
|
56
|
+
workspaceRoot,
|
|
57
|
+
profile,
|
|
58
|
+
task: options.intelligenceTask,
|
|
59
|
+
prompt: options.intelligencePrompt,
|
|
60
|
+
relevantPaths: options.intelligencePaths ?? [],
|
|
61
|
+
includePrivate: options.includePrivate,
|
|
62
|
+
cloudConsent: options.cloudConsent,
|
|
63
|
+
confirmed: options.yes || options.confirm,
|
|
64
|
+
json: options.json,
|
|
65
|
+
env: process.env,
|
|
66
|
+
packageManifest
|
|
67
|
+
});
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Unknown intelligence action "${action}". Use status, models, context, route, or ask.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function printIntelligenceStatus({ homeDir, workspaceRoot, profile, sources, json, env }) {
|
|
76
|
+
const backends = await inspectIntelligenceBackends({
|
|
77
|
+
env,
|
|
78
|
+
customProviders: profile.customProviders
|
|
79
|
+
});
|
|
80
|
+
const summary = summarizeIntelligenceBackends(backends);
|
|
81
|
+
const routing = resolveRoutingDecision({
|
|
82
|
+
backends,
|
|
83
|
+
profile,
|
|
84
|
+
cloudConsent: false
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const payload = {
|
|
88
|
+
readOnly: true,
|
|
89
|
+
homeDir,
|
|
90
|
+
workspaceRoot,
|
|
91
|
+
profile: buildProfileJson({ profile, sources }),
|
|
92
|
+
backends,
|
|
93
|
+
summary,
|
|
94
|
+
routing
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (json) {
|
|
98
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
99
|
+
return payload;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`${BRAND.displayName} intelligence — status`);
|
|
103
|
+
console.log(`Home: ${homeDir}`);
|
|
104
|
+
console.log(`Workspace: ${workspaceRoot}`);
|
|
105
|
+
console.log("");
|
|
106
|
+
for (const backend of backends) {
|
|
107
|
+
const models = backend.models?.length ?? 0;
|
|
108
|
+
console.log(
|
|
109
|
+
` ${backend.label.padEnd(14)} ${backend.state.padEnd(14)} models=${models}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
console.log("");
|
|
113
|
+
console.log(`Routing: ${routing.reason}`);
|
|
114
|
+
console.log(`Can invoke: ${routing.canInvoke ? "yes" : "no"}`);
|
|
115
|
+
if (!summary.localAvailable && !summary.cloudAuthenticated) {
|
|
116
|
+
console.log("");
|
|
117
|
+
console.log("Diagnostics mode: configure Ollama or OPENROUTER_API_KEY to enable inference.");
|
|
118
|
+
}
|
|
119
|
+
return payload;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function printIntelligenceModels({ profile, json, env }) {
|
|
123
|
+
const backends = await inspectIntelligenceBackends({
|
|
124
|
+
env,
|
|
125
|
+
customProviders: profile.customProviders
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const models = backends.flatMap((backend) =>
|
|
129
|
+
(backend.models ?? []).map((model) => ({
|
|
130
|
+
...model,
|
|
131
|
+
backendState: backend.state,
|
|
132
|
+
available: backend.available
|
|
133
|
+
}))
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const payload = { readOnly: true, models, backends: backends.map((entry) => ({
|
|
137
|
+
id: entry.id,
|
|
138
|
+
state: entry.state,
|
|
139
|
+
available: entry.available
|
|
140
|
+
})) };
|
|
141
|
+
|
|
142
|
+
if (json) {
|
|
143
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
144
|
+
return payload;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`${BRAND.displayName} intelligence — models`);
|
|
148
|
+
if (models.length === 0) {
|
|
149
|
+
console.log(" No models detected. Start Ollama or set OPENROUTER_API_KEY.");
|
|
150
|
+
} else {
|
|
151
|
+
for (const model of models) {
|
|
152
|
+
console.log(
|
|
153
|
+
` ${model.provider.padEnd(12)} ${model.modelId.padEnd(32)} ${model.privacyClass}/${model.costClass}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return payload;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function printIntelligenceContext({
|
|
161
|
+
workspaceRoot,
|
|
162
|
+
profile,
|
|
163
|
+
task,
|
|
164
|
+
relevantPaths,
|
|
165
|
+
includePrivate,
|
|
166
|
+
confirmed,
|
|
167
|
+
json
|
|
168
|
+
}) {
|
|
169
|
+
const privateConfirmationRequired = includePrivate && !confirmed;
|
|
170
|
+
const pack = await compileContextPack({
|
|
171
|
+
workspaceRoot,
|
|
172
|
+
task,
|
|
173
|
+
relevantPaths,
|
|
174
|
+
includePrivate: includePrivate && confirmed,
|
|
175
|
+
stableBudgetTokens: profile.stableContextBudget ?? undefined,
|
|
176
|
+
requestBudgetTokens: profile.requestContextBudget ?? undefined
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const payload = {
|
|
180
|
+
readOnly: true,
|
|
181
|
+
estimatedTokens: pack.estimatedTokens,
|
|
182
|
+
project: pack.stable.project,
|
|
183
|
+
evidence: pack.evidence,
|
|
184
|
+
privacy: pack.privacy,
|
|
185
|
+
skills: pack.stable.skills,
|
|
186
|
+
sdd: pack.stable.sdd,
|
|
187
|
+
tdd: pack.stable.tdd,
|
|
188
|
+
graphify: pack.stable.graphify,
|
|
189
|
+
hasAgentsMd: Boolean(pack.stable.agentsMd),
|
|
190
|
+
relevantFiles: pack.perRequest.files.map((file) => file.path)
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (privateConfirmationRequired) {
|
|
194
|
+
payload.privateConfirmationRequired = true;
|
|
195
|
+
payload.error = "Including private context requires explicit confirmation (--include-private --yes / --confirm).";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (json) {
|
|
199
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
200
|
+
return payload;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(`${BRAND.displayName} intelligence — context`);
|
|
204
|
+
console.log(`Estimated tokens: ${pack.estimatedTokens}`);
|
|
205
|
+
console.log(`Project: ${pack.stable.project.name} (${pack.stable.project.stack})`);
|
|
206
|
+
console.log(`Evidence files: ${pack.evidence.filter((entry) => entry.kind === "file").length}`);
|
|
207
|
+
if (privateConfirmationRequired) {
|
|
208
|
+
console.log(`Blocked: ${payload.error}`);
|
|
209
|
+
}
|
|
210
|
+
if (pack.privacy.excludedPrivate.length > 0) {
|
|
211
|
+
console.log(`Excluded private: ${pack.privacy.excludedPrivate.join(", ")}`);
|
|
212
|
+
}
|
|
213
|
+
return payload;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function printIntelligenceRoute({
|
|
217
|
+
workspaceRoot,
|
|
218
|
+
profile,
|
|
219
|
+
task,
|
|
220
|
+
cloudConsent,
|
|
221
|
+
json,
|
|
222
|
+
env
|
|
223
|
+
}) {
|
|
224
|
+
const backends = await inspectIntelligenceBackends({
|
|
225
|
+
env,
|
|
226
|
+
customProviders: profile.customProviders
|
|
227
|
+
});
|
|
228
|
+
const pack = await compileContextPack({ workspaceRoot, task });
|
|
229
|
+
const routing = resolveRoutingDecision({
|
|
230
|
+
backends,
|
|
231
|
+
profile,
|
|
232
|
+
contextPack: pack,
|
|
233
|
+
task,
|
|
234
|
+
cloudConsent: Boolean(cloudConsent)
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const payload = {
|
|
238
|
+
readOnly: true,
|
|
239
|
+
routing,
|
|
240
|
+
estimatedTokens: pack.estimatedTokens,
|
|
241
|
+
evidenceUsed: pack.evidence.filter((entry) => entry.kind === "file").map((entry) => entry.path)
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (json) {
|
|
245
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
246
|
+
return payload;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(`${BRAND.displayName} intelligence — routing`);
|
|
250
|
+
console.log(`Backend: ${routing.backendId ?? "none"}`);
|
|
251
|
+
console.log(`Model: ${routing.model?.modelId ?? "none"}`);
|
|
252
|
+
console.log(`Reason: ${routing.reason}`);
|
|
253
|
+
console.log(`Estimated tokens: ${routing.estimatedTokens}`);
|
|
254
|
+
console.log(`Privacy: ${routing.privacyImpact}`);
|
|
255
|
+
console.log(`Can invoke: ${routing.canInvoke ? "yes" : "no"}`);
|
|
256
|
+
return payload;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function runIntelligenceAsk({
|
|
260
|
+
workspaceRoot,
|
|
261
|
+
profile,
|
|
262
|
+
task,
|
|
263
|
+
prompt,
|
|
264
|
+
relevantPaths,
|
|
265
|
+
includePrivate,
|
|
266
|
+
cloudConsent,
|
|
267
|
+
confirmed,
|
|
268
|
+
json,
|
|
269
|
+
env
|
|
270
|
+
}) {
|
|
271
|
+
if (!prompt && !task) {
|
|
272
|
+
throw new Error(`Missing prompt. Use: ${formatCliCommand("intelligence ask --prompt \"...\"")}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const outcome = await runIntelligenceRequest({
|
|
276
|
+
workspaceRoot,
|
|
277
|
+
profile,
|
|
278
|
+
task,
|
|
279
|
+
prompt: prompt ?? task,
|
|
280
|
+
relevantPaths,
|
|
281
|
+
includePrivate,
|
|
282
|
+
cloudConsent,
|
|
283
|
+
confirmed,
|
|
284
|
+
env
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (json) {
|
|
288
|
+
console.log(JSON.stringify({
|
|
289
|
+
ok: outcome.ok,
|
|
290
|
+
mode: outcome.mode,
|
|
291
|
+
diagnosticsOnly: outcome.diagnosticsOnly,
|
|
292
|
+
routing: outcome.routing,
|
|
293
|
+
explanation: outcome.explanation,
|
|
294
|
+
telemetry: outcome.telemetry,
|
|
295
|
+
content: outcome.result?.content ?? null,
|
|
296
|
+
error: outcome.error
|
|
297
|
+
}, null, 2));
|
|
298
|
+
return outcome;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log(`${BRAND.displayName} intelligence — ask`);
|
|
302
|
+
console.log(`Routing: ${outcome.explanation.reason}`);
|
|
303
|
+
console.log(`Estimated tokens: ${outcome.explanation.estimatedTokens}`);
|
|
304
|
+
console.log(`Privacy: ${outcome.explanation.privacyImpact}`);
|
|
305
|
+
console.log("");
|
|
306
|
+
|
|
307
|
+
if (!outcome.ok) {
|
|
308
|
+
console.log(`Blocked: ${outcome.error}`);
|
|
309
|
+
if (outcome.routing?.requiresCloudConsent) {
|
|
310
|
+
console.log(`Retry with --cloud-consent --yes after reviewing context (${formatCliCommand("intelligence context")}).`);
|
|
311
|
+
}
|
|
312
|
+
return outcome;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log(outcome.result.content);
|
|
316
|
+
if (outcome.telemetry) {
|
|
317
|
+
console.log("");
|
|
318
|
+
console.log(
|
|
319
|
+
`Usage: in=${outcome.telemetry.inputTokens ?? "?"} out=${outcome.telemetry.outputTokens ?? "?"} model=${outcome.telemetry.model}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return outcome;
|
|
323
|
+
}
|
|
@@ -119,6 +119,17 @@ export async function runOrchestratorDiagnostics({
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
if (diagnostics.intelligence) {
|
|
123
|
+
console.log("");
|
|
124
|
+
console.log("Intelligence backends:");
|
|
125
|
+
for (const backend of diagnostics.intelligence.backends) {
|
|
126
|
+
console.log(
|
|
127
|
+
` ${backend.label.padEnd(14)} ${backend.state.padEnd(14)} models=${backend.models?.length ?? 0}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
console.log(` Routing: ${diagnostics.intelligence.routingPreview?.reason ?? "n/a"}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
122
133
|
console.log("");
|
|
123
134
|
console.log("Recommendations:");
|
|
124
135
|
for (const recommendation of diagnostics.recommendations) {
|
package/src/global/profile.js
CHANGED
|
@@ -3,19 +3,34 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { harnessHomePaths } from "./paths.js";
|
|
5
5
|
import { AGENT_CAPABILITY_IDS } from "./agent-capabilities/index.js";
|
|
6
|
+
import { classifyCustomBaseUrl, isValidEnvironmentName } from "./intelligence/custom-url.js";
|
|
6
7
|
|
|
7
8
|
export const PROFILE_KEYS = new Set([
|
|
8
9
|
"coordinator",
|
|
9
10
|
"defaultAgents",
|
|
10
11
|
"defaultComponents",
|
|
11
|
-
"applyMode"
|
|
12
|
+
"applyMode",
|
|
13
|
+
"preferredBackend",
|
|
14
|
+
"preferredModel",
|
|
15
|
+
"cloudConsent",
|
|
16
|
+
"tokenBudget",
|
|
17
|
+
"stableContextBudget",
|
|
18
|
+
"requestContextBudget",
|
|
19
|
+
"customProviders"
|
|
12
20
|
]);
|
|
13
21
|
|
|
14
22
|
export const DEFAULT_PROFILE = {
|
|
15
23
|
coordinator: null,
|
|
16
24
|
defaultAgents: "detected",
|
|
17
25
|
defaultComponents: null,
|
|
18
|
-
applyMode: "prompt"
|
|
26
|
+
applyMode: "prompt",
|
|
27
|
+
preferredBackend: null,
|
|
28
|
+
preferredModel: null,
|
|
29
|
+
cloudConsent: false,
|
|
30
|
+
tokenBudget: null,
|
|
31
|
+
stableContextBudget: null,
|
|
32
|
+
requestContextBudget: null,
|
|
33
|
+
customProviders: []
|
|
19
34
|
};
|
|
20
35
|
|
|
21
36
|
const APPLY_MODES = new Set(["prompt", "confirm"]);
|
|
@@ -64,6 +79,7 @@ export async function resolveProfile({ homeDir, workspaceRoot }) {
|
|
|
64
79
|
};
|
|
65
80
|
|
|
66
81
|
validateProfile(merged);
|
|
82
|
+
merged.customProviders = sanitizeCustomProviders(merged.customProviders);
|
|
67
83
|
|
|
68
84
|
return {
|
|
69
85
|
profile: merged,
|
|
@@ -82,6 +98,13 @@ export function buildProfileJson(resolved) {
|
|
|
82
98
|
defaultAgents: profile.defaultAgents,
|
|
83
99
|
defaultComponents: profile.defaultComponents,
|
|
84
100
|
applyMode: profile.applyMode,
|
|
101
|
+
preferredBackend: profile.preferredBackend,
|
|
102
|
+
preferredModel: profile.preferredModel,
|
|
103
|
+
cloudConsent: profile.cloudConsent,
|
|
104
|
+
tokenBudget: profile.tokenBudget,
|
|
105
|
+
stableContextBudget: profile.stableContextBudget,
|
|
106
|
+
requestContextBudget: profile.requestContextBudget,
|
|
107
|
+
customProviders: sanitizeCustomProviders(profile.customProviders),
|
|
85
108
|
sources: {
|
|
86
109
|
global: sources.global,
|
|
87
110
|
project: sources.project,
|
|
@@ -143,4 +166,132 @@ function validateProfile(profile) {
|
|
|
143
166
|
if (profile.defaultComponents != null && !Array.isArray(profile.defaultComponents)) {
|
|
144
167
|
throw new Error("Profile defaultComponents must be an array or null.");
|
|
145
168
|
}
|
|
169
|
+
|
|
170
|
+
if (profile.cloudConsent != null && typeof profile.cloudConsent !== "boolean") {
|
|
171
|
+
throw new Error("Profile cloudConsent must be a boolean.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const key of ["tokenBudget", "stableContextBudget", "requestContextBudget"]) {
|
|
175
|
+
if (profile[key] != null && (!Number.isFinite(profile[key]) || profile[key] < 1)) {
|
|
176
|
+
throw new Error(`Profile ${key} must be a positive number or null.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (profile.preferredBackend != null && typeof profile.preferredBackend !== "string") {
|
|
181
|
+
throw new Error("Profile preferredBackend must be a string or null.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (profile.preferredModel != null && typeof profile.preferredModel !== "string") {
|
|
185
|
+
throw new Error("Profile preferredModel must be a string or null.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
validateNoSecrets(profile);
|
|
189
|
+
validateCustomProviders(profile.customProviders);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Secret-looking key segments after camelCase → snake_case normalization.
|
|
194
|
+
* `api_key_env` is the only credential-related key allowed (env var name, not a secret).
|
|
195
|
+
*/
|
|
196
|
+
const SECRET_KEY_PATTERN = /(^|_)(api_?key|api_?token|access_?token|auth_?token|client_?secret|private_?key|authorization(_header)?|password|secrets?|credentials?|bearer|token)(_|$)/;
|
|
197
|
+
const SECRET_KEY_ALLOWLIST = new Set([
|
|
198
|
+
"api_key_env",
|
|
199
|
+
"token_budget",
|
|
200
|
+
"stable_context_budget",
|
|
201
|
+
"request_context_budget"
|
|
202
|
+
]);
|
|
203
|
+
const SECRET_VALUE_PATTERN = /^(sk-[A-Za-z0-9]|sk-or-|gh[pousr]_|xox[baprs]-|AKIA[0-9A-Z]{16}\b|Bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.)|-----BEGIN [A-Z ]*PRIVATE KEY-----/i;
|
|
204
|
+
|
|
205
|
+
export function normalizeProfileKey(key) {
|
|
206
|
+
return String(key)
|
|
207
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
208
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
209
|
+
.replace(/-/g, "_")
|
|
210
|
+
.toLowerCase();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function isForbiddenSecretKey(key) {
|
|
214
|
+
const normalized = normalizeProfileKey(key);
|
|
215
|
+
if (SECRET_KEY_ALLOWLIST.has(normalized)) return false;
|
|
216
|
+
return SECRET_KEY_PATTERN.test(normalized);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validateNoSecrets(profile) {
|
|
220
|
+
walkForSecrets(profile);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function walkForSecrets(value) {
|
|
224
|
+
if (typeof value === "string") {
|
|
225
|
+
if (SECRET_VALUE_PATTERN.test(value)) {
|
|
226
|
+
throw new Error("Profile must not store credential-like values. Use environment variables.");
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (value == null || typeof value !== "object") return;
|
|
231
|
+
|
|
232
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
233
|
+
const normalizedKey = normalizeProfileKey(key);
|
|
234
|
+
|
|
235
|
+
// apiKeyEnv is the only allowed credential-related key: it names an env var.
|
|
236
|
+
if (normalizedKey === "api_key_env") {
|
|
237
|
+
if (typeof nested === "string" && SECRET_VALUE_PATTERN.test(nested)) {
|
|
238
|
+
throw new Error("Profile must not store credential-like values. Use environment variables.");
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isForbiddenSecretKey(key)) {
|
|
244
|
+
throw new Error(`Profile must not store credentials (rejected key "${key}"). Use environment variables.`);
|
|
245
|
+
}
|
|
246
|
+
if (typeof nested === "string" && SECRET_VALUE_PATTERN.test(nested)) {
|
|
247
|
+
throw new Error("Profile must not store credential-like values. Use environment variables.");
|
|
248
|
+
}
|
|
249
|
+
walkForSecrets(nested);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validateCustomProviders(providers) {
|
|
254
|
+
if (providers == null) return;
|
|
255
|
+
if (!Array.isArray(providers)) {
|
|
256
|
+
throw new Error("Profile customProviders must be an array.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const provider of providers) {
|
|
260
|
+
if (provider == null || typeof provider !== "object" || Array.isArray(provider)) {
|
|
261
|
+
throw new Error("Each customProviders entry must be an object.");
|
|
262
|
+
}
|
|
263
|
+
if (!provider.baseUrl || typeof provider.baseUrl !== "string") {
|
|
264
|
+
throw new Error("customProviders entries require baseUrl.");
|
|
265
|
+
}
|
|
266
|
+
for (const key of Object.keys(provider)) {
|
|
267
|
+
if (normalizeProfileKey(key) === "api_key_env") continue;
|
|
268
|
+
if (isForbiddenSecretKey(key)) {
|
|
269
|
+
throw new Error(`customProviders must not include "${key}". Use apiKeyEnv to name an environment variable.`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (provider.apiKey != null || provider.token != null || provider.secret != null) {
|
|
273
|
+
throw new Error("customProviders must not embed secrets. Set apiKeyEnv to an environment variable name.");
|
|
274
|
+
}
|
|
275
|
+
if (provider.apiKeyEnv != null && !isValidEnvironmentName(provider.apiKeyEnv)) {
|
|
276
|
+
throw new Error("customProviders apiKeyEnv must be a valid uppercase environment variable name.");
|
|
277
|
+
}
|
|
278
|
+
const location = classifyCustomBaseUrl(provider.baseUrl);
|
|
279
|
+
if (provider.apiKeyEnv && !location.local) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
"Remote custom providers cannot use apiKeyEnv in 0.2.0; use a built-in provider or a local endpoint."
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function sanitizeCustomProviders(providers) {
|
|
288
|
+
if (!Array.isArray(providers)) return [];
|
|
289
|
+
return providers.map((provider) => ({
|
|
290
|
+
id: provider.id ?? "custom",
|
|
291
|
+
label: provider.label ?? "Custom provider",
|
|
292
|
+
baseUrl: provider.baseUrl,
|
|
293
|
+
modelId: provider.modelId ?? null,
|
|
294
|
+
apiKeyEnv: provider.apiKeyEnv ?? null,
|
|
295
|
+
local: classifyCustomBaseUrl(provider.baseUrl).local
|
|
296
|
+
}));
|
|
146
297
|
}
|