@pikku/core 0.12.80 → 0.12.83
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 +345 -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 -16
- package/dist/services/index.js +5 -5
- package/dist/services/meta-service.d.ts +2 -1
- 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 +2 -3
- package/dist/types/state.types.d.ts +19 -1
- 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 +4 -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 +8 -0
- package/dist/wirings/rpc/addon-runner.js +31 -3
- package/dist/wirings/rpc/rpc-runner.js +4 -0
- package/dist/wirings/rpc/rpc-types.d.ts +8 -0
- package/dist/wirings/rpc/wire-addon.d.ts +25 -0
- package/dist/wirings/rpc/wire-addon.js +8 -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/feature.d.ts +2 -1
- package/dist/wirings/workflow/index.d.ts +5 -16
- package/dist/wirings/workflow/index.js +1 -9
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +48 -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/scenario.types.d.ts +37 -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 +2 -37
- 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 -2
- 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 +81 -118
- 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 +7 -58
- package/src/services/meta-service.ts +2 -4
- 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 +4 -7
- package/src/types/state.types.ts +21 -1
- 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 +4 -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 +62 -3
- package/src/wirings/rpc/addon-secrets.test.ts +391 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +4 -0
- package/src/wirings/rpc/rpc-types.ts +8 -0
- package/src/wirings/rpc/wire-addon.ts +33 -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/feature.ts +2 -5
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -68
- package/src/wirings/workflow/pikku-scenario-service.ts +81 -16
- 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/scenario.types.ts +63 -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 +1 -63
- 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/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
- /package/dist/{services/user-flow-actors-service.js → wirings/workflow/scenario.types.js} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApprovalOutcome, StepState, WorkflowApprovalOptions } from './workflow.types.js';
|
|
2
|
+
import type { ApprovalDecider } from './dsl/workflow-dsl.types.js';
|
|
2
3
|
/** The durable step name an approval point is recorded under. */
|
|
3
4
|
export declare const approvalStepNameFor: (reason: string) => string;
|
|
4
5
|
/**
|
|
@@ -9,6 +10,20 @@ export declare const approvalStepNameFor: (reason: string) => string;
|
|
|
9
10
|
* shadow, another key.
|
|
10
11
|
*/
|
|
11
12
|
export declare const approvalStateKey: (stepName: string) => string;
|
|
13
|
+
/**
|
|
14
|
+
* An answer to an approval gate, as the audit trail sees it.
|
|
15
|
+
*
|
|
16
|
+
* `denied` covers both a submission refused at the door and a decision cleared
|
|
17
|
+
* on replay — from the trail's point of view they are the same event, someone
|
|
18
|
+
* tried to answer a gate they could not.
|
|
19
|
+
*/
|
|
20
|
+
export type ApprovalAuditEvent = {
|
|
21
|
+
runId: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
outcome: 'success' | 'denied';
|
|
24
|
+
decidedBy?: ApprovalDecider;
|
|
25
|
+
refusal?: string;
|
|
26
|
+
};
|
|
12
27
|
/** What the approval gate needs from the workflow service. */
|
|
13
28
|
export type ApprovalStore = {
|
|
14
29
|
getStepState: (runId: string, stepName: string) => Promise<StepState>;
|
|
@@ -19,6 +34,8 @@ export type ApprovalStore = {
|
|
|
19
34
|
updateRunState: (runId: string, key: string, value: unknown) => Promise<void>;
|
|
20
35
|
resumeWorkflow: (runId: string) => Promise<void>;
|
|
21
36
|
scheduleRunWake: (runId: string, delay: number) => Promise<void>;
|
|
37
|
+
getRunOwner: (runId: string) => Promise<string | undefined>;
|
|
38
|
+
auditApproval: (event: ApprovalAuditEvent) => Promise<void>;
|
|
22
39
|
};
|
|
23
40
|
/**
|
|
24
41
|
* Record a decision against an approval point and wake the run.
|
|
@@ -26,8 +43,14 @@ export type ApprovalStore = {
|
|
|
26
43
|
* A decision arriving for an already-settled approval is refused rather than
|
|
27
44
|
* overwriting it: the run has moved on, and a second answer would be recorded
|
|
28
45
|
* against a gate nobody is waiting at.
|
|
46
|
+
*
|
|
47
|
+
* The decider is recorded rather than judged here, for the same reason the
|
|
48
|
+
* payload is: the gate's policy is a value on the workflow, and a decision can
|
|
49
|
+
* legitimately arrive before the run has ever reached the gate. Where the run
|
|
50
|
+
* has already published its policy, it is applied eagerly so a caller who
|
|
51
|
+
* cannot answer is told so rather than left waiting.
|
|
29
52
|
*/
|
|
30
|
-
export declare const recordApprovalDecision: (store: ApprovalStore, runId: string, reason: string, decision: unknown) => Promise<void>;
|
|
53
|
+
export declare const recordApprovalDecision: (store: ApprovalStore, runId: string, reason: string, decision: unknown, decidedBy?: ApprovalDecider) => Promise<void>;
|
|
31
54
|
/**
|
|
32
55
|
* Evaluate an approval gate on replay.
|
|
33
56
|
*
|
|
@@ -35,5 +58,10 @@ export declare const recordApprovalDecision: (store: ApprovalStore, runId: strin
|
|
|
35
58
|
* schema is a value on the workflow and only the workflow has it. A payload
|
|
36
59
|
* that fails validation is cleared and the run suspends again, so a bad
|
|
37
60
|
* submission cannot settle the gate.
|
|
61
|
+
*
|
|
62
|
+
* The decider is judged here for the same reason, and for one more: a decision
|
|
63
|
+
* can be recorded before the run has ever reached the gate, so submission time
|
|
64
|
+
* is not a point at which the policy is reliably knowable. Judging on replay is
|
|
65
|
+
* what makes the policy hold in every ordering.
|
|
38
66
|
*/
|
|
39
67
|
export declare const evaluateApprovalStep: (store: ApprovalStore, runId: string, reason: string, approvalStepName: string, fromStepName: string | undefined, options: WorkflowApprovalOptions) => Promise<ApprovalOutcome<unknown>>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDurationInMilliseconds } from '../../time-utils.js';
|
|
2
2
|
import { WorkflowApprovalResolvedError, WorkflowSuspendedException, } from './workflow-errors.js';
|
|
3
|
+
import { approvalPolicyRefusal, WorkflowApprovalForbiddenError, } from './workflow-approval-policy.js';
|
|
3
4
|
/** The durable step name an approval point is recorded under. */
|
|
4
5
|
export const approvalStepNameFor = (reason) => `__workflow_approval:${reason}`;
|
|
5
6
|
/**
|
|
@@ -22,8 +23,14 @@ export const approvalStateKey = (stepName) => {
|
|
|
22
23
|
* A decision arriving for an already-settled approval is refused rather than
|
|
23
24
|
* overwriting it: the run has moved on, and a second answer would be recorded
|
|
24
25
|
* against a gate nobody is waiting at.
|
|
26
|
+
*
|
|
27
|
+
* The decider is recorded rather than judged here, for the same reason the
|
|
28
|
+
* payload is: the gate's policy is a value on the workflow, and a decision can
|
|
29
|
+
* legitimately arrive before the run has ever reached the gate. Where the run
|
|
30
|
+
* has already published its policy, it is applied eagerly so a caller who
|
|
31
|
+
* cannot answer is told so rather than left waiting.
|
|
25
32
|
*/
|
|
26
|
-
export const recordApprovalDecision = async (store, runId, reason, decision) => {
|
|
33
|
+
export const recordApprovalDecision = async (store, runId, reason, decision, decidedBy) => {
|
|
27
34
|
const stepName = approvalStepNameFor(reason);
|
|
28
35
|
const stateKey = approvalStateKey(stepName);
|
|
29
36
|
let resolved;
|
|
@@ -39,12 +46,27 @@ export const recordApprovalDecision = async (store, runId, reason, decision) =>
|
|
|
39
46
|
}
|
|
40
47
|
const state = await store.getRunState(runId);
|
|
41
48
|
const record = (state[stateKey] ?? {});
|
|
49
|
+
if (record.policy) {
|
|
50
|
+
const refusal = approvalPolicyRefusal(record.policy, await store.getRunOwner(runId), decidedBy);
|
|
51
|
+
if (refusal) {
|
|
52
|
+
await store.auditApproval({
|
|
53
|
+
runId,
|
|
54
|
+
reason,
|
|
55
|
+
outcome: 'denied',
|
|
56
|
+
decidedBy,
|
|
57
|
+
refusal,
|
|
58
|
+
});
|
|
59
|
+
throw new WorkflowApprovalForbiddenError(reason, refusal);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
42
62
|
await store.updateRunState(runId, stateKey, {
|
|
43
63
|
...record,
|
|
44
64
|
decision,
|
|
45
65
|
decidedAt: new Date().toISOString(),
|
|
66
|
+
decidedBy,
|
|
46
67
|
error: undefined,
|
|
47
68
|
});
|
|
69
|
+
await store.auditApproval({ runId, reason, outcome: 'success', decidedBy });
|
|
48
70
|
await store.resumeWorkflow(runId);
|
|
49
71
|
};
|
|
50
72
|
/**
|
|
@@ -54,9 +76,19 @@ export const recordApprovalDecision = async (store, runId, reason, decision) =>
|
|
|
54
76
|
* schema is a value on the workflow and only the workflow has it. A payload
|
|
55
77
|
* that fails validation is cleared and the run suspends again, so a bad
|
|
56
78
|
* submission cannot settle the gate.
|
|
79
|
+
*
|
|
80
|
+
* The decider is judged here for the same reason, and for one more: a decision
|
|
81
|
+
* can be recorded before the run has ever reached the gate, so submission time
|
|
82
|
+
* is not a point at which the policy is reliably knowable. Judging on replay is
|
|
83
|
+
* what makes the policy hold in every ordering.
|
|
57
84
|
*/
|
|
58
85
|
export const evaluateApprovalStep = async (store, runId, reason, approvalStepName, fromStepName, options) => {
|
|
59
|
-
const insert = () => store.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', {
|
|
86
|
+
const insert = () => store.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', {
|
|
87
|
+
reason,
|
|
88
|
+
expiry: options.expiry,
|
|
89
|
+
approvers: options.approvers,
|
|
90
|
+
approverScope: options.approverScope,
|
|
91
|
+
}, undefined, fromStepName);
|
|
60
92
|
let stepState;
|
|
61
93
|
try {
|
|
62
94
|
stepState = await store.getStepState(runId, approvalStepName);
|
|
@@ -72,8 +104,13 @@ export const evaluateApprovalStep = async (store, runId, reason, approvalStepNam
|
|
|
72
104
|
}
|
|
73
105
|
const stateKey = approvalStateKey(approvalStepName);
|
|
74
106
|
let record = ((await store.getRunState(runId))[stateKey] ?? {});
|
|
107
|
+
const policy = {
|
|
108
|
+
approvers: options.approvers,
|
|
109
|
+
approverScope: options.approverScope,
|
|
110
|
+
};
|
|
75
111
|
if (stepState.status === 'pending') {
|
|
76
112
|
await store.setStepRunning(stepState.stepId);
|
|
113
|
+
record = { ...record, policy };
|
|
77
114
|
if (options.expiry !== undefined && !record.expiresAt) {
|
|
78
115
|
const expiry = getDurationInMilliseconds(options.expiry);
|
|
79
116
|
record = {
|
|
@@ -83,14 +120,36 @@ export const evaluateApprovalStep = async (store, runId, reason, approvalStepNam
|
|
|
83
120
|
await store.updateRunState(runId, stateKey, record);
|
|
84
121
|
await store.scheduleRunWake(runId, expiry);
|
|
85
122
|
}
|
|
123
|
+
else {
|
|
124
|
+
await store.updateRunState(runId, stateKey, record);
|
|
125
|
+
}
|
|
86
126
|
}
|
|
87
127
|
if (record.decision !== undefined) {
|
|
128
|
+
const refusal = approvalPolicyRefusal(policy, await store.getRunOwner(runId), record.decidedBy);
|
|
129
|
+
if (refusal) {
|
|
130
|
+
await store.updateRunState(runId, stateKey, {
|
|
131
|
+
...record,
|
|
132
|
+
decision: undefined,
|
|
133
|
+
decidedAt: undefined,
|
|
134
|
+
decidedBy: undefined,
|
|
135
|
+
error: [{ message: refusal }],
|
|
136
|
+
});
|
|
137
|
+
await store.auditApproval({
|
|
138
|
+
runId,
|
|
139
|
+
reason,
|
|
140
|
+
outcome: 'denied',
|
|
141
|
+
decidedBy: record.decidedBy,
|
|
142
|
+
refusal,
|
|
143
|
+
});
|
|
144
|
+
throw new WorkflowSuspendedException(runId, reason);
|
|
145
|
+
}
|
|
88
146
|
const validation = await options.schema['~standard'].validate(record.decision);
|
|
89
147
|
if (validation.issues) {
|
|
90
148
|
await store.updateRunState(runId, stateKey, {
|
|
91
149
|
...record,
|
|
92
150
|
decision: undefined,
|
|
93
151
|
decidedAt: undefined,
|
|
152
|
+
decidedBy: undefined,
|
|
94
153
|
error: validation.issues.map((issue) => ({
|
|
95
154
|
message: issue.message,
|
|
96
155
|
path: issue.path?.map((segment) => typeof segment === 'object' ? segment.key : segment),
|
|
@@ -98,9 +157,13 @@ export const evaluateApprovalStep = async (store, runId, reason, approvalStepNam
|
|
|
98
157
|
});
|
|
99
158
|
throw new WorkflowSuspendedException(runId, reason);
|
|
100
159
|
}
|
|
160
|
+
// Spread rather than assigned, so a gate answered without a session keeps
|
|
161
|
+
// the shape it had before there was anything to record.
|
|
101
162
|
const outcome = {
|
|
102
163
|
status: 'decided',
|
|
103
164
|
data: validation.value,
|
|
165
|
+
...(record.decidedBy ? { decidedBy: record.decidedBy } : {}),
|
|
166
|
+
...(record.decidedAt ? { decidedAt: record.decidedAt } : {}),
|
|
104
167
|
};
|
|
105
168
|
await store.setStepResult(stepState.stepId, outcome);
|
|
106
169
|
return outcome;
|
|
@@ -6,7 +6,8 @@ export declare class WorkflowRunForbiddenError extends ForbiddenError {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* A run started through a session records that session's user as its owner, and
|
|
9
|
-
* only that user may read it
|
|
9
|
+
* only that user may read it. Who may *answer* a run's approval gates is the
|
|
10
|
+
* gate's own declaration — see `approvers` on `workflow.approval()`.
|
|
10
11
|
*
|
|
11
12
|
* A run with no recorded owner — started by a trigger, a scheduler, or a route
|
|
12
13
|
* wired without auth — has nobody to compare a caller against, so ownership is
|
|
@@ -11,7 +11,8 @@ addError(WorkflowRunForbiddenError, {
|
|
|
11
11
|
});
|
|
12
12
|
/**
|
|
13
13
|
* A run started through a session records that session's user as its owner, and
|
|
14
|
-
* only that user may read it
|
|
14
|
+
* only that user may read it. Who may *answer* a run's approval gates is the
|
|
15
|
+
* gate's own declaration — see `approvers` on `workflow.approval()`.
|
|
15
16
|
*
|
|
16
17
|
* A run with no recorded owner — started by a trigger, a scheduler, or a route
|
|
17
18
|
* wired without auth — has nobody to compare a caller against, so ownership is
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { SerializedError, CommonWireMeta } from '../../types/core.types.js';
|
|
2
|
-
import type { CorePikkuFunctionConfig
|
|
2
|
+
import type { CorePikkuFunctionConfig } from '../../function/functions.types.js';
|
|
3
3
|
import type { GroupConcurrencyConfig } from '../queue/queue.types.js';
|
|
4
4
|
export type { WorkflowService } from '../../services/workflow-service.js';
|
|
5
|
-
export type { WorkflowStepOptions,
|
|
6
|
-
export type { ScenarioStepPhase, ScenarioStepOptions, PikkuScenarioStepWire, PikkuBrowserWire, ScenarioBrowserProvider, } from './scenario-step.types.js';
|
|
5
|
+
export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, Condition, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './dsl/workflow-dsl.types.js';
|
|
7
6
|
import type { WorkflowStepMeta } from './dsl/workflow-dsl.types.js';
|
|
8
7
|
export interface WorkflowRunWire {
|
|
9
8
|
type: string;
|
|
@@ -141,40 +140,6 @@ export type CoreWorkflow<PikkuFunctionConfig extends CorePikkuFunctionConfig<any
|
|
|
141
140
|
middleware?: PikkuFunctionConfig['middleware'];
|
|
142
141
|
tags?: string[];
|
|
143
142
|
};
|
|
144
|
-
export type CoreFeatureScenario = CorePikkuFunctionConfig<any, any, any> | {
|
|
145
|
-
scenario: CorePikkuFunctionConfig<any, any, any>;
|
|
146
|
-
data: unknown;
|
|
147
|
-
};
|
|
148
|
-
export type CoreFeature = {
|
|
149
|
-
name: string;
|
|
150
|
-
description?: string;
|
|
151
|
-
tags?: string[];
|
|
152
|
-
scenarios: readonly CoreFeatureScenario[];
|
|
153
|
-
before?: CorePikkuFunctionHook;
|
|
154
|
-
after?: CorePikkuFunctionHook;
|
|
155
|
-
};
|
|
156
|
-
export type FeatureMetaEntry = {
|
|
157
|
-
scenario: string;
|
|
158
|
-
data?: unknown;
|
|
159
|
-
};
|
|
160
|
-
export type FeatureMeta = {
|
|
161
|
-
id: string;
|
|
162
|
-
name: string;
|
|
163
|
-
description?: string;
|
|
164
|
-
tags: string[];
|
|
165
|
-
entries: FeatureMetaEntry[];
|
|
166
|
-
unresolvedEntries: number;
|
|
167
|
-
hasBefore: boolean;
|
|
168
|
-
hasAfter: boolean;
|
|
169
|
-
};
|
|
170
|
-
export type FeaturesMeta = Record<string, FeatureMeta>;
|
|
171
|
-
export type FeaturePlanEntry = {
|
|
172
|
-
featureId: string;
|
|
173
|
-
featureName: string;
|
|
174
|
-
scenarioName: string;
|
|
175
|
-
data?: unknown;
|
|
176
|
-
tags: string[];
|
|
177
|
-
};
|
|
178
143
|
export interface PikkuWorkflow {
|
|
179
144
|
start: <I>(input: I) => Promise<{
|
|
180
145
|
runId: string;
|
package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An addon's .pikku meta ships at the package root or under dist, and readers probe both
|
|
4
|
+
description: The layout depends on the addon's build and files field, so a consumer resolving the directory by path must try `<pkg>/.pikku` and `<pkg>/dist/.pikku` before concluding an addon declares nothing
|
|
5
|
+
tags: services
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An addon's .pikku meta ships at the package root or under dist, and readers probe both
|
|
9
|
+
|
|
10
|
+
An addon generates `.pikku/` at its project root, and whether that directory
|
|
11
|
+
reaches the published tarball at the root or inside `dist/` depends on the
|
|
12
|
+
addon's own `tsconfig` and `files` — both layouts are in the wild and both are
|
|
13
|
+
supported. The published `exports` map hides the difference for anyone
|
|
14
|
+
resolving by specifier: `"./.pikku/*"` points at wherever it landed, so
|
|
15
|
+
`require.resolve('<pkg>/.pikku/scopes/pikku-scopes-meta.gen.json')` works for
|
|
16
|
+
either shape, and that is the resolution every consumer should prefer.
|
|
17
|
+
|
|
18
|
+
Code that builds the path itself must probe both. `addonPikkuDir`
|
|
19
|
+
(`packages/addon/pikku-console/src/lib/derive-instance-overrides.ts`) tries
|
|
20
|
+
`<pkg>/.pikku` then `<pkg>/dist/.pikku` and returns `null` when neither exists.
|
|
21
|
+
The reason to say so out loud is the failure mode: a reader that assumes the
|
|
22
|
+
root finds nothing under a `dist`-shipping addon and cannot distinguish that
|
|
23
|
+
from an addon that declares no secrets, variables or scopes at all. Both answers
|
|
24
|
+
are "empty", and the wrong one is indistinguishable from a correct one until
|
|
25
|
+
something downstream reports an addon as ready when its secrets were never
|
|
26
|
+
checked.
|
|
27
|
+
|
|
28
|
+
**What this rules out:** hardcoding either path in a consumer, and treating a
|
|
29
|
+
missing directory as evidence an addon declares nothing without having tried
|
|
30
|
+
both. It also rules out normalising the layout by making the CLI move the
|
|
31
|
+
directory at publish time — the export map already makes the layout private to
|
|
32
|
+
the package, and the addons shipping each shape are already published.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An addon's scope root loses to a root the host app already declares
|
|
4
|
+
description: loadAddonFunctionsMeta merges addon scope trees by root name, first declaration wins, so an addon sharing a root with its host contributes nothing — silently
|
|
5
|
+
tags: services
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An addon's scope root loses to a root the host app already declares
|
|
9
|
+
|
|
10
|
+
`loadAddonFunctionsMeta`
|
|
11
|
+
(`packages/inspector/src/utils/load-addon-functions-meta.ts`) reads each wired
|
|
12
|
+
addon's `pikku-scopes-meta.gen.json` and pushes its entries into
|
|
13
|
+
`state.scopes.definitions`, skipping any whose root name is already present. The
|
|
14
|
+
host app's own declarations were inspected first, so the host always wins.
|
|
15
|
+
|
|
16
|
+
The skip is per **root**, not per node. An addon declaring `admin.console.*` in
|
|
17
|
+
an app that declares `admin` contributes none of it — not the root it shares,
|
|
18
|
+
and not the branch the app never declared. Nothing errors: the scopes simply do
|
|
19
|
+
not exist, so `ScopeId` never gains them, no role can be granted one, and every
|
|
20
|
+
addon function requiring one denies everybody. The failure surfaces as a
|
|
21
|
+
`MissingScopeError` against a scope that cannot be granted, which reads as a
|
|
22
|
+
permissions bug rather than a merge that dropped.
|
|
23
|
+
|
|
24
|
+
This is a different rule from
|
|
25
|
+
[scope roots may be co-declared by an addon and its host
|
|
26
|
+
app](scope-roots-may-be-co-declared-by-an-addon-and-its-host-app.md): that one
|
|
27
|
+
is about `flattenScopeDefinitions` deduping ids *within* a build where both
|
|
28
|
+
declarations are present and identical. This one is about the addon's copy never
|
|
29
|
+
arriving. The consequence for addon authors is the same either way — own a root
|
|
30
|
+
outright, named for the package or vendor, and nest everything under it. It is
|
|
31
|
+
why `@pikku/addon-console` declares `pikku:console:*` rather than
|
|
32
|
+
`admin:console:*`.
|
|
33
|
+
|
|
34
|
+
**What this rules out:** shipping an addon whose scopes hang off a root a host
|
|
35
|
+
is likely to declare, and deep-merging the two trees here so both survive. A
|
|
36
|
+
merge would have to reconcile conflicting descriptions and display names for the
|
|
37
|
+
shared nodes, and would let an addon graft capabilities onto the host's `admin`
|
|
38
|
+
tree — where a role granting `admin` would pick them up without anyone having
|
|
39
|
+
asked for them.
|
|
@@ -22,6 +22,7 @@ caller is entitled to assume.
|
|
|
22
22
|
- [The actor-flow conversation engine only sees a transport-agnostic target driver](actor-flow-drives-the-target-through-a-transport-seam.md) — The engine never imports the agent runner; the target is injected as run/approve, so scenarios exercise the real wire path
|
|
23
23
|
- [An actor-flow verdict is the persona's self-evaluation, not an assertion](actor-flow-verdicts-are-llm-self-evaluations.md) — The engine returns what the actor judged plus the transcript; deterministic checks stay with the caller
|
|
24
24
|
- [Addon package roots resolve by walking node module search paths](addon-package-roots-resolve-by-walking-node-module-search-paths.md) — LocalMetaService finds an addon's directory by scanning resolve.paths, not by require.resolve, because addon packages expose no main entry
|
|
25
|
+
- [An addon's .pikku meta ships at the package root or under dist, and readers probe both](addon-pikku-meta-ships-at-the-package-root-or-under-dist.md) — The layout depends on the addon's build and files field, so a consumer resolving the directory by path must try `<pkg>/.pikku` and `<pkg>/dist/.pikku` before concluding an addon declares nothing
|
|
25
26
|
- [Addon singleton services are cached per namespace, not per package](addon-singleton-services-are-cached-per-namespace-not-per-package.md) — Each wireAddon instance gets its own services built from its own overrides, at the cost of one service graph per wired instance
|
|
26
27
|
- [Bare workflow names from inside an addon are prefixed with the consumer's namespace](addon-workflow-names-are-prefixed-with-the-consumer-namespace.md) — The addon's workflowService is proxied so an addon never has to hardcode the name its consumer chose
|
|
27
28
|
- [Loading agent context waits for a tool result that may still be landing](agent-context-waits-for-a-tool-result-still-being-written.md) — An interrupted run's tool can still be writing to the thread, and in voice the next turn arrives within seconds — soon enough to load context missing what it is about to be asked about
|
|
@@ -39,6 +40,7 @@ caller is entitled to assume.
|
|
|
39
40
|
- [A tool's execute() failure is logged before the AI SDK swallows it](ai-agent-tool-execute-failures-are-logged-unconditionally.md) — Every agent tool is wrapped in a logging try/catch, because a thrown tool error otherwise becomes a conversational reply and is invisible server-side
|
|
40
41
|
- [Voice input transcribes audio parts sequentially and in place](ai-agent-voice-input-transcribes-audio-parts-in-place.md) — Each audio part is replaced by its text where it sat, one at a time, bounding concurrent downloads and preserving content order
|
|
41
42
|
- [Working memory is persisted only when the merged value validates](ai-agent-working-memory-is-persisted-only-when-valid.md) — A failed schema check logs and drops the update rather than saving it, because invalid state poisons every later read
|
|
43
|
+
- [An addon's scope root loses to a root the host app already declares](an-addon-scope-root-loses-to-a-root-the-host-already-declares.md) — loadAddonFunctionsMeta merges addon scope trees by root name, first declaration wins, so an addon sharing a root with its host contributes nothing — silently
|
|
42
44
|
- [An interrupt is not a failure, and the non-streaming path throws rather than returning](an-agent-interrupt-is-not-a-failure.md) — It skips the onError hooks and never becomes an errorMessage; with no partial reply to hand back, a typed throw is what distinguishes it from a provider outage
|
|
43
45
|
- [An interrupt for a run owned by another instance says so, rather than returning false](an-agent-run-owned-by-another-instance-says-so.md) — A bare false is indistinguishable from "already finished", which is the one deployment shape the in-process registry cannot cover
|
|
44
46
|
- [A wrapped agent-stream send must return the inner send's promise](an-agent-stream-send-must-return-the-inner-sends-promise.md) — Middleware runs asynchronously, so dropping the returned promise turns every awaited channel.send upstream into a no-op
|
|
@@ -54,11 +56,15 @@ caller is entitled to assume.
|
|
|
54
56
|
- [CLI parse errors are routed by message prefix](cli-parse-errors-are-routed-by-message-prefix.md) — The CLI runners decide between printing help and printing errors by string-matching the prefixes the parser writes, so those message strings are an interface
|
|
55
57
|
- [CLI stdout is reserved for machine-readable output](cli-stdout-is-reserved-for-machine-readable-output.md) — The default renderer emits single-line NDJSON, diagnostics go to stderr, and --json only hijacks rendering for commands that declared a renderer
|
|
56
58
|
- [CLI unknown long options warn instead of failing](cli-unknown-long-options-warn-instead-of-failing.md) — Unrecognised --long options are accepted, warned about and dropped so older binaries tolerate newer invocations, while unknown short flags stay hard errors
|
|
59
|
+
- [A column's at-rest form is an axis of its own](core-column-form-is-an-axis-of-its-own.md) — How a value is stored is independent of how sensitive it is, so form carries a required nominal brand on writes while classification stays optional on reads
|
|
60
|
+
- [The data-classification brand is an optional property](core-data-classification-brand-is-an-optional-property.md) — Making __classification__ required would break ordinary Kysely operands, so the brand only constrains values flowing out
|
|
57
61
|
- [The function runner restores the wire fields it overwrites](core-function-runner-restores-the-wire-fields-it-overwrites.md) — One wire object is reused across nested calls, so functionId, audit, addonNamespace and rpc are saved and put back in a finally
|
|
58
62
|
- [Hot reload merges generated meta and never replaces it](core-hot-reload-merges-generated-meta-never-replaces-it.md) — Reloading codegen output must preserve runtime-registered meta, which no generated JSON contains
|
|
59
63
|
- [Hot reload owns its module registry instead of re-importing](core-hot-reload-owns-its-module-registry.md) — Dev reload transpiles to CJS and runs modules through vm.compileFunction, because the native ESM loader map cannot be evicted
|
|
60
64
|
- [Middleware order is resolution scope first, then priority](core-middleware-order-is-scope-then-priority.md) — Middleware is collected global to function, then stably sorted by priority, deduped, frozen and cached per wire
|
|
65
|
+
- [Schema defaults are applied on every transport, not just HTTP](core-schema-defaults-apply-on-every-transport.md) — Defaults belong to the schema rather than the call's encoding, so they run unconditionally and are cloned per request
|
|
61
66
|
- [Scopes are an AND gate, separate from permissions](core-scopes-are-an-and-gate-separate-from-permissions.md) — Every declared scope must be held, so adding one can only narrow access — permissions OR, and can only widen it
|
|
67
|
+
- [Pikku state is a global map written only at registration time](core-state-is-a-global-map-written-only-at-registration-time.md) — A symbol-keyed globalThis map holds the wiring registry; nothing per-request may ever be written to it
|
|
62
68
|
- [Email meta is read uncached because codegen rewrites it mid-session](email-meta-is-read-uncached-because-codegen-rewrites-it-mid-session.md) — getEmailMeta re-reads its file on every call, unlike every other meta accessor, because the file appears and changes during a long-lived session
|
|
63
69
|
- [Gateway adapters resolve lazily and are promise-cached](gateway-adapters-resolve-lazily-and-are-promise-cached.md) — wireGateway accepts an adapter factory because real adapters need boot-time secrets, which forces the webhook GET route to be registered unconditionally
|
|
64
70
|
- [Gateway middleware sees wire.rpc on webhook and websocket, but not on listener](gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md) — The listener transport invokes its handler directly, so middleware runs outside any runPikkuFunc call and the lazily-attached rpc is absent
|
|
@@ -68,22 +74,27 @@ caller is entitled to assume.
|
|
|
68
74
|
- [Hot reload writes into the function map captured at startup, not pikkuState's current one](hot-reload-writes-into-the-function-map-captured-at-startup.md) — A dev-server watcher may have swapped in a codegen-scoped map whose writes are discarded on restore, and schemas are deliberately left alone
|
|
69
75
|
- [HTTP request bodies are read once and shared between consumers](http-request-bodies-are-read-once-and-shared.md) — The fetch request wrapper memoises the single-use body and builds web Requests lazily, at the cost of holding the whole body in memory
|
|
70
76
|
- [HTTP route groups cascade config in a fixed, per-field order](http-route-groups-cascade-config-in-a-fixed-order.md) — basePath concatenates, tags and middleware merge outward-in, auth is overridden by the innermost group
|
|
77
|
+
- [The HTTP router matches normalized paths but returns the registered path](http-router-matches-normalized-paths-but-returns-registered-ones.md) — Matching normalizes the leading slash while the match result carries the original key, because pikkuState is keyed by the registered string
|
|
78
|
+
- [The HTTP runner logs through a trace-scoped logger, functions do not](http-runner-logs-through-a-trace-scoped-logger-functions-do-not.md) — Only runner-internal logging is scoped to the request id; functions keep the singleton logger for compatibility
|
|
79
|
+
- [Set-Cookie headers are appended individually, never joined](http-set-cookie-headers-are-appended-never-joined.md) — Every cookie gets its own header line, because Set-Cookie is the one header comma-joining corrupts
|
|
80
|
+
- [HTTP SSE streams flush headers only after middleware has run](http-sse-streams-flush-headers-only-after-middleware.md) — SSE responses defer the header flush and register with the eventHub, at the cost of a stricter ordering the runner must preserve
|
|
71
81
|
- [HTTP wiring without generated metadata is skipped, not fatal](http-wiring-without-metadata-is-skipped-not-fatal.md) — wireHTTP warns and returns when a route has no metadata, so partial deploy units still boot
|
|
72
82
|
- [In a scenario a 4xx is data, not an exception](in-a-scenario-a-4xx-is-data-not-an-exception.md) — postScenarioJson and invokeRaw report the status instead of throwing, because a refusal is the expected outcome of a permissions scenario
|
|
73
83
|
- [In-memory workflow history aliases the live step object](in-memory-workflow-history-aliases-the-live-step-object.md) — stepHistory pushes the same StepState reference that steps holds, so later mutations to a step are visible in its history entry
|
|
74
84
|
- [Istanbul statement counts attach to the start line only](istanbul-statement-counts-attach-to-the-start-line-only.md) — The istanbul coverage reader credits a statement's hits to its first line, so an enclosing multi-line statement cannot mask an unexecuted inner one
|
|
75
85
|
- [Local trigger and gateway services assume a single process](local-trigger-and-gateway-services-assume-a-single-process.md) — InMemoryTriggerService and LocalGatewayService start every listener unconditionally with no distributed claiming, so a second instance duplicates every event
|
|
76
86
|
- [Node-only builtins are imported dynamically](node-only-builtins-are-imported-dynamically.md) — V8CoverageService imports node:inspector inside start() so the module stays loadable on runtimes that have no such builtin
|
|
77
|
-
- [One project-shape check, called by both validators](one-project-shape-check-two-validators.md) — workspace validate and fabric validate were separate walks over the same project that duplicated sixteen findings verbatim; the shared half now lives in shared-checks.ts
|
|
87
|
+
- [One project-shape check, called by both validators](one-project-shape-check-two-validators.md) — workspace validate and fabric validate were separate walks over the same project that duplicated sixteen findings verbatim; the shared half now lives in shared-checks.ts and fabric validate is that plus the deploy-shaped checks
|
|
78
88
|
- [Only functions marked `expose: true` enter a virtual user's catalogue](only-exposed-functions-enter-a-virtual-user-catalogue.md) — Absent is not permissive — an unexposed function 404s over rpc, so offering one spends a step to learn nothing about the product
|
|
79
89
|
- [Queue group concurrency keeps one shared queue fair](queue-group-concurrency-keeps-one-shared-queue-fair.md) — Per-group slot caps let many producers share a single queue instead of one queue each, at the cost of a cap that must stay under batchSize
|
|
80
90
|
- [Queue jobs always carry an explicit attempts count](queue-jobs-always-carry-an-explicit-attempts-count.md) — resolveJobOptions always passes attempts so a queue backend can never apply its own retry default, and an explicit retries of 0 is honoured
|
|
81
91
|
- [Remote addons dispatch over HTTP instead of through local function meta](remote-addons-dispatch-over-http-instead-of-local-meta.md) — wireRemoteAddon ships the addon as a devDependency and posts to the host, so there is deliberately no local handler to resolve
|
|
92
|
+
- [Bare RPC names resolve through the caller's package scope before root](rpc-names-resolve-through-package-scope-before-root.md) — An addon's own functions win over root RPC meta for bare names, and the resolving scope is returned so it can be threaded into runPikkuFunc
|
|
82
93
|
- [Scenario agent calls sign in on 401 only](scenario-agent-calls-sign-in-on-401-only.md) — postAgent sends the first request with whatever session it holds and logs in only if refused, so an actor can converse with a no-auth agent with no sign-in wiring
|
|
83
94
|
- [`given` and `when` are sugar for each other; `then` is not](scenario-given-and-when-are-sugar-but-then-is-not.md) — The phase decides whether a step's bindings are alternatives or witnesses, so the same step function called as `when` and as `then` runs differently
|
|
84
95
|
- [Scenario meta lives apart from app meta but merges when read off disk](scenario-meta-lives-apart-from-app-meta-but-merges-when-read-off-disk.md) — Scenarios generate into .pikku/scenarios so no app module imports them, yet MetaService folds them back into the workflow and function meta
|
|
85
96
|
- [Scenario steps return drained response records](scenario-steps-return-drained-response-records.md) — A scenario step hands back ScenarioHttpResponse rather than a Response, because the body stream reads once and the return value crosses into the run record
|
|
86
|
-
- [Scenarios, steps and personas live in files named for them](scenarios-live-in-files-named-for-them.md) — pikkuScenario/pikkuScenarioStep must be in *.scenario.ts
|
|
97
|
+
- [Scenarios, steps and personas live in files named for them, and validate errors when they do not](scenarios-live-in-files-named-for-them.md) — pikkuScenario/pikkuFeature/pikkuScenarioStep must be in *.scenario.ts, *.scenarios.ts or *.steps.ts, and definePersonas/runVirtualUser in *.virtual-user.ts or *.vu.ts — an error, because the mixing is only cheap to undo while it is one file
|
|
87
98
|
- [Scope roots may be co-declared by an addon and its host app](scope-roots-may-be-co-declared-by-an-addon-and-its-host-app.md) — flattenScopeDefinitions dedupes ids because the same root can legitimately be declared twice, and every consumer requires one entry per scope
|
|
88
99
|
- [Serverless channel disconnect must tolerate a missing channel](serverless-channel-disconnect-must-tolerate-a-missing-channel.md) — A failed channel lookup on disconnect returns quietly, because serverless runtimes deliver disconnect more than once
|
|
89
100
|
- [sideEffects is an allowlist, because core genuinely has some](side-effects-are-an-allowlist-not-a-boolean.md) — The error registry is built by addError calls that run on import, so claiming sideEffects:false would let a bundler drop it and leave getErrorResponse unable to find any error
|
|
@@ -106,7 +117,8 @@ caller is entitled to assume.
|
|
|
106
117
|
- [Thread history records what the model was asked, which for a spoken turn is the transcript](thread-history-records-the-transcript-not-the-audio.md) — The wire carried a base64 audio blob; persisting it writes megabytes of unreadable data and loses the only readable record of the turn
|
|
107
118
|
- [Trigger declaration is split from trigger source](trigger-declaration-is-split-from-trigger-source.md) — Triggers are declared everywhere but subscribed only in the trigger worker, so app processes never open the underlying subscription
|
|
108
119
|
- [TypedSecretService caches for the process lifetime](typed-secret-service-caches-for-the-process-lifetime.md) — Resolved secrets are cached with no TTL, so a secret rotated out of band is not picked up until restart — tracked as pikkujs/pikku#964
|
|
109
|
-
- [Both validators check personas through one shared module, and a persona counts from either the meta or the source](validate-checks-personas-through-a-shared-module.md) — workspace validate and fabric validate are separate implementations, so the persona/actor/knowledge checks live in persona-checks.ts and are called by both
|
|
120
|
+
- [Both validators check personas through one shared module, and a persona counts from either the meta or the source](validate-checks-personas-through-a-shared-module.md) — workspace validate and fabric validate are separate implementations, so the persona/actor/knowledge checks live in persona-checks.ts and are called by both — and a persona is counted from the generated meta or a definePersonas() source scan, whichever answers first
|
|
121
|
+
- [pikku validate runs every applicable check, it does not detect a project kind](validate-runs-checks-by-precondition.md) — one `pikku validate` replaces `pikku workspace validate`; checks declare a precondition and run wherever it holds, because the repos that matter are an app and a pile of addons at the same time
|
|
110
122
|
- [A scenario's step order for a virtual user is insertion order, not a graph traversal](virtual-user-step-order-comes-from-insertion-order.md) — The CLI writes nodes in declaration order, so following `next` would buy an ordering that is already true and cost a traversal that has to interpret branches
|
|
111
123
|
- [Only an explicit `false` silences a spoken reply](voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md) — The key being absent means no voice input is wired and nothing reported either way, so those callers keep the behaviour they had before the option existed
|
|
112
124
|
- [Webhook delivery history records every attempt, best effort](webhook-delivery-history-records-every-attempt-best-effort.md) — The webhook worker persists each attempt before it throws, and a failure to persist is logged rather than allowed to mask the delivery result
|
|
@@ -116,14 +128,29 @@ caller is entitled to assume.
|
|
|
116
128
|
- [Workflow approval expiry is decided from a recorded deadline, not from a timer firing](workflow-approval-expiry-is-decided-from-a-recorded-deadline.md) — The wake-up job is best-effort liveness; losing, duplicating or delaying it cannot change the gate's answer
|
|
117
129
|
- [Core declares the scenario browser surface structurally and never imports a driver](workflow-core-never-imports-a-browser-driver.md) — `@pikku/core` must stay dependency-free for edge runtimes, so playwright augments the interface instead of being imported by it
|
|
118
130
|
- [Workflow DSL meta keeps runtime expressions in their own field, apart from literal values](workflow-dsl-meta-separates-runtime-expressions-from-literals.md) — A string `value` regenerates as a string literal; an `expression` regenerates as code, so the two can never share a field
|
|
131
|
+
- [A feature resolves its scenarios by object identity, never by name or shape](workflow-features-resolve-scenarios-by-object-identity.md) — An unregistered scenario comes back explicitly unresolved rather than silently running as something else
|
|
119
132
|
- [Inline and queued workflow graph runs share one transition planner](workflow-graph-inline-and-queued-runs-share-one-planner.md) — A second, weaker inline traversal would lose joins, cycle revisits and step provenance that the queued path has
|
|
120
133
|
- [Workflow graph node notes are non-semantic and excluded from the graph hash](workflow-graph-node-notes-are-excluded-from-the-graph-hash.md) — Documentation on a node must not count as a topology change, or editing a comment redeploys the workflow
|
|
134
|
+
- [An inline workflow run reports its run id the moment it exists, because a failure throws instead of returning](workflow-inline-runs-report-their-run-id-before-they-can-fail.md) — `onRunCreated` is the only moment guaranteed to happen whether the run passes, fails or suspends
|
|
121
135
|
- [`invocationId` is a workflow step's dedupe key; `stepId` is store-specific and must never be used as one](workflow-invocation-id-is-the-dedupe-key-not-step-id.md) — The invocation id is a frozen UUIDv5 of runId + stepName, identical across retries on every backend
|
|
136
|
+
- [A workflow step goes through the queue only if its function opts in, and there is no inline fallback](workflow-queued-step-dispatch-requires-an-explicit-opt-in.md) — `workflowQueued: true` is the whole decision; a missing queue service is a hard error, not a silent downgrade
|
|
122
137
|
- [Workflows get their own queues by default, and queue names are resolved from queue meta](workflow-queues-are-per-workflow-by-default.md) — Per-workflow queues stop one slow step head-of-line-blocking every other workflow; `shared-groups` trades that for one set of pollers
|
|
138
|
+
- [A workflow step name repeated in one run gets an ordinal suffix, and the first reach stays bare](workflow-repeated-step-names-get-an-ordinal-suffix.md) — `name`, `name#1`, `name#2` keys each reach separately without changing the durable key of any existing run
|
|
139
|
+
- [A workflow replay reads its steps once and caches only the run's immutable half](workflow-replay-reads-its-steps-once-and-caches-only-the-immutable-half.md) — The per-replay snapshot collapses O(N^2) step reads to one, but caching mutable run fields would make the replay read a lie
|
|
123
140
|
- [Workflow step retries are owned by the workflow, never by the queue](workflow-retries-are-owned-by-the-workflow-not-the-queue.md) — A step's retry count is resolved once and always passed to the queue as `attempts`, so the queue can never apply its own default
|
|
124
141
|
- [Workflow run capabilities are extensions, not subclasses](workflow-run-capabilities-are-extensions-not-subclasses.md) — Scenario support lives in a separate module behind `setRunExtension` because a bundler drops an unused module but never an unused class member
|
|
142
|
+
- [The workflow run mirror is an observability sink, never a second source of truth](workflow-run-mirror-is-never-a-source-of-truth.md) — Every mirrored write happens after the authoritative write lands, and a mirror failure can never fail the workflow
|
|
125
143
|
- [Workflow run polling starts short and backs off to the caller's ceiling](workflow-run-polling-backs-off-to-the-callers-ceiling.md) — `pollIntervalMs` is a ceiling, not a cadence, and the wait lives in its own method so the schedule can be asserted without the clock
|
|
144
|
+
- [The workflow run timeline is a pure fold over durable history, with the row's status as the authority](workflow-run-timeline-is-a-pure-fold-over-durable-history.md) — No IO in the fold keeps time-travel transport-independent; the terminal event comes from `status`, not from a timestamp every backend populates
|
|
126
145
|
- [Scenario steps default to no retries, and a whole poll is one durable step](workflow-scenario-assertions-never-retry-and-record-one-step.md) — Retrying a failed assertion is wrong for a test primitive; recording the poll as one step means replay returns the outcome, not the loop
|
|
146
|
+
- [Scenario lifecycle hooks are a scenario-only affordance and never mask the failure they follow](workflow-scenario-hooks-are-a-scenario-only-affordance.md) — A durable workflow replays, so a callback that reruns each replay has no honest meaning there
|
|
127
147
|
- [Scenario prose is rendered out of typed calls, not parsed into them](workflow-scenario-prose-is-rendered-from-typed-calls-not-parsed-from-english.md) — The inversion of cucumber — a readable report with no regex step registry to maintain
|
|
148
|
+
- [A quarantined scenario states its reason in code, not in a CI invocation](workflow-scenario-quarantine-reason-lives-in-code.md) — `skip` carries the why next to the scenario it applies to, and naming the scenario explicitly still runs it
|
|
128
149
|
- [Scenario step targets are string literals so the inspector can read them statically](workflow-scenario-step-targets-are-string-literals-for-the-inspector.md) — `step/given/when/then` mirror `do`'s RPC shape because the extractor reads a literal, not an imported symbol
|
|
150
|
+
- [A step's compensation handler runs as a durable step of its own, and never compensates itself](workflow-step-compensation-runs-as-its-own-durable-step.md) — A refund or rollback must not fire twice on replay, so `onError` is recorded as `<step>:onError` with retries disabled
|
|
151
|
+
- [A failed workflow step dispatch is transient infrastructure, not a run failure](workflow-step-dispatch-failure-is-transient-not-a-run-failure.md) — Queue-unreachable errors leave the run running and the step pending so the orchestrator replays; marking the run failed loses it
|
|
152
|
+
- [A workflow step lock is held only to claim the step, never across its execution](workflow-step-lock-is-held-only-to-claim-the-step.md) — Holding the advisory lock — and its pooled connection — across step work exhausted the connection pool and self-deadlocked
|
|
153
|
+
- [A workflow step's recorded `rpcName` is provenance only — nothing dispatches off it](workflow-step-rpc-name-is-provenance-only.md) — It exists so a reader can join a runtime step row back to the declaration that produced it, especially when the durable name was built in a loop
|
|
154
|
+
- [A suspend or approval `reason` is the step's durable identity, not just a message](workflow-suspend-and-approval-reasons-are-durable-step-identities.md) — The reason is namespaced and used raw as the step key, so it must be derived deterministically across replays
|
|
155
|
+
- [A suspended workflow run keeps its in-process context; only terminal runs release it](workflow-suspended-runs-keep-their-in-process-context.md) — `suspended` is absent from the terminal set on purpose, and a context is dropped only when nothing is holding it open
|
|
129
156
|
<!-- /pikku:knowledge-index -->
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: pikku validate runs every applicable check, it does not detect a project kind
|
|
4
|
+
description: one `pikku validate` replaces `pikku workspace validate`; checks declare a precondition and run wherever it holds, because the repos that matter are an app and a pile of addons at the same time
|
|
5
|
+
tags: cli, validate, addons
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# pikku validate runs every applicable check, it does not detect a project kind
|
|
9
|
+
|
|
10
|
+
`pikku workspace validate` was named after one of the things a repo can be.
|
|
11
|
+
Adding a second — "is this addon publishable" — invited a second command,
|
|
12
|
+
`pikku addon validate`, and that is where the naming falls apart: the addons
|
|
13
|
+
repo is a workspace *containing* 217 publishable addons. Standing at its root,
|
|
14
|
+
`pikku addon validate` would have to refuse or sweep every package, at which
|
|
15
|
+
point it is doing the workspace thing anyway. The two nouns were never
|
|
16
|
+
alternatives.
|
|
17
|
+
|
|
18
|
+
So there is one command, and it does not detect a kind and dispatch. Each check
|
|
19
|
+
declares the condition under which it means anything, and runs wherever that
|
|
20
|
+
condition holds:
|
|
21
|
+
|
|
22
|
+
- `app-project` — a `pikku.config.json` with no `types/application-types.d.ts`
|
|
23
|
+
beside it. The marker matters: an addon carries a `pikku.config.json` too, and
|
|
24
|
+
the app-shaped checks would report every app convention it has no reason to
|
|
25
|
+
follow, starting with a `packages/functions/` it will never have.
|
|
26
|
+
- `addon-package` — a non-private package that ships generated pikku output.
|
|
27
|
+
Having a `.pikku` directory is not enough on its own: an app's
|
|
28
|
+
`packages/functions` has one, and it is codegen for that app rather than
|
|
29
|
+
something anyone installs. The signal is a `files`/`exports` that carries it
|
|
30
|
+
into the tarball.
|
|
31
|
+
|
|
32
|
+
Detection-as-dispatch fails on the first repo that is two things at once.
|
|
33
|
+
Detection-as-precondition composes: the addons repo plans 217 addon checks and
|
|
34
|
+
no app check; the online-shop template plans one app check and no addon check;
|
|
35
|
+
a repo that is both plans both.
|
|
36
|
+
|
|
37
|
+
## Targets come from walking, not from `workspaces`
|
|
38
|
+
|
|
39
|
+
The field is an array in one repo and `{ packages: [...] }` in the next, spells
|
|
40
|
+
the same layout as `packages/**` or as six explicit globs, and a package that is
|
|
41
|
+
real but unlisted is exactly the kind of thing worth validating. So discovery
|
|
42
|
+
walks the tree for `package.json` files and lets the preconditions decide.
|
|
43
|
+
Over-collecting targets is free; guessing the glob dialect is not.
|
|
44
|
+
|
|
45
|
+
## A run that checked nothing must not print a tick
|
|
46
|
+
|
|
47
|
+
The failure mode of auto-detection is that finding nothing looks exactly like
|
|
48
|
+
finding everything and liking it. When no check applies, the renderer says so
|
|
49
|
+
and names what it looked for, rather than reporting success.
|
|
50
|
+
|
|
51
|
+
## The check the addon case exists for
|
|
52
|
+
|
|
53
|
+
Every relative import in a shipped generated file must resolve to a file that is
|
|
54
|
+
itself shipped — stated as a property, not as "the build script must copy
|
|
55
|
+
`types/`". The build script is not the only way to get this wrong and will not
|
|
56
|
+
always be a `cp`; the generated files already declare what they need.
|
|
57
|
+
|
|
58
|
+
That property was false in all 217 published addons. `cp -r .pikku dist/`
|
|
59
|
+
shipped the generated output but not the `types/application-types.d.ts` it
|
|
60
|
+
imports, and `tsc` never emits a hand-written `.d.ts` to `outDir`, so nothing
|
|
61
|
+
put it in `dist`. Consumers got 8 unresolved-module errors plus 6
|
|
62
|
+
`RequiredServices does not satisfy CoreSecretlessSingletonServices` follow-ons
|
|
63
|
+
inside `node_modules`, for merely depending on an addon. The scaffold template
|
|
64
|
+
had been right the whole time — `templates/function-addon` copies `types/` —
|
|
65
|
+
and the published packages had drifted from it with nothing watching.
|
|
66
|
+
|
|
67
|
+
## The check walks whichever generated directory a package ships
|
|
68
|
+
|
|
69
|
+
`dist/.pikku` is the target shape (below), but the check cannot assume it: the
|
|
70
|
+
shape is what this work moved addons *to*, and a package that has not moved —
|
|
71
|
+
or was published before it did — still carries `.pikku` at the root, listed in
|
|
72
|
+
`files` and mapped by `exports` as `./.pikku/*`. There it is a public entry
|
|
73
|
+
point rather than build input, and its imports climb one level fewer — to
|
|
74
|
+
`<pkg>/src` and `<pkg>/types` — where the copy under `dist` reaches
|
|
75
|
+
`<pkg>/dist/src` and `<pkg>/dist/types`. Two roots, two ways to fall outside
|
|
76
|
+
the tarball, so the check walks whichever ones are actually shipped rather than
|
|
77
|
+
the one it would prefer to find.
|
|
78
|
+
|
|
79
|
+
`exports` and `imports` get the same treatment one level up: a target outside
|
|
80
|
+
the published file set is the same defect, and the one the import walk cannot
|
|
81
|
+
see, because nothing inside `dist/.pikku` mentions it. That is how a `#pikku`
|
|
82
|
+
still pointing at `./.pikku/pikku-types.gen.ts` hides.
|
|
83
|
+
|
|
84
|
+
## An addon's entry points all resolve under dist
|
|
85
|
+
|
|
86
|
+
Everything an installed package reaches for lives under `dist`; the addon's own
|
|
87
|
+
build resolves `#pikku` through tsconfig `paths`, so nothing in `exports` or
|
|
88
|
+
`imports` has to point into the source tree, and `files` is just `["dist"]`.
|
|
89
|
+
The alternative — shipping `src/` and `types/` at the root so the existing paths
|
|
90
|
+
resolve as written — publishes TypeScript source and a second copy of
|
|
91
|
+
everything `dist` already has.
|
|
92
|
+
|
|
93
|
+
Checking only `dist` is what let the root `.pikku` stay broken through the first
|
|
94
|
+
round of fixes: in the published `@pikku/addon-assemblyai@0.1.4` tarball, `.pikku`
|
|
95
|
+
ships `.gen.ts` files importing a `../../src/` and `../types/` that the tarball
|
|
96
|
+
does not contain, and the `pikku-bootstrap.gen.js` that consumers import through
|
|
97
|
+
that subpath exists only under `dist`. Everything resolved locally through the
|
|
98
|
+
workspace link and none of it resolved on install.
|
|
99
|
+
|
|
100
|
+
## Only a package that publishes gets the dist shape
|
|
101
|
+
|
|
102
|
+
The shape describes a tarball, so it means nothing for a `private` package —
|
|
103
|
+
and applying it there actively breaks: `exports` *is* enforced across a
|
|
104
|
+
workspace link, so repointing a private fixture at `dist` makes every consumer
|
|
105
|
+
demand a directory that only a build produces. The three `verifiers/db-schema`
|
|
106
|
+
and `verifiers/addon-registry` fixtures have no build script at all, so `dist`
|
|
107
|
+
never exists for them; the five `e2e/packages` addons build, but their metadata
|
|
108
|
+
is read straight from the source tree before any build has run. Repointing all
|
|
109
|
+
eight left the db-schema verifier unable to resolve
|
|
110
|
+
`dist/.pikku/db/pikku-db-meta.gen.json` and every e2e addon reporting "no
|
|
111
|
+
function metadata".
|
|
112
|
+
|
|
113
|
+
`isAddonPackage` already draws this line — it returns false for `private` — so
|
|
114
|
+
the checks and the shape agree: a package the registry never sees is consumed
|
|
115
|
+
from source.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A function never receives the secret service
|
|
4
|
+
description: Every function-, permission- and auth-facing services type is bounded by SecretlessServices, so reaching for `secrets` in a function body is a type error rather than a lint
|
|
5
|
+
tags: services
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A function never receives the secret service
|
|
9
|
+
|
|
10
|
+
`SecretlessServices<Services>` is `Omit<Services, 'secrets'>`
|
|
11
|
+
(`packages/core/src/types/core.types.ts`), and
|
|
12
|
+
`CoreSecretlessSingletonServices` built from it is the constraint every
|
|
13
|
+
function-, permission- and auth-facing type is bounded by
|
|
14
|
+
(`packages/core/src/function/functions.types.ts`). Destructuring `secrets`
|
|
15
|
+
inside a `pikkuFunc` body does not lint — it does not compile.
|
|
16
|
+
|
|
17
|
+
The rule itself is older than the type: a function holding a `SecretService` can
|
|
18
|
+
read every secret in the vault, which makes its blast radius the whole vault
|
|
19
|
+
rather than the one credential it needs, and makes "which secrets does this
|
|
20
|
+
function depend on?" unanswerable. `[PKU950]` enforces the same confinement for
|
|
21
|
+
a `SecretService` reaching a function under an alias, because a rename does not
|
|
22
|
+
change what it is. Encoding it in the type is what makes the honest mistake
|
|
23
|
+
impossible rather than merely reported: secrets are resolved where things are
|
|
24
|
+
constructed — `pikkuServices`, `pikkuWireServices`, addon service factories,
|
|
25
|
+
middleware — and the function is handed the configured client.
|
|
26
|
+
|
|
27
|
+
The cost is that a function which needs to *ask about* a secret rather than read
|
|
28
|
+
one — "is this key set?", for a readiness or provisioning check — cannot do it
|
|
29
|
+
directly either. It goes through a service that holds `secrets` and exposes only
|
|
30
|
+
that question, which is how `@pikku/addon-console` checks whether an installed
|
|
31
|
+
addon's declared secrets are present.
|
|
32
|
+
|
|
33
|
+
**What this rules out:** widening a function's services type back to
|
|
34
|
+
`CoreServices` for a function that "only needs one secret", and passing the
|
|
35
|
+
secret service through under another name — the type follows the shape and
|
|
36
|
+
`[PKU950]` follows the type. It also rules out treating the absence as an
|
|
37
|
+
oversight to be patched with a cast.
|