@pikku/inspector 0.12.50 → 0.12.52

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 (99) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/OPTIMIZATION-PLAN.md +9 -3
  3. package/dist/add/add-auth.d.ts +1 -1
  4. package/dist/add/add-auth.js +1 -1
  5. package/dist/add/add-cli.js +7 -0
  6. package/dist/add/add-credential.js +1 -1
  7. package/dist/add/add-functions.js +156 -7
  8. package/dist/add/add-http-route.js +16 -6
  9. package/dist/add/add-middleware.js +46 -3
  10. package/dist/add/add-personas.d.ts +13 -0
  11. package/dist/add/add-personas.js +210 -0
  12. package/dist/add/add-scope.js +1 -1
  13. package/dist/add/add-secret.js +1 -1
  14. package/dist/add/add-system-role.d.ts +2 -0
  15. package/dist/add/add-system-role.js +102 -0
  16. package/dist/add/add-variable.js +1 -1
  17. package/dist/add/add-wire-addon.js +31 -0
  18. package/dist/error-codes.d.ts +10 -1
  19. package/dist/error-codes.js +11 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/inspector.js +33 -1
  22. package/dist/types.d.ts +46 -2
  23. package/dist/utils/annotate-http-route-auth.d.ts +22 -0
  24. package/dist/utils/annotate-http-route-auth.js +48 -0
  25. package/dist/utils/extract-function-name.js +13 -4
  26. package/dist/utils/extract-function-node.js +18 -1
  27. package/dist/utils/extract-secret-usage.d.ts +9 -0
  28. package/dist/utils/extract-secret-usage.js +61 -0
  29. package/dist/utils/literal-properties.d.ts +22 -0
  30. package/dist/utils/literal-properties.js +69 -0
  31. package/dist/utils/load-addon-functions-meta.js +26 -1
  32. package/dist/utils/permissions.js +8 -0
  33. package/dist/utils/post-process.d.ts +68 -3
  34. package/dist/utils/post-process.js +243 -13
  35. package/dist/utils/secret-alias-services.d.ts +8 -0
  36. package/dist/utils/secret-alias-services.js +45 -0
  37. package/dist/utils/secret-brokers.d.ts +2 -0
  38. package/dist/utils/secret-brokers.js +9 -0
  39. package/dist/utils/serialize-inspector-state.d.ts +10 -0
  40. package/dist/utils/serialize-inspector-state.js +18 -0
  41. package/dist/utils/serialize-middleware-groups-meta.js +3 -0
  42. package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
  43. package/dist/utils/validate-exposed-functions-gated.js +85 -0
  44. package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
  45. package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
  46. package/dist/visit.js +4 -0
  47. package/package.json +3 -3
  48. package/src/add/add-auth.test.ts +5 -3
  49. package/src/add/add-auth.ts +1 -1
  50. package/src/add/add-cli.ts +8 -0
  51. package/src/add/add-credential.ts +1 -1
  52. package/src/add/add-functions.test.ts +263 -1
  53. package/src/add/add-functions.ts +219 -9
  54. package/src/add/add-http-route.ts +20 -7
  55. package/src/add/add-middleware.test.ts +196 -0
  56. package/src/add/add-middleware.ts +64 -4
  57. package/src/add/add-personas.test.ts +268 -0
  58. package/src/add/add-personas.ts +276 -0
  59. package/src/add/add-scenario-step.test.ts +49 -3
  60. package/src/add/add-scope.test.ts +30 -30
  61. package/src/add/add-scope.ts +1 -1
  62. package/src/add/add-secret.ts +1 -1
  63. package/src/add/add-subject-scenario-steps.test.ts +161 -0
  64. package/src/add/add-system-role.test.ts +214 -0
  65. package/src/add/add-system-role.ts +164 -0
  66. package/src/add/add-variable.ts +1 -1
  67. package/src/add/add-wire-addon.test.ts +111 -0
  68. package/src/add/add-wire-addon.ts +29 -0
  69. package/src/add/addon-bans.test.ts +4 -4
  70. package/src/add/config-value-metadata.test.ts +11 -11
  71. package/src/add/wire-name-literal.test.ts +2 -2
  72. package/src/error-codes.ts +13 -0
  73. package/src/index.ts +4 -1
  74. package/src/inspector.ts +39 -0
  75. package/src/types.ts +47 -2
  76. package/src/utils/annotate-http-route-auth.test.ts +163 -0
  77. package/src/utils/annotate-http-route-auth.ts +52 -0
  78. package/src/utils/extract-function-name.ts +14 -4
  79. package/src/utils/extract-function-node.ts +18 -6
  80. package/src/utils/extract-secret-usage.test.ts +81 -0
  81. package/src/utils/extract-secret-usage.ts +73 -0
  82. package/src/utils/literal-properties.ts +97 -0
  83. package/src/utils/load-addon-functions-meta.ts +32 -2
  84. package/src/utils/permissions-shorthand.test.ts +79 -0
  85. package/src/utils/permissions.ts +7 -0
  86. package/src/utils/post-process.test.ts +133 -0
  87. package/src/utils/post-process.ts +310 -12
  88. package/src/utils/secret-alias-services.test.ts +146 -0
  89. package/src/utils/secret-alias-services.ts +58 -0
  90. package/src/utils/secret-brokers.test.ts +53 -0
  91. package/src/utils/secret-brokers.ts +11 -0
  92. package/src/utils/serialize-inspector-state.ts +28 -0
  93. package/src/utils/serialize-middleware-groups-meta.ts +3 -0
  94. package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
  95. package/src/utils/validate-exposed-functions-gated.ts +94 -0
  96. package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
  97. package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
  98. package/src/visit.ts +4 -0
  99. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,102 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue } from '../utils/get-property-value.js';
3
+ import { ErrorCode } from '../error-codes.js';
4
+ const SEPARATOR = ':';
5
+ /**
6
+ * Unwraps `x as const` / `x satisfies T` so a cast declaration is still
7
+ * extracted rather than silently skipped.
8
+ */
9
+ const unwrapAs = (node) => ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
10
+ ? unwrapAs(node.expression)
11
+ : node;
12
+ /**
13
+ * Reads a `scopes: [...]` array of string literals.
14
+ *
15
+ * Returns `undefined` rather than an empty array when the property is absent
16
+ * or unreadable, so the caller can tell "declared no scopes" from "we could
17
+ * not see the scopes" — the first is a legitimate placeholder role, the second
18
+ * would silently produce one.
19
+ */
20
+ const extractScopes = (obj, roleName, logger) => {
21
+ const prop = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
22
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
23
+ p.name.text === 'scopes');
24
+ if (!prop) {
25
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${roleName}' has no 'scopes'. A role that grants nothing should say so with 'scopes: []'.`);
26
+ return undefined;
27
+ }
28
+ if (!ts.isPropertyAssignment(prop) ||
29
+ !ts.isArrayLiteralExpression(prop.initializer)) {
30
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${roleName}' must declare 'scopes' as an array literal.`);
31
+ return undefined;
32
+ }
33
+ const scopes = [];
34
+ for (const element of prop.initializer.elements) {
35
+ const unwrapped = unwrapAs(element);
36
+ if (!ts.isStringLiteral(unwrapped)) {
37
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${roleName}' lists a scope that is not a string literal. ` +
38
+ `Scopes are extracted by AST, so a computed value cannot be checked against the declared set.`);
39
+ continue;
40
+ }
41
+ scopes.push(unwrapped.text);
42
+ }
43
+ return scopes;
44
+ };
45
+ export const addSystemRole = (logger, node, _checker, state, _options) => {
46
+ if (!ts.isCallExpression(node)) {
47
+ return;
48
+ }
49
+ const expression = node.expression;
50
+ if (!ts.isIdentifier(expression) || expression.text !== 'defineSystemRole') {
51
+ return;
52
+ }
53
+ const firstArg = node.arguments[0];
54
+ if (!firstArg) {
55
+ return;
56
+ }
57
+ const unwrapped = unwrapAs(firstArg);
58
+ if (!ts.isObjectLiteralExpression(unwrapped)) {
59
+ return;
60
+ }
61
+ const sourceFile = node.getSourceFile().fileName;
62
+ for (const prop of unwrapped.properties) {
63
+ if (!ts.isPropertyAssignment(prop)) {
64
+ continue;
65
+ }
66
+ let name;
67
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
68
+ name = prop.name.text;
69
+ }
70
+ if (name === undefined) {
71
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A system role is declared with a key that is not a literal.');
72
+ continue;
73
+ }
74
+ if (name.length === 0) {
75
+ logger.critical(ErrorCode.INVALID_VALUE, 'A system role is declared with an empty name.');
76
+ continue;
77
+ }
78
+ if (name.includes(SEPARATOR)) {
79
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${name}' contains the '${SEPARATOR}' separator. ` +
80
+ `'${SEPARATOR}' delimits scope ids; a role named like a scope reads as one.`);
81
+ continue;
82
+ }
83
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
84
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${name}' must be an object literal.`);
85
+ continue;
86
+ }
87
+ const scopes = extractScopes(prop.initializer, name, logger);
88
+ if (scopes === undefined) {
89
+ continue;
90
+ }
91
+ const displayName = getPropertyValue(prop.initializer, 'displayName');
92
+ const description = getPropertyValue(prop.initializer, 'description');
93
+ state.systemRoles.files.add(sourceFile);
94
+ state.systemRoles.definitions.push({
95
+ name,
96
+ displayName: displayName || undefined,
97
+ description: description || undefined,
98
+ scopes,
99
+ sourceFile,
100
+ });
101
+ }
102
+ };
@@ -1,6 +1,6 @@
1
1
  import { createAddKeyedWiring } from './add-keyed-wiring.js';
2
2
  export const addVariable = createAddKeyedWiring({
3
- functionName: 'wireVariable',
3
+ functionName: 'defineVariable',
4
4
  idField: 'variableId',
5
5
  label: 'Variable',
6
6
  schemaPrefix: 'VariableSchema',
@@ -1,4 +1,18 @@
1
1
  import * as ts from 'typescript';
2
+ function parseStringArray(node) {
3
+ if (!ts.isArrayLiteralExpression(node))
4
+ return undefined;
5
+ const values = [];
6
+ for (const element of node.elements) {
7
+ // A non-literal entry (a spread, a const reference) is not statically
8
+ // knowable. Dropping the whole array rather than a silent partial keeps a
9
+ // consumer from reading a short list as the complete set of gates.
10
+ if (!ts.isStringLiteral(element))
11
+ return undefined;
12
+ values.push(element.text);
13
+ }
14
+ return values;
15
+ }
2
16
  function parseStringRecord(obj) {
3
17
  const result = {};
4
18
  for (const prop of obj.properties) {
@@ -33,6 +47,9 @@ export function addWireAddon(node, state, logger) {
33
47
  let pkg;
34
48
  let rpcEndpoint;
35
49
  let mcp;
50
+ let auth;
51
+ let tags;
52
+ let scopes;
36
53
  let secretOverrides;
37
54
  let variableOverrides;
38
55
  let credentialOverrides;
@@ -54,6 +71,17 @@ export function addWireAddon(node, state, logger) {
54
71
  prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
55
72
  mcp = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
56
73
  }
74
+ else if (key === 'auth' &&
75
+ (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
76
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
77
+ auth = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
78
+ }
79
+ else if (key === 'tags') {
80
+ tags = parseStringArray(prop.initializer);
81
+ }
82
+ else if (key === 'scopes') {
83
+ scopes = parseStringArray(prop.initializer);
84
+ }
57
85
  else if (key === 'secretOverrides' &&
58
86
  ts.isObjectLiteralExpression(prop.initializer)) {
59
87
  secretOverrides = parseStringRecord(prop.initializer);
@@ -74,6 +102,9 @@ export function addWireAddon(node, state, logger) {
74
102
  package: pkg,
75
103
  rpcEndpoint,
76
104
  mcp,
105
+ auth,
106
+ tags,
107
+ scopes,
77
108
  secretOverrides,
78
109
  variableOverrides,
79
110
  credentialOverrides,
@@ -23,6 +23,7 @@ export declare enum ErrorCode {
23
23
  SCENARIO_BROWSER_STEP_NEEDS_ACTOR = "PKU677",
24
24
  SCENARIO_STEP_TARGET_NOT_STATIC = "PKU678",
25
25
  SCENARIO_NOT_EXTRACTABLE = "PKU679",
26
+ SCENARIO_HAS_NO_ASSERTION = "PKU680",
26
27
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
27
28
  INVALID_DSL_WORKFLOW = "PKU641",
28
29
  WORKFLOW_GRAPH_ADDON_NOT_WIRED = "PKU642",
@@ -39,6 +40,8 @@ export declare enum ErrorCode {
39
40
  ROUTE_PARAM_MISMATCH = "PKU571",
40
41
  ROUTE_QUERY_MISMATCH = "PKU572",
41
42
  AUTH_DISABLED_REQUIRES_SESSIONLESS = "PKU573",
43
+ EXPOSED_FUNCTION_HAS_NO_GATE = "PKU574",
44
+ TAG_RESOLVES_TO_NO_MIDDLEWARE = "PKU575",
42
45
  DUPLICATE_FUNCTION_VERSION = "PKU850",
43
46
  DUPLICATE_FUNCTION_NAME = "PKU851",
44
47
  MANIFEST_MISSING = "PKU860",
@@ -49,6 +52,9 @@ export declare enum ErrorCode {
49
52
  MANIFEST_INTEGRITY_ERROR = "PKU865",
50
53
  MISSING_MODEL = "PKU145",
51
54
  INVALID_MODEL = "PKU146",
55
+ AGENT_TOOL_UNKNOWN_NAMESPACE = "PKU152",
56
+ AGENT_TOOL_NOT_FOUND = "PKU153",
57
+ AGENT_TOOL_MISSING_DESCRIPTION = "PKU154",
52
58
  SCHEMA_AND_WIRING_COLOCATED = "PKU490",
53
59
  SERVICES_NOT_DESTRUCTURED = "PKU410",
54
60
  WIRES_NOT_DESTRUCTURED = "PKU411",
@@ -58,7 +64,10 @@ export declare enum ErrorCode {
58
64
  PII_IN_OUTPUT = "PKU910",
59
65
  ADDON_WIRING_NOT_ALLOWED = "PKU920",
60
66
  ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
61
- RPC_INVOCATION_TYPE_CAST = "PKU940"
67
+ RPC_INVOCATION_TYPE_CAST = "PKU940",
68
+ SECRET_SERVICE_ALIASED = "PKU950",
69
+ SECRET_NOT_DECLARED = "PKU951",
70
+ SECRET_KEY_NOT_STATIC = "PKU952"
62
71
  }
63
72
  /**
64
73
  * Severity of a tracked, coded diagnostic. `critical` always blocks the build;
@@ -25,6 +25,7 @@ export var ErrorCode;
25
25
  ErrorCode["SCENARIO_BROWSER_STEP_NEEDS_ACTOR"] = "PKU677";
26
26
  ErrorCode["SCENARIO_STEP_TARGET_NOT_STATIC"] = "PKU678";
27
27
  ErrorCode["SCENARIO_NOT_EXTRACTABLE"] = "PKU679";
28
+ ErrorCode["SCENARIO_HAS_NO_ASSERTION"] = "PKU680";
28
29
  ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
29
30
  ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
30
31
  ErrorCode["WORKFLOW_GRAPH_ADDON_NOT_WIRED"] = "PKU642";
@@ -47,6 +48,8 @@ export var ErrorCode;
47
48
  ErrorCode["ROUTE_PARAM_MISMATCH"] = "PKU571";
48
49
  ErrorCode["ROUTE_QUERY_MISMATCH"] = "PKU572";
49
50
  ErrorCode["AUTH_DISABLED_REQUIRES_SESSIONLESS"] = "PKU573";
51
+ ErrorCode["EXPOSED_FUNCTION_HAS_NO_GATE"] = "PKU574";
52
+ ErrorCode["TAG_RESOLVES_TO_NO_MIDDLEWARE"] = "PKU575";
50
53
  // Versioning errors
51
54
  ErrorCode["DUPLICATE_FUNCTION_VERSION"] = "PKU850";
52
55
  ErrorCode["DUPLICATE_FUNCTION_NAME"] = "PKU851";
@@ -60,6 +63,10 @@ export var ErrorCode;
60
63
  // Model configuration errors
61
64
  ErrorCode["MISSING_MODEL"] = "PKU145";
62
65
  ErrorCode["INVALID_MODEL"] = "PKU146";
66
+ // Agent tool reference errors
67
+ ErrorCode["AGENT_TOOL_UNKNOWN_NAMESPACE"] = "PKU152";
68
+ ErrorCode["AGENT_TOOL_NOT_FOUND"] = "PKU153";
69
+ ErrorCode["AGENT_TOOL_MISSING_DESCRIPTION"] = "PKU154";
63
70
  // File structure errors
64
71
  ErrorCode["SCHEMA_AND_WIRING_COLOCATED"] = "PKU490";
65
72
  // Optimization diagnostics
@@ -75,4 +82,8 @@ export var ErrorCode;
75
82
  ErrorCode["ADDON_WIRING_NOT_ALLOWED"] = "PKU920";
76
83
  ErrorCode["ADDON_CONTRACT_HANDLERS_NOT_ALLOWED"] = "PKU921";
77
84
  ErrorCode["RPC_INVOCATION_TYPE_CAST"] = "PKU940";
85
+ // Secret boundary errors
86
+ ErrorCode["SECRET_SERVICE_ALIASED"] = "PKU950";
87
+ ErrorCode["SECRET_NOT_DECLARED"] = "PKU951";
88
+ ErrorCode["SECRET_KEY_NOT_STATIC"] = "PKU952";
78
89
  })(ErrorCode || (ErrorCode = {}));
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
14
14
  export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
15
15
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
16
16
  export type { ContractEntry, VersionHashEntry, VersionValidateError, VersionManifest, VersionManifestEntry, } from './utils/contract-hashes.js';
17
- export type { InspectorFeature, InspectorFeatureEntry } from './types.js';
17
+ export type { InspectorFeature, InspectorFeatureEntry, } from './types.js';
18
18
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
19
19
  export type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
20
20
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
package/dist/inspector.js CHANGED
@@ -6,13 +6,17 @@ import { TypesMap } from './types-map.js';
6
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
7
7
  import { findCommonAncestor } from './utils/find-root-dir.js';
8
8
  import { createNestedProjectFilter } from './utils/nested-project-filter.js';
9
- import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
9
+ import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeSecretBrokers, validateNoSecretAliasServices, validateSecretUsage, computeDiagnostics, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
10
+ import { validateExposedFunctionsGated } from './utils/validate-exposed-functions-gated.js';
11
+ import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js';
12
+ import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js';
10
13
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
11
14
  import { pikkuState } from '@pikku/core/internal';
12
15
  import { resolveLatestVersions } from './utils/resolve-versions.js';
13
16
  import { finalizeWorkflows } from './utils/workflow/graph/finalize-workflows.js';
14
17
  import { finalizeWorkflowHelperTypes, finalizeWorkflowWires, } from './utils/workflow/graph/finalize-workflow-wires.js';
15
18
  import { generateAllSchemas } from './utils/schema-generator.js';
19
+ import { extractSecretUsage } from './utils/extract-secret-usage.js';
16
20
  import { loadAddonFunctionsMeta, loadAddonSchemas, } from './utils/load-addon-functions-meta.js';
17
21
  import { computeContractHashes, extractContractsFromMeta, updateManifest, createEmptyManifest, validateContracts, } from './utils/contract-hashes.js';
18
22
  /**
@@ -132,6 +136,7 @@ export function getInitialInspectorState(rootDir) {
132
136
  secrets: {
133
137
  definitions: [],
134
138
  files: new Set(),
139
+ usage: new Map(),
135
140
  },
136
141
  credentials: {
137
142
  definitions: [],
@@ -141,6 +146,14 @@ export function getInitialInspectorState(rootDir) {
141
146
  definitions: [],
142
147
  files: new Set(),
143
148
  },
149
+ systemRoles: {
150
+ definitions: [],
151
+ files: new Set(),
152
+ },
153
+ personas: {
154
+ definitions: [],
155
+ files: new Set(),
156
+ },
144
157
  variables: {
145
158
  definitions: [],
146
159
  files: new Set(),
@@ -282,6 +295,12 @@ export const inspect = async (logger, routeFiles, options = {}) => {
282
295
  .sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
283
296
  logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
284
297
  const state = getInitialInspectorState(rootDir);
298
+ for (const sourceFile of sourceFiles) {
299
+ const usage = extractSecretUsage(sourceFile);
300
+ if (usage.keys.length > 0 || usage.dynamic.length > 0) {
301
+ state.secrets.usage.set(sourceFile.fileName, usage);
302
+ }
303
+ }
285
304
  // First sweep: add all functions
286
305
  const startSetup = performance.now();
287
306
  for (const sourceFile of sourceFiles) {
@@ -342,21 +361,34 @@ export const inspect = async (logger, routeFiles, options = {}) => {
342
361
  computeResolvedIOTypes(state);
343
362
  computeMiddlewareGroupsMeta(state);
344
363
  computePermissionsGroupsMeta(state);
364
+ computeSecretBrokers(state);
365
+ validateNoSecretAliasServices(logger, checker, state);
366
+ validateSecretUsage(logger, state);
345
367
  computeDiagnostics(state);
346
368
  validateSchemaWiringSeparation(logger, state);
347
369
  validateWorkflowGraphAddons(logger, state);
348
370
  validateScenarioServices(logger, state);
349
371
  validateScenarioSteps(logger, state);
372
+ // Needs every route, every function and every wireAddon declaration
373
+ // together, so it can only run once the whole program is inspected.
374
+ annotateHttpRouteAuth(state);
375
+ // Last of the validators: they read function meta and every wireAddon
376
+ // declaration together, so they need the whole program inspected.
377
+ validateExposedFunctionsGated(logger, state);
378
+ validateTagsResolveToMiddleware(logger, state);
350
379
  if (options.openAPI) {
351
380
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
352
381
  }
353
382
  validateAgentModels(logger, state);
383
+ validateAgentToolReferences(logger, state, options);
354
384
  validateSecretOverrides(logger, state);
355
385
  validateVariableOverrides(logger, state);
356
386
  validateCredentialOverrides(logger, state);
357
387
  validateRemoteAddonDependencies(logger, state);
358
388
  validateRemoteAddonAuth(logger, state);
359
389
  validateScopeReferences(logger, state);
390
+ validateSystemRoleScopes(logger, state);
391
+ validatePersonaRoles(logger, state);
360
392
  }
361
393
  state.program = program;
362
394
  return state;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type * as ts from 'typescript';
2
+ import type { SecretUsage } from './utils/extract-secret-usage.js';
2
3
  import type { ChannelMessageMeta, ChannelsMeta } from '@pikku/core/channel';
3
4
  import type { GatewaysMeta } from '@pikku/core/gateway';
4
5
  import type { HTTPWiringsMeta } from '@pikku/core/http';
@@ -14,6 +15,8 @@ import type { NodesMeta } from '@pikku/core/node';
14
15
  import type { SecretDefinitions } from '@pikku/core/secret';
15
16
  import type { CredentialDefinitions } from '@pikku/core/credential';
16
17
  import type { ScopeDefinitions } from '@pikku/core/scope';
18
+ import type { SystemRoleDefinitions } from '@pikku/core/role';
19
+ import type { PersonaDefinitions } from '@pikku/core/persona';
17
20
  import type { VariableDefinitions } from '@pikku/core/variable';
18
21
  import type { TypesMap } from './types-map.js';
19
22
  import type { FunctionsMeta, FunctionServicesMeta, FunctionWiresMeta, JSONValue } from '@pikku/core';
@@ -31,14 +34,30 @@ export type MetaInputTypes = Map<string, {
31
34
  params: string[] | undefined;
32
35
  body: string[] | undefined;
33
36
  }>;
34
- export interface MiddlewareGroupMeta {
37
+ /** One `addHTTPMiddleware`/`addTagMiddleware`/`addChannelMiddleware` call. */
38
+ export interface MiddlewareRegistration {
35
39
  exportName: string | null;
36
40
  sourceFile: string;
37
41
  position: number;
42
+ isFactory: boolean;
43
+ }
44
+ export interface MiddlewareGroupMeta extends MiddlewareRegistration {
38
45
  services: FunctionServicesMeta;
39
46
  count: number;
40
47
  instanceIds: string[];
41
- isFactory: boolean;
48
+ /**
49
+ * The registrations after the first, when several files add middleware for
50
+ * the same pattern or tag.
51
+ *
52
+ * A group is keyed by its pattern/tag, so without this a second file's call
53
+ * overwrote the first's and only the survivor got an import emitted into
54
+ * pikku-middleware.gen.ts — the losing file was never imported, so its
55
+ * registration never ran. The runtime composes repeated registrations for a
56
+ * pattern happily; only codegen dropped one, which is how an app could lose
57
+ * its session-bridge middleware to an unrelated `'*'` group and see nothing
58
+ * go wrong until a request arrived without a session.
59
+ */
60
+ additionalRegistrations?: MiddlewareRegistration[];
42
61
  }
43
62
  export interface InspectorHTTPState {
44
63
  metaInputTypes: MetaInputTypes;
@@ -204,6 +223,12 @@ export type InspectorOptions = Partial<{
204
223
  setupOnly: boolean;
205
224
  rootDir: string;
206
225
  isAddon: boolean;
226
+ /**
227
+ * Fail the build on metadata that only an agent needs — currently an agent
228
+ * tool with no description. Off by default: the runtime falls back to the
229
+ * tool's own name, so this is a quality gate rather than a correctness one.
230
+ */
231
+ strictMeta: boolean;
207
232
  sourceFile: ts.SourceFile;
208
233
  /**
209
234
  * The project's tsconfig, read for its path mappings only. The inspector's
@@ -464,6 +489,15 @@ export interface InspectorState {
464
489
  package: string;
465
490
  rpcEndpoint?: string;
466
491
  mcp?: boolean;
492
+ /**
493
+ * The addon's own gates. `runPikkuFunc` applies these to every function
494
+ * in the package, on every wiring path — not just `namespace:function`.
495
+ * Absent when the declared value was not a statically-knowable literal,
496
+ * which must be read as "unknown", never as "ungated".
497
+ */
498
+ auth?: boolean;
499
+ tags?: string[];
500
+ scopes?: string[];
467
501
  /** True when declared via `wireRemoteAddon` — import the addon's `.remote.gen` map, not `.internal.gen` */
468
502
  remote?: boolean;
469
503
  /** wireRemoteAddon: whether an `auth` binding was supplied (vs a public surface) */
@@ -522,6 +556,8 @@ export interface InspectorState {
522
556
  secrets: {
523
557
  definitions: SecretDefinitions;
524
558
  files: Set<string>;
559
+ /** Secret reads found per source file, keyed by absolute path. */
560
+ usage: Map<string, SecretUsage>;
525
561
  };
526
562
  credentials: {
527
563
  definitions: CredentialDefinitions;
@@ -531,6 +567,14 @@ export interface InspectorState {
531
567
  definitions: ScopeDefinitions;
532
568
  files: Set<string>;
533
569
  };
570
+ systemRoles: {
571
+ definitions: SystemRoleDefinitions;
572
+ files: Set<string>;
573
+ };
574
+ personas: {
575
+ definitions: PersonaDefinitions;
576
+ files: Set<string>;
577
+ };
534
578
  variables: {
535
579
  definitions: VariableDefinitions;
536
580
  files: Set<string>;
@@ -0,0 +1,22 @@
1
+ import type { InspectorState } from '../types.js';
2
+ /**
3
+ * Resolve, per HTTP route, whether reaching it requires a session.
4
+ *
5
+ * Four separate places can demand one — the function's `sessionless`, the
6
+ * function's own `auth`, the route's (or its group's) `auth`, and the addon the
7
+ * function belongs to — and they combine with OR: any of them is enough. Until
8
+ * this was written down, "which routes are open?" could only be answered by
9
+ * joining all four by hand, and getting the join wrong reads an open route as a
10
+ * closed one, which is the direction that costs something.
11
+ *
12
+ * A scope gate counts as requiring a session. Scopes are checked against the
13
+ * session's and fail closed, so an anonymous caller is rejected either way; not
14
+ * counting them would mark a route open that nobody can reach anonymously.
15
+ *
16
+ * Anything not statically knowable resolves to requiring a session, matching
17
+ * `validateExposedFunctionsGated`: an over-cautious `true` costs a route that
18
+ * looks stricter than it is, a wrong `false` costs an audit.
19
+ *
20
+ * knowledge: decisions/security/addon-auth-and-tags-only-tighten.md
21
+ */
22
+ export declare function annotateHttpRouteAuth(state: InspectorState): void;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Resolve, per HTTP route, whether reaching it requires a session.
3
+ *
4
+ * Four separate places can demand one — the function's `sessionless`, the
5
+ * function's own `auth`, the route's (or its group's) `auth`, and the addon the
6
+ * function belongs to — and they combine with OR: any of them is enough. Until
7
+ * this was written down, "which routes are open?" could only be answered by
8
+ * joining all four by hand, and getting the join wrong reads an open route as a
9
+ * closed one, which is the direction that costs something.
10
+ *
11
+ * A scope gate counts as requiring a session. Scopes are checked against the
12
+ * session's and fail closed, so an anonymous caller is rejected either way; not
13
+ * counting them would mark a route open that nobody can reach anonymously.
14
+ *
15
+ * Anything not statically knowable resolves to requiring a session, matching
16
+ * `validateExposedFunctionsGated`: an over-cautious `true` costs a route that
17
+ * looks stricter than it is, a wrong `false` costs an audit.
18
+ *
19
+ * knowledge: decisions/security/addon-auth-and-tags-only-tighten.md
20
+ */
21
+ export function annotateHttpRouteAuth(state) {
22
+ const gatedPackages = packagesGatedByTheirAddon(state);
23
+ for (const routes of Object.values(state.http?.meta ?? {})) {
24
+ for (const route of Object.values(routes)) {
25
+ const meta = state.functions.meta[route.refTarget ?? route.pikkuFuncId];
26
+ route.requiresSession =
27
+ // A pikkuFunc always requires a session, on every path.
28
+ meta?.sessionless === false ||
29
+ meta?.auth === true ||
30
+ (meta?.scopes?.length ?? 0) > 0 ||
31
+ route.auth === true ||
32
+ (route.packageName !== undefined &&
33
+ gatedPackages.has(route.packageName));
34
+ }
35
+ }
36
+ }
37
+ /** Packages whose `wireAddon` demands a session from everything inside them. */
38
+ function packagesGatedByTheirAddon(state) {
39
+ const gated = new Set();
40
+ for (const declaration of state.rpc?.wireAddonDeclarations?.values() ?? []) {
41
+ if (!declaration.package)
42
+ continue;
43
+ if (declaration.auth === true || (declaration.scopes?.length ?? 0) > 0) {
44
+ gated.add(declaration.package);
45
+ }
46
+ }
47
+ return gated;
48
+ }
@@ -29,15 +29,24 @@ export function funcIdToTypeName(id) {
29
29
  .join('');
30
30
  }
31
31
  /**
32
- * `pikkuScenarioStep({ name })` is the one wrapper whose `name` is its identity:
33
- * scenarios reference a step by that string, so it must become the pikkuFuncId
34
- * (and hence the generated step map's key), not just a cosmetic label the way
32
+ * The step wrappers, whichever kind of subject acts in them. A scenario calls a
33
+ * platform or addon step by name exactly as it calls a persona one.
34
+ */
35
+ const SCENARIO_STEP_WRAPPERS = new Set([
36
+ 'pikkuScenarioStep',
37
+ 'pikkuPlatformScenarioStep',
38
+ 'pikkuAddonScenarioStep',
39
+ ]);
40
+ /**
41
+ * A scenario step is the one wrapper whose `name` is its identity: scenarios
42
+ * reference a step by that string, so it must become the pikkuFuncId (and hence
43
+ * the generated step map's key), not just a cosmetic label the way
35
44
  * `pikkuMiddleware`/`pikkuAgent` use it.
36
45
  */
37
46
  function extractScenarioStepName(callExpr, result) {
38
47
  if (!ts.isCallExpression(callExpr) ||
39
48
  !ts.isIdentifier(callExpr.expression) ||
40
- callExpr.expression.text !== 'pikkuScenarioStep') {
49
+ !SCENARIO_STEP_WRAPPERS.has(callExpr.expression.text)) {
41
50
  return;
42
51
  }
43
52
  const firstArg = callExpr.arguments[0];
@@ -1,5 +1,15 @@
1
1
  import * as ts from 'typescript';
2
2
  import { getPropertyAssignmentInitializer, resolveFunctionDeclaration, } from './type-utils.js';
3
+ /**
4
+ * Where a config object's implementation can live, in the order it is looked for.
5
+ *
6
+ * `func` is the ordinary case. A `pikkuScenarioStep` instead declares one
7
+ * implementation per surface, and every binding shares the step's input and
8
+ * output — that is what makes them interchangeable — so any one of them types
9
+ * the step. `default` is preferred because it is the binding every step is
10
+ * expected to have.
11
+ */
12
+ const IMPLEMENTATION_KEYS = ['func', 'default', 'browser', 'cli'];
3
13
  /**
4
14
  * Extracts the actual function node from a pikkuFunc/pikkuWorkflowFunc call
5
15
  * Handles both direct function form and config object form { func: ... }
@@ -10,7 +20,14 @@ export function extractFunctionNode(firstArg, checker) {
10
20
  // Check if first argument is a config object with 'func' property
11
21
  if (ts.isObjectLiteralExpression(firstArg)) {
12
22
  isDirectFunction = false;
13
- const funcInitializer = getPropertyAssignmentInitializer(firstArg, 'func', true, checker);
23
+ let funcInitializer;
24
+ for (const key of IMPLEMENTATION_KEYS) {
25
+ funcInitializer =
26
+ getPropertyAssignmentInitializer(firstArg, key, true, checker) ??
27
+ undefined;
28
+ if (funcInitializer)
29
+ break;
30
+ }
14
31
  if (funcInitializer) {
15
32
  funcNode = funcInitializer;
16
33
  }
@@ -0,0 +1,9 @@
1
+ import ts from 'typescript';
2
+ export type SecretUsage = {
3
+ /** Secret ids read with a string literal, so statically known. */
4
+ keys: string[];
5
+ /** Reads whose key could not be resolved statically, as `line:source`. */
6
+ dynamic: string[];
7
+ };
8
+ /** Secret reads in a file, whether the key is statically known or not. */
9
+ export declare const extractSecretUsage: (sourceFile: ts.SourceFile) => SecretUsage;
@@ -0,0 +1,61 @@
1
+ import ts from 'typescript';
2
+ const SINGLE_KEY_METHODS = new Set(['getSecret', 'hasSecret']);
3
+ const KEY_LIST_METHODS = new Set(['getSecrets']);
4
+ const literalKey = (node) => {
5
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
6
+ return node.text;
7
+ }
8
+ return undefined;
9
+ };
10
+ /** Secret reads in a file, whether the key is statically known or not. */
11
+ export const extractSecretUsage = (sourceFile) => {
12
+ const keys = new Set();
13
+ const dynamic = new Set();
14
+ const record = (node, argument) => {
15
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
16
+ dynamic.add(`${line + 1}:${(argument ?? node).getText()}`);
17
+ };
18
+ const visit = (node) => {
19
+ if (ts.isCallExpression(node) &&
20
+ ts.isPropertyAccessExpression(node.expression)) {
21
+ const method = node.expression.name.text;
22
+ const [firstArgument] = node.arguments;
23
+ if (SINGLE_KEY_METHODS.has(method)) {
24
+ if (!firstArgument) {
25
+ record(node);
26
+ }
27
+ else {
28
+ const key = literalKey(firstArgument);
29
+ if (key === undefined) {
30
+ record(node, firstArgument);
31
+ }
32
+ else {
33
+ keys.add(key);
34
+ }
35
+ }
36
+ }
37
+ else if (KEY_LIST_METHODS.has(method)) {
38
+ if (firstArgument && ts.isArrayLiteralExpression(firstArgument)) {
39
+ for (const element of firstArgument.elements) {
40
+ const key = literalKey(element);
41
+ if (key === undefined) {
42
+ record(node, element);
43
+ }
44
+ else {
45
+ keys.add(key);
46
+ }
47
+ }
48
+ }
49
+ else {
50
+ record(node, firstArgument);
51
+ }
52
+ }
53
+ }
54
+ ts.forEachChild(node, visit);
55
+ };
56
+ visit(sourceFile);
57
+ return {
58
+ keys: [...keys].sort(),
59
+ dynamic: [...dynamic].sort(),
60
+ };
61
+ };