@pikku/inspector 0.12.42 → 0.12.44

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.
Files changed (126) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/dist/add/add-addon-bans.js +1 -0
  3. package/dist/add/add-ai-agent.d.ts +3 -1
  4. package/dist/add/add-ai-agent.js +82 -0
  5. package/dist/add/add-credential.js +5 -1
  6. package/dist/add/add-functions.js +10 -9
  7. package/dist/add/add-gateway.js +3 -0
  8. package/dist/add/add-http-route.js +2 -2
  9. package/dist/add/add-keyed-wiring.js +2 -0
  10. package/dist/add/add-mcp-prompt.js +0 -1
  11. package/dist/add/add-mcp-resource.js +0 -1
  12. package/dist/add/add-permission.d.ts +1 -1
  13. package/dist/add/add-permission.js +3 -177
  14. package/dist/add/add-queue-worker.js +3 -0
  15. package/dist/add/add-schedule.js +3 -0
  16. package/dist/add/add-scope.d.ts +2 -0
  17. package/dist/add/add-scope.js +146 -0
  18. package/dist/add/add-trigger.js +3 -0
  19. package/dist/add/add-wire-remote-addon.d.ts +10 -0
  20. package/dist/add/add-wire-remote-addon.js +66 -0
  21. package/dist/add/add-workflow-graph.js +32 -3
  22. package/dist/add/add-workflow.js +9 -4
  23. package/dist/error-codes.d.ts +3 -0
  24. package/dist/error-codes.js +4 -0
  25. package/dist/inspector.js +11 -5
  26. package/dist/types.d.ts +19 -13
  27. package/dist/utils/custom-types-generator.js +2 -1
  28. package/dist/utils/ensure-function-metadata.d.ts +15 -0
  29. package/dist/utils/ensure-function-metadata.js +24 -0
  30. package/dist/utils/filter-inspector-state.js +9 -0
  31. package/dist/utils/get-property-value.d.ts +7 -2
  32. package/dist/utils/get-property-value.js +48 -1
  33. package/dist/utils/load-addon-functions-meta.js +81 -6
  34. package/dist/utils/permissions.d.ts +1 -21
  35. package/dist/utils/permissions.js +17 -108
  36. package/dist/utils/post-process.d.ts +30 -1
  37. package/dist/utils/post-process.js +150 -72
  38. package/dist/utils/schema-generator.js +19 -4
  39. package/dist/utils/serialize-inspector-state.d.ts +13 -8
  40. package/dist/utils/serialize-inspector-state.js +12 -6
  41. package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
  42. package/dist/utils/serialize-permissions-groups-meta.js +0 -26
  43. package/dist/utils/workflow/derive-workflow-plan.js +9 -3
  44. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +373 -91
  45. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +274 -100
  46. package/dist/utils/workflow/dsl/patterns.d.ts +4 -0
  47. package/dist/utils/workflow/dsl/patterns.js +11 -0
  48. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +76 -5
  49. package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
  50. package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
  51. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
  52. package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
  53. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -4
  54. package/dist/visit.js +4 -0
  55. package/package.json +3 -3
  56. package/run-tests.sh +1 -1
  57. package/src/add/add-addon-bans.ts +1 -0
  58. package/src/add/add-ai-agent.test.ts +87 -0
  59. package/src/add/add-ai-agent.ts +122 -0
  60. package/src/add/add-credential.ts +8 -0
  61. package/src/add/add-functions.ts +10 -12
  62. package/src/add/add-gateway.ts +11 -0
  63. package/src/add/add-http-route.ts +2 -8
  64. package/src/add/add-keyed-wiring.ts +2 -0
  65. package/src/add/add-mcp-prompt.ts +0 -1
  66. package/src/add/add-mcp-resource.ts +0 -1
  67. package/src/add/add-permission.ts +4 -242
  68. package/src/add/add-queue-worker.ts +11 -0
  69. package/src/add/add-schedule.ts +11 -0
  70. package/src/add/add-scope.test.ts +346 -0
  71. package/src/add/add-scope.ts +225 -0
  72. package/src/add/add-trigger.ts +11 -0
  73. package/src/add/add-wire-remote-addon.ts +77 -0
  74. package/src/add/add-workflow-edge-cases.test.ts +201 -0
  75. package/src/add/add-workflow-fanout.test.ts +60 -0
  76. package/src/add/add-workflow-graph-input.test.ts +94 -0
  77. package/src/add/add-workflow-graph-notes.test.ts +74 -0
  78. package/src/add/add-workflow-graph.ts +35 -3
  79. package/src/add/add-workflow-on-error.test.ts +118 -0
  80. package/src/add/add-workflow-return-switch.test.ts +121 -0
  81. package/src/add/add-workflow-silent-drops.test.ts +163 -0
  82. package/src/add/add-workflow-sleep-expression.test.ts +97 -0
  83. package/src/add/add-workflow.ts +9 -4
  84. package/src/add/config-value-metadata.test.ts +169 -0
  85. package/src/add/inline-wiring-function.test.ts +104 -0
  86. package/src/add/validate-workflow-graph-addons.test.ts +102 -0
  87. package/src/error-codes.ts +5 -0
  88. package/src/inspector.ts +14 -4
  89. package/src/types.ts +16 -17
  90. package/src/utils/custom-types-generator.test.ts +26 -1
  91. package/src/utils/custom-types-generator.ts +2 -1
  92. package/src/utils/ensure-function-metadata.ts +42 -0
  93. package/src/utils/filter-inspector-state.test.ts +0 -2
  94. package/src/utils/filter-inspector-state.ts +9 -0
  95. package/src/utils/get-property-value.test.ts +141 -0
  96. package/src/utils/get-property-value.ts +62 -1
  97. package/src/utils/load-addon-functions-meta.test.ts +157 -0
  98. package/src/utils/load-addon-functions-meta.ts +94 -6
  99. package/src/utils/load-addon-scopes.test.ts +138 -0
  100. package/src/utils/permissions.test.ts +7 -232
  101. package/src/utils/permissions.ts +20 -160
  102. package/src/utils/post-process.test.ts +269 -4
  103. package/src/utils/post-process.ts +216 -95
  104. package/src/utils/schema-generator.test.ts +164 -0
  105. package/src/utils/schema-generator.ts +26 -3
  106. package/src/utils/serialize-inspector-state.ts +22 -25
  107. package/src/utils/serialize-permissions-groups-meta.ts +0 -28
  108. package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
  109. package/src/utils/workflow/derive-workflow-plan.ts +11 -4
  110. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +480 -119
  111. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +71 -0
  112. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +99 -0
  113. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +161 -0
  114. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +121 -0
  115. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +116 -0
  116. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +145 -0
  117. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +311 -105
  118. package/src/utils/workflow/dsl/patterns.ts +18 -0
  119. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +82 -10
  120. package/src/utils/workflow/graph/fanout-node-identity.test.ts +105 -0
  121. package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
  122. package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
  123. package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
  124. package/src/utils/workflow/graph/workflow-graph.types.ts +11 -2
  125. package/src/visit.ts +4 -0
  126. package/tsconfig.tsbuildinfo +1 -1
@@ -19,7 +19,7 @@ export declare function stampAuthHandlerServices(state: InspectorState | Omit<In
19
19
  * Only extracts type:'wire' variants (individual middleware/permissions).
20
20
  * Skips type:'http' and type:'tag' (reference groups, not individuals).
21
21
  */
22
- export declare function extractWireNames(list?: MiddlewareMetadata[] | PermissionMetadata[]): string[];
22
+ export declare function extractWireNames(list?: Array<MiddlewareMetadata | PermissionMetadata>): string[];
23
23
  /**
24
24
  * Aggregates all required services from wired functions, middleware, and permissions.
25
25
  * Must be called after AST traversal completes.
@@ -31,11 +31,32 @@ export declare function aggregateRequiredServices(state: InspectorState | Omit<I
31
31
  export declare function validateSecretOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
32
32
  export declare function validateCredentialOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
33
33
  export declare function validateVariableOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
34
+ /**
35
+ * A `wireRemoteAddon` package ships types only — its handlers run on the host —
36
+ * so it MUST be a devDependency, not a production dependency (a prod dep would
37
+ * drag in the runtime deps remote consumption exists to avoid). This is the
38
+ * mirror image of `wireAddon`, which requires a production dependency.
39
+ */
40
+ export declare function validateRemoteAddonDependencies(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
41
+ /**
42
+ * The auth a `wireRemoteAddon` consumer binds must reference a slot the consumer
43
+ * actually declares: a `credentialId` must be a wired credential, a `secretId`
44
+ * a wired secret. A custom `resolve()` and a public (omitted) surface are not
45
+ * statically checkable and are left to runtime.
46
+ */
47
+ export declare function validateRemoteAddonAuth(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
34
48
  export declare function computeResolvedIOTypes(state: InspectorState): void;
35
49
  export declare function computeMiddlewareGroupsMeta(state: InspectorState): void;
36
50
  export declare function computePermissionsGroupsMeta(state: InspectorState): void;
37
51
  export declare function computeRequiredSchemas(state: InspectorState, options: InspectorOptions): void;
38
52
  export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
53
+ /**
54
+ * A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
55
+ * `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
56
+ * registers and codegen fails deep in type-checking with an opaque error. Fail
57
+ * early with an actionable message pointing at `scaffold: { graph: true }`.
58
+ */
59
+ export declare function validateWorkflowGraphAddons(logger: InspectorLogger, state: InspectorState): void;
39
60
  /**
40
61
  * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
41
62
  * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
@@ -46,3 +67,11 @@ export declare function validateAgentModels(logger: InspectorLogger, state: Insp
46
67
  */
47
68
  export declare function validateSchemaWiringSeparation(logger: InspectorLogger, state: InspectorState): void;
48
69
  export declare function computeDiagnostics(state: InspectorState): void;
70
+ /**
71
+ * Validates that every scope referenced by a function is declared via
72
+ * `wireScope`. Runs after all visitors, so declaration order does not matter.
73
+ *
74
+ * A `*` suffix is a wildcard requirement: `admin:*` requires the `admin` scope
75
+ * to be declared, and grants its whole subtree.
76
+ */
77
+ export declare function validateScopeReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
@@ -1,6 +1,9 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import { extractTypeKeys } from './type-utils.js';
2
4
  import { ErrorCode } from '../error-codes.js';
3
5
  import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js';
6
+ import { flattenScopeDefinitions } from '@pikku/core/scope';
4
7
  /**
5
8
  * Stamp the inspected authorize/callbacks service set onto the generated auth
6
9
  * handler's function meta.
@@ -39,28 +42,22 @@ export function extractWireNames(list) {
39
42
  .map((item) => item.name);
40
43
  }
41
44
  /**
42
- * Helper to expand middleware/permission groups into individual names
43
- * and add their services to the aggregation.
44
- * This handles tag-based and HTTP-pattern-based middleware/permission groups.
45
+ * Helper to expand middleware groups into individual names and add their
46
+ * services to the aggregation. Handles tag-based and HTTP-pattern-based
47
+ * middleware groups. Permissions are function-scoped only and carry no groups.
45
48
  */
46
- function expandAndAddGroupServices(list, state, addServices, isMiddleware) {
49
+ function expandAndAddGroupServices(list, state, addServices) {
47
50
  if (!list)
48
51
  return;
49
52
  for (const item of list) {
50
53
  if (item.type === 'tag') {
51
- // Expand tag-based group
52
- const groupMeta = isMiddleware
53
- ? state.middleware.tagMiddleware.get(item.tag)
54
- : state.permissions.tagPermissions.get(item.tag);
54
+ const groupMeta = state.middleware.tagMiddleware.get(item.tag);
55
55
  if (groupMeta?.services) {
56
56
  addServices(groupMeta.services);
57
57
  }
58
58
  }
59
59
  else if (item.type === 'http' && 'route' in item) {
60
- // Expand HTTP-pattern-based group
61
- const groupMeta = isMiddleware
62
- ? state.http.routeMiddleware.get(item.route)
63
- : state.http.routePermissions.get(item.route);
60
+ const groupMeta = state.http.routeMiddleware.get(item.route);
64
61
  if (groupMeta?.services) {
65
62
  addServices(groupMeta.services);
66
63
  }
@@ -149,34 +146,27 @@ export function aggregateRequiredServices(state) {
149
146
  'options',
150
147
  ]) {
151
148
  for (const routeMeta of Object.values(state.http.meta[method])) {
152
- expandAndAddGroupServices(routeMeta.middleware, state, addServices, true);
153
- expandAndAddGroupServices(routeMeta.permissions, state, addServices, false);
149
+ expandAndAddGroupServices(routeMeta.middleware, state, addServices);
154
150
  }
155
151
  }
156
152
  // Also check other wiring types (channels, queues, schedulers, MCP)
157
153
  for (const channelMeta of Object.values(state.channels.meta)) {
158
- expandAndAddGroupServices(channelMeta.middleware, state, addServices, true);
159
- expandAndAddGroupServices(channelMeta.permissions, state, addServices, false);
154
+ expandAndAddGroupServices(channelMeta.middleware, state, addServices);
160
155
  }
161
156
  for (const queueMeta of Object.values(state.queueWorkers.meta)) {
162
- expandAndAddGroupServices(queueMeta.middleware, state, addServices, true);
163
- // expandAndAddGroupServices(queueMeta.permissions, state, addServices, false)
157
+ expandAndAddGroupServices(queueMeta.middleware, state, addServices);
164
158
  }
165
159
  for (const scheduleMeta of Object.values(state.scheduledTasks.meta)) {
166
- expandAndAddGroupServices(scheduleMeta.middleware, state, addServices, true);
167
- // expandAndAddGroupServices(scheduleMeta.permissions, state, addServices, false)
160
+ expandAndAddGroupServices(scheduleMeta.middleware, state, addServices);
168
161
  }
169
162
  for (const toolMeta of Object.values(state.mcpEndpoints.toolsMeta)) {
170
- expandAndAddGroupServices(toolMeta.middleware, state, addServices, true);
171
- expandAndAddGroupServices(toolMeta.permissions, state, addServices, false);
163
+ expandAndAddGroupServices(toolMeta.middleware, state, addServices);
172
164
  }
173
165
  for (const promptMeta of Object.values(state.mcpEndpoints.promptsMeta)) {
174
- expandAndAddGroupServices(promptMeta.middleware, state, addServices, true);
175
- expandAndAddGroupServices(promptMeta.permissions, state, addServices, false);
166
+ expandAndAddGroupServices(promptMeta.middleware, state, addServices);
176
167
  }
177
168
  for (const resourceMeta of Object.values(state.mcpEndpoints.resourcesMeta)) {
178
- expandAndAddGroupServices(resourceMeta.middleware, state, addServices, true);
179
- expandAndAddGroupServices(resourceMeta.permissions, state, addServices, false);
169
+ expandAndAddGroupServices(resourceMeta.middleware, state, addServices);
180
170
  }
181
171
  // 5. Services from session service factories
182
172
  for (const singletonServices of state.wireServicesMeta.values()) {
@@ -299,14 +289,17 @@ export function validateSecretOverrides(logger, state) {
299
289
  const { wireAddonDeclarations } = state.rpc;
300
290
  if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
301
291
  return;
302
- const secretNames = new Set(state.secrets.definitions.map((d) => d.name));
292
+ // secretOverrides key on (and resolve to) SECRET IDs — the string the addon
293
+ // passes to getSecret — so validate against secretId, falling back to name for
294
+ // older meta without a secretId field.
295
+ const secretIds = new Set(state.secrets.definitions.map((d) => d.secretId ?? d.name));
303
296
  for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
304
297
  if (!addonDecl.secretOverrides)
305
298
  continue;
306
- for (const secretKey of Object.keys(addonDecl.secretOverrides)) {
307
- if (!secretNames.has(secretKey)) {
308
- const availableSecrets = Array.from(secretNames);
309
- logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${secretKey}' in addon '${namespace}' (${addonDecl.package}) does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`);
299
+ for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides)) {
300
+ if (!secretIds.has(resolvedName)) {
301
+ const availableSecrets = Array.from(secretIds);
302
+ 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'}`);
310
303
  }
311
304
  }
312
305
  }
@@ -319,10 +312,10 @@ export function validateCredentialOverrides(logger, state) {
319
312
  for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
320
313
  if (!addonDecl.credentialOverrides)
321
314
  continue;
322
- for (const credentialKey of Object.keys(addonDecl.credentialOverrides)) {
323
- if (!credentialNames.has(credentialKey)) {
315
+ for (const [logicalName, resolvedName] of Object.entries(addonDecl.credentialOverrides)) {
316
+ if (!credentialNames.has(resolvedName)) {
324
317
  const availableCredentials = Array.from(credentialNames);
325
- logger.critical(ErrorCode.INVALID_VALUE, `Credential override '${credentialKey}' in addon '${namespace}' (${addonDecl.package}) does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
318
+ logger.critical(ErrorCode.INVALID_VALUE, `Credential override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
326
319
  }
327
320
  }
328
321
  }
@@ -331,18 +324,82 @@ export function validateVariableOverrides(logger, state) {
331
324
  const { wireAddonDeclarations } = state.rpc;
332
325
  if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
333
326
  return;
334
- const variableNames = new Set(state.variables.definitions.map((d) => d.name));
327
+ // variableOverrides key on (and resolve to) VARIABLE IDs, so validate against
328
+ // variableId, falling back to name for older meta without a variableId field.
329
+ const variableIds = new Set(state.variables.definitions.map((d) => d.variableId ?? d.name));
335
330
  for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
336
331
  if (!addonDecl.variableOverrides)
337
332
  continue;
338
- for (const variableKey of Object.keys(addonDecl.variableOverrides)) {
339
- if (!variableNames.has(variableKey)) {
340
- const availableVariables = Array.from(variableNames);
341
- logger.critical(ErrorCode.INVALID_VALUE, `Variable override '${variableKey}' in addon '${namespace}' (${addonDecl.package}) does not exist. Available variables: ${availableVariables.join(', ') || 'none'}`);
333
+ for (const [logicalName, resolvedName] of Object.entries(addonDecl.variableOverrides)) {
334
+ if (!variableIds.has(resolvedName)) {
335
+ const availableVariables = Array.from(variableIds);
336
+ 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'}`);
342
337
  }
343
338
  }
344
339
  }
345
340
  }
341
+ /**
342
+ * A `wireRemoteAddon` package ships types only — its handlers run on the host —
343
+ * so it MUST be a devDependency, not a production dependency (a prod dep would
344
+ * drag in the runtime deps remote consumption exists to avoid). This is the
345
+ * mirror image of `wireAddon`, which requires a production dependency.
346
+ */
347
+ export function validateRemoteAddonDependencies(logger, state) {
348
+ const { wireAddonDeclarations } = state.rpc;
349
+ if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
350
+ return;
351
+ const hasRemote = Array.from(wireAddonDeclarations.values()).some((d) => d.remote);
352
+ if (!hasRemote)
353
+ return;
354
+ const pkgJsonPath = join(state.rootDir, 'package.json');
355
+ if (!existsSync(pkgJsonPath))
356
+ return; // no manifest to check (e.g. some tests)
357
+ let pkgJson;
358
+ try {
359
+ pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
360
+ }
361
+ catch (e) {
362
+ logger.warn(`Could not read ${pkgJsonPath} to verify remote addon dependencies: ${e?.message ?? e}`);
363
+ return;
364
+ }
365
+ const prodDeps = pkgJson.dependencies ?? {};
366
+ const devDeps = pkgJson.devDependencies ?? {};
367
+ for (const [namespace, decl] of wireAddonDeclarations.entries()) {
368
+ if (!decl.remote)
369
+ continue;
370
+ if (decl.package in devDeps)
371
+ continue; // correct
372
+ if (decl.package in prodDeps) {
373
+ logger.critical(ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY, `Remote addon '${namespace}' ('${decl.package}') is a production dependency, but wireRemoteAddon consumes it for types only — its handlers run on the host. Move '${decl.package}' from "dependencies" to "devDependencies".`);
374
+ }
375
+ else {
376
+ logger.critical(ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY, `Remote addon '${namespace}' ('${decl.package}') is wired with wireRemoteAddon but is not in "devDependencies". Add '${decl.package}' to "devDependencies" (types only).`);
377
+ }
378
+ }
379
+ }
380
+ /**
381
+ * The auth a `wireRemoteAddon` consumer binds must reference a slot the consumer
382
+ * actually declares: a `credentialId` must be a wired credential, a `secretId`
383
+ * a wired secret. A custom `resolve()` and a public (omitted) surface are not
384
+ * statically checkable and are left to runtime.
385
+ */
386
+ export function validateRemoteAddonAuth(logger, state) {
387
+ const { wireAddonDeclarations } = state.rpc;
388
+ if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
389
+ return;
390
+ const credentialNames = new Set(state.credentials?.definitions.map((d) => d.name) ?? []);
391
+ const secretNames = new Set(state.secrets.definitions.map((d) => d.name));
392
+ for (const [namespace, decl] of wireAddonDeclarations.entries()) {
393
+ if (!decl.remote)
394
+ continue;
395
+ if (decl.authCredentialId && !credentialNames.has(decl.authCredentialId)) {
396
+ 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'}`);
397
+ }
398
+ if (decl.authSecretId && !secretNames.has(decl.authSecretId)) {
399
+ 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'}`);
400
+ }
401
+ }
402
+ }
346
403
  export function computeResolvedIOTypes(state) {
347
404
  const { functions } = state;
348
405
  for (const [pikkuFuncId, meta] of Object.entries(functions.meta)) {
@@ -424,34 +481,8 @@ export function computeMiddlewareGroupsMeta(state) {
424
481
  };
425
482
  }
426
483
  export function computePermissionsGroupsMeta(state) {
427
- const httpGroups = {};
428
- for (const [pattern, meta] of state.http.routePermissions.entries()) {
429
- httpGroups[pattern] = {
430
- exportName: meta.exportName,
431
- sourceFile: meta.sourceFile,
432
- position: meta.position,
433
- services: meta.services,
434
- count: meta.count,
435
- instanceIds: meta.instanceIds,
436
- isFactory: meta.isFactory,
437
- };
438
- }
439
- const tagGroups = {};
440
- for (const [tag, meta] of state.permissions.tagPermissions.entries()) {
441
- tagGroups[tag] = {
442
- exportName: meta.exportName,
443
- sourceFile: meta.sourceFile,
444
- position: meta.position,
445
- services: meta.services,
446
- count: meta.count,
447
- instanceIds: meta.instanceIds,
448
- isFactory: meta.isFactory,
449
- };
450
- }
451
484
  state.permissionsGroupsMeta = {
452
485
  definitions: state.permissions.definitions,
453
- httpGroups,
454
- tagGroups,
455
486
  };
456
487
  }
457
488
  const PRIMITIVE_TYPES = new Set([
@@ -508,6 +539,34 @@ export function validateAgentModels(logger, state) {
508
539
  }
509
540
  }
510
541
  }
542
+ /**
543
+ * A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
544
+ * `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
545
+ * registers and codegen fails deep in type-checking with an opaque error. Fail
546
+ * early with an actionable message pointing at `scaffold: { graph: true }`.
547
+ */
548
+ export function validateWorkflowGraphAddons(logger, state) {
549
+ const addonGraphWired = Array.from(state.rpc.wireAddonDeclarations.values()).some((decl) => decl.package === '@pikku/addon-graph');
550
+ if (addonGraphWired) {
551
+ return;
552
+ }
553
+ for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
554
+ for (const node of Object.values(graph.nodes)) {
555
+ if (!('rpcName' in node) || typeof node.rpcName !== 'string') {
556
+ continue;
557
+ }
558
+ if (!node.rpcName.startsWith('graph:')) {
559
+ continue;
560
+ }
561
+ if (state.functions.meta[node.rpcName]) {
562
+ continue;
563
+ }
564
+ logger.critical(ErrorCode.WORKFLOW_GRAPH_ADDON_NOT_WIRED, `Workflow graph '${name}' references '${node.rpcName}' but @pikku/addon-graph is not wired. ` +
565
+ `Enable "scaffold": { "graph": true } in pikku.config.json (and install @pikku/addon-graph), ` +
566
+ `or wire it manually with wireAddon({ name: 'graph', package: '@pikku/addon-graph' }).`);
567
+ }
568
+ }
569
+ }
511
570
  /**
512
571
  * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
513
572
  * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
@@ -528,13 +587,6 @@ export function validateSchemaWiringSeparation(logger, state) {
528
587
  for (const file of state.http.files) {
529
588
  wiringFiles.add(file);
530
589
  }
531
- // Permission wirings (addPermission calls)
532
- for (const meta of state.permissions.tagPermissions.values()) {
533
- wiringFiles.add(meta.sourceFile);
534
- }
535
- for (const meta of state.http.routePermissions.values()) {
536
- wiringFiles.add(meta.sourceFile);
537
- }
538
590
  // Middleware wirings (addHTTPMiddleware calls)
539
591
  for (const meta of state.http.routeMiddleware.values()) {
540
592
  wiringFiles.add(meta.sourceFile);
@@ -628,3 +680,29 @@ export function computeDiagnostics(state) {
628
680
  }
629
681
  state.diagnostics = diagnostics;
630
682
  }
683
+ /**
684
+ * Validates that every scope referenced by a function is declared via
685
+ * `wireScope`. Runs after all visitors, so declaration order does not matter.
686
+ *
687
+ * A `*` suffix is a wildcard requirement: `admin:*` requires the `admin` scope
688
+ * to be declared, and grants its whole subtree.
689
+ */
690
+ export function validateScopeReferences(logger, state) {
691
+ const declared = new Set(flattenScopeDefinitions(state.scopes.definitions).map((s) => s.id));
692
+ for (const [funcName, meta] of Object.entries(state.functions.meta)) {
693
+ if (!meta.scopes?.length)
694
+ continue;
695
+ for (const scope of meta.scopes) {
696
+ // A trailing wildcard grants a subtree; the node it hangs off must exist.
697
+ const declaredForm = scope.endsWith(':*') ? scope.slice(0, -2) : scope;
698
+ if (scope === '*') {
699
+ 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.`);
700
+ continue;
701
+ }
702
+ if (!declared.has(declaredForm)) {
703
+ const available = Array.from(declared);
704
+ logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires scope '${scope}' which is not declared. Declare it with wireScope. Available scopes: ${available.join(', ') || 'none'}`);
705
+ }
706
+ }
707
+ }
708
+ }
@@ -67,7 +67,9 @@ const inspectorVersion = (() => {
67
67
  try {
68
68
  const pkgUrl = new URL('../../package.json', import.meta.url);
69
69
  const pkg = JSON.parse(readFileSync(pkgUrl, 'utf-8'));
70
- return typeof pkg.version === 'string' ? pkg.version : `v${SCHEMA_CACHE_VERSION}`;
70
+ return typeof pkg.version === 'string'
71
+ ? pkg.version
72
+ : `v${SCHEMA_CACHE_VERSION}`;
71
73
  }
72
74
  catch {
73
75
  return `v${SCHEMA_CACHE_VERSION}`;
@@ -112,7 +114,7 @@ function writeDiskTSSchemas(logger, cacheDir, key, schemas) {
112
114
  logger.debug(`Failed to persist TS-schema cache: ${e.message}`);
113
115
  }
114
116
  }
115
- function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileContent) {
117
+ function createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, virtualFileContent) {
116
118
  const configPath = resolve(tsconfig);
117
119
  // Cache the parsed tsconfig — it doesn't change between runs
118
120
  if (!cachedParsedConfig || cachedTsconfigPath !== configPath) {
@@ -123,7 +125,11 @@ function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileCont
123
125
  cachedSchemaProgram = undefined;
124
126
  }
125
127
  const resolvedVirtualPath = resolve(virtualFilePath);
126
- const fileNames = [...cachedParsedConfig.fileNames, resolvedVirtualPath];
128
+ // The virtual file imports every type it references, so it is a complete root
129
+ // on its own and TypeScript pulls in exactly the transitive closure it needs.
130
+ // Rooting at cachedParsedConfig.fileNames instead loaded the whole project
131
+ // (2572 files vs 870 on a real tree) without widening what could be resolved.
132
+ const fileNames = [resolvedVirtualPath];
127
133
  const defaultHost = ts.createCompilerHost(cachedParsedConfig.options);
128
134
  const customHost = {
129
135
  ...defaultHost,
@@ -144,9 +150,11 @@ function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileCont
144
150
  return defaultHost.readFile(fileName);
145
151
  },
146
152
  };
153
+ const startProgram = performance.now();
147
154
  const program = ts.createProgram(fileNames, cachedParsedConfig.options, customHost, cachedSchemaProgram // reuse previous program for incremental compilation
148
155
  );
149
156
  cachedSchemaProgram = program;
157
+ logger.debug(`Created schema program in ${(performance.now() - startProgram).toFixed(0)}ms (${program.getSourceFiles().length} files)`);
150
158
  return program;
151
159
  }
152
160
  function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, functionMeta, httpWiringsMeta, additionalTypes, additionalProperties = false, generatedZodSchemas) {
@@ -192,7 +200,7 @@ function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, funct
192
200
  }
193
201
  logger.debug(`generateTSSchemas needed for ${uncoveredSchemas.length} types: ${uncoveredSchemas.slice(0, 3).join(', ')}${uncoveredSchemas.length > 3 ? '...' : ''}`);
194
202
  const virtualFilePath = join(dirname(resolve(tsconfig)), '__pikku_virtual_types__.ts');
195
- const program = createProgramWithVirtualFile(tsconfig, virtualFilePath, customTypesContent);
203
+ const program = createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, customTypesContent);
196
204
  const generator = createGenerator({
197
205
  tsProgram: program,
198
206
  skipTypeCheck: true,
@@ -408,6 +416,13 @@ export async function generateAllSchemas(logger, config, state) {
408
416
  }
409
417
  }
410
418
  const tsSchemas = generateTSSchemas(logger, config.tsconfig, customTypesContent, state.functions.typesMap, state.functions.meta, state.http.meta, config.schemasFromTypes, config.schema?.additionalProperties, zodSchemas);
419
+ // Release the program once the schemas are out of it. It spans the whole
420
+ // tsconfig (~2.5k files, ~600MB on a large tree) and was previously pinned at
421
+ // module scope for the life of the process, so it stacked on top of the main
422
+ // inspector program and pushed `pikku all` past a 2GB heap (#982). The result
423
+ // cache below is what the same-process fast path actually needs; the program
424
+ // only bought incremental reuse on a rebuild, at a cost that dwarfs it.
425
+ cachedSchemaProgram = undefined;
411
426
  cachedCustomTypesContent = customTypesContent;
412
427
  cachedTSSchemas = tsSchemas;
413
428
  if (config.cacheDir && cacheKey) {
@@ -127,10 +127,6 @@ export interface SerializableInspectorState {
127
127
  string,
128
128
  InspectorState['http']['routeMiddleware'] extends Map<string, infer V> ? V : never
129
129
  ]>;
130
- routePermissions: Array<[
131
- string,
132
- InspectorState['http']['routePermissions'] extends Map<string, infer V> ? V : never
133
- ]>;
134
130
  };
135
131
  channels: {
136
132
  files: string[];
@@ -177,6 +173,11 @@ export interface SerializableInspectorState {
177
173
  path: string;
178
174
  exportedName: string;
179
175
  }]>;
176
+ remoteMeta: InspectorState['rpc']['remoteMeta'];
177
+ remoteFiles: Array<[string, {
178
+ path: string;
179
+ exportedName: string;
180
+ }]>;
180
181
  invokedFunctions: string[];
181
182
  invokedFunctionsByFile?: Array<[string, string[]]>;
182
183
  usedAddons: string[];
@@ -185,6 +186,10 @@ export interface SerializableInspectorState {
185
186
  {
186
187
  package: string;
187
188
  rpcEndpoint?: string;
189
+ remote?: boolean;
190
+ hasAuth?: boolean;
191
+ authCredentialId?: string;
192
+ authSecretId?: string;
188
193
  secretOverrides?: Record<string, string>;
189
194
  variableOverrides?: Record<string, string>;
190
195
  credentialOverrides?: Record<string, string>;
@@ -227,6 +232,10 @@ export interface SerializableInspectorState {
227
232
  definitions: InspectorState['credentials']['definitions'];
228
233
  files: string[];
229
234
  };
235
+ scopes: {
236
+ definitions: InspectorState['scopes']['definitions'];
237
+ files: string[];
238
+ };
230
239
  variables: {
231
240
  definitions: InspectorState['variables']['definitions'];
232
241
  files: string[];
@@ -254,10 +263,6 @@ export interface SerializableInspectorState {
254
263
  permissions: {
255
264
  definitions: InspectorState['permissions']['definitions'];
256
265
  instances: InspectorState['permissions']['instances'];
257
- tagPermissions: Array<[
258
- string,
259
- InspectorState['permissions']['tagPermissions'] extends Map<string, infer V> ? V : never
260
- ]>;
261
266
  };
262
267
  serviceAggregation: {
263
268
  requiredServices: string[];
@@ -42,7 +42,6 @@ export function serializeInspectorState(state) {
42
42
  meta: state.http.meta,
43
43
  files: Array.from(state.http.files),
44
44
  routeMiddleware: Array.from(state.http.routeMiddleware.entries()),
45
- routePermissions: Array.from(state.http.routePermissions.entries()),
46
45
  },
47
46
  channels: {
48
47
  files: Array.from(state.channels.files),
@@ -77,6 +76,8 @@ export function serializeInspectorState(state) {
77
76
  internalFiles: Array.from(state.rpc.internalFiles.entries()),
78
77
  exposedMeta: state.rpc.exposedMeta,
79
78
  exposedFiles: Array.from(state.rpc.exposedFiles.entries()),
79
+ remoteMeta: state.rpc.remoteMeta,
80
+ remoteFiles: Array.from(state.rpc.remoteFiles.entries()),
80
81
  invokedFunctions: Array.from(state.rpc.invokedFunctions),
81
82
  invokedFunctionsByFile: Array.from((state.rpc.invokedFunctionsByFile ?? new Map()).entries()).map(([file, fns]) => [file, Array.from(fns)]),
82
83
  usedAddons: Array.from(state.rpc.usedAddons),
@@ -115,6 +116,10 @@ export function serializeInspectorState(state) {
115
116
  definitions: state.credentials.definitions,
116
117
  files: Array.from(state.credentials.files),
117
118
  },
119
+ scopes: {
120
+ definitions: state.scopes.definitions,
121
+ files: Array.from(state.scopes.files),
122
+ },
118
123
  variables: {
119
124
  definitions: state.variables.definitions,
120
125
  files: Array.from(state.variables.files),
@@ -136,7 +141,6 @@ export function serializeInspectorState(state) {
136
141
  permissions: {
137
142
  definitions: state.permissions.definitions,
138
143
  instances: state.permissions.instances,
139
- tagPermissions: Array.from(state.permissions.tagPermissions.entries()),
140
144
  },
141
145
  serviceAggregation: {
142
146
  requiredServices: Array.from(state.serviceAggregation.requiredServices),
@@ -201,7 +205,6 @@ export function deserializeInspectorState(data) {
201
205
  meta: data.http.meta,
202
206
  files: new Set(data.http.files),
203
207
  routeMiddleware: new Map(data.http.routeMiddleware),
204
- routePermissions: new Map(data.http.routePermissions),
205
208
  },
206
209
  channels: {
207
210
  files: new Set(data.channels.files),
@@ -236,6 +239,8 @@ export function deserializeInspectorState(data) {
236
239
  internalFiles: new Map(data.rpc.internalFiles),
237
240
  exposedMeta: data.rpc.exposedMeta,
238
241
  exposedFiles: new Map(data.rpc.exposedFiles),
242
+ remoteMeta: data.rpc.remoteMeta ?? {},
243
+ remoteFiles: new Map(data.rpc.remoteFiles ?? []),
239
244
  invokedFunctions: new Set(data.rpc.invokedFunctions),
240
245
  invokedFunctionsByFile: new Map((data.rpc.invokedFunctionsByFile || []).map(([file, fns]) => [file, new Set(fns)])),
241
246
  usedAddons: new Set(data.rpc.usedAddons || []),
@@ -274,6 +279,10 @@ export function deserializeInspectorState(data) {
274
279
  definitions: data.credentials?.definitions || [],
275
280
  files: new Set(data.credentials?.files || []),
276
281
  },
282
+ scopes: {
283
+ definitions: data.scopes?.definitions || [],
284
+ files: new Set(data.scopes?.files || []),
285
+ },
277
286
  variables: {
278
287
  definitions: data.variables?.definitions || [],
279
288
  files: new Set(data.variables?.files || []),
@@ -295,7 +304,6 @@ export function deserializeInspectorState(data) {
295
304
  permissions: {
296
305
  definitions: data.permissions.definitions,
297
306
  instances: data.permissions.instances || {},
298
- tagPermissions: new Map(data.permissions.tagPermissions),
299
307
  },
300
308
  serviceAggregation: {
301
309
  requiredServices: new Set(data.serviceAggregation.requiredServices),
@@ -315,8 +323,6 @@ export function deserializeInspectorState(data) {
315
323
  },
316
324
  permissionsGroupsMeta: data.permissionsGroupsMeta || {
317
325
  definitions: {},
318
- httpGroups: {},
319
- tagGroups: {},
320
326
  },
321
327
  requiredSchemas: new Set(data.requiredSchemas || []),
322
328
  openAPISpec: data.openAPISpec || null,
@@ -1,6 +1,4 @@
1
1
  import type { InspectorState } from '../types.js';
2
2
  export declare const serializePermissionsGroupsMeta: (state: InspectorState) => {
3
3
  definitions: Record<string, import("../types.js").InspectorPermissionDefinition>;
4
- httpGroups: Record<string, any>;
5
- tagGroups: Record<string, any>;
6
4
  };
@@ -1,31 +1,5 @@
1
1
  export const serializePermissionsGroupsMeta = (state) => {
2
- const httpGroups = {};
3
- for (const [pattern, meta] of state.http.routePermissions.entries()) {
4
- httpGroups[pattern] = {
5
- exportName: meta.exportName,
6
- sourceFile: meta.sourceFile,
7
- position: meta.position,
8
- services: meta.services,
9
- count: meta.count,
10
- instanceIds: meta.instanceIds,
11
- isFactory: meta.isFactory,
12
- };
13
- }
14
- const tagGroups = {};
15
- for (const [tag, meta] of state.permissions.tagPermissions.entries()) {
16
- tagGroups[tag] = {
17
- exportName: meta.exportName,
18
- sourceFile: meta.sourceFile,
19
- position: meta.position,
20
- services: meta.services,
21
- count: meta.count,
22
- instanceIds: meta.instanceIds,
23
- isFactory: meta.isFactory,
24
- };
25
- }
26
2
  return {
27
3
  definitions: state.permissions.definitions,
28
- httpGroups,
29
- tagGroups,
30
4
  };
31
5
  };
@@ -48,9 +48,7 @@ function containsConditional(steps) {
48
48
  * e.g. "awaiting_approval" → "Awaiting approval", "building-image" → "Building image"
49
49
  */
50
50
  function reasonToLabel(reason) {
51
- return reason
52
- .replace(/[-_]/g, ' ')
53
- .replace(/^(.)/, (c) => c.toUpperCase());
51
+ return reason.replace(/[-_]/g, ' ').replace(/^(.)/, (c) => c.toUpperCase());
54
52
  }
55
53
  /** Flatten named steps (rpc/inline/sleep/parallel children) in source order. */
56
54
  function collectNamedSteps(steps) {
@@ -72,6 +70,14 @@ function collectNamedSteps(steps) {
72
70
  displayName: reasonToLabel(step.reason),
73
71
  });
74
72
  break;
73
+ case 'approval':
74
+ // Same as suspend, but namespaced `__workflow_approval:` to match the
75
+ // runtime's separate step key.
76
+ planned.push({
77
+ stepName: `__workflow_approval:${step.reason}`,
78
+ displayName: reasonToLabel(step.reason),
79
+ });
80
+ break;
75
81
  case 'parallel':
76
82
  for (const child of step.children) {
77
83
  planned.push({ stepName: child.stepName });