@pikku/inspector 0.12.50 → 0.12.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/OPTIMIZATION-PLAN.md +9 -3
  3. package/dist/add/add-auth.d.ts +1 -1
  4. package/dist/add/add-auth.js +1 -1
  5. package/dist/add/add-cli.js +7 -0
  6. package/dist/add/add-credential.js +1 -1
  7. package/dist/add/add-functions.js +156 -7
  8. package/dist/add/add-http-route.js +16 -6
  9. package/dist/add/add-middleware.js +46 -3
  10. package/dist/add/add-personas.d.ts +13 -0
  11. package/dist/add/add-personas.js +210 -0
  12. package/dist/add/add-scope.js +1 -1
  13. package/dist/add/add-secret.js +1 -1
  14. package/dist/add/add-system-role.d.ts +2 -0
  15. package/dist/add/add-system-role.js +102 -0
  16. package/dist/add/add-variable.js +1 -1
  17. package/dist/add/add-wire-addon.js +31 -0
  18. package/dist/error-codes.d.ts +10 -1
  19. package/dist/error-codes.js +11 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/inspector.js +33 -1
  22. package/dist/types.d.ts +46 -2
  23. package/dist/utils/annotate-http-route-auth.d.ts +22 -0
  24. package/dist/utils/annotate-http-route-auth.js +48 -0
  25. package/dist/utils/extract-function-name.js +13 -4
  26. package/dist/utils/extract-function-node.js +18 -1
  27. package/dist/utils/extract-secret-usage.d.ts +9 -0
  28. package/dist/utils/extract-secret-usage.js +61 -0
  29. package/dist/utils/literal-properties.d.ts +22 -0
  30. package/dist/utils/literal-properties.js +69 -0
  31. package/dist/utils/load-addon-functions-meta.js +26 -1
  32. package/dist/utils/permissions.js +8 -0
  33. package/dist/utils/post-process.d.ts +68 -3
  34. package/dist/utils/post-process.js +243 -13
  35. package/dist/utils/secret-alias-services.d.ts +8 -0
  36. package/dist/utils/secret-alias-services.js +45 -0
  37. package/dist/utils/secret-brokers.d.ts +2 -0
  38. package/dist/utils/secret-brokers.js +9 -0
  39. package/dist/utils/serialize-inspector-state.d.ts +10 -0
  40. package/dist/utils/serialize-inspector-state.js +18 -0
  41. package/dist/utils/serialize-middleware-groups-meta.js +3 -0
  42. package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
  43. package/dist/utils/validate-exposed-functions-gated.js +85 -0
  44. package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
  45. package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
  46. package/dist/visit.js +4 -0
  47. package/package.json +3 -3
  48. package/src/add/add-auth.test.ts +5 -3
  49. package/src/add/add-auth.ts +1 -1
  50. package/src/add/add-cli.ts +8 -0
  51. package/src/add/add-credential.ts +1 -1
  52. package/src/add/add-functions.test.ts +263 -1
  53. package/src/add/add-functions.ts +219 -9
  54. package/src/add/add-http-route.ts +20 -7
  55. package/src/add/add-middleware.test.ts +196 -0
  56. package/src/add/add-middleware.ts +64 -4
  57. package/src/add/add-personas.test.ts +268 -0
  58. package/src/add/add-personas.ts +276 -0
  59. package/src/add/add-scenario-step.test.ts +49 -3
  60. package/src/add/add-scope.test.ts +30 -30
  61. package/src/add/add-scope.ts +1 -1
  62. package/src/add/add-secret.ts +1 -1
  63. package/src/add/add-subject-scenario-steps.test.ts +161 -0
  64. package/src/add/add-system-role.test.ts +214 -0
  65. package/src/add/add-system-role.ts +164 -0
  66. package/src/add/add-variable.ts +1 -1
  67. package/src/add/add-wire-addon.test.ts +111 -0
  68. package/src/add/add-wire-addon.ts +29 -0
  69. package/src/add/addon-bans.test.ts +4 -4
  70. package/src/add/config-value-metadata.test.ts +11 -11
  71. package/src/add/wire-name-literal.test.ts +2 -2
  72. package/src/error-codes.ts +13 -0
  73. package/src/index.ts +4 -1
  74. package/src/inspector.ts +39 -0
  75. package/src/types.ts +47 -2
  76. package/src/utils/annotate-http-route-auth.test.ts +163 -0
  77. package/src/utils/annotate-http-route-auth.ts +52 -0
  78. package/src/utils/extract-function-name.ts +14 -4
  79. package/src/utils/extract-function-node.ts +18 -6
  80. package/src/utils/extract-secret-usage.test.ts +81 -0
  81. package/src/utils/extract-secret-usage.ts +73 -0
  82. package/src/utils/literal-properties.ts +97 -0
  83. package/src/utils/load-addon-functions-meta.ts +32 -2
  84. package/src/utils/permissions-shorthand.test.ts +79 -0
  85. package/src/utils/permissions.ts +7 -0
  86. package/src/utils/post-process.test.ts +133 -0
  87. package/src/utils/post-process.ts +310 -12
  88. package/src/utils/secret-alias-services.test.ts +146 -0
  89. package/src/utils/secret-alias-services.ts +58 -0
  90. package/src/utils/secret-brokers.test.ts +53 -0
  91. package/src/utils/secret-brokers.ts +11 -0
  92. package/src/utils/serialize-inspector-state.ts +28 -0
  93. package/src/utils/serialize-middleware-groups-meta.ts +3 -0
  94. package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
  95. package/src/utils/validate-exposed-functions-gated.ts +94 -0
  96. package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
  97. package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
  98. package/src/visit.ts +4 -0
  99. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,444 @@
1
+ ## 0.12.52
2
+
3
+ ### Patch Changes
4
+
5
+ - 8075f6a: Confine `SecretService` to the places an app is wired.
6
+
7
+ `secrets` is now omitted from the services every function, AI agent, workflow,
8
+ permission and wire receives, and the function runner replaces it with a
9
+ throwing accessor so a cast cannot reach past the type. It stays available in
10
+ `pikkuServices`, `pikkuWireServices`, addon service factories and middleware —
11
+ read a secret there, give it to a service, and have the function ask that
12
+ service.
13
+
14
+ Alongside it:
15
+ - `wireSecret` gains `allowedHosts`, refusing a secret attached to a host it was
16
+ not declared for. Permissive by default; strict via
17
+ `config.secrets.requireAllowedHosts`.
18
+ - `pikku-graph`'s `httpRequest` resolves and attaches its credential inside a new
19
+ `httpRequester` service instead of holding the plaintext in the function.
20
+ - New inspector diagnostics: `PKU950` (a `SecretService` exposed under another
21
+ service name), `PKU951` (a secret read that no `wireSecret` declares) and
22
+ `PKU952` (a secret read with a non-literal key).
23
+
24
+ - Updated dependencies [6a307f0]
25
+ - Updated dependencies [afef587]
26
+ - Updated dependencies [8075f6a]
27
+ - @pikku/core@0.12.74
28
+
29
+ ## 0.12.51
30
+
31
+ ### Patch Changes
32
+
33
+ - c984df6: Give an agent's tools back the descriptions their authors wrote
34
+
35
+ A tool's description is what the model is told the tool does, and the main
36
+ thing it chooses between tools on. It was not reaching the model. `description`
37
+ is classed as a verbose field, so it is stripped from the metadata bundled into
38
+ the generated bootstrap — the copy `pikkuState('function', 'meta')` is built
39
+ from. `buildToolDefs` read the description from there, found it always
40
+ undefined, and fell through to the tool's own name. Every agent has been
41
+ choosing between bare identifiers. The same fallback was offering an addon's
42
+ MCP tools under their names, for the same reason.
43
+
44
+ Tool definitions now resolve descriptions through `metaService`, which reads
45
+ the verbose metadata and falls back to the minimal copy, so the authored text
46
+ is recovered wherever the generated `.pikku` directory is readable. Where it is
47
+ not — no `metaService`, or a deployment shipping only the stripped copy — a
48
+ tool falls back to its name, which is what it did before. Addon metadata is
49
+ likewise loaded verbose-first. `title` is no longer part of the chain: a title
50
+ labels a tool in a UI, it does not tell a model when to reach for it.
51
+
52
+ An addon has to ship the verbose file for any of this to reach it. `tsc` only
53
+ emits the JSON it sees imported and nothing imports the verbose meta, so the
54
+ bundled addons now copy it into `dist` explicitly.
55
+
56
+ `ref()` is resolved at build time. It used to be pushed through codegen as an
57
+ opaque string, so `ref('todos:doesNotExist')` generated cleanly and failed only
58
+ when the agent ran. The inspector now resolves each reference against the
59
+ project's functions, or — using the namespace-to-package mapping `wireAddon`
60
+ already provides — against the addon's own metadata, and reports an unwired
61
+ namespace (`PKU152`) or a missing function (`PKU153`) at codegen. An addon that
62
+ has not been built yet contributed no metadata and is skipped rather than
63
+ reported missing.
64
+
65
+ New `pikku --strict-meta` additionally fails the build on any agent tool with
66
+ no description (`PKU154`), including tools reached through an addon. It is off
67
+ by default, so nothing that builds today stops building; turn it on to hold a
68
+ project to the metadata its agents actually run on.
69
+
70
+ - 63ff32b: Run a CLI's commands on the server, over the connection the client opened
71
+
72
+ A CLI that talks to a service has to ship the service's command tree, so the
73
+ two versions drift: the binary someone installed months ago still believes in
74
+ flags and commands the server has since changed. This makes the command tree
75
+ the server's, and leaves the client holding only a socket.
76
+
77
+ `wireCLI` gains `auth`, and a program wired with a channel entrypoint now
78
+ generates a `__raw` route: the client forwards argv untouched, the server
79
+ parses it, runs the command, and streams the output back as it happens. The
80
+ terminating frame carries the exit code, so a failed remote command still exits
81
+ non-zero locally. Renderers stay on the client and are matched by the command
82
+ id the server reports; an unrecognised command falls back to JSON rather than
83
+ failing.
84
+
85
+ Every channel gains `channel.remote(...)`: calling a function on the peer at
86
+ the other end of the connection and waiting for its answer. A channel is
87
+ otherwise fire-and-forget in both directions, so this is what reaches a peer
88
+ that has no address of its own — a CLI on a laptop, a browser tab, a sandbox
89
+ behind NAT. It is on `channel` rather than `rpc` because it is bound to one
90
+ connection: which peer answers is the socket the call goes out on, not
91
+ something the RPC map could resolve. Any `wireChannel` gets it — a client
92
+ registers what it is willing to answer to, and a name outside that list is
93
+ refused.
94
+
95
+ Requests are correlated by id, time out, and fail fast when the socket closes
96
+ rather than waiting out the timeout. Replies are taken off the socket ahead of
97
+ routing, so a channel needs no route for them and an answer can never be
98
+ mistaken for a new message; the transport is created on first use and released
99
+ when the channel closes, which is also what fails anything the departing peer
100
+ still owed an answer to. Channels that only flow one way — SSE, an agent's
101
+ output stream, a locally-run CLI — refuse the call outright instead of waiting
102
+ for an answer that was never going to come.
103
+
104
+ What a peer answers with is its word, so it is checked before a caller sees it
105
+ — against the schema codegen already generated from the function's declared
106
+ return type, the same one an agent tool or an HTTP response is checked against.
107
+ A capability is declared with `pikkuRemoteChannelFunc`, which takes the usual
108
+ `title` / `description` / `input` / `output` but no `func` — this side owns the
109
+ contract, the peer owns the body. It registers under its name like any other
110
+ function, so `channel.remote` is typed off the same generated map as
111
+ `rpc.remote` and no caller has to cast, and a local call throws rather than
112
+ missing: reaching it locally means a command asked the server for something
113
+ only a client knows. A client on an older build fails the call it answered
114
+ rather than the caller failing later somewhere with no reason to expect a bad
115
+ shape; a name with no declared contract is left alone. Both frame guards
116
+ validate the whole envelope rather than the action tag alone, and a failure
117
+ payload with a non-string name or message falls back rather than being attached
118
+ to an `Error`.
119
+
120
+ The arguments going the other way are checked too, against the schema for the
121
+ capability's declared input, before anything is registered or sent. That is not
122
+ a boundary — the peer runs the code and has to check what it was handed, and a
123
+ caller that meant harm would send arguments that pass. It catches drift, where a
124
+ server built against a newer capability signature calls a client that predates
125
+ it, and fails it here rather than inside someone else's process.
126
+
127
+ A channel-driven CLI command uses this to ask its caller for machine-local
128
+ facts mid-run — a git sha, a working tree, a local file. The CLI wire's own
129
+ channel is synthetic (it exists so a command can stream progress without
130
+ knowing where that goes), so it delegates `remote` to the connection the
131
+ command actually arrived on.
132
+
133
+ Because that runs code on someone's machine at a remote caller's request, the
134
+ capability map says what _can_ run and approval says whether a particular call
135
+ _should_. A capability may be declared `{ execute, needsApproval }`, sharing
136
+ `ApprovalPolicy` — `needsApproval` and `approvalDescriptionFn` — with
137
+ `AIAgentToolDef`, which has carried both since before channels could call back:
138
+ both are an allowlist of named callables invoked by something other than the
139
+ code that wrote them. The runtime around them is deliberately not shared, since
140
+ an agent suspends its run and resumes it later while a reverse call is a live
141
+ await with a person at the other end.
142
+
143
+ A capability written as a bare function is unclassified, and unclassified means
144
+ approval is required — the annotation nobody got round to writing is the one
145
+ most likely to matter, so it fails closed. Declare
146
+ `{ execute, needsApproval: false }` for a capability that may run unattended.
147
+ Nothing infers this: core cannot tell a read-only capability from a destructive
148
+ one, so `needsApproval: false` is the author asserting it, and the assertion is
149
+ the only thing standing between a remote caller and the machine.
150
+
151
+ The default is the opposite of `AIAgentToolDef`'s, where absence means "do not
152
+ ask" — a tool is written by the same people who run the server it executes on,
153
+ and a capability is not.
154
+
155
+ `executeRawCLIViaChannel` reads `--auto-approve` and
156
+ `--dangerously-auto-approve` out of argv (or `PIKKU_AUTO_APPROVE` /
157
+ `PIKKU_DANGEROUSLY_AUTO_APPROVE`) and strips them before argv reaches the
158
+ server — what may run on this machine is this machine's decision, and a flag
159
+ the server can see is one the server could act on. `--auto-approve` permits the
160
+ classified-safe set and refuses the rest; `--dangerously-auto-approve` permits
161
+ everything and says so once on stderr. Interactively the user is asked per
162
+ call, with `y` / `n` / `a`, where `a` is remembered for that one capability for
163
+ the rest of the run and never written to disk — widening it to the session
164
+ would quietly turn an interactive run into `--dangerously-auto-approve`. A run
165
+ with no terminal and no flag refuses rather than assuming yes, because CI is
166
+ exactly where an unattended `git push` would otherwise happen. The tiers are
167
+ meaningful here in a way they would not be for an agent: the caller is a
168
+ deterministic program whose source can be read, so "these calls are always
169
+ fine" is a claim someone can actually justify.
170
+
171
+ A peer that is asking a human sends a pending frame first, which stops the
172
+ caller's timeout. Without it any approval slower than the timeout would fail
173
+ the call and then discard the decision when it finally arrived. The call is
174
+ still failed the moment the socket drops — what actually happens when a peer
175
+ dies mid-prompt — and a peer that sends the frame dishonestly can do nothing
176
+ but keep its own call waiting. A refusal is sent as an answer, so a denied call
177
+ fails its command immediately rather than hanging.
178
+
179
+ Fixes found on the way, each of which broke this path:
180
+ - A websocket upgrade wrote middleware headers (CORS, on every request)
181
+ straight onto the socket, so the first bytes a client saw were headers rather
182
+ than `ws`'s `101` status line and the handshake failed to parse. Header
183
+ writes are now buffered and flushed behind a status line only when the
184
+ upgrade is actually being rejected.
185
+ - An upgrade socket had no error listener while the channel opened, so a client
186
+ that gave up mid-handshake took the whole server process down with an
187
+ unhandled `ECONNRESET`.
188
+ - `onConnect` and `onDisconnect` never saw the session established during the
189
+ upgrade, so a channel could not tell who had just connected.
190
+ - Setting the routing key on a channel result mutated the value in place, which
191
+ throws for a primitive under ESM strict mode.
192
+
193
+ - a7b26c5: rename the inspected declarations to `define*`: `wireScope` → `defineScope`, `wireSecret` → `defineSecret`, `wireVariable` → `defineVariable`, `wireCredential` → `defineCredential`
194
+
195
+ `wire*` meant two unrelated things. A transport wiring attaches a function to
196
+ something that can invoke it — `wireHTTP`, `wireChannel`, `wireScheduler`,
197
+ `wireQueueWorker` and the rest — and the thing it wires runs. These four wire
198
+ nothing: they are no-ops that exist only so the call typechecks, they are
199
+ tree-shaken out of the build, and their whole job is to be found by the
200
+ inspector's AST pass and turned into a type union. One word for both left the
201
+ declaration reading like a registration with a runtime.
202
+
203
+ So the vocabulary splits: **`wire*` is a transport, `define*` is an inspected
204
+ declaration.**
205
+
206
+ ```ts
207
+ import { defineScope } from '@pikku/core/scope'
208
+ import { defineSecret } from '@pikku/core/secret'
209
+ import { defineVariable } from '@pikku/core/variable'
210
+ import { defineCredential } from '@pikku/core/credential'
211
+
212
+ defineScope({ admin: { scopes: { invoices: { scopes: { create: {} } } } } })
213
+ ```
214
+
215
+ **Breaking:** no alias is kept. Rename the four call sites; the module subpaths
216
+ (`@pikku/core/scope`, `/secret`, `/variable`) are unchanged.
217
+
218
+ The inspector matches these by identifier text, so a stale `wire*` call is not a
219
+ type error — it is silently not extracted, and the generated union comes back
220
+ empty. That fails as "this scope isn't declared" on code that was fine a moment
221
+ ago, nowhere near the declaration. Grep for the old names rather than trusting a
222
+ clean build.
223
+
224
+ An addon published with `.pikku` output generated before this release re-exports
225
+ `wireSecret` from `@pikku/core/secret` and will not typecheck against this core
226
+ until it is rebuilt and republished.
227
+
228
+ - 457cb25: Add `definePersonas()`: the people a project's scenarios and virtual users run
229
+ as, declared in code.
230
+
231
+ There used to be three names for two-and-a-bit things — an _actor_ in
232
+ `scenarios.actors`, a _persona_ in `scenarios.personas`, and a _virtual user_
233
+ declared separately against an actor. In practice almost every actor was its own
234
+ kind, so the second set carried no information and the third was a third place
235
+ for a name to drift. There is now one declaration:
236
+
237
+ ```ts
238
+ definePersonas({
239
+ shopper: {
240
+ name: 'Sam Shopper',
241
+ jobTitle: 'Shopper',
242
+ personality: 'Buys in a hurry and leaves tabs open',
243
+ roles: ['customer'],
244
+ disposition: 'careless',
245
+ goals: ['Buy something without reading anything'],
246
+ account: {},
247
+ },
248
+ })
249
+ ```
250
+
251
+ A persona is a person: what they are like, what they want, the roles they hold,
252
+ and **one** account they sign in with — `account: {}` plus `linkedAccounts` for
253
+ the rare case of more, modelled on how better-auth does linking. A persona with a
254
+ `disposition` is a virtual user; `runnable: false` marks someone who only ever
255
+ exists to be acted upon — banned, shared with, reset — and is never handed a
256
+ session.
257
+
258
+ **A persona names roles, never scopes.** Scopes come from `defineSystemRole()`
259
+ expansion, so the build fails if a persona names a role nobody declared, and
260
+ fails again if a role confers a scope no `defineScope` declares. Running one only
261
+ ever has to check that its roles are still valid.
262
+
263
+ **Addresses are computed, never declared.** `personaEmail(id, domain, runId)`
264
+ derives `<id>[+runId]@<domain>` from `scenarios.emailDomain`, so a seed, a
265
+ scenario run and a virtual-user run cannot disagree about who they are signing in
266
+ as. `scenarios.actors` and `scenarios.personas` are gone from
267
+ `pikku.config.json` — only `emailDomain` remains.
268
+
269
+ `actor` survives in exactly one place: the name of a **slot in a scenario step**,
270
+ which is the role a persona is cast in for that step. `pikkuVirtualUser()`,
271
+ `kind`, `grants` and the `actor` field are removed; the `actors` service is now
272
+ `personas`, and the CLI's `virtual-user` commands are now `pikku persona list` /
273
+ `pikku persona run`. `budget` and `allowApprovalRequired` moved to run flags —
274
+ how much you will spend today is not a fact about a person.
275
+
276
+ `@pikku/cucumber` drops its `Actor` class and `ActorDispatchContext`: a
277
+ hand-rolled cookie jar that a persona's own typed session replaces outright.
278
+
279
+ - f7567ad: Add `defineSystemRole()`: roles that ship with the product, declared in code.
280
+
281
+ A system role is to a console-composed role what an AWS managed policy is to a
282
+ customer-managed one — the console may show and grant it, but not rename,
283
+ re-scope or delete it. The CLI extracts declarations by AST and generates a
284
+ `SystemRoleName` union, so naming a role that does not exist fails the build,
285
+ and a role granting a scope no `defineScope` declares fails it too.
286
+
287
+ Removal is additive on the same terms as `defineScope`: deleting a declaration
288
+ leaves an inert row rather than revoking everyone's grant mid-deploy.
289
+
290
+ `ScopeService` gains `syncSystemRoles`, `findStaleSystemRoles` and
291
+ `pruneSystemRoles`; `Role` gains `system` and `declared`. Implementations
292
+ enforce immutability through the shared `assertRoleIsMutable` /
293
+ `assertRoleNameAvailable` guards rather than each inventing the rule.
294
+
295
+ - 457cb25: Let a persona do a real job in production, and say where it may act.
296
+
297
+ A persona was only ever a test subject: something you pointed at a stage to find
298
+ out what the product does wrong. But the same declaration — a name, a job, the
299
+ roles it holds and what it is trying to get done — describes a teammate doing
300
+ the work for real, and nothing about the engine cared which one it was.
301
+
302
+ Four changes make that difference explicit and enforced.
303
+
304
+ **`environments` moves to the top level of `pikku.config.json`**, out from under
305
+ `scenarios`. It was never a scenario's anything: `persona run` targets one, and
306
+ now so does `persona sync`. An environment may be flagged `production: true` —
307
+ a flag rather than a reserved name, because projects call it `prod`, `live` or
308
+ `eu-prod`, and more than one environment can be production.
309
+
310
+ **A persona may name its `environments`.** Omitting them means every configured
311
+ environment _except_ the production ones, so nothing reaches production by being
312
+ forgotten. Naming a production environment requires `disposition: 'accountable'`.
313
+ The rule is checked twice, on purpose: the inspector refuses to generate a
314
+ declaration that breaks it, and sign-in re-checks against the environment
315
+ actually resolved — the build check trusts the file, and the run check does not
316
+ trust which artifact got deployed. An unresolved environment fails closed.
317
+
318
+ **`disposition: 'accountable'`** is that production disposition. It sits opposite
319
+ `adversarial` on the intent axis rather than the care axis: what it changes stays
320
+ changed, every call is recorded against its name, and it stops to ask rather than
321
+ acting and reporting afterwards. Alongside it, **agents now appear in a persona's
322
+ computed catalogue**, gated by the same scopes as the RPCs — an agent is reached
323
+ rather than declared, so a persona finds the specialists its roles unlock and
324
+ chooses between calling the API itself and handing the work over. That also fixes
325
+ a latent gap: `talkTo` was wired at the target but never advertised in the
326
+ instructions, so it was never used.
327
+
328
+ **`pikku persona sync <environment>`** provisions them: it creates each account
329
+ and applies the roles it declares, additively, and never revokes. Seeding is test
330
+ data and `db seed` does not run in production; a teammate doing a real job still
331
+ needs an account and its grants. It needs both halves of an environment — its API
332
+ to sign the person in, its database to write the grants — and `--dry-run` reports
333
+ who would be provisioned, with what, and why anyone was skipped.
334
+
335
+ In the console, a virtual user now says where it may act — the environments it
336
+ named, or the rule when it named none — and its dossier carries the `sync`
337
+ command alongside the `run` one, because the account is not a by-product of a
338
+ run. `accountable` reads as a disposition like the rest.
339
+
340
+ - 86a50b9: scenario: replace `browser: true` + `func` with per-surface bindings on `pikkuScenarioStep`
341
+
342
+ A step now declares one implementation per surface it can be driven through:
343
+
344
+ ```ts
345
+ export const buysTheItem = pikkuScenarioStep<{ sku: string }, { orderId: string }>({
346
+ name: 'buysTheItem',
347
+ description: 'buys the item',
348
+ browser: async (services, data, { browser }) => { ... },
349
+ default: async (services, data, { rpc }) => { ... },
350
+ })
351
+ ```
352
+
353
+ `pikku scenario run --run browser|cli|default` picks which surface the run drives,
354
+ and the two phases resolve bindings differently:
355
+ - **Actions** (`given` / `when` / `step`) run exactly one binding — the run
356
+ surface if it has one, otherwise `default`. A step with neither now fails with
357
+ `ScenarioNoSurfaceBinding` instead of silently running server-side.
358
+ - **Assertions** (`then`) are witnesses, not alternatives: every declared binding
359
+ runs and they must agree. Two surfaces reporting different things fails the run
360
+ with `ScenarioWitnessDisagreement` rather than reporting a pass. An assertion
361
+ with no witness the run can execute at all fails with `ScenarioNoWitness` —
362
+ without it the step returns `undefined` and renders as a tick, reporting a pass
363
+ for something nobody checked.
364
+
365
+ A scenario written as a step ladder that never calls `then` is now a **PKU680**
366
+ critical. It proves only that nothing threw, so an assertion-free ladder of
367
+ browser-bound actions would score perfect coverage while checking nothing.
368
+
369
+ The report gains a surface-coverage line — `n/m steps ran on browser`, counted
370
+ over every step, so an action that fell back to the server lowers the ratio
371
+ rather than needing a footnote. That also makes surfaces comparable over one
372
+ denominator: a scenario is `4/4` on a default run and `3/4` on a browser one.
373
+ Assertions that fell back are named separately and gate `--strict`, since a
374
+ sentence claiming the actor saw something nobody looked at is a different problem
375
+ from an action taking a shortcut.
376
+
377
+ **Breaking:** `browser: true` and the third `B extends boolean` type argument are
378
+ gone. Rename `func` to `default` (or to `browser` where the step drove a browser)
379
+ and drop the type argument.
380
+
381
+ - 0e0f6eb: Add virtual users: LLM-driven synthetic users that work a real stage in
382
+ character.
383
+
384
+ A scenario proves a path somebody thought of. A virtual user works the same
385
+ ground without the script — it signs in as a declared persona over the app's own
386
+ auth, is handed the scenarios' BDD prose and the schema of every endpoint it may
387
+ reach, and decides for itself what to do. It asserts nothing; a run produces
388
+ findings, and their absence only ever means "not this time, not with this seed".
389
+
390
+ There is nothing extra to declare. A persona with a `disposition` is a virtual
391
+ user, and running it is what makes it one — see the `definePersonas` changeset
392
+ for the declaration itself. Listing, describing or running one never loads the
393
+ app: the inspector reads the literal declaration, the CLI writes
394
+ `scopes/pikku-personas-meta.gen.json`, and `MetaService.getPersonasMeta()`
395
+ serves it.
396
+
397
+ **Dispositions are engine dials, not prose.** Each carries its own intent weights
398
+ (continue / suspend / resume / abandon), temperature, re-read and repeat rates,
399
+ and switches: `careless` puts things down and picks them up in the wrong order,
400
+ `newcomer` starts with no memory, `auditor` is never offered a mutation,
401
+ `adversarial` is shown the catalogue its roles do not cover — being offered a
402
+ call it should not be able to make is the test — while those roles stay live as
403
+ the oracle, so a success outside them is authorization drift rather than a pass.
404
+
405
+ **Nothing is retrieved against.** The whole reachable catalogue goes into the
406
+ instructions (~8k tokens on a 430-RPC project, cached for the run), because a
407
+ ranking function would make the user only as adventurous as the ranking and lose
408
+ exactly the endpoints worth stumbling into. Schema first: an endpoint must be
409
+ described before it may be called.
410
+
411
+ **No money in core.** The engine counts steps, calls, mutations and tokens; what
412
+ they cost is the app's to decide through `stop(tally)`.
413
+
414
+ CLI: `pikku persona list` and `pikku persona run <environment> [name]`, with
415
+ flags overriding a declaration for reproduction (`--seed`, `--steps`,
416
+ `--disposition`). Spending is a run flag too — `--steps`, `--mutations` and
417
+ `--duration` bound a run, because how much you will spend today is not a fact
418
+ about a person. Console: a Virtual Users screen beside Scenarios, built out of
419
+ core's own derivation functions so it shows a run's actual inputs rather than a
420
+ second implementation of them.
421
+
422
+ `dev-ai-runner` now ships its own `@pikku/ai-vercel` and
423
+ `@ai-sdk/openai-compatible` instead of requiring them from the project. Behind a
424
+ proxy one openai-compatible provider answers for every prefix, so there was never
425
+ a per-vendor package worth making somebody install; the project's copies still
426
+ win when it has them, and both load from the same place or neither does.
427
+
428
+ - Updated dependencies [c984df6]
429
+ - Updated dependencies [63ff32b]
430
+ - Updated dependencies [ba6cc08]
431
+ - Updated dependencies [d007191]
432
+ - Updated dependencies [a7b26c5]
433
+ - Updated dependencies [457cb25]
434
+ - Updated dependencies [f7567ad]
435
+ - Updated dependencies [ba6cc08]
436
+ - Updated dependencies [a2e21e5]
437
+ - Updated dependencies [457cb25]
438
+ - Updated dependencies [86a50b9]
439
+ - Updated dependencies [0e0f6eb]
440
+ - @pikku/core@0.12.73
441
+
1
442
  ## 0.12.50
2
443
 
3
444
  ### Patch Changes
@@ -59,9 +59,15 @@ node --import tsx src/benchmark.ts <target-dir> <iterations>
59
59
  | `wireMCPTool` | `addMCPTool` |
60
60
  | `wireMCPPrompt` | `addMCPPrompt` |
61
61
  | `wireWorkflowGraph` | `addWorkflowGraph` |
62
- | `wireSecret` | `addSecret` |
63
- | `wireOAuth2Credential` | `addOAuth2Credential` |
64
- | `wireVariable` | `addVariable` |
62
+ | `defineSecret` | `addSecret` |
63
+ | `defineCredential` | `addCredential` |
64
+ | `defineScope` | `addScope` |
65
+ | `defineVariable` | `addVariable` |
66
+
67
+ `addAuth` stays outside the map: it matches several identifiers
68
+ (`betterAuthStatelessSession`, `betterAuthSession`) and gates them on
69
+ surrounding context rather than on the identifier alone, so it keeps running on
70
+ every CallExpression.
65
71
 
66
72
  Fallback for unmatched identifiers: check `/pikku.*func/i` regex -> `addFunctions`.
67
73
 
@@ -19,7 +19,7 @@ export declare const AUTH_HANDLER_FUNC_ID = "authHandler";
19
19
  *
20
20
  * The pikku CLI discovers that single export and generates a catch-all
21
21
  * `auth.gen.ts` that wires `${basePath}/**` to one shared handler, registers the
22
- * better-auth session middleware, and emits a `wireSecret` for every configured
22
+ * better-auth session middleware, and emits a `defineSecret` for every configured
23
23
  * social provider — so the auth routes and secret requirements flow through
24
24
  * normal inspection into the deploy manifest.
25
25
  *
@@ -112,7 +112,7 @@ const isInsideGlobalMiddlewareRegistration = (node) => {
112
112
  *
113
113
  * The pikku CLI discovers that single export and generates a catch-all
114
114
  * `auth.gen.ts` that wires `${basePath}/**` to one shared handler, registers the
115
- * better-auth session middleware, and emits a `wireSecret` for every configured
115
+ * better-auth session middleware, and emits a `defineSecret` for every configured
116
116
  * social provider — so the auth routes and secret requirements flow through
117
117
  * normal inspection into the deploy manifest.
118
118
  *
@@ -99,6 +99,13 @@ function processCLIConfig(logger, node, sourceFile, typeChecker, inspectorState,
99
99
  case 'tags':
100
100
  // Already handled in first pass
101
101
  break;
102
+ case 'auth': {
103
+ const value = getPropertyValue(node, 'auth');
104
+ if (typeof value === 'boolean') {
105
+ programMeta.auth = value;
106
+ }
107
+ break;
108
+ }
102
109
  case 'commands':
103
110
  if (ts.isObjectLiteralExpression(prop.initializer)) {
104
111
  programMeta.commands = processCommands(logger, prop.initializer, sourceFile, typeChecker, programName, inspectorState, options, programTags);
@@ -9,7 +9,7 @@ export const addCredential = (logger, node, checker, state, _options) => {
9
9
  const args = node.arguments;
10
10
  const firstArg = args[0];
11
11
  const expression = node.expression;
12
- if (!ts.isIdentifier(expression) || expression.text !== 'wireCredential') {
12
+ if (!ts.isIdentifier(expression) || expression.text !== 'defineCredential') {
13
13
  return;
14
14
  }
15
15
  if (!firstArg) {