@pikku/core 0.12.122 → 0.12.123

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 CHANGED
@@ -1,3 +1,42 @@
1
+ ## 0.12.123
2
+
3
+ ### Patch Changes
4
+
5
+ - 50b59a3: Keep the agent runtime out of deployment units that hold no agent.
6
+
7
+ `ContextAwareRPCService.agent` imported `agent-rpc.ts` directly, and every unit
8
+ reaches that class through the function runner, so the agent runner, stream,
9
+ memory and AGUI modules were pinned into every bundle — 51.8 KB raw / 15.7 KB
10
+ gzip a unit. The facade is now resolved through state, registered by
11
+ `@pikku/core/agent` on import, so only a unit that actually holds an agent
12
+ bundles it. A unit that reaches `rpc.agent` without importing the agent entry
13
+ point throws rather than silently pulling the runtime back in.
14
+
15
+ - 2b946e9: fix(dev): keep `import.meta` pointing at the real file in the hot-reload runner
16
+
17
+ The dev module runner transforms each user file to `cjs` before compiling it,
18
+ and esbuild's `cjs` output rewrites `import.meta` to an empty object. Any module
19
+ that resolves its own neighbours through `createRequire(import.meta.url)` —
20
+ sharp, onnxruntime-node, essentially every package with a native binding —
21
+ therefore received `undefined` and failed with `Cannot find module
22
+ '@img/sharp-linux-x64/sharp.node' from ''`. sharp's own loader filters on
23
+ `MODULE_NOT_FOUND` and reports its generic "could not load the sharp module"
24
+ instead, so the empty referrer never appears in the error the developer sees.
25
+
26
+ `import.meta.url`, `import.meta.filename` and `import.meta.dirname` are now
27
+ defined to the file being run, so resolution behaves as it does under Node's
28
+ ESM loader.
29
+
30
+ - bc488cf: fix(inspector): an explicit `auth: false` declares an exposed sessionless function public, so PKU574 no longer warns about it
31
+
32
+ A genuinely public endpoint — a published programme, a health check — had no
33
+ honest way to quiet PKU574: the only options were an always-true permission or
34
+ `permissionsInBody: true`, both of which claim a gate that does not exist. The
35
+ inspector now records `auth` on function meta exactly as written instead of
36
+ dropping `false`, and the check treats an explicit `auth: false` as the author
37
+ declaring the function public on purpose. A sessionless function that leaves
38
+ `auth` out still warns.
39
+
1
40
  ## 0.12.122
2
41
 
3
42
  ### Patch Changes
@@ -68,6 +68,15 @@ const evaluateModule = (absPath, cache) => {
68
68
  loader: loaderFor(absPath),
69
69
  format: 'cjs',
70
70
  sourcefile: absPath,
71
+ // `cjs` output rewrites `import.meta` to an empty object, so a module
72
+ // that resolves its own neighbours through `createRequire(import.meta.url)`
73
+ // -- sharp, onnxruntime-node, any package with a native binding -- gets
74
+ // `undefined` and fails with "Cannot find module ... from ''".
75
+ define: {
76
+ 'import.meta.url': JSON.stringify(pathToFileURL(absPath).href),
77
+ 'import.meta.filename': JSON.stringify(absPath),
78
+ 'import.meta.dirname': JSON.stringify(dirname(absPath)),
79
+ },
71
80
  });
72
81
  const fn = compileFunction(code, ['require', 'exports', 'module', '__filename', '__dirname'], { filename: absPath });
73
82
  const moduleObj = { exports: {} };
@@ -29,10 +29,11 @@ export type FunctionRuntimeMeta = {
29
29
  featureFlag?: string;
30
30
  expose?: boolean;
31
31
  /**
32
- * A sessionless function's own `auth: true`. `sessionless` carries the
33
- * baseline — a `pikkuFunc` always requires a session — and this carries the
34
- * tightening a `pikkuSessionlessFunc` applies to itself. Both are needed to
35
- * know whether a function is gated without running it.
32
+ * A sessionless function's own `auth`, as written. `sessionless` carries the
33
+ * baseline — a `pikkuFunc` always requires a session — and this carries what
34
+ * a `pikkuSessionlessFunc` declares about itself: `true` requires a session,
35
+ * `false` says it is public on purpose, absent says nothing. All three are
36
+ * needed to know whether an exposed function is gated without running it.
36
37
  */
37
38
  auth?: boolean;
38
39
  /**
@@ -6,7 +6,7 @@ import type { CoreChannel, ChannelsMeta } from '../wirings/channel/channel.types
6
6
  import type { CLIMeta, CLIProgramState } from '../wirings/cli/cli.types.js';
7
7
  import type { HTTPMethod, CoreHTTPFunctionWiring, HTTPWiringsMeta } from '../wirings/http/http.types.js';
8
8
  import type { CoreMCPResource, MCPResourceMeta, MCPToolMeta, CoreMCPPrompt, MCPPromptMeta } from '../wirings/mcp/mcp.types.js';
9
- import type { CoreAgent, AgentsMeta } from '../wirings/agent/agent.types.js';
9
+ import type { CoreAgent, AgentsMeta, AgentRPCFactory } from '../wirings/agent/agent.types.js';
10
10
  import type { PikkuAgentScorer, ScorerMeta } from '../wirings/agent-scorer/agent-scorer.types.js';
11
11
  import type { CoreGateway, GatewaysMeta } from '../wirings/gateway/gateway.types.js';
12
12
  import type { CoreQueueWorker, QueueWorkersMeta } from '../wirings/queue/queue.types.js';
@@ -116,6 +116,11 @@ export interface PikkuPackageState {
116
116
  scorersMeta: ScorerMeta;
117
117
  /** Alias -> `provider/model`, from the `models` table in pikku.config.json. */
118
118
  modelAliases: Record<string, string>;
119
+ /**
120
+ * Set by `@pikku/core/agent` on import. Absent in a deployment unit that
121
+ * holds no agent, which is what keeps the agent runtime out of its bundle.
122
+ */
123
+ rpcFactory?: AgentRPCFactory;
119
124
  };
120
125
  gateway: {
121
126
  gateways: Map<string, CoreGateway>;
@@ -1,10 +1,7 @@
1
1
  import type { PikkuRawWire } from '../../types/core.types.js';
2
- import type { SessionService } from '../../services/user-session-service.js';
3
- import type { CoreUserSession } from '../../types/core.types.js';
4
2
  import type { PikkuRPC } from '../rpc/rpc-types.js';
5
- export type AgentRPCOptions = {
6
- sessionService?: SessionService<CoreUserSession>;
7
- };
3
+ import type { AgentRPCOptions } from './agent.types.js';
4
+ export type { AgentRPCOptions };
8
5
  /**
9
6
  * `wire.rpc.agent`, implemented.
10
7
  *
@@ -1,3 +1,4 @@
1
+ import { pikkuState } from '../../pikku-state.js';
1
2
  import { runAgent, resumeAgentSync } from './agent-runner.js';
2
3
  import { streamAgent, resumeAgent, interruptAgent } from './agent-stream.js';
3
4
  import { wrapChannelWithAGUI } from './agent-agui.js';
@@ -51,3 +52,12 @@ export const createAgentRPC = (wire, options) => {
51
52
  approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAgentSync(runId, approvals, { sessionService: options.sessionService }, expectedAgentName)),
52
53
  };
53
54
  };
55
+ /**
56
+ * The RPC primitive offers `rpc.agent` but must not import this file — a static
57
+ * reference from `rpc-runner` pins the whole agent runtime into every
58
+ * deployment unit, including the ones that hold no agent. Registering the
59
+ * factory here inverts that: only a unit that imports `@pikku/core/agent` pays
60
+ * for it, which is the same seam the inspector already uses to decide whether
61
+ * a unit requires the agent services.
62
+ */
63
+ pikkuState(null, 'agent', 'rpcFactory', createAgentRPC);
@@ -6,6 +6,19 @@ import type { AIProviderOptions } from '../../services/agent-runner-service.js';
6
6
  import type { PikkuChannel } from '../channel/channel.types.js';
7
7
  import type { CorePikkuChannelMiddleware } from '../channel/channel.types.js';
8
8
  import type { ApprovalPolicy } from '../channel/channel-rpc.js';
9
+ import type { PikkuRawWire } from '../../types/core.types.js';
10
+ import type { PikkuRPC } from '../rpc/rpc-types.js';
11
+ import type { SessionService } from '../../services/user-session-service.js';
12
+ import type { CoreUserSession } from '../../types/core.types.js';
13
+ export type AgentRPCOptions = {
14
+ sessionService?: SessionService<CoreUserSession>;
15
+ };
16
+ /**
17
+ * Builds `wire.rpc.agent`. Registered by `agent-rpc.ts` when a unit imports
18
+ * `@pikku/core/agent`, so the RPC primitive can offer the agent surface
19
+ * without naming the agent runtime.
20
+ */
21
+ export type AgentRPCFactory = (wire: PikkuRawWire, options: AgentRPCOptions) => PikkuRPC['agent'];
9
22
  export interface AgentThread {
10
23
  id: string;
11
24
  resourceId: string;
@@ -1,4 +1,11 @@
1
+ /**
2
+ * Registers `wire.rpc.agent`. Imported for the side effect alone so the RPC
3
+ * primitive can offer the agent surface without naming the agent runtime, which
4
+ * would pin it into every deployment unit.
5
+ */
6
+ import './agent-rpc.js';
1
7
  export { agent, agentStream, agentResume, agentApprove, agentInterrupt, } from './agent-helpers.js';
8
+ export type { AgentRPCOptions, AgentRPCFactory } from './agent.types.js';
2
9
  export { wrapChannelWithAGUI } from './agent-agui.js';
3
10
  export { runAgent, resumeAgentSync } from './agent-runner.js';
4
11
  export { resolveModelAlias } from './agent-model-config.js';
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Registers `wire.rpc.agent`. Imported for the side effect alone so the RPC
3
+ * primitive can offer the agent surface without naming the agent runtime, which
4
+ * would pin it into every deployment unit.
5
+ */
6
+ import './agent-rpc.js';
1
7
  export { agent, agentStream, agentResume, agentApprove, agentInterrupt, } from './agent-helpers.js';
2
8
  export { wrapChannelWithAGUI } from './agent-agui.js';
3
9
  export { runAgent, resumeAgentSync } from './agent-runner.js';
@@ -52,9 +52,12 @@ export declare class ContextAwareRPCService {
52
52
  /**
53
53
  * The agent facade, built on access.
54
54
  *
55
- * The implementation lives in `agent/agent-rpc.ts` so the agent surface is
56
- * one file rather than a wing of this one; a getter rather than a field so a
57
- * request that never touches an agent never builds it.
55
+ * Resolved through state rather than imported: naming `agent-rpc.ts` here
56
+ * pinned the agent runtime — runner, stream, memory, AGUI — into every
57
+ * deployment unit, because every unit reaches this file through
58
+ * `function-runner`. `@pikku/core/agent` registers the factory on import, so
59
+ * a unit that holds an agent has one and a unit that does not never bundles
60
+ * the tree behind it.
58
61
  */
59
62
  get agent(): PikkuRPC['agent'];
60
63
  remote<In = any, Out = any>(funcName: string, data: In): Promise<Out>;
@@ -5,7 +5,6 @@ import { pikkuState } from '../../pikku-state.js';
5
5
  import { PikkuError, addError } from '../../errors/error-handler.js';
6
6
  import { parseVersionedId } from '../../version.js';
7
7
  import { resolveRemoteAddonToken } from '../addon/remote-addon-auth.js';
8
- import { createAgentRPC } from '../agent/agent-rpc.js';
9
8
  /**
10
9
  * The session for a wire: read through `getSession` when a runner attached one,
11
10
  * otherwise whatever was placed on the wire directly.
@@ -330,11 +329,18 @@ export class ContextAwareRPCService {
330
329
  /**
331
330
  * The agent facade, built on access.
332
331
  *
333
- * The implementation lives in `agent/agent-rpc.ts` so the agent surface is
334
- * one file rather than a wing of this one; a getter rather than a field so a
335
- * request that never touches an agent never builds it.
332
+ * Resolved through state rather than imported: naming `agent-rpc.ts` here
333
+ * pinned the agent runtime — runner, stream, memory, AGUI — into every
334
+ * deployment unit, because every unit reaches this file through
335
+ * `function-runner`. `@pikku/core/agent` registers the factory on import, so
336
+ * a unit that holds an agent has one and a unit that does not never bundles
337
+ * the tree behind it.
336
338
  */
337
339
  get agent() {
340
+ const createAgentRPC = pikkuState(null, 'agent', 'rpcFactory');
341
+ if (!createAgentRPC) {
342
+ throw new Error('Agent runtime not available: import @pikku/core/agent in this deployment unit to use rpc.agent');
343
+ }
338
344
  return createAgentRPC(this.wire, this.options);
339
345
  }
340
346
  async remote(funcName, data) {
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: decision
3
+ title: A public function says so with auth: false
4
+ description: An exposed sessionless function that writes auth: false is public on purpose and PKU574 stays quiet; one that leaves auth out still warns
5
+ tags: authorization, inspector, rpc
6
+ ---
7
+
8
+ # A public function says so with auth: false
9
+
10
+ PKU574 warns about an exposed sessionless function with no gate. Some of those
11
+ are exactly what the author meant: a published programme, a price list, a health
12
+ check. Before this, the only way to quiet the warning on one was a gate that
13
+ gated nothing — an always-true permission, or `permissionsInBody: true`, which
14
+ claims a check the body does not make. Both put a false statement into meta to
15
+ silence a true one, and the alternative, living with the warning, teaches
16
+ everyone to stop reading it.
17
+
18
+ `auth: false` is already the field that says "no session required". Writing it
19
+ on a `pikkuSessionlessFunc` is redundant at runtime — sessionless already means
20
+ that — which is what makes it useful as a declaration: nobody writes it by
21
+ accident, so its presence is the author deciding the function is public. The
22
+ inspector now records `auth` as written (`true`, `false` or absent) instead of
23
+ folding `false` into absent, and the check treats `false` as a gate the author
24
+ declared, the same way it treats `permissionsInBody`.
25
+
26
+ The case the warning exists for is unchanged. A sessionless function that picked
27
+ up `expose: true` later, with nobody thinking about who may call it, has no
28
+ `auth` at all — and still warns.
29
+
30
+ **What this rules out:** a separate `public: true` flag, which would be a second
31
+ field meaning what `auth: false` already means; reading `readonly` or the
32
+ function's name as evidence it is safe to expose; and silencing PKU574 for every
33
+ sessionless function, which would drop the accidental-exposure case with the
34
+ intentional one.
@@ -42,7 +42,10 @@ case it would catch.
42
42
  Severity is `warn`, not `critical`. `expose: true` on an ungated sessionless
43
43
  function is legitimate for a genuinely public endpoint — a health check, a
44
44
  sign-up — so this cannot block a build without breaking correct programs. It
45
- fails a build only under `--fail-on-warn`.
45
+ fails a build only under `--fail-on-warn`. And the public endpoint is not left
46
+ warning forever: an explicit `auth: false` on a sessionless function declares it
47
+ public on purpose and silences the check (see
48
+ [public is declared with auth: false](./a-public-function-says-so-with-auth-false.md)).
46
49
 
47
50
  **What this rules out:** checking at boot, where the report arrives after
48
51
  deployment; checking in `pikku validate` from `.pikku` JSON, which does not
@@ -58,6 +58,7 @@ A rule about who may do what, and which way it fails when it is unsure.
58
58
  - [A scaffolded surface is authenticated unless the config opts out in writing](scaffold-features-are-authenticated-unless-opted-out.md) — SUPERSEDED; retains why the legacy 'auth' | 'no-auth' strings are refused rather than coerced
59
59
  - [Scenario-step functions are never externally invocable over RPC](scenario-step-functions-are-never-externally-invocable.md) — rpcExposed requires expose and rejects scenarioStep, so test steps stay reachable only from inside a scenario run
60
60
  - [Scope resolution happens at the session boundary and scope sync never deletes](scope-resolution-happens-at-the-session-boundary-and-sync-never-deletes.md) — ScopeService is called when a session is built, never by the function runner, and syncScopes only ever adds — revoking is an explicit operation
61
+ - [A public function says so with auth: false](a-public-function-says-so-with-auth-false.md) — An exposed sessionless function that writes auth: false is public on purpose and PKU574 stays quiet; one that leaves auth out still warns
61
62
  - [Self-authentication is declared, not detected](self-authentication-is-declared-not-detected.md) — A function that authorizes callers in its own body says so with selfAuthenticated; codegen never tries to infer it
62
63
  - [Signed content URLs bind the request path and verify fail-closed](signed-content-urls-bind-the-request-path.md) — A signature that only carries timestamps authorizes every asset, and a verifier with no key must refuse rather than allow
63
64
  - [Webhook bodies are signed before they are enqueued](webhook-bodies-are-signed-before-they-are-enqueued.md) — QueueWebhookService computes the HMAC at enqueue time so the signing key never travels in the queue payload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.122",
3
+ "version": "0.12.123",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -24,6 +24,7 @@
24
24
  "sideEffects": [
25
25
  "./dist/errors/errors.js",
26
26
  "./dist/wirings/addon/remote-addon-auth.js",
27
+ "./dist/wirings/agent/agent-rpc.js",
27
28
  "./dist/wirings/rpc/rpc-runner.js",
28
29
  "./dist/wirings/workflow/pikku-scenario-service.js",
29
30
  "./dist/wirings/workflow/workflow-approval-policy.js",