@pikku/core 0.12.80 → 0.12.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +312 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/function/function-runner.js +2 -5
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +3 -3
- package/dist/pikku-state.js +4 -0
- package/dist/services/ai-agent-runner-service.d.ts +7 -0
- package/dist/services/ai-run-state-service.d.ts +10 -0
- package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
- package/dist/services/in-memory-ai-run-state-service.js +9 -0
- package/dist/services/index.d.ts +15 -15
- package/dist/services/index.js +5 -5
- package/dist/services/scoped-credential-service.d.ts +21 -0
- package/dist/services/scoped-credential-service.js +53 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
- package/dist/types/core.types.d.ts +0 -2
- package/dist/types/state.types.d.ts +13 -0
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
- package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
- package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
- package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
- package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
- package/dist/wirings/ai-agent/index.d.ts +8 -7
- package/dist/wirings/ai-agent/index.js +5 -4
- package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
- package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
- package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
- package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
- package/dist/wirings/ai-scorer/index.d.ts +6 -0
- package/dist/wirings/ai-scorer/index.js +5 -0
- package/dist/wirings/channel/index.d.ts +5 -6
- package/dist/wirings/channel/index.js +3 -4
- package/dist/wirings/channel/local/local-channel-runner.js +8 -1
- package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
- package/dist/wirings/cli/channel/index.d.ts +1 -2
- package/dist/wirings/cli/channel/index.js +0 -1
- package/dist/wirings/cli/cli-runner.js +13 -1
- package/dist/wirings/credential/index.d.ts +1 -1
- package/dist/wirings/gateway/index.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/index.d.ts +1 -2
- package/dist/wirings/mcp/index.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
- package/dist/wirings/mcp/mcp-runner.js +18 -5
- package/dist/wirings/persona/index.d.ts +3 -4
- package/dist/wirings/persona/index.js +2 -3
- package/dist/wirings/queue/index.d.ts +1 -3
- package/dist/wirings/queue/index.js +1 -3
- package/dist/wirings/rpc/addon-runner.d.ts +4 -0
- package/dist/wirings/rpc/addon-runner.js +19 -3
- package/dist/wirings/rpc/rpc-runner.js +2 -0
- package/dist/wirings/rpc/rpc-types.d.ts +4 -0
- package/dist/wirings/rpc/wire-addon.d.ts +13 -0
- package/dist/wirings/rpc/wire-addon.js +4 -0
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/trigger/index.d.ts +1 -1
- package/dist/wirings/virtual-user/index.d.ts +5 -6
- package/dist/wirings/virtual-user/index.js +2 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
- package/dist/wirings/workflow/index.d.ts +6 -6
- package/dist/wirings/workflow/index.js +2 -2
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +7 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +39 -13
- package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
- package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
- package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
- package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
- package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
- package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
- package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
- package/dist/wirings/workflow/workflow-approval.js +65 -2
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
- package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
- package/dist/wirings/workflow/workflow.types.d.ts +1 -1
- package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
- package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
- package/knowledge/decisions/internals/index.md +30 -3
- package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
- package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
- package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
- package/knowledge/decisions/security/index.md +3 -1
- package/knowledge/questions/index.md +1 -1
- package/package.json +3 -1
- package/scripts/generate-api-report.mts +143 -18
- package/src/api-report.test.ts +2 -2
- package/src/errors/index.ts +1 -1
- package/src/function/function-runner.test.ts +52 -0
- package/src/function/function-runner.ts +5 -9
- package/src/function/index.ts +0 -2
- package/src/index.ts +0 -35
- package/src/pikku-state.ts +5 -0
- package/src/public-surface.json +70 -94
- package/src/services/ai-agent-runner-service.ts +12 -1
- package/src/services/ai-run-state-service.ts +11 -0
- package/src/services/in-memory-ai-run-state-service.ts +13 -0
- package/src/services/index.ts +3 -43
- package/src/services/scoped-credential-service.test.ts +86 -0
- package/src/services/scoped-credential-service.ts +63 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
- package/src/types/core.types.ts +3 -6
- package/src/types/state.types.ts +16 -0
- package/src/wirings/actor-flow/index.ts +0 -3
- package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
- package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
- package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
- package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
- package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
- package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
- package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
- package/src/wirings/ai-agent/index.ts +2 -16
- package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
- package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
- package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
- package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
- package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
- package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
- package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
- package/src/wirings/ai-scorer/index.ts +24 -0
- package/src/wirings/channel/index.ts +1 -20
- package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
- package/src/wirings/channel/local/local-channel-runner.ts +8 -1
- package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
- package/src/wirings/cli/channel/index.ts +0 -7
- package/src/wirings/cli/cli-runner.test.ts +68 -0
- package/src/wirings/cli/cli-runner.ts +18 -1
- package/src/wirings/credential/index.ts +0 -1
- package/src/wirings/gateway/index.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +66 -0
- package/src/wirings/http/http-runner.ts +10 -2
- package/src/wirings/http/index.ts +1 -1
- package/src/wirings/mcp/index.ts +0 -1
- package/src/wirings/mcp/mcp-runner.test.ts +181 -0
- package/src/wirings/mcp/mcp-runner.ts +35 -5
- package/src/wirings/persona/index.ts +0 -8
- package/src/wirings/queue/index.ts +0 -14
- package/src/wirings/rpc/addon-runner.ts +34 -3
- package/src/wirings/rpc/addon-secrets.test.ts +261 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +2 -0
- package/src/wirings/rpc/rpc-types.ts +4 -0
- package/src/wirings/rpc/wire-addon.ts +17 -0
- package/src/wirings/scheduler/index.ts +0 -1
- package/src/wirings/trigger/index.ts +0 -1
- package/src/wirings/virtual-user/index.ts +0 -16
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -20
- package/src/wirings/workflow/pikku-scenario-service.ts +60 -15
- package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
- package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
- package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
- package/src/wirings/workflow/scenario-step.types.ts +8 -0
- package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
- package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
- package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
- package/src/wirings/workflow/workflow-approval.ts +113 -9
- package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
- package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
- package/src/wirings/workflow/workflow.types.ts +0 -9
- package/src/wirings-stay-decoupled.test.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -2
- package/dist/middleware/timeout.d.ts +0 -9
- package/dist/middleware/timeout.js +0 -15
- package/dist/pikku-response.d.ts +0 -6
- package/dist/pikku-response.js +0 -6
- package/dist/services/gopass-secrets.d.ts +0 -15
- package/dist/services/gopass-secrets.js +0 -76
- package/dist/services/http-scenario-actors.d.ts +0 -75
- package/dist/services/http-scenario-actors.js +0 -195
- package/dist/services/http-user-flow-actors.d.ts +0 -67
- package/dist/services/http-user-flow-actors.js +0 -193
- package/dist/services/scenario-actors-service.d.ts +0 -127
- package/dist/services/scenario-actors-service.js +0 -40
- package/dist/services/user-flow-actors-service.d.ts +0 -39
- package/dist/services/user-flow-actors-service.js +0 -1
- package/dist/wirings/credential/wire-credential.d.ts +0 -48
- package/dist/wirings/credential/wire-credential.js +0 -47
- package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
- package/dist/wirings/oauth2/oauth2-client.js +0 -263
- package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
- package/dist/wirings/oauth2/oauth2-routes.js +0 -146
- package/dist/wirings/scope/wire-scope.d.ts +0 -33
- package/dist/wirings/scope/wire-scope.js +0 -32
- package/dist/wirings/workflow/dsl/index.d.ts +0 -5
- package/dist/wirings/workflow/dsl/index.js +0 -4
- package/dist/wirings/workflow/graph/index.d.ts +0 -5
- package/dist/wirings/workflow/graph/index.js +0 -4
|
@@ -284,6 +284,8 @@ export class ContextAwareRPCService {
|
|
|
284
284
|
secretOverrides: resolved.addonConfig?.secretOverrides,
|
|
285
285
|
variableOverrides: resolved.addonConfig?.variableOverrides,
|
|
286
286
|
credentialOverrides: resolved.addonConfig?.credentialOverrides,
|
|
287
|
+
globalSecrets: resolved.addonConfig?.globalSecrets,
|
|
288
|
+
globalCredentials: resolved.addonConfig?.globalCredentials,
|
|
287
289
|
},
|
|
288
290
|
}
|
|
289
291
|
}
|
|
@@ -67,6 +67,10 @@ export interface ResolvedFunction {
|
|
|
67
67
|
secretOverrides?: Record<string, string>
|
|
68
68
|
variableOverrides?: Record<string, string>
|
|
69
69
|
credentialOverrides?: Record<string, string>
|
|
70
|
+
/** Set by the consuming app: hand this instance the unscoped `SecretService` */
|
|
71
|
+
globalSecrets?: string
|
|
72
|
+
/** Set by the consuming app: hand this instance the unscoped `CredentialService` */
|
|
73
|
+
globalCredentials?: string
|
|
70
74
|
/** Set by `wireRemoteAddon`: dispatch this namespace's RPCs over HTTP */
|
|
71
75
|
remote?: boolean
|
|
72
76
|
serverUrl?: string | ((services: any) => string | Promise<string>)
|
|
@@ -14,6 +14,19 @@ export type WireAddonConfig = {
|
|
|
14
14
|
secretOverrides?: Record<string, string>
|
|
15
15
|
variableOverrides?: Record<string, string>
|
|
16
16
|
credentialOverrides?: Record<string, string>
|
|
17
|
+
/**
|
|
18
|
+
* Hands this instance the whole `SecretService` instead of one scoped to the
|
|
19
|
+
* secrets it declared. The value is the reason, recorded in the deploy
|
|
20
|
+
* manifest — an addon that names secrets at runtime cannot be scoped, and
|
|
21
|
+
* only the consuming app, never the addon, can grant it.
|
|
22
|
+
*/
|
|
23
|
+
globalSecrets?: string
|
|
24
|
+
/**
|
|
25
|
+
* Hands this instance the whole `CredentialService` instead of one narrowed
|
|
26
|
+
* to the credentials it declared. The value is the reason, recorded in the
|
|
27
|
+
* deploy manifest, and only the consuming app can grant it.
|
|
28
|
+
*/
|
|
29
|
+
globalCredentials?: string
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export const wireAddon = (config: WireAddonConfig): void => {
|
|
@@ -32,6 +45,10 @@ export const wireAddon = (config: WireAddonConfig): void => {
|
|
|
32
45
|
...(config.credentialOverrides
|
|
33
46
|
? { credentialOverrides: config.credentialOverrides }
|
|
34
47
|
: {}),
|
|
48
|
+
...(config.globalSecrets ? { globalSecrets: config.globalSecrets } : {}),
|
|
49
|
+
...(config.globalCredentials
|
|
50
|
+
? { globalCredentials: config.globalCredentials }
|
|
51
|
+
: {}),
|
|
35
52
|
})
|
|
36
53
|
}
|
|
37
54
|
|
|
@@ -18,25 +18,16 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export type {
|
|
20
20
|
ApiCatalogueEntry,
|
|
21
|
-
IntentRecord,
|
|
22
21
|
IntentSource,
|
|
23
|
-
IntentStatus,
|
|
24
|
-
StepRecord,
|
|
25
|
-
VirtualUserAction,
|
|
26
|
-
VirtualUserBudget,
|
|
27
22
|
VirtualUserDisposition,
|
|
28
23
|
VirtualUserFinding,
|
|
29
|
-
VirtualUserFindingKind,
|
|
30
24
|
VirtualUserRunResult,
|
|
31
|
-
VirtualUserTally,
|
|
32
25
|
VirtualUserTarget,
|
|
33
26
|
} from './virtual-user.types.js'
|
|
34
27
|
export { PRODUCTION_DISPOSITION } from './virtual-user.types.js'
|
|
35
28
|
export {
|
|
36
29
|
runVirtualUser,
|
|
37
|
-
rememberIds,
|
|
38
30
|
type RunVirtualUserParams,
|
|
39
|
-
type VirtualUserCallContext,
|
|
40
31
|
} from './run-virtual-user.js'
|
|
41
32
|
export {
|
|
42
33
|
personaScopes,
|
|
@@ -57,26 +48,19 @@ export {
|
|
|
57
48
|
} from './virtual-user-dispositions.js'
|
|
58
49
|
export {
|
|
59
50
|
catalogueClassification,
|
|
60
|
-
catalogueIndex,
|
|
61
51
|
catalogueLookup,
|
|
62
|
-
describeEntry,
|
|
63
52
|
isReadOnly,
|
|
64
53
|
reachableCatalogue,
|
|
65
|
-
renderCatalogue,
|
|
66
54
|
unreachableCatalogue,
|
|
67
55
|
} from './virtual-user-catalogue.js'
|
|
68
56
|
export {
|
|
69
|
-
reachableAgents,
|
|
70
57
|
type AgentReachability,
|
|
71
58
|
type ReachableAgent,
|
|
72
59
|
} from './virtual-user-agents.js'
|
|
73
60
|
export {
|
|
74
61
|
IntentStack,
|
|
75
62
|
intentsForPersona,
|
|
76
|
-
type IntentMove,
|
|
77
|
-
type ScheduledTick,
|
|
78
63
|
} from './virtual-user-intents.js'
|
|
79
|
-
export { createRng, type VirtualUserRng } from './virtual-user-rng.js'
|
|
80
64
|
export {
|
|
81
65
|
deriveCatalogue,
|
|
82
66
|
deriveIntents,
|
|
@@ -65,6 +65,20 @@ export interface WorkflowExpectServiceOptions extends WorkflowStepOptions {
|
|
|
65
65
|
times?: number
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** Options for workflow.expectScore() */
|
|
69
|
+
export interface WorkflowExpectScoreOptions extends WorkflowStepOptions {
|
|
70
|
+
/** Fail below this score. Defaults to 0.5 — a scorer that answered at all. */
|
|
71
|
+
atLeast?: number
|
|
72
|
+
/** Fail above this score, for a scorer where high is the failure. */
|
|
73
|
+
atMost?: number
|
|
74
|
+
/**
|
|
75
|
+
* The known-correct answer, which is what a reference-based judge grades
|
|
76
|
+
* against. Live traffic has no answer key, so a judge declared
|
|
77
|
+
* `requiresReference` is only ever reachable from a scenario.
|
|
78
|
+
*/
|
|
79
|
+
reference?: string
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
/**
|
|
69
83
|
* Type signature for workflow.do() RPC form - used by inspector
|
|
70
84
|
*/
|
|
@@ -114,12 +128,55 @@ export type WorkflowWireSleep = (
|
|
|
114
128
|
*/
|
|
115
129
|
export type WorkflowWireSuspend = (reason: string) => Promise<void>
|
|
116
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Who is allowed to answer an approval gate, relative to the user who started
|
|
133
|
+
* the run.
|
|
134
|
+
*
|
|
135
|
+
* - `any` — anyone the approve entrypoint lets through. The gate is a pause for
|
|
136
|
+
* a decision, not an authorization boundary.
|
|
137
|
+
* - `owner` — only the user who started the run, so a gate can be used to
|
|
138
|
+
* confirm an action against the same person who requested it.
|
|
139
|
+
* - `not-initiator` — anyone *except* the user who started the run: four-eyes.
|
|
140
|
+
* A run with no recorded initiator has nobody to exclude, so this degrades to
|
|
141
|
+
* requiring a signed-in decider.
|
|
142
|
+
*/
|
|
143
|
+
export type WorkflowApprovalApprovers = 'any' | 'owner' | 'not-initiator'
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Who may answer an approval gate. Declared on the gate rather than the
|
|
147
|
+
* entrypoint because it is a property of the decision being made, and because
|
|
148
|
+
* one workflow can hold several gates with different requirements.
|
|
149
|
+
*/
|
|
150
|
+
export interface WorkflowApprovalPolicy {
|
|
151
|
+
/**
|
|
152
|
+
* Defaults to `any`. See {@link WorkflowApprovalApprovers}.
|
|
153
|
+
*/
|
|
154
|
+
approvers?: WorkflowApprovalApprovers
|
|
155
|
+
/**
|
|
156
|
+
* Additionally require the decider's session to hold this scope, so
|
|
157
|
+
* "a second pair of eyes" can be narrowed to "a second pair of *senior*
|
|
158
|
+
* eyes". Combines with `approvers` — both must pass.
|
|
159
|
+
*/
|
|
160
|
+
approverScope?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The decider, reduced to the two facts a policy can be expressed in terms of.
|
|
165
|
+
* Recorded alongside the decision so the gate can be judged on replay — the
|
|
166
|
+
* session itself is long gone by then — and carried into the settled outcome so
|
|
167
|
+
* the answer keeps its provenance.
|
|
168
|
+
*/
|
|
169
|
+
export interface ApprovalDecider {
|
|
170
|
+
userId?: string
|
|
171
|
+
scopes?: string[]
|
|
172
|
+
}
|
|
173
|
+
|
|
117
174
|
/**
|
|
118
175
|
* Options for workflow.approval().
|
|
119
176
|
*/
|
|
120
177
|
export interface WorkflowApprovalOptions<
|
|
121
178
|
TSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
122
|
-
> {
|
|
179
|
+
> extends WorkflowApprovalPolicy {
|
|
123
180
|
/**
|
|
124
181
|
* Schema the decision payload is validated against. This is a VALUE, not a
|
|
125
182
|
* type generic: the payload arrives from an untrusted caller over the approve
|
|
@@ -142,7 +199,18 @@ export interface WorkflowApprovalOptions<
|
|
|
142
199
|
* carried in `data` and is the application's business, not the framework's.
|
|
143
200
|
*/
|
|
144
201
|
export type ApprovalOutcome<T> =
|
|
145
|
-
| {
|
|
202
|
+
| {
|
|
203
|
+
status: 'decided'
|
|
204
|
+
data: T
|
|
205
|
+
/**
|
|
206
|
+
* Who answered, and when. Run state holds the decision only while the gate
|
|
207
|
+
* is open — it is overwritten by the next write and cleared outright when a
|
|
208
|
+
* decision is refused — so the settled answer carries its own provenance
|
|
209
|
+
* into the step result, which is append-only.
|
|
210
|
+
*/
|
|
211
|
+
decidedBy?: ApprovalDecider
|
|
212
|
+
decidedAt?: string
|
|
213
|
+
}
|
|
146
214
|
| { status: 'expired' }
|
|
147
215
|
|
|
148
216
|
/**
|
|
@@ -416,6 +484,10 @@ export interface ApprovalStepMeta {
|
|
|
416
484
|
outputVar?: string
|
|
417
485
|
/** Expiry duration, when one was given */
|
|
418
486
|
expiry?: string | number
|
|
487
|
+
/** Who may answer the gate, when the default was not used */
|
|
488
|
+
approvers?: WorkflowApprovalApprovers
|
|
489
|
+
/** Scope the decider must hold, when one was required */
|
|
490
|
+
approverScope?: string
|
|
419
491
|
}
|
|
420
492
|
|
|
421
493
|
/**
|
|
@@ -531,15 +603,14 @@ export interface PikkuWorkflowWire {
|
|
|
531
603
|
/**
|
|
532
604
|
* What a scenario has accumulated so far, shared by its body and its hooks.
|
|
533
605
|
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
* object output gets an open record rather than `Partial<never>`.
|
|
606
|
+
* `Partial` because a scenario can fail at any step: `Out` describes what a
|
|
607
|
+
* *completed* run produces, and the context holds however much of it this run
|
|
608
|
+
* reached.
|
|
538
609
|
*
|
|
539
|
-
* `
|
|
540
|
-
*
|
|
541
|
-
* and a constraint carrying a concrete
|
|
542
|
-
* scenario output differs from it.
|
|
610
|
+
* The `0 extends 1 & Out` branch keeps `any` collapsing to `any` rather than to
|
|
611
|
+
* the union of the other two. `PikkuWire` and `PikkuFunctionConfig` both use
|
|
612
|
+
* this type as a generic *constraint*, and a constraint carrying a concrete
|
|
613
|
+
* context would reject every wire whose scenario output differs from it.
|
|
543
614
|
*/
|
|
544
615
|
export type ScenarioContext<Out = unknown> = 0 extends 1 & Out
|
|
545
616
|
? any
|
|
@@ -549,12 +620,9 @@ export type ScenarioContext<Out = unknown> = 0 extends 1 & Out
|
|
|
549
620
|
|
|
550
621
|
export interface PikkuScenarioWire<Out = unknown> extends PikkuWorkflowWire {
|
|
551
622
|
/**
|
|
552
|
-
* Scratch the body writes and the `before`/`after` hooks read.
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
* is why teardown had nowhere to learn the ids the body minted. Assigning
|
|
556
|
-
* them here (`scenario.context.projectId = project.projectId`) hands them to
|
|
557
|
-
* `after`, which runs in a `finally` and so cleans up on a failed run too.
|
|
623
|
+
* Scratch the body writes and the `before`/`after` hooks read. A hook is a
|
|
624
|
+
* separate function and cannot see the body's locals, so this is how teardown
|
|
625
|
+
* learns the ids the body minted.
|
|
558
626
|
*
|
|
559
627
|
* Deliberately *not* a world: it is scoped to one run, and scenario steps
|
|
560
628
|
* cannot reach it. Steps stay pure functions of their declared inputs, so the
|
|
@@ -589,6 +657,18 @@ export interface PikkuScenarioWire<Out = unknown> extends PikkuWorkflowWire {
|
|
|
589
657
|
options?: WorkflowExpectServiceOptions
|
|
590
658
|
) => Promise<void>
|
|
591
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Grade-assertion step: runs one declared scorer against a finished agent run
|
|
662
|
+
* and asserts the score. Returns the grade, so a scenario can report the
|
|
663
|
+
* reason a judge gave rather than only that it fell short.
|
|
664
|
+
*/
|
|
665
|
+
expectScore: (
|
|
666
|
+
stepName: string,
|
|
667
|
+
runId: string,
|
|
668
|
+
scorerName: string,
|
|
669
|
+
options?: WorkflowExpectScoreOptions
|
|
670
|
+
) => Promise<{ score: number; reason?: string }>
|
|
671
|
+
|
|
592
672
|
/**
|
|
593
673
|
* Run a registered scenario step, as the setup the scenario starts from.
|
|
594
674
|
*
|
|
@@ -54,6 +54,78 @@ describe('graph-runner bugs', () => {
|
|
|
54
54
|
assert.equal(run?.status, 'running')
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
+
test('startWorkflow rejects a startNode that is not a declared entry node', async () => {
|
|
58
|
+
const ws = new InMemoryWorkflowService()
|
|
59
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
60
|
+
metaState['testStartNodeGate'] = {
|
|
61
|
+
name: 'testStartNodeGate',
|
|
62
|
+
pikkuFuncId: 'testStartNodeGate',
|
|
63
|
+
source: 'graph',
|
|
64
|
+
entryNodeIds: ['gate'],
|
|
65
|
+
graphHash: 'startnode-hash',
|
|
66
|
+
nodes: {
|
|
67
|
+
// 'charge' reads only trigger input, so it is dependency-free and would
|
|
68
|
+
// otherwise be directly startable — skipping the 'gate' entry node.
|
|
69
|
+
gate: { nodeId: 'gate', rpcName: 'checkEligibility', next: 'charge' },
|
|
70
|
+
charge: { nodeId: 'charge', rpcName: 'capturePayment' },
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await assert.rejects(
|
|
75
|
+
() =>
|
|
76
|
+
ws.startWorkflow(
|
|
77
|
+
'testStartNodeGate',
|
|
78
|
+
{},
|
|
79
|
+
{ type: 'test' },
|
|
80
|
+
{ rpcWithWire: async () => ({}) },
|
|
81
|
+
{ startNode: 'charge', inline: true }
|
|
82
|
+
),
|
|
83
|
+
/not a declared entry node/
|
|
84
|
+
)
|
|
85
|
+
assert.equal(
|
|
86
|
+
[...(ws as any).runs.keys()].length,
|
|
87
|
+
0,
|
|
88
|
+
'no run should be created for a non-entry startNode'
|
|
89
|
+
)
|
|
90
|
+
delete metaState['testStartNodeGate']
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('startWorkflow accepts a startNode that is a declared entry node', async () => {
|
|
94
|
+
const ws = new InMemoryWorkflowService()
|
|
95
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
96
|
+
// 'b' is a declared entry but carries an unknown input ref, so it clears the
|
|
97
|
+
// startNode gate and then throws at reference validation — proving the gate
|
|
98
|
+
// admitted it, without dispatching real work.
|
|
99
|
+
metaState['testStartNodeAllowed'] = {
|
|
100
|
+
name: 'testStartNodeAllowed',
|
|
101
|
+
pikkuFuncId: 'testStartNodeAllowed',
|
|
102
|
+
source: 'graph',
|
|
103
|
+
entryNodeIds: ['a', 'b'],
|
|
104
|
+
graphHash: 'startnode-ok-hash',
|
|
105
|
+
nodes: {
|
|
106
|
+
a: { nodeId: 'a', rpcName: 'doA' },
|
|
107
|
+
b: {
|
|
108
|
+
nodeId: 'b',
|
|
109
|
+
rpcName: 'doB',
|
|
110
|
+
input: { dep: { $ref: 'missingNode', path: 'value' } },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
await assert.rejects(
|
|
115
|
+
() =>
|
|
116
|
+
ws.startWorkflow(
|
|
117
|
+
'testStartNodeAllowed',
|
|
118
|
+
{},
|
|
119
|
+
{ type: 'test' },
|
|
120
|
+
{ rpcWithWire: async () => ({}) },
|
|
121
|
+
{ startNode: 'b', inline: true }
|
|
122
|
+
),
|
|
123
|
+
/references unknown node 'missingNode'/,
|
|
124
|
+
'a declared entry node must pass the startNode gate (reaching ref validation)'
|
|
125
|
+
)
|
|
126
|
+
delete metaState['testStartNodeAllowed']
|
|
127
|
+
})
|
|
128
|
+
|
|
57
129
|
test('runWorkflowGraph should throw and not create a run for unknown input refs', async () => {
|
|
58
130
|
const ws = new InMemoryWorkflowService()
|
|
59
131
|
|
|
@@ -13,8 +13,8 @@ export {
|
|
|
13
13
|
assertWorkflowRunOwner,
|
|
14
14
|
WorkflowRunForbiddenError,
|
|
15
15
|
} from './workflow-run-ownership.js'
|
|
16
|
+
export { WorkflowApprovalForbiddenError } from './workflow-approval-policy.js'
|
|
16
17
|
export type {
|
|
17
|
-
RunLifecycleContext,
|
|
18
18
|
WorkflowRunEngine,
|
|
19
19
|
WorkflowRunExtension,
|
|
20
20
|
} from './workflow-run-engine.types.js'
|
|
@@ -30,10 +30,7 @@ export {
|
|
|
30
30
|
} from './run-timeline.js'
|
|
31
31
|
export type {
|
|
32
32
|
RunTimeline,
|
|
33
|
-
RunTimelineEvent,
|
|
34
33
|
ReconstructedRunState,
|
|
35
|
-
ReconstructedStep,
|
|
36
|
-
RunPhase,
|
|
37
34
|
} from './run-timeline.js'
|
|
38
35
|
|
|
39
36
|
export { addWorkflow } from './dsl/workflow-runner.js'
|
|
@@ -45,12 +42,6 @@ export {
|
|
|
45
42
|
type PikkuWorkflowGraphConfig,
|
|
46
43
|
type PikkuWorkflowGraphResult,
|
|
47
44
|
} from './graph/wire-workflow-graph.js'
|
|
48
|
-
|
|
49
|
-
export {
|
|
50
|
-
pikkuWorkflowWorkerFunc,
|
|
51
|
-
pikkuWorkflowOrchestratorFunc,
|
|
52
|
-
pikkuWorkflowSleeperFunc,
|
|
53
|
-
} from './workflow-queue-workers.js'
|
|
54
45
|
export type {
|
|
55
46
|
WorkflowStepInput as WorkflowStepQueueInput,
|
|
56
47
|
PikkuWorkflowOrchestratorInput,
|
|
@@ -73,34 +64,25 @@ export type {
|
|
|
73
64
|
WorkflowRunMirror,
|
|
74
65
|
CoreWorkflow,
|
|
75
66
|
CoreFeature,
|
|
76
|
-
CoreFeatureScenario,
|
|
77
67
|
FeatureMeta,
|
|
78
|
-
FeatureMetaEntry,
|
|
79
68
|
FeaturesMeta,
|
|
80
69
|
FeaturePlanEntry,
|
|
81
70
|
PikkuWorkflow,
|
|
82
71
|
ContextVariable,
|
|
83
72
|
WorkflowContext,
|
|
84
73
|
WorkflowsMeta,
|
|
85
|
-
WorkflowRuntimeMeta,
|
|
86
74
|
WorkflowsRuntimeMeta,
|
|
87
75
|
} from './workflow.types.js'
|
|
88
76
|
|
|
89
77
|
export type {
|
|
90
78
|
WorkflowStepOptions,
|
|
91
79
|
WorkflowWireDoRPC,
|
|
92
|
-
WorkflowWireDoInline,
|
|
93
|
-
WorkflowWireSleep,
|
|
94
|
-
WorkflowWireSuspend,
|
|
95
|
-
WorkflowWireApproval,
|
|
96
80
|
WorkflowApprovalOptions,
|
|
97
81
|
ApprovalOutcome,
|
|
98
82
|
InputSource,
|
|
99
83
|
OutputBinding,
|
|
100
84
|
RpcStepMeta,
|
|
101
|
-
SimpleCondition,
|
|
102
85
|
Condition,
|
|
103
|
-
BranchCase,
|
|
104
86
|
BranchStepMeta,
|
|
105
87
|
ParallelGroupStepMeta,
|
|
106
88
|
FanoutStepMeta,
|
|
@@ -135,7 +117,7 @@ export type {
|
|
|
135
117
|
export { SCENARIO_SURFACES } from './scenario-step.types.js'
|
|
136
118
|
|
|
137
119
|
// Which of a step's bindings run: one for an action, every witness for a `then`
|
|
138
|
-
export { resolveScenarioSurfaces
|
|
120
|
+
export { resolveScenarioSurfaces } from './scenario-surface.js'
|
|
139
121
|
|
|
140
122
|
export { requireActor, requireScenarioEnv } from './scenario-step-guards.js'
|
|
141
123
|
|
|
@@ -29,11 +29,14 @@ import { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js'
|
|
|
29
29
|
import type {
|
|
30
30
|
PikkuScenarioWire,
|
|
31
31
|
PikkuWorkflowWire,
|
|
32
|
+
WorkflowQueueOptions,
|
|
33
|
+
} from './workflow.types.js'
|
|
34
|
+
import type {
|
|
32
35
|
WorkflowExpectEventuallyOptions,
|
|
33
36
|
WorkflowExpectErrorOptions,
|
|
37
|
+
WorkflowExpectScoreOptions,
|
|
34
38
|
WorkflowExpectServiceOptions,
|
|
35
|
-
|
|
36
|
-
} from './workflow.types.js'
|
|
39
|
+
} from './dsl/workflow-dsl.types.js'
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
42
|
* A workflow service with the scenario capability attached — the two lines
|
|
@@ -245,9 +248,9 @@ export class PikkuScenarioService implements WorkflowRunExtension {
|
|
|
245
248
|
// Scenario actors per run: live authenticated clients (cookie jars) are
|
|
246
249
|
// process-local by nature, so they ride this map, never the persisted wire.
|
|
247
250
|
private runActors = new Map<string, ScenarioPersonas>()
|
|
248
|
-
// What each run has accumulated so far
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
+
// What each run has accumulated so far — process-local like the actors above,
|
|
252
|
+
// so the body and its hooks share one object rather than reading it back off
|
|
253
|
+
// the persisted wire.
|
|
251
254
|
private runContexts = new Map<string, Record<string, unknown>>()
|
|
252
255
|
private scenarioBrowserProvider?: ScenarioBrowserProvider
|
|
253
256
|
private scenarioEnvironment?: ScenarioEnvironment
|
|
@@ -319,19 +322,19 @@ export class PikkuScenarioService implements WorkflowRunExtension {
|
|
|
319
322
|
}
|
|
320
323
|
|
|
321
324
|
/**
|
|
322
|
-
* What the run has accumulated
|
|
323
|
-
*
|
|
325
|
+
* What the run has accumulated.
|
|
326
|
+
*
|
|
327
|
+
* Undefined only for a run whose wire was never decorated — decoration calls
|
|
328
|
+
* `contextForRun` unconditionally, so a plain workflow that has reached a
|
|
329
|
+
* step holds an empty context rather than none.
|
|
324
330
|
*/
|
|
325
331
|
public getRunContext(runId: string): Record<string, unknown> | undefined {
|
|
326
332
|
return this.runContexts.get(runId)
|
|
327
333
|
}
|
|
328
334
|
|
|
329
335
|
/**
|
|
330
|
-
* The run's context, created on demand
|
|
331
|
-
*
|
|
332
|
-
* `attachRunContext` seeds it for an ordinary scenario run, but the wire is
|
|
333
|
-
* also built on paths that skip attach; without this the body would be
|
|
334
|
-
* writing to an `undefined` and teardown would read nothing.
|
|
336
|
+
* The run's context, created on demand because the wire is also built on
|
|
337
|
+
* paths that skip `attachRunContext`.
|
|
335
338
|
*/
|
|
336
339
|
private contextForRun(runId: string): Record<string, unknown> {
|
|
337
340
|
let runContext = this.runContexts.get(runId)
|
|
@@ -539,9 +542,6 @@ export class PikkuScenarioService implements WorkflowRunExtension {
|
|
|
539
542
|
rpcService,
|
|
540
543
|
})
|
|
541
544
|
Object.assign(workflowWire, {
|
|
542
|
-
// One object per run, resolved through the map rather than created here,
|
|
543
|
-
// so the body and its before/after hooks share the same scratch even
|
|
544
|
-
// though the hooks are separate functions that cannot see its locals.
|
|
545
545
|
context: this.contextForRun(runId),
|
|
546
546
|
|
|
547
547
|
// Durable polling step: invoke an RPC (as an actor when options.as is
|
|
@@ -684,6 +684,51 @@ export class PikkuScenarioService implements WorkflowRunExtension {
|
|
|
684
684
|
)
|
|
685
685
|
},
|
|
686
686
|
|
|
687
|
+
expectScore: async (
|
|
688
|
+
stepName: string,
|
|
689
|
+
agentRunId: string,
|
|
690
|
+
scorerName: string,
|
|
691
|
+
options?: WorkflowExpectScoreOptions
|
|
692
|
+
) => {
|
|
693
|
+
this.engine.verifyStepName(stepName)
|
|
694
|
+
return await this.engine.inlineStep(
|
|
695
|
+
runId,
|
|
696
|
+
stepName,
|
|
697
|
+
async () => {
|
|
698
|
+
const rpcName = 'pikkuScenarioGradeRun'
|
|
699
|
+
const data = {
|
|
700
|
+
runId: agentRunId,
|
|
701
|
+
scorer: scorerName,
|
|
702
|
+
...(options?.reference !== undefined
|
|
703
|
+
? { reference: options.reference }
|
|
704
|
+
: {}),
|
|
705
|
+
}
|
|
706
|
+
const grade: { score: number; reason?: string } = options?.actor
|
|
707
|
+
? await options.actor.invoke(rpcName, data)
|
|
708
|
+
: await rpcService.rpcWithWire(rpcName, data, {})
|
|
709
|
+
|
|
710
|
+
// A scorer that graded is a scorer that answered, so an unstated
|
|
711
|
+
// bound still fails a zero rather than passing anything at all.
|
|
712
|
+
const atLeast = options?.atLeast ?? 0.5
|
|
713
|
+
const failed =
|
|
714
|
+
grade.score < atLeast ||
|
|
715
|
+
(options?.atMost !== undefined && grade.score > options.atMost)
|
|
716
|
+
if (failed) {
|
|
717
|
+
const bound =
|
|
718
|
+
options?.atMost !== undefined
|
|
719
|
+
? `between ${atLeast} and ${options.atMost}`
|
|
720
|
+
: `at least ${atLeast}`
|
|
721
|
+
throw new Error(
|
|
722
|
+
`[workflow] expectScore '${stepName}' expected '${scorerName}' to grade run ${agentRunId} ${bound}, got ${grade.score}` +
|
|
723
|
+
`${grade.reason ? `: ${grade.reason}` : ''}`
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
return grade
|
|
727
|
+
},
|
|
728
|
+
options
|
|
729
|
+
)
|
|
730
|
+
},
|
|
731
|
+
|
|
687
732
|
// knowledge: decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md
|
|
688
733
|
given: (stepName, stepFunc, data, options) =>
|
|
689
734
|
this.scenarioStep(
|
|
@@ -661,12 +661,9 @@ describe('pikku-workflow-service approval', () => {
|
|
|
661
661
|
|
|
662
662
|
const run = await ws.getRun(runId)
|
|
663
663
|
assert.equal(run?.status, 'completed')
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
data: { approved: true, comment: 'lgtm' },
|
|
668
|
-
},
|
|
669
|
-
})
|
|
664
|
+
const decision = (run?.output as any)?.decision
|
|
665
|
+
assert.equal(decision.status, 'decided')
|
|
666
|
+
assert.deepEqual(decision.data, { approved: true, comment: 'lgtm' })
|
|
670
667
|
|
|
671
668
|
cleanup()
|
|
672
669
|
})
|
|
@@ -714,9 +711,9 @@ describe('pikku-workflow-service approval', () => {
|
|
|
714
711
|
await ws.runWorkflowJob(runId, {})
|
|
715
712
|
const run = await ws.getRun(runId)
|
|
716
713
|
assert.equal(run?.status, 'completed')
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
})
|
|
714
|
+
const decision = (run?.output as any)?.decision
|
|
715
|
+
assert.equal(decision.status, 'decided')
|
|
716
|
+
assert.deepEqual(decision.data, { approved: false })
|
|
720
717
|
|
|
721
718
|
cleanup()
|
|
722
719
|
})
|
|
@@ -834,9 +831,13 @@ describe('pikku-workflow-service approval', () => {
|
|
|
834
831
|
|
|
835
832
|
const run = await ws.getRun(runId)
|
|
836
833
|
assert.equal(run?.status, 'completed')
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
})
|
|
834
|
+
const decision = (run?.output as any)?.decision
|
|
835
|
+
assert.equal(decision.status, 'decided')
|
|
836
|
+
assert.deepEqual(decision.data, { approved: true })
|
|
837
|
+
// Answered with no session, so there is nobody to name — but when the
|
|
838
|
+
// answer arrived is recorded either way.
|
|
839
|
+
assert.equal(decision.decidedBy, undefined)
|
|
840
|
+
assert.ok(!Number.isNaN(Date.parse(decision.decidedAt)))
|
|
840
841
|
|
|
841
842
|
cleanup()
|
|
842
843
|
})
|
|
@@ -47,7 +47,7 @@ import { PikkuMissingMetaError } from '../../errors/errors.js'
|
|
|
47
47
|
import { RPCNotFoundError } from '../rpc/rpc-runner.js'
|
|
48
48
|
import type { PikkuRPC } from '../rpc/rpc-types.js'
|
|
49
49
|
import { deriveInvocationId } from './workflow-invocation-id.js'
|
|
50
|
-
import {
|
|
50
|
+
import { approvalDeciderFrom } from './workflow-approval-policy.js'
|
|
51
51
|
import {
|
|
52
52
|
buildRunTimeline,
|
|
53
53
|
reconstructStateAt,
|
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
recordApprovalDecision,
|
|
96
96
|
type ApprovalStore,
|
|
97
97
|
} from './workflow-approval.js'
|
|
98
|
+
import { auditApprovalDecision } from './workflow-approval-audit.js'
|
|
98
99
|
import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js'
|
|
99
100
|
import {
|
|
100
101
|
RedispatchBackoff,
|
|
@@ -844,6 +845,22 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
844
845
|
}
|
|
845
846
|
|
|
846
847
|
if (workflowMeta.source === 'graph') {
|
|
848
|
+
// A caller-supplied startNode must be one of the graph's declared entry
|
|
849
|
+
// nodes. startWorkflow is the boundary the public
|
|
850
|
+
// `/workflow/:name/graph/:nodeId` route and triggers enter through, so
|
|
851
|
+
// without this a request could name any dependency-free node — one whose
|
|
852
|
+
// input reads only `trigger` — and fire its RPC directly with
|
|
853
|
+
// attacker-chosen data, skipping every upstream eligibility, validation or
|
|
854
|
+
// approval node. (Internal resume/replay drives runWorkflowGraph directly
|
|
855
|
+
// and is unaffected.)
|
|
856
|
+
if (
|
|
857
|
+
options?.startNode &&
|
|
858
|
+
!(workflowMeta.entryNodeIds ?? []).includes(options.startNode)
|
|
859
|
+
) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`Workflow graph '${name}': '${options.startNode}' is not a declared entry node`
|
|
862
|
+
)
|
|
863
|
+
}
|
|
847
864
|
const shouldInline =
|
|
848
865
|
options?.inline || !getSingletonServices()?.queueService
|
|
849
866
|
return runWorkflowGraph(
|
|
@@ -1840,6 +1857,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1840
1857
|
this.updateRunState(runId, key, value),
|
|
1841
1858
|
resumeWorkflow: (runId) => this.resumeWorkflow(runId),
|
|
1842
1859
|
scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
|
|
1860
|
+
getRunOwner: async (runId) =>
|
|
1861
|
+
(await this.getRunIdentity(runId))?.wire?.pikkuUserId,
|
|
1862
|
+
auditApproval: (event) => auditApprovalDecision(event),
|
|
1843
1863
|
}
|
|
1844
1864
|
}
|
|
1845
1865
|
|
|
@@ -1849,9 +1869,13 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1849
1869
|
decision: unknown,
|
|
1850
1870
|
session?: CoreUserSession
|
|
1851
1871
|
): Promise<void> {
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1872
|
+
return recordApprovalDecision(
|
|
1873
|
+
this.approvalStore,
|
|
1874
|
+
runId,
|
|
1875
|
+
reason,
|
|
1876
|
+
decision,
|
|
1877
|
+
approvalDeciderFrom(session)
|
|
1878
|
+
)
|
|
1855
1879
|
}
|
|
1856
1880
|
|
|
1857
1881
|
private async approvalStep(
|