@pikku/core 0.12.98 → 0.12.100

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 (38) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/services/http-personas.d.ts +16 -5
  3. package/dist/services/http-personas.js +7 -3
  4. package/dist/services/index.d.ts +1 -0
  5. package/dist/services/index.js +1 -0
  6. package/dist/services/persona-actor-secret.d.ts +38 -0
  7. package/dist/services/persona-actor-secret.js +39 -0
  8. package/dist/services/persona-sign-in.d.ts +23 -1
  9. package/dist/services/persona-sign-in.js +25 -1
  10. package/dist/services/typed-secret-service.js +4 -1
  11. package/dist/wirings/agent-scorer/agent-scorer.d.ts +14 -0
  12. package/dist/wirings/gateway/gateway.types.d.ts +13 -0
  13. package/dist/wirings/persona/index.d.ts +2 -1
  14. package/dist/wirings/persona/index.js +2 -1
  15. package/dist/wirings/secret/secret.types.d.ts +8 -0
  16. package/knowledge/decisions/internals/a-virtual-user-cadence-is-a-row-not-a-timer.md +1 -1
  17. package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md +65 -0
  18. package/knowledge/decisions/internals/index.md +1 -1
  19. package/knowledge/decisions/security/actor-sign-in-only-works-for-actor-flagged-users.md +19 -15
  20. package/knowledge/decisions/security/an-actor-credential-is-derived-per-persona.md +41 -0
  21. package/knowledge/decisions/security/index.md +2 -1
  22. package/package.json +4 -4
  23. package/src/public-surface.json +13 -0
  24. package/src/services/http-personas-converse.test.ts +3 -3
  25. package/src/services/http-personas.test.ts +92 -5
  26. package/src/services/http-personas.ts +27 -6
  27. package/src/services/index.ts +8 -0
  28. package/src/services/persona-actor-secret.test.ts +68 -0
  29. package/src/services/persona-actor-secret.ts +70 -0
  30. package/src/services/persona-sign-in.ts +36 -2
  31. package/src/services/typed-secret-service.test.ts +26 -1
  32. package/src/services/typed-secret-service.ts +4 -1
  33. package/src/wirings/agent-scorer/agent-scorer.ts +14 -0
  34. package/src/wirings/gateway/gateway.types.ts +20 -1
  35. package/src/wirings/persona/index.ts +10 -0
  36. package/src/wirings/secret/secret.types.ts +8 -0
  37. package/tsconfig.tsbuildinfo +1 -1
  38. package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-and-not-a-queued-job.md +0 -53
@@ -137,7 +137,10 @@ describe('TypedSecretService', () => {
137
137
  optional: true,
138
138
  },
139
139
  })
140
- assert.strictEqual(await service.getSecret('SCENARIO_ACTOR_SECRET'), undefined)
140
+ assert.strictEqual(
141
+ await service.getSecret('SCENARIO_ACTOR_SECRET'),
142
+ undefined
143
+ )
141
144
  })
142
145
 
143
146
  test('an optional secret that IS set resolves its value', async () => {
@@ -159,6 +162,28 @@ describe('TypedSecretService', () => {
159
162
  await assert.rejects(() => service.getSecret('STRIPE_KEY'))
160
163
  })
161
164
 
165
+ test('a resolved-absent optional secret still reports hasSecret false', async () => {
166
+ // The cache stores `undefined` to remember the absence, so a `has(key)`
167
+ // test on it answers "we have looked", not "there is a value". Reading an
168
+ // optional secret must not turn its own absence into a claim it is set.
169
+ const service = new TypedSecretService(createMockSecrets(), {
170
+ OPTIONAL: { name: 'opt', displayName: 'Optional', optional: true },
171
+ })
172
+ assert.strictEqual(await service.getSecret('OPTIONAL'), undefined)
173
+ assert.strictEqual(await service.hasSecret('OPTIONAL'), false)
174
+ })
175
+
176
+ test('an optional secret set after an absent read is picked up', async () => {
177
+ const store = new Map<string, string>()
178
+ const service = new TypedSecretService(createMockSecrets(store), {
179
+ OPTIONAL: { name: 'opt', displayName: 'Optional', optional: true },
180
+ })
181
+ assert.strictEqual(await service.getSecret('OPTIONAL'), undefined)
182
+ await service.setSecret('OPTIONAL', 'later')
183
+ assert.strictEqual(await service.hasSecret('OPTIONAL'), true)
184
+ assert.strictEqual(await service.getSecret('OPTIONAL'), 'later')
185
+ })
186
+
162
187
  test('an optional secret does not re-hit the store once resolved', async () => {
163
188
  let reads = 0
164
189
  const base = createMockSecrets()
@@ -64,8 +64,11 @@ export class TypedSecretService<
64
64
  }
65
65
 
66
66
  async hasSecret(key: string): Promise<boolean> {
67
+ // `undefined` is cached for an optional secret that resolved absent, so a
68
+ // cache hit means "already looked", not "there is a value". Reporting true
69
+ // for it would let a read of an optional secret assert its own presence.
67
70
  if (this.cache.has(key)) {
68
- return true
71
+ return this.cache.get(key) !== undefined
69
72
  }
70
73
  return this.secrets.hasSecret(key)
71
74
  }
@@ -22,7 +22,9 @@ const assertSampleRate = (name: string, sampleRate: number | undefined) => {
22
22
  * @example snippet: agentScorer
23
23
  */
24
24
  export const pikkuAgentScorer = <Services = any>(config: {
25
+ /** Identifies the scorer in results and in the Console. Unique per project. */
25
26
  name: string
27
+ /** What this scorer grades, in one line, for whoever reads the score later. */
26
28
  description: string
27
29
  /** 0..1 fraction of live runs to grade. Defaults to all of them. */
28
30
  sampleRate?: number
@@ -31,6 +33,10 @@ export const pikkuAgentScorer = <Services = any>(config: {
31
33
  * traffic has no answer key, so the runtime never samples it.
32
34
  */
33
35
  requiresReference?: boolean
36
+ /**
37
+ * The grade itself: read the finished run and return `{ score, reason }`.
38
+ * Runs in-process, so it may use your own services.
39
+ */
34
40
  score: (
35
41
  input: ScorerInput,
36
42
  services: Services
@@ -55,7 +61,9 @@ export const pikkuAgentScorer = <Services = any>(config: {
55
61
  * @example snippet: agentJudge
56
62
  */
57
63
  export const pikkuAgentJudge = <Services = any>(config: {
64
+ /** Identifies the judge in results and in the Console. Unique per project. */
58
65
  name: string
66
+ /** What this judge grades, in one line, for whoever reads the score later. */
59
67
  description: string
60
68
  /** 0..1 fraction of live runs to grade. Defaults to all of them. */
61
69
  sampleRate?: number
@@ -64,7 +72,9 @@ export const pikkuAgentJudge = <Services = any>(config: {
64
72
  * traffic has no answer key, so the runtime never samples it.
65
73
  */
66
74
  requiresReference?: boolean
75
+ /** The model that grades, e.g. `'claude-sonnet-4-5'`. Not the model under test. */
67
76
  model: string
77
+ /** The rubric: what a good answer looks like, phrased as the goal it should meet. */
68
78
  goal: string
69
79
  /**
70
80
  * How much of the run's trajectory to disclose to the judge. Defaults to
@@ -72,6 +82,10 @@ export const pikkuAgentJudge = <Services = any>(config: {
72
82
  * sending a third-party model the rows the tools returned.
73
83
  */
74
84
  toolCalls?: JudgeToolCallDisclosure
85
+ /**
86
+ * Replaces the generated rubric prompt outright, for framing `goal` cannot
87
+ * express. The `{ score, reason }` response is still forced.
88
+ */
75
89
  prompt?: (input: ScorerInput) => string
76
90
  }): PikkuAgentScorer<Services> => ({
77
91
  name: config.name,
@@ -24,9 +24,13 @@ export interface GatewayAttachment {
24
24
  export interface GatewayInboundMessage {
25
25
  /** Platform-specific: a phone number, a Slack user id, and so on. */
26
26
  senderId: string
27
+ /** What they said, as plain text, with the provider's markup stripped. */
27
28
  text: string
29
+ /** The provider's own event, untouched, for anything this shape drops. */
28
30
  raw: unknown
31
+ /** Files and media that came with the message. */
29
32
  attachments?: GatewayAttachment[]
33
+ /** Anything else the adapter wants to carry through to the wiring. */
30
34
  metadata?: Record<string, unknown>
31
35
  }
32
36
 
@@ -35,8 +39,11 @@ export interface GatewayInboundMessage {
35
39
  * own rich content.
36
40
  */
37
41
  export interface GatewayOutboundMessage {
42
+ /** The reply as plain text. Every provider can render this. */
38
43
  text?: string
44
+ /** The provider's own rich payload, e.g. Slack blocks. Passed through as-is. */
39
45
  richContent?: Record<string, unknown>
46
+ /** Files and media to send alongside. */
40
47
  attachments?: GatewayAttachment[]
41
48
  }
42
49
 
@@ -45,19 +52,31 @@ export interface GatewayOutboundMessage {
45
52
  * provider expects back, or not.
46
53
  */
47
54
  export type WebhookVerificationResult =
48
- { verified: true; response: unknown } | { verified: false }
55
+ | {
56
+ /** True when the request really came from the provider. */
57
+ verified: true
58
+ /** What to echo back, e.g. Meta's hub.challenge. */
59
+ response: unknown
60
+ }
61
+ | {
62
+ /** False when the signature or challenge did not check out. */
63
+ verified: false
64
+ }
49
65
 
50
66
  /**
51
67
  * What a gateway integration implements: parse an incoming event into a
52
68
  * message, send one back, and open and close the connection.
53
69
  */
54
70
  export interface GatewayAdapter {
71
+ /** Identifies the gateway in wirings and logs, e.g. `'slack'`. */
55
72
  name: string
56
73
  /** Return null to ignore the event, e.g. a delivery receipt. */
57
74
  parse(data: unknown): GatewayInboundMessage | null
75
+ /** Deliver a reply back to the sender the message came from. */
58
76
  send(senderId: string, message: GatewayOutboundMessage): Promise<void>
59
77
  /** Called by GatewayService.start(); must call onMessage per incoming event. */
60
78
  init(onMessage: (data: unknown) => Promise<void>): Promise<void>
79
+ /** Called by GatewayService.stop(); release the connection init() opened. */
61
80
  close(): Promise<void>
62
81
  /** Receives the GET query params, or the POST body when called from the POST handler. */
63
82
  verifyWebhook?(
@@ -55,10 +55,20 @@ export {
55
55
  OperatorSignIn,
56
56
  establishOperatorSession,
57
57
  IMPERSONATE_USER_ID_HEADER,
58
+ type ActorSecretResolver,
58
59
  type PersonaSignIn,
59
60
  type OperatorSignInOptions,
60
61
  type OperatorSessionResult,
62
+ authMount,
61
63
  } from '../../services/persona-sign-in.js'
64
+ export {
65
+ ACTOR_ROOT_SECRET_MIN_LENGTH,
66
+ ACTOR_SECRET_INFO,
67
+ ACTOR_SECRET_NAME,
68
+ actorSecretSubject,
69
+ deriveActorSecret,
70
+ verifyActorSecret,
71
+ } from '../../services/persona-actor-secret.js'
62
72
  export {
63
73
  postScenarioJson,
64
74
  readScenarioHttpResponse,
@@ -1,8 +1,16 @@
1
1
  export type CoreSecret<T = unknown> = {
2
+ /** The key code reads it by: `secrets.getSecret('NAME')`. SCREAMING_SNAKE_CASE. */
2
3
  name: string
4
+ /** How the secret is labelled wherever a person is asked to supply it. */
3
5
  displayName: string
6
+ /** What this secret is for, shown beside the field someone has to fill in. */
4
7
  description?: string
8
+ /** The id under the backing store, which is where the value actually lives. */
5
9
  secretId: string
10
+ /**
11
+ * The shape of the value, as a schema. This is what types `getSecret`'s
12
+ * result — pass the schema itself, not an instance of it.
13
+ */
6
14
  schema: T
7
15
  /** Required by default: this says absence is a supported state, and `getSecret` resolves `undefined` rather than throwing. */
8
16
  optional?: boolean