@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
|
@@ -77,89 +77,122 @@ const text = (status: number, body: string) =>
|
|
|
77
77
|
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
getJWT,
|
|
84
|
-
}: LocalContentRequestHandlerOptions): LocalContentRequestHandler => {
|
|
85
|
-
// Logged at most once. An unverifiable request is attacker-triggerable, so
|
|
86
|
-
// this reports a startup misconfiguration rather than per-request news.
|
|
87
|
-
let loggedMissingJWT = false
|
|
80
|
+
export type SignedContentVerification =
|
|
81
|
+
| { ok: true }
|
|
82
|
+
| { ok: false; status: number; body: string }
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Verifies a signed content URL — the one function that decides whether a
|
|
86
|
+
* signed read or a signed upload is genuine.
|
|
87
|
+
*
|
|
88
|
+
* Exported and shared so the runtimes that serve content (the core handler here,
|
|
89
|
+
* the node server, the express adapter) verify identically. They used to each
|
|
90
|
+
* carry their own copy, which is how the express upload path ended up with no
|
|
91
|
+
* check at all: one copy can rot or be forgotten in isolation.
|
|
92
|
+
*
|
|
93
|
+
* `onMissingJWT` is called at most where the caller wants to log the
|
|
94
|
+
* misconfiguration once; a signed request with no verifier is treated as
|
|
95
|
+
* invalid, never as trusted.
|
|
96
|
+
*/
|
|
97
|
+
export const verifySignedContentRequest = async (
|
|
98
|
+
requestUrl: URL,
|
|
99
|
+
jwt: JWTService | undefined,
|
|
100
|
+
onMissingJWT?: () => void
|
|
101
|
+
): Promise<SignedContentVerification> => {
|
|
102
|
+
const signedAtValue = requestUrl.searchParams.get('signedAt')
|
|
103
|
+
const expiresAtValue = requestUrl.searchParams.get('expiresAt')
|
|
104
|
+
const notBeforeValue = requestUrl.searchParams.get('notBefore')
|
|
105
|
+
const signature = requestUrl.searchParams.get('signature')
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
notBeforeValue == null ? undefined : Number(notBeforeValue)
|
|
107
|
+
if (!signedAtValue || !expiresAtValue) {
|
|
108
|
+
return { ok: false, status: 403, body: 'Signed URL required' }
|
|
109
|
+
}
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
(notBefore != null && !Number.isFinite(notBefore))
|
|
110
|
-
) {
|
|
111
|
-
return { ok: false, status: 403, body: 'Invalid signed URL' }
|
|
112
|
-
}
|
|
111
|
+
const signedAt = Number(signedAtValue)
|
|
112
|
+
const expiresAt = Number(expiresAtValue)
|
|
113
|
+
const notBefore = notBeforeValue == null ? undefined : Number(notBeforeValue)
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
if (
|
|
116
|
+
!Number.isFinite(signedAt) ||
|
|
117
|
+
!Number.isFinite(expiresAt) ||
|
|
118
|
+
(notBefore != null && !Number.isFinite(notBefore))
|
|
119
|
+
) {
|
|
120
|
+
return { ok: false, status: 403, body: 'Invalid signed URL' }
|
|
121
|
+
}
|
|
118
122
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
logger.error(
|
|
124
|
-
'pikku: refusing signed asset reads — no JWTService is available to verify them. Pass `contentSigningJWT` (the same service LocalContent signs with) or expose it as `singletonServices.jwt`.'
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
return { ok: false, status: 403, body: 'Invalid signed URL' }
|
|
128
|
-
}
|
|
123
|
+
const now = Date.now()
|
|
124
|
+
if (now > expiresAt || (notBefore != null && now < notBefore)) {
|
|
125
|
+
return { ok: false, status: 403, body: 'Signed URL expired' }
|
|
126
|
+
}
|
|
129
127
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
128
|
+
if (!jwt) {
|
|
129
|
+
onMissingJWT?.()
|
|
130
|
+
return { ok: false, status: 403, body: 'Invalid signed URL' }
|
|
131
|
+
}
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
133
|
+
if (!signature) {
|
|
134
|
+
return { ok: false, status: 403, body: 'Signed URL signature required' }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const payload = await jwt.decode<{
|
|
139
|
+
signedAt?: number
|
|
140
|
+
expiresAt?: number
|
|
141
|
+
notBefore?: number
|
|
142
|
+
path?: string
|
|
143
|
+
}>(signature)
|
|
144
|
+
|
|
145
|
+
// Every claim is compared, the path included: without it a signature
|
|
146
|
+
// minted for one asset would read any other.
|
|
147
|
+
if (
|
|
148
|
+
payload.signedAt !== signedAt ||
|
|
149
|
+
payload.expiresAt !== expiresAt ||
|
|
150
|
+
payload.notBefore !== notBefore ||
|
|
151
|
+
payload.path !== signedContentPath(requestUrl.pathname)
|
|
152
|
+
) {
|
|
153
153
|
return { ok: false, status: 403, body: 'Invalid signed URL' }
|
|
154
154
|
}
|
|
155
|
-
|
|
156
|
-
return { ok:
|
|
155
|
+
} catch {
|
|
156
|
+
return { ok: false, status: 403, body: 'Invalid signed URL' }
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
return { ok: true }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const createLocalContentRequestHandler = ({
|
|
163
|
+
content,
|
|
164
|
+
logger,
|
|
165
|
+
getJWT,
|
|
166
|
+
}: LocalContentRequestHandlerOptions): LocalContentRequestHandler => {
|
|
167
|
+
// Logged at most once. An unverifiable request is attacker-triggerable, so
|
|
168
|
+
// this reports a startup misconfiguration rather than per-request news.
|
|
169
|
+
let loggedMissingJWT = false
|
|
170
|
+
|
|
171
|
+
const validateSignedAssetRequest = (
|
|
172
|
+
requestUrl: URL
|
|
173
|
+
): Promise<SignedContentVerification> =>
|
|
174
|
+
verifySignedContentRequest(requestUrl, getJWT(), () => {
|
|
175
|
+
if (loggedMissingJWT) return
|
|
176
|
+
loggedMissingJWT = true
|
|
177
|
+
logger.error(
|
|
178
|
+
'pikku: refusing signed asset reads — no JWTService is available to verify them. Pass `contentSigningJWT` (the same service LocalContent signs with) or expose it as `singletonServices.jwt`.'
|
|
179
|
+
)
|
|
180
|
+
})
|
|
181
|
+
|
|
159
182
|
const handleUpload = async (
|
|
160
183
|
request: Request,
|
|
184
|
+
requestUrl: URL,
|
|
161
185
|
pathname: string
|
|
162
186
|
): Promise<Response> => {
|
|
187
|
+
// Uploads are presigned exactly like reads: getUploadURL mints a signed URL
|
|
188
|
+
// and this verifies it before writing. Without this an unauthenticated PUT
|
|
189
|
+
// could write arbitrary bytes to any key under the content root — the write
|
|
190
|
+
// side had none of the signature checking the read side has always had.
|
|
191
|
+
const signed = await validateSignedAssetRequest(requestUrl)
|
|
192
|
+
if (!signed.ok) {
|
|
193
|
+
return text(signed.status, signed.body)
|
|
194
|
+
}
|
|
195
|
+
|
|
163
196
|
const key = contentKey(pathname, content.uploadUrlPrefix)
|
|
164
197
|
const targetPath = toTargetPath(content.localFileUploadPath, key)
|
|
165
198
|
if (!targetPath) {
|
|
@@ -168,11 +201,7 @@ export const createLocalContentRequestHandler = ({
|
|
|
168
201
|
|
|
169
202
|
const maxBytes = parseSizeLimit(content.sizeLimit ?? '1mb')
|
|
170
203
|
|
|
171
|
-
//
|
|
172
|
-
// oversized upload costs the limit rather than its own size — `arrayBuffer()`
|
|
173
|
-
// would have to hold all of it first, which hands an unauthenticated caller
|
|
174
|
-
// a way to spend the server's memory. Mirrors node-http-server's
|
|
175
|
-
// `readRequestBody`, which aborts the same way.
|
|
204
|
+
// knowledge: decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md
|
|
176
205
|
const chunks: Buffer[] = []
|
|
177
206
|
let bytesRead = 0
|
|
178
207
|
const reader = request.body?.getReader()
|
|
@@ -252,7 +281,7 @@ export const createLocalContentRequestHandler = ({
|
|
|
252
281
|
request.method === 'PUT' &&
|
|
253
282
|
matchesPrefix(pathname, content.uploadUrlPrefix)
|
|
254
283
|
) {
|
|
255
|
-
return handleUpload(request, pathname)
|
|
284
|
+
return handleUpload(request, requestUrl, pathname)
|
|
256
285
|
}
|
|
257
286
|
|
|
258
287
|
if (
|
|
@@ -48,6 +48,9 @@ export const signedContentPath = (urlOrPath: string): string => {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** How long a presigned upload URL stays valid. Short, like an S3 PUT URL. */
|
|
52
|
+
const UPLOAD_URL_TTL_MS = 15 * 60 * 1000
|
|
53
|
+
|
|
51
54
|
export class LocalContent implements ContentService {
|
|
52
55
|
constructor(
|
|
53
56
|
private config: LocalContentConfig,
|
|
@@ -137,13 +140,24 @@ export class LocalContent implements ContentService {
|
|
|
137
140
|
})
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Presigned upload, mirroring the read side: the URL carries a short-lived,
|
|
145
|
+
* path-bound signature that {@link createLocalContentRequestHandler} verifies
|
|
146
|
+
* before writing. An unsigned upload URL cannot be verified, and the handler
|
|
147
|
+
* now refuses one — so this must sign, or every upload 403s.
|
|
148
|
+
*/
|
|
140
149
|
public async getUploadURL(args: GetUploadURLArgs): Promise<UploadURLResult> {
|
|
141
150
|
void args.visibility
|
|
142
151
|
const fullKey = this.joinKey(args.bucket, args.fileKey)
|
|
143
152
|
this.logger.debug(`Going to upload with key: ${fullKey}`)
|
|
153
|
+
const uploadUrl = await this.signURL({
|
|
154
|
+
url: `${this.config.uploadUrlPrefix}/${fullKey}`,
|
|
155
|
+
dateLessThan: new Date(Date.now() + UPLOAD_URL_TTL_MS),
|
|
156
|
+
})
|
|
144
157
|
return {
|
|
145
|
-
uploadUrl
|
|
158
|
+
uploadUrl,
|
|
146
159
|
assetKey: fullKey,
|
|
160
|
+
uploadMethod: 'PUT',
|
|
147
161
|
}
|
|
148
162
|
}
|
|
149
163
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
EmailService,
|
|
3
|
+
SendEmailInput,
|
|
4
|
+
SendEmailResult,
|
|
5
|
+
} from './email-service.js'
|
|
2
6
|
|
|
3
7
|
export class LocalEmailService implements EmailService {
|
|
4
8
|
async send(input: SendEmailInput): Promise<SendEmailResult> {
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
SystemRoleShadowedError,
|
|
12
12
|
} from '../errors/errors.js'
|
|
13
13
|
|
|
14
|
-
const system =
|
|
14
|
+
const system =
|
|
15
|
+
(...names: string[]) =>
|
|
16
|
+
(name: string) =>
|
|
17
|
+
names.includes(name)
|
|
15
18
|
|
|
16
19
|
describe('mutating a role', () => {
|
|
17
20
|
test('a declared role refuses to be deleted or re-scoped', async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SerializedError } from '../types/core.types.js'
|
|
1
|
+
import type { CoreUserSession, SerializedError } from '../types/core.types.js'
|
|
2
2
|
import type {
|
|
3
3
|
WorkflowRun,
|
|
4
4
|
WorkflowPlannedStep,
|
|
@@ -47,8 +47,15 @@ export interface WorkflowService {
|
|
|
47
47
|
/**
|
|
48
48
|
* The payload is stored as given and validated on replay inside the workflow
|
|
49
49
|
* body, so an invalid one re-closes the gate rather than failing the run.
|
|
50
|
+
*
|
|
51
|
+
* The session must belong to the run's owner — see `assertWorkflowRunOwner`.
|
|
50
52
|
*/
|
|
51
|
-
approveStep(
|
|
53
|
+
approveStep(
|
|
54
|
+
runId: string,
|
|
55
|
+
reason: string,
|
|
56
|
+
decision: unknown,
|
|
57
|
+
session?: CoreUserSession
|
|
58
|
+
): Promise<void>
|
|
52
59
|
startWorkflow<I>(
|
|
53
60
|
name: string,
|
|
54
61
|
input: I,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { join, dirname, relative } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const srcRoot = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const packageRoot = join(srcRoot, '..')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A statement that runs when the module is imported, rather than when
|
|
12
|
+
* something in it is called. Today every one is an `addError(...)` registering
|
|
13
|
+
* an error class in the runtime registry.
|
|
14
|
+
*/
|
|
15
|
+
const TOP_LEVEL_CALL = /^[a-zA-Z_$][\w.$]*\(/
|
|
16
|
+
|
|
17
|
+
const collectSourceFiles = (directory: string): string[] =>
|
|
18
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
19
|
+
const entryPath = join(directory, entry.name)
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
|
|
22
|
+
}
|
|
23
|
+
return entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')
|
|
24
|
+
? [entryPath]
|
|
25
|
+
: []
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/** Source modules that do something merely by being imported. */
|
|
29
|
+
const modulesWithSideEffects = (): string[] =>
|
|
30
|
+
collectSourceFiles(srcRoot)
|
|
31
|
+
.filter((file) =>
|
|
32
|
+
readFileSync(file, 'utf-8')
|
|
33
|
+
.split('\n')
|
|
34
|
+
.some((line) => TOP_LEVEL_CALL.test(line))
|
|
35
|
+
)
|
|
36
|
+
.map((file) => `./dist/${relative(srcRoot, file).replace(/\.ts$/, '.js')}`)
|
|
37
|
+
.sort()
|
|
38
|
+
|
|
39
|
+
const declared = (): string[] => {
|
|
40
|
+
const pkg = JSON.parse(
|
|
41
|
+
readFileSync(join(packageRoot, 'package.json'), 'utf-8')
|
|
42
|
+
)
|
|
43
|
+
return [...(pkg.sideEffects ?? [])].sort()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `sideEffects` tells a bundler which modules it may not drop. Claiming
|
|
48
|
+
* `false` outright would be untrue — the error registry is built by
|
|
49
|
+
* `addError(...)` calls that run on import, and a bundler that dropped
|
|
50
|
+
* `errors/errors.js` would leave `getErrorResponse` unable to find any of
|
|
51
|
+
* them. Naming the modules exactly lets everything else be tree-shaken.
|
|
52
|
+
*
|
|
53
|
+
* knowledge: decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md
|
|
54
|
+
*/
|
|
55
|
+
describe('every module with import-time side effects is declared', () => {
|
|
56
|
+
test('the allowlist matches the modules that actually have them', () => {
|
|
57
|
+
const actual = modulesWithSideEffects()
|
|
58
|
+
const listed = declared()
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
{ missing: actual.filter((m) => !listed.includes(m)) },
|
|
62
|
+
{ missing: [] },
|
|
63
|
+
'a module runs code on import but is not in package.json sideEffects. ' +
|
|
64
|
+
'A bundler may drop it, and whatever it registered will be absent at ' +
|
|
65
|
+
'runtime.'
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(
|
|
69
|
+
{ stale: listed.filter((m) => !actual.includes(m)) },
|
|
70
|
+
{ stale: [] },
|
|
71
|
+
'package.json sideEffects names a module that no longer has any — ' +
|
|
72
|
+
'remove it so bundlers can tree-shake it.'
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('the detector finds a known side effect', () => {
|
|
77
|
+
// Guards the test above: a detector matching nothing would pass whatever
|
|
78
|
+
// package.json claimed.
|
|
79
|
+
assert.ok(
|
|
80
|
+
modulesWithSideEffects().includes('./dist/errors/errors.js'),
|
|
81
|
+
'errors/errors.js registers every error class at import and must match'
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { join, dirname, relative } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const srcRoot = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The ceiling a single module may reach before it has to be composed out of
|
|
11
|
+
* smaller ones.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_LINES = 2000
|
|
14
|
+
|
|
15
|
+
const collectSourceFiles = (directory: string): string[] =>
|
|
16
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
17
|
+
const entryPath = join(directory, entry.name)
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
|
|
20
|
+
}
|
|
21
|
+
return entry.name.endsWith('.ts') ? [entryPath] : []
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('source files stay composable', () => {
|
|
25
|
+
test(`no module in @pikku/core exceeds ${MAX_LINES} lines`, () => {
|
|
26
|
+
const offenders = collectSourceFiles(srcRoot)
|
|
27
|
+
.map((file) => ({
|
|
28
|
+
file: relative(srcRoot, file),
|
|
29
|
+
lines: readFileSync(file, 'utf-8').split('\n').length,
|
|
30
|
+
}))
|
|
31
|
+
.filter(({ lines }) => lines > MAX_LINES)
|
|
32
|
+
.sort((a, b) => b.lines - a.lines)
|
|
33
|
+
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
offenders,
|
|
36
|
+
[],
|
|
37
|
+
`modules over ${MAX_LINES} lines:\n` +
|
|
38
|
+
offenders.map(({ file, lines }) => ` ${lines} ${file}`).join('\n')
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, test, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import type { AgentRunService } from '../../wirings/ai-agent/ai-agent.types.js'
|
|
5
|
+
import type { ServiceTestConfig } from '../service-tests.js'
|
|
6
|
+
|
|
7
|
+
/** Conformance suite for `agentRunService`. Runs only when a backend supplies one. */
|
|
8
|
+
export const defineAgentRunServiceTests = (
|
|
9
|
+
name: string,
|
|
10
|
+
agentRunService: NonNullable<
|
|
11
|
+
ServiceTestConfig['services']['agentRunService']
|
|
12
|
+
>,
|
|
13
|
+
/**
|
|
14
|
+
* Thread-ownership is a property of the two services together — the run
|
|
15
|
+
* service lists what the storage service owns — so that half of the suite
|
|
16
|
+
* only runs when a backend supplies both. Passed explicitly rather than
|
|
17
|
+
* closed over, so the dependency is visible in the signature.
|
|
18
|
+
*/
|
|
19
|
+
aiStorageService?: ServiceTestConfig['services']['aiStorageService']
|
|
20
|
+
): void => {
|
|
21
|
+
const factory = agentRunService
|
|
22
|
+
describe(`AgentRunService [${name}]`, () => {
|
|
23
|
+
let agentService: AgentRunService
|
|
24
|
+
|
|
25
|
+
before(async () => {
|
|
26
|
+
agentService = await factory()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('listThreads', async () => {
|
|
30
|
+
const threads = await agentService.listThreads()
|
|
31
|
+
assert.ok(Array.isArray(threads))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (aiStorageService) {
|
|
35
|
+
const storageFactory = aiStorageService
|
|
36
|
+
|
|
37
|
+
// The `owners` constraint is what keeps the generated thread-management
|
|
38
|
+
// functions from leaking across tenants: a caller may only list threads
|
|
39
|
+
// owned by one of their session principals, matching the
|
|
40
|
+
// `principal:sub-partition` composition resolveOwnerResourceId writes.
|
|
41
|
+
test('listThreads scopes to the given owners, including sub-partitions', async () => {
|
|
42
|
+
const storage = await storageFactory()
|
|
43
|
+
await storage.createThread('owner-alice')
|
|
44
|
+
await storage.createThread('owner-alice:project-1')
|
|
45
|
+
await storage.createThread('owner-bob:secret')
|
|
46
|
+
|
|
47
|
+
const threads = await agentService.listThreads({
|
|
48
|
+
owners: ['owner-alice'],
|
|
49
|
+
})
|
|
50
|
+
const ids = threads.map((t) => t.resourceId)
|
|
51
|
+
|
|
52
|
+
assert.ok(ids.includes('owner-alice'))
|
|
53
|
+
assert.ok(ids.includes('owner-alice:project-1'))
|
|
54
|
+
assert.ok(
|
|
55
|
+
!ids.some((id) => id.startsWith('owner-bob')),
|
|
56
|
+
"another owner's threads must not be listed"
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('listThreads with an owner does not match a lookalike prefix', async () => {
|
|
61
|
+
const storage = await storageFactory()
|
|
62
|
+
await storage.createThread('owner-al')
|
|
63
|
+
await storage.createThread('owner-alice-evil:p')
|
|
64
|
+
|
|
65
|
+
const threads = await agentService.listThreads({
|
|
66
|
+
owners: ['owner-al'],
|
|
67
|
+
})
|
|
68
|
+
const ids = threads.map((t) => t.resourceId)
|
|
69
|
+
|
|
70
|
+
assert.ok(ids.includes('owner-al'))
|
|
71
|
+
assert.ok(!ids.includes('owner-alice-evil:p'))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('listThreads with an empty owners list returns nothing', async () => {
|
|
75
|
+
const storage = await storageFactory()
|
|
76
|
+
await storage.createThread('owner-empty-check')
|
|
77
|
+
|
|
78
|
+
const threads = await agentService.listThreads({ owners: [] })
|
|
79
|
+
assert.deepEqual(threads, [])
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test('getThread returns null for missing', async () => {
|
|
84
|
+
const thread = await agentService.getThread('missing-thread')
|
|
85
|
+
assert.equal(thread, null)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('getDistinctAgentNames', async () => {
|
|
89
|
+
const names = await agentService.getDistinctAgentNames()
|
|
90
|
+
assert.ok(Array.isArray(names))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('deleteThread returns false for missing', async () => {
|
|
94
|
+
const result = await agentService.deleteThread('missing-thread')
|
|
95
|
+
assert.equal(result, false)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
}
|