@pikku/inspector 0.12.86 → 0.12.88
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 +54 -0
- package/dist/add/add-agent.d.ts +10 -0
- package/dist/add/add-agent.js +29 -0
- package/dist/add/add-functions.js +9 -5
- package/dist/add/add-wire-addon.js +15 -0
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +4 -0
- package/dist/types-map.d.ts +7 -0
- package/dist/types-map.js +10 -1
- package/dist/types.d.ts +7 -0
- package/dist/utils/literal-properties.d.ts +11 -0
- package/dist/utils/literal-properties.js +79 -0
- package/dist/utils/load-addon-functions-meta.d.ts +1 -0
- package/dist/utils/load-addon-functions-meta.js +92 -5
- package/dist/utils/post-process.js +7 -0
- package/dist/utils/validate-exposed-functions-gated.js +34 -17
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
+
## 0.12.88
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 55ab4d1: An addon's metadata now resolves from the package that calls `wireAddon`, as well as from the repo root, so a workspace addon listed only in `packages/functions` loads. When it still cannot be found, the warning names the package, the directories tried and the fix: add the dependency and install, or build the addon if it is installed but has no generated metadata.
|
|
6
|
+
- f6c1dd6: A fresh project with Better Auth no longer gets PKU951 for `BETTER_AUTH_SECRET` on its first `pikku all`. The secret is declared by the generated `auth-secrets.gen.ts`, which did not exist yet when the first inspection ran.
|
|
7
|
+
- 0210e96: Review fixes for the OpenAPI addon onboarding:
|
|
8
|
+
|
|
9
|
+
- A delegated sign-in whose email the upstream did not return, including a login typed as an email, never links to an existing user. An authenticator that omits `syntheticEmail` counts as synthetic.
|
|
10
|
+
- `pikkuActor` credentials take an optional `remove`, which drops a credential the environment no longer sets. The actor log line names the user id, not the email.
|
|
11
|
+
- Basic credentials are UTF-8 encoded, and a Swagger 2 `application` OAuth flow keeps its token URL.
|
|
12
|
+
- `pikku new addon` writes a `file:` path relative to each package when the app is not a workspace, and reports an auth.ts factory it cannot edit instead of half-wiring it.
|
|
13
|
+
- The inspector reads an addon from the package that declares it before the root.
|
|
14
|
+
- The dev credentials key file is created exclusively, so two `pikku dev` processes agree on one key.
|
|
15
|
+
|
|
16
|
+
- 86c2f1d: `pikku dev` keeps stored credentials in the dev database, so connected accounts and delegated sign-ins survive a restart. The key comes from `PIKKU_DEV_CREDENTIALS_KEY`, or is generated once into `.pikku-runtime/dev-credentials.key`. A project that declares a credential now gets the credential tables in its generated migration; without them dev falls back to the in-memory store and says so once.
|
|
17
|
+
- da9b931: An inline `input`/`output` schema (PKU489) is now an `error` diagnostic instead of a critical one, so `pikku dev` keeps running. The function is validated against its TypeScript type until the schema is extracted to an exported variable; `--fail-on-error` still blocks it.
|
|
18
|
+
- Updated dependencies [e84abd0]
|
|
19
|
+
- @pikku/core@0.12.121
|
|
20
|
+
|
|
21
|
+
## 0.12.87
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- a95179a: An AI agent's `providerOptions` now reaches the generated metadata.
|
|
26
|
+
|
|
27
|
+
The inspector read `model`, `temperature`, `maxSteps` and `toolChoice` off an agent declaration but never `providerOptions`, so `agentsMeta` always showed an agent with no provider configuration — even though the type says it carries one. Runs were unaffected, since the runner takes the value from the live agent object, which is exactly why the gap went unnoticed by everything except what reads the compiled meta: the console's agent view and `infra.json`.
|
|
28
|
+
|
|
29
|
+
The value is read as an object literal of strings, numbers, booleans, arrays and nested objects, all-or-nothing. A computed one is reported as `PKU156` rather than silently dropped.
|
|
30
|
+
|
|
31
|
+
- 145b32b: Generated type aliases are deterministic
|
|
32
|
+
|
|
33
|
+
A type name that collided across files got a `Math.random()` suffix, so every
|
|
34
|
+
`pikku all` emitted different names into `pikku-agent-map.gen.d.ts` and
|
|
35
|
+
`pikku-workflow-map.gen.d.ts`. Those files sit in the schema generator's
|
|
36
|
+
dependency closure, so rewriting them invalidated the schema cache partway
|
|
37
|
+
through the same run and forced a full `ts-json-schema-generator` pass each
|
|
38
|
+
time. The suffix now comes from the declaring file's path.
|
|
39
|
+
|
|
40
|
+
- 42b7ac3: The app decides which of an addon's functions `rpc.exposed` reaches
|
|
41
|
+
|
|
42
|
+
`wireAddon` takes `expose?: boolean | string[]`, mirroring `mcp`. Unset or
|
|
43
|
+
`true` keeps the functions the addon declared `expose: true`; `false` exposes
|
|
44
|
+
none of the instance's functions; a list names exactly the functions to expose,
|
|
45
|
+
whether or not the addon declared them, typed against the addon's function
|
|
46
|
+
names. A listed name the addon does not publish fails the build with PKU343, a value that is not written inline fails it with
|
|
47
|
+
PKU344,
|
|
48
|
+
and the deploy analyzer's per-addon unit carries only what the wiring exposes.
|
|
49
|
+
|
|
50
|
+
- Updated dependencies [4a9dcd2]
|
|
51
|
+
- Updated dependencies [5ab24ad]
|
|
52
|
+
- Updated dependencies [42b7ac3]
|
|
53
|
+
- @pikku/core@0.12.120
|
|
54
|
+
|
|
1
55
|
## 0.12.86
|
|
2
56
|
|
|
3
57
|
### Patch Changes
|
package/dist/add/add-agent.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import type { AddWiring, InspectorLogger } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads `providerOptions` off the declaration so it survives into `agentsMeta`.
|
|
5
|
+
*
|
|
6
|
+
* The runner takes the value from the live agent object, so an in-process run
|
|
7
|
+
* honours it either way — but everything that reads the compiled meta instead
|
|
8
|
+
* (the console's agent view, `infra.json`, the deploy analyzer) saw an agent
|
|
9
|
+
* with no provider configuration at all. A computed value is reported rather
|
|
10
|
+
* than dropped, because that is the case nobody notices.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveProviderOptions(obj: ts.ObjectLiteralExpression, agentName: string, logger: InspectorLogger): Record<string, Record<string, unknown>> | null;
|
|
3
13
|
export declare function resolveWorkflowReferences(obj: ts.ObjectLiteralExpression, checker: ts.TypeChecker, agentName: string, logger: InspectorLogger): string[] | null;
|
|
4
14
|
export declare const addAgent: AddWiring;
|
package/dist/add/add-agent.js
CHANGED
|
@@ -4,8 +4,33 @@ import { extractWireNames } from '../utils/post-process.js';
|
|
|
4
4
|
import { extractFunctionName, funcIdToTypeName, } from '../utils/extract-function-name.js';
|
|
5
5
|
import { resolveMiddleware, resolveChannelMiddleware, resolveAgentMiddleware, } from '../utils/middleware.js';
|
|
6
6
|
import { resolvePermissions } from '../utils/permissions.js';
|
|
7
|
+
import { getProperty, jsonObjectProperty } from '../utils/literal-properties.js';
|
|
7
8
|
import { ErrorCode } from '../error-codes.js';
|
|
8
9
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
|
|
10
|
+
/**
|
|
11
|
+
* Reads `providerOptions` off the declaration so it survives into `agentsMeta`.
|
|
12
|
+
*
|
|
13
|
+
* The runner takes the value from the live agent object, so an in-process run
|
|
14
|
+
* honours it either way — but everything that reads the compiled meta instead
|
|
15
|
+
* (the console's agent view, `infra.json`, the deploy analyzer) saw an agent
|
|
16
|
+
* with no provider configuration at all. A computed value is reported rather
|
|
17
|
+
* than dropped, because that is the case nobody notices.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveProviderOptions(obj, agentName, logger) {
|
|
20
|
+
if (!getProperty(obj, 'providerOptions')) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const providerOptions = jsonObjectProperty(obj, 'providerOptions');
|
|
24
|
+
if (!providerOptions) {
|
|
25
|
+
logger.diagnostic({
|
|
26
|
+
severity: 'warn',
|
|
27
|
+
code: ErrorCode.AGENT_PROVIDER_OPTIONS_UNREADABLE,
|
|
28
|
+
message: `AI agent '${agentName}' has a 'providerOptions' the inspector cannot read statically, so it is missing from the generated metadata. Inline it as an object literal of strings, numbers, booleans, arrays and nested objects.`,
|
|
29
|
+
});
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return providerOptions;
|
|
33
|
+
}
|
|
9
34
|
function resolveToolReferences(obj, checker, agentName, logger) {
|
|
10
35
|
const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
|
|
11
36
|
ts.isIdentifier(p.name) &&
|
|
@@ -267,6 +292,7 @@ export const addAgent = (logger, node, checker, state, options) => {
|
|
|
267
292
|
const maxStepsValue = getPropertyValue(obj, 'maxSteps');
|
|
268
293
|
const temperatureValue = getPropertyValue(obj, 'temperature');
|
|
269
294
|
const toolChoiceValue = getPropertyValue(obj, 'toolChoice');
|
|
295
|
+
const providerOptionsValue = resolveProviderOptions(obj, nameValue || '', logger);
|
|
270
296
|
const scorersValue = resolveScorerNames(obj, nameValue || '', logger);
|
|
271
297
|
const toolsValue = resolveToolReferences(obj, checker, nameValue || '', logger);
|
|
272
298
|
if (toolsValue) {
|
|
@@ -410,6 +436,9 @@ export const addAgent = (logger, node, checker, state, options) => {
|
|
|
410
436
|
...(toolChoiceValue !== null && {
|
|
411
437
|
toolChoice: toolChoiceValue,
|
|
412
438
|
}),
|
|
439
|
+
...(providerOptionsValue !== null && {
|
|
440
|
+
providerOptions: providerOptionsValue,
|
|
441
|
+
}),
|
|
413
442
|
...(toolsValue !== null && { tools: toolsValue }),
|
|
414
443
|
...(agentsValue !== null && { agents: agentsValue }),
|
|
415
444
|
...(workflowsValue !== null && { workflows: workflowsValue }),
|
|
@@ -629,12 +629,16 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
629
629
|
}
|
|
630
630
|
}
|
|
631
631
|
else if (ts.isCallExpression(prop.initializer)) {
|
|
632
|
-
// Bad - it's an inline expression
|
|
633
632
|
const schemaName = `${funcIdToTypeName(name)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
|
|
634
|
-
logger.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
`
|
|
633
|
+
logger.diagnostic({
|
|
634
|
+
severity: 'error',
|
|
635
|
+
code: ErrorCode.INLINE_SCHEMA,
|
|
636
|
+
message: `Inline schemas are not supported for '${propName}' in '${name}'; ` +
|
|
637
|
+
`it is validated against its TypeScript type only, so refinements are not enforced.\n` +
|
|
638
|
+
` Extract to an exported variable:\n` +
|
|
639
|
+
` export const ${schemaName} = ${prop.initializer.getText()}\n` +
|
|
640
|
+
` Then use: ${propName}: ${schemaName}`,
|
|
641
|
+
});
|
|
638
642
|
}
|
|
639
643
|
}
|
|
640
644
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { ErrorCode } from '../error-codes.js';
|
|
2
3
|
function parseStringArray(node) {
|
|
3
4
|
if (!ts.isArrayLiteralExpression(node))
|
|
4
5
|
return undefined;
|
|
@@ -95,6 +96,7 @@ export function addWireAddon(node, state, logger) {
|
|
|
95
96
|
let pkg;
|
|
96
97
|
let rpcEndpoint;
|
|
97
98
|
let mcp;
|
|
99
|
+
let expose;
|
|
98
100
|
let mcpEndpoint;
|
|
99
101
|
let auth;
|
|
100
102
|
let tags;
|
|
@@ -126,6 +128,18 @@ export function addWireAddon(node, state, logger) {
|
|
|
126
128
|
? prop.initializer.kind === ts.SyntaxKind.TrueKeyword
|
|
127
129
|
: parseStringArray(prop.initializer);
|
|
128
130
|
}
|
|
131
|
+
else if (key === 'expose') {
|
|
132
|
+
expose =
|
|
133
|
+
prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
134
|
+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword
|
|
135
|
+
? prop.initializer.kind === ts.SyntaxKind.TrueKeyword
|
|
136
|
+
: parseStringArray(prop.initializer);
|
|
137
|
+
// Read as unset, the deploy analyzer would build this addon's unit from
|
|
138
|
+
// the addon's own declarations while the runtime follows the real list.
|
|
139
|
+
if (expose === undefined) {
|
|
140
|
+
logger.critical(ErrorCode.ADDON_EXPOSE_NOT_STATIC, `wireAddon's expose must be true, false or an array of string literals, got: ${prop.initializer.getText()}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
129
143
|
else if (key === 'mcpEndpoint') {
|
|
130
144
|
if (ts.isStringLiteral(prop.initializer)) {
|
|
131
145
|
mcpEndpoint = prop.initializer.text;
|
|
@@ -186,6 +200,7 @@ export function addWireAddon(node, state, logger) {
|
|
|
186
200
|
rpcEndpoint,
|
|
187
201
|
mcp,
|
|
188
202
|
mcpEndpoint,
|
|
203
|
+
expose,
|
|
189
204
|
auth,
|
|
190
205
|
tags,
|
|
191
206
|
scopes,
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export declare enum ErrorCode {
|
|
|
34
34
|
REMOTE_ADDON_AUTH_UNRESOLVED = "PKU339",
|
|
35
35
|
ADDON_NOT_INSTALLED = "PKU340",
|
|
36
36
|
ADDON_MCP_FUNCTION_NOT_FOUND = "PKU341",
|
|
37
|
+
ADDON_EXPOSE_FUNCTION_NOT_FOUND = "PKU343",
|
|
38
|
+
ADDON_EXPOSE_NOT_STATIC = "PKU344",
|
|
37
39
|
DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
|
|
38
40
|
DB_JSON_COLUMN_UNTYPED = "PKU481",
|
|
39
41
|
DB_FORMAT_ON_NON_STRING = "PKU482",
|
|
@@ -67,6 +69,7 @@ export declare enum ErrorCode {
|
|
|
67
69
|
AGENT_TOOL_NOT_FOUND = "PKU153",
|
|
68
70
|
AGENT_TOOL_MISSING_DESCRIPTION = "PKU154",
|
|
69
71
|
AGENT_SCORER_NOT_FOUND = "PKU155",
|
|
72
|
+
AGENT_PROVIDER_OPTIONS_UNREADABLE = "PKU156",
|
|
70
73
|
SCHEMA_AND_WIRING_COLOCATED = "PKU490",
|
|
71
74
|
SCHEMA_TRANSFORM_NOT_SUPPORTED = "PKU491",
|
|
72
75
|
SERVICES_NOT_DESTRUCTURED = "PKU410",
|
package/dist/error-codes.js
CHANGED
|
@@ -37,6 +37,8 @@ export var ErrorCode;
|
|
|
37
37
|
ErrorCode["REMOTE_ADDON_AUTH_UNRESOLVED"] = "PKU339";
|
|
38
38
|
ErrorCode["ADDON_NOT_INSTALLED"] = "PKU340";
|
|
39
39
|
ErrorCode["ADDON_MCP_FUNCTION_NOT_FOUND"] = "PKU341";
|
|
40
|
+
ErrorCode["ADDON_EXPOSE_FUNCTION_NOT_FOUND"] = "PKU343";
|
|
41
|
+
ErrorCode["ADDON_EXPOSE_NOT_STATIC"] = "PKU344";
|
|
40
42
|
// Database schema codegen warnings
|
|
41
43
|
ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
|
|
42
44
|
ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
|
|
@@ -85,6 +87,8 @@ export var ErrorCode;
|
|
|
85
87
|
ErrorCode["AGENT_TOOL_MISSING_DESCRIPTION"] = "PKU154";
|
|
86
88
|
// Agent scorer reference errors
|
|
87
89
|
ErrorCode["AGENT_SCORER_NOT_FOUND"] = "PKU155";
|
|
90
|
+
// Agent provider configuration errors
|
|
91
|
+
ErrorCode["AGENT_PROVIDER_OPTIONS_UNREADABLE"] = "PKU156";
|
|
88
92
|
// File structure errors
|
|
89
93
|
ErrorCode["SCHEMA_AND_WIRING_COLOCATED"] = "PKU490";
|
|
90
94
|
// Schema authoring errors
|
package/dist/types-map.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ export declare class TypesMap {
|
|
|
6
6
|
}>;
|
|
7
7
|
addCustomType(name: string, type: string, references: string[]): void;
|
|
8
8
|
addType(originalName: string, path: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* A pure function of the path, with no dependence on insertion order: the
|
|
11
|
+
* alias lands in generated `.d.ts` maps, and anything that varies between
|
|
12
|
+
* runs rewrites those files, which invalidates the schema cache mid-`pikku
|
|
13
|
+
* all` and costs a full ts-json-schema-generator pass each time. A counter
|
|
14
|
+
* over already-taken names would reintroduce that through traversal order.
|
|
15
|
+
*/
|
|
9
16
|
addUniqueType(originalName: string, path: string): string;
|
|
10
17
|
getUniqueName(name: string): string;
|
|
11
18
|
getTypeMeta(name: string): {
|
package/dist/types-map.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
export class TypesMap {
|
|
2
3
|
map = new Map();
|
|
3
4
|
customTypes = new Map();
|
|
@@ -7,8 +8,16 @@ export class TypesMap {
|
|
|
7
8
|
addType(originalName, path) {
|
|
8
9
|
this.map.set(originalName, { originalName, path });
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A pure function of the path, with no dependence on insertion order: the
|
|
13
|
+
* alias lands in generated `.d.ts` maps, and anything that varies between
|
|
14
|
+
* runs rewrites those files, which invalidates the schema cache mid-`pikku
|
|
15
|
+
* all` and costs a full ts-json-schema-generator pass each time. A counter
|
|
16
|
+
* over already-taken names would reintroduce that through traversal order.
|
|
17
|
+
*/
|
|
10
18
|
addUniqueType(originalName, path) {
|
|
11
|
-
const
|
|
19
|
+
const digest = createHash('sha256').update(path).digest('hex').slice(0, 12);
|
|
20
|
+
const uniqueName = `${originalName}_${digest}`;
|
|
12
21
|
this.map.set(uniqueName, { originalName, path });
|
|
13
22
|
return uniqueName;
|
|
14
23
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -609,6 +609,13 @@ export interface InspectorState {
|
|
|
609
609
|
* endpoint, which is where they have always been.
|
|
610
610
|
*/
|
|
611
611
|
mcpEndpoint?: boolean | string;
|
|
612
|
+
/**
|
|
613
|
+
* Which functions `rpc.exposed` may call under `<name>:<function>`.
|
|
614
|
+
* Unset or `true` keeps what the addon declared `expose: true`; `false`
|
|
615
|
+
* exposes none; a list names exactly the functions to expose, whatever
|
|
616
|
+
* the addon declared.
|
|
617
|
+
*/
|
|
618
|
+
expose?: boolean | string[];
|
|
612
619
|
/**
|
|
613
620
|
* The addon's own gates. `runPikkuFunc` applies these to every function
|
|
614
621
|
* in the package, on every wiring path — not just `namespace:function`.
|
|
@@ -20,3 +20,14 @@ export declare const booleanProperty: (config: ts.ObjectLiteralExpression, name:
|
|
|
20
20
|
export declare const stringArrayProperty: (config: ts.ObjectLiteralExpression, name: string) => string[] | undefined;
|
|
21
21
|
/** Unwraps `x as const` / `x satisfies T` so a cast is still extracted. */
|
|
22
22
|
export declare const unwrapAs: (node: ts.Expression) => ts.Expression;
|
|
23
|
+
/**
|
|
24
|
+
* Reads a property whose value is a JSON-shaped object literal — nested
|
|
25
|
+
* objects, arrays, strings, numbers, booleans and `null`.
|
|
26
|
+
*
|
|
27
|
+
* All-or-nothing for the same reason as `stringArrayProperty`, and then some:
|
|
28
|
+
* these values are handed to a provider verbatim, so a half-read
|
|
29
|
+
* `providerOptions` would send the model a *different* configuration from the
|
|
30
|
+
* one in source rather than none. `undefined` for anything computed lets the
|
|
31
|
+
* caller say so instead of guessing.
|
|
32
|
+
*/
|
|
33
|
+
export declare const jsonObjectProperty: (config: ts.ObjectLiteralExpression, name: string) => Record<string, unknown> | undefined;
|
|
@@ -67,3 +67,82 @@ export const stringArrayProperty = (config, name) => {
|
|
|
67
67
|
export const unwrapAs = (node) => ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
|
|
68
68
|
? unwrapAs(node.expression)
|
|
69
69
|
: node;
|
|
70
|
+
/**
|
|
71
|
+
* Reads a property whose value is a JSON-shaped object literal — nested
|
|
72
|
+
* objects, arrays, strings, numbers, booleans and `null`.
|
|
73
|
+
*
|
|
74
|
+
* All-or-nothing for the same reason as `stringArrayProperty`, and then some:
|
|
75
|
+
* these values are handed to a provider verbatim, so a half-read
|
|
76
|
+
* `providerOptions` would send the model a *different* configuration from the
|
|
77
|
+
* one in source rather than none. `undefined` for anything computed lets the
|
|
78
|
+
* caller say so instead of guessing.
|
|
79
|
+
*/
|
|
80
|
+
export const jsonObjectProperty = (config, name) => {
|
|
81
|
+
const value = getProperty(config, name);
|
|
82
|
+
if (!value) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const read = readJsonValue(value);
|
|
86
|
+
return read.ok && isPlainRecord(read.value) ? read.value : undefined;
|
|
87
|
+
};
|
|
88
|
+
const isPlainRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
89
|
+
const UNREADABLE = { ok: false };
|
|
90
|
+
const readJsonValue = (node) => {
|
|
91
|
+
const value = unwrapAs(node);
|
|
92
|
+
if (ts.isStringLiteralLike(value)) {
|
|
93
|
+
return { ok: true, value: value.text };
|
|
94
|
+
}
|
|
95
|
+
if (ts.isNumericLiteral(value)) {
|
|
96
|
+
return { ok: true, value: Number(value.text) };
|
|
97
|
+
}
|
|
98
|
+
if (ts.isPrefixUnaryExpression(value) &&
|
|
99
|
+
value.operator === ts.SyntaxKind.MinusToken &&
|
|
100
|
+
ts.isNumericLiteral(value.operand)) {
|
|
101
|
+
return { ok: true, value: -Number(value.operand.text) };
|
|
102
|
+
}
|
|
103
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) {
|
|
104
|
+
return { ok: true, value: true };
|
|
105
|
+
}
|
|
106
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword) {
|
|
107
|
+
return { ok: true, value: false };
|
|
108
|
+
}
|
|
109
|
+
if (value.kind === ts.SyntaxKind.NullKeyword) {
|
|
110
|
+
return { ok: true, value: null };
|
|
111
|
+
}
|
|
112
|
+
if (ts.isArrayLiteralExpression(value)) {
|
|
113
|
+
const entries = [];
|
|
114
|
+
for (const element of value.elements) {
|
|
115
|
+
const read = readJsonValue(element);
|
|
116
|
+
if (!read.ok) {
|
|
117
|
+
return UNREADABLE;
|
|
118
|
+
}
|
|
119
|
+
entries.push(read.value);
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, value: entries };
|
|
122
|
+
}
|
|
123
|
+
if (ts.isObjectLiteralExpression(value)) {
|
|
124
|
+
const record = {};
|
|
125
|
+
for (const property of value.properties) {
|
|
126
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
127
|
+
return UNREADABLE;
|
|
128
|
+
}
|
|
129
|
+
const key = ts.isIdentifier(property.name)
|
|
130
|
+
? property.name.text
|
|
131
|
+
: ts.isStringLiteralLike(property.name)
|
|
132
|
+
? property.name.text
|
|
133
|
+
: ts.isNumericLiteral(property.name)
|
|
134
|
+
? property.name.text
|
|
135
|
+
: null;
|
|
136
|
+
if (key === null) {
|
|
137
|
+
return UNREADABLE;
|
|
138
|
+
}
|
|
139
|
+
const read = readJsonValue(property.initializer);
|
|
140
|
+
if (!read.ok) {
|
|
141
|
+
return UNREADABLE;
|
|
142
|
+
}
|
|
143
|
+
record[key] = read.value;
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, value: record };
|
|
146
|
+
}
|
|
147
|
+
return UNREADABLE;
|
|
148
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InspectorState, InspectorLogger } from '../types.js';
|
|
2
|
+
export declare const describeMissingAddonMeta: (namespace: string, packageName: string, dirs: string[], declFile?: string) => string;
|
|
2
3
|
/**
|
|
3
4
|
* After the setup sweep discovers wireAddon() declarations, load each addon
|
|
4
5
|
* package's function metadata so that wiring handlers (channels, HTTP routes,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { readFile, readdir } from 'fs/promises';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
|
-
import { join, dirname } from 'path';
|
|
4
|
+
import { join, dirname, parse, relative } from 'path';
|
|
5
5
|
import { ErrorCode } from '../error-codes.js';
|
|
6
6
|
const isHTTPRouteConfig = (value) => typeof value === 'object' &&
|
|
7
7
|
value !== null &&
|
|
@@ -90,6 +90,77 @@ const resolveAddonMeta = (require, packageName, baseName) => {
|
|
|
90
90
|
}
|
|
91
91
|
return null;
|
|
92
92
|
};
|
|
93
|
+
const nearestPackageDir = (file) => {
|
|
94
|
+
const root = parse(file).root;
|
|
95
|
+
let dir = dirname(file);
|
|
96
|
+
while (dir && dir !== root) {
|
|
97
|
+
if (existsSync(join(dir, 'package.json')))
|
|
98
|
+
return dir;
|
|
99
|
+
const parent = dirname(dir);
|
|
100
|
+
if (parent === dir)
|
|
101
|
+
break;
|
|
102
|
+
dir = parent;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* An addon is installed wherever the package that calls `wireAddon` lists it,
|
|
108
|
+
* which in a workspace is usually not the repo root. Resolve from the declaring
|
|
109
|
+
* package first, as its code does at runtime, then from the root.
|
|
110
|
+
*/
|
|
111
|
+
const addonResolutionDirs = (rootDir, declFile) => {
|
|
112
|
+
const declDir = declFile ? nearestPackageDir(declFile) : null;
|
|
113
|
+
return declDir && declDir !== rootDir ? [declDir, rootDir] : [rootDir];
|
|
114
|
+
};
|
|
115
|
+
const createAddonResolver = (dirs) => {
|
|
116
|
+
const requires = dirs.map((dir) => createRequire(join(dir, 'package.json')));
|
|
117
|
+
return {
|
|
118
|
+
resolve: (id) => {
|
|
119
|
+
let lastError;
|
|
120
|
+
for (const req of requires) {
|
|
121
|
+
try {
|
|
122
|
+
return req.resolve(id);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
lastError = e;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw lastError;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const findInstalledPackageDir = (dirs, packageName) => {
|
|
133
|
+
for (const start of dirs) {
|
|
134
|
+
const root = parse(start).root;
|
|
135
|
+
let dir = start;
|
|
136
|
+
while (dir) {
|
|
137
|
+
const candidate = join(dir, 'node_modules', packageName);
|
|
138
|
+
if (existsSync(join(candidate, 'package.json')))
|
|
139
|
+
return candidate;
|
|
140
|
+
if (dir === root)
|
|
141
|
+
break;
|
|
142
|
+
const parent = dirname(dir);
|
|
143
|
+
if (parent === dir)
|
|
144
|
+
break;
|
|
145
|
+
dir = parent;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
export const describeMissingAddonMeta = (namespace, packageName, dirs, declFile) => {
|
|
151
|
+
const caller = declFile
|
|
152
|
+
? relative(dirs[dirs.length - 1], declFile) || declFile
|
|
153
|
+
: undefined;
|
|
154
|
+
const declaredIn = caller ? ` (declared in ${caller})` : '';
|
|
155
|
+
const packageDir = findInstalledPackageDir(dirs, packageName);
|
|
156
|
+
if (!packageDir) {
|
|
157
|
+
const declaringPackage = dirs[0];
|
|
158
|
+
return (`wireAddon('${namespace}')${declaredIn} names ${packageName}, which is not installed where it can be resolved — tried ${dirs.join(', ')}. ` +
|
|
159
|
+
`Add "${packageName}": "workspace:*" (or a version) to the dependencies of ${join(declaringPackage, 'package.json')} and run install.`);
|
|
160
|
+
}
|
|
161
|
+
return (`wireAddon('${namespace}')${declaredIn}: ${packageName} is installed at ${packageDir} but has not been built — ` +
|
|
162
|
+
`its dist/.pikku/addon/function/pikku-functions-meta.gen.json does not exist. Run the addon's build (pikku all && tsc && pikku dist) in ${packageDir}.`);
|
|
163
|
+
};
|
|
93
164
|
/**
|
|
94
165
|
* Give the addon's input/output type names a declaration file to be imported
|
|
95
166
|
* from, so a wiring that references an addon function can name its contract.
|
|
@@ -128,21 +199,24 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
128
199
|
const { wireAddonDeclarations } = state.rpc;
|
|
129
200
|
if (wireAddonDeclarations.size === 0)
|
|
130
201
|
return;
|
|
131
|
-
const require = createRequire(join(state.rootDir, 'package.json'));
|
|
132
202
|
for (const [namespace, decl] of wireAddonDeclarations) {
|
|
133
203
|
// Remote addons (wireRemoteAddon) ship as a devDependency: types only.
|
|
134
204
|
// Their functions, secrets, variables, schemas and services live on the
|
|
135
205
|
// HOST that runs them — never load or require any of that here.
|
|
136
206
|
if (decl.remote)
|
|
137
207
|
continue;
|
|
208
|
+
const dirs = addonResolutionDirs(state.rootDir, decl.file);
|
|
209
|
+
const require = createAddonResolver(dirs);
|
|
138
210
|
try {
|
|
139
211
|
// Verbose first. `description` is one of the fields stripped from the
|
|
140
212
|
// minimal copy, and it is what an addon's function is offered to a model
|
|
141
213
|
// under — both as an MCP tool below and as an agent tool. Read the minimal
|
|
142
214
|
// one and every addon function looks undescribed.
|
|
143
215
|
const metaPath = resolveAddonMeta(require, decl.package, 'function/pikku-functions-meta');
|
|
144
|
-
if (!metaPath)
|
|
145
|
-
|
|
216
|
+
if (!metaPath) {
|
|
217
|
+
logger.warn(describeMissingAddonMeta(namespace, decl.package, dirs, decl.file));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
146
220
|
const raw = await readFile(metaPath, 'utf-8');
|
|
147
221
|
const meta = JSON.parse(raw);
|
|
148
222
|
state.addonFunctions[namespace] = meta;
|
|
@@ -188,6 +262,15 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
188
262
|
}
|
|
189
263
|
}
|
|
190
264
|
}
|
|
265
|
+
// Same reasoning as mcp: a listed name the addon does not publish is a
|
|
266
|
+
// function the app believes callable and never is.
|
|
267
|
+
if (Array.isArray(decl.expose)) {
|
|
268
|
+
for (const funcName of decl.expose) {
|
|
269
|
+
if (!Object.hasOwn(meta, funcName)) {
|
|
270
|
+
logger.critical(ErrorCode.ADDON_EXPOSE_FUNCTION_NOT_FOUND, `wireAddon('${namespace}') lists '${funcName}' under expose, but ${decl.package} publishes no such function.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
191
274
|
// Load addon secrets meta
|
|
192
275
|
try {
|
|
193
276
|
const secretsMetaPath = require.resolve(`${decl.package}/.pikku/secrets/pikku-secrets-meta.gen.json`);
|
|
@@ -366,11 +449,11 @@ export async function loadAddonSchemas(logger, state) {
|
|
|
366
449
|
const { wireAddonDeclarations } = state.rpc;
|
|
367
450
|
if (wireAddonDeclarations.size === 0)
|
|
368
451
|
return;
|
|
369
|
-
const require = createRequire(join(state.rootDir, 'package.json'));
|
|
370
452
|
for (const [namespace, decl] of wireAddonDeclarations) {
|
|
371
453
|
// Remote addons carry no local schemas — their funcs run on the host.
|
|
372
454
|
if (decl.remote)
|
|
373
455
|
continue;
|
|
456
|
+
const require = createAddonResolver(addonResolutionDirs(state.rootDir, decl.file));
|
|
374
457
|
try {
|
|
375
458
|
const metaPath = require.resolve(`${decl.package}/.pikku/function/pikku-functions-meta.gen.json`);
|
|
376
459
|
const schemasDir = join(dirname(metaPath), '..', 'schemas', 'schemas');
|
|
@@ -391,6 +474,10 @@ export async function loadAddonSchemas(logger, state) {
|
|
|
391
474
|
}
|
|
392
475
|
}
|
|
393
476
|
catch (error) {
|
|
477
|
+
if (error?.code === 'MODULE_NOT_FOUND' ||
|
|
478
|
+
error?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
394
481
|
logger.warn(`Failed to load addon schemas for '${namespace}' (${decl.package}): ${error.message}`);
|
|
395
482
|
}
|
|
396
483
|
}
|
|
@@ -287,6 +287,9 @@ export function aggregateRequiredServices(state) {
|
|
|
287
287
|
if ((state.analytics?.length ?? 0) > 0) {
|
|
288
288
|
requiredServices.add('analyticsService');
|
|
289
289
|
}
|
|
290
|
+
if ((state.credentials?.definitions?.length ?? 0) > 0) {
|
|
291
|
+
requiredServices.add('credentialService');
|
|
292
|
+
}
|
|
290
293
|
// 7. Services that consumed addons need from the parent project.
|
|
291
294
|
const addonFnServices = new Map();
|
|
292
295
|
for (const [namespace, fns] of Object.entries(state.addonFunctions ?? {})) {
|
|
@@ -1146,6 +1149,10 @@ export function validateSecretUsage(logger, state) {
|
|
|
1146
1149
|
declared.add(definition.oauth2.tokenSecretId);
|
|
1147
1150
|
}
|
|
1148
1151
|
}
|
|
1152
|
+
// auth-secrets.gen.ts declares it, but that file is written after the first
|
|
1153
|
+
// inspection of a fresh project, so its defineSecret is not seen yet.
|
|
1154
|
+
if (state.auth?.definition)
|
|
1155
|
+
declared.add('BETTER_AUTH_SECRET');
|
|
1149
1156
|
for (const [file, usage] of state.secrets.usage) {
|
|
1150
1157
|
const relativeFile = relative(state.rootDir, file);
|
|
1151
1158
|
const undeclared = usage.keys.filter((key) => !declared.has(key));
|
|
@@ -16,28 +16,32 @@ export function validateExposedFunctionsGated(logger, state) {
|
|
|
16
16
|
for (const [funcName, meta] of Object.entries(state.functions.meta)) {
|
|
17
17
|
if (!meta.expose)
|
|
18
18
|
continue;
|
|
19
|
-
|
|
20
|
-
if (meta.sessionless === false)
|
|
19
|
+
if (gatesItself(meta))
|
|
21
20
|
continue;
|
|
22
|
-
|
|
23
|
-
if (meta.scenarioStep)
|
|
24
|
-
continue;
|
|
25
|
-
// The author declared that the body authorizes its own callers — a gate
|
|
26
|
-
// this check has no way to see. Taking the claim at face value is the
|
|
27
|
-
// point: without it the warning fires forever on functions that are fine,
|
|
28
|
-
// and a warning that is usually wrong stops being read.
|
|
29
|
-
if (meta.permissionsInBody === true)
|
|
30
|
-
continue;
|
|
31
|
-
// The function gates itself.
|
|
32
|
-
if (meta.auth === true)
|
|
21
|
+
if (isGatedByItsAddon(state, meta.packageName))
|
|
33
22
|
continue;
|
|
34
|
-
|
|
23
|
+
ungated.push(funcName);
|
|
24
|
+
}
|
|
25
|
+
// A `wireAddon` expose list can open an addon function its author never
|
|
26
|
+
// exposed, so the app that listed it is the one to tell. The namespaced call
|
|
27
|
+
// is governed by that instance's config alone.
|
|
28
|
+
//
|
|
29
|
+
// knowledge: decisions/security/wire-addon-expose-selects-the-rpc-surface.md
|
|
30
|
+
for (const [namespace, declaration] of state.rpc?.wireAddonDeclarations ??
|
|
31
|
+
[]) {
|
|
32
|
+
if (!Array.isArray(declaration.expose))
|
|
35
33
|
continue;
|
|
36
|
-
if (
|
|
34
|
+
if (declaration.auth === true)
|
|
37
35
|
continue;
|
|
38
|
-
if (
|
|
36
|
+
if (declaration.scopes && declaration.scopes.length > 0)
|
|
39
37
|
continue;
|
|
40
|
-
|
|
38
|
+
const addonMeta = state.addonFunctions?.[namespace] ?? {};
|
|
39
|
+
for (const funcName of declaration.expose) {
|
|
40
|
+
const meta = addonMeta[funcName];
|
|
41
|
+
if (!meta || gatesItself(meta))
|
|
42
|
+
continue;
|
|
43
|
+
ungated.push(`${namespace}:${funcName}`);
|
|
44
|
+
}
|
|
41
45
|
}
|
|
42
46
|
if (ungated.length === 0)
|
|
43
47
|
return;
|
|
@@ -64,6 +68,19 @@ export function validateExposedFunctionsGated(logger, state) {
|
|
|
64
68
|
`${one ? 'it was' : 'they were'} not meant to be callable from outside.`,
|
|
65
69
|
});
|
|
66
70
|
}
|
|
71
|
+
const gatesItself = (meta) =>
|
|
72
|
+
// A pikkuFunc always requires a session, on every path.
|
|
73
|
+
meta.sessionless === false ||
|
|
74
|
+
// Scenario steps are refused by rpcExposed regardless of `expose`.
|
|
75
|
+
meta.scenarioStep === true ||
|
|
76
|
+
// The author declared that the body authorizes its own callers — a gate
|
|
77
|
+
// this check has no way to see. Taking the claim at face value is the
|
|
78
|
+
// point: without it the warning fires forever on functions that are fine,
|
|
79
|
+
// and a warning that is usually wrong stops being read.
|
|
80
|
+
meta.permissionsInBody === true ||
|
|
81
|
+
meta.auth === true ||
|
|
82
|
+
(meta.scopes?.length ?? 0) > 0 ||
|
|
83
|
+
(meta.permissions?.length ?? 0) > 0;
|
|
67
84
|
/**
|
|
68
85
|
* Whether every function in this package is already gated by its `wireAddon`.
|
|
69
86
|
* Those gates are applied by `runPikkuFunc` on every wiring path, so a function
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.88",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"zod-to-ts": "^2.1.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@pikku/core": "^0.12.
|
|
58
|
+
"@pikku/core": "^0.12.121"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@pikku/core": "^0.12.
|
|
61
|
+
"@pikku/core": "^0.12.121",
|
|
62
62
|
"@types/node": "^24.13.3"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|