@pikku/core 0.12.79 → 0.12.80
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 +161 -0
- package/dist/dev/hot-reload.js +1 -6
- package/dist/ecosystem.d.ts +27 -0
- package/dist/ecosystem.js +26 -0
- package/dist/errors/error-handler.js +4 -2
- package/dist/function/function-runner.js +20 -42
- package/dist/function/functions.types.d.ts +9 -5
- package/dist/index.d.ts +4 -9
- package/dist/index.js +3 -8
- package/dist/middleware/auth-cookie.d.ts +0 -4
- package/dist/middleware/auth-cookie.js +38 -2
- package/dist/middleware/cors.js +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.js +14 -3
- package/dist/permissions.d.ts +8 -10
- package/dist/permissions.js +0 -11
- package/dist/pikku-state.js +12 -5
- package/dist/schema.js +35 -1
- package/dist/services/ai-run-state-service.d.ts +7 -1
- package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
- package/dist/services/in-memory-ai-run-state-service.js +2 -1
- package/dist/services/in-memory-workflow-service.js +2 -0
- package/dist/services/local-content-request-handler.d.ts +21 -0
- package/dist/services/local-content-request-handler.js +72 -53
- package/dist/services/local-content.d.ts +6 -0
- package/dist/services/local-content.js +14 -1
- package/dist/services/workflow-service.d.ts +4 -2
- package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
- package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
- package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
- package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/channel-store-tests.js +72 -0
- package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/credential-service-tests.js +109 -0
- package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/deployment-service-tests.js +21 -0
- package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
- package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/secret-service-tests.js +80 -0
- package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/session-store-tests.js +43 -0
- package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
- package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-service-tests.js +150 -0
- package/dist/testing/service-tests.d.ts +6 -0
- package/dist/testing/service-tests.js +26 -791
- package/dist/types/core.types.d.ts +11 -1
- package/dist/types/state.types.d.ts +1 -1
- package/dist/wirings/actor-flow/run-conversation.js +1 -6
- package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
- package/dist/wirings/ai-agent/agent-rpc.js +53 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
- package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
- package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
- package/dist/wirings/ai-agent/voice-input.js +1 -6
- package/dist/wirings/ai-agent/voice-output.js +2 -12
- package/dist/wirings/channel/channel-common.js +1 -0
- package/dist/wirings/channel/channel-handler.js +3 -5
- package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
- package/dist/wirings/channel/channel-rpc-service.js +0 -8
- package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
- package/dist/wirings/channel/channel-rpc.types.js +8 -0
- package/dist/wirings/channel/channel-runner.d.ts +1 -3
- package/dist/wirings/channel/channel-runner.js +16 -8
- package/dist/wirings/channel/channel.types.d.ts +2 -0
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
- package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
- package/dist/wirings/cli/cli-runner.js +4 -2
- package/dist/wirings/cli/cli.types.d.ts +0 -8
- package/dist/wirings/cli/command-parser.js +13 -0
- package/dist/wirings/gateway/gateway-runner.js +9 -2
- package/dist/wirings/http/http-routes.js +2 -0
- package/dist/wirings/http/http-runner.d.ts +0 -10
- package/dist/wirings/http/http-runner.js +6 -13
- package/dist/wirings/http/http.types.d.ts +0 -10
- package/dist/wirings/http/index.d.ts +1 -1
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
- package/dist/wirings/mcp/mcp-runner.js +0 -6
- package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
- package/dist/wirings/rpc/rpc-runner.js +53 -86
- package/dist/wirings/rpc/rpc-types.d.ts +3 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -2
- package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
- package/dist/wirings/trigger/trigger-runner.js +1 -0
- package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
- package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
- package/dist/wirings/workflow/feature.js +7 -4
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
- package/dist/wirings/workflow/graph/graph-runner.js +8 -7
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
- package/dist/wirings/workflow/index.d.ts +7 -2
- package/dist/wirings/workflow/index.js +5 -1
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
- package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +28 -147
- package/dist/wirings/workflow/pikku-workflow-service.js +71 -493
- package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
- package/dist/wirings/workflow/workflow-approval.js +114 -0
- package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
- package/dist/wirings/workflow/workflow-constants.js +35 -0
- package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
- package/dist/wirings/workflow/workflow-errors.js +112 -0
- package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
- package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
- package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
- package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
- package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
- package/dist/wirings/workflow/workflow-recovery.js +101 -0
- package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
- package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
- package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
- package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
- package/dist/wirings/workflow/workflow-suspend.js +33 -0
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
- package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
- package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
- package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
- package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
- package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
- package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
- package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
- package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
- package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
- package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
- package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
- package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
- package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
- package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
- package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
- package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
- package/knowledge/decisions/internals/index.md +38 -26
- package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
- package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
- package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
- package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
- package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
- package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
- package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
- package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
- package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
- package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
- package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
- package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
- package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
- package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
- package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
- package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
- package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
- package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
- package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
- package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
- package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
- package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
- package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
- package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
- package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
- package/knowledge/decisions/security/index.md +7 -0
- package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
- package/knowledge/questions/index.md +2 -1
- package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
- package/package.json +13 -3
- package/scripts/generate-api-report.d.mts +1 -0
- package/scripts/generate-api-report.mjs +89 -0
- package/scripts/generate-api-report.mts +218 -0
- package/src/api-report.test.ts +69 -0
- package/src/column-form.ts +1 -4
- package/src/crypto-utils.test.ts +15 -2
- package/src/dev/hot-reload.ts +2 -7
- package/src/ecosystem.ts +40 -0
- package/src/errors/error-handler.ts +5 -3
- package/src/errors/error.test.ts +4 -1
- package/src/function/function-runner.test.ts +1 -1
- package/src/function/function-runner.ts +28 -39
- package/src/function/functions.types.ts +15 -9
- package/src/handle-error.ts +1 -1
- package/src/index.ts +3 -22
- package/src/middleware/auth-cookie.test.ts +50 -0
- package/src/middleware/auth-cookie.ts +38 -2
- package/src/middleware/cors.ts +2 -1
- package/src/middleware/index.ts +1 -1
- package/src/middleware/remote-auth.test.ts +43 -0
- package/src/middleware/remote-auth.ts +17 -3
- package/src/middleware-runner.ts +4 -2
- package/src/no-any-casts.test.ts +57 -0
- package/src/permissions.test.ts +3 -1
- package/src/permissions.ts +22 -24
- package/src/pikku-state.ts +17 -6
- package/src/public-surface.json +578 -0
- package/src/public-surface.json.README +25 -0
- package/src/public-surface.test.ts +105 -0
- package/src/removed-legacy-exports.test.ts +62 -0
- package/src/schema.test.ts +78 -0
- package/src/schema.ts +36 -1
- package/src/services/ai-run-state-service.ts +7 -1
- package/src/services/audit-service.ts +2 -2
- package/src/services/in-memory-ai-run-state-service.ts +3 -2
- package/src/services/in-memory-workflow-service.ts +2 -0
- package/src/services/index.ts +1 -4
- package/src/services/local-content-request-handler.test.ts +43 -5
- package/src/services/local-content-request-handler.ts +103 -74
- package/src/services/local-content.ts +15 -1
- package/src/services/local-email-service.ts +5 -1
- package/src/services/system-role-guard.test.ts +4 -1
- package/src/services/workflow-service.ts +9 -2
- package/src/side-effects-are-declared.test.ts +84 -0
- package/src/source-files-stay-composable.test.ts +41 -0
- package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
- package/src/testing/service-tests/channel-store-tests.ts +98 -0
- package/src/testing/service-tests/credential-service-tests.ts +143 -0
- package/src/testing/service-tests/deployment-service-tests.ts +33 -0
- package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
- package/src/testing/service-tests/secret-service-tests.ts +105 -0
- package/src/testing/service-tests/session-store-tests.ts +62 -0
- package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
- package/src/testing/service-tests/workflow-service-tests.ts +308 -0
- package/src/testing/service-tests.ts +31 -1111
- package/src/types/core.types.ts +15 -1
- package/src/types/state.types.ts +1 -1
- package/src/wirings/actor-flow/run-conversation.ts +1 -6
- package/src/wirings/ai-agent/agent-rpc.ts +120 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
- package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
- package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
- package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
- package/src/wirings/ai-agent/voice-input.ts +1 -6
- package/src/wirings/ai-agent/voice-output.ts +2 -12
- package/src/wirings/channel/channel-common.ts +5 -2
- package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
- package/src/wirings/channel/channel-handler.ts +7 -7
- package/src/wirings/channel/channel-rpc-service.ts +0 -14
- package/src/wirings/channel/channel-rpc.test.ts +25 -4
- package/src/wirings/channel/channel-rpc.types.ts +14 -0
- package/src/wirings/channel/channel-runner.ts +33 -20
- package/src/wirings/channel/channel.types.ts +2 -0
- package/src/wirings/channel/local/local-channel-runner.ts +1 -1
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
- package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +7 -4
- package/src/wirings/cli/cli.types.ts +0 -39
- package/src/wirings/cli/command-parser.test.ts +19 -0
- package/src/wirings/cli/command-parser.ts +17 -1
- package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +27 -19
- package/src/wirings/http/http-routes.ts +4 -1
- package/src/wirings/http/http-runner.ts +11 -26
- package/src/wirings/http/http.types.ts +0 -15
- package/src/wirings/http/index.ts +1 -7
- package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
- package/src/wirings/http/web-request.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +2 -16
- package/src/wirings/persona/persona-environments.test.ts +14 -3
- package/src/wirings/persona/persona.test.ts +13 -3
- package/src/wirings/persona/validate-personas.ts +5 -1
- package/src/wirings/queue/queue-runner.ts +1 -1
- package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
- package/src/wirings/rpc/rpc-runner.ts +58 -136
- package/src/wirings/rpc/rpc-types.ts +7 -0
- package/src/wirings/rpc/wire-addon.ts +3 -1
- package/src/wirings/secret/validate-secret-definitions.test.ts +22 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -2
- package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
- package/src/wirings/trigger/trigger-runner.ts +7 -5
- package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
- package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
- package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
- package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
- package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
- package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
- package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
- package/src/wirings/workflow/feature.ts +7 -4
- package/src/wirings/workflow/graph/graph-node.ts +1 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
- package/src/wirings/workflow/graph/graph-runner.ts +13 -16
- package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
- package/src/wirings/workflow/index.ts +12 -4
- package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
- package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +190 -743
- package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
- package/src/wirings/workflow/workflow-approval.ts +185 -0
- package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
- package/src/wirings/workflow/workflow-constants.ts +47 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-errors.ts +128 -0
- package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
- package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
- package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
- package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
- package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
- package/src/wirings/workflow/workflow-recovery.ts +162 -0
- package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
- package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
- package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
- package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
- package/src/wirings/workflow/workflow-suspend.ts +61 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/src/wirings-stay-decoupled.test.ts +123 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/internal.ts +0 -10
package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The middleware resolution cache is deliberately unbounded
|
|
4
|
+
description: Its keyspace is the set of registered wires, not request traffic, and middleware is dynamic — so eviction would buy nothing and cost the dedupe guarantee
|
|
5
|
+
tags: core, middleware
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The middleware resolution cache is deliberately unbounded
|
|
9
|
+
|
|
10
|
+
`middlewareCache` in `packages/core/src/middleware-runner.ts` is a plain
|
|
11
|
+
`Record<PikkuWiringTypes, Record<string, readonly CorePikkuMiddleware[]>>` with
|
|
12
|
+
no size limit and no eviction. That reads like an unbounded-growth bug and is
|
|
13
|
+
not one.
|
|
14
|
+
|
|
15
|
+
**The keyspace is finite by construction.** The cache is keyed by wire type and
|
|
16
|
+
wire id, and wire ids come from registration, not from traffic — an HTTP route
|
|
17
|
+
pattern, a channel name, a queue name. A million requests to the same route
|
|
18
|
+
produce one entry. Nothing a caller sends creates a key: a channel message key
|
|
19
|
+
is `${channel}:${routingProperty}:${routerValue}`, and `routerValue` is only
|
|
20
|
+
reached after matching `routes[routerValue]`, so an unregistered value never
|
|
21
|
+
gets that far.
|
|
22
|
+
|
|
23
|
+
**Middleware is dynamic, and the cache is invalidated rather than aged out.**
|
|
24
|
+
`addGlobalMiddleware` and `addTagMiddleware` can run after startup, and dev
|
|
25
|
+
hot-reload rewires wholesale — so correctness comes from `clearMiddlewareCache()`
|
|
26
|
+
at the points where the middleware set actually changes, not from entries
|
|
27
|
+
expiring. An LRU would evict entries that are still current while doing nothing
|
|
28
|
+
about entries that are stale.
|
|
29
|
+
|
|
30
|
+
**Eviction would also weaken the chain itself.** `combineMiddleware` returns a
|
|
31
|
+
`freezeDedupe`d array, and dedupe is by function identity — a middleware
|
|
32
|
+
reachable through both a tag group and a direct wire registration runs exactly
|
|
33
|
+
once. Callers hold that array. Recomputing it under memory pressure hands out a
|
|
34
|
+
second array for the same wire, which is churn at best and, for anything
|
|
35
|
+
comparing chains by identity, a silent behaviour change.
|
|
36
|
+
|
|
37
|
+
**What this rules out:** adding a `MIDDLEWARE_CACHE_MAX` with LRU eviction as a
|
|
38
|
+
denial-of-service mitigation. There is no traffic-driven growth to mitigate. If
|
|
39
|
+
a future change ever keys this cache by something a caller controls, that is the
|
|
40
|
+
bug — bound the key, not the cache.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The per-invocation rpc view is a class, because an object literal with a getter is slow to build
|
|
4
|
+
description: An accessor declared on an object literal is defined per instance, which drops the literal off V8's fast construction path — measured at 1.15µs against 0.47µs, on every request
|
|
5
|
+
tags: core, rpc, performance
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The per-invocation rpc view is a class
|
|
9
|
+
|
|
10
|
+
`getContextRPCService` builds `wire.rpc` once per invocation. It used to return
|
|
11
|
+
an object literal, and that literal declared `get agent()` so the agent facade
|
|
12
|
+
stayed lazy — most requests never touch it, and reading it builds five closures.
|
|
13
|
+
|
|
14
|
+
The laziness is right. Declaring the accessor *on the literal* was not: a
|
|
15
|
+
literal containing an accessor needs a real property descriptor per instance,
|
|
16
|
+
which takes it off V8's fast object-literal construction path and slows the
|
|
17
|
+
whole object, not just the accessor.
|
|
18
|
+
|
|
19
|
+
Measured with `benchmarks/bench-profile-granular.ts`, three runs each:
|
|
20
|
+
|
|
21
|
+
| shape | per call |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| object literal with `get agent()` | 1.106 / 1.433 / 1.147 µs |
|
|
24
|
+
| class with `agent` on the prototype | 0.523 / 0.461 / 0.525 µs |
|
|
25
|
+
|
|
26
|
+
Roughly 2.4×, well outside the run-to-run variance, on a path every request
|
|
27
|
+
takes. On the same machine a full `fetchData` measures 12–19µs, so this was on
|
|
28
|
+
the order of a tenth of a request spent constructing one object.
|
|
29
|
+
|
|
30
|
+
The obvious alternative is worse. Making `agent` an eager property removes the
|
|
31
|
+
accessor but builds those five closures unconditionally, and measured *slower*
|
|
32
|
+
than the original at 1.967µs. A prototype accessor is the only shape that keeps
|
|
33
|
+
the laziness and the fast construction path.
|
|
34
|
+
|
|
35
|
+
**What this rules out:** "simplifying" `ContextRPCView` back to an object
|
|
36
|
+
literal. It reads as the plainer option and costs double. It also rules out
|
|
37
|
+
trusting a synthetic microbenchmark here — a standalone benchmark of these
|
|
38
|
+
shapes pointed at the eager version, because its stand-in for the agent getter
|
|
39
|
+
was too cheap to represent the real one. The numbers above come from the
|
|
40
|
+
profiler running the real code.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The persona runtime is exported from @pikku/core/persona, never from services
|
|
4
|
+
description: Those values reach the actor-flow and agent runners, which no production server runs — and an unbundled deploy loads whatever the import graph names
|
|
5
|
+
tags: core, services
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The persona runtime lives on `@pikku/core/persona`
|
|
9
|
+
|
|
10
|
+
`HttpPersona`, `createHttpPersonas`, `readScenarioHttpResponse` and
|
|
11
|
+
`postScenarioJson` are exported from `@pikku/core/persona`, not from
|
|
12
|
+
`@pikku/core/services`. Their *types* are re-exported freely — TypeScript erases
|
|
13
|
+
those and they cost a bundle nothing.
|
|
14
|
+
|
|
15
|
+
The values are different. They reach `http-personas`, which reaches the
|
|
16
|
+
actor-flow conversation runner, which reaches the agent runner: an entire
|
|
17
|
+
scenario and virtual-user runtime that no production server executes. An
|
|
18
|
+
unbundled Node or Lambda deploy does no tree-shaking — it loads whatever the
|
|
19
|
+
import graph names — so exporting one value from the services barrel pulls all
|
|
20
|
+
of it into every application that imports a service.
|
|
21
|
+
|
|
22
|
+
This is enforced, not merely intended: `production-barrels-stay-lean.test.ts`
|
|
23
|
+
walks the value-import graph from `services/index.ts` and
|
|
24
|
+
`wirings/workflow/index.ts` and fails if any of those modules is reachable.
|
|
25
|
+
|
|
26
|
+
**What this rules out:** re-exporting a persona value from the services barrel
|
|
27
|
+
for convenience, and "just one small helper" — the graph is transitive, and one
|
|
28
|
+
value is enough to pull the whole runtime.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A voice turn's transcript is sent before the run starts, on the raw channel
|
|
4
|
+
description: The client does not know what it said, and an answer starts streaming within a few hundred milliseconds — a question arriving after its answer reads as the wrong question
|
|
5
|
+
tags: core, ai-agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A voice turn's transcript is sent ahead of the run
|
|
9
|
+
|
|
10
|
+
A voice client sends audio, so it does not know what it said. Until the
|
|
11
|
+
transcript reaches it, its own message renders as a blank bubble.
|
|
12
|
+
|
|
13
|
+
The event is sent *before* the run rather than alongside it because the answer
|
|
14
|
+
begins streaming within a few hundred milliseconds. Sent concurrently, the
|
|
15
|
+
transcript routinely lands after the first tokens of its own answer — and a
|
|
16
|
+
question that appears beneath its answer reads as a question about something
|
|
17
|
+
else entirely.
|
|
18
|
+
|
|
19
|
+
It goes on the raw channel rather than through the stream middleware because it
|
|
20
|
+
is not part of the reply: it is what the user said, and the stream hooks are
|
|
21
|
+
built to transform what the agent says.
|
|
22
|
+
|
|
23
|
+
**What this rules out:** folding the transcript into the run's event stream for
|
|
24
|
+
uniformity. Ordering is the whole point, and the stream is where ordering is
|
|
25
|
+
least under this code's control.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The catalogue is the only gate on what a virtual user may call
|
|
4
|
+
description: Narrowing happens once at derivation, so there is no second check in the run loop that could drift out of step with it
|
|
5
|
+
tags: core, virtual-user
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The catalogue is the only gate on what a virtual user may call
|
|
9
|
+
|
|
10
|
+
What a virtual user may invoke is decided once, when the catalogue is narrowed
|
|
11
|
+
for its disposition: a read-only disposition is never offered a mutation, and an
|
|
12
|
+
approval-gated endpoint is never offered at all.
|
|
13
|
+
|
|
14
|
+
The run loop then indexes into that catalogue and calls what it finds. There is
|
|
15
|
+
deliberately no second permission check at call time, because a second check is
|
|
16
|
+
a second thing to keep correct — and the failure mode of the two disagreeing is
|
|
17
|
+
that one of them silently stops mattering.
|
|
18
|
+
|
|
19
|
+
**What this rules out:** adding a "defence in depth" guard in the run loop. It
|
|
20
|
+
would not be defence in depth; it would be a duplicate of the derivation rules,
|
|
21
|
+
maintained separately, and the one that runs first wins.
|
package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The worker disposition is the only one not testing anything
|
|
4
|
+
description: It is doing the job, so it abandons rarely, runs cool, and is told to stop and say so rather than guess
|
|
5
|
+
tags: core, virtual-user
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The worker disposition is the only one not testing anything
|
|
9
|
+
|
|
10
|
+
Every other disposition exists to probe the product. The worker exists to *use*
|
|
11
|
+
it, which changes every parameter: it abandons rarely, runs at a low temperature,
|
|
12
|
+
and is instructed to stop and report rather than guess its way past an obstacle.
|
|
13
|
+
|
|
14
|
+
The failure mode being guarded against is different too. For the probing
|
|
15
|
+
dispositions the risk is a missed bug. For this one it is a wrong action nobody
|
|
16
|
+
asked for — a real mutation performed because the model improvised when it
|
|
17
|
+
should have stopped.
|
|
18
|
+
|
|
19
|
+
**What this rules out:** tuning this disposition toward the others for
|
|
20
|
+
consistency, in particular raising its temperature or its tolerance for
|
|
21
|
+
guessing. Those settings are what make the others useful and what would make this
|
|
22
|
+
one dangerous.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Thread history records what the model was asked, which for a spoken turn is the transcript
|
|
4
|
+
description: The wire carried a base64 audio blob; persisting it writes megabytes of unreadable data and loses the only readable record of the turn
|
|
5
|
+
tags: core, ai-agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Thread history records the transcript, not the audio that arrived
|
|
9
|
+
|
|
10
|
+
What goes into thread history is what the model was actually asked. For a typed
|
|
11
|
+
turn that is what arrived over the wire. For a spoken turn it is not: the wire
|
|
12
|
+
carried a base64 audio blob, and `voiceInput` replaced it with a transcript
|
|
13
|
+
before the model ever saw it.
|
|
14
|
+
|
|
15
|
+
Persisting the blob would write megabytes of unreadable data into the history
|
|
16
|
+
*and* discard the only readable record of what was said — the worst of both.
|
|
17
|
+
|
|
18
|
+
Both the streaming and non-streaming paths do this, and both check identity
|
|
19
|
+
rather than assuming: a middleware is free to rewrite the message list into
|
|
20
|
+
something with no relation to this turn, and in that case there is no transcript
|
|
21
|
+
to substitute and the list is persisted as-is.
|
|
22
|
+
|
|
23
|
+
**What this rules out:** persisting the inbound message verbatim on the grounds
|
|
24
|
+
that it is the ground truth. For voice it is the least useful representation
|
|
25
|
+
available, and the substitution is exactly what makes the history readable.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A scenario's step order for a virtual user is insertion order, not a graph traversal
|
|
4
|
+
description: 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
|
|
5
|
+
tags: core, virtual-user
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Step order comes from insertion order, not a graph traversal
|
|
9
|
+
|
|
10
|
+
When a scenario becomes catalogue entries, its steps are read in the order the
|
|
11
|
+
nodes appear — which is the order the CLI wrote them, which is the order the
|
|
12
|
+
scenario declares them.
|
|
13
|
+
|
|
14
|
+
Following `next` edges instead would produce the same ordering in the ordinary
|
|
15
|
+
case, and in the branching case it would force a decision nobody needs: which
|
|
16
|
+
arm of a branch to present to a reader who will never take one. The traversal
|
|
17
|
+
costs work to reproduce a fact that already holds.
|
|
18
|
+
|
|
19
|
+
**What this rules out:** "correcting" this to a topological walk because a graph
|
|
20
|
+
is present. The graph describes execution; the catalogue describes what the
|
|
21
|
+
scenario *says*, and those are read in declaration order.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Only an explicit `false` silences a spoken reply
|
|
4
|
+
description: 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
|
|
5
|
+
tags: core, ai-agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Only an explicit `false` silences a spoken reply
|
|
9
|
+
|
|
10
|
+
`voiceOutput` speaks a reply unless the turn is explicitly marked as not having
|
|
11
|
+
arrived by voice. The distinction is between three states, not two:
|
|
12
|
+
|
|
13
|
+
- `true` — `voiceInput` handled this turn and it carried speech.
|
|
14
|
+
- `false` — `voiceInput` handled this turn and a real user really typed it.
|
|
15
|
+
- absent — no voice input is wired at all, so nothing reported either way.
|
|
16
|
+
|
|
17
|
+
Treating absent as `false` would silence every caller who wired `voiceOutput`
|
|
18
|
+
without `voiceInput`, changing behaviour that worked before the option existed.
|
|
19
|
+
Treating absent as `true` would speak replies to typists on any stack where the
|
|
20
|
+
flag never gets set.
|
|
21
|
+
|
|
22
|
+
**What this rules out:** normalising the flag to a boolean at any point between
|
|
23
|
+
`voiceInput` and `voiceOutput` — that collapses absent into one of the other two
|
|
24
|
+
and picks a wrong answer for somebody.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Wiring registries erase the generics their wire* functions capture
|
|
4
|
+
description: A registry is typed at unknown so every wiring shares one map; storing a generic config in it needs an assertion, because function parameters are contravariant
|
|
5
|
+
tags: core
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Wiring registries erase the generics their `wire*` functions capture
|
|
9
|
+
|
|
10
|
+
Every `wire*` entry point — `wireTrigger`, `wireTriggerSource`, `wireChannel`,
|
|
11
|
+
`wireQueueWorker`, and the rest — is generic in the shape a caller declares, so
|
|
12
|
+
the caller gets a checked `func`. The registry each one writes to is not:
|
|
13
|
+
`pikkuState(null, 'trigger', 'triggers')` is a
|
|
14
|
+
`Map<string, CoreTriggerSource>` where `CoreTriggerSource` fixes input and
|
|
15
|
+
output at `unknown`, because one map has to hold every trigger in the app.
|
|
16
|
+
|
|
17
|
+
Handing `CoreTriggerSource<TInput, TOutput>` to a slot typed
|
|
18
|
+
`CoreTriggerSource<unknown, unknown>` is not an upcast. `func` takes its input
|
|
19
|
+
as a *parameter*, and parameters are contravariant: a function that accepts
|
|
20
|
+
`TInput` cannot stand in for one that accepts `unknown`, since `unknown` admits
|
|
21
|
+
values `TInput` does not. TypeScript is right to reject it, and no variance
|
|
22
|
+
annotation makes it go away — the registry genuinely holds functions whose input
|
|
23
|
+
types it cannot name.
|
|
24
|
+
|
|
25
|
+
The runtime is nonetheless sound, because the only thing that ever invokes a
|
|
26
|
+
registered `func` is the function runner, which validates the incoming data
|
|
27
|
+
against that function's own generated schema before the call. The type the
|
|
28
|
+
registry lost is re-established at the call boundary by the schema, not by the
|
|
29
|
+
type system.
|
|
30
|
+
|
|
31
|
+
**What this rules out:** deleting these assertions as if they were noise — the
|
|
32
|
+
code does not compile without them. It equally rules out `as any` in their
|
|
33
|
+
place: `as any` discards the target type too, so a genuinely wrong config would
|
|
34
|
+
also slip through. Assert to the registry's own element type and let everything
|
|
35
|
+
except the erased generic stay checked.
|
|
@@ -13,8 +13,10 @@ Its `write` discards the event — but before it does, it warns once, and it loo
|
|
|
13
13
|
for a logger in two places: the wire's own, then the singleton passed to the
|
|
14
14
|
constructor.
|
|
15
15
|
|
|
16
|
-
The fallback is the point.
|
|
17
|
-
|
|
16
|
+
The fallback is the point. `wire.logger` is an optional hook for a host that
|
|
17
|
+
wants invocation-scoped logging — core never sets it, so on every path core
|
|
18
|
+
itself drives, the singleton is the only source there is. With only
|
|
19
|
+
`this.wire.logger` the warning would be dropped every time.
|
|
18
20
|
An audit call that silently does nothing is the worst available outcome: the
|
|
19
21
|
function believes it is producing an audit trail, the trail does not exist, and
|
|
20
22
|
nothing anywhere says so. The warning names the function and the fix, and fires
|
package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A graph run starts at an entry node the graph declared
|
|
4
|
+
description: startNode may only name a node in meta.entryNodeIds, and no generated route offers it, because otherwise a caller picks which half of the graph to skip
|
|
5
|
+
tags: workflow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A graph run starts at an entry node the graph declared
|
|
9
|
+
|
|
10
|
+
`runWorkflowGraph` in
|
|
11
|
+
`packages/core/src/wirings/workflow/graph/graph-runner.ts` takes an optional
|
|
12
|
+
`startNode`. It used to accept any node id the graph contained, validated only
|
|
13
|
+
by `validateGraphReferences` (does the node exist) and `areDependenciesSatisfied`
|
|
14
|
+
(does its input reference another node). A node whose input is a literal or comes
|
|
15
|
+
from the trigger passes both — including a node that is only ever reached after a
|
|
16
|
+
validation, payment or approval node. Choosing it as the start does not skip the
|
|
17
|
+
gate so much as never reach it.
|
|
18
|
+
|
|
19
|
+
`startNode` is now checked against `meta.entryNodeIds`, the set the graph itself
|
|
20
|
+
declared, and the generated `POST /workflow/:workflowName/graph/:nodeId` route
|
|
21
|
+
that offered it to HTTP callers is gone.
|
|
22
|
+
|
|
23
|
+
The parameter stays, because it has a real internal user: `PikkuTriggerService`
|
|
24
|
+
(`packages/core/src/wirings/trigger/pikku-trigger-service.ts`) passes a target's
|
|
25
|
+
`startNode` when a trigger fires. That caller names a node the graph declared, so
|
|
26
|
+
the restriction costs it nothing.
|
|
27
|
+
|
|
28
|
+
**What this rules out:** re-exposing entry-node choice on a generated route, and
|
|
29
|
+
"validating" a `startNode` by existence or by dependency satisfaction — neither
|
|
30
|
+
says the graph meant that node to be an entry point. It does not rule out a graph
|
|
31
|
+
declaring several entry nodes; that is exactly how a graph says which starts are
|
|
32
|
+
legitimate. If a graph needs a start that is not an entry node, the answer is to
|
|
33
|
+
declare it as one, not to widen the check.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A permission gets a wire it cannot reply on
|
|
4
|
+
description: The permission wire is typed with Out = never so a permission cannot send on the channel; that narrowing is not a subtype, so the call site asserts
|
|
5
|
+
tags: core, permissions
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A permission gets a wire it cannot reply on
|
|
9
|
+
|
|
10
|
+
`CorePikkuPermission` in `packages/core/src/function/functions.types.ts` types
|
|
11
|
+
its wire as `PikkuWire<In, never, false, any, PikkuRPC, never, never>`. The
|
|
12
|
+
second parameter is `Out`, and `never` there is deliberate: it makes
|
|
13
|
+
`wire.channel` a `PikkuChannel<unknown, never, …>`, whose `send` accepts
|
|
14
|
+
nothing. A permission is a gate — it answers `true` or `false` — and must not
|
|
15
|
+
be able to write a reply to the caller it is deciding about. A permission that
|
|
16
|
+
could `send` would be able to leak the very data the gate exists to withhold,
|
|
17
|
+
and it would do so before the function it guards has run.
|
|
18
|
+
|
|
19
|
+
`runPikkuFunc` holds an ordinary `PikkuWire`, whose default `Out` is `unknown`.
|
|
20
|
+
`unknown` is not assignable to `never`, and `send` is contravariant in its
|
|
21
|
+
argument, so the narrowing cannot be expressed as a subtype relation — the call
|
|
22
|
+
into `runPermissions` asserts to the exact permission-wire type rather than
|
|
23
|
+
relying on assignability.
|
|
24
|
+
|
|
25
|
+
**What this rules out:** widening `Out` on the permission wire to `unknown` to
|
|
26
|
+
delete the assertion. That silently hands every permission function a working
|
|
27
|
+
`channel.send`. It equally rules out replacing the assertion with `as any`,
|
|
28
|
+
which erases the target type and hides the fact that a specific, intentional
|
|
29
|
+
narrowing is happening.
|
|
30
|
+
|
|
31
|
+
The same reasoning explains the `never` in the fifth and sixth positions
|
|
32
|
+
(`MCPTools`, `IsChannel`): a permission is not an MCP tool host and does not own
|
|
33
|
+
the channel lifecycle.
|
package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A step runs the function the workflow dispatched it with
|
|
4
|
+
description: StepState records the step's function name so the worker can reject a queue message naming a different one, because the step executes under the run owner's identity
|
|
5
|
+
tags: workflow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A step runs the function the workflow dispatched it with
|
|
9
|
+
|
|
10
|
+
`pikkuWorkflowStepWorker` (`workflow-queue-workers.ts`) takes `rpcName` straight
|
|
11
|
+
off the queue message and hands it to `executeWorkflowStep`. That call runs as
|
|
12
|
+
the run's owner — `invokeStepRpc` copies `run.wire.pikkuUserId` onto the wire —
|
|
13
|
+
and `rpcWithWire` does not apply the `expose` gate that the public `/rpc` route
|
|
14
|
+
applies. So the message decided both *what* ran and *as whom*.
|
|
15
|
+
|
|
16
|
+
The claim in `executeWorkflowStepInner` read the step's status and nothing else;
|
|
17
|
+
there was no stored function name to compare against. `StepState` now carries
|
|
18
|
+
`rpcName`, written by `insertStepState` (which already received it) and returned
|
|
19
|
+
by every backend, and the claim rejects a message naming anything else with
|
|
20
|
+
`WorkflowStepFunctionMismatchError` — before any status is mutated, so a forged
|
|
21
|
+
message leaves the run untouched. The graph path takes the same value from
|
|
22
|
+
`nodes[nodeId].rpcName` rather than from the message.
|
|
23
|
+
|
|
24
|
+
`rpcName: undefined` means a store that never recorded one and cannot be
|
|
25
|
+
compared; `null` is a step with no function of its own. Only a recorded value is
|
|
26
|
+
checked.
|
|
27
|
+
|
|
28
|
+
**Reachability, stated plainly:** the step-worker queue is not reachable from the
|
|
29
|
+
public `/rpc/:rpcName` route — the worker is registered without `expose`. The
|
|
30
|
+
exposure this closes is write access to the queue backend, and any in-process
|
|
31
|
+
caller reaching `executeWorkflowStep` directly.
|
|
32
|
+
|
|
33
|
+
**What this rules out:** trusting a queue payload to name the function it
|
|
34
|
+
executes, here or in any future worker, and dropping `rpcName` from `StepState`
|
|
35
|
+
as redundant with the step data — the comparison is the only thing standing
|
|
36
|
+
between queue-write access and running any registered function as the run's
|
|
37
|
+
owner.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user is never offered a scenario, platform or addon step
|
|
4
|
+
description: Being able to invoke "the webhook arrives" lets the user manufacture the outcome it exists to discover, which invalidates every finding downstream
|
|
5
|
+
tags: core, virtual-user
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user is never offered a scenario, platform or addon step
|
|
9
|
+
|
|
10
|
+
`deriveVirtualUserCatalogue` excludes scenario bodies and their steps, and
|
|
11
|
+
platform and addon steps, from what a virtual user may call.
|
|
12
|
+
|
|
13
|
+
For scenario bodies the argument is only efficiency: they are held out of every
|
|
14
|
+
deployed unit and are not network-callable, so offering one wastes a turn on a
|
|
15
|
+
404.
|
|
16
|
+
|
|
17
|
+
For a platform or addon step the argument is the oracle itself. A virtual user's
|
|
18
|
+
findings are worth something *because* it cannot manufacture the outcomes it is
|
|
19
|
+
meant to discover. A user that could invoke "Stripe's webhook arrives" forges its
|
|
20
|
+
own payment success, and every finding downstream of that forgery is worthless —
|
|
21
|
+
not merely unreliable, but actively misleading, because it looks like evidence.
|
|
22
|
+
|
|
23
|
+
This is the same class of argument as `allowApprovalRequired` defaulting to
|
|
24
|
+
false, and it is enforced here at derivation rather than left to convention.
|
|
25
|
+
|
|
26
|
+
**What this rules out:** exposing platform steps behind a flag "for
|
|
27
|
+
convenience", or filtering them later in the run loop where a caller could skip
|
|
28
|
+
the filter.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A workflow run is read and approved by its owner
|
|
4
|
+
description: A run started through a session records that user and only that user may read it or answer its approval gates; a run with no recorded owner has no ownership to enforce
|
|
5
|
+
tags: workflow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A workflow run is read and approved by its owner
|
|
9
|
+
|
|
10
|
+
`WorkflowRunWire.pikkuUserId` has always been recorded on every run started
|
|
11
|
+
through a session — `RPCService.startWorkflow` copies it off the wire — and
|
|
12
|
+
nothing read it back. Run ids were the only secret protecting both the status
|
|
13
|
+
routes (which stream `output` and `error`) and `approveStep`, which took no
|
|
14
|
+
session at all and rejected only an already-resolved gate.
|
|
15
|
+
|
|
16
|
+
`assertWorkflowRunOwner`
|
|
17
|
+
(`packages/core/src/wirings/workflow/workflow-run-ownership.ts`) is the one check
|
|
18
|
+
both paths share. `approveStep` takes the caller's session and asserts it, and
|
|
19
|
+
the generated status routes assert it against the run they were already reading.
|
|
20
|
+
|
|
21
|
+
**A run with no recorded owner is not gated.** Triggers, schedulers and routes
|
|
22
|
+
wired without auth start runs with no `pikkuUserId`; there is nobody to compare a
|
|
23
|
+
caller against, and inventing one would reject the framework's own callers rather
|
|
24
|
+
than secure anything. Gate those at the entrypoint with `auth` or `permissions`.
|
|
25
|
+
|
|
26
|
+
This is ownership, not an approver model. It answers "is this your run", not "are
|
|
27
|
+
you entitled to approve this particular gate" — `WorkflowApprovalOptions` still
|
|
28
|
+
carries no approver, role or permission, and a second approver on someone else's
|
|
29
|
+
run is still a matter for the route's own `permissions`.
|
|
30
|
+
|
|
31
|
+
**What this rules out:** treating a run id as a capability, and adding a new run
|
|
32
|
+
read path that does not take a session. It does not rule out a richer approver
|
|
33
|
+
model on `WorkflowApprovalOptions` later; that would narrow this gate, never
|
|
34
|
+
replace it.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An agent approval is claimed before the tool runs
|
|
4
|
+
description: resolveApproval is a compare-and-swap returning whether this caller won, because the read that precedes it is not a claim and ten concurrent approvals would otherwise mean ten refunds
|
|
5
|
+
tags: ai-agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An agent approval is claimed before the tool runs
|
|
9
|
+
|
|
10
|
+
Resuming a suspended agent run read the run, checked `status === 'suspended'`,
|
|
11
|
+
snapshotted `pendingApprovals`, called `resolveApproval`, executed the tool, and
|
|
12
|
+
only then wrote `status: 'running'`. Nothing spanned that sequence — no
|
|
13
|
+
transaction, no row lock, no conditional update — so concurrent approvals of the
|
|
14
|
+
same tool call all observed `suspended`, all snapshotted the same list, and all
|
|
15
|
+
reached `execute`. `resolveApproval` returned `void`, so a loser could not even
|
|
16
|
+
tell.
|
|
17
|
+
|
|
18
|
+
`resolveApproval` is now the claim, and returns whether *this* caller made it.
|
|
19
|
+
The stores implement it as a compare-and-swap: Kysely updates the run row only
|
|
20
|
+
while `status = 'suspended'` and `pendingApprovals` still equals the list it
|
|
21
|
+
read; the tool-call stores move the row off `approvalStatus = 'pending'` and
|
|
22
|
+
count the rows they changed. Both resume paths run the tool only for the ids they
|
|
23
|
+
claimed, and a caller that claimed nothing gets an error rather than a silent
|
|
24
|
+
re-run.
|
|
25
|
+
|
|
26
|
+
The claim is per tool call, not per run, so concurrent approvals of *different*
|
|
27
|
+
tool calls on one run all proceed — which is the case that made a run-level
|
|
28
|
+
`claimSuspendedRun` the worse fit.
|
|
29
|
+
|
|
30
|
+
**What this rules out:** treating `getRun` as a claim, adding a resume path that
|
|
31
|
+
executes a tool without a `true` from `resolveApproval`, and implementing
|
|
32
|
+
`resolveApproval` as a read-modify-write in any new store — the return value is
|
|
33
|
+
a promise about atomicity, not a convenience.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An upload is counted as it arrives, not buffered and then measured
|
|
4
|
+
description: Reading the whole body before checking its size hands an unauthenticated caller a way to spend the server's memory
|
|
5
|
+
tags: core, content
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An upload is counted as it arrives, not buffered and then measured
|
|
9
|
+
|
|
10
|
+
The local content request handler accumulates the request body chunk by chunk,
|
|
11
|
+
tracking the running total, and abandons the read the moment it crosses the
|
|
12
|
+
limit. An oversized upload therefore costs the limit, not its own size.
|
|
13
|
+
|
|
14
|
+
The obvious alternative — `await request.arrayBuffer()` and then check
|
|
15
|
+
`byteLength` — has to hold the entire body in memory before it can decide the
|
|
16
|
+
body is too large. For an endpoint reachable before authentication that is a
|
|
17
|
+
way to spend the server's memory for the price of one request, and no size limit
|
|
18
|
+
configured anywhere prevents it.
|
|
19
|
+
|
|
20
|
+
`readRequestBody` in the node HTTP server aborts on the same terms, for the same
|
|
21
|
+
reason.
|
|
22
|
+
|
|
23
|
+
**What this rules out:** replacing the streaming accumulation with a single
|
|
24
|
+
buffered read because the limit check "still happens".
|
|
@@ -10,6 +10,11 @@ A rule about who may do what, and which way it fails when it is unsure.
|
|
|
10
10
|
|
|
11
11
|
<!-- pikku:knowledge-index -->
|
|
12
12
|
- [A dropped audit write is always logged](a-dropped-audit-write-is-always-logged.md) — The no-op audit service falls back to the singleton logger when the wire carries none, so an unconfigured audit call is never silent
|
|
13
|
+
- [A graph run starts at an entry node the graph declared](a-graph-run-starts-at-an-entry-node-the-graph-declared.md) — startNode may only name a node in meta.entryNodeIds, and no generated route offers it, because otherwise a caller picks which half of the graph to skip
|
|
14
|
+
- [A permission gets a wire it cannot reply on](a-permission-gets-a-wire-it-cannot-reply-on.md) — The permission wire is typed with Out = never so a permission cannot send on the channel; that narrowing is not a subtype, so the call site asserts
|
|
15
|
+
- [A step runs the function the workflow dispatched it with](a-step-runs-the-function-the-workflow-dispatched-it-with.md) — StepState records the step's function name so the worker can reject a queue message naming a different one, because the step executes under the run owner's identity
|
|
16
|
+
- [A virtual user is never offered a scenario, platform or addon step](a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md) — Being able to invoke "the webhook arrives" lets the user manufacture the outcome it exists to discover, which invalidates every finding downstream
|
|
17
|
+
- [A workflow run is read and approved by its owner](a-workflow-run-is-read-and-approved-by-its-owner.md) — A run started through a session records that user and only that user may read it or answer its approval gates; a run with no recorded owner has no ownership to enforce
|
|
13
18
|
- [An actor's missing approval decision defaults to denied](actor-flow-missing-approval-decisions-default-to-denied.md) — Every pending tool call gets an explicit decision; an id the persona LLM omitted is denied, so a dropped field can never read as consent
|
|
14
19
|
- [Actor sign-in is proven by Set-Cookie, not a non-empty jar](actor-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md) — HttpScenarioActor tracks its own signedIn flag and requires the sign-in response itself to set a cookie, because a populated jar proves nothing
|
|
15
20
|
- [Actor sign-in only works for actor-flagged users](actor-sign-in-only-works-for-actor-flagged-users.md) — The scenario actor secret mints sessions for user rows flagged actor and nothing else, so holding it never impersonates a real user
|
|
@@ -24,8 +29,10 @@ A rule about who may do what, and which way it fails when it is unsure.
|
|
|
24
29
|
- [Agent thread ownership fails closed when there is no principal](ai-agent-sessionless-deployments-have-no-thread-ownership.md) — A sessionless caller gets an ephemeral owner and reaches no stored thread, rather than reaching all of them
|
|
25
30
|
- [An agent thread key is always prefixed with the trusted principal](ai-agent-thread-ownership-composes-the-session-principal.md) — Ownership keys are composed as principal:resourceId, so a client id can sub-divide its own boundary but never widen it
|
|
26
31
|
- [Agent tool permission filtering reads the live function config, not the metadata](ai-agent-tool-filtering-reads-the-live-function-config.md) — The pikkuAuth brand survives only on live permission objects, so a metadata-driven check would silently admit every gated tool
|
|
32
|
+
- [An agent approval is claimed before the tool runs](an-agent-approval-is-claimed-before-the-tool-runs.md) — resolveApproval is a compare-and-swap returning whether this caller won, because the read that precedes it is not a claim and ten concurrent approvals would otherwise mean ten refunds
|
|
27
33
|
- [An empty owners constraint matches nothing](an-empty-owners-constraint-matches-nothing.md) — owners is an authorization boundary, so every storage backend must treat [] as no rows rather than no filter
|
|
28
34
|
- [An exposed function with no gate is reported at codegen, not at boot](an-exposed-ungated-function-is-a-codegen-warning.md) — The check runs in the inspector where function meta and every wireAddon declaration are both in hand, because neither source alone can tell a gated function from an ungated one
|
|
35
|
+
- [An upload is counted as it arrives, not buffered and then measured](an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md) — Reading the whole body before checking its size hands an unauthenticated caller a way to spend the server's memory
|
|
29
36
|
- [The console addon's privileged functions gate themselves](console-addon-privileged-functions-gate-themselves.md) — Thread listing is owner-scoped unless the caller holds admin, and addon installation requires an admin session, rather than trusting the host to register a global permission
|
|
30
37
|
- [Core's SSRF guard matches host literals because edge runtimes have no DNS](core-safe-fetch-blocks-ssrf-by-host-literal-not-dns.md) — safeFetch rejects internal address literals and re-validates every redirect hop; it cannot stop DNS rebinding
|
|
31
38
|
- [Core secrets are encrypted with a per-secret DEK wrapped by a KEK](core-secrets-use-a-per-secret-dek-wrapped-by-a-kek.md) — Envelope encryption keeps ciphertext untouched during key rotation, at the cost of storing two blobs per secret
|
package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: question
|
|
3
|
+
title: A channel's middleware list accepts bare factories that nothing ever resolves
|
|
4
|
+
description: CoreChannel.channelMiddleware admits CorePikkuChannelMiddlewareFactory, but no runner calls one, so a bare factory would stall the chain
|
|
5
|
+
tags: core, channel
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A channel's middleware list accepts bare factories that nothing resolves
|
|
9
|
+
|
|
10
|
+
`CoreChannel.channelMiddleware` is typed as
|
|
11
|
+
`(CorePikkuChannelMiddleware | CorePikkuChannelMiddlewareFactory)[]`, but
|
|
12
|
+
`runPikkuFunc`'s `wireChannelMiddleware` parameter admits only
|
|
13
|
+
`CorePikkuChannelMiddleware[]`. The two call sites that bridge them —
|
|
14
|
+
`channel-common.ts` and `channel-handler.ts` — assert across the gap.
|
|
15
|
+
|
|
16
|
+
A factory is `(input: In) => CorePikkuChannelMiddleware`: the author is meant to
|
|
17
|
+
*call* it at wiring time and put the result in the array.
|
|
18
|
+
`combineChannelMiddleware` contains no factory-resolution branch, and neither
|
|
19
|
+
does `combineMiddleware` for ordinary middleware — that is consistent and
|
|
20
|
+
deliberate. So a bare, uncalled factory in `channelMiddleware` would be pushed
|
|
21
|
+
into the chain and then invoked as if it were middleware: it would receive
|
|
22
|
+
`(services, channel, next)` as its single `input` argument, return a middleware
|
|
23
|
+
function that nothing runs, and never call `next` — stalling the chain silently.
|
|
24
|
+
|
|
25
|
+
Nothing in this repository puts a bare factory there, so the failure is
|
|
26
|
+
hypothetical today. The type is what invites it.
|
|
27
|
+
|
|
28
|
+
**What would settle it:** deciding whether `channelMiddleware` should accept
|
|
29
|
+
only resolved middleware — in which case the union is simply wrong and should
|
|
30
|
+
lose its factory arm, and both assertions disappear — or whether the runner
|
|
31
|
+
should resolve factories, in which case `combineChannelMiddleware` needs a
|
|
32
|
+
branch and the ordinary middleware path probably needs the matching one.
|
|
33
|
+
|
|
34
|
+
Until then the assertions name the exact target type rather than `as any`, so
|
|
35
|
+
the gap is visible at both call sites.
|
|
@@ -11,5 +11,6 @@ currently makes a choice that nobody has defended — the choice stays, the doub
|
|
|
11
11
|
gets recorded.
|
|
12
12
|
|
|
13
13
|
<!-- pikku:knowledge-index -->
|
|
14
|
-
|
|
14
|
+
- [A channel's middleware list accepts bare factories that nothing ever resolves](channel-middleware-accepts-bare-factories-that-nothing-resolves.md) — CoreChannel.channelMiddleware admits CorePikkuChannelMiddlewareFactory, but no runner calls one, so a bare factory would stall the chain
|
|
15
|
+
- [An unauthorized channel reply is sent outside the channel's declared Out type](unauthorized-channel-replies-escape-the-declared-out-type.md) — processMessageHandlers sends a bare string on auth failure that no generated client type admits, and the alternatives were never worked through
|
|
15
16
|
<!-- /pikku:knowledge-index -->
|