@pikku/core 0.12.78 → 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 +234 -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.d.ts +10 -0
- package/dist/services/in-memory-workflow-service.js +25 -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/utils/node-host-resolver.d.ts +12 -0
- package/dist/utils/node-host-resolver.js +16 -0
- package/dist/utils/safe-fetch.d.ts +18 -0
- package/dist/utils/safe-fetch.js +167 -29
- 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 +41 -5
- 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 -0
- 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 +44 -111
- package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
- 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/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
- 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 +14 -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 +25 -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/utils/node-host-resolver.ts +20 -0
- package/src/utils/safe-fetch.test.ts +143 -1
- package/src/utils/safe-fetch.ts +200 -25
- 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-authorization.test.ts +131 -0
- package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +62 -22
- 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 +69 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- 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 +208 -630
- 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-dispatch-relay.test.ts +128 -0
- 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/src/types/core.types.ts
CHANGED
|
@@ -298,6 +298,11 @@ export interface CoreUserSession {
|
|
|
298
298
|
* Populated by whoever builds the session — core reads them, never fetches.
|
|
299
299
|
*/
|
|
300
300
|
scopes?: string[]
|
|
301
|
+
/**
|
|
302
|
+
* Restricts the session to functions declared `readonly`. The function runner
|
|
303
|
+
* throws `ReadonlySessionError` for anything else.
|
|
304
|
+
*/
|
|
305
|
+
readonly?: boolean
|
|
301
306
|
}
|
|
302
307
|
|
|
303
308
|
/**
|
|
@@ -378,7 +383,11 @@ export type PikkuWire<
|
|
|
378
383
|
MCPTools extends string | never = never,
|
|
379
384
|
TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire,
|
|
380
385
|
TriggerOutput = unknown,
|
|
381
|
-
|
|
386
|
+
// Defaulted to `any` rather than to `Out`: the emitted `TypedScenario<Out>`
|
|
387
|
+
// supplies the real context, while `PikkuWire`'s own defaults are what other
|
|
388
|
+
// generics constrain against — and a constraint that pinned the context to
|
|
389
|
+
// `Out` would reject every wire whose scenario output is anything else.
|
|
390
|
+
TypedScenario extends PikkuScenarioWire<any> | never = PikkuScenarioWire<any>,
|
|
382
391
|
TypedActors extends ScenarioPersonas = ScenarioPersonas,
|
|
383
392
|
> = {
|
|
384
393
|
/** Always present — lazily initialised on first access for every function invocation */
|
|
@@ -386,6 +395,11 @@ export type PikkuWire<
|
|
|
386
395
|
} & Partial<{
|
|
387
396
|
wireType: PikkuWiringTypes
|
|
388
397
|
wireId: string
|
|
398
|
+
/**
|
|
399
|
+
* A logger scoped to this invocation, when a host attaches one. Core never
|
|
400
|
+
* sets it; services that log fall back to the singleton logger.
|
|
401
|
+
*/
|
|
402
|
+
logger: Logger
|
|
389
403
|
/** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
|
|
390
404
|
traceId: string
|
|
391
405
|
functionId: string
|
package/src/types/state.types.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises'
|
|
2
|
+
|
|
3
|
+
import { setDefaultHostResolver, type HostResolver } from './safe-fetch.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a hostname through the platform resolver, returning every address it
|
|
7
|
+
* points at so `safeFetch` can reject a public name aimed at an internal one.
|
|
8
|
+
*
|
|
9
|
+
* This module is Node-only and is never imported by core itself — Workers has no
|
|
10
|
+
* DNS API, and a static `node:dns` import in the shared path would break that
|
|
11
|
+
* build.
|
|
12
|
+
*/
|
|
13
|
+
export const nodeHostResolver: HostResolver = async (hostname) => {
|
|
14
|
+
const results = await lookup(hostname, { all: true, verbatim: true })
|
|
15
|
+
return results.map(({ address }) => address)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Installs {@link nodeHostResolver} as the default for every `safeFetch`. */
|
|
19
|
+
export const installNodeHostResolver = () =>
|
|
20
|
+
setDefaultHostResolver(nodeHostResolver)
|
|
@@ -41,15 +41,54 @@ describe('isPrivateHost', () => {
|
|
|
41
41
|
}
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
+
test('flags reserved IPv4 blocks that are not routable public space', () => {
|
|
45
|
+
for (const host of [
|
|
46
|
+
'100.64.0.1', // carrier-grade NAT
|
|
47
|
+
'100.100.100.200', // Alibaba Cloud metadata endpoint
|
|
48
|
+
'100.127.255.255', // top of 100.64.0.0/10
|
|
49
|
+
'192.0.0.192', // IETF protocol assignments
|
|
50
|
+
'198.18.0.1', // benchmarking
|
|
51
|
+
'198.19.255.255', // top of 198.18.0.0/15
|
|
52
|
+
'192.88.99.1', // 6to4 anycast
|
|
53
|
+
'192.0.2.1', // TEST-NET-1
|
|
54
|
+
'198.51.100.1', // TEST-NET-2
|
|
55
|
+
'203.0.113.1', // TEST-NET-3
|
|
56
|
+
'224.0.0.1', // multicast
|
|
57
|
+
'240.0.0.1', // reserved
|
|
58
|
+
'255.255.255.255', // broadcast
|
|
59
|
+
]) {
|
|
60
|
+
assert.equal(isPrivateHost(host), true, `${host} should be private`)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('flags IPv6 forms that tunnel to an internal IPv4 address', () => {
|
|
65
|
+
for (const host of [
|
|
66
|
+
'64:ff9b::a9fe:a9fe', // NAT64 wrapping 169.254.169.254
|
|
67
|
+
'[64:ff9b::169.254.169.254]', // same, dotted-quad tail
|
|
68
|
+
'2002:a9fe:a9fe::', // 6to4 wrapping 169.254.169.254
|
|
69
|
+
'2002:6464:64c8::', // 6to4 wrapping 100.100.100.200
|
|
70
|
+
'fec0::1', // deprecated site-local
|
|
71
|
+
'ff02::1', // multicast
|
|
72
|
+
'100::1', // discard-only
|
|
73
|
+
]) {
|
|
74
|
+
assert.equal(isPrivateHost(host), true, `${host} should be private`)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
44
78
|
test('allows public hosts', () => {
|
|
45
79
|
for (const host of [
|
|
46
80
|
'example.com',
|
|
47
81
|
'8.8.8.8',
|
|
48
82
|
'172.32.0.1',
|
|
49
83
|
'11.0.0.1',
|
|
84
|
+
'100.63.255.255', // just below the CGNAT block
|
|
85
|
+
'100.128.0.0', // just above the CGNAT block
|
|
86
|
+
'198.17.255.255', // just below the benchmarking block
|
|
87
|
+
'198.20.0.0', // just above the benchmarking block
|
|
50
88
|
'134744072', // decimal-encoded 8.8.8.8 — public
|
|
51
89
|
'2001:db8::1', // documentation range — public
|
|
52
|
-
'
|
|
90
|
+
'2002:808:808::', // 6to4 wrapping public 8.8.8.8
|
|
91
|
+
'64:ff9b::808:808', // NAT64 wrapping public 8.8.8.8
|
|
53
92
|
]) {
|
|
54
93
|
assert.equal(isPrivateHost(host), false, `${host} should be public`)
|
|
55
94
|
}
|
|
@@ -138,6 +177,109 @@ describe('safeFetch', () => {
|
|
|
138
177
|
)
|
|
139
178
|
})
|
|
140
179
|
|
|
180
|
+
test('refuses a public hostname that resolves to a private address', async () => {
|
|
181
|
+
await withStubbedFetch(
|
|
182
|
+
() => new Response('ok', { status: 200 }),
|
|
183
|
+
async (calls) => {
|
|
184
|
+
// The shape the literal-only check misses: a name that parses as public
|
|
185
|
+
// but whose A record points at the cloud metadata endpoint.
|
|
186
|
+
await assert.rejects(
|
|
187
|
+
safeFetch(
|
|
188
|
+
'https://169-254-169-254.nip.io/latest/meta-data/',
|
|
189
|
+
{},
|
|
190
|
+
{
|
|
191
|
+
resolveHost: async () => ['169.254.169.254'],
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
/resolves to 169\.254\.169\.254/
|
|
195
|
+
)
|
|
196
|
+
assert.equal(calls.length, 0)
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('refuses when any one resolved address is private', async () => {
|
|
202
|
+
await withStubbedFetch(
|
|
203
|
+
() => new Response('ok', { status: 200 }),
|
|
204
|
+
async (calls) => {
|
|
205
|
+
await assert.rejects(
|
|
206
|
+
safeFetch(
|
|
207
|
+
'https://split-horizon.example.com/',
|
|
208
|
+
{},
|
|
209
|
+
{
|
|
210
|
+
resolveHost: async () => ['93.184.216.34', '10.0.0.5'],
|
|
211
|
+
}
|
|
212
|
+
),
|
|
213
|
+
/resolves to 10\.0\.0\.5/
|
|
214
|
+
)
|
|
215
|
+
assert.equal(calls.length, 0)
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('re-resolves the host of a redirect hop', async () => {
|
|
221
|
+
await withStubbedFetch(
|
|
222
|
+
(url) =>
|
|
223
|
+
url.includes('start.com')
|
|
224
|
+
? new Response(null, {
|
|
225
|
+
status: 302,
|
|
226
|
+
headers: { location: 'https://rebound.example.com/next' },
|
|
227
|
+
})
|
|
228
|
+
: new Response('ok', { status: 200 }),
|
|
229
|
+
async (calls) => {
|
|
230
|
+
await assert.rejects(
|
|
231
|
+
safeFetch(
|
|
232
|
+
'https://start.com',
|
|
233
|
+
{},
|
|
234
|
+
{
|
|
235
|
+
resolveHost: async (hostname) =>
|
|
236
|
+
hostname === 'start.com' ? ['93.184.216.34'] : ['127.0.0.1'],
|
|
237
|
+
}
|
|
238
|
+
),
|
|
239
|
+
/resolves to 127\.0\.0\.1/
|
|
240
|
+
)
|
|
241
|
+
assert.equal(calls.length, 1)
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('allows a public hostname that resolves to public addresses', async () => {
|
|
247
|
+
await withStubbedFetch(
|
|
248
|
+
() => new Response('ok', { status: 200 }),
|
|
249
|
+
async (calls) => {
|
|
250
|
+
const res = await safeFetch(
|
|
251
|
+
'https://example.com/',
|
|
252
|
+
{},
|
|
253
|
+
{
|
|
254
|
+
resolveHost: async () => ['93.184.216.34'],
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
assert.equal(res.status, 200)
|
|
258
|
+
assert.equal(calls.length, 1)
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('does not resolve an IP literal or an allowlisted host', async () => {
|
|
264
|
+
await withStubbedFetch(
|
|
265
|
+
() => new Response('ok', { status: 200 }),
|
|
266
|
+
async () => {
|
|
267
|
+
let resolverCalls = 0
|
|
268
|
+
const resolveHost = async () => {
|
|
269
|
+
resolverCalls++
|
|
270
|
+
return ['10.0.0.1']
|
|
271
|
+
}
|
|
272
|
+
await safeFetch('https://93.184.216.34/', {}, { resolveHost })
|
|
273
|
+
await safeFetch(
|
|
274
|
+
'https://internal.example.com/',
|
|
275
|
+
{},
|
|
276
|
+
{ resolveHost, allowedHosts: ['internal.example.com'] }
|
|
277
|
+
)
|
|
278
|
+
assert.equal(resolverCalls, 0)
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
})
|
|
282
|
+
|
|
141
283
|
test('follows a redirect to another public host', async () => {
|
|
142
284
|
await withStubbedFetch(
|
|
143
285
|
(url) =>
|
package/src/utils/safe-fetch.ts
CHANGED
|
@@ -32,6 +32,134 @@ function parseIPv4Octets(
|
|
|
32
32
|
return octets as [number, number, number, number]
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* IPv4 blocks that must never be reachable from user-supplied URLs: private,
|
|
37
|
+
* loopback, link-local (cloud metadata), carrier-grade NAT (Alibaba's
|
|
38
|
+
* `100.100.100.200` metadata endpoint), IETF protocol assignments, benchmarking,
|
|
39
|
+
* 6to4 anycast, the documentation TEST-NETs, multicast and reserved space.
|
|
40
|
+
*/
|
|
41
|
+
const PRIVATE_IPV4_BLOCKS: ReadonlyArray<readonly [string, number]> = [
|
|
42
|
+
['0.0.0.0', 8],
|
|
43
|
+
['10.0.0.0', 8],
|
|
44
|
+
['100.64.0.0', 10],
|
|
45
|
+
['127.0.0.0', 8],
|
|
46
|
+
['169.254.0.0', 16],
|
|
47
|
+
['172.16.0.0', 12],
|
|
48
|
+
['192.0.0.0', 24],
|
|
49
|
+
['192.0.2.0', 24],
|
|
50
|
+
['192.88.99.0', 24],
|
|
51
|
+
['192.168.0.0', 16],
|
|
52
|
+
['198.18.0.0', 15],
|
|
53
|
+
['198.51.100.0', 24],
|
|
54
|
+
['203.0.113.0', 24],
|
|
55
|
+
['224.0.0.0', 4],
|
|
56
|
+
['240.0.0.0', 4],
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const toUint32 = (octets: [number, number, number, number]): number =>
|
|
60
|
+
((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0
|
|
61
|
+
|
|
62
|
+
const PRIVATE_IPV4_RANGES = PRIVATE_IPV4_BLOCKS.map(([base, prefix]) => {
|
|
63
|
+
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0
|
|
64
|
+
return [(toUint32(parseIPv4Octets(base)!) & mask) >>> 0, mask] as const
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const isPrivateIPv4 = (octets: [number, number, number, number]): boolean => {
|
|
68
|
+
const addr = toUint32(octets)
|
|
69
|
+
return PRIVATE_IPV4_RANGES.some(
|
|
70
|
+
([base, mask]) => (addr & mask) >>> 0 === base
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Expands an IPv6 literal into its eight 16-bit groups, handling `::` elision
|
|
76
|
+
* and a trailing dotted-quad. `null` when not a well-formed IPv6 literal.
|
|
77
|
+
*/
|
|
78
|
+
function parseIPv6Groups(host: string): number[] | null {
|
|
79
|
+
const zoneless = host.split('%')[0]!
|
|
80
|
+
const halves = zoneless.split('::')
|
|
81
|
+
if (halves.length > 2) return null
|
|
82
|
+
|
|
83
|
+
const parseSide = (side: string): number[] | null => {
|
|
84
|
+
if (side === '') return []
|
|
85
|
+
const parts = side.split(':')
|
|
86
|
+
const groups: number[] = []
|
|
87
|
+
for (let i = 0; i < parts.length; i++) {
|
|
88
|
+
const part = parts[i]!
|
|
89
|
+
if (i === parts.length - 1 && part.includes('.')) {
|
|
90
|
+
const v4 = parseIPv4Octets(part)
|
|
91
|
+
if (!v4) return null
|
|
92
|
+
groups.push((v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3])
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (!/^[0-9a-f]{1,4}$/.test(part)) return null
|
|
96
|
+
groups.push(parseInt(part, 16))
|
|
97
|
+
}
|
|
98
|
+
return groups
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const head = parseSide(halves[0]!)
|
|
102
|
+
if (head === null) return null
|
|
103
|
+
|
|
104
|
+
if (halves.length === 1) {
|
|
105
|
+
return head.length === 8 ? head : null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const tail = parseSide(halves[1]!)
|
|
109
|
+
if (tail === null) return null
|
|
110
|
+
if (head.length + tail.length > 7) return null
|
|
111
|
+
return [...head, ...new Array(8 - head.length - tail.length).fill(0), ...tail]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const embeddedIPv4 = (
|
|
115
|
+
hi: number,
|
|
116
|
+
lo: number
|
|
117
|
+
): [number, number, number, number] => [
|
|
118
|
+
(hi >> 8) & 0xff,
|
|
119
|
+
hi & 0xff,
|
|
120
|
+
(lo >> 8) & 0xff,
|
|
121
|
+
lo & 0xff,
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
function isPrivateIPv6(groups: number[]): boolean {
|
|
125
|
+
const [g0, g1, g2, g3, g4, g5, g6, g7] = groups as [
|
|
126
|
+
number,
|
|
127
|
+
number,
|
|
128
|
+
number,
|
|
129
|
+
number,
|
|
130
|
+
number,
|
|
131
|
+
number,
|
|
132
|
+
number,
|
|
133
|
+
number,
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
if (groups.every((g) => g === 0)) return true // :: unspecified
|
|
137
|
+
if (groups.slice(0, 7).every((g) => g === 0) && g7 === 1) return true // ::1
|
|
138
|
+
|
|
139
|
+
// IPv4-mapped ::ffff:0:0/96 and IPv4-compatible ::/96
|
|
140
|
+
if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0) {
|
|
141
|
+
if (g5 === 0xffff || g5 === 0) return isPrivateIPv4(embeddedIPv4(g6, g7))
|
|
142
|
+
}
|
|
143
|
+
// NAT64 well-known prefix 64:ff9b::/96
|
|
144
|
+
if (
|
|
145
|
+
g0 === 0x64 &&
|
|
146
|
+
g1 === 0xff9b &&
|
|
147
|
+
g2 === 0 &&
|
|
148
|
+
g3 === 0 &&
|
|
149
|
+
g4 === 0 &&
|
|
150
|
+
g5 === 0
|
|
151
|
+
)
|
|
152
|
+
return isPrivateIPv4(embeddedIPv4(g6, g7))
|
|
153
|
+
// 6to4 2002::/16 carries the IPv4 address in the next 32 bits
|
|
154
|
+
if (g0 === 0x2002) return isPrivateIPv4(embeddedIPv4(g1, g2))
|
|
155
|
+
if (g0 === 0x100 && g1 === 0 && g2 === 0 && g3 === 0) return true // discard-only 100::/64
|
|
156
|
+
if ((g0 & 0xffc0) === 0xfe80) return true // link-local fe80::/10
|
|
157
|
+
if ((g0 & 0xfe00) === 0xfc00) return true // unique-local fc00::/7
|
|
158
|
+
if ((g0 & 0xffc0) === 0xfec0) return true // deprecated site-local fec0::/10
|
|
159
|
+
if ((g0 & 0xff00) === 0xff00) return true // multicast ff00::/8
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
|
|
35
163
|
/**
|
|
36
164
|
* Whether a hostname is an obvious internal target. Best-effort literal
|
|
37
165
|
* matching only: it cannot catch a public hostname that *resolves* to a
|
|
@@ -46,31 +174,30 @@ export function isPrivateHost(hostname: string): boolean {
|
|
|
46
174
|
return true
|
|
47
175
|
|
|
48
176
|
if (host.includes(':')) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (mappedV4) return isPrivateHost(mappedV4[1]!)
|
|
52
|
-
const mappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)
|
|
53
|
-
if (mappedHex) {
|
|
54
|
-
const hi = parseInt(mappedHex[1]!, 16)
|
|
55
|
-
const lo = parseInt(mappedHex[2]!, 16)
|
|
56
|
-
return isPrivateHost(
|
|
57
|
-
`${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
if (/^fe[89ab]/.test(host)) return true // link-local fe80::/10
|
|
61
|
-
if (host.startsWith('fc') || host.startsWith('fd')) return true // unique-local fc00::/7
|
|
62
|
-
return false
|
|
177
|
+
const groups = parseIPv6Groups(host)
|
|
178
|
+
return groups ? isPrivateIPv6(groups) : false
|
|
63
179
|
}
|
|
64
180
|
|
|
65
181
|
const v4 = parseIPv4Octets(host)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
182
|
+
return v4 ? isPrivateIPv4(v4) : false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Resolves a hostname to the IP addresses it points at. */
|
|
186
|
+
export type HostResolver = (hostname: string) => Promise<string[]>
|
|
187
|
+
|
|
188
|
+
let defaultHostResolver: HostResolver | undefined
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Installs the resolver {@link safeFetch} uses when a call passes no
|
|
192
|
+
* `resolveHost` of its own.
|
|
193
|
+
*
|
|
194
|
+
* Core cannot resolve DNS itself — Workers has no DNS API — so without a
|
|
195
|
+
* resolver the guard is literal-only and a public name pointing at
|
|
196
|
+
* `169.254.169.254` passes. Node runtimes install
|
|
197
|
+
* `nodeHostResolver` from `@pikku/core/node-host-resolver` at startup.
|
|
198
|
+
*/
|
|
199
|
+
export function setDefaultHostResolver(resolver: HostResolver | undefined) {
|
|
200
|
+
defaultHostResolver = resolver
|
|
74
201
|
}
|
|
75
202
|
|
|
76
203
|
export interface SafeFetchOptions {
|
|
@@ -81,6 +208,50 @@ export interface SafeFetchOptions {
|
|
|
81
208
|
allowedHosts?: string[]
|
|
82
209
|
/** Maximum redirect hops to follow (each re-validated). Defaults to 3. */
|
|
83
210
|
maxRedirects?: number
|
|
211
|
+
/**
|
|
212
|
+
* Resolves a hostname so a *public* name pointing at a private address is
|
|
213
|
+
* refused. Defaults to whatever {@link setDefaultHostResolver} installed;
|
|
214
|
+
* pass `null` to opt a call out of resolution entirely.
|
|
215
|
+
*/
|
|
216
|
+
resolveHost?: HostResolver | null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Whether a hostname is already an IP literal, which the sync check covers. */
|
|
220
|
+
function isIpLiteral(hostname: string): boolean {
|
|
221
|
+
const host = hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '')
|
|
222
|
+
if (host.includes(':')) return parseIPv6Groups(host.toLowerCase()) !== null
|
|
223
|
+
return parseIPv4Octets(host) !== null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Rejects a hostname that resolves to an internal address.
|
|
228
|
+
*
|
|
229
|
+
* Resolution happens once per hop and the connection is not pinned to the
|
|
230
|
+
* address checked, so a rebind between this check and the socket connecting is
|
|
231
|
+
* still possible; catching that needs a runtime-level connect hook.
|
|
232
|
+
*/
|
|
233
|
+
async function assertResolvedHostAllowed(
|
|
234
|
+
hostname: string,
|
|
235
|
+
options: SafeFetchOptions
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
if (options.allowedHosts) return
|
|
238
|
+
const resolver =
|
|
239
|
+
options.resolveHost === null
|
|
240
|
+
? undefined
|
|
241
|
+
: (options.resolveHost ?? defaultHostResolver)
|
|
242
|
+
if (!resolver || isIpLiteral(hostname)) return
|
|
243
|
+
|
|
244
|
+
const addresses = await resolver(hostname)
|
|
245
|
+
if (addresses.length === 0) {
|
|
246
|
+
throw new Error(`Refusing to fetch: '${hostname}' resolved to no addresses`)
|
|
247
|
+
}
|
|
248
|
+
for (const address of addresses) {
|
|
249
|
+
if (isPrivateHost(address)) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Refusing to fetch from a private/internal host: '${hostname}' resolves to ${address}`
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
84
255
|
}
|
|
85
256
|
|
|
86
257
|
export function assertFetchableUrl(
|
|
@@ -142,7 +313,9 @@ export async function safeFetch(
|
|
|
142
313
|
options: SafeFetchOptions = {}
|
|
143
314
|
): Promise<Response> {
|
|
144
315
|
const maxRedirects = options.maxRedirects ?? 3
|
|
145
|
-
|
|
316
|
+
const initial = assertFetchableUrl(url, options)
|
|
317
|
+
await assertResolvedHostAllowed(initial.hostname, options)
|
|
318
|
+
let currentUrl = initial.toString()
|
|
146
319
|
let currentInit = init
|
|
147
320
|
|
|
148
321
|
for (let hop = 0; ; hop++) {
|
|
@@ -157,10 +330,12 @@ export async function safeFetch(
|
|
|
157
330
|
if (!location || hop >= maxRedirects) {
|
|
158
331
|
return response
|
|
159
332
|
}
|
|
160
|
-
const
|
|
333
|
+
const next = assertFetchableUrl(
|
|
161
334
|
new URL(location, currentUrl).toString(),
|
|
162
335
|
options
|
|
163
|
-
)
|
|
336
|
+
)
|
|
337
|
+
await assertResolvedHostAllowed(next.hostname, options)
|
|
338
|
+
const nextUrl = next.toString()
|
|
164
339
|
await response.body?.cancel()
|
|
165
340
|
let nextInit = redirectInit(response.status, currentInit)
|
|
166
341
|
if (new URL(nextUrl).origin !== new URL(currentUrl).origin) {
|
|
@@ -119,12 +119,7 @@ function actorInstructions(persona: ResolvedPersona, task: string): string {
|
|
|
119
119
|
: '',
|
|
120
120
|
`Your goal in this conversation: ${task}.`,
|
|
121
121
|
`Send one message at a time. Set "done" to true only once your goal is clearly accomplished, or clearly impossible.`,
|
|
122
|
-
//
|
|
123
|
-
// that cannot take a JSON *schema* degrades to OpenAI's `json_object`
|
|
124
|
-
// mode — which refuses the request outright unless the word "json" appears
|
|
125
|
-
// somewhere in the prompt. Saying it once here covers all three call sites
|
|
126
|
-
// (turn, approvals, verdict), since each builds on these instructions, and
|
|
127
|
-
// it costs nothing on providers that never needed telling.
|
|
122
|
+
// knowledge: decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md
|
|
128
123
|
`Reply with json matching the schema you are given, and nothing else.`,
|
|
129
124
|
]
|
|
130
125
|
.filter(Boolean)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { PikkuRawWire } from '../../types/core.types.js'
|
|
2
|
+
import type { SessionService } from '../../services/user-session-service.js'
|
|
3
|
+
import type { CoreUserSession } from '../../types/core.types.js'
|
|
4
|
+
import type { PikkuRPC } from '../rpc/rpc-types.js'
|
|
5
|
+
import type { AIAgentInput, AIStreamChannel } from './ai-agent.types.js'
|
|
6
|
+
import type { StreamAIAgentOptions } from './ai-agent-prepare.js'
|
|
7
|
+
import { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js'
|
|
8
|
+
import {
|
|
9
|
+
streamAIAgent,
|
|
10
|
+
resumeAIAgent,
|
|
11
|
+
interruptAIAgent,
|
|
12
|
+
} from './ai-agent-stream.js'
|
|
13
|
+
import { wrapChannelWithAGUI } from './ai-agent-agui.js'
|
|
14
|
+
|
|
15
|
+
export type AgentRPCOptions = {
|
|
16
|
+
sessionService?: SessionService<CoreUserSession>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `wire.rpc.agent`, implemented.
|
|
21
|
+
*
|
|
22
|
+
* Lives here rather than in `rpc-runner` so the agent surface is one file next
|
|
23
|
+
* to the runner and stream code it delegates to, instead of a wing of the RPC
|
|
24
|
+
* primitive.
|
|
25
|
+
*/
|
|
26
|
+
export const createAgentRPC = (
|
|
27
|
+
wire: PikkuRawWire,
|
|
28
|
+
options: AgentRPCOptions
|
|
29
|
+
): PikkuRPC['agent'] => {
|
|
30
|
+
const params = () => ({
|
|
31
|
+
sessionService: options.sessionService,
|
|
32
|
+
getCredential: wire.getCredential?.bind(wire),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const streamingChannel = (): AIStreamChannel => {
|
|
36
|
+
const channel = wire.channel as unknown as AIStreamChannel | undefined
|
|
37
|
+
if (!channel) throw new Error('No channel available for streaming')
|
|
38
|
+
return channel
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `run` and `approve` return the same shape; only the call differs. */
|
|
42
|
+
const asRunResult = (result: Awaited<ReturnType<typeof runAIAgent>>) => ({
|
|
43
|
+
runId: result.runId,
|
|
44
|
+
result: result.object ?? result.text,
|
|
45
|
+
usage: result.usage,
|
|
46
|
+
...(result.status === 'suspended' && {
|
|
47
|
+
status: 'suspended' as const,
|
|
48
|
+
pendingApprovals: result.pendingApprovals,
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
run: async (agentName: string, input: AIAgentInput) =>
|
|
54
|
+
asRunResult(await runAIAgent(agentName, input, params())),
|
|
55
|
+
|
|
56
|
+
stream: async (
|
|
57
|
+
agentName: string,
|
|
58
|
+
input: {
|
|
59
|
+
message: string
|
|
60
|
+
threadId: string
|
|
61
|
+
resourceId: string
|
|
62
|
+
model?: string
|
|
63
|
+
temperature?: number
|
|
64
|
+
},
|
|
65
|
+
streamOptions?: StreamAIAgentOptions
|
|
66
|
+
) => {
|
|
67
|
+
let currentRunId: string | undefined
|
|
68
|
+
await streamAIAgent(
|
|
69
|
+
agentName,
|
|
70
|
+
input,
|
|
71
|
+
wrapChannelWithAGUI(streamingChannel(), {
|
|
72
|
+
threadId: input.threadId,
|
|
73
|
+
getRunId: () => currentRunId,
|
|
74
|
+
}),
|
|
75
|
+
params(),
|
|
76
|
+
undefined,
|
|
77
|
+
{
|
|
78
|
+
...streamOptions,
|
|
79
|
+
onRunCreated: (runId) => {
|
|
80
|
+
currentRunId = runId
|
|
81
|
+
streamOptions?.onRunCreated?.(runId)
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
resume: async (
|
|
88
|
+
runId: string,
|
|
89
|
+
input: { toolCallId: string; approved: boolean },
|
|
90
|
+
streamOptions?: StreamAIAgentOptions
|
|
91
|
+
) => {
|
|
92
|
+
await resumeAIAgent(
|
|
93
|
+
{ runId, ...input },
|
|
94
|
+
wrapChannelWithAGUI(streamingChannel(), { runId }),
|
|
95
|
+
params(),
|
|
96
|
+
streamOptions
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
interrupt: async (runId: string, reason?: 'speech' | 'user' | 'timeout') =>
|
|
101
|
+
interruptAIAgent(
|
|
102
|
+
{ runId, ...(reason ? { reason } : {}) },
|
|
103
|
+
{ sessionService: options.sessionService }
|
|
104
|
+
),
|
|
105
|
+
|
|
106
|
+
approve: async (
|
|
107
|
+
runId: string,
|
|
108
|
+
approvals: { toolCallId: string; approved: boolean }[],
|
|
109
|
+
expectedAgentName?: string
|
|
110
|
+
) =>
|
|
111
|
+
asRunResult(
|
|
112
|
+
await resumeAIAgentSync(
|
|
113
|
+
runId,
|
|
114
|
+
approvals,
|
|
115
|
+
{ sessionService: options.sessionService },
|
|
116
|
+
expectedAgentName
|
|
117
|
+
)
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -372,11 +372,7 @@ export function wrapChannelWithAGUI(
|
|
|
372
372
|
break
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
//
|
|
376
|
-
// pikku-specific ones. Dropping it instead — which is what this did —
|
|
377
|
-
// means a voice agent reached over HTTP is inaudible: `voiceOutput`
|
|
378
|
-
// synthesizes every sentence, the provider bills for it, and nothing
|
|
379
|
-
// gets past the mapper.
|
|
375
|
+
// knowledge: decisions/internals/agent-speech-travels-as-a-custom-agui-event.md
|
|
380
376
|
case 'audio-delta': {
|
|
381
377
|
send({
|
|
382
378
|
type: 'CUSTOM',
|
|
@@ -574,7 +574,7 @@ describe('non-streaming agent interruption', () => {
|
|
|
574
574
|
updateRun: async (_runId: string, patch: unknown) => {
|
|
575
575
|
updates.push(patch)
|
|
576
576
|
},
|
|
577
|
-
resolveApproval: async () =>
|
|
577
|
+
resolveApproval: async () => true,
|
|
578
578
|
},
|
|
579
579
|
} as any)
|
|
580
580
|
|
|
@@ -648,6 +648,7 @@ describe('streaming resume interruption', () => {
|
|
|
648
648
|
// for approvals that have already been given.
|
|
649
649
|
resolveApproval: async () => {
|
|
650
650
|
run.pendingApprovals = []
|
|
651
|
+
return true
|
|
651
652
|
},
|
|
652
653
|
},
|
|
653
654
|
} as any)
|
|
@@ -19,7 +19,14 @@ export function resolveMemoryServices(
|
|
|
19
19
|
} {
|
|
20
20
|
const memoryConfig = agent.memory
|
|
21
21
|
const storage = memoryConfig?.storage
|
|
22
|
-
?
|
|
22
|
+
? // The service is named by string in agent config, so the lookup cannot be
|
|
23
|
+
// checked — only the shape it is required to have.
|
|
24
|
+
(
|
|
25
|
+
singletonServices as unknown as Record<
|
|
26
|
+
string,
|
|
27
|
+
AIStorageService | undefined
|
|
28
|
+
>
|
|
29
|
+
)[memoryConfig.storage]
|
|
23
30
|
: singletonServices.aiStorage
|
|
24
31
|
return { storage }
|
|
25
32
|
}
|