@pikku/inspector 0.12.92 → 0.12.95
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 +31 -0
- package/dist/add/add-trigger.js +4 -15
- package/dist/utils/ensure-function-metadata.js +3 -0
- package/dist/utils/post-process.d.ts +23 -23
- package/dist/utils/post-process.js +151 -155
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
## 0.12.95
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 6ecf80e: Stop the services-destructure lint (PKU410) firing on the stub a wiring registers for its function. Since inline functions carry a source file, a queue worker wired to a named function failed the build with a critical PKU410 even though that function destructures its services.
|
|
6
|
+
- 2612a67: The services-destructure lint also skips the `helper` wiring functions `ensureFunctionMetadata` synthesizes, alongside the `inline` ones it already skips, and a test pins that an inline wiring function that records a source file is not flagged.
|
|
7
|
+
|
|
8
|
+
## 0.12.94
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 4323bcf: Inline wiring functions now record their source file, so `pikku release` leaves scaffold-injected ones out of the API surface.
|
|
13
|
+
|
|
14
|
+
## 0.12.93
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 658f047: A function whose services are all singletons no longer builds wire services, so an addon's credential-bound wire factory does not run for, or fail, a webhook `receive` step that needs no connection. The inspector decides this from the `Services` type and records it as `singletonServicesOnly` on the function's runtime meta; the runner only reads the flag.
|
|
19
|
+
- 658f047: Webhook signing secrets live in the credential store. `WebhookSigningSecret.fromCredential(provider, credentialService, name)` reads the secret per delivery via `load()`, so a `setup` step (or a handshake) that stores a new one with `credentialService.set` takes effect without a deploy.
|
|
20
|
+
|
|
21
|
+
Breaking: `setup` no longer returns `secret`, `wireTriggerWebhookSource` no longer takes `secret`, lifecycle outcomes drop `secretName`/`secret`, and `pikku webhooks setup` drops `--secretsOut`.
|
|
22
|
+
|
|
23
|
+
- 658f047: A webhook source's `method` may be a list, mounting one route per method, for providers that verify the URL with a GET and deliver events with a POST (WhatsApp, Strava, Onfleet, Mailchimp), or a HEAD (Trello, Mandrill, SurveyMonkey).
|
|
24
|
+
- Updated dependencies [658f047]
|
|
25
|
+
- Updated dependencies [de63ab2]
|
|
26
|
+
- Updated dependencies [658f047]
|
|
27
|
+
- Updated dependencies [658f047]
|
|
28
|
+
- Updated dependencies [658f047]
|
|
29
|
+
- Updated dependencies [658f047]
|
|
30
|
+
- @pikku/core@0.12.128
|
|
31
|
+
|
|
1
32
|
## 0.12.92
|
|
2
33
|
|
|
3
34
|
### Patch Changes
|
package/dist/add/add-trigger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
|
|
2
|
+
import { getArrayPropertyValue, getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
|
|
3
3
|
import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
|
|
4
4
|
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
|
|
5
5
|
import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js';
|
|
@@ -114,9 +114,10 @@ const addWireTriggerWebhookSource = (logger, node, checker, state, firstArg) =>
|
|
|
114
114
|
logger.critical(ErrorCode.MISSING_NAME, `wireTriggerWebhookSource in ${node.getSourceFile().fileName} needs 'name' as a string literal.`);
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
|
-
const method =
|
|
117
|
+
const method = getArrayPropertyValue(obj, 'method') ??
|
|
118
|
+
getPropertyValue(obj, 'method') ??
|
|
119
|
+
'post';
|
|
118
120
|
const route = getPropertyValue(obj, 'route') ?? `/webhooks/${name}`;
|
|
119
|
-
let secret = getPropertyValue(obj, 'secret');
|
|
120
121
|
const eventsInitializer = getPropertyAssignmentInitializer(obj, 'events', false, checker);
|
|
121
122
|
const events = eventsInitializer
|
|
122
123
|
? checker
|
|
@@ -151,23 +152,11 @@ const addWireTriggerWebhookSource = (logger, node, checker, state, firstArg) =>
|
|
|
151
152
|
steps[step] = pikkuFuncId;
|
|
152
153
|
state.serviceAggregation.usedFunctions.add(pikkuFuncId);
|
|
153
154
|
}
|
|
154
|
-
// An addon's receive reads the secret by its own name; the app may have
|
|
155
|
-
// renamed it for this instance.
|
|
156
|
-
const namespace = steps.receive?.includes(':')
|
|
157
|
-
? steps.receive.slice(0, steps.receive.indexOf(':'))
|
|
158
|
-
: null;
|
|
159
|
-
const addon = namespace
|
|
160
|
-
? state.rpc.wireAddonDeclarations.get(namespace)
|
|
161
|
-
: undefined;
|
|
162
|
-
if (secret && addon?.secretOverrides?.[secret]) {
|
|
163
|
-
secret = addon.secretOverrides[secret];
|
|
164
|
-
}
|
|
165
155
|
const meta = {
|
|
166
156
|
name,
|
|
167
157
|
method: method,
|
|
168
158
|
route,
|
|
169
159
|
events,
|
|
170
|
-
...(secret ? { secret } : {}),
|
|
171
160
|
...steps,
|
|
172
161
|
};
|
|
173
162
|
state.triggers.webhookSourceMeta[name] = meta;
|
|
@@ -176,6 +176,9 @@ export function ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcIni
|
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
const meta = state.functions.meta[pikkuFuncId];
|
|
179
|
+
if (!meta.sourceFile && funcInitializer) {
|
|
180
|
+
meta.sourceFile = funcInitializer.getSourceFile().fileName;
|
|
181
|
+
}
|
|
179
182
|
if (funcInitializer && checker) {
|
|
180
183
|
let pikkuFuncCall = null;
|
|
181
184
|
if (ts.isCallExpression(funcInitializer)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { InspectorState, InspectorLogger, InspectorOptions } from
|
|
2
|
-
import type { PermissionMetadata } from
|
|
3
|
-
import type { MiddlewareMetadata } from
|
|
4
|
-
import type ts from
|
|
1
|
+
import type { InspectorState, InspectorLogger, InspectorOptions } from "../types.js";
|
|
2
|
+
import type { PermissionMetadata } from "@pikku/core/function";
|
|
3
|
+
import type { MiddlewareMetadata } from "@pikku/core/middleware";
|
|
4
|
+
import type ts from "typescript";
|
|
5
5
|
/**
|
|
6
6
|
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
7
7
|
* handler's function meta.
|
|
@@ -15,7 +15,7 @@ import type ts from 'typescript';
|
|
|
15
15
|
* onto the handler meta. Re-derived every inspect and ordered BEFORE
|
|
16
16
|
* `aggregateRequiredServices` so it flows into `requiredServices`.
|
|
17
17
|
*/
|
|
18
|
-
export declare function stampAuthHandlerServices(state: InspectorState | Omit<InspectorState,
|
|
18
|
+
export declare function stampAuthHandlerServices(state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
19
19
|
/**
|
|
20
20
|
* Helper to extract wire-level middleware/permission names from metadata.
|
|
21
21
|
* Only extracts type:'wire' variants (individual middleware/permissions).
|
|
@@ -29,10 +29,10 @@ export declare function extractWireNames(list?: Array<MiddlewareMetadata | Permi
|
|
|
29
29
|
* Note: usedFunctions, usedMiddleware, and usedPermissions are tracked directly
|
|
30
30
|
* in the add-* methods during AST traversal for efficiency.
|
|
31
31
|
*/
|
|
32
|
-
export declare function aggregateRequiredServices(state: InspectorState | Omit<InspectorState,
|
|
33
|
-
export declare function validateSecretOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
34
|
-
export declare function validateCredentialOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
35
|
-
export declare function validateVariableOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
32
|
+
export declare function aggregateRequiredServices(state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
33
|
+
export declare function validateSecretOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
34
|
+
export declare function validateCredentialOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
35
|
+
export declare function validateVariableOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
36
36
|
/**
|
|
37
37
|
* Every wired addon must be installed, in the bucket matching how it is used.
|
|
38
38
|
*
|
|
@@ -56,14 +56,14 @@ export declare function validateVariableOverrides(logger: InspectorLogger, state
|
|
|
56
56
|
* up in `dependencies`. Asking the resolver is the only question that matches
|
|
57
57
|
* what the loader will actually do.
|
|
58
58
|
*/
|
|
59
|
-
export declare function validateRemoteAddonDependencies(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
59
|
+
export declare function validateRemoteAddonDependencies(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
60
60
|
/**
|
|
61
61
|
* The auth a `wireRemoteAddon` consumer binds must reference a slot the consumer
|
|
62
62
|
* actually declares: a `credentialId` must be a wired credential, a `secretId`
|
|
63
63
|
* a wired secret. A custom `resolve()` and a public (omitted) surface are not
|
|
64
64
|
* statically checkable and are left to runtime.
|
|
65
65
|
*/
|
|
66
|
-
export declare function validateRemoteAddonAuth(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
66
|
+
export declare function validateRemoteAddonAuth(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
67
67
|
export declare function computeResolvedIOTypes(state: InspectorState): void;
|
|
68
68
|
export declare function computeMiddlewareGroupsMeta(state: InspectorState): void;
|
|
69
69
|
export declare function computePermissionsGroupsMeta(state: InspectorState): void;
|
|
@@ -88,7 +88,7 @@ export declare function validateSchemaReferences(logger: InspectorLogger, state:
|
|
|
88
88
|
* A model is either concrete (`provider/model`) or an alias, which must appear
|
|
89
89
|
* in the `models` table from pikku.config.json.
|
|
90
90
|
*/
|
|
91
|
-
export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
91
|
+
export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">, modelAliases?: Record<string, string>): void;
|
|
92
92
|
/**
|
|
93
93
|
* Scenarios are pure stories of remote RPCs (same rule as client-side CLI
|
|
94
94
|
* renderers): the func may only destructure logger/config — everything else
|
|
@@ -100,7 +100,7 @@ export declare function validateAgentModels(logger: InspectorLogger, state: Insp
|
|
|
100
100
|
* `addFunctions` (`visitFunctions`) has populated `state.functions.meta` — so
|
|
101
101
|
* the check read `undefined` there and never fired.
|
|
102
102
|
*/
|
|
103
|
-
export declare function validateScenarioServices(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
103
|
+
export declare function validateScenarioServices(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
104
104
|
/**
|
|
105
105
|
* Does this step's prose open by naming the actor who is already its subject?
|
|
106
106
|
*
|
|
@@ -128,7 +128,7 @@ export declare const proseOpensWithActor: (prose: string, actor: string) => bool
|
|
|
128
128
|
* It also marks the step's function as used, so `scenario run` boots the
|
|
129
129
|
* singletons the step actually needs.
|
|
130
130
|
*/
|
|
131
|
-
export declare function validateScenarioSteps(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
131
|
+
export declare function validateScenarioSteps(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
132
132
|
/**
|
|
133
133
|
* Reports scenarios that no `pikkuFeature` lists.
|
|
134
134
|
*
|
|
@@ -153,7 +153,7 @@ export declare function validateScenarioSteps(logger: InspectorLogger, state: In
|
|
|
153
153
|
* the check reports it then and downgrades to a warn naming both sides, rather
|
|
154
154
|
* than accusing a scenario that may well be in that array.
|
|
155
155
|
*/
|
|
156
|
-
export declare function validateScenarioFeatures(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
156
|
+
export declare function validateScenarioFeatures(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
157
157
|
/**
|
|
158
158
|
* A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
|
|
159
159
|
* `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
|
|
@@ -178,7 +178,7 @@ export declare function validateSchemaWiringSeparation(logger: InspectorLogger,
|
|
|
178
178
|
* a name the omit does not cover. A name-based rule misses this; the type does
|
|
179
179
|
* not.
|
|
180
180
|
*/
|
|
181
|
-
export declare function validateNoSecretAliasServices(logger: InspectorLogger, checker: ts.TypeChecker, state: InspectorState | Omit<InspectorState,
|
|
181
|
+
export declare function validateNoSecretAliasServices(logger: InspectorLogger, checker: ts.TypeChecker, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
182
182
|
/**
|
|
183
183
|
* Reports secret reads the catalogue does not account for.
|
|
184
184
|
*
|
|
@@ -191,8 +191,8 @@ export declare function validateNoSecretAliasServices(logger: InspectorLogger, c
|
|
|
191
191
|
* deployment is asked for the client id and secret every connect flow needs
|
|
192
192
|
* without an author restating a shape the runtime already fixes.
|
|
193
193
|
*/
|
|
194
|
-
export declare function registerDerivedOAuth2AppSecrets(state: InspectorState | Omit<InspectorState,
|
|
195
|
-
export declare function validateSecretUsage(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
194
|
+
export declare function registerDerivedOAuth2AppSecrets(state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
195
|
+
export declare function validateSecretUsage(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
196
196
|
export declare function computeDiagnostics(state: InspectorState): void;
|
|
197
197
|
/**
|
|
198
198
|
* Validates that every scope referenced by a function is declared via
|
|
@@ -201,7 +201,7 @@ export declare function computeDiagnostics(state: InspectorState): void;
|
|
|
201
201
|
* A `*` suffix is a wildcard requirement: `admin:*` requires the `admin` scope
|
|
202
202
|
* to be declared, and grants its whole subtree.
|
|
203
203
|
*/
|
|
204
|
-
export declare function validateScopeReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
204
|
+
export declare function validateScopeReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
205
205
|
/**
|
|
206
206
|
* Resolves every `tools:` entry on every agent and reports the ones that do not
|
|
207
207
|
* land on a real function, or that land on one with nothing to tell the model.
|
|
@@ -218,7 +218,7 @@ export declare function validateScopeReferences(logger: InspectorLogger, state:
|
|
|
218
218
|
* quality loss rather than a failure. A title deliberately does not satisfy it —
|
|
219
219
|
* a title labels a tool in a UI, it does not tell a model when to reach for it.
|
|
220
220
|
*/
|
|
221
|
-
export declare function validateAgentToolReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
221
|
+
export declare function validateAgentToolReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">, options?: InspectorOptions): void;
|
|
222
222
|
/**
|
|
223
223
|
* Validates that every scorer an agent asks to be graded by exists.
|
|
224
224
|
*
|
|
@@ -226,7 +226,7 @@ export declare function validateAgentToolReferences(logger: InspectorLogger, sta
|
|
|
226
226
|
* that has already answered the user — which means a typo would otherwise cost
|
|
227
227
|
* nothing visible and simply grade nothing, forever.
|
|
228
228
|
*/
|
|
229
|
-
export declare function validateAgentScorerReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
229
|
+
export declare function validateAgentScorerReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
230
230
|
/**
|
|
231
231
|
* Validates that every scope granted by a system role is declared via
|
|
232
232
|
* `defineScope`. Runs after all visitors, so declaration order does not matter.
|
|
@@ -239,7 +239,7 @@ export declare function validateAgentScorerReferences(logger: InspectorLogger, s
|
|
|
239
239
|
*
|
|
240
240
|
* A `*` suffix grants a subtree; the node it hangs off must exist.
|
|
241
241
|
*/
|
|
242
|
-
export declare function validateSystemRoleScopes(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
242
|
+
export declare function validateSystemRoleScopes(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
243
243
|
/**
|
|
244
244
|
* Validates that every role a persona names is declared with
|
|
245
245
|
* `defineSystemRole`. Runs after all visitors, so declaration order does not
|
|
@@ -251,4 +251,4 @@ export declare function validateSystemRoleScopes(logger: InspectorLogger, state:
|
|
|
251
251
|
* symptom is a scatter of 403s that read like authorization findings rather
|
|
252
252
|
* than like a deleted role.
|
|
253
253
|
*/
|
|
254
|
-
export declare function validatePersonaRoles(logger: InspectorLogger, state: InspectorState | Omit<InspectorState,
|
|
254
|
+
export declare function validatePersonaRoles(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
@@ -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
|
|
@@ -94,6 +94,24 @@ function extractAllServices(state) {
|
|
|
94
94
|
state.serviceAggregation.allWireServices = extractTypeKeys(servicesType)
|
|
95
95
|
.filter((name) => !singletonSet.has(name))
|
|
96
96
|
.sort();
|
|
97
|
+
markSingletonServicesOnly(state, new Set(state.serviceAggregation.allWireServices));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Marks the functions that never read a wire service, so the runner can skip
|
|
102
|
+
* `createWireServices` for them. Only called once the `Services` type has
|
|
103
|
+
* resolved: an unresolved type yields no wire services, which would otherwise
|
|
104
|
+
* mark every function.
|
|
105
|
+
*/
|
|
106
|
+
function markSingletonServicesOnly(state, wireServices) {
|
|
107
|
+
for (const meta of Object.values(state.functions.meta)) {
|
|
108
|
+
if (meta.services?.optimized &&
|
|
109
|
+
!meta.services.services.some((name) => wireServices.has(name))) {
|
|
110
|
+
meta.singletonServicesOnly = true;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
delete meta.singletonServicesOnly;
|
|
114
|
+
}
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
117
|
/**
|
|
@@ -108,7 +126,7 @@ export function aggregateRequiredServices(state) {
|
|
|
108
126
|
extractAllServices(state);
|
|
109
127
|
const { requiredServices, usedFunctions, usedMiddleware, usedPermissions } = state.serviceAggregation;
|
|
110
128
|
// Internal services (always excluded from tree-shaking)
|
|
111
|
-
const internalServices = new Set([
|
|
129
|
+
const internalServices = new Set(["rpc", "mcp", "channel", "userSession"]);
|
|
112
130
|
const addServices = (services) => {
|
|
113
131
|
if (!services || !services.services)
|
|
114
132
|
return;
|
|
@@ -154,15 +172,7 @@ export function aggregateRequiredServices(state) {
|
|
|
154
172
|
});
|
|
155
173
|
// 4. Services from middleware/permission groups used in wirings
|
|
156
174
|
// We need to check all wirings and expand any tag/HTTP-pattern groups they use
|
|
157
|
-
for (const method of [
|
|
158
|
-
'get',
|
|
159
|
-
'post',
|
|
160
|
-
'put',
|
|
161
|
-
'patch',
|
|
162
|
-
'delete',
|
|
163
|
-
'head',
|
|
164
|
-
'options',
|
|
165
|
-
]) {
|
|
175
|
+
for (const method of ["get", "post", "put", "patch", "delete", "head", "options"]) {
|
|
166
176
|
for (const routeMeta of Object.values(state.http.meta[method])) {
|
|
167
177
|
expandAndAddGroupServices(routeMeta.middleware, state, addServices);
|
|
168
178
|
}
|
|
@@ -200,23 +210,22 @@ export function aggregateRequiredServices(state) {
|
|
|
200
210
|
// that wrap workflow operations but don't destructure the services.
|
|
201
211
|
const hasWorkflows = Object.keys(state.workflows.graphMeta).length > 0 ||
|
|
202
212
|
Object.keys(state.workflows.meta).length > 0 ||
|
|
203
|
-
Object.keys(state.functions.meta).some((id) => id.startsWith(
|
|
204
|
-
id.startsWith(
|
|
205
|
-
id.startsWith(
|
|
213
|
+
Object.keys(state.functions.meta).some((id) => id.startsWith("workflowStart:") ||
|
|
214
|
+
id.startsWith("workflowStatus:") ||
|
|
215
|
+
id.startsWith("workflow:"));
|
|
206
216
|
if (hasWorkflows) {
|
|
207
|
-
requiredServices.add(
|
|
208
|
-
requiredServices.add(
|
|
209
|
-
requiredServices.add(
|
|
210
|
-
requiredServices.add(
|
|
217
|
+
requiredServices.add("workflowService");
|
|
218
|
+
requiredServices.add("workflowRunService");
|
|
219
|
+
requiredServices.add("schedulerService");
|
|
220
|
+
requiredServices.add("queueService");
|
|
211
221
|
}
|
|
212
222
|
// Webhook source routes and their worker are generated and destructure
|
|
213
223
|
// nothing, so they are found by id: the route accepts into the queue through
|
|
214
224
|
// incomingWebhookService and the worker records attempts through it.
|
|
215
225
|
const hasWebhookSources = Object.values(state.triggers?.webhookSourceMeta ?? {}).some(({ method, route }) => state.functions.meta[`http:${method}:${route}`] !== undefined);
|
|
216
|
-
if (hasWebhookSources ||
|
|
217
|
-
|
|
218
|
-
requiredServices.add(
|
|
219
|
-
requiredServices.add('queueService');
|
|
226
|
+
if (hasWebhookSources || state.functions.meta["queue:pikku-incoming-webhooks"] !== undefined) {
|
|
227
|
+
requiredServices.add("incomingWebhookService");
|
|
228
|
+
requiredServices.add("queueService");
|
|
220
229
|
}
|
|
221
230
|
// 6b. Inject synthetic queue workers for workflow graph steps.
|
|
222
231
|
// Each workflow gets an orchestrator queue and per-step queues.
|
|
@@ -230,11 +239,11 @@ export function aggregateRequiredServices(state) {
|
|
|
230
239
|
// synthetic entries were pure leakage: they put a
|
|
231
240
|
// `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
|
|
232
241
|
// bundle imports and a provider then creates as a real production queue.
|
|
233
|
-
if (graph.source ===
|
|
242
|
+
if (graph.source === "scenario")
|
|
234
243
|
continue;
|
|
235
244
|
const toKebab = (s) => s
|
|
236
|
-
.replace(/([a-z0-9])([A-Z])/g,
|
|
237
|
-
.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")
|
|
238
247
|
.toLowerCase();
|
|
239
248
|
// Orchestrator queue
|
|
240
249
|
const orchQueueName = `wf-orchestrator-${toKebab(graph.name)}`;
|
|
@@ -246,14 +255,11 @@ export function aggregateRequiredServices(state) {
|
|
|
246
255
|
}
|
|
247
256
|
// Per-step queues — only for steps explicitly marked workflowQueued: true
|
|
248
257
|
for (const node of Object.values(graph.nodes)) {
|
|
249
|
-
if (!(
|
|
258
|
+
if (!("rpcName" in node) || !node.rpcName)
|
|
250
259
|
continue;
|
|
251
260
|
const rpcName = node.rpcName;
|
|
252
|
-
const funcId = state.rpc?.internalMeta?.[rpcName] ??
|
|
253
|
-
|
|
254
|
-
rpcName;
|
|
255
|
-
const funcMeta = (state.functions.meta[funcId] ??
|
|
256
|
-
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]);
|
|
257
263
|
if (funcMeta?.workflowQueued !== true)
|
|
258
264
|
continue;
|
|
259
265
|
const stepQueueName = `wf-step-${toKebab(rpcName)}`;
|
|
@@ -267,20 +273,20 @@ export function aggregateRequiredServices(state) {
|
|
|
267
273
|
}
|
|
268
274
|
// AI agents need agentStorage + agentRunState + agentRunService + agentRunner
|
|
269
275
|
if (Object.keys(state.agents.agentsMeta).length > 0) {
|
|
270
|
-
requiredServices.add(
|
|
271
|
-
requiredServices.add(
|
|
272
|
-
requiredServices.add(
|
|
273
|
-
requiredServices.add(
|
|
276
|
+
requiredServices.add("agentStorage");
|
|
277
|
+
requiredServices.add("agentRunState");
|
|
278
|
+
requiredServices.add("agentRunService");
|
|
279
|
+
requiredServices.add("agentRunner");
|
|
274
280
|
}
|
|
275
281
|
// Channels need eventHub for pub/sub
|
|
276
282
|
if (Object.keys(state.channels.meta).length > 0) {
|
|
277
|
-
requiredServices.add(
|
|
283
|
+
requiredServices.add("eventHub");
|
|
278
284
|
}
|
|
279
285
|
// Declared scopes need scopeService to resolve a grant. Nothing in a project
|
|
280
286
|
// destructures it — the generated auth layer reaches it — so the declaration
|
|
281
287
|
// is the only signal there is.
|
|
282
288
|
if ((state.scopes?.definitions?.length ?? 0) > 0) {
|
|
283
|
-
requiredServices.add(
|
|
289
|
+
requiredServices.add("scopeService");
|
|
284
290
|
}
|
|
285
291
|
// Declared feature flags need featureFlags for the same reason, from the
|
|
286
292
|
// other side: `assertFeatureAvailable` reads the source out of the singleton
|
|
@@ -288,17 +294,17 @@ export function aggregateRequiredServices(state) {
|
|
|
288
294
|
// ever says the project has flags. Without this the flag tables are left out
|
|
289
295
|
// of the generated migration and every gate silently resolves open.
|
|
290
296
|
if ((state.featureFlags?.definitions?.length ?? 0) > 0) {
|
|
291
|
-
requiredServices.add(
|
|
297
|
+
requiredServices.add("featureFlags");
|
|
292
298
|
}
|
|
293
299
|
// A `defineAnalyticsEvents` declaration means events are emitted, and the
|
|
294
300
|
// invocation buffer flushes into `analyticsService`. Nothing destructures
|
|
295
301
|
// that either — functions are handed the request-scoped `analytics` — so the
|
|
296
302
|
// declaration is again the only signal.
|
|
297
303
|
if ((state.analytics?.length ?? 0) > 0) {
|
|
298
|
-
requiredServices.add(
|
|
304
|
+
requiredServices.add("analyticsService");
|
|
299
305
|
}
|
|
300
306
|
if ((state.credentials?.definitions?.length ?? 0) > 0) {
|
|
301
|
-
requiredServices.add(
|
|
307
|
+
requiredServices.add("credentialService");
|
|
302
308
|
}
|
|
303
309
|
// 7. Services that consumed addons need from the parent project.
|
|
304
310
|
const addonFnServices = new Map();
|
|
@@ -309,13 +315,7 @@ export function aggregateRequiredServices(state) {
|
|
|
309
315
|
}
|
|
310
316
|
const parentDeclared = state.addonRequiredParentServices ?? [];
|
|
311
317
|
const parentDeclaredSet = new Set(parentDeclared);
|
|
312
|
-
const defaultServices = new Set([
|
|
313
|
-
'config',
|
|
314
|
-
'logger',
|
|
315
|
-
'variables',
|
|
316
|
-
'schema',
|
|
317
|
-
'secrets',
|
|
318
|
-
]);
|
|
318
|
+
const defaultServices = new Set(["config", "logger", "variables", "schema", "secrets"]);
|
|
319
319
|
let usesAddonFn = false;
|
|
320
320
|
let addonFactoryNeeded = false;
|
|
321
321
|
for (const funcId of usedFunctions) {
|
|
@@ -331,8 +331,7 @@ export function aggregateRequiredServices(state) {
|
|
|
331
331
|
if (parentDeclaredSet.has(service)) {
|
|
332
332
|
requiredServices.add(service);
|
|
333
333
|
}
|
|
334
|
-
else if (!internalServices.has(service) &&
|
|
335
|
-
!defaultServices.has(service)) {
|
|
334
|
+
else if (!internalServices.has(service) && !defaultServices.has(service)) {
|
|
336
335
|
addonFactoryNeeded = true;
|
|
337
336
|
}
|
|
338
337
|
}
|
|
@@ -380,7 +379,7 @@ export function validateSecretOverrides(logger, state) {
|
|
|
380
379
|
for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides ?? {})) {
|
|
381
380
|
if (!secretIds.has(resolvedName)) {
|
|
382
381
|
const availableSecrets = Array.from(secretIds);
|
|
383
|
-
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"}`);
|
|
384
383
|
}
|
|
385
384
|
}
|
|
386
385
|
// A grant names the secret as the addon reads it, so it resolves through
|
|
@@ -389,7 +388,7 @@ export function validateSecretOverrides(logger, state) {
|
|
|
389
388
|
const resolvedName = addonDecl.secretOverrides?.[logicalName] ?? logicalName;
|
|
390
389
|
if (!secretIds.has(resolvedName)) {
|
|
391
390
|
const availableSecrets = Array.from(secretIds);
|
|
392
|
-
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"}`);
|
|
393
392
|
}
|
|
394
393
|
}
|
|
395
394
|
}
|
|
@@ -400,8 +399,8 @@ export function validateCredentialOverrides(logger, state) {
|
|
|
400
399
|
return;
|
|
401
400
|
const credentialNames = new Set(state.credentials?.definitions.map((d) => d.name) ?? []);
|
|
402
401
|
/** Only a rename has a target to check; a mode-only override renames nothing. */
|
|
403
|
-
const renameTarget = (override) => typeof override ===
|
|
404
|
-
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;
|
|
405
404
|
/**
|
|
406
405
|
* Which declaration set each resolved name's mode. Credential generation
|
|
407
406
|
* writes one shared metadata entry per name, so two declarations that map
|
|
@@ -418,10 +417,8 @@ export function validateCredentialOverrides(logger, state) {
|
|
|
418
417
|
const resolvedName = renamed ?? logicalName;
|
|
419
418
|
if (!credentialNames.has(resolvedName)) {
|
|
420
419
|
const availableCredentials = Array.from(credentialNames);
|
|
421
|
-
const target = renamed
|
|
422
|
-
|
|
423
|
-
: `'${logicalName}'`;
|
|
424
|
-
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"}`);
|
|
425
422
|
}
|
|
426
423
|
const mode = overrideMode(override);
|
|
427
424
|
if (!mode)
|
|
@@ -437,7 +434,7 @@ export function validateCredentialOverrides(logger, state) {
|
|
|
437
434
|
const resolvedName = (override ? renameTarget(override) : undefined) ?? logicalName;
|
|
438
435
|
if (!credentialNames.has(resolvedName)) {
|
|
439
436
|
const availableCredentials = Array.from(credentialNames);
|
|
440
|
-
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"}`);
|
|
441
438
|
}
|
|
442
439
|
}
|
|
443
440
|
}
|
|
@@ -455,7 +452,7 @@ export function validateVariableOverrides(logger, state) {
|
|
|
455
452
|
for (const [logicalName, resolvedName] of Object.entries(addonDecl.variableOverrides)) {
|
|
456
453
|
if (!variableIds.has(resolvedName)) {
|
|
457
454
|
const availableVariables = Array.from(variableIds);
|
|
458
|
-
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"}`);
|
|
459
456
|
}
|
|
460
457
|
}
|
|
461
458
|
}
|
|
@@ -471,11 +468,11 @@ export function validateVariableOverrides(logger, state) {
|
|
|
471
468
|
* attempt means it is genuinely absent.
|
|
472
469
|
*/
|
|
473
470
|
const isPackageResolvable = (pkg, dirs) => {
|
|
474
|
-
const NOT_FOUND = new Set([
|
|
471
|
+
const NOT_FOUND = new Set(["MODULE_NOT_FOUND", "ERR_MODULE_NOT_FOUND"]);
|
|
475
472
|
for (const dir of dirs) {
|
|
476
473
|
let req;
|
|
477
474
|
try {
|
|
478
|
-
req = createRequire(join(dir,
|
|
475
|
+
req = createRequire(join(dir, "package.json"));
|
|
479
476
|
}
|
|
480
477
|
catch {
|
|
481
478
|
return true; // cannot ask; do not accuse
|
|
@@ -529,7 +526,7 @@ export function validateRemoteAddonDependencies(logger, state) {
|
|
|
529
526
|
const dirs = addonResolutionDirs(state.rootDir, decl.file);
|
|
530
527
|
if (isPackageResolvable(decl.package, dirs))
|
|
531
528
|
continue;
|
|
532
|
-
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.`);
|
|
533
530
|
}
|
|
534
531
|
// A remote addon belongs in the devDependencies of the package that wires
|
|
535
532
|
// it, which in a workspace is not the root.
|
|
@@ -540,7 +537,7 @@ export function validateRemoteAddonDependencies(logger, state) {
|
|
|
540
537
|
let manifest = null;
|
|
541
538
|
if (existsSync(path)) {
|
|
542
539
|
try {
|
|
543
|
-
manifest = JSON.parse(readFileSync(path,
|
|
540
|
+
manifest = JSON.parse(readFileSync(path, "utf-8"));
|
|
544
541
|
}
|
|
545
542
|
catch (e) {
|
|
546
543
|
logger.warn(`Could not read ${path} to verify remote addon dependencies: ${e?.message ?? e}`);
|
|
@@ -552,7 +549,7 @@ export function validateRemoteAddonDependencies(logger, state) {
|
|
|
552
549
|
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
553
550
|
if (!decl.remote)
|
|
554
551
|
continue;
|
|
555
|
-
const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0],
|
|
552
|
+
const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0], "package.json");
|
|
556
553
|
const pkgJson = readManifest(pkgJsonPath);
|
|
557
554
|
if (!pkgJson)
|
|
558
555
|
continue; // no manifest to check (e.g. some tests)
|
|
@@ -584,10 +581,10 @@ export function validateRemoteAddonAuth(logger, state) {
|
|
|
584
581
|
if (!decl.remote)
|
|
585
582
|
continue;
|
|
586
583
|
if (decl.authCredentialId && !credentialNames.has(decl.authCredentialId)) {
|
|
587
|
-
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"}`);
|
|
588
585
|
}
|
|
589
586
|
if (decl.authSecretId && !secretNames.has(decl.authSecretId)) {
|
|
590
|
-
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"}`);
|
|
591
588
|
}
|
|
592
589
|
}
|
|
593
590
|
}
|
|
@@ -596,7 +593,7 @@ export function computeResolvedIOTypes(state) {
|
|
|
596
593
|
for (const [pikkuFuncId, meta] of Object.entries(functions.meta)) {
|
|
597
594
|
const input = meta.inputs?.[0];
|
|
598
595
|
const output = meta.outputs?.[0];
|
|
599
|
-
let inputType =
|
|
596
|
+
let inputType = "null";
|
|
600
597
|
if (input) {
|
|
601
598
|
try {
|
|
602
599
|
inputType = functions.typesMap.getTypeMeta(input).uniqueName;
|
|
@@ -605,7 +602,7 @@ export function computeResolvedIOTypes(state) {
|
|
|
605
602
|
inputType = input;
|
|
606
603
|
}
|
|
607
604
|
}
|
|
608
|
-
let outputType =
|
|
605
|
+
let outputType = "null";
|
|
609
606
|
if (output) {
|
|
610
607
|
try {
|
|
611
608
|
outputType = functions.typesMap.getTypeMeta(output).uniqueName;
|
|
@@ -615,10 +612,10 @@ export function computeResolvedIOTypes(state) {
|
|
|
615
612
|
}
|
|
616
613
|
}
|
|
617
614
|
state.resolvedIOTypes[pikkuFuncId] = { inputType, outputType };
|
|
618
|
-
if (meta.inputSchemaName && inputType !==
|
|
615
|
+
if (meta.inputSchemaName && inputType !== "null") {
|
|
619
616
|
meta.inputSchemaName = inputType;
|
|
620
617
|
}
|
|
621
|
-
if (meta.outputSchemaName && outputType !==
|
|
618
|
+
if (meta.outputSchemaName && outputType !== "null") {
|
|
622
619
|
meta.outputSchemaName = outputType;
|
|
623
620
|
}
|
|
624
621
|
if (meta.inputs) {
|
|
@@ -680,15 +677,15 @@ export function computePermissionsGroupsMeta(state) {
|
|
|
680
677
|
};
|
|
681
678
|
}
|
|
682
679
|
const PRIMITIVE_TYPES = new Set([
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
680
|
+
"boolean",
|
|
681
|
+
"string",
|
|
682
|
+
"number",
|
|
683
|
+
"null",
|
|
684
|
+
"undefined",
|
|
685
|
+
"void",
|
|
686
|
+
"any",
|
|
687
|
+
"unknown",
|
|
688
|
+
"never",
|
|
692
689
|
]);
|
|
693
690
|
export function computeRequiredSchemas(state, options) {
|
|
694
691
|
const { functions, schemaLookup } = state;
|
|
@@ -749,8 +746,8 @@ export function unresolvedSchemaReferences(state) {
|
|
|
749
746
|
};
|
|
750
747
|
for (const [funcName, meta] of Object.entries(functions.meta)) {
|
|
751
748
|
for (const [role, declared] of [
|
|
752
|
-
[
|
|
753
|
-
[
|
|
749
|
+
["input", meta.inputs?.[0]],
|
|
750
|
+
["output", meta.outputs?.[0]],
|
|
754
751
|
]) {
|
|
755
752
|
if (!declared || PRIMITIVE_TYPES.has(declared))
|
|
756
753
|
continue;
|
|
@@ -767,12 +764,12 @@ export function validateSchemaReferences(logger, state) {
|
|
|
767
764
|
if (unresolved.length === 0)
|
|
768
765
|
return;
|
|
769
766
|
logger.diagnostic({
|
|
770
|
-
severity:
|
|
767
|
+
severity: "error",
|
|
771
768
|
code: ErrorCode.SCHEMA_REFERENCE_UNRESOLVED,
|
|
772
|
-
message: `${unresolved.length} function contract${unresolved.length === 1 ?
|
|
773
|
-
`name${unresolved.length === 1 ?
|
|
774
|
-
`calling ${unresolved.length === 1 ?
|
|
775
|
-
`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(", ")}. ` +
|
|
776
773
|
`The usual cause is a named type used as the function's input/output that ` +
|
|
777
774
|
`is declared but not exported — export it, or inline the type at the call site.`,
|
|
778
775
|
});
|
|
@@ -791,11 +788,11 @@ export function validateAgentModels(logger, state, modelAliases = {}) {
|
|
|
791
788
|
// `hasOwn`, not a truthy lookup: every object literal inherits `toString`,
|
|
792
789
|
// `constructor` and `__proto__`, so a bare model named after one of them
|
|
793
790
|
// would read as a configured alias and pass a check nothing else repeats.
|
|
794
|
-
if (!model.includes(
|
|
791
|
+
if (!model.includes("/") && !Object.hasOwn(modelAliases, model)) {
|
|
795
792
|
const known = Object.keys(modelAliases).sort();
|
|
796
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. ` +
|
|
797
794
|
(known.length
|
|
798
|
-
? `Known aliases: ${known.join(
|
|
795
|
+
? `Known aliases: ${known.join(", ")}.`
|
|
799
796
|
: `No "models" aliases are configured.`));
|
|
800
797
|
}
|
|
801
798
|
}
|
|
@@ -818,9 +815,9 @@ export function validateScenarioServices(logger, state) {
|
|
|
818
815
|
const funcMeta = state.functions.meta[meta.pikkuFuncId];
|
|
819
816
|
if (!funcMeta?.services)
|
|
820
817
|
continue;
|
|
821
|
-
const disallowed = funcMeta.services.services.filter((svc) => svc !==
|
|
818
|
+
const disallowed = funcMeta.services.services.filter((svc) => svc !== "logger" && svc !== "config");
|
|
822
819
|
if (disallowed.length > 0) {
|
|
823
|
-
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(", ")}. ` +
|
|
824
821
|
`Scenarios may only use 'logger'/'config' — drive everything else through ` +
|
|
825
822
|
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
826
823
|
`runs against the target environment.`);
|
|
@@ -840,8 +837,8 @@ export const proseOpensWithActor = (prose, actor) => {
|
|
|
840
837
|
if (!actor) {
|
|
841
838
|
return false;
|
|
842
839
|
}
|
|
843
|
-
const escaped = actor.replace(/[.*+?^${}()|[\]\\]/g,
|
|
844
|
-
return new RegExp(`^["'“‘\\s]*${escaped}(?:['’]s)?\\b`,
|
|
840
|
+
const escaped = actor.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
841
|
+
return new RegExp(`^["'“‘\\s]*${escaped}(?:['’]s)?\\b`, "i").test(prose);
|
|
845
842
|
};
|
|
846
843
|
/**
|
|
847
844
|
* The prose that will actually render for one step, in the runtime's own
|
|
@@ -857,11 +854,7 @@ const renderableProse = (step, stepMeta) => {
|
|
|
857
854
|
if (description) {
|
|
858
855
|
return [step.stepName, description];
|
|
859
856
|
}
|
|
860
|
-
return [
|
|
861
|
-
step.stepName,
|
|
862
|
-
stepMeta?.scenarioStepTemplate,
|
|
863
|
-
stepMeta?.description,
|
|
864
|
-
].filter((prose) => typeof prose === 'string');
|
|
857
|
+
return [step.stepName, stepMeta?.scenarioStepTemplate, stepMeta?.description].filter((prose) => typeof prose === "string");
|
|
865
858
|
};
|
|
866
859
|
/**
|
|
867
860
|
* A step's prose must not open by naming the actor it already runs as (PKU681).
|
|
@@ -909,7 +902,7 @@ export function validateScenarioSteps(logger, state) {
|
|
|
909
902
|
const phasesSeen = new Set();
|
|
910
903
|
const visit = (workflowName, steps) => {
|
|
911
904
|
for (const step of steps) {
|
|
912
|
-
if (step.type ===
|
|
905
|
+
if (step.type === "scenarioStep") {
|
|
913
906
|
phasesSeen.add(step.phase);
|
|
914
907
|
if (step.stepFunc === DYNAMIC_SCENARIO_STEP_TARGET) {
|
|
915
908
|
logger.critical(ErrorCode.SCENARIO_STEP_TARGET_NOT_STATIC, `Scenario '${workflowName}' calls step '${step.stepName}' with a target that isn't a string literal. ` +
|
|
@@ -921,21 +914,21 @@ export function validateScenarioSteps(logger, state) {
|
|
|
921
914
|
const stepMeta = state.functions.meta[step.stepFunc];
|
|
922
915
|
validateStepProse(logger, workflowName, step, stepMeta);
|
|
923
916
|
if (stepMeta?.scenarioStepRequiresActor && !step.actor) {
|
|
924
|
-
const why = stepMeta.scenarioStepSurfaces?.includes(
|
|
925
|
-
?
|
|
926
|
-
:
|
|
917
|
+
const why = stepMeta.scenarioStepSurfaces?.includes("browser")
|
|
918
|
+
? "declares a browser binding"
|
|
919
|
+
: "is driven by a persona";
|
|
927
920
|
logger.critical(ErrorCode.SCENARIO_STEP_NEEDS_ACTOR, `Scenario '${workflowName}' calls step '${step.stepFunc}', which ${why}, without an actor. ` +
|
|
928
921
|
`Pass { actor: actors.<name> } so it runs as that persona.`);
|
|
929
922
|
}
|
|
930
923
|
continue;
|
|
931
924
|
}
|
|
932
|
-
if (step.type ===
|
|
925
|
+
if (step.type === "branch") {
|
|
933
926
|
for (const branch of step.branches)
|
|
934
927
|
visit(workflowName, branch.steps);
|
|
935
928
|
if (step.elseSteps)
|
|
936
929
|
visit(workflowName, step.elseSteps);
|
|
937
930
|
}
|
|
938
|
-
else if (step.type ===
|
|
931
|
+
else if (step.type === "switch") {
|
|
939
932
|
for (const c of step.cases ?? []) {
|
|
940
933
|
if (c.steps)
|
|
941
934
|
visit(workflowName, c.steps);
|
|
@@ -943,10 +936,10 @@ export function validateScenarioSteps(logger, state) {
|
|
|
943
936
|
if (step.defaultSteps)
|
|
944
937
|
visit(workflowName, step.defaultSteps);
|
|
945
938
|
}
|
|
946
|
-
else if (step.type ===
|
|
939
|
+
else if (step.type === "fanout" && step.body) {
|
|
947
940
|
visit(workflowName, step.body);
|
|
948
941
|
}
|
|
949
|
-
else if (step.type ===
|
|
942
|
+
else if (step.type === "parallel" && step.children) {
|
|
950
943
|
visit(workflowName, step.children);
|
|
951
944
|
}
|
|
952
945
|
}
|
|
@@ -961,8 +954,8 @@ export function validateScenarioSteps(logger, state) {
|
|
|
961
954
|
// `asserts` is the escape for a flow whose witness is an expectation helper
|
|
962
955
|
// instead: those are inline steps and leave no phase behind, but a recorded
|
|
963
956
|
// service call or a refusal is an assertion all the same.
|
|
964
|
-
if (phasesSeen.size > 0 && !phasesSeen.has(
|
|
965
|
-
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. ` +
|
|
966
959
|
`Add a scenario.then(...) naming what the actor should now see — a flow with no 'then' ` +
|
|
967
960
|
`only proves nothing threw, and contributes nothing to witness coverage.`);
|
|
968
961
|
}
|
|
@@ -1023,17 +1016,17 @@ export function validateScenarioFeatures(logger, state) {
|
|
|
1023
1016
|
.map((feature) => feature.exportedName);
|
|
1024
1017
|
if (unreadable.length > 0) {
|
|
1025
1018
|
logger.diagnostic({
|
|
1026
|
-
severity:
|
|
1019
|
+
severity: "warn",
|
|
1027
1020
|
code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
|
|
1028
|
-
message: `${unowned.map((name) => `'${name}'`).join(
|
|
1029
|
-
`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. ` +
|
|
1030
1023
|
`List those scenarios literally and this becomes a definite answer either way.`,
|
|
1031
1024
|
});
|
|
1032
1025
|
return;
|
|
1033
1026
|
}
|
|
1034
1027
|
for (const name of unowned) {
|
|
1035
1028
|
logger.diagnostic({
|
|
1036
|
-
severity:
|
|
1029
|
+
severity: "error",
|
|
1037
1030
|
code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
|
|
1038
1031
|
message: `Scenario '${name}' is listed by no pikkuFeature, so nothing downstream can reach it — ` +
|
|
1039
1032
|
`a run stamps no feature onto its results, the console groups it under none, and a guide cannot cite it. ` +
|
|
@@ -1048,16 +1041,16 @@ export function validateScenarioFeatures(logger, state) {
|
|
|
1048
1041
|
* early with an actionable message pointing at `scaffold: { graph: true }`.
|
|
1049
1042
|
*/
|
|
1050
1043
|
export function validateWorkflowGraphAddons(logger, state) {
|
|
1051
|
-
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");
|
|
1052
1045
|
if (addonGraphWired) {
|
|
1053
1046
|
return;
|
|
1054
1047
|
}
|
|
1055
1048
|
for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
1056
1049
|
for (const node of Object.values(graph.nodes)) {
|
|
1057
|
-
if (!(
|
|
1050
|
+
if (!("rpcName" in node) || typeof node.rpcName !== "string") {
|
|
1058
1051
|
continue;
|
|
1059
1052
|
}
|
|
1060
|
-
if (!node.rpcName.startsWith(
|
|
1053
|
+
if (!node.rpcName.startsWith("graph:")) {
|
|
1061
1054
|
continue;
|
|
1062
1055
|
}
|
|
1063
1056
|
if (state.functions.meta[node.rpcName]) {
|
|
@@ -1107,7 +1100,7 @@ export function validateSchemaWiringSeparation(logger, state) {
|
|
|
1107
1100
|
const schemas = Array.from(state.schemaLookup.entries())
|
|
1108
1101
|
.filter(([, ref]) => ref.sourceFile === file)
|
|
1109
1102
|
.map(([name]) => name);
|
|
1110
|
-
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.). ` +
|
|
1111
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. ` +
|
|
1112
1105
|
`Move the route/wiring definitions to a dedicated wiring file.`);
|
|
1113
1106
|
}
|
|
@@ -1122,7 +1115,7 @@ export function validateSchemaWiringSeparation(logger, state) {
|
|
|
1122
1115
|
* not.
|
|
1123
1116
|
*/
|
|
1124
1117
|
export function validateNoSecretAliasServices(logger, checker, state) {
|
|
1125
|
-
if (!(
|
|
1118
|
+
if (!("typesLookup" in state)) {
|
|
1126
1119
|
return;
|
|
1127
1120
|
}
|
|
1128
1121
|
const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
|
|
@@ -1138,14 +1131,14 @@ export function validateNoSecretAliasServices(logger, checker, state) {
|
|
|
1138
1131
|
if (used.length === 0) {
|
|
1139
1132
|
return;
|
|
1140
1133
|
}
|
|
1141
|
-
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. ` +
|
|
1142
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.`);
|
|
1143
1136
|
};
|
|
1144
1137
|
for (const [id, meta] of Object.entries(state.functions.meta)) {
|
|
1145
|
-
report(
|
|
1138
|
+
report("Function", id, meta.services?.services ?? []);
|
|
1146
1139
|
}
|
|
1147
1140
|
for (const [id, def] of Object.entries(state.permissions.definitions)) {
|
|
1148
|
-
report(
|
|
1141
|
+
report("Permission", id, def.services?.services ?? []);
|
|
1149
1142
|
}
|
|
1150
1143
|
}
|
|
1151
1144
|
/**
|
|
@@ -1174,22 +1167,22 @@ export function validateSecretUsage(logger, state) {
|
|
|
1174
1167
|
// auth-secrets.gen.ts declares it, but that file is written after the first
|
|
1175
1168
|
// inspection of a fresh project, so its defineSecret is not seen yet.
|
|
1176
1169
|
if (state.auth?.definition)
|
|
1177
|
-
declared.add(
|
|
1170
|
+
declared.add("BETTER_AUTH_SECRET");
|
|
1178
1171
|
for (const [file, usage] of state.secrets.usage) {
|
|
1179
1172
|
const relativeFile = relative(state.rootDir, file);
|
|
1180
1173
|
const undeclared = usage.keys.filter((key) => !declared.has(key));
|
|
1181
1174
|
if (undeclared.length > 0) {
|
|
1182
1175
|
logger.diagnostic({
|
|
1183
|
-
severity:
|
|
1176
|
+
severity: "warn",
|
|
1184
1177
|
code: ErrorCode.SECRET_NOT_DECLARED,
|
|
1185
|
-
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.`,
|
|
1186
1179
|
});
|
|
1187
1180
|
}
|
|
1188
1181
|
if (usage.dynamic.length > 0) {
|
|
1189
1182
|
logger.diagnostic({
|
|
1190
|
-
severity:
|
|
1183
|
+
severity: "warn",
|
|
1191
1184
|
code: ErrorCode.SECRET_KEY_NOT_STATIC,
|
|
1192
|
-
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.`,
|
|
1193
1186
|
});
|
|
1194
1187
|
}
|
|
1195
1188
|
}
|
|
@@ -1202,7 +1195,10 @@ export function computeDiagnostics(state) {
|
|
|
1202
1195
|
// bridges that reference addon functions (id `http:<method>:<route>`, no
|
|
1203
1196
|
// source file). The user can't edit any of these, so a destructure lint
|
|
1204
1197
|
// meant to nudge them about their own code must not fail the build over them.
|
|
1205
|
-
if (!meta.sourceFile ||
|
|
1198
|
+
if (!meta.sourceFile ||
|
|
1199
|
+
meta.sourceFile.endsWith(".gen.ts") ||
|
|
1200
|
+
meta.functionType === "inline" ||
|
|
1201
|
+
meta.functionType === "helper") {
|
|
1206
1202
|
continue;
|
|
1207
1203
|
}
|
|
1208
1204
|
if (meta.services && !meta.services.optimized) {
|
|
@@ -1282,14 +1278,14 @@ export function validateScopeReferences(logger, state) {
|
|
|
1282
1278
|
continue;
|
|
1283
1279
|
for (const scope of meta.scopes) {
|
|
1284
1280
|
// A trailing wildcard grants a subtree; the node it hangs off must exist.
|
|
1285
|
-
const declaredForm = scope.endsWith(
|
|
1286
|
-
if (scope ===
|
|
1281
|
+
const declaredForm = scope.endsWith(":*") ? scope.slice(0, -2) : scope;
|
|
1282
|
+
if (scope === "*") {
|
|
1287
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.`);
|
|
1288
1284
|
continue;
|
|
1289
1285
|
}
|
|
1290
1286
|
if (!declared.has(declaredForm)) {
|
|
1291
1287
|
const available = Array.from(declared);
|
|
1292
|
-
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"}`);
|
|
1293
1289
|
}
|
|
1294
1290
|
}
|
|
1295
1291
|
}
|
|
@@ -1316,12 +1312,12 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1316
1312
|
return;
|
|
1317
1313
|
const { wireAddonDeclarations } = state.rpc;
|
|
1318
1314
|
for (const [agentKey, agent] of agents) {
|
|
1319
|
-
const where = agent.sourceFile ? ` (${agent.sourceFile})` :
|
|
1315
|
+
const where = agent.sourceFile ? ` (${agent.sourceFile})` : "";
|
|
1320
1316
|
for (const tool of agent.tools ?? []) {
|
|
1321
1317
|
// A workflow reference is resolved against the workflow map, not here.
|
|
1322
|
-
if (tool.startsWith(
|
|
1318
|
+
if (tool.startsWith("workflow:"))
|
|
1323
1319
|
continue;
|
|
1324
|
-
const separator = tool.indexOf(
|
|
1320
|
+
const separator = tool.indexOf(":");
|
|
1325
1321
|
let meta;
|
|
1326
1322
|
if (separator === -1) {
|
|
1327
1323
|
const funcId = state.rpc.internalMeta[tool] ?? tool;
|
|
@@ -1338,7 +1334,7 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1338
1334
|
if (!addon) {
|
|
1339
1335
|
const known = Array.from(wireAddonDeclarations?.keys() ?? []);
|
|
1340
1336
|
logger.critical(ErrorCode.AGENT_TOOL_UNKNOWN_NAMESPACE, `AI agent '${agentKey}'${where} references tool '${tool}', but no addon is wired under the namespace '${namespace}'. ` +
|
|
1341
|
-
`Wired namespaces: ${known.join(
|
|
1337
|
+
`Wired namespaces: ${known.join(", ") || "none"}.`);
|
|
1342
1338
|
continue;
|
|
1343
1339
|
}
|
|
1344
1340
|
// An addon that has not been built yet contributed no metadata, which
|
|
@@ -1355,7 +1351,7 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1355
1351
|
if (options.strictMeta && !meta.description) {
|
|
1356
1352
|
logger.critical(ErrorCode.AGENT_TOOL_MISSING_DESCRIPTION, `AI agent '${agentKey}'${where} uses tool '${tool}', which has no description. ` +
|
|
1357
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. ` +
|
|
1358
|
-
`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)" : ""}.`);
|
|
1359
1355
|
}
|
|
1360
1356
|
}
|
|
1361
1357
|
}
|
|
@@ -1370,12 +1366,12 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1370
1366
|
export function validateAgentScorerReferences(logger, state) {
|
|
1371
1367
|
const known = Object.keys(state.scorers.scorersMeta);
|
|
1372
1368
|
for (const [agentKey, agent] of Object.entries(state.agents.agentsMeta)) {
|
|
1373
|
-
const where = agent.sourceFile ? ` (${agent.sourceFile})` :
|
|
1369
|
+
const where = agent.sourceFile ? ` (${agent.sourceFile})` : "";
|
|
1374
1370
|
for (const scorer of agent.scorers ?? []) {
|
|
1375
1371
|
if (known.includes(scorer))
|
|
1376
1372
|
continue;
|
|
1377
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. ` +
|
|
1378
|
-
`Declared scorers: ${known.join(
|
|
1374
|
+
`Declared scorers: ${known.join(", ") || "none"}.`);
|
|
1379
1375
|
}
|
|
1380
1376
|
}
|
|
1381
1377
|
}
|
|
@@ -1398,14 +1394,14 @@ export function validateSystemRoleScopes(logger, state) {
|
|
|
1398
1394
|
const declared = new Set(flattenScopeDefinitions(state.scopes.definitions).map((s) => s.id));
|
|
1399
1395
|
for (const role of state.systemRoles.definitions) {
|
|
1400
1396
|
for (const scope of role.scopes) {
|
|
1401
|
-
if (scope ===
|
|
1397
|
+
if (scope === "*") {
|
|
1402
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.`);
|
|
1403
1399
|
continue;
|
|
1404
1400
|
}
|
|
1405
|
-
const declaredForm = scope.endsWith(
|
|
1401
|
+
const declaredForm = scope.endsWith(":*") ? scope.slice(0, -2) : scope;
|
|
1406
1402
|
if (!declared.has(declaredForm)) {
|
|
1407
1403
|
const available = Array.from(declared);
|
|
1408
|
-
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"}`);
|
|
1409
1405
|
}
|
|
1410
1406
|
}
|
|
1411
1407
|
}
|
|
@@ -1432,7 +1428,7 @@ export function validatePersonaRoles(logger, state) {
|
|
|
1432
1428
|
const available = Array.from(declared).sort();
|
|
1433
1429
|
logger.critical(ErrorCode.INVALID_VALUE, `Persona '${persona.id}' holds role '${role}', which is not declared with defineSystemRole. ` +
|
|
1434
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. ` +
|
|
1435
|
-
`Declared roles: ${available.join(
|
|
1431
|
+
`Declared roles: ${available.join(", ") || "none"}`);
|
|
1436
1432
|
}
|
|
1437
1433
|
}
|
|
1438
1434
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.95",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"zod": "^4.4.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@pikku/core": "^0.12.
|
|
57
|
+
"@pikku/core": "^0.12.128"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@pikku/core": "^0.12.
|
|
60
|
+
"@pikku/core": "^0.12.128",
|
|
61
61
|
"@types/node": "^24.13.3"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|