@pikku/cli 0.12.93 → 0.12.96

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 (180) hide show
  1. package/console-app/assets/{index-CmAplHjZ.js → index-CeQodYfg.js} +149 -149
  2. package/console-app/assets/{index-BYj7ZbUU.css → index-CyYHTGWZ.css} +1 -1
  3. package/console-app/index.html +2 -2
  4. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-channel.d.ts +13 -15
  8. package/dist/.pikku/cli/pikku-cli-channel.js +62 -3
  9. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +14 -4
  10. package/dist/.pikku/cli/pikku-cli-client.gen.js +20 -10
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  14. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +129 -2
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  18. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  20. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  21. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.d.ts +55 -20
  23. package/dist/.pikku/function/pikku-function-types.gen.js +27 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  25. package/dist/.pikku/function/pikku-functions-meta.gen.json +138 -13
  26. package/dist/.pikku/function/pikku-functions.gen.js +11 -1
  27. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  28. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  31. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +1 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  37. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  39. package/dist/.pikku/pikku-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  45. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +7 -0
  47. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
  48. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
  49. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
  50. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
  51. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
  52. package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
  53. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  54. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  55. package/dist/.pikku/schemas/register.gen.js +13 -3
  56. package/dist/.pikku/schemas/schemas/PersonaRunInput.schema.json +1 -0
  57. package/dist/.pikku/schemas/schemas/PersonaSyncInput.schema.json +1 -0
  58. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  59. package/dist/.pikku/schemas/schemas/PikkuPersonasInput.schema.json +1 -0
  60. package/dist/.pikku/schemas/schemas/PikkuRolesInput.schema.json +1 -0
  61. package/dist/.pikku/schemas/schemas/RolesAuditInput.schema.json +1 -0
  62. package/dist/.pikku/schemas/schemas/RolesPruneInput.schema.json +1 -0
  63. package/dist/.pikku/scopes/pikku-personas-meta.gen.json +1 -0
  64. package/dist/.pikku/scopes/pikku-personas.gen.d.ts +36 -0
  65. package/dist/.pikku/scopes/pikku-personas.gen.js +17 -0
  66. package/dist/.pikku/scopes/pikku-roles-meta.gen.json +1 -0
  67. package/dist/.pikku/scopes/pikku-roles.gen.d.ts +30 -0
  68. package/dist/.pikku/scopes/pikku-roles.gen.js +18 -0
  69. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +4 -2
  70. package/dist/.pikku/scopes/pikku-scope-types.gen.js +3 -2
  71. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  72. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +2 -2
  73. package/dist/.pikku/secrets/pikku-secret-types.gen.js +2 -2
  74. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  75. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  76. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  77. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  78. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +2 -2
  79. package/dist/.pikku/variables/pikku-variable-types.gen.js +2 -2
  80. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  81. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  82. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +27 -3
  83. package/dist/.pikku/workflow/pikku-personas.gen.d.ts +15 -0
  84. package/dist/.pikku/workflow/pikku-personas.gen.js +17 -0
  85. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +96 -19
  86. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +22 -2
  87. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  88. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  89. package/dist/bin/pikku-bin.mjs +2 -2
  90. package/dist/bin/pikku.js +0 -0
  91. package/dist/src/cli.wiring.js +101 -2
  92. package/dist/src/deploy/analyzer/analyzer.js +14 -0
  93. package/dist/src/deploy/analyzer/manifest.d.ts +11 -0
  94. package/dist/src/fabric/functions/validate.function.js +4 -4
  95. package/dist/src/functions/commands/bootstrap.js +3 -0
  96. package/dist/src/functions/commands/dev-ai-runner.d.ts +16 -4
  97. package/dist/src/functions/commands/dev-ai-runner.js +63 -29
  98. package/dist/src/functions/commands/dev.js +9 -4
  99. package/dist/src/functions/commands/enable.js +15 -19
  100. package/dist/src/functions/commands/{scenario-environment.d.ts → environment.d.ts} +9 -6
  101. package/dist/src/functions/commands/{scenario-environment.js → environment.js} +5 -4
  102. package/dist/src/functions/commands/new-addon.js +12 -12
  103. package/dist/src/functions/commands/persona-sync.d.ts +33 -0
  104. package/dist/src/functions/commands/persona-sync.js +151 -0
  105. package/dist/src/functions/commands/persona.d.ts +58 -0
  106. package/dist/src/functions/commands/persona.js +217 -0
  107. package/dist/src/functions/commands/roles-audit.d.ts +1 -0
  108. package/dist/src/functions/commands/roles-audit.js +37 -0
  109. package/dist/src/functions/commands/roles-prune.d.ts +16 -0
  110. package/dist/src/functions/commands/roles-prune.js +52 -0
  111. package/dist/src/functions/commands/roles-shared.d.ts +40 -0
  112. package/dist/src/functions/commands/roles-shared.js +77 -0
  113. package/dist/src/functions/commands/scenario-browser.d.ts +3 -3
  114. package/dist/src/functions/commands/scenario-browser.js +1 -1
  115. package/dist/src/functions/commands/scenario-ladder.d.ts +44 -2
  116. package/dist/src/functions/commands/scenario-ladder.js +51 -2
  117. package/dist/src/functions/commands/scenario.d.ts +7 -0
  118. package/dist/src/functions/commands/scenario.js +79 -17
  119. package/dist/src/functions/commands/scopes-shared.d.ts +8 -0
  120. package/dist/src/functions/commands/scopes-shared.js +1 -1
  121. package/dist/src/functions/commands/serve.js +7 -3
  122. package/dist/src/functions/commands/virtual-user-formatter.d.ts +30 -0
  123. package/dist/src/functions/commands/virtual-user-formatter.js +65 -0
  124. package/dist/src/functions/db/local-db.js +38 -1
  125. package/dist/src/functions/validate/workspace-validate.js +1 -1
  126. package/dist/src/functions/wirings/ai-agent/pikku-command-public-agent.js +2 -1
  127. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +1 -0
  128. package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +2 -2
  129. package/dist/src/functions/wirings/auth/serialize-auth-gen.js +8 -8
  130. package/dist/src/functions/wirings/cli/serialize-channel-cli-client.d.ts +7 -3
  131. package/dist/src/functions/wirings/cli/serialize-channel-cli-client.js +32 -18
  132. package/dist/src/functions/wirings/cli/serialize-channel-cli.js +63 -6
  133. package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -7
  134. package/dist/src/functions/wirings/functions/serialize-function-types.js +88 -28
  135. package/dist/src/functions/wirings/middleware/serialize-middleware-imports.js +25 -9
  136. package/dist/src/functions/wirings/package/serialize-package-types.js +5 -3
  137. package/dist/src/functions/wirings/personas/pikku-command-personas.d.ts +7 -0
  138. package/dist/src/functions/wirings/personas/pikku-command-personas.js +43 -0
  139. package/dist/src/functions/wirings/personas/serialize-personas-types.d.ts +18 -0
  140. package/dist/src/functions/wirings/personas/serialize-personas-types.js +76 -0
  141. package/dist/src/functions/wirings/realtime/pikku-command-events-scaffold.js +2 -1
  142. package/dist/src/functions/wirings/roles/pikku-command-roles.d.ts +7 -0
  143. package/dist/src/functions/wirings/roles/pikku-command-roles.js +33 -0
  144. package/dist/src/functions/wirings/roles/serialize-roles-types.d.ts +14 -0
  145. package/dist/src/functions/wirings/roles/serialize-roles-types.js +55 -0
  146. package/dist/src/functions/wirings/rpc/pikku-command-public-rpc.js +2 -1
  147. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +3 -3
  148. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +3 -3
  149. package/dist/src/functions/wirings/scopes/serialize-scopes-types.d.ts +1 -1
  150. package/dist/src/functions/wirings/scopes/serialize-scopes-types.js +1 -1
  151. package/dist/src/functions/wirings/user-admin/pikku-command-user-admin-functions.js +2 -1
  152. package/dist/src/functions/wirings/user-admin/serialize-user-admin-functions.js +2 -2
  153. package/dist/src/functions/wirings/workflow/pikku-command-workflow-routes.js +2 -1
  154. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +13 -13
  155. package/dist/src/functions/wirings/workflow/serialize-personas.d.ts +10 -0
  156. package/dist/src/functions/wirings/workflow/serialize-personas.js +46 -0
  157. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +11 -2
  158. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +165 -29
  159. package/dist/src/functions/workflows/all.workflow.js +7 -1
  160. package/dist/src/server/content-signing-jwt.d.ts +13 -0
  161. package/dist/src/server/content-signing-jwt.js +51 -0
  162. package/dist/src/server/dev-server-runner.interface.d.ts +10 -2
  163. package/dist/src/server/node-server-runner.d.ts +2 -2
  164. package/dist/src/server/node-server-runner.js +2 -1
  165. package/dist/src/services.d.ts +3 -1
  166. package/dist/src/services.js +2 -1
  167. package/dist/src/utils/pikku-cli-config.js +45 -2
  168. package/dist/src/utils/resolve-personas.d.ts +32 -0
  169. package/dist/src/utils/resolve-personas.js +46 -0
  170. package/dist/src/utils/resolve-scaffold-feature.d.ts +19 -0
  171. package/dist/src/utils/resolve-scaffold-feature.js +49 -0
  172. package/dist/tsconfig.tsbuildinfo +1 -1
  173. package/package.json +17 -14
  174. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +0 -1
  175. package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +0 -19
  176. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +0 -17
  177. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +0 -2
  178. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +0 -40
  179. package/dist/src/utils/resolve-scenario-actors.d.ts +0 -23
  180. package/dist/src/utils/resolve-scenario-actors.js +0 -74
@@ -7,10 +7,22 @@ import type { AIAgentRunnerService } from '@pikku/core/services';
7
7
  * has no equivalent, so agents 503 with AIProviderNotConfiguredError unless we
8
8
  * construct one here. When an OpenAI-compatible base URL + key are present
9
9
  * (fabric injects LITELLM_PROXY_URL/LITELLM_API_KEY; the standard OPENAI_*
10
- * vars are also honored) we point a single openai-compatible provider at it and
11
- * register it under every common provider prefix. Returns undefined when no AI
12
- * env is configured (agents stay disabled, with the clear downstream error) or
13
- * when the AI SDK packages aren't installed in the project.
10
+ * vars are also honored) we point one provider at it and register it under
11
+ * `'*'`, so every provider prefix resolves to it. The CLI ships the SDK rather
12
+ * than asking the project for it behind a proxy one openai-compatible
13
+ * provider answers for `openai/…`, `anthropic/…` and `google/…` alike, so there
14
+ * was never a per-vendor package worth making somebody install. The project's
15
+ * own copies still win when it has them. Returns undefined when no AI env is
16
+ * configured (agents stay disabled, with the clear downstream error) or when
17
+ * the SDK packages cannot be loaded from either place.
18
+ *
19
+ * Audio does not work here: `@ai-sdk/openai-compatible` exposes only
20
+ * language/embedding/image models, so a voice agent under `pikku dev` throws
21
+ * `Provider does not support transcription models`. Fixing that means the full
22
+ * `@ai-sdk/openai` provider, which assumes OpenAI's own request specifics — a
23
+ * bad default for a path whose entire purpose is fronting arbitrary gateways.
24
+ * If it becomes worth it, delegate just `transcription`/`speech` to the full
25
+ * provider and leave everything else on this one.
14
26
  */
15
27
  export declare function createDevAIAgentRunner({ logger, projectRoot, variables, }: {
16
28
  logger: Logger;
@@ -1,19 +1,6 @@
1
1
  import { createRequire } from 'module';
2
2
  import { join } from 'path';
3
3
  import { pathToFileURL } from 'url';
4
- // Provider prefixes a fabric/proxy baseURL fronts. Models are written as
5
- // `openai/gpt-4o-mini`, `openai/deepseek-v4-flash`, etc. — the runner splits on
6
- // the first `/`, so the prefix only selects the provider entry and the bare
7
- // model name is forwarded to the OpenAI-compatible proxy, which routes it.
8
- const PROXY_PROVIDER_NAMES = [
9
- 'openai',
10
- 'anthropic',
11
- 'google',
12
- 'gemini',
13
- 'deepseek',
14
- 'xai',
15
- 'litellm',
16
- ];
17
4
  /**
18
5
  * Build the AI agent runner for `pikku dev` from env.
19
6
  *
@@ -21,10 +8,22 @@ const PROXY_PROVIDER_NAMES = [
21
8
  * has no equivalent, so agents 503 with AIProviderNotConfiguredError unless we
22
9
  * construct one here. When an OpenAI-compatible base URL + key are present
23
10
  * (fabric injects LITELLM_PROXY_URL/LITELLM_API_KEY; the standard OPENAI_*
24
- * vars are also honored) we point a single openai-compatible provider at it and
25
- * register it under every common provider prefix. Returns undefined when no AI
26
- * env is configured (agents stay disabled, with the clear downstream error) or
27
- * when the AI SDK packages aren't installed in the project.
11
+ * vars are also honored) we point one provider at it and register it under
12
+ * `'*'`, so every provider prefix resolves to it. The CLI ships the SDK rather
13
+ * than asking the project for it behind a proxy one openai-compatible
14
+ * provider answers for `openai/…`, `anthropic/…` and `google/…` alike, so there
15
+ * was never a per-vendor package worth making somebody install. The project's
16
+ * own copies still win when it has them. Returns undefined when no AI env is
17
+ * configured (agents stay disabled, with the clear downstream error) or when
18
+ * the SDK packages cannot be loaded from either place.
19
+ *
20
+ * Audio does not work here: `@ai-sdk/openai-compatible` exposes only
21
+ * language/embedding/image models, so a voice agent under `pikku dev` throws
22
+ * `Provider does not support transcription models`. Fixing that means the full
23
+ * `@ai-sdk/openai` provider, which assumes OpenAI's own request specifics — a
24
+ * bad default for a path whose entire purpose is fronting arbitrary gateways.
25
+ * If it becomes worth it, delegate just `transcription`/`speech` to the full
26
+ * provider and leave everything else on this one.
28
27
  */
29
28
  export async function createDevAIAgentRunner({ logger, projectRoot, variables, }) {
30
29
  // Pair the URL with its matching key — coalescing each var independently could
@@ -43,28 +42,63 @@ export async function createDevAIAgentRunner({ logger, projectRoot, variables, }
43
42
  logger.debug('pikku dev: no AI provider env (OPENAI_BASE_URL/OPENAI_API_KEY or LITELLM_PROXY_URL/LITELLM_API_KEY) — AI agents disabled');
44
43
  return undefined;
45
44
  }
46
- // Resolve from the project's node_modules the AI SDK packages are the
47
- // project's deps, not the CLI's, so they share the project's `ai` version.
48
- const require = createRequire(pathToFileURL(join(projectRoot, 'package.json')).href);
45
+ // The project's own copies win, so an app that pins an `ai` version gets a
46
+ // runner built from that one rather than a second copy alongside it. The
47
+ // CLI's are the fallback: this function exists for projects that have not
48
+ // installed an AI SDK at all, and behind a proxy a single openai-compatible
49
+ // provider reaches every model, so making someone add three packages before
50
+ // agents work in dev buys nothing.
51
+ const fromProject = createRequire(pathToFileURL(join(projectRoot, 'package.json')).href);
52
+ //
53
+ // Both come from the same place or neither does. A provider built against one
54
+ // copy of `@ai-sdk/provider` handed to a runner built against another is the
55
+ // one combination that fails at call time rather than at load time, with an
56
+ // error about the model spec that names neither package.
57
+ const loadPair = async () => {
58
+ const project = ['@pikku/ai-vercel', '@ai-sdk/openai-compatible'].map((name) => {
59
+ try {
60
+ return pathToFileURL(fromProject.resolve(name)).href;
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ });
66
+ const [runner, provider] = project.every(Boolean)
67
+ ? project
68
+ : ['@pikku/ai-vercel', '@ai-sdk/openai-compatible'];
69
+ return Promise.all([import(runner), import(provider)]);
70
+ };
49
71
  let VercelAIAgentRunner;
50
72
  let createOpenAICompatible;
51
73
  try {
52
- ;
53
- ({ VercelAIAgentRunner } = await import(pathToFileURL(require.resolve('@pikku/ai-vercel')).href));
54
- ({ createOpenAICompatible } = await import(pathToFileURL(require.resolve('@ai-sdk/openai-compatible')).href));
74
+ const [runnerModule, providerModule] = await loadPair();
75
+ ({ VercelAIAgentRunner } = runnerModule);
76
+ ({ createOpenAICompatible } = providerModule);
55
77
  }
56
78
  catch (error) {
57
- logger.warn(`pikku dev: AI provider env is set but the AI SDK packages could not be loaded (install @pikku/ai-vercel, @ai-sdk/openai-compatible, and ai) — AI agents disabled: ${error instanceof Error ? error.message : String(error)}`);
79
+ logger.warn(`pikku dev: AI provider env is set but the AI SDK packages could not be loaded — AI agents disabled: ${error instanceof Error ? error.message : String(error)}`);
58
80
  return undefined;
59
81
  }
60
- const buildProviders = (key) => {
61
- const provider = createOpenAICompatible({
82
+ // One provider under '*', so every prefix a model might name resolves to the
83
+ // proxy including ones nobody thought to list. This was a hardcoded array
84
+ // of seven names, which silently excluded everything else.
85
+ //
86
+ // `supportsStructuredOutputs` defaults to false, and the default fails in the
87
+ // worst available way: an `outputSchema` is dropped, the call degrades to
88
+ // bare `json_object`, and the model returns well-formed JSON with whatever
89
+ // keys it felt like. Nothing errors — the caller reads `undefined` off every
90
+ // field it asked for and behaves as though the model said nothing. Anything
91
+ // fronting this URL that is worth calling speaks `json_schema`; the ones that
92
+ // do not now fail loudly on the first schema'd call, which is the outcome we
93
+ // want over silently ignoring the schema.
94
+ const buildProviders = (key) => ({
95
+ '*': createOpenAICompatible({
62
96
  name: 'pikku-dev',
63
97
  baseURL,
64
98
  apiKey: key,
65
- });
66
- return Object.fromEntries(PROXY_PROVIDER_NAMES.map((name) => [name, provider]));
67
- };
99
+ supportsStructuredOutputs: true,
100
+ }),
101
+ });
68
102
  logger.info(`pikku dev: AI agent runner wired to ${baseURL}`);
69
103
  return new VercelAIAgentRunner(buildProviders(apiKey), buildProviders);
70
104
  }
@@ -16,6 +16,8 @@ import { startCoverageService } from './start-coverage.js';
16
16
  import { createDevAIAgentRunner } from './dev-ai-runner.js';
17
17
  import { resolveConsoleMount } from './serve-console.js';
18
18
  import { serverReadyLine } from '../../server/server-ready.js';
19
+ import { createEphemeralContentSigningJWT } from '../../server/content-signing-jwt.js';
20
+ import { resolveScaffoldFeature } from '../../utils/resolve-scaffold-feature.js';
19
21
  export const dev = pikkuSessionlessFunc({
20
22
  remote: true,
21
23
  func: async ({ logger, config, getInspectorState, invalidateInspectorState, variables, devServerRunner, }, { port, watch, hmr, coverage, test }, { rpc }) => {
@@ -122,7 +124,7 @@ export const dev = pikkuSessionlessFunc({
122
124
  // project — an app bootstrap is what a deployed bundle imports, and coverage
123
125
  // endpoints have no business in one.
124
126
  if (config.scaffold?.scenarios) {
125
- registerScenarioInstrumentation(config.scaffold.scenarios === 'auth');
127
+ registerScenarioInstrumentation(resolveScaffoldFeature('scenarios', config.scaffold.scenarios).auth);
126
128
  }
127
129
  const configModule = await loadUserModule(pikkuConfigFactory.file);
128
130
  const servicesModule = await loadUserModule(singletonServicesFactory.file);
@@ -150,8 +152,11 @@ export const dev = pikkuSessionlessFunc({
150
152
  sizeLimit: userConfig.content.sizeLimit,
151
153
  }
152
154
  : undefined;
153
- const localContent = localContentConfig
154
- ? new LocalContent(localContentConfig, logger)
155
+ const contentSigningJWT = localContentConfig
156
+ ? createEphemeralContentSigningJWT()
157
+ : undefined;
158
+ const localContent = localContentConfig && contentSigningJWT
159
+ ? new LocalContent(localContentConfig, logger, contentSigningJWT)
155
160
  : undefined;
156
161
  const schedulerService = new InMemorySchedulerService();
157
162
  const aiStorage = kysely
@@ -239,7 +244,7 @@ export const dev = pikkuSessionlessFunc({
239
244
  port: resolvedPort,
240
245
  content: localContentConfig,
241
246
  ...(consoleMount ? { staticMounts: [consoleMount] } : {}),
242
- }, logger);
247
+ }, logger, { contentSigningJWT });
243
248
  const lifecycle = await loadLifecycle();
244
249
  await pikkuServer.init();
245
250
  await lifecycle?.beforeStart?.(resolvedServices);
@@ -1,15 +1,12 @@
1
1
  import { readFile, writeFile } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
  import { pikkuVoidFunc } from '#pikku';
4
- const FEATURE_DEFAULTS = {
5
- rpc: 'auth',
6
- agent: 'auth',
7
- console: 'auth',
8
- scenarios: 'auth',
9
- workflow: 'auth',
10
- events: 'auth',
11
- remoteRpc: 'no-auth',
12
- };
4
+ /**
5
+ * Remote RPC workers carry no auth dimension — `serialize-remote-rpc.ts`
6
+ * hardcodes `auth: false` on what it generates — so enabling it writes a bare
7
+ * `true` and there is nothing to opt out of.
8
+ */
9
+ const NO_AUTH_DIMENSION = new Set(['remoteRpc']);
13
10
  async function enableFeature(feature, logger, config, data) {
14
11
  const noAuth = data?.noAuth ?? false;
15
12
  const configPath = join(config.configDir, 'pikku.config.json');
@@ -21,18 +18,17 @@ async function enableFeature(feature, logger, config, data) {
21
18
  if (!json.scaffold.pikkuDir) {
22
19
  json.scaffold.pikkuDir = 'pikku';
23
20
  }
24
- // The console is an admin surface every RPC requires a session, so it can
25
- // never be scaffolded no-auth (the --no-auth flag is ignored for it).
26
- const value = feature === 'webhook'
27
- ? true
28
- : feature === 'console'
29
- ? 'auth'
30
- : noAuth
31
- ? 'no-auth'
32
- : FEATURE_DEFAULTS[feature];
21
+ // `true` is enabled AND authenticated. Only an explicit --no-auth writes the
22
+ // object form, so a feature can never be turned on public by accident.
23
+ //
24
+ // The console is an admin surface — every RPC requires a session, so
25
+ // --no-auth is ignored for it.
26
+ const wantsPublic = noAuth && feature !== 'console' && !NO_AUTH_DIMENSION.has(feature);
27
+ const value = feature === 'webhook' ? true : wantsPublic ? { auth: false } : true;
33
28
  json.scaffold[feature] = value;
34
29
  await writeFile(configPath, JSON.stringify(json, null, 2) + '\n', 'utf-8');
35
- logger.info(`Enabled scaffold.${feature} = '${value}' in ${configPath}`);
30
+ logger.info(`Enabled scaffold.${feature} = ${JSON.stringify(value)} in ${configPath}` +
31
+ (wantsPublic ? ' (public — no session required)' : ''));
36
32
  }
37
33
  export const enableRpc = pikkuVoidFunc({
38
34
  func: async ({ logger, config }, data) => enableFeature('rpc', logger, config, data),
@@ -1,22 +1,25 @@
1
1
  /**
2
- * Resolving `pikku scenario run <environment>` to the URLs the run targets.
2
+ * Resolving `pikku scenario run <environment>` and `pikku persona run
3
+ * <environment>` to the URLs the run targets.
3
4
  *
4
5
  * The configured environment is the base; `--api-url`/`--app-url` override it
5
6
  * for one invocation, which is what makes a suite runnable against a target
6
7
  * that only exists at run time (a freshly provisioned sandbox) without writing
7
8
  * a pikku.config.json per deployment.
8
9
  */
9
- /** One entry of `scenarios.environments` in pikku.config.json. */
10
- export interface ScenarioEnvironment {
10
+ /** One entry of `environments` in pikku.config.json. */
11
+ export interface PikkuEnvironment {
11
12
  apiUrl: string;
12
13
  signInPath?: string;
13
14
  rpcPath?: string;
14
15
  appUrl?: string;
16
+ /** Real consequences: only an `accountable` persona may run against it. */
17
+ production?: boolean;
15
18
  }
16
- export interface ResolveScenarioEnvironmentOptions {
19
+ export interface ResolveEnvironmentOptions {
17
20
  /** The name given on the command line. Must exist in config. */
18
21
  environment: string;
19
- environments: Record<string, ScenarioEnvironment>;
22
+ environments: Record<string, PikkuEnvironment>;
20
23
  /** `--api-url`, overriding the configured `apiUrl`. */
21
24
  apiUrl?: string;
22
25
  /** `--app-url`, overriding the configured `appUrl`. */
@@ -29,4 +32,4 @@ export interface ResolveScenarioEnvironmentOptions {
29
32
  * all see the same target. The environment is still looked up by name: the
30
33
  * flags override fields on a configured environment, they do not invent one.
31
34
  */
32
- export declare const resolveScenarioEnvironment: ({ environment, environments, apiUrl, appUrl, spawn, }: ResolveScenarioEnvironmentOptions) => ScenarioEnvironment;
35
+ export declare const resolveEnvironment: ({ environment, environments, apiUrl, appUrl, spawn, }: ResolveEnvironmentOptions) => PikkuEnvironment;
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Resolving `pikku scenario run <environment>` to the URLs the run targets.
2
+ * Resolving `pikku scenario run <environment>` and `pikku persona run
3
+ * <environment>` to the URLs the run targets.
3
4
  *
4
5
  * The configured environment is the base; `--api-url`/`--app-url` override it
5
6
  * for one invocation, which is what makes a suite runnable against a target
@@ -35,14 +36,14 @@ const assertAbsoluteUrl = (flag, value) => {
35
36
  * all see the same target. The environment is still looked up by name: the
36
37
  * flags override fields on a configured environment, they do not invent one.
37
38
  */
38
- export const resolveScenarioEnvironment = ({ environment, environments, apiUrl, appUrl, spawn = false, }) => {
39
+ export const resolveEnvironment = ({ environment, environments, apiUrl, appUrl, spawn = false, }) => {
39
40
  const configured = environments[environment];
40
41
  if (!configured) {
41
42
  const known = Object.keys(environments);
42
- throw new Error(`Unknown scenario environment '${environment}'. ` +
43
+ throw new Error(`Unknown environment '${environment}'. ` +
43
44
  (known.length
44
45
  ? `Configured environments: ${known.join(', ')}`
45
- : `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
46
+ : `Add environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
46
47
  }
47
48
  const resolved = { ...configured };
48
49
  if (apiUrl !== undefined) {
@@ -566,13 +566,13 @@ export interface Services extends CoreServices<SingletonServices> {}
566
566
  // Conditional: credential / secret file
567
567
  if (flags.credential === 'apikey') {
568
568
  files[`src/${name}.credential.ts`] = `import { z } from 'zod'
569
- import { wireCredential } from '@pikku/core/credential'
569
+ import { defineCredential } from '@pikku/core/credential'
570
570
 
571
571
  export const ${camelName}CredentialSchema = z.object({
572
572
  apiKey: z.string().describe(${literal(`${displayName} API key`)}),
573
573
  })
574
574
 
575
- wireCredential({
575
+ defineCredential({
576
576
  name: '${camelName}',
577
577
  displayName: ${literal(displayName)},
578
578
  description: ${literal(description)},
@@ -588,13 +588,13 @@ wireCredential({
588
588
  tenantId: z.string().optional().describe('Upstream tenant id'),`
589
589
  : ` token: z.string().describe(${literal(`${displayName} bearer token`)}),`;
590
590
  files[`src/${name}.credential.ts`] = `import { z } from 'zod'
591
- import { wireCredential } from '@pikku/core/credential'
591
+ import { defineCredential } from '@pikku/core/credential'
592
592
 
593
593
  export const ${camelName}CredentialSchema = z.object({
594
594
  ${credentialFields}
595
595
  })
596
596
 
597
- wireCredential({
597
+ defineCredential({
598
598
  name: '${camelName}',
599
599
  displayName: ${literal(displayName)},
600
600
  description: ${literal(description)},
@@ -605,15 +605,15 @@ wireCredential({
605
605
  }
606
606
  else if (flags.oauth || flags.credential === 'oauth2') {
607
607
  files[`src/${name}.credential.ts`] = `import { z } from 'zod'
608
- import { wireCredential } from '@pikku/core/credential'
609
- import { wireSecret } from '@pikku/core/secret'
608
+ import { defineCredential } from '@pikku/core/credential'
609
+ import { defineSecret } from '@pikku/core/secret'
610
610
 
611
611
  export const ${camelName}TokenSchema = z.object({
612
612
  accessToken: z.string(),
613
613
  refreshToken: z.string().optional(),
614
614
  })
615
615
 
616
- wireCredential({
616
+ defineCredential({
617
617
  name: '${camelName}',
618
618
  displayName: ${literal(displayName)},
619
619
  description: ${literal(description)},
@@ -628,7 +628,7 @@ wireCredential({
628
628
  },
629
629
  })
630
630
 
631
- wireSecret({
631
+ defineSecret({
632
632
  name: '${camelName}OAuthApp',
633
633
  displayName: ${literal(`${displayName} OAuth App`)},
634
634
  description: ${literal(`OAuth2 app credentials for ${displayName}`)},
@@ -638,7 +638,7 @@ wireSecret({
638
638
  }
639
639
  else if (flags.secret) {
640
640
  files[`src/${name}.secret.ts`] = `import { z } from 'zod'
641
- import { wireSecret } from '@pikku/core/secret'
641
+ import { defineSecret } from '@pikku/core/secret'
642
642
 
643
643
  export const ${camelName}SecretsSchema = z.object({
644
644
  apiKey: z.string().describe(${literal(`${displayName} API key`)}),
@@ -647,7 +647,7 @@ export const ${camelName}SecretsSchema = z.object({
647
647
 
648
648
  export type ${pascalName}Secrets = z.infer<typeof ${camelName}SecretsSchema>
649
649
 
650
- wireSecret({
650
+ defineSecret({
651
651
  name: '${camelName}',
652
652
  displayName: ${literal(`${displayName} API`)},
653
653
  description: ${literal(description)},
@@ -659,11 +659,11 @@ wireSecret({
659
659
  // Conditional: variable file
660
660
  if (flags.variable) {
661
661
  files[`src/${name}.variable.ts`] = `import { z } from 'zod'
662
- import { wireVariable } from '@pikku/core/variable'
662
+ import { defineVariable } from '@pikku/core/variable'
663
663
 
664
664
  export const ${camelName}VariableSchema = z.string().optional().describe('TODO: describe this variable')
665
665
 
666
- wireVariable({
666
+ defineVariable({
667
667
  name: '${camelName}_variable',
668
668
  displayName: ${literal(`${displayName} Variable`)},
669
669
  description: 'TODO: describe this variable',
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Provision the declared personas into an environment: their accounts, and the
3
+ * roles they declare.
4
+ *
5
+ * This is deployment, not seeding. `pikku db seed` applies a project's test
6
+ * fixtures and does not run in production; an accountable persona doing a real
7
+ * job in production still needs an account and its grants, and this is where
8
+ * they come from.
9
+ *
10
+ * Additive in the same way `syncSystemRoles` is: it creates and grants, and
11
+ * never deletes or revokes. Removing a persona from the code leaves their
12
+ * account and their grants standing until somebody decides otherwise — which is
13
+ * the point, because the alternative is a rolling deploy quietly locking out the
14
+ * teammate that the older replica is still running.
15
+ *
16
+ * It needs both halves of an environment: its API, to sign the persona in, and
17
+ * its database, to write the grants. They must be the same deployment — the
18
+ * command has no way to check that, so pointing it at a stage's API and a local
19
+ * database provisions nobody usefully.
20
+ */
21
+ export declare const personaSync: import("#pikku").PikkuFunctionConfig<{
22
+ environment: string;
23
+ apiUrl?: string;
24
+ dryRun?: boolean;
25
+ }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
26
+ environment: string;
27
+ apiUrl?: string;
28
+ dryRun?: boolean;
29
+ }, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
30
+ environment: string;
31
+ apiUrl?: string;
32
+ dryRun?: boolean;
33
+ }, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
@@ -0,0 +1,151 @@
1
+ import { pikkuSessionlessFunc } from '#pikku';
2
+ import { createHttpPersonas } from '@pikku/core/persona';
3
+ import { personaEnvironmentRefusal } from '@pikku/core/persona';
4
+ import { resolvePersonas } from '../../utils/resolve-personas.js';
5
+ import { resolveEnvironment } from './environment.js';
6
+ import { loadDeclaredRoles, openScopeServiceForRoles } from './roles-shared.js';
7
+ /**
8
+ * The user id behind a persona's address.
9
+ *
10
+ * Read straight off better-auth's own `user` table rather than through the
11
+ * ScopeService, because the ScopeService has no notion of an address — its
12
+ * grants are keyed by user id, and `pikku_user_role.user_id` already FKs into
13
+ * this exact table. Looking it up anywhere else would mean inventing a second
14
+ * account concept for personas, which is the one thing this command exists to
15
+ * avoid: a persona is a user, provisioned the way users are.
16
+ */
17
+ const userIdByEmail = async (db, email) => {
18
+ const row = await db
19
+ .selectFrom('user')
20
+ .select('id')
21
+ .where('email', '=', email)
22
+ .executeTakeFirst();
23
+ return row ? String(row.id) : null;
24
+ };
25
+ /**
26
+ * Provision the declared personas into an environment: their accounts, and the
27
+ * roles they declare.
28
+ *
29
+ * This is deployment, not seeding. `pikku db seed` applies a project's test
30
+ * fixtures and does not run in production; an accountable persona doing a real
31
+ * job in production still needs an account and its grants, and this is where
32
+ * they come from.
33
+ *
34
+ * Additive in the same way `syncSystemRoles` is: it creates and grants, and
35
+ * never deletes or revokes. Removing a persona from the code leaves their
36
+ * account and their grants standing until somebody decides otherwise — which is
37
+ * the point, because the alternative is a rolling deploy quietly locking out the
38
+ * teammate that the older replica is still running.
39
+ *
40
+ * It needs both halves of an environment: its API, to sign the persona in, and
41
+ * its database, to write the grants. They must be the same deployment — the
42
+ * command has no way to check that, so pointing it at a stage's API and a local
43
+ * database provisions nobody usefully.
44
+ */
45
+ export const personaSync = pikkuSessionlessFunc({
46
+ remote: true,
47
+ func: async ({ logger, config, getInspectorState, variables }, { environment, apiUrl, dryRun }) => {
48
+ const state = await getInspectorState(true, false, false, true);
49
+ const personas = resolvePersonas(state.personas?.definitions ?? [], config.scenarios?.emailDomain);
50
+ if (Object.keys(personas).length === 0) {
51
+ logger.info('persona sync: no personas are declared — add a definePersonas({ … }) call.');
52
+ return;
53
+ }
54
+ const environments = config.environments ?? {};
55
+ const env = resolveEnvironment({ environment, environments, apiUrl });
56
+ // The same rule that decides who may *run* against an environment decides
57
+ // who may be provisioned into it. Two rules would drift, and the one that
58
+ // drifted would leave an account standing in production for a persona the
59
+ // engine then refuses to sign in.
60
+ //
61
+ // A `runnable: false` persona is still provisioned: their account is the
62
+ // whole reason they were declared, because other people act on it.
63
+ const eligible = [];
64
+ const skipped = [];
65
+ for (const [id, persona] of Object.entries(personas)) {
66
+ const refusal = personaEnvironmentRefusal(id, persona, environment, environments);
67
+ if (refusal) {
68
+ skipped.push(refusal);
69
+ continue;
70
+ }
71
+ eligible.push([id, persona]);
72
+ }
73
+ if (eligible.length === 0) {
74
+ logger.info(`persona sync: no persona may act in '${environment}' — ${skipped.length} declared persona(s) were all skipped.`);
75
+ logger.info(' Run with --dry-run for the reasons, or give the persona this environment and disposition `accountable` if it is production.');
76
+ return;
77
+ }
78
+ if (dryRun) {
79
+ logger.info(`persona sync (dry run): ${eligible.length} persona(s) would be provisioned into '${environment}' at ${env.apiUrl}:`);
80
+ for (const [id, persona] of eligible) {
81
+ logger.info(` ${id.padEnd(20)} ${persona.email}${persona.roles.length
82
+ ? ` -> ${persona.roles.join(' + ')}`
83
+ : ' (no roles)'}`);
84
+ }
85
+ for (const refusal of skipped) {
86
+ logger.info(` skipped: ${refusal}`);
87
+ }
88
+ logger.info('Re-run without --dry-run to apply.');
89
+ return;
90
+ }
91
+ const secret = await variables.get('SCENARIO_ACTOR_SECRET');
92
+ if (!secret) {
93
+ throw new Error('SCENARIO_ACTOR_SECRET is not set — a persona account cannot be created. ' +
94
+ 'Export it in the environment running this command (never put it in pikku.config.json).');
95
+ }
96
+ const declaredRoles = await loadDeclaredRoles(config.rolesMetaJsonFile, logger);
97
+ if (!declaredRoles) {
98
+ throw new Error('role metadata not found');
99
+ }
100
+ const signedIn = createHttpPersonas({
101
+ apiUrl: env.apiUrl,
102
+ secret,
103
+ personas: Object.fromEntries(eligible),
104
+ signInPath: env.signInPath,
105
+ rpcPath: env.rpcPath,
106
+ });
107
+ // Roles are synced on the way in, so a grant can name a role this deploy
108
+ // introduced. Both that sync and everything below are additive.
109
+ const opened = await openScopeServiceForRoles({ config, logger }, declaredRoles, 'pikku persona sync');
110
+ if (!opened) {
111
+ return;
112
+ }
113
+ try {
114
+ let accounts = 0;
115
+ let granted = 0;
116
+ let held = 0;
117
+ for (const [id, persona] of eligible) {
118
+ // Signing in is what creates the account: the actor plugin upserts an
119
+ // `actor: true` user row on first sign-in, and nothing else does.
120
+ // `sessionRoles()` is the public call that forces it — its answer is a
121
+ // by-product here, not the reason for the call.
122
+ await signedIn[id].sessionRoles();
123
+ accounts++;
124
+ if (persona.roles.length === 0) {
125
+ logger.info(` ${id.padEnd(20)} ${persona.email} (no roles)`);
126
+ continue;
127
+ }
128
+ const userId = await userIdByEmail(opened.db, persona.email);
129
+ if (!userId) {
130
+ throw new Error(`persona sync: signed '${id}' in at ${env.apiUrl}, but no user row for ${persona.email} exists in the database this command opened. ` +
131
+ `That database is not the one '${environment}' runs on, so its grants would land nowhere.`);
132
+ }
133
+ const already = new Set(await opened.service.listUserRoles(userId));
134
+ const missing = persona.roles.filter((role) => !already.has(role));
135
+ for (const role of missing) {
136
+ await opened.service.addUserToRole(userId, role);
137
+ }
138
+ granted += missing.length;
139
+ held += persona.roles.length - missing.length;
140
+ logger.info(` ${id.padEnd(20)} ${persona.email} -> ${persona.roles.join(' + ')}${missing.length === 0 ? ' (already held)' : ''}`);
141
+ }
142
+ logger.info(`persona sync: ${accounts} account(s) in '${environment}', ${granted} role grant(s) applied, ${held} already held`);
143
+ if (skipped.length > 0) {
144
+ logger.info(` ${skipped.length} persona(s) skipped — they do not act in '${environment}'. Run with --dry-run for the reasons.`);
145
+ }
146
+ }
147
+ finally {
148
+ await opened.destroy();
149
+ }
150
+ },
151
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Run one declared persona against a real stage as a virtual user.
3
+ *
4
+ * Everything it needs is already in the project: the catalogue is the function
5
+ * meta, the intents are the scenarios' own prose, the identity is the persona
6
+ * signing in over the app's real auth, and the scopes come from the roles they
7
+ * declared. The only new input is which person to be — everything else is a
8
+ * flag for narrowing or replaying a run.
9
+ *
10
+ * This is deliberately not a test runner. It asserts nothing, and a green run
11
+ * proves nothing — what it produces is findings, and the absence of them is
12
+ * only ever "not this time, not with this seed".
13
+ */
14
+ export declare const personaRun: import("#pikku").PikkuFunctionConfig<{
15
+ environment: string;
16
+ persona: string;
17
+ disposition?: string;
18
+ goals?: string;
19
+ steps?: string;
20
+ mutations?: string;
21
+ duration?: string;
22
+ seed?: string;
23
+ model?: string;
24
+ apiUrl?: string;
25
+ allowApproval?: boolean;
26
+ skipRoleCheck?: boolean;
27
+ out?: string;
28
+ }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
29
+ environment: string;
30
+ persona: string;
31
+ disposition?: string;
32
+ goals?: string;
33
+ steps?: string;
34
+ mutations?: string;
35
+ duration?: string;
36
+ seed?: string;
37
+ model?: string;
38
+ apiUrl?: string;
39
+ allowApproval?: boolean;
40
+ skipRoleCheck?: boolean;
41
+ out?: string;
42
+ }, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
43
+ environment: string;
44
+ persona: string;
45
+ disposition?: string;
46
+ goals?: string;
47
+ steps?: string;
48
+ mutations?: string;
49
+ duration?: string;
50
+ seed?: string;
51
+ model?: string;
52
+ apiUrl?: string;
53
+ allowApproval?: boolean;
54
+ skipRoleCheck?: boolean;
55
+ out?: string;
56
+ }, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
57
+ /** Who this app is for, and what each of them wants, without running anything. */
58
+ export declare const personaList: import("#pikku").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<void, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;