@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.
- package/CHANGELOG.md +441 -0
- package/OPTIMIZATION-PLAN.md +9 -3
- package/dist/add/add-auth.d.ts +1 -1
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-cli.js +7 -0
- package/dist/add/add-credential.js +1 -1
- package/dist/add/add-functions.js +156 -7
- package/dist/add/add-http-route.js +16 -6
- package/dist/add/add-middleware.js +46 -3
- package/dist/add/add-personas.d.ts +13 -0
- package/dist/add/add-personas.js +210 -0
- package/dist/add/add-scope.js +1 -1
- package/dist/add/add-secret.js +1 -1
- package/dist/add/add-system-role.d.ts +2 -0
- package/dist/add/add-system-role.js +102 -0
- package/dist/add/add-variable.js +1 -1
- package/dist/add/add-wire-addon.js +31 -0
- package/dist/error-codes.d.ts +10 -1
- package/dist/error-codes.js +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +33 -1
- package/dist/types.d.ts +46 -2
- package/dist/utils/annotate-http-route-auth.d.ts +22 -0
- package/dist/utils/annotate-http-route-auth.js +48 -0
- package/dist/utils/extract-function-name.js +13 -4
- package/dist/utils/extract-function-node.js +18 -1
- package/dist/utils/extract-secret-usage.d.ts +9 -0
- package/dist/utils/extract-secret-usage.js +61 -0
- package/dist/utils/literal-properties.d.ts +22 -0
- package/dist/utils/literal-properties.js +69 -0
- package/dist/utils/load-addon-functions-meta.js +26 -1
- package/dist/utils/permissions.js +8 -0
- package/dist/utils/post-process.d.ts +68 -3
- package/dist/utils/post-process.js +243 -13
- package/dist/utils/secret-alias-services.d.ts +8 -0
- package/dist/utils/secret-alias-services.js +45 -0
- package/dist/utils/secret-brokers.d.ts +2 -0
- package/dist/utils/secret-brokers.js +9 -0
- package/dist/utils/serialize-inspector-state.d.ts +10 -0
- package/dist/utils/serialize-inspector-state.js +18 -0
- package/dist/utils/serialize-middleware-groups-meta.js +3 -0
- package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
- package/dist/utils/validate-exposed-functions-gated.js +85 -0
- package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
- package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/src/add/add-auth.test.ts +5 -3
- package/src/add/add-auth.ts +1 -1
- package/src/add/add-cli.ts +8 -0
- package/src/add/add-credential.ts +1 -1
- package/src/add/add-functions.test.ts +263 -1
- package/src/add/add-functions.ts +219 -9
- package/src/add/add-http-route.ts +20 -7
- package/src/add/add-middleware.test.ts +196 -0
- package/src/add/add-middleware.ts +64 -4
- package/src/add/add-personas.test.ts +268 -0
- package/src/add/add-personas.ts +276 -0
- package/src/add/add-scenario-step.test.ts +49 -3
- package/src/add/add-scope.test.ts +30 -30
- package/src/add/add-scope.ts +1 -1
- package/src/add/add-secret.ts +1 -1
- package/src/add/add-subject-scenario-steps.test.ts +161 -0
- package/src/add/add-system-role.test.ts +214 -0
- package/src/add/add-system-role.ts +164 -0
- package/src/add/add-variable.ts +1 -1
- package/src/add/add-wire-addon.test.ts +111 -0
- package/src/add/add-wire-addon.ts +29 -0
- package/src/add/addon-bans.test.ts +4 -4
- package/src/add/config-value-metadata.test.ts +11 -11
- package/src/add/wire-name-literal.test.ts +2 -2
- package/src/error-codes.ts +13 -0
- package/src/index.ts +4 -1
- package/src/inspector.ts +39 -0
- package/src/types.ts +47 -2
- package/src/utils/annotate-http-route-auth.test.ts +163 -0
- package/src/utils/annotate-http-route-auth.ts +52 -0
- package/src/utils/extract-function-name.ts +14 -4
- package/src/utils/extract-function-node.ts +18 -6
- package/src/utils/extract-secret-usage.test.ts +81 -0
- package/src/utils/extract-secret-usage.ts +73 -0
- package/src/utils/literal-properties.ts +97 -0
- package/src/utils/load-addon-functions-meta.ts +32 -2
- package/src/utils/permissions-shorthand.test.ts +79 -0
- package/src/utils/permissions.ts +7 -0
- package/src/utils/post-process.test.ts +133 -0
- package/src/utils/post-process.ts +310 -12
- package/src/utils/secret-alias-services.test.ts +146 -0
- package/src/utils/secret-alias-services.ts +58 -0
- package/src/utils/secret-brokers.test.ts +53 -0
- package/src/utils/secret-brokers.ts +11 -0
- package/src/utils/serialize-inspector-state.ts +28 -0
- package/src/utils/serialize-middleware-groups-meta.ts +3 -0
- package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
- package/src/utils/validate-exposed-functions-gated.ts +94 -0
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
- package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
- package/src/visit.ts +4 -0
- 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
|
+
};
|
package/dist/add/add-variable.js
CHANGED
|
@@ -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,
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -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;
|
package/dist/error-codes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|