@pikku/inspector 0.12.94 → 0.12.96
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/CHANGELOG.md +29 -0
- package/dist/add/add-functions.js +19 -0
- package/dist/add/add-rpc-invocations.d.ts +17 -0
- package/dist/add/add-rpc-invocations.js +62 -5
- package/dist/add/collect-started-workflows.d.ts +26 -0
- package/dist/add/collect-started-workflows.js +72 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/filter-inspector-state.js +48 -0
- package/dist/utils/load-addon-functions-meta.d.ts +8 -0
- package/dist/utils/load-addon-functions-meta.js +47 -0
- package/dist/utils/post-process.d.ts +23 -23
- package/dist/utils/post-process.js +133 -155
- package/dist/utils/schema-generator.js +30 -3
- package/package.json +3 -3
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from
|
|
2
|
-
import { createRequire } from
|
|
3
|
-
import { join } from
|
|
4
|
-
import { extractTypeKeys } from
|
|
5
|
-
import { ErrorCode } from
|
|
6
|
-
import { findSecretAliasServices } from
|
|
7
|
-
import { deriveOAuth2AppSecrets } from
|
|
8
|
-
import { resolveCoreType } from
|
|
9
|
-
import { relative } from
|
|
10
|
-
import { AUTH_HANDLER_FUNC_ID } from
|
|
11
|
-
import { flattenScopeDefinitions } from
|
|
12
|
-
import { DYNAMIC_SCENARIO_STEP_TARGET } from
|
|
13
|
-
import { addonResolutionDirs } from
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { extractTypeKeys } from "./type-utils.js";
|
|
5
|
+
import { ErrorCode } from "../error-codes.js";
|
|
6
|
+
import { findSecretAliasServices } from "./secret-alias-services.js";
|
|
7
|
+
import { deriveOAuth2AppSecrets } from "@pikku/core/secret";
|
|
8
|
+
import { resolveCoreType } from "./resolve-core-type.js";
|
|
9
|
+
import { relative } from "node:path";
|
|
10
|
+
import { AUTH_HANDLER_FUNC_ID } from "../add/add-auth.js";
|
|
11
|
+
import { flattenScopeDefinitions } from "@pikku/core/scope";
|
|
12
|
+
import { DYNAMIC_SCENARIO_STEP_TARGET } from "./workflow/dsl/patterns.js";
|
|
13
|
+
import { addonResolutionDirs } from "./addon-resolution.js";
|
|
14
14
|
/**
|
|
15
15
|
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
16
16
|
* handler's function meta.
|
|
@@ -45,7 +45,7 @@ export function extractWireNames(list) {
|
|
|
45
45
|
if (!list)
|
|
46
46
|
return [];
|
|
47
47
|
return list
|
|
48
|
-
.filter((item) => item.type ===
|
|
48
|
+
.filter((item) => item.type === "wire")
|
|
49
49
|
.map((item) => item.name);
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
@@ -57,13 +57,13 @@ function expandAndAddGroupServices(list, state, addServices) {
|
|
|
57
57
|
if (!list)
|
|
58
58
|
return;
|
|
59
59
|
for (const item of list) {
|
|
60
|
-
if (item.type ===
|
|
60
|
+
if (item.type === "tag") {
|
|
61
61
|
const groupMeta = state.middleware.tagMiddleware.get(item.tag);
|
|
62
62
|
if (groupMeta?.services) {
|
|
63
63
|
addServices(groupMeta.services);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
else if (item.type ===
|
|
66
|
+
else if (item.type === "http" && "route" in item) {
|
|
67
67
|
const groupMeta = state.http.routeMiddleware.get(item.route);
|
|
68
68
|
if (groupMeta?.services) {
|
|
69
69
|
addServices(groupMeta.services);
|
|
@@ -78,7 +78,7 @@ function expandAndAddGroupServices(list, state, addServices) {
|
|
|
78
78
|
*/
|
|
79
79
|
function extractAllServices(state) {
|
|
80
80
|
// Skip if typesLookup is not available (e.g., deserialized state)
|
|
81
|
-
if (!(
|
|
81
|
+
if (!("typesLookup" in state)) {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
// Extract all singleton services from the SingletonServices type
|
|
@@ -126,7 +126,7 @@ export function aggregateRequiredServices(state) {
|
|
|
126
126
|
extractAllServices(state);
|
|
127
127
|
const { requiredServices, usedFunctions, usedMiddleware, usedPermissions } = state.serviceAggregation;
|
|
128
128
|
// Internal services (always excluded from tree-shaking)
|
|
129
|
-
const internalServices = new Set([
|
|
129
|
+
const internalServices = new Set(["rpc", "mcp", "channel", "userSession"]);
|
|
130
130
|
const addServices = (services) => {
|
|
131
131
|
if (!services || !services.services)
|
|
132
132
|
return;
|
|
@@ -172,15 +172,7 @@ export function aggregateRequiredServices(state) {
|
|
|
172
172
|
});
|
|
173
173
|
// 4. Services from middleware/permission groups used in wirings
|
|
174
174
|
// We need to check all wirings and expand any tag/HTTP-pattern groups they use
|
|
175
|
-
for (const method of [
|
|
176
|
-
'get',
|
|
177
|
-
'post',
|
|
178
|
-
'put',
|
|
179
|
-
'patch',
|
|
180
|
-
'delete',
|
|
181
|
-
'head',
|
|
182
|
-
'options',
|
|
183
|
-
]) {
|
|
175
|
+
for (const method of ["get", "post", "put", "patch", "delete", "head", "options"]) {
|
|
184
176
|
for (const routeMeta of Object.values(state.http.meta[method])) {
|
|
185
177
|
expandAndAddGroupServices(routeMeta.middleware, state, addServices);
|
|
186
178
|
}
|
|
@@ -218,23 +210,22 @@ export function aggregateRequiredServices(state) {
|
|
|
218
210
|
// that wrap workflow operations but don't destructure the services.
|
|
219
211
|
const hasWorkflows = Object.keys(state.workflows.graphMeta).length > 0 ||
|
|
220
212
|
Object.keys(state.workflows.meta).length > 0 ||
|
|
221
|
-
Object.keys(state.functions.meta).some((id) => id.startsWith(
|
|
222
|
-
id.startsWith(
|
|
223
|
-
id.startsWith(
|
|
213
|
+
Object.keys(state.functions.meta).some((id) => id.startsWith("workflowStart:") ||
|
|
214
|
+
id.startsWith("workflowStatus:") ||
|
|
215
|
+
id.startsWith("workflow:"));
|
|
224
216
|
if (hasWorkflows) {
|
|
225
|
-
requiredServices.add(
|
|
226
|
-
requiredServices.add(
|
|
227
|
-
requiredServices.add(
|
|
228
|
-
requiredServices.add(
|
|
217
|
+
requiredServices.add("workflowService");
|
|
218
|
+
requiredServices.add("workflowRunService");
|
|
219
|
+
requiredServices.add("schedulerService");
|
|
220
|
+
requiredServices.add("queueService");
|
|
229
221
|
}
|
|
230
222
|
// Webhook source routes and their worker are generated and destructure
|
|
231
223
|
// nothing, so they are found by id: the route accepts into the queue through
|
|
232
224
|
// incomingWebhookService and the worker records attempts through it.
|
|
233
225
|
const hasWebhookSources = Object.values(state.triggers?.webhookSourceMeta ?? {}).some(({ method, route }) => state.functions.meta[`http:${method}:${route}`] !== undefined);
|
|
234
|
-
if (hasWebhookSources ||
|
|
235
|
-
|
|
236
|
-
requiredServices.add(
|
|
237
|
-
requiredServices.add('queueService');
|
|
226
|
+
if (hasWebhookSources || state.functions.meta["queue:pikku-incoming-webhooks"] !== undefined) {
|
|
227
|
+
requiredServices.add("incomingWebhookService");
|
|
228
|
+
requiredServices.add("queueService");
|
|
238
229
|
}
|
|
239
230
|
// 6b. Inject synthetic queue workers for workflow graph steps.
|
|
240
231
|
// Each workflow gets an orchestrator queue and per-step queues.
|
|
@@ -248,11 +239,11 @@ export function aggregateRequiredServices(state) {
|
|
|
248
239
|
// synthetic entries were pure leakage: they put a
|
|
249
240
|
// `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
|
|
250
241
|
// bundle imports and a provider then creates as a real production queue.
|
|
251
|
-
if (graph.source ===
|
|
242
|
+
if (graph.source === "scenario")
|
|
252
243
|
continue;
|
|
253
244
|
const toKebab = (s) => s
|
|
254
|
-
.replace(/([a-z0-9])([A-Z])/g,
|
|
255
|
-
.replace(/([A-Z])([A-Z][a-z])/g,
|
|
245
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
246
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
|
|
256
247
|
.toLowerCase();
|
|
257
248
|
// Orchestrator queue
|
|
258
249
|
const orchQueueName = `wf-orchestrator-${toKebab(graph.name)}`;
|
|
@@ -264,14 +255,11 @@ export function aggregateRequiredServices(state) {
|
|
|
264
255
|
}
|
|
265
256
|
// Per-step queues — only for steps explicitly marked workflowQueued: true
|
|
266
257
|
for (const node of Object.values(graph.nodes)) {
|
|
267
|
-
if (!(
|
|
258
|
+
if (!("rpcName" in node) || !node.rpcName)
|
|
268
259
|
continue;
|
|
269
260
|
const rpcName = node.rpcName;
|
|
270
|
-
const funcId = state.rpc?.internalMeta?.[rpcName] ??
|
|
271
|
-
|
|
272
|
-
rpcName;
|
|
273
|
-
const funcMeta = (state.functions.meta[funcId] ??
|
|
274
|
-
state.functions.meta[rpcName]);
|
|
261
|
+
const funcId = state.rpc?.internalMeta?.[rpcName] ?? state.rpc?.exposedMeta?.[rpcName] ?? rpcName;
|
|
262
|
+
const funcMeta = (state.functions.meta[funcId] ?? state.functions.meta[rpcName]);
|
|
275
263
|
if (funcMeta?.workflowQueued !== true)
|
|
276
264
|
continue;
|
|
277
265
|
const stepQueueName = `wf-step-${toKebab(rpcName)}`;
|
|
@@ -285,20 +273,20 @@ export function aggregateRequiredServices(state) {
|
|
|
285
273
|
}
|
|
286
274
|
// AI agents need agentStorage + agentRunState + agentRunService + agentRunner
|
|
287
275
|
if (Object.keys(state.agents.agentsMeta).length > 0) {
|
|
288
|
-
requiredServices.add(
|
|
289
|
-
requiredServices.add(
|
|
290
|
-
requiredServices.add(
|
|
291
|
-
requiredServices.add(
|
|
276
|
+
requiredServices.add("agentStorage");
|
|
277
|
+
requiredServices.add("agentRunState");
|
|
278
|
+
requiredServices.add("agentRunService");
|
|
279
|
+
requiredServices.add("agentRunner");
|
|
292
280
|
}
|
|
293
281
|
// Channels need eventHub for pub/sub
|
|
294
282
|
if (Object.keys(state.channels.meta).length > 0) {
|
|
295
|
-
requiredServices.add(
|
|
283
|
+
requiredServices.add("eventHub");
|
|
296
284
|
}
|
|
297
285
|
// Declared scopes need scopeService to resolve a grant. Nothing in a project
|
|
298
286
|
// destructures it — the generated auth layer reaches it — so the declaration
|
|
299
287
|
// is the only signal there is.
|
|
300
288
|
if ((state.scopes?.definitions?.length ?? 0) > 0) {
|
|
301
|
-
requiredServices.add(
|
|
289
|
+
requiredServices.add("scopeService");
|
|
302
290
|
}
|
|
303
291
|
// Declared feature flags need featureFlags for the same reason, from the
|
|
304
292
|
// other side: `assertFeatureAvailable` reads the source out of the singleton
|
|
@@ -306,17 +294,17 @@ export function aggregateRequiredServices(state) {
|
|
|
306
294
|
// ever says the project has flags. Without this the flag tables are left out
|
|
307
295
|
// of the generated migration and every gate silently resolves open.
|
|
308
296
|
if ((state.featureFlags?.definitions?.length ?? 0) > 0) {
|
|
309
|
-
requiredServices.add(
|
|
297
|
+
requiredServices.add("featureFlags");
|
|
310
298
|
}
|
|
311
299
|
// A `defineAnalyticsEvents` declaration means events are emitted, and the
|
|
312
300
|
// invocation buffer flushes into `analyticsService`. Nothing destructures
|
|
313
301
|
// that either — functions are handed the request-scoped `analytics` — so the
|
|
314
302
|
// declaration is again the only signal.
|
|
315
303
|
if ((state.analytics?.length ?? 0) > 0) {
|
|
316
|
-
requiredServices.add(
|
|
304
|
+
requiredServices.add("analyticsService");
|
|
317
305
|
}
|
|
318
306
|
if ((state.credentials?.definitions?.length ?? 0) > 0) {
|
|
319
|
-
requiredServices.add(
|
|
307
|
+
requiredServices.add("credentialService");
|
|
320
308
|
}
|
|
321
309
|
// 7. Services that consumed addons need from the parent project.
|
|
322
310
|
const addonFnServices = new Map();
|
|
@@ -327,13 +315,7 @@ export function aggregateRequiredServices(state) {
|
|
|
327
315
|
}
|
|
328
316
|
const parentDeclared = state.addonRequiredParentServices ?? [];
|
|
329
317
|
const parentDeclaredSet = new Set(parentDeclared);
|
|
330
|
-
const defaultServices = new Set([
|
|
331
|
-
'config',
|
|
332
|
-
'logger',
|
|
333
|
-
'variables',
|
|
334
|
-
'schema',
|
|
335
|
-
'secrets',
|
|
336
|
-
]);
|
|
318
|
+
const defaultServices = new Set(["config", "logger", "variables", "schema", "secrets"]);
|
|
337
319
|
let usesAddonFn = false;
|
|
338
320
|
let addonFactoryNeeded = false;
|
|
339
321
|
for (const funcId of usedFunctions) {
|
|
@@ -349,8 +331,7 @@ export function aggregateRequiredServices(state) {
|
|
|
349
331
|
if (parentDeclaredSet.has(service)) {
|
|
350
332
|
requiredServices.add(service);
|
|
351
333
|
}
|
|
352
|
-
else if (!internalServices.has(service) &&
|
|
353
|
-
!defaultServices.has(service)) {
|
|
334
|
+
else if (!internalServices.has(service) && !defaultServices.has(service)) {
|
|
354
335
|
addonFactoryNeeded = true;
|
|
355
336
|
}
|
|
356
337
|
}
|
|
@@ -398,7 +379,7 @@ export function validateSecretOverrides(logger, state) {
|
|
|
398
379
|
for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides ?? {})) {
|
|
399
380
|
if (!secretIds.has(resolvedName)) {
|
|
400
381
|
const availableSecrets = Array.from(secretIds);
|
|
401
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(
|
|
382
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(", ") || "none"}`);
|
|
402
383
|
}
|
|
403
384
|
}
|
|
404
385
|
// A grant names the secret as the addon reads it, so it resolves through
|
|
@@ -407,7 +388,7 @@ export function validateSecretOverrides(logger, state) {
|
|
|
407
388
|
const resolvedName = addonDecl.secretOverrides?.[logicalName] ?? logicalName;
|
|
408
389
|
if (!secretIds.has(resolvedName)) {
|
|
409
390
|
const availableSecrets = Array.from(secretIds);
|
|
410
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Secret grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(
|
|
391
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Secret grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(", ") || "none"}`);
|
|
411
392
|
}
|
|
412
393
|
}
|
|
413
394
|
}
|
|
@@ -418,8 +399,8 @@ export function validateCredentialOverrides(logger, state) {
|
|
|
418
399
|
return;
|
|
419
400
|
const credentialNames = new Set(state.credentials?.definitions.map((d) => d.name) ?? []);
|
|
420
401
|
/** Only a rename has a target to check; a mode-only override renames nothing. */
|
|
421
|
-
const renameTarget = (override) => typeof override ===
|
|
422
|
-
const overrideMode = (override) => typeof override ===
|
|
402
|
+
const renameTarget = (override) => typeof override === "string" ? override : override.name;
|
|
403
|
+
const overrideMode = (override) => typeof override === "string" ? undefined : override.mode;
|
|
423
404
|
/**
|
|
424
405
|
* Which declaration set each resolved name's mode. Credential generation
|
|
425
406
|
* writes one shared metadata entry per name, so two declarations that map
|
|
@@ -436,10 +417,8 @@ export function validateCredentialOverrides(logger, state) {
|
|
|
436
417
|
const resolvedName = renamed ?? logicalName;
|
|
437
418
|
if (!credentialNames.has(resolvedName)) {
|
|
438
419
|
const availableCredentials = Array.from(credentialNames);
|
|
439
|
-
const target = renamed
|
|
440
|
-
|
|
441
|
-
: `'${logicalName}'`;
|
|
442
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Credential override ${target} in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
|
|
420
|
+
const target = renamed ? `'${logicalName}' -> '${resolvedName}'` : `'${logicalName}'`;
|
|
421
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Credential override ${target} in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(", ") || "none"}`);
|
|
443
422
|
}
|
|
444
423
|
const mode = overrideMode(override);
|
|
445
424
|
if (!mode)
|
|
@@ -455,7 +434,7 @@ export function validateCredentialOverrides(logger, state) {
|
|
|
455
434
|
const resolvedName = (override ? renameTarget(override) : undefined) ?? logicalName;
|
|
456
435
|
if (!credentialNames.has(resolvedName)) {
|
|
457
436
|
const availableCredentials = Array.from(credentialNames);
|
|
458
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Credential grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(
|
|
437
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Credential grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(", ") || "none"}`);
|
|
459
438
|
}
|
|
460
439
|
}
|
|
461
440
|
}
|
|
@@ -473,7 +452,7 @@ export function validateVariableOverrides(logger, state) {
|
|
|
473
452
|
for (const [logicalName, resolvedName] of Object.entries(addonDecl.variableOverrides)) {
|
|
474
453
|
if (!variableIds.has(resolvedName)) {
|
|
475
454
|
const availableVariables = Array.from(variableIds);
|
|
476
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Variable override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a variable that does not exist. Available variables: ${availableVariables.join(
|
|
455
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Variable override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a variable that does not exist. Available variables: ${availableVariables.join(", ") || "none"}`);
|
|
477
456
|
}
|
|
478
457
|
}
|
|
479
458
|
}
|
|
@@ -489,11 +468,11 @@ export function validateVariableOverrides(logger, state) {
|
|
|
489
468
|
* attempt means it is genuinely absent.
|
|
490
469
|
*/
|
|
491
470
|
const isPackageResolvable = (pkg, dirs) => {
|
|
492
|
-
const NOT_FOUND = new Set([
|
|
471
|
+
const NOT_FOUND = new Set(["MODULE_NOT_FOUND", "ERR_MODULE_NOT_FOUND"]);
|
|
493
472
|
for (const dir of dirs) {
|
|
494
473
|
let req;
|
|
495
474
|
try {
|
|
496
|
-
req = createRequire(join(dir,
|
|
475
|
+
req = createRequire(join(dir, "package.json"));
|
|
497
476
|
}
|
|
498
477
|
catch {
|
|
499
478
|
return true; // cannot ask; do not accuse
|
|
@@ -547,7 +526,7 @@ export function validateRemoteAddonDependencies(logger, state) {
|
|
|
547
526
|
const dirs = addonResolutionDirs(state.rootDir, decl.file);
|
|
548
527
|
if (isPackageResolvable(decl.package, dirs))
|
|
549
528
|
continue;
|
|
550
|
-
logger.critical(ErrorCode.ADDON_NOT_INSTALLED, `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved from ${dirs.join(
|
|
529
|
+
logger.critical(ErrorCode.ADDON_NOT_INSTALLED, `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved from ${dirs.join(" or ")} — every ref('${namespace}:…') will resolve to nothing and the surface will be dead at runtime. Add it to the dependencies of ${join(dirs[0], "package.json")} and install, or remove the wireAddon call.`);
|
|
551
530
|
}
|
|
552
531
|
// A remote addon belongs in the devDependencies of the package that wires
|
|
553
532
|
// it, which in a workspace is not the root.
|
|
@@ -558,7 +537,7 @@ export function validateRemoteAddonDependencies(logger, state) {
|
|
|
558
537
|
let manifest = null;
|
|
559
538
|
if (existsSync(path)) {
|
|
560
539
|
try {
|
|
561
|
-
manifest = JSON.parse(readFileSync(path,
|
|
540
|
+
manifest = JSON.parse(readFileSync(path, "utf-8"));
|
|
562
541
|
}
|
|
563
542
|
catch (e) {
|
|
564
543
|
logger.warn(`Could not read ${path} to verify remote addon dependencies: ${e?.message ?? e}`);
|
|
@@ -570,7 +549,7 @@ export function validateRemoteAddonDependencies(logger, state) {
|
|
|
570
549
|
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
571
550
|
if (!decl.remote)
|
|
572
551
|
continue;
|
|
573
|
-
const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0],
|
|
552
|
+
const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0], "package.json");
|
|
574
553
|
const pkgJson = readManifest(pkgJsonPath);
|
|
575
554
|
if (!pkgJson)
|
|
576
555
|
continue; // no manifest to check (e.g. some tests)
|
|
@@ -602,10 +581,10 @@ export function validateRemoteAddonAuth(logger, state) {
|
|
|
602
581
|
if (!decl.remote)
|
|
603
582
|
continue;
|
|
604
583
|
if (decl.authCredentialId && !credentialNames.has(decl.authCredentialId)) {
|
|
605
|
-
logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.credentialId '${decl.authCredentialId}', but no such credential is wired. Available credentials: ${Array.from(credentialNames).join(
|
|
584
|
+
logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.credentialId '${decl.authCredentialId}', but no such credential is wired. Available credentials: ${Array.from(credentialNames).join(", ") || "none"}`);
|
|
606
585
|
}
|
|
607
586
|
if (decl.authSecretId && !secretNames.has(decl.authSecretId)) {
|
|
608
|
-
logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.secretId '${decl.authSecretId}', but no such secret is wired. Available secrets: ${Array.from(secretNames).join(
|
|
587
|
+
logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.secretId '${decl.authSecretId}', but no such secret is wired. Available secrets: ${Array.from(secretNames).join(", ") || "none"}`);
|
|
609
588
|
}
|
|
610
589
|
}
|
|
611
590
|
}
|
|
@@ -614,7 +593,7 @@ export function computeResolvedIOTypes(state) {
|
|
|
614
593
|
for (const [pikkuFuncId, meta] of Object.entries(functions.meta)) {
|
|
615
594
|
const input = meta.inputs?.[0];
|
|
616
595
|
const output = meta.outputs?.[0];
|
|
617
|
-
let inputType =
|
|
596
|
+
let inputType = "null";
|
|
618
597
|
if (input) {
|
|
619
598
|
try {
|
|
620
599
|
inputType = functions.typesMap.getTypeMeta(input).uniqueName;
|
|
@@ -623,7 +602,7 @@ export function computeResolvedIOTypes(state) {
|
|
|
623
602
|
inputType = input;
|
|
624
603
|
}
|
|
625
604
|
}
|
|
626
|
-
let outputType =
|
|
605
|
+
let outputType = "null";
|
|
627
606
|
if (output) {
|
|
628
607
|
try {
|
|
629
608
|
outputType = functions.typesMap.getTypeMeta(output).uniqueName;
|
|
@@ -633,10 +612,10 @@ export function computeResolvedIOTypes(state) {
|
|
|
633
612
|
}
|
|
634
613
|
}
|
|
635
614
|
state.resolvedIOTypes[pikkuFuncId] = { inputType, outputType };
|
|
636
|
-
if (meta.inputSchemaName && inputType !==
|
|
615
|
+
if (meta.inputSchemaName && inputType !== "null") {
|
|
637
616
|
meta.inputSchemaName = inputType;
|
|
638
617
|
}
|
|
639
|
-
if (meta.outputSchemaName && outputType !==
|
|
618
|
+
if (meta.outputSchemaName && outputType !== "null") {
|
|
640
619
|
meta.outputSchemaName = outputType;
|
|
641
620
|
}
|
|
642
621
|
if (meta.inputs) {
|
|
@@ -698,15 +677,15 @@ export function computePermissionsGroupsMeta(state) {
|
|
|
698
677
|
};
|
|
699
678
|
}
|
|
700
679
|
const PRIMITIVE_TYPES = new Set([
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
680
|
+
"boolean",
|
|
681
|
+
"string",
|
|
682
|
+
"number",
|
|
683
|
+
"null",
|
|
684
|
+
"undefined",
|
|
685
|
+
"void",
|
|
686
|
+
"any",
|
|
687
|
+
"unknown",
|
|
688
|
+
"never",
|
|
710
689
|
]);
|
|
711
690
|
export function computeRequiredSchemas(state, options) {
|
|
712
691
|
const { functions, schemaLookup } = state;
|
|
@@ -767,8 +746,8 @@ export function unresolvedSchemaReferences(state) {
|
|
|
767
746
|
};
|
|
768
747
|
for (const [funcName, meta] of Object.entries(functions.meta)) {
|
|
769
748
|
for (const [role, declared] of [
|
|
770
|
-
[
|
|
771
|
-
[
|
|
749
|
+
["input", meta.inputs?.[0]],
|
|
750
|
+
["output", meta.outputs?.[0]],
|
|
772
751
|
]) {
|
|
773
752
|
if (!declared || PRIMITIVE_TYPES.has(declared))
|
|
774
753
|
continue;
|
|
@@ -785,12 +764,12 @@ export function validateSchemaReferences(logger, state) {
|
|
|
785
764
|
if (unresolved.length === 0)
|
|
786
765
|
return;
|
|
787
766
|
logger.diagnostic({
|
|
788
|
-
severity:
|
|
767
|
+
severity: "error",
|
|
789
768
|
code: ErrorCode.SCHEMA_REFERENCE_UNRESOLVED,
|
|
790
|
-
message: `${unresolved.length} function contract${unresolved.length === 1 ?
|
|
791
|
-
`name${unresolved.length === 1 ?
|
|
792
|
-
`calling ${unresolved.length === 1 ?
|
|
793
|
-
`MissingSchemaError: ${unresolved.join(
|
|
769
|
+
message: `${unresolved.length} function contract${unresolved.length === 1 ? "" : "s"} ` +
|
|
770
|
+
`name${unresolved.length === 1 ? "s" : ""} a schema that was never generated, so ` +
|
|
771
|
+
`calling ${unresolved.length === 1 ? "it" : "them"} fails at runtime with ` +
|
|
772
|
+
`MissingSchemaError: ${unresolved.join(", ")}. ` +
|
|
794
773
|
`The usual cause is a named type used as the function's input/output that ` +
|
|
795
774
|
`is declared but not exported — export it, or inline the type at the call site.`,
|
|
796
775
|
});
|
|
@@ -809,11 +788,11 @@ export function validateAgentModels(logger, state, modelAliases = {}) {
|
|
|
809
788
|
// `hasOwn`, not a truthy lookup: every object literal inherits `toString`,
|
|
810
789
|
// `constructor` and `__proto__`, so a bare model named after one of them
|
|
811
790
|
// would read as a configured alias and pass a check nothing else repeats.
|
|
812
|
-
if (!model.includes(
|
|
791
|
+
if (!model.includes("/") && !Object.hasOwn(modelAliases, model)) {
|
|
813
792
|
const known = Object.keys(modelAliases).sort();
|
|
814
793
|
logger.critical(ErrorCode.INVALID_MODEL, `AI agent '${meta.name}' uses model '${model}', which is neither provider-qualified as '<provider>/<model>' (e.g. 'openai/gpt-4') nor an alias in the "models" table of pikku.config.json. ` +
|
|
815
794
|
(known.length
|
|
816
|
-
? `Known aliases: ${known.join(
|
|
795
|
+
? `Known aliases: ${known.join(", ")}.`
|
|
817
796
|
: `No "models" aliases are configured.`));
|
|
818
797
|
}
|
|
819
798
|
}
|
|
@@ -836,9 +815,9 @@ export function validateScenarioServices(logger, state) {
|
|
|
836
815
|
const funcMeta = state.functions.meta[meta.pikkuFuncId];
|
|
837
816
|
if (!funcMeta?.services)
|
|
838
817
|
continue;
|
|
839
|
-
const disallowed = funcMeta.services.services.filter((svc) => svc !==
|
|
818
|
+
const disallowed = funcMeta.services.services.filter((svc) => svc !== "logger" && svc !== "config");
|
|
840
819
|
if (disallowed.length > 0) {
|
|
841
|
-
logger.critical(ErrorCode.SCENARIO_HAS_SERVICES, `Scenario '${workflowName}' destructures services: ${disallowed.join(
|
|
820
|
+
logger.critical(ErrorCode.SCENARIO_HAS_SERVICES, `Scenario '${workflowName}' destructures services: ${disallowed.join(", ")}. ` +
|
|
842
821
|
`Scenarios may only use 'logger'/'config' — drive everything else through ` +
|
|
843
822
|
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
844
823
|
`runs against the target environment.`);
|
|
@@ -858,8 +837,8 @@ export const proseOpensWithActor = (prose, actor) => {
|
|
|
858
837
|
if (!actor) {
|
|
859
838
|
return false;
|
|
860
839
|
}
|
|
861
|
-
const escaped = actor.replace(/[.*+?^${}()|[\]\\]/g,
|
|
862
|
-
return new RegExp(`^["'“‘\\s]*${escaped}(?:['’]s)?\\b`,
|
|
840
|
+
const escaped = actor.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
841
|
+
return new RegExp(`^["'“‘\\s]*${escaped}(?:['’]s)?\\b`, "i").test(prose);
|
|
863
842
|
};
|
|
864
843
|
/**
|
|
865
844
|
* The prose that will actually render for one step, in the runtime's own
|
|
@@ -875,11 +854,7 @@ const renderableProse = (step, stepMeta) => {
|
|
|
875
854
|
if (description) {
|
|
876
855
|
return [step.stepName, description];
|
|
877
856
|
}
|
|
878
|
-
return [
|
|
879
|
-
step.stepName,
|
|
880
|
-
stepMeta?.scenarioStepTemplate,
|
|
881
|
-
stepMeta?.description,
|
|
882
|
-
].filter((prose) => typeof prose === 'string');
|
|
857
|
+
return [step.stepName, stepMeta?.scenarioStepTemplate, stepMeta?.description].filter((prose) => typeof prose === "string");
|
|
883
858
|
};
|
|
884
859
|
/**
|
|
885
860
|
* A step's prose must not open by naming the actor it already runs as (PKU681).
|
|
@@ -927,7 +902,7 @@ export function validateScenarioSteps(logger, state) {
|
|
|
927
902
|
const phasesSeen = new Set();
|
|
928
903
|
const visit = (workflowName, steps) => {
|
|
929
904
|
for (const step of steps) {
|
|
930
|
-
if (step.type ===
|
|
905
|
+
if (step.type === "scenarioStep") {
|
|
931
906
|
phasesSeen.add(step.phase);
|
|
932
907
|
if (step.stepFunc === DYNAMIC_SCENARIO_STEP_TARGET) {
|
|
933
908
|
logger.critical(ErrorCode.SCENARIO_STEP_TARGET_NOT_STATIC, `Scenario '${workflowName}' calls step '${step.stepName}' with a target that isn't a string literal. ` +
|
|
@@ -939,21 +914,21 @@ export function validateScenarioSteps(logger, state) {
|
|
|
939
914
|
const stepMeta = state.functions.meta[step.stepFunc];
|
|
940
915
|
validateStepProse(logger, workflowName, step, stepMeta);
|
|
941
916
|
if (stepMeta?.scenarioStepRequiresActor && !step.actor) {
|
|
942
|
-
const why = stepMeta.scenarioStepSurfaces?.includes(
|
|
943
|
-
?
|
|
944
|
-
:
|
|
917
|
+
const why = stepMeta.scenarioStepSurfaces?.includes("browser")
|
|
918
|
+
? "declares a browser binding"
|
|
919
|
+
: "is driven by a persona";
|
|
945
920
|
logger.critical(ErrorCode.SCENARIO_STEP_NEEDS_ACTOR, `Scenario '${workflowName}' calls step '${step.stepFunc}', which ${why}, without an actor. ` +
|
|
946
921
|
`Pass { actor: actors.<name> } so it runs as that persona.`);
|
|
947
922
|
}
|
|
948
923
|
continue;
|
|
949
924
|
}
|
|
950
|
-
if (step.type ===
|
|
925
|
+
if (step.type === "branch") {
|
|
951
926
|
for (const branch of step.branches)
|
|
952
927
|
visit(workflowName, branch.steps);
|
|
953
928
|
if (step.elseSteps)
|
|
954
929
|
visit(workflowName, step.elseSteps);
|
|
955
930
|
}
|
|
956
|
-
else if (step.type ===
|
|
931
|
+
else if (step.type === "switch") {
|
|
957
932
|
for (const c of step.cases ?? []) {
|
|
958
933
|
if (c.steps)
|
|
959
934
|
visit(workflowName, c.steps);
|
|
@@ -961,10 +936,10 @@ export function validateScenarioSteps(logger, state) {
|
|
|
961
936
|
if (step.defaultSteps)
|
|
962
937
|
visit(workflowName, step.defaultSteps);
|
|
963
938
|
}
|
|
964
|
-
else if (step.type ===
|
|
939
|
+
else if (step.type === "fanout" && step.body) {
|
|
965
940
|
visit(workflowName, step.body);
|
|
966
941
|
}
|
|
967
|
-
else if (step.type ===
|
|
942
|
+
else if (step.type === "parallel" && step.children) {
|
|
968
943
|
visit(workflowName, step.children);
|
|
969
944
|
}
|
|
970
945
|
}
|
|
@@ -979,8 +954,8 @@ export function validateScenarioSteps(logger, state) {
|
|
|
979
954
|
// `asserts` is the escape for a flow whose witness is an expectation helper
|
|
980
955
|
// instead: those are inline steps and leave no phase behind, but a recorded
|
|
981
956
|
// service call or a refusal is an assertion all the same.
|
|
982
|
-
if (phasesSeen.size > 0 && !phasesSeen.has(
|
|
983
|
-
logger.critical(ErrorCode.SCENARIO_HAS_NO_ASSERTION, `Scenario '${workflowName}' has ${[...phasesSeen].join(
|
|
957
|
+
if (phasesSeen.size > 0 && !phasesSeen.has("then") && !meta.asserts) {
|
|
958
|
+
logger.critical(ErrorCode.SCENARIO_HAS_NO_ASSERTION, `Scenario '${workflowName}' has ${[...phasesSeen].join("/")} steps but never asserts. ` +
|
|
984
959
|
`Add a scenario.then(...) naming what the actor should now see — a flow with no 'then' ` +
|
|
985
960
|
`only proves nothing threw, and contributes nothing to witness coverage.`);
|
|
986
961
|
}
|
|
@@ -1041,17 +1016,17 @@ export function validateScenarioFeatures(logger, state) {
|
|
|
1041
1016
|
.map((feature) => feature.exportedName);
|
|
1042
1017
|
if (unreadable.length > 0) {
|
|
1043
1018
|
logger.diagnostic({
|
|
1044
|
-
severity:
|
|
1019
|
+
severity: "warn",
|
|
1045
1020
|
code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
|
|
1046
|
-
message: `${unowned.map((name) => `'${name}'`).join(
|
|
1047
|
-
`but ${unreadable.map((name) => `'${name}'`).join(
|
|
1021
|
+
message: `${unowned.map((name) => `'${name}'`).join(", ")} appear in no feature's \`scenarios\` array, ` +
|
|
1022
|
+
`but ${unreadable.map((name) => `'${name}'`).join(", ")} build theirs with a spread or a call, so one of them may still be listed there. ` +
|
|
1048
1023
|
`List those scenarios literally and this becomes a definite answer either way.`,
|
|
1049
1024
|
});
|
|
1050
1025
|
return;
|
|
1051
1026
|
}
|
|
1052
1027
|
for (const name of unowned) {
|
|
1053
1028
|
logger.diagnostic({
|
|
1054
|
-
severity:
|
|
1029
|
+
severity: "error",
|
|
1055
1030
|
code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
|
|
1056
1031
|
message: `Scenario '${name}' is listed by no pikkuFeature, so nothing downstream can reach it — ` +
|
|
1057
1032
|
`a run stamps no feature onto its results, the console groups it under none, and a guide cannot cite it. ` +
|
|
@@ -1066,16 +1041,16 @@ export function validateScenarioFeatures(logger, state) {
|
|
|
1066
1041
|
* early with an actionable message pointing at `scaffold: { graph: true }`.
|
|
1067
1042
|
*/
|
|
1068
1043
|
export function validateWorkflowGraphAddons(logger, state) {
|
|
1069
|
-
const addonGraphWired = Array.from(state.rpc.wireAddonDeclarations.values()).some((decl) => decl.package ===
|
|
1044
|
+
const addonGraphWired = Array.from(state.rpc.wireAddonDeclarations.values()).some((decl) => decl.package === "@pikku/addon-graph");
|
|
1070
1045
|
if (addonGraphWired) {
|
|
1071
1046
|
return;
|
|
1072
1047
|
}
|
|
1073
1048
|
for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
1074
1049
|
for (const node of Object.values(graph.nodes)) {
|
|
1075
|
-
if (!(
|
|
1050
|
+
if (!("rpcName" in node) || typeof node.rpcName !== "string") {
|
|
1076
1051
|
continue;
|
|
1077
1052
|
}
|
|
1078
|
-
if (!node.rpcName.startsWith(
|
|
1053
|
+
if (!node.rpcName.startsWith("graph:")) {
|
|
1079
1054
|
continue;
|
|
1080
1055
|
}
|
|
1081
1056
|
if (state.functions.meta[node.rpcName]) {
|
|
@@ -1125,7 +1100,7 @@ export function validateSchemaWiringSeparation(logger, state) {
|
|
|
1125
1100
|
const schemas = Array.from(state.schemaLookup.entries())
|
|
1126
1101
|
.filter(([, ref]) => ref.sourceFile === file)
|
|
1127
1102
|
.map(([name]) => name);
|
|
1128
|
-
logger.critical(ErrorCode.SCHEMA_AND_WIRING_COLOCATED, `File '${file}' contains both Zod schemas (${schemas.join(
|
|
1103
|
+
logger.critical(ErrorCode.SCHEMA_AND_WIRING_COLOCATED, `File '${file}' contains both Zod schemas (${schemas.join(", ")}) and wiring calls (wireHTTPRoutes, addPermission, etc.). ` +
|
|
1129
1104
|
`These must be in separate files because the CLI imports schema files at runtime, which triggers wiring side-effects that crash without server context. ` +
|
|
1130
1105
|
`Move the route/wiring definitions to a dedicated wiring file.`);
|
|
1131
1106
|
}
|
|
@@ -1140,7 +1115,7 @@ export function validateSchemaWiringSeparation(logger, state) {
|
|
|
1140
1115
|
* not.
|
|
1141
1116
|
*/
|
|
1142
1117
|
export function validateNoSecretAliasServices(logger, checker, state) {
|
|
1143
|
-
if (!(
|
|
1118
|
+
if (!("typesLookup" in state)) {
|
|
1144
1119
|
return;
|
|
1145
1120
|
}
|
|
1146
1121
|
const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
|
|
@@ -1156,14 +1131,14 @@ export function validateNoSecretAliasServices(logger, checker, state) {
|
|
|
1156
1131
|
if (used.length === 0) {
|
|
1157
1132
|
return;
|
|
1158
1133
|
}
|
|
1159
|
-
logger.critical(ErrorCode.SECRET_SERVICE_ALIASED, `${kind} '${id}' receives ${used.map((s) => `'${s}'`).join(
|
|
1134
|
+
logger.critical(ErrorCode.SECRET_SERVICE_ALIASED, `${kind} '${id}' receives ${used.map((s) => `'${s}'`).join(", ")}, which ${used.length === 1 ? "is" : "are"} a SecretService under another name. ` +
|
|
1160
1135
|
`SecretService is confined to pikkuServices, pikkuWireServices, addon service factories and middleware — give a service the secret value when you construct it and expose only what the function needs.`);
|
|
1161
1136
|
};
|
|
1162
1137
|
for (const [id, meta] of Object.entries(state.functions.meta)) {
|
|
1163
|
-
report(
|
|
1138
|
+
report("Function", id, meta.services?.services ?? []);
|
|
1164
1139
|
}
|
|
1165
1140
|
for (const [id, def] of Object.entries(state.permissions.definitions)) {
|
|
1166
|
-
report(
|
|
1141
|
+
report("Permission", id, def.services?.services ?? []);
|
|
1167
1142
|
}
|
|
1168
1143
|
}
|
|
1169
1144
|
/**
|
|
@@ -1192,22 +1167,22 @@ export function validateSecretUsage(logger, state) {
|
|
|
1192
1167
|
// auth-secrets.gen.ts declares it, but that file is written after the first
|
|
1193
1168
|
// inspection of a fresh project, so its defineSecret is not seen yet.
|
|
1194
1169
|
if (state.auth?.definition)
|
|
1195
|
-
declared.add(
|
|
1170
|
+
declared.add("BETTER_AUTH_SECRET");
|
|
1196
1171
|
for (const [file, usage] of state.secrets.usage) {
|
|
1197
1172
|
const relativeFile = relative(state.rootDir, file);
|
|
1198
1173
|
const undeclared = usage.keys.filter((key) => !declared.has(key));
|
|
1199
1174
|
if (undeclared.length > 0) {
|
|
1200
1175
|
logger.diagnostic({
|
|
1201
|
-
severity:
|
|
1176
|
+
severity: "warn",
|
|
1202
1177
|
code: ErrorCode.SECRET_NOT_DECLARED,
|
|
1203
|
-
message: `${relativeFile} reads ${undeclared.map((k) => `'${k}'`).join(
|
|
1178
|
+
message: `${relativeFile} reads ${undeclared.map((k) => `'${k}'`).join(", ")}, which no defineSecret declares. Declare it so it is validated at deploy time and shown to whoever has to provision it.`,
|
|
1204
1179
|
});
|
|
1205
1180
|
}
|
|
1206
1181
|
if (usage.dynamic.length > 0) {
|
|
1207
1182
|
logger.diagnostic({
|
|
1208
|
-
severity:
|
|
1183
|
+
severity: "warn",
|
|
1209
1184
|
code: ErrorCode.SECRET_KEY_NOT_STATIC,
|
|
1210
|
-
message: `${relativeFile} reads a secret with a non-literal key (${usage.dynamic.join(
|
|
1185
|
+
message: `${relativeFile} reads a secret with a non-literal key (${usage.dynamic.join(", ")}). The deployment cannot narrow its secret scope around a key it cannot see.`,
|
|
1211
1186
|
});
|
|
1212
1187
|
}
|
|
1213
1188
|
}
|
|
@@ -1220,7 +1195,10 @@ export function computeDiagnostics(state) {
|
|
|
1220
1195
|
// bridges that reference addon functions (id `http:<method>:<route>`, no
|
|
1221
1196
|
// source file). The user can't edit any of these, so a destructure lint
|
|
1222
1197
|
// meant to nudge them about their own code must not fail the build over them.
|
|
1223
|
-
if (!meta.sourceFile ||
|
|
1198
|
+
if (!meta.sourceFile ||
|
|
1199
|
+
meta.sourceFile.endsWith(".gen.ts") ||
|
|
1200
|
+
meta.functionType === "inline" ||
|
|
1201
|
+
meta.functionType === "helper") {
|
|
1224
1202
|
continue;
|
|
1225
1203
|
}
|
|
1226
1204
|
if (meta.services && !meta.services.optimized) {
|
|
@@ -1300,14 +1278,14 @@ export function validateScopeReferences(logger, state) {
|
|
|
1300
1278
|
continue;
|
|
1301
1279
|
for (const scope of meta.scopes) {
|
|
1302
1280
|
// A trailing wildcard grants a subtree; the node it hangs off must exist.
|
|
1303
|
-
const declaredForm = scope.endsWith(
|
|
1304
|
-
if (scope ===
|
|
1281
|
+
const declaredForm = scope.endsWith(":*") ? scope.slice(0, -2) : scope;
|
|
1282
|
+
if (scope === "*") {
|
|
1305
1283
|
logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires the bare wildcard scope '*'. A function must require a specific scope — '*' is only meaningful as a grant.`);
|
|
1306
1284
|
continue;
|
|
1307
1285
|
}
|
|
1308
1286
|
if (!declared.has(declaredForm)) {
|
|
1309
1287
|
const available = Array.from(declared);
|
|
1310
|
-
logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires scope '${scope}' which is not declared. Declare it with defineScope. Available scopes: ${available.join(
|
|
1288
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires scope '${scope}' which is not declared. Declare it with defineScope. Available scopes: ${available.join(", ") || "none"}`);
|
|
1311
1289
|
}
|
|
1312
1290
|
}
|
|
1313
1291
|
}
|
|
@@ -1334,12 +1312,12 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1334
1312
|
return;
|
|
1335
1313
|
const { wireAddonDeclarations } = state.rpc;
|
|
1336
1314
|
for (const [agentKey, agent] of agents) {
|
|
1337
|
-
const where = agent.sourceFile ? ` (${agent.sourceFile})` :
|
|
1315
|
+
const where = agent.sourceFile ? ` (${agent.sourceFile})` : "";
|
|
1338
1316
|
for (const tool of agent.tools ?? []) {
|
|
1339
1317
|
// A workflow reference is resolved against the workflow map, not here.
|
|
1340
|
-
if (tool.startsWith(
|
|
1318
|
+
if (tool.startsWith("workflow:"))
|
|
1341
1319
|
continue;
|
|
1342
|
-
const separator = tool.indexOf(
|
|
1320
|
+
const separator = tool.indexOf(":");
|
|
1343
1321
|
let meta;
|
|
1344
1322
|
if (separator === -1) {
|
|
1345
1323
|
const funcId = state.rpc.internalMeta[tool] ?? tool;
|
|
@@ -1356,7 +1334,7 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1356
1334
|
if (!addon) {
|
|
1357
1335
|
const known = Array.from(wireAddonDeclarations?.keys() ?? []);
|
|
1358
1336
|
logger.critical(ErrorCode.AGENT_TOOL_UNKNOWN_NAMESPACE, `AI agent '${agentKey}'${where} references tool '${tool}', but no addon is wired under the namespace '${namespace}'. ` +
|
|
1359
|
-
`Wired namespaces: ${known.join(
|
|
1337
|
+
`Wired namespaces: ${known.join(", ") || "none"}.`);
|
|
1360
1338
|
continue;
|
|
1361
1339
|
}
|
|
1362
1340
|
// An addon that has not been built yet contributed no metadata, which
|
|
@@ -1373,7 +1351,7 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1373
1351
|
if (options.strictMeta && !meta.description) {
|
|
1374
1352
|
logger.critical(ErrorCode.AGENT_TOOL_MISSING_DESCRIPTION, `AI agent '${agentKey}'${where} uses tool '${tool}', which has no description. ` +
|
|
1375
1353
|
`An agent tool's description is what the model is told it does; without one it is offered the tool under its own name. ` +
|
|
1376
|
-
`Add a 'description' to '${tool}'${meta.title ? " (a 'title' does not count — it labels the tool, it does not explain it)" :
|
|
1354
|
+
`Add a 'description' to '${tool}'${meta.title ? " (a 'title' does not count — it labels the tool, it does not explain it)" : ""}.`);
|
|
1377
1355
|
}
|
|
1378
1356
|
}
|
|
1379
1357
|
}
|
|
@@ -1388,12 +1366,12 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1388
1366
|
export function validateAgentScorerReferences(logger, state) {
|
|
1389
1367
|
const known = Object.keys(state.scorers.scorersMeta);
|
|
1390
1368
|
for (const [agentKey, agent] of Object.entries(state.agents.agentsMeta)) {
|
|
1391
|
-
const where = agent.sourceFile ? ` (${agent.sourceFile})` :
|
|
1369
|
+
const where = agent.sourceFile ? ` (${agent.sourceFile})` : "";
|
|
1392
1370
|
for (const scorer of agent.scorers ?? []) {
|
|
1393
1371
|
if (known.includes(scorer))
|
|
1394
1372
|
continue;
|
|
1395
1373
|
logger.critical(ErrorCode.AGENT_SCORER_NOT_FOUND, `AI agent '${agentKey}'${where} asks to be graded by scorer '${scorer}', which is not declared in this project. ` +
|
|
1396
|
-
`Declared scorers: ${known.join(
|
|
1374
|
+
`Declared scorers: ${known.join(", ") || "none"}.`);
|
|
1397
1375
|
}
|
|
1398
1376
|
}
|
|
1399
1377
|
}
|
|
@@ -1416,14 +1394,14 @@ export function validateSystemRoleScopes(logger, state) {
|
|
|
1416
1394
|
const declared = new Set(flattenScopeDefinitions(state.scopes.definitions).map((s) => s.id));
|
|
1417
1395
|
for (const role of state.systemRoles.definitions) {
|
|
1418
1396
|
for (const scope of role.scopes) {
|
|
1419
|
-
if (scope ===
|
|
1397
|
+
if (scope === "*") {
|
|
1420
1398
|
logger.critical(ErrorCode.INVALID_VALUE, `System role '${role.name}' grants the bare wildcard scope '*'. Grant the roots it should cover instead — '*' hides what the role actually confers.`);
|
|
1421
1399
|
continue;
|
|
1422
1400
|
}
|
|
1423
|
-
const declaredForm = scope.endsWith(
|
|
1401
|
+
const declaredForm = scope.endsWith(":*") ? scope.slice(0, -2) : scope;
|
|
1424
1402
|
if (!declared.has(declaredForm)) {
|
|
1425
1403
|
const available = Array.from(declared);
|
|
1426
|
-
logger.critical(ErrorCode.INVALID_VALUE, `System role '${role.name}' grants scope '${scope}' which is not declared. Declare it with defineScope. Available scopes: ${available.join(
|
|
1404
|
+
logger.critical(ErrorCode.INVALID_VALUE, `System role '${role.name}' grants scope '${scope}' which is not declared. Declare it with defineScope. Available scopes: ${available.join(", ") || "none"}`);
|
|
1427
1405
|
}
|
|
1428
1406
|
}
|
|
1429
1407
|
}
|
|
@@ -1450,7 +1428,7 @@ export function validatePersonaRoles(logger, state) {
|
|
|
1450
1428
|
const available = Array.from(declared).sort();
|
|
1451
1429
|
logger.critical(ErrorCode.INVALID_VALUE, `Persona '${persona.id}' holds role '${role}', which is not declared with defineSystemRole. ` +
|
|
1452
1430
|
`A persona may only name a system role — a role composed in the console can be deleted, and the persona would go on claiming to test it. ` +
|
|
1453
|
-
`Declared roles: ${available.join(
|
|
1431
|
+
`Declared roles: ${available.join(", ") || "none"}`);
|
|
1454
1432
|
}
|
|
1455
1433
|
}
|
|
1456
1434
|
}
|