@pikku/core 0.12.136 → 0.12.138

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 (59) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/dev/hot-reload.js +5 -2
  3. package/dist/env.d.ts +18 -0
  4. package/dist/env.js +60 -1
  5. package/dist/function/abort-scope.d.ts +6 -5
  6. package/dist/function/abort-scope.js +6 -17
  7. package/dist/function/function-runner.js +2 -1
  8. package/dist/middleware/auth-apikey.js +1 -1
  9. package/dist/middleware/auth-bearer.js +1 -1
  10. package/dist/middleware/auth-cookie.js +1 -1
  11. package/dist/schema.js +3 -1
  12. package/dist/services/in-memory-queue-service.js +4 -2
  13. package/dist/services/in-memory-workflow-service.js +7 -6
  14. package/dist/services/index.d.ts +0 -1
  15. package/dist/services/index.js +0 -1
  16. package/dist/services/local-variables.d.ts +5 -0
  17. package/dist/services/local-variables.js +7 -2
  18. package/dist/services/meta-service.d.ts +11 -0
  19. package/dist/services/meta-service.js +10 -1
  20. package/dist/services/queue-webhook-service.js +1 -1
  21. package/dist/services/webhook-service.d.ts +2 -2
  22. package/dist/services/webhook-service.js +6 -4
  23. package/dist/types/core.types.d.ts +10 -1
  24. package/dist/utils/hmac.d.ts +25 -10
  25. package/dist/utils/hmac.js +202 -24
  26. package/dist/wirings/agent/agent-agui.js +65 -62
  27. package/dist/wirings/agent/agent-interrupt.d.ts +4 -1
  28. package/dist/wirings/agent/agent-interrupt.js +3 -3
  29. package/dist/wirings/agent/agent-memory.js +5 -6
  30. package/dist/wirings/agent/agent-model-config.js +2 -1
  31. package/dist/wirings/agent/agent-prepare.js +21 -13
  32. package/dist/wirings/agent/agent-stream.js +38 -34
  33. package/dist/wirings/agent/agent.types.d.ts +8 -1
  34. package/dist/wirings/channel/channel-handler.js +1 -1
  35. package/dist/wirings/channel/local/local-channel-handler.js +5 -1
  36. package/dist/wirings/channel/local/local-channel-runner.js +6 -4
  37. package/dist/wirings/channel/local/local-eventhub-service.d.ts +1 -0
  38. package/dist/wirings/channel/local/local-eventhub-service.js +4 -1
  39. package/dist/wirings/cli/channel/cli-channel-runner.js +7 -2
  40. package/dist/wirings/cli/channel/cli-raw-client-runner.js +8 -4
  41. package/dist/wirings/cli/cli-runner.js +1 -1
  42. package/dist/wirings/gateway/gateway-runner.js +3 -3
  43. package/dist/wirings/http/http-runner.js +6 -6
  44. package/dist/wirings/persona/persona-app-scopes.js +1 -0
  45. package/dist/wirings/scheduler/scheduler-runner.js +1 -1
  46. package/dist/wirings/scope/define-scope.d.ts +19 -0
  47. package/dist/wirings/scope/define-scope.js +19 -0
  48. package/dist/wirings/scope/index.d.ts +2 -2
  49. package/dist/wirings/scope/index.js +1 -1
  50. package/dist/wirings/scope/scope.types.d.ts +14 -0
  51. package/dist/wirings/scope/validate-scope-definitions.js +1 -0
  52. package/dist/wirings/trigger/trigger-runner.js +3 -1
  53. package/dist/wirings/trigger/webhook-source-runner.js +5 -2
  54. package/dist/wirings/virtual-user/virtual-user-scaffold.js +3 -2
  55. package/dist/wirings/workflow/scenario-coverage.d.ts +70 -0
  56. package/dist/wirings/workflow/scenario-coverage.js +159 -0
  57. package/dist/wirings/workflow/workflow-invocation-id.js +85 -8
  58. package/package.json +4 -1
  59. package/src/public-surface.json +10 -3
package/CHANGELOG.md CHANGED
@@ -1,3 +1,44 @@
1
+ ## 0.12.138
2
+
3
+ ### Patch Changes
4
+
5
+ - 6f11bb5: A deployed Cloudflare Worker is always production. The adapter now pins `process.env.NODE_ENV` to `"production"` in the bundle, and core's `isProduction()` reads the literal `process.env.NODE_ENV` so that pin takes effect. Before, a Worker had no `NODE_ENV`, so `isProduction()` was false and every 5xx response returned the error message and stack trace to the client. `process.env.NODE_ENV` now wins over a variables service when it is set.
6
+ - 0c67e33: `@pikku/core` no longer imports `node:async_hooks`, so it is edge-clean end to end. The function abort scope that `beginChanges()` observes is now passed explicitly (agent tool `execute(input, { abortScope })` onto `wire.abortScope`, carried through nested `rpc` calls) instead of living in `AsyncLocalStorage`, which also means concurrent runs in one process or isolate cannot see each other's scope. `AgentToolDef.execute` gains an optional second argument; nothing else in the public API changes.
7
+ - 15c4968: The default runtime path no longer needs `node:*`, `Buffer` or an unguarded `process`, so it loads on edge runtimes (Web APIs only).
8
+
9
+ **Breaking:** `FileScenarioRunStore`, `scenarioArtifactContentType` and `scenarioRunSummary` leave the `@pikku/core/services` barrel because they use `node:fs`. Import them from `@pikku/core/services/file-scenario-run-store`. Pre-0.13 breaking changes still ship as a patch.
10
+
11
+ **Breaking:** the HMAC helpers in `@pikku/core/hmac` run on Web Crypto, which is async. `hmacSha256Hex`, `hmacDigest`, `verifyHmacSignature` and `verifyPublicKeySignature` now return a Promise; `timingSafeStringEqual` stays synchronous. `WebhookService.sign` and `WebhookService.verify` return a Promise, and so does `verifySlackSignature` in `@pikku/gateway-slack`: `await` them. A function-style `verify` on a webhook source may return a Promise as well. `verifyPublicKeySignature` accepts RSA and EC (P-256, P-384, P-521) PEM keys; Ed25519 and RSA-PSS keys verify as false.
12
+
13
+ `isProduction()` (and the `PIKKU_MODEL_ALIASES` and `PIKKU_ENV` reads) now ask the registered singleton services' `variables` service first, then `process.env` when `process` exists. With neither, `isProduction()` is true, so an edge deployment never exposes error details by accident. `LocalVariablesService` resolves its `process.env` default when constructed and falls back to an empty store without `process`.
14
+
15
+ Workflow invocation ids use a built-in SHA-1 and stay byte-identical. `@pikku/kysely` and the Cloudflare hibernation server use `globalThis.crypto.randomUUID()` and no `Buffer`; `@pikku/kysely` is marked `sideEffects: false`. `@pikku/ai-deepinfra` reads `DEEPINFRA_API_KEY` only when `process` exists and decodes base64 audio without `Buffer`.
16
+
17
+ Runtime tiers. A package declares where it can run in `package.json`: `"pikku": { "runtime": "edge" | "serverless" | "server" }`, with optional per-subpath overrides (`"exports": { "./dev": "server" }`). `server` is the default. `@pikku/core` and the default services are `edge` (core's Node-only subpaths are `server`), `@pikku/better-auth`, `@pikku/addon-admin` and `@pikku/cloudflare` are `serverless` (better-auth statically imports `node:crypto` scrypt), and `@pikku/addon-console` is `server` and must never land in a deployed unit.
18
+
19
+ `pikku deploy` now verifies each deployed unit before bundling it. The unit is bundled with esbuild (neutral platform, `workerd`/`worker`/`browser` conditions) under its tier's profile, ahead of the adapter's aliases and stubs so they cannot hide anything, and the build fails when the bundle holds a package declared above the unit's tier or a Node built-in the runtime does not provide. The error names the unit, package, import chain and the fix. A built-in the provider stubs is a warning. On an edge unit, a bundled `InMemory*` class also fails the build. The unit's tier is its `runtime`, else the project's own `pikku.runtime`, else the provider default (`serverless` on Cloudflare). Packages that declare nothing are judged by their imports only.
20
+
21
+ `ProviderAdapter.getRuntimeProfile(tier?)` returns the compat date, compat flags, the built-ins the runtime provides (keyed by compat date), the stubbed ones and the externals. The Cloudflare adapter derives `wrangler.toml`, the upload metadata and the bundle externals from it; `node:os` and `node:fs` are not provided before compat date 2025-09-15. The default is `nodejs_compat_v2` with compat date 2024-12-18, which is what `wrangler.toml` and Fabric's live workers already use, so Fabric deploys see no runtime change. Only the direct-API upload in `@pikku/deploy-cloudflare` (`createWorker`, used by its own `deploy()`) moves: it sent `nodejs_compat` with 2024-01-01 before. A newer date or flag is opt-in through the adapter option `new CloudflareProviderAdapter({ compatDate })` or `getCloudflareRuntimeProfile(tier, compatDate)`. From 2025-08-15 `node:http` and `node:https` exist, and from 2025-09-15 `node:os`, `node:fs` (stubbed by the build regardless) and `node:perf_hooks`; the verifier's allowed built-ins follow the effective date. A newer date changes the runtime of every worker deployed with it.
22
+
23
+ `declareScopes(['pikku:console'])` (from `@pikku/core/scope`) declares scopes by id without wiring anything. `wireAddon({ scopes })` only requires a scope of an addon's functions; the tree arrives with the addon's metadata, so a role granting `pikku:console` used to fail PKU124 unless the addon was wired, and a wired `@pikku/addon-console` lands in every unit carrying the `/rpc` catch-all. An app that grants the scope without the console now calls `declareScopes` and no longer needs the scaffold or the addon. When the addon is wired its tree and descriptions win and the bare id adds nothing.
24
+
25
+ More packages declare a tier after bundling each with the edge profile. `edge`: `@pikku/addon-graph`, `@pikku/pino`, `@pikku/jose`, `@pikku/ai-vercel`, `@pikku/ai-voice`. `serverless`: `@pikku/kysely-postgres` (postgres.js needs `node:buffer`, `stream`, `events`), `@pikku/backblaze` (`copyFile` reads a local file, which the build stubs), `@pikku/gateway-slack` (`@slack/web-api` imports `node:os` and `zlib`). `server`: `@pikku/schedule` (`cron` imports `child_process`), `@pikku/aws-services` (the AWS SDK's Node handler needs `node:http2`), `@pikku/browser` (puppeteer), and `@pikku/modelcontextprotocol` except its `./fetch` subpath, which is `edge`.
26
+
27
+ `@pikku/addon-graph` no longer needs `node:crypto` or `Buffer`: the Crypto function hashes and HMACs on Web Crypto (MD5 is built in, since Web Crypto has none) and encodes with `TextEncoder`/`atob`/`btoa`, with output unchanged.
28
+
29
+ - d75a23d: Await previously un-awaited promises (session setters, channel sends, schema compile, trigger and CLI renderer calls) and handle rejections in fire-and-forget timers and signal handlers, found by type-aware `no-floating-promises` / `no-misused-promises`.
30
+ - 9b12c8a: Harden webhook signature verification. An empty or missing secret now fails closed at every call site (`verifyHmacSignature`, webhook source verification, `WebhookService.verify`, `verifySlackSignature`), including a signature computed over an empty key. HMAC verification now decodes the signature to bytes and compares with `crypto.subtle.verify('HMAC', ...)` instead of a pure-JS string compare, and a malformed hex signature is rejected. `verifyPublicKeySignature` accepts Ed25519 SPKI keys again.
31
+
32
+ ## 0.12.137
33
+
34
+ ### Patch Changes
35
+
36
+ - 0c865d9: Changes and changesets replace milestones. The changes commands run on a local queue when the checkout has no fabric project; `pikku changes next` routes to a changes, upgrade or knowledge agent and merges finished changesets. A changeset that creates or alters a table, or has 6 or more changes, needs a plan at `knowledge/plans/<changeset>.plan.json` (anything else goes to an optional judge at `PIKKU_PLAN_JUDGE_URL`), and `changes done` holds it to that plan. `pikku knowledge gaps` replaces `knowledge next`/`reconcile`, and also reports code a merged plan built that is gone and notes that were deleted.
37
+ - c36b066: Scenario coverage moves into OSS. `@pikku/core/scenario/coverage` adds `readScenarioCoverage`, which reports the lines no scenario reaches, the mutations no scenario drives, and the pages scenarios open. It is exposed through `pikku scenario coverage`, `console:getScenarioCoverage`, and a "Not tested by any scenario" card on the Scenarios page. `LocalMetaService.getRpcMeta` now reads the `.internal.gen.json` file that codegen actually writes.
38
+ - 0963b73: Every scope tree now records where it came from: `origin` on `ScopeDefinitionMeta` is `{ kind: 'app' }` for the app's own `defineScope`, `{ kind: 'generated' }` for a tree the CLI wrote into the project (a `.gen.ts`, or the `app` root derived from personas), and `{ kind: 'addon', package, displayName }` for one an installed addon declared. An addon's build stamps its own package and display name on its trees, and the host keeps that name while recording the package it actually installed. `MetaService.getScopesMeta()` reads the result, and the console's all-meta payload carries it as `scopes`.
39
+
40
+ The console's Scopes page becomes **Permissions** and is grouped by that origin: the app's own permissions first, then what Pikku generated into the project, then one card per addon (Pikku's own before third-party, each alphabetical), each card folding to its header — the app's own starts open, every addon's starts folded, and any search or filter opens them all. Above the cards, two filters narrow the page to what one role is given (or to what no role is given yet) and to one source. A permission group no longer says "Not part of any role yet" above lines that are each given to a role.
41
+
1
42
  ## 0.12.136
2
43
 
3
44
  ### Patch Changes
@@ -105,14 +105,17 @@ export async function pikkuDevReloader(options) {
105
105
  pendingChanges.add(filePath);
106
106
  if (debounceTimer)
107
107
  clearTimeout(debounceTimer);
108
- debounceTimer = setTimeout(async () => {
108
+ const flush = async () => {
109
109
  const files = [...pendingChanges];
110
110
  pendingChanges.clear();
111
111
  for (const file of files) {
112
112
  postCodegenQueue.add(file);
113
113
  await safeHandleFileChange(file);
114
114
  }
115
- }, 50);
115
+ };
116
+ // Fire-and-forget timer callback: safeHandleFileChange catches and logs
117
+ // every error, so nothing can reject here.
118
+ debounceTimer = setTimeout(() => void flush(), 50);
116
119
  };
117
120
  for (const srcDir of absSrcDirs) {
118
121
  try {
package/dist/env.d.ts CHANGED
@@ -1 +1,19 @@
1
+ /**
2
+ * Reads one variable without `process`, which edge runtimes do not have.
3
+ *
4
+ * Looks in the registered singleton services' `variables` service first, then
5
+ * in `process.env` when there is one. Never throws: services that were never
6
+ * registered (a unit test, an early boot path) simply fall through, which is
7
+ * why this reads the state directly instead of calling
8
+ * `getSingletonServices()`.
9
+ *
10
+ * The read is synchronous, so a `VariablesService` that answers with a Promise
11
+ * is ignored and the lookup falls through to `process.env`.
12
+ */
13
+ export declare const readEnvVariable: (name: string) => string | undefined;
14
+ /**
15
+ * `NODE_ENV` decides whether error details are exposed. With neither a
16
+ * variables service nor `process` to say, this is production: failing closed
17
+ * means an edge deployment never leaks error details by accident.
18
+ */
1
19
  export declare const isProduction: () => boolean;
package/dist/env.js CHANGED
@@ -1 +1,60 @@
1
- export const isProduction = () => process.env.NODE_ENV === 'production';
1
+ import { pikkuState } from './pikku-state.js';
2
+ /**
3
+ * Reads one variable without `process`, which edge runtimes do not have.
4
+ *
5
+ * Looks in the registered singleton services' `variables` service first, then
6
+ * in `process.env` when there is one. Never throws: services that were never
7
+ * registered (a unit test, an early boot path) simply fall through, which is
8
+ * why this reads the state directly instead of calling
9
+ * `getSingletonServices()`.
10
+ *
11
+ * The read is synchronous, so a `VariablesService` that answers with a Promise
12
+ * is ignored and the lookup falls through to `process.env`.
13
+ */
14
+ export const readEnvVariable = (name) => {
15
+ try {
16
+ const variables = pikkuState(null, 'package', 'singletonServices')?.variables;
17
+ if (variables) {
18
+ const value = variables.get(name);
19
+ if (value instanceof Promise) {
20
+ value.catch(() => { });
21
+ }
22
+ else if (typeof value === 'string' ||
23
+ typeof value === 'number' ||
24
+ typeof value === 'boolean') {
25
+ return String(value);
26
+ }
27
+ }
28
+ }
29
+ catch {
30
+ // A faulty variables service must not take error handling down with it.
31
+ }
32
+ return hasProcessEnv() ? globalThis.process.env[name] : undefined;
33
+ };
34
+ const hasProcessEnv = () => typeof globalThis.process?.env === 'object';
35
+ /**
36
+ * The literal `process.env.NODE_ENV`, kept as a literal on purpose: a deploy
37
+ * adapter pins it with a bundler `define` (a Cloudflare Worker is always
38
+ * production), and `define` only rewrites this exact expression, not
39
+ * `process.env[name]`. Without that define it reads the real value, and on a
40
+ * runtime with no `process` the ReferenceError means "unknown".
41
+ */
42
+ const buildTimeNodeEnv = () => {
43
+ try {
44
+ return process.env.NODE_ENV;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ };
50
+ /**
51
+ * `NODE_ENV` decides whether error details are exposed. With neither a
52
+ * variables service nor `process` to say, this is production: failing closed
53
+ * means an edge deployment never leaks error details by accident.
54
+ */
55
+ export const isProduction = () => {
56
+ const nodeEnv = buildTimeNodeEnv() ?? readEnvVariable('NODE_ENV');
57
+ if (nodeEnv !== undefined)
58
+ return nodeEnv === 'production';
59
+ return !hasProcessEnv();
60
+ };
@@ -14,10 +14,6 @@ export interface AbortScope {
14
14
  /** Called by `beginChanges()` once a function commits to mutating. */
15
15
  onBeginChanges?: () => void;
16
16
  }
17
- /** Run `fn` with an abort scope that `beginChanges()` will observe. */
18
- export declare const runInAbortScope: <T>(scope: AbortScope, fn: () => T) => T;
19
- /** The current scope, if the caller is running inside one. */
20
- export declare const getAbortScope: () => AbortScope | undefined;
21
17
  /**
22
18
  * Declare that everything after this line changes something.
23
19
  *
@@ -38,10 +34,15 @@ export declare const getAbortScope: () => AbortScope | undefined;
38
34
  * ignoring the answer, and the failure mode of ignoring it is a mutation nobody
39
35
  * asked for.
40
36
  *
37
+ * The scope is passed explicitly — the function runner binds it from
38
+ * `wire.abortScope` — rather than looked up ambiently, so concurrent runs in
39
+ * one process (or one edge isolate) can never see each other's scope and core
40
+ * needs no `AsyncLocalStorage`.
41
+ *
41
42
  * Entirely cooperative, and safe to call anywhere — outside a scope it is a
42
43
  * no-op, so a function does not need to know how it was invoked. A function
43
44
  * that never calls it keeps the conservative default: if it is not marked
44
45
  * `readonly`, an interrupt assumes it changed something and says so. The tool
45
46
  * that forgot to call this is exactly the one that cannot be assumed harmless.
46
47
  */
47
- export declare const beginChanges: () => Promise<void>;
48
+ export declare const beginChanges: (scope?: AbortScope) => Promise<void>;
@@ -1,4 +1,3 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
1
  /**
3
2
  * Raised by {@link beginChanges} when the caller has gone away before anything
4
3
  * was changed. Distinct from a failure: nothing happened, so there is nothing
@@ -12,20 +11,6 @@ export class AbandonedError extends Error {
12
11
  this.name = 'AbandonedError';
13
12
  }
14
13
  }
15
- /**
16
- * The ambient "is my caller still there?" signal.
17
- *
18
- * Ambient rather than a parameter because the question is the same one for
19
- * every wiring — an agent run that was interrupted, an HTTP request whose
20
- * client disconnected, a cancelled workflow — and threading it through every
21
- * signature would mean changing every function that merely sits between the
22
- * wiring and the mutation.
23
- */
24
- const scopeStorage = new AsyncLocalStorage();
25
- /** Run `fn` with an abort scope that `beginChanges()` will observe. */
26
- export const runInAbortScope = (scope, fn) => scopeStorage.run(scope, fn);
27
- /** The current scope, if the caller is running inside one. */
28
- export const getAbortScope = () => scopeStorage.getStore();
29
14
  /**
30
15
  * Declare that everything after this line changes something.
31
16
  *
@@ -46,14 +31,18 @@ export const getAbortScope = () => scopeStorage.getStore();
46
31
  * ignoring the answer, and the failure mode of ignoring it is a mutation nobody
47
32
  * asked for.
48
33
  *
34
+ * The scope is passed explicitly — the function runner binds it from
35
+ * `wire.abortScope` — rather than looked up ambiently, so concurrent runs in
36
+ * one process (or one edge isolate) can never see each other's scope and core
37
+ * needs no `AsyncLocalStorage`.
38
+ *
49
39
  * Entirely cooperative, and safe to call anywhere — outside a scope it is a
50
40
  * no-op, so a function does not need to know how it was invoked. A function
51
41
  * that never calls it keeps the conservative default: if it is not marked
52
42
  * `readonly`, an interrupt assumes it changed something and says so. The tool
53
43
  * that forgot to call this is exactly the one that cannot be assumed harmless.
54
44
  */
55
- export const beginChanges = async () => {
56
- const scope = scopeStorage.getStore();
45
+ export const beginChanges = async (scope) => {
57
46
  if (!scope)
58
47
  return;
59
48
  if (scope.abandoned) {
@@ -235,7 +235,8 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
235
235
  // and offering it there would invite the contradiction of a `readonly`
236
236
  // function announcing where its changes begin.
237
237
  if (!funcMeta.readonly) {
238
- invocationWire.beginChanges = beginChanges;
238
+ const abortScope = resolvedWire.abortScope;
239
+ invocationWire.beginChanges = () => beginChanges(abortScope);
239
240
  }
240
241
  // knowledge: decisions/security/addon-scopes-are-resolved-where-the-function-runs.md
241
242
  const addonScopes = resolveAddonScopes(packageName, addonInstance?.namespace);
@@ -19,7 +19,7 @@ export const authAPIKey = pikkuMiddlewareFactory(({ source }) => pikkuMiddleware
19
19
  if (apiKey && jwtService) {
20
20
  const userSession = await jwtService.decode(apiKey);
21
21
  if (userSession) {
22
- setSession?.(userSession);
22
+ await setSession?.(userSession);
23
23
  }
24
24
  }
25
25
  return next();
@@ -48,7 +48,7 @@ export const authBearer = pikkuMiddlewareFactory(({ token } = {}) => pikkuMiddle
48
48
  userSession = await jwtService.decode(bearerToken);
49
49
  }
50
50
  if (userSession) {
51
- setSession?.(userSession);
51
+ await setSession?.(userSession);
52
52
  }
53
53
  }
54
54
  return next();
@@ -47,7 +47,7 @@ export const authCookie = pikkuMiddlewareFactory(({ name, options, expiresIn })
47
47
  // standard JWT timestamp, so an identity-less token is treated as no
48
48
  // session rather than as an authenticated one.
49
49
  if (userSession && hasSessionIdentity(userSession)) {
50
- setSession?.(userSession);
50
+ await setSession?.(userSession);
51
51
  }
52
52
  }
53
53
  await next();
package/dist/schema.js CHANGED
@@ -18,7 +18,9 @@ export const compileAllSchemas = (logger, schemaService) => {
18
18
  for (const [pkgName, packageState] of getAllPackageStates()) {
19
19
  const resolvedPkgName = pkgName === '__main__' ? null : pkgName;
20
20
  for (const [name, schema] of packageState.misc.schemas) {
21
- schemaService.compileSchema(schemaKey(name, resolvedPkgName), schema);
21
+ // compileSchema may be async; this function is synchronous (public
22
+ // signature), so a rejection is logged instead of left unhandled.
23
+ Promise.resolve(schemaService.compileSchema(schemaKey(name, resolvedPkgName), schema)).catch((e) => logger.error(`Failed to compile schema '${name}': ${e?.message ?? e}`));
22
24
  }
23
25
  }
24
26
  validateAllSchemasLoaded(logger, schemaService);
@@ -24,7 +24,8 @@ export class InMemoryQueueService {
24
24
  }
25
25
  catch (e) {
26
26
  if (attemptsMade < maxAttempts) {
27
- setTimeout(runAttempt, this.backoffDelay(options?.backoff, attemptsMade));
27
+ // Fire-and-forget retry: runAttempt catches every error itself.
28
+ setTimeout(() => void runAttempt(), this.backoffDelay(options?.backoff, attemptsMade));
28
29
  }
29
30
  else {
30
31
  console.error(`[InMemoryQueue] Job ${jobId} on ${queueName} failed after ${attemptsMade} attempt(s):`, e.message);
@@ -32,7 +33,8 @@ export class InMemoryQueueService {
32
33
  }
33
34
  };
34
35
  const delay = options?.delay ?? 100 + Math.floor(Math.random() * 201);
35
- setTimeout(runAttempt, delay);
36
+ // Fire-and-forget: runAttempt catches every error itself.
37
+ setTimeout(() => void runAttempt(), delay);
36
38
  return jobId;
37
39
  }
38
40
  backoffDelay(backoff, attemptsMade) {
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'crypto';
2
1
  import { PikkuWorkflowService } from '../wirings/workflow/pikku-workflow-service.js';
3
2
  import { isExpectedError } from '../errors/error-handler.js';
4
3
  import { isStepLeaseLive } from '../wirings/workflow/workflow-constants.js';
@@ -17,7 +16,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
17
16
  branchKeys = new Map(); // keyed by stepId
18
17
  workflowVersions = new Map(); // keyed by `${name}:${graphHash}`
19
18
  async createRunImpl(workflowName, input, inline, graphHash, wire, options) {
20
- const runId = randomUUID();
19
+ const runId = globalThis.crypto.randomUUID();
21
20
  const now = new Date();
22
21
  const run = {
23
22
  id: runId,
@@ -41,7 +40,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
41
40
  return runId;
42
41
  }
43
42
  async scheduleSleep(runId, stepId, duration) {
44
- const timer = setTimeout(async () => {
43
+ const resume = async () => {
45
44
  this.sleepTimers.delete(timer);
46
45
  try {
47
46
  await this.executeWorkflowSleepCompleted(runId, stepId);
@@ -49,7 +48,9 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
49
48
  catch (error) {
50
49
  this.logger?.error(`Failed to resume workflow sleep for runId ${runId}: ${error?.message ?? error}`);
51
50
  }
52
- }, duration);
51
+ };
52
+ // Fire-and-forget timer callback: resume catches and logs every error.
53
+ const timer = setTimeout(() => void resume(), duration);
53
54
  this.sleepTimers.add(timer);
54
55
  return true;
55
56
  }
@@ -71,7 +72,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
71
72
  }
72
73
  }
73
74
  async insertStepStateImpl(runId, stepName, rpcName, data, stepOptions, fromStepName) {
74
- const stepId = randomUUID();
75
+ const stepId = globalThis.crypto.randomUUID();
75
76
  const now = new Date();
76
77
  const step = {
77
78
  stepId,
@@ -202,7 +203,7 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
202
203
  throw new Error(`Step not found: ${failedStepId}`);
203
204
  }
204
205
  const failedStepData = this.stepData.get(failedStepId);
205
- const newStepId = randomUUID();
206
+ const newStepId = globalThis.crypto.randomUUID();
206
207
  const now = new Date();
207
208
  const newStep = {
208
209
  stepId: newStepId,
@@ -21,7 +21,6 @@ export { LeaseLostError, LeaseTakenError, holdLease, type Lease, type LeaseServi
21
21
  export { InMemoryLeaseService } from './in-memory-lease-service.js';
22
22
  export { InMemoryAgentRunStateService } from './in-memory-agent-run-state-service.js';
23
23
  export { LocalGatewayService } from './local-gateway-service.js';
24
- export { FileScenarioRunStore, scenarioArtifactContentType, scenarioRunSummary, type FileScenarioRunStoreOptions, } from './file-scenario-run-store.js';
25
24
  export type { ContentService, SignContentKeyArgs, SignURLArgs, GetUploadURLArgs, UploadURLResult, BucketKeyArgs, WriteFileArgs, CopyFileArgs, } from './content-service.js';
26
25
  export type { ScenarioPersona, ResolvedPersona, ScenarioPersonas, } from './personas-service.js';
27
26
  export type { JWTService } from './jwt-service.js';
@@ -21,7 +21,6 @@ export { LeaseLostError, LeaseTakenError, holdLease, } from './lease-service.js'
21
21
  export { InMemoryLeaseService } from './in-memory-lease-service.js';
22
22
  export { InMemoryAgentRunStateService } from './in-memory-agent-run-state-service.js';
23
23
  export { LocalGatewayService } from './local-gateway-service.js';
24
- export { FileScenarioRunStore, scenarioArtifactContentType, scenarioRunSummary, } from './file-scenario-run-store.js';
25
24
  export { renderEmail, } from './email-template.js';
26
25
  export { DEFAULT_WEBHOOK_RETRIES, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, WebhookService, } from './webhook-service.js';
27
26
  export { ACTOR_ROOT_SECRET_MIN_LENGTH, ACTOR_SECRET_INFO, ACTOR_SECRET_NAME, actorSecretSubject, deriveActorSecret, verifyActorSecret, } from './persona-actor-secret.js';
@@ -1,6 +1,11 @@
1
1
  import type { VariablesService } from './variables-service.js';
2
2
  export declare class LocalVariablesService implements VariablesService {
3
3
  private variables;
4
+ /**
5
+ * Defaults to `process.env`, resolved when the service is built rather than
6
+ * when this module loads, and guarded so a runtime without `process` gets an
7
+ * empty store instead of a ReferenceError.
8
+ */
4
9
  constructor(variables?: Record<string, string | undefined>);
5
10
  getAll(): Record<string, string | undefined>;
6
11
  getVariables<T extends Record<string, unknown> = Record<string, unknown>>(names: (keyof T & string)[]): Partial<T>;
@@ -1,7 +1,12 @@
1
1
  export class LocalVariablesService {
2
2
  variables;
3
- constructor(variables = process.env) {
4
- this.variables = variables;
3
+ /**
4
+ * Defaults to `process.env`, resolved when the service is built rather than
5
+ * when this module loads, and guarded so a runtime without `process` gets an
6
+ * empty store instead of a ReferenceError.
7
+ */
8
+ constructor(variables) {
9
+ this.variables = variables ?? globalThis.process?.env ?? {};
5
10
  }
6
11
  getAll() {
7
12
  return this.variables || {};
@@ -10,6 +10,7 @@ import type { FeaturesMeta } from '../wirings/workflow/scenario.types.js';
10
10
  import type { ResolvedPersona } from './personas-service.js';
11
11
  import type { SystemRoleDefinitionsMeta } from '../wirings/role/role.types.js';
12
12
  import type { FeatureFlagDefinitionsMeta } from '../wirings/flag/flag.types.js';
13
+ import type { ScopeDefinitionsMeta } from '../wirings/scope/scope.types.js';
13
14
  import type { AnalyticsEventsMeta } from '../analytics/analytics.types.js';
14
15
  import type { TriggerMeta, TriggerSourceMeta } from '../wirings/trigger/trigger.types.js';
15
16
  import type { SecretDefinitionsMeta } from '../wirings/secret/secret.types.js';
@@ -156,6 +157,14 @@ export interface MetaService {
156
157
  * seed granted from rather than a second one.
157
158
  */
158
159
  getSystemRolesMeta(): Promise<SystemRoleDefinitionsMeta>;
160
+ /**
161
+ * The scope trees declared with `defineScope`, keyed by root, each carrying
162
+ * where it came from.
163
+ *
164
+ * The scope store holds the grantable ids; this holds what the store does
165
+ * not — a root's display name and which addon, if any, declared it.
166
+ */
167
+ getScopesMeta(): Promise<ScopeDefinitionsMeta>;
159
168
  /**
160
169
  * The flags declared with `defineFeatureFlags`, keyed by name.
161
170
  *
@@ -199,6 +208,7 @@ export declare class LocalMetaService implements MetaService {
199
208
  private workflowMetaCache;
200
209
  private personasMetaCache;
201
210
  private systemRolesMetaCache;
211
+ private scopesMetaCache;
202
212
  private featureFlagsMetaCache;
203
213
  private analyticsMetaCache;
204
214
  private featuresMetaCache;
@@ -236,6 +246,7 @@ export declare class LocalMetaService implements MetaService {
236
246
  getWorkflowMeta(): Promise<WorkflowsMeta>;
237
247
  getPersonasMeta(): Promise<Record<string, ResolvedPersona>>;
238
248
  getSystemRolesMeta(): Promise<SystemRoleDefinitionsMeta>;
249
+ getScopesMeta(): Promise<ScopeDefinitionsMeta>;
239
250
  getFeatureFlagsMeta(): Promise<FeatureFlagDefinitionsMeta>;
240
251
  getAnalyticsMeta(): Promise<AnalyticsEventsMeta>;
241
252
  getFeaturesMeta(): Promise<FeaturesMeta>;
@@ -16,6 +16,7 @@ export class LocalMetaService {
16
16
  workflowMetaCache = null;
17
17
  personasMetaCache = null;
18
18
  systemRolesMetaCache = null;
19
+ scopesMetaCache = null;
19
20
  featureFlagsMetaCache = null;
20
21
  analyticsMetaCache = null;
21
22
  featuresMetaCache = null;
@@ -115,6 +116,7 @@ export class LocalMetaService {
115
116
  this.workflowMetaCache = null;
116
117
  this.personasMetaCache = null;
117
118
  this.systemRolesMetaCache = null;
119
+ this.scopesMetaCache = null;
118
120
  this.featureFlagsMetaCache = null;
119
121
  this.analyticsMetaCache = null;
120
122
  this.featuresMetaCache = null;
@@ -216,7 +218,7 @@ export class LocalMetaService {
216
218
  if (this.rpcMetaCache)
217
219
  return this.rpcMetaCache;
218
220
  try {
219
- const content = await this.readFile('rpc/pikku-rpc-wirings-meta.gen.json');
221
+ const content = await this.readFile('rpc/pikku-rpc-wirings-meta.internal.gen.json');
220
222
  this.rpcMetaCache = content ? JSON.parse(content) : {};
221
223
  return this.rpcMetaCache;
222
224
  }
@@ -278,6 +280,13 @@ export class LocalMetaService {
278
280
  this.systemRolesMetaCache = content ? JSON.parse(content) : {};
279
281
  return this.systemRolesMetaCache;
280
282
  }
283
+ async getScopesMeta() {
284
+ if (this.scopesMetaCache)
285
+ return this.scopesMetaCache;
286
+ const content = await this.readFile('scopes/pikku-scopes-meta.gen.json');
287
+ this.scopesMetaCache = content ? JSON.parse(content) : {};
288
+ return this.scopesMetaCache;
289
+ }
281
290
  async getFeatureFlagsMeta() {
282
291
  if (this.featureFlagsMetaCache)
283
292
  return this.featureFlagsMetaCache;
@@ -39,7 +39,7 @@ export class QueueWebhookService extends WebhookService {
39
39
  }
40
40
  if (secret) {
41
41
  const signatureHeader = webhookConfig?.signatureHeader ?? DEFAULT_WEBHOOK_SIGNATURE_HEADER;
42
- headers[signatureHeader] = this.sign(secret, body);
42
+ headers[signatureHeader] = await this.sign(secret, body);
43
43
  }
44
44
  const jobData = {
45
45
  url: input.url,
@@ -87,9 +87,9 @@ export declare const DEFAULT_WEBHOOK_RETRIES = 3;
87
87
  export declare abstract class WebhookService {
88
88
  abstract send<T extends SendWebhookInput>(input: Safe<T>): Promise<SendWebhookResult>;
89
89
  /** Produces the header value, `sha256=<hex>`, not the bare digest. */
90
- protected sign(secret: string, body: string): string;
90
+ protected sign(secret: string, body: string): Promise<string>;
91
91
  /** Public because receivers verify with it; they share the signing scheme. */
92
- verify(secret: string, signature: string, body: string): boolean;
92
+ verify(secret: string, signature: string, body: string): Promise<boolean>;
93
93
  /**
94
94
  * The three methods below are optional capability: the default queue-only
95
95
  * service keeps no history, and a store-backed implementation (e.g.
@@ -5,12 +5,14 @@ export const DEFAULT_WEBHOOK_SIGNATURE_HEADER = 'X-Pikku-Signature';
5
5
  export const DEFAULT_WEBHOOK_RETRIES = 3;
6
6
  export class WebhookService {
7
7
  /** Produces the header value, `sha256=<hex>`, not the bare digest. */
8
- sign(secret, body) {
9
- return `sha256=${hmacSha256Hex(secret, body)}`;
8
+ async sign(secret, body) {
9
+ return `sha256=${await hmacSha256Hex(secret, body)}`;
10
10
  }
11
11
  /** Public because receivers verify with it; they share the signing scheme. */
12
- verify(secret, signature, body) {
13
- return timingSafeStringEqual(this.sign(secret, body), signature);
12
+ async verify(secret, signature, body) {
13
+ if (!secret || !signature)
14
+ return false;
15
+ return timingSafeStringEqual(await this.sign(secret, body), signature);
14
16
  }
15
17
  /**
16
18
  * The three methods below are optional capability: the default queue-only
@@ -1,3 +1,4 @@
1
+ import type { AbortScope } from '../function/abort-scope.js';
1
2
  import type { IncomingWebhookService } from '../services/incoming-webhook-service.js';
2
3
  import type { TriggerSourceStore } from '../services/trigger-source-store.js';
3
4
  import type { LeaseService } from '../services/lease-service.js';
@@ -287,7 +288,15 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
287
288
  beginChanges: () => Promise<void>;
288
289
  }>;
289
290
  /** Wire as constructed by runners, before the function runner adds `rpc` and the credential readers. */
290
- export type PikkuRawWire = Omit<PikkuWire, 'rpc' | 'getCredential' | 'getCredentials'> & Partial<Pick<PikkuWire, 'getCredential' | 'getCredentials'>>;
291
+ export type PikkuRawWire = Omit<PikkuWire, 'rpc' | 'getCredential' | 'getCredentials'> & Partial<Pick<PikkuWire, 'getCredential' | 'getCredentials'>> & {
292
+ /**
293
+ * Whether whatever asked for this work is still there. Set by wirings that
294
+ * can be abandoned (an interrupted agent run); the function runner binds
295
+ * `wire.beginChanges` to it. Carried through nested `rpc` calls by the
296
+ * wire spread, so a sub-call observes the same scope.
297
+ */
298
+ abortScope?: AbortScope;
299
+ };
291
300
  export type CoreServices<SingletonServices = CoreSingletonServices> = SingletonServices;
292
301
  /** Strips `secrets` from a services type. */
293
302
  export type SecretlessServices<Services> = Omit<Services, 'secrets'>;
@@ -1,22 +1,37 @@
1
1
  /**
2
- * HMAC-SHA256 of a payload, hex-encoded. Senders wrap this in their own scheme
3
- * prefix (`sha256=`, `v0=`, …).
2
+ * Web Crypto only: no `node:crypto` and no `Buffer`, so this loads on edge
3
+ * runtimes. Web Crypto is async, so everything that touches a key returns a
4
+ * Promise; `timingSafeStringEqual` is plain JS and stays synchronous.
4
5
  */
5
- export declare function hmacSha256Hex(secret: string, payload: string): string;
6
6
  /**
7
- * Constant-time signature comparison. Returns false on a length mismatch,
8
- * where `timingSafeEqual` itself throws.
7
+ * Constant-time string comparison (pure JS; for opaque tokens, not HMACs, which
8
+ * `verifyHmacSignature` checks with Web Crypto). Returns false on a length mismatch
9
+ * (the lengths of the two values are not secret).
9
10
  */
10
11
  export declare function timingSafeStringEqual(a: string, b: string): boolean;
11
12
  export type WebhookPayload = string | Uint8Array;
12
13
  export type HmacAlgorithm = 'sha1' | 'sha256' | 'sha512';
13
14
  export type SecretEncoding = 'utf8' | 'hex' | 'base64';
15
+ /**
16
+ * HMAC-SHA256 of a payload, hex-encoded. Senders wrap this in their own scheme
17
+ * prefix (`sha256=`, `v0=`, …).
18
+ */
19
+ export declare function hmacSha256Hex(secret: string, payload: string): Promise<string>;
14
20
  /** The HMAC of `payload` under `secret`, for handshakes that answer with a digest. */
15
- export declare function hmacDigest(secret: string, algorithm: HmacAlgorithm, payload: WebhookPayload, encoding: 'hex' | 'base64', secretEncoding?: SecretEncoding): string;
16
- /** Whether `signature` is the HMAC of `payload` under `secret`, compared in constant time. */
17
- export declare function verifyHmacSignature(secret: string, signature: string | undefined, algorithm: HmacAlgorithm, payload: WebhookPayload, encoding: 'hex' | 'base64', secretEncoding?: SecretEncoding): boolean;
18
- /** Whether `signature` (base64) is `payload` signed by the private half of `publicKey` (PEM). */
21
+ export declare function hmacDigest(secret: string, algorithm: HmacAlgorithm, payload: WebhookPayload, encoding: 'hex' | 'base64', secretEncoding?: SecretEncoding): Promise<string>;
22
+ /**
23
+ * Whether `signature` is the HMAC of `payload` under `secret`. Fails closed on
24
+ * an empty secret (or one that decodes to an empty key) and on a missing or
25
+ * malformed signature. The comparison is `crypto.subtle.verify('HMAC', ...)`,
26
+ * so it runs in the platform's constant-time implementation.
27
+ */
28
+ export declare function verifyHmacSignature(secret: string, signature: string | undefined, algorithm: HmacAlgorithm, payload: WebhookPayload, encoding: 'hex' | 'base64', secretEncoding?: SecretEncoding): Promise<boolean>;
29
+ /**
30
+ * Whether `signature` (base64) is `payload` signed by the private half of
31
+ * `publicKey` (an RSA, EC or Ed25519 PEM `SPKI` key). RSA-PSS keys are not
32
+ * supported and verify as false. Ed25519 ignores `algorithm` and `dsaEncoding`.
33
+ */
19
34
  export declare function verifyPublicKeySignature(publicKey: string, signature: string | undefined, payload: WebhookPayload, options?: {
20
35
  algorithm?: string;
21
36
  dsaEncoding?: 'der' | 'ieee-p1363';
22
- }): boolean;
37
+ }): Promise<boolean>;