@pikku/core 0.12.141 → 0.12.143
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 +50 -0
- package/dist/errors/errors.d.ts +1 -1
- package/dist/errors/errors.js +1 -1
- package/dist/function/function-meta.types.d.ts +6 -6
- package/dist/function/functions.types.d.ts +2 -2
- package/dist/services/audit-service.d.ts +4 -4
- package/dist/services/feature-flag-service.d.ts +3 -3
- package/dist/services/{http-personas.d.ts → http-virtual-users.d.ts} +25 -47
- package/dist/services/{http-personas.js → http-virtual-users.js} +29 -53
- package/dist/services/in-memory-queue-service.d.ts +8 -1
- package/dist/services/in-memory-queue-service.js +67 -0
- package/dist/services/incoming-webhook-service.d.ts +2 -0
- package/dist/services/incoming-webhook-service.js +4 -0
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.js +1 -1
- package/dist/services/meta-service.d.ts +6 -6
- package/dist/services/meta-service.js +8 -8
- package/dist/{wirings/workflow → services}/scenario-cookie-jar.d.ts +2 -0
- package/dist/{wirings/workflow → services}/scenario-cookie-jar.js +18 -14
- package/dist/services/scheduler-service.d.ts +3 -0
- package/dist/services/scheduler-service.js +4 -0
- package/dist/services/system-role-guard.d.ts +1 -1
- package/dist/services/virtual-user-sign-in.d.ts +52 -0
- package/dist/services/virtual-user-sign-in.js +80 -0
- package/dist/services/virtual-user-statement.d.ts +31 -0
- package/dist/services/virtual-user-statement.js +24 -0
- package/dist/services/{personas-service.d.ts → virtual-users-service.d.ts} +25 -23
- package/dist/services/{personas-service.js → virtual-users-service.js} +3 -3
- package/dist/types/core.types.d.ts +11 -11
- package/dist/wirings/queue/index.d.ts +1 -1
- package/dist/wirings/queue/queue.types.d.ts +21 -0
- package/dist/wirings/queue/signed-queue-service.d.ts +4 -1
- package/dist/wirings/queue/signed-queue-service.js +3 -0
- package/dist/wirings/role/define-system-role.d.ts +1 -1
- package/dist/wirings/role/define-system-role.js +1 -1
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/scheduler/scheduler.types.d.ts +12 -0
- package/dist/wirings/{actor-flow/actor-flow.types.d.ts → virtual-user/conversation/conversation.types.d.ts} +5 -5
- package/dist/wirings/virtual-user/conversation/index.d.ts +2 -0
- package/dist/wirings/virtual-user/conversation/run-conversation.d.ts +36 -0
- package/dist/wirings/{actor-flow → virtual-user/conversation}/run-conversation.js +30 -30
- package/dist/wirings/virtual-user/define-virtual-users.d.ts +32 -0
- package/dist/wirings/{persona/define-personas.js → virtual-user/define-virtual-users.js} +9 -9
- package/dist/wirings/virtual-user/index.d.ts +30 -7
- package/dist/wirings/virtual-user/index.js +23 -4
- package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +8 -8
- package/dist/wirings/virtual-user/prepare-virtual-user-run.js +9 -9
- package/dist/wirings/virtual-user/run-virtual-user.d.ts +6 -6
- package/dist/wirings/virtual-user/run-virtual-user.js +10 -10
- package/dist/wirings/{persona/validate-personas.d.ts → virtual-user/validate-virtual-users.d.ts} +11 -11
- package/dist/wirings/{persona/validate-personas.js → virtual-user/validate-virtual-users.js} +24 -24
- package/dist/wirings/virtual-user/virtual-user-agents.d.ts +7 -7
- package/dist/wirings/virtual-user/virtual-user-agents.js +2 -2
- package/dist/wirings/{persona/persona-app-scopes.d.ts → virtual-user/virtual-user-app-scopes.d.ts} +10 -10
- package/dist/wirings/{persona/persona-app-scopes.js → virtual-user/virtual-user-app-scopes.js} +12 -12
- package/dist/wirings/virtual-user/virtual-user-catalogue.d.ts +3 -3
- package/dist/wirings/virtual-user/virtual-user-catalogue.js +2 -2
- package/dist/wirings/{persona/persona.types.d.ts → virtual-user/virtual-user-declaration.types.d.ts} +24 -24
- package/dist/wirings/virtual-user/virtual-user-derive.js +13 -7
- package/dist/wirings/virtual-user/virtual-user-dispositions.d.ts +1 -1
- package/dist/wirings/{persona/persona-email.d.ts → virtual-user/virtual-user-email.d.ts} +11 -11
- package/dist/wirings/{persona/persona-email.js → virtual-user/virtual-user-email.js} +19 -19
- package/dist/wirings/{persona/persona-environments.d.ts → virtual-user/virtual-user-environments.d.ts} +10 -10
- package/dist/wirings/{persona/persona-environments.js → virtual-user/virtual-user-environments.js} +21 -21
- package/dist/wirings/virtual-user/virtual-user-intents.d.ts +3 -3
- package/dist/wirings/virtual-user/virtual-user-intents.js +3 -3
- package/dist/wirings/{persona/persona-mailbox.d.ts → virtual-user/virtual-user-mailbox.d.ts} +4 -4
- package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +4 -4
- package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +42 -61
- package/dist/wirings/virtual-user/virtual-user-scaffold.js +69 -117
- package/dist/wirings/virtual-user/virtual-user-schedule-store.d.ts +15 -15
- package/dist/wirings/virtual-user/virtual-user-schedule.d.ts +6 -6
- package/dist/wirings/virtual-user/virtual-user-schedule.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-target.d.ts +6 -6
- package/dist/wirings/virtual-user/virtual-user-target.js +5 -5
- package/dist/wirings/virtual-user/virtual-user.types.d.ts +11 -11
- package/dist/wirings/virtual-user/virtual-user.types.js +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +9 -9
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +28 -28
- package/dist/wirings/workflow/pikku-scenario-service.js +77 -77
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +4 -4
- package/dist/wirings/workflow/scenario-prose.d.ts +8 -8
- package/dist/wirings/workflow/scenario-prose.js +12 -8
- package/dist/wirings/workflow/scenario-run.types.d.ts +13 -13
- package/dist/wirings/workflow/scenario-step.types.d.ts +23 -23
- package/dist/wirings/workflow/workflow.types.d.ts +2 -2
- package/knowledge/decisions/index.md +0 -2
- package/knowledge/decisions/internals/a-credential-name-declared-twice-must-agree-on-schema-and-type.md +29 -0
- package/knowledge/decisions/internals/a-grant-can-name-a-scope-declared-without-its-addon.md +29 -0
- package/knowledge/decisions/internals/a-latency-budget-is-asserted-by-its-cause.md +32 -0
- package/knowledge/decisions/internals/a-scheduled-task-writes-its-caller-session-before-it-runs.md +22 -0
- package/knowledge/decisions/internals/a-secret-set-in-memory-shadows-the-variable-of-the-same-name.md +28 -0
- package/knowledge/decisions/internals/a-virtual-user-acts-through-invokeraw-so-a-refusal-is-data.md +32 -0
- package/knowledge/decisions/internals/a-virtual-user-cadence-is-a-row-not-a-timer.md +12 -12
- package/knowledge/decisions/internals/a-virtual-user-converse-signs-in-before-its-first-turn.md +24 -0
- package/knowledge/decisions/internals/a-virtual-user-email-is-computed-with-the-run-id-as-a-sub-address.md +34 -0
- package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md +2 -2
- package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +3 -0
- package/knowledge/decisions/internals/an-agent-is-offered-under-its-registry-key-and-only-within-the-virtual-users-scopes.md +27 -0
- package/knowledge/decisions/internals/an-oauth2-credential-implies-an-optional-app-secret.md +26 -0
- package/knowledge/decisions/internals/analytics-identity-resolvers-run-in-declared-order.md +22 -0
- package/knowledge/decisions/internals/feature-flag-declarations-refuse-an-empty-anyof-and-conflicting-scopes.md +22 -0
- package/knowledge/decisions/internals/feature-flags-fail-open-and-never-enforce-access.md +28 -0
- package/knowledge/decisions/internals/flag-sync-rewrites-only-the-declared-text.md +24 -0
- package/knowledge/decisions/internals/in-a-scenario-a-4xx-is-data-not-an-exception.md +3 -3
- package/knowledge/decisions/internals/index.md +47 -31
- package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +10 -10
- package/knowledge/decisions/internals/role-names-cannot-contain-the-scope-separator.md +30 -0
- package/knowledge/decisions/internals/scenario-agent-calls-sign-in-on-401-only.md +23 -19
- package/knowledge/decisions/internals/scenario-steps-return-drained-response-records.md +2 -2
- package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +12 -12
- package/knowledge/decisions/internals/the-core-scope-validator-refuses-separator-wildcard-and-empty-segments.md +22 -0
- package/knowledge/decisions/internals/the-pikku-user-id-resolves-in-a-fixed-order.md +24 -0
- package/knowledge/decisions/internals/{the-actor-prompt-says-json-because-of-json-object-mode.md → the-virtual-user-prompt-says-json-because-of-json-object-mode.md} +4 -4
- package/knowledge/decisions/internals/{the-persona-runtime-is-exported-from-the-persona-entry-point.md → the-virtual-user-runtime-is-exported-from-the-virtual-user-entry-point.md} +8 -8
- package/knowledge/decisions/internals/validate-checks-virtual-users-through-a-shared-module.md +43 -0
- package/knowledge/decisions/internals/{actor-flow-drives-the-target-through-a-transport-seam.md → virtual-user-conversation-drives-the-target-through-a-transport-seam.md} +6 -6
- package/knowledge/decisions/internals/virtual-user-conversation-verdicts-are-llm-self-evaluations.md +25 -0
- package/knowledge/decisions/internals/virtual-user-conversations-seed-a-hidden-kickoff-message.md +23 -0
- package/knowledge/decisions/internals/virtual-users-are-declared-in-one-definevirtualusers-call-per-codebase.md +30 -0
- package/knowledge/decisions/internals/workflow-core-never-imports-a-browser-driver.md +5 -5
- package/knowledge/decisions/internals/workflow-run-capabilities-are-extensions-not-subclasses.md +2 -2
- package/knowledge/decisions/internals/workflow-scenario-assertions-never-retry-and-record-one-step.md +4 -4
- package/knowledge/decisions/internals/workflow-scenario-hooks-are-a-scenario-only-affordance.md +1 -1
- package/knowledge/decisions/internals/workflow-scenario-prose-is-rendered-from-typed-calls-not-parsed-from-english.md +2 -2
- package/knowledge/decisions/internals/workflow-scenario-step-targets-are-string-literals-for-the-inspector.md +1 -1
- package/knowledge/decisions/internals/workflow-suspended-runs-keep-their-in-process-context.md +1 -1
- package/knowledge/decisions/security/a-bearer-token-checked-against-a-secret-reads-it-per-request.md +26 -0
- package/knowledge/decisions/security/a-call-without-a-session-raises-missing-session-not-forbidden.md +20 -0
- package/knowledge/decisions/security/a-caller-cookie-wins-over-the-jar-for-the-same-name.md +23 -0
- package/knowledge/decisions/security/a-channel-lifecycle-acts-as-the-socket-upgrade-session.md +16 -0
- package/knowledge/decisions/security/a-channel-requires-a-session-unless-it-says-auth-false.md +26 -0
- package/knowledge/decisions/security/a-credential-read-path-follows-its-declared-type-not-absence.md +32 -0
- package/knowledge/decisions/security/a-credential-type-is-a-literal-the-inspector-checks-at-declaration.md +27 -0
- package/knowledge/decisions/security/a-first-party-cookie-is-written-only-after-its-purposes-consent.md +33 -0
- package/knowledge/decisions/security/a-readonly-session-reaches-only-readonly-functions.md +25 -0
- package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +10 -5
- package/knowledge/decisions/security/a-scheduled-virtual-user-run-needs-no-admin-at-run-time.md +27 -0
- package/knowledge/decisions/security/a-scoped-credential-view-is-limited-to-declared-names.md +26 -0
- package/knowledge/decisions/security/a-scoped-secret-view-reads-only-declared-keys-and-never-writes.md +25 -0
- package/knowledge/decisions/security/a-secret-is-sent-only-to-the-hosts-it-declares.md +29 -0
- package/knowledge/decisions/security/a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md +25 -0
- package/knowledge/decisions/security/a-stopped-run-by-id-skips-the-ownership-check.md +24 -0
- package/knowledge/decisions/security/a-vault-secret-redacts-on-serialization-and-throws-on-coercion.md +29 -0
- package/knowledge/decisions/security/a-virtual-user-catalogue-entry-is-filtered-by-scope-and-approval-is-denied-by-default.md +30 -0
- package/knowledge/decisions/security/a-virtual-user-environment-fails-closed.md +38 -0
- package/knowledge/decisions/security/a-virtual-user-never-runs-against-production.md +23 -0
- package/knowledge/decisions/security/a-virtual-user-reads-its-roles-from-the-apps-own-rpc-before-better-auth.md +27 -0
- package/knowledge/decisions/security/a-virtual-user-run-treats-an-unresolved-environment-as-production.md +41 -0
- package/knowledge/decisions/security/a-virtual-user-scopes-are-expanded-from-its-roles-once-at-run-preparation.md +25 -0
- package/knowledge/decisions/security/a-virtual-user-whose-roles-drift-from-the-declaration-refuses-to-run.md +32 -0
- package/knowledge/decisions/security/addon-screen-scopes-hide-and-do-not-enforce.md +20 -0
- package/knowledge/decisions/security/an-addon-header-can-only-narrow-the-session.md +26 -0
- package/knowledge/decisions/security/an-addon-instance-reads-only-the-secrets-and-credentials-it-is-named.md +33 -0
- package/knowledge/decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md +16 -0
- package/knowledge/decisions/security/an-api-key-is-jwt-decoded-into-the-session.md +26 -0
- package/knowledge/decisions/security/an-external-virtual-user-caller-presents-one-signed-statement.md +38 -0
- package/knowledge/decisions/security/an-inbound-webhook-source-dispatches-only-verified-requests.md +31 -0
- package/knowledge/decisions/security/an-mcp-call-with-no-http-request-stays-anonymous.md +18 -0
- package/knowledge/decisions/security/an-omitted-user-id-addresses-the-platform-credential.md +32 -0
- package/knowledge/decisions/security/analytics-identity-is-stamped-from-the-session-not-the-event.md +24 -0
- package/knowledge/decisions/security/app-access-is-a-scope-tree-synthesised-from-virtual-users.md +26 -0
- package/knowledge/decisions/security/approval-policy-decides-who-answers-a-tool-call.md +27 -0
- package/knowledge/decisions/security/auth-false-on-a-session-function-warns-and-still-requires-one.md +25 -0
- package/knowledge/decisions/security/bearer-token-hashes-are-unpeppered-sha256.md +18 -0
- package/knowledge/decisions/security/consent-is-read-from-a-cookie-and-a-recognised-denial-counts-as-refusal.md +29 -0
- package/knowledge/decisions/security/cors-defaults-to-any-origin-and-refuses-the-wildcard-with-credentials.md +22 -0
- package/knowledge/decisions/security/credential-backends-share-one-conformance-suite-for-per-user-isolation.md +29 -0
- package/knowledge/decisions/security/each-stage-has-one-virtual-user-key-pair.md +27 -0
- package/knowledge/decisions/security/hmac-verification-fails-closed-on-empty-inputs.md +25 -0
- package/knowledge/decisions/security/index.md +88 -24
- package/knowledge/decisions/security/local-content-refuses-private-buckets-under-the-public-segment.md +21 -0
- package/knowledge/decisions/security/mail-is-allowlisted-before-a-virtual-user-run-sees-it.md +38 -0
- package/knowledge/decisions/security/mcp-resources-tools-and-prompts-take-sessionless-functions.md +18 -0
- package/knowledge/decisions/security/origin-locked-routes-match-the-origin-exactly.md +22 -0
- package/knowledge/decisions/security/oss-auth-works-without-pikku-studio-cloud.md +26 -0
- package/knowledge/decisions/security/outbound-webhooks-use-an-allowlist-when-configured.md +24 -0
- package/knowledge/decisions/security/pikku-carries-virtual-user-scopes-as-data-and-the-app-grants-them.md +35 -0
- package/knowledge/decisions/security/scoped-content-access-is-a-root-folder-with-longest-prefix-grants.md +31 -0
- package/knowledge/decisions/security/services-without-secrets-get-a-throwing-secrets-accessor.md +22 -0
- package/knowledge/decisions/security/shared-guards-refuse-to-mutate-or-shadow-a-system-role.md +34 -0
- package/knowledge/decisions/security/signed-uploads-are-verified-by-the-same-check-as-signed-reads.md +26 -0
- package/knowledge/decisions/security/system-role-scopes-must-be-declared-with-define-scope.md +24 -0
- package/knowledge/decisions/security/system-roles-are-additive-and-immutable.md +16 -0
- package/knowledge/decisions/security/the-channel-rpc-does-not-send-the-callers-session.md +21 -0
- package/knowledge/decisions/security/the-remote-jobs-inbox-fails-closed-without-a-secret.md +24 -0
- package/knowledge/decisions/security/the-remote-rpc-gate-fails-closed-and-binds-the-token-to-its-function.md +34 -0
- package/knowledge/decisions/security/the-workflow-status-stream-rechecks-ownership-on-every-poll.md +16 -0
- package/knowledge/decisions/security/two-credentials-on-one-key-must-share-a-read-mode.md +29 -0
- package/knowledge/decisions/security/verified-bearer-claims-are-pass-through-data-that-core-never-fills.md +23 -0
- package/knowledge/decisions/security/virtual-user-catalogue-excludes-the-machinery-that-runs-virtual-users.md +25 -0
- package/knowledge/decisions/security/{actor-flow-missing-approval-decisions-default-to-denied.md → virtual-user-conversation-missing-approval-decisions-default-to-denied.md} +7 -7
- package/knowledge/decisions/security/virtual-user-run-records-are-gated-behind-a-read-scope.md +18 -0
- package/knowledge/decisions/security/virtual-user-sign-in-does-not-exist-in-production.md +25 -0
- package/knowledge/decisions/security/virtual-user-sign-in-has-one-door.md +42 -0
- package/knowledge/decisions/security/{actor-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md → virtual-user-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md} +10 -8
- package/knowledge/decisions/security/virtual-user-sign-in-only-works-for-virtual-user-flagged-users.md +29 -0
- package/knowledge/decisions/security/workflow-helper-functions-are-sessionless-by-declaration.md +18 -0
- package/knowledge/decisions/security/{workflow-scenario-sessions-are-isolated-per-actor-and-per-scenario.md → workflow-scenario-sessions-are-isolated-per-virtual-user-and-per-scenario.md} +6 -6
- package/knowledge/decisions/security/workflow-scenario-steps-are-never-network-invocable.md +5 -5
- package/knowledge/decisions/security/{workflow-actor-steps-always-use-the-real-transport.md → workflow-virtual-user-steps-always-use-the-real-transport.md} +11 -11
- package/knowledge/decisions/security/writing-a-virtual-user-schedule-is-gated-by-its-own-scope.md +18 -0
- package/knowledge/index.md +0 -2
- package/knowledge/questions/a-bearer-jwt-with-no-identity-becomes-a-session.md +24 -0
- package/knowledge/questions/dev-quick-login-as-impersonation.md +25 -0
- package/knowledge/questions/does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with.md +26 -0
- package/knowledge/questions/does-a-user-id-with-a-colon-read-another-users-credentials.md +27 -0
- package/knowledge/questions/does-a-wirecli-command-need-a-session-by-default.md +26 -0
- package/knowledge/questions/does-an-adversarial-in-scope-success-count-as-a-finding.md +26 -0
- package/knowledge/questions/does-approve-step-need-the-run-owners-session-or-the-gates-approvers.md +34 -0
- package/knowledge/questions/does-cors-origin-true-reflect-any-origin-with-credentials.md +20 -0
- package/knowledge/questions/does-skip-user-session-do-anything-on-a-channel.md +24 -0
- package/knowledge/questions/does-the-store-session-match-the-other-two.md +19 -0
- package/knowledge/questions/index.md +25 -2
- package/knowledge/questions/is-the-agent-score-worker-reachable-without-a-session.md +21 -0
- package/knowledge/questions/live-scheduled-tests-may-write.md +19 -0
- package/knowledge/questions/provision-virtual-users-has-no-caller-in-this-repo.md +23 -0
- package/knowledge/questions/remote-secret-refuses-requests-without-a-remote-token-on-every-path.md +32 -0
- package/knowledge/questions/should-a-middleware-set-session-persist-to-the-store.md +25 -0
- package/knowledge/questions/should-a-scoped-secret-view-throw-from-has-secret-for-a-disallowed-key.md +23 -0
- package/knowledge/questions/should-a-wrong-bearer-token-be-anonymous-or-refused.md +35 -0
- package/knowledge/questions/should-an-empty-webhook-signing-secret-send-unsigned.md +23 -0
- package/knowledge/questions/should-http-virtual-user-sessions-reset-between-scenarios-in-one-run.md +27 -0
- package/knowledge/questions/should-the-cookie-jar-scope-cookies-to-the-api-origin.md +26 -0
- package/knowledge/questions/should-voice-input-fetch-audio-urls-without-a-host-allowlist.md +20 -0
- package/knowledge/questions/staging-impersonation-rules.md +27 -0
- package/knowledge/questions/virtual-user-keys-on-cloud-stages-are-pikku-studios-to-build.md +33 -0
- package/knowledge/questions/virtual-user-statement-replay-is-per-process.md +20 -0
- package/knowledge/questions/what-an-impersonated-session-carries.md +22 -0
- package/package.json +2 -3
- package/src/public-surface.json +38 -47
- package/dist/services/persona-actor-secret.d.ts +0 -38
- package/dist/services/persona-actor-secret.js +0 -39
- package/dist/services/persona-sign-in.d.ts +0 -117
- package/dist/services/persona-sign-in.js +0 -154
- package/dist/wirings/actor-flow/index.d.ts +0 -2
- package/dist/wirings/actor-flow/run-conversation.d.ts +0 -36
- package/dist/wirings/persona/define-personas.d.ts +0 -32
- package/dist/wirings/persona/index.d.ts +0 -23
- package/dist/wirings/persona/index.js +0 -19
- package/knowledge/decisions/internals/actor-flow-conversations-seed-a-hidden-kickoff-message.md +0 -23
- package/knowledge/decisions/internals/actor-flow-verdicts-are-llm-self-evaluations.md +0 -25
- package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +0 -43
- package/knowledge/decisions/security/actor-sign-in-only-works-for-actor-flagged-users.md +0 -31
- package/knowledge/decisions/security/an-actor-credential-is-derived-per-persona.md +0 -42
- package/knowledge/decisions/security/pikku-carries-actor-scopes-as-data-and-the-app-grants-them.md +0 -26
- /package/dist/wirings/{actor-flow/actor-flow.types.js → virtual-user/conversation/conversation.types.js} +0 -0
- /package/dist/wirings/{actor-flow → virtual-user/conversation}/index.js +0 -0
- /package/dist/wirings/{persona/persona.types.js → virtual-user/virtual-user-declaration.types.js} +0 -0
- /package/dist/wirings/{persona/persona-mailbox.js → virtual-user/virtual-user-mailbox.js} +0 -0
package/knowledge/decisions/security/analytics-identity-is-stamped-from-the-session-not-the-event.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Analytics identity is stamped from the session and resolvers, never from the event body
|
|
4
|
+
description: Each analytics record takes userId and orgId from the wire session, pikkuUserId from the wire, and the rest from the identity resolvers, not from the event payload
|
|
5
|
+
tags: core, analytics, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Analytics identity is stamped from the session and resolvers, never from the event body
|
|
9
|
+
|
|
10
|
+
`resolveEvent` in `packages/core/src/analytics/analytics.ts:55-72` builds
|
|
11
|
+
`userIdentity` from the wire session (`:59`, `:67-69`), `this.wire.pikkuUserId`
|
|
12
|
+
(`:70`), and the output of the identity resolver (`:61`, `:71`). The event's own
|
|
13
|
+
fields are `name`, `props` and the optional `at` (`:60`, `:66`). The identity
|
|
14
|
+
fields are not read from the event.
|
|
15
|
+
|
|
16
|
+
`packages/core/src/analytics/analytics.types.ts:26-29` gives the reason: identity
|
|
17
|
+
is stamped server-side and never read from the request body, which is what makes
|
|
18
|
+
an unauthenticated ingest safe to expose.
|
|
19
|
+
|
|
20
|
+
The consent notes in this folder cover the cookie side of identity. They do not
|
|
21
|
+
cover how the record's identity is attached.
|
|
22
|
+
|
|
23
|
+
**What this rules out:** a client supplying `userId`, `orgId` or `pikkuUserId` in
|
|
24
|
+
an analytics event and having it recorded as the caller's identity.
|
package/knowledge/decisions/security/app-access-is-a-scope-tree-synthesised-from-virtual-users.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: App access is a scope tree synthesised from the virtual users that name an app
|
|
4
|
+
description: buildAppScopeDefinition creates one app:<name> scope per app any virtual user names, so which frontend a person may sign into is a runtime-revocable grant rather than a column per app
|
|
5
|
+
tags: core, virtual-users, scopes, access
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# App access is a scope tree synthesised from the virtual users that name an app
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/virtual-user/virtual-user-app-scopes.ts` builds an `app` scope
|
|
11
|
+
tree from the virtual users. `declaredApps` returns the app names used by virtual users,
|
|
12
|
+
sorted and without duplicates (`virtual-user-app-scopes.ts:23-30`).
|
|
13
|
+
`buildAppScopeDefinition` creates one child scope per app, with the origin
|
|
14
|
+
`generated` (`virtual-user-app-scopes.ts:60-74`). It returns `null` when no virtual user
|
|
15
|
+
names an app (`virtual-user-app-scopes.ts:55-58`).
|
|
16
|
+
|
|
17
|
+
The doc comment gives the reasons. Which frontend a person may use is the same
|
|
18
|
+
kind of question as a scope: an admin can grant and revoke it at runtime, it
|
|
19
|
+
resolves at the session boundary, and a restricted API key can decline to inherit
|
|
20
|
+
it (`virtual-user-app-scopes.ts:35-41`). The tree is synthesised, not written by hand,
|
|
21
|
+
because a hand-written copy could drift from the virtual users. The comment says that
|
|
22
|
+
failure "is one `pikku scopes prune` away from silently revoking sign-in"
|
|
23
|
+
(`virtual-user-app-scopes.ts:45-47`).
|
|
24
|
+
|
|
25
|
+
**What this rules out:** a separate per-app access column, and a hand-maintained
|
|
26
|
+
list of apps that has to match the virtual users.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The approval policy decides who answers a tool call
|
|
4
|
+
description: The conversation approvals option picks either a fixed answer or the virtual user's model for each pending tool call, and a call with no answer is denied
|
|
5
|
+
tags: core, virtual-users, approvals, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The approval policy decides who answers a tool call
|
|
9
|
+
|
|
10
|
+
`ConversationApprovalPolicy` in
|
|
11
|
+
`packages/core/src/wirings/virtual-user/conversation/conversation.types.ts:1` has
|
|
12
|
+
three values, and `approvals` is an optional field on `ConverseOptions` (`:9`).
|
|
13
|
+
|
|
14
|
+
`decideApprovals` in
|
|
15
|
+
`packages/core/src/wirings/virtual-user/conversation/run-conversation.ts:133-181`
|
|
16
|
+
applies it. The default is `in-virtual-user` (`:138`). `always` approves every
|
|
17
|
+
pending call and `never` denies every one (`:139-144`). `in-virtual-user` sends the
|
|
18
|
+
pending calls to the virtual user's model with `tools: []` and `toolChoice: 'none'`
|
|
19
|
+
(`:153-169`), and the model's decisions are what get applied.
|
|
20
|
+
|
|
21
|
+
Every pending call must get a decision. The code maps the answers by `toolCallId`
|
|
22
|
+
and a call with no answer is denied (`:176-180`). The missing-decision rule has
|
|
23
|
+
its own note,
|
|
24
|
+
[missing approval decisions default to denied](virtual-user-conversation-missing-approval-decisions-default-to-denied.md).
|
|
25
|
+
|
|
26
|
+
**What this rules out:** reading a missing answer as consent, and an
|
|
27
|
+
`in-virtual-user` run approving a call the virtual user's model did not name.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: auth false on a session-required function warns and still requires a session
|
|
4
|
+
description: Writing auth: false on a function that is not sessionless does not open it; the runner logs a warning and still throws MissingSessionError without a session
|
|
5
|
+
tags: core, auth, function-runner
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# auth false on a session-required function warns and still requires a session
|
|
9
|
+
|
|
10
|
+
In `runPikkuFunc` (`packages/core/src/function/function-runner.ts`), a function
|
|
11
|
+
that is not `sessionless` is session-required. If the wiring or the function
|
|
12
|
+
config says `auth: false`, the runner logs
|
|
13
|
+
"requires a session but auth was explicitly disabled — use pikkuSessionlessFunc
|
|
14
|
+
instead." and then still throws `MissingSessionError` when no session is present.
|
|
15
|
+
|
|
16
|
+
The test "should warn when auth is disabled for a session-required function but
|
|
17
|
+
still enforce session" in `function-runner.test.ts` states the intent. The
|
|
18
|
+
warning points the author at the function kind that actually means "no session",
|
|
19
|
+
which is `pikkuSessionlessFunc`. The same flag on a sessionless function is the
|
|
20
|
+
opposite declaration and is covered by
|
|
21
|
+
[A public function says so with auth: false](a-public-function-says-so-with-auth-false.md).
|
|
22
|
+
|
|
23
|
+
**What this rules out:** treating `auth: false` on a `pikkuFunc` as making it
|
|
24
|
+
public. Making a function public means using `pikkuSessionlessFunc`, and a
|
|
25
|
+
session-required function that disables auth still fails closed.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Bearer token hashes are unpeppered SHA-256
|
|
4
|
+
description: hashToken stores bearer tokens as plain SHA-256 hex because they are high-entropy random values; a low-entropy secret is not a hashed column
|
|
5
|
+
tags: core, classification, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Bearer token hashes are unpeppered SHA-256
|
|
9
|
+
|
|
10
|
+
`hashToken` in `packages/core/src/classification/column-form.ts:21-32` returns the
|
|
11
|
+
lowercase hex SHA-256 of its input through WebCrypto. The doc comment at `:8-15`
|
|
12
|
+
gives the reason: bearer tokens are high-entropy random values with no candidate
|
|
13
|
+
set to search, so a pepper would add a key to manage and a rotation story while
|
|
14
|
+
buying nothing. The same comment says a column that holds a low-entropy secret is
|
|
15
|
+
not a `hashed` column and needs a password KDF instead.
|
|
16
|
+
|
|
17
|
+
**What this rules out:** adding a pepper or key to token hashes, and using this
|
|
18
|
+
hash for passwords or other low-entropy values.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Consent is read from a cookie, and a recognised denial value counts as a refusal
|
|
4
|
+
description: cookieAnalyticsIdentity grants a consent purpose only when its cookie is present and not one of a fixed set of denial values, and resolves no identity off a non-HTTP wire
|
|
5
|
+
tags: core, analytics, consent, cookies
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Consent is read from a cookie, and a recognised denial value counts as a refusal
|
|
9
|
+
|
|
10
|
+
`packages/core/src/analytics/cookie-analytics-identity.ts` reads consent and
|
|
11
|
+
vendor identifiers from first-party cookies. The doc comment says a purpose is
|
|
12
|
+
true "only when the cookie is present and is not a recognised denial", so a
|
|
13
|
+
consent tool that writes a refusal rather than deleting the cookie is read as a
|
|
14
|
+
refusal (`cookie-analytics-identity.ts:7-11`).
|
|
15
|
+
|
|
16
|
+
The denial values are `0`, `false`, `denied`, `deny` and `no`
|
|
17
|
+
(`cookie-analytics-identity.ts:15`). The comparison is case-insensitive
|
|
18
|
+
(`cookie-analytics-identity.ts:49`).
|
|
19
|
+
|
|
20
|
+
A purpose whose cookie is absent is left out of the consent record entirely
|
|
21
|
+
(`cookie-analytics-identity.ts:47-48`). Since `mintCookie` requires `=== true`
|
|
22
|
+
(`mint-cookie.ts:34`), an absent cookie is a refusal there.
|
|
23
|
+
|
|
24
|
+
A wire with no HTTP request resolves nothing (`cookie-analytics-identity.ts:34-35`).
|
|
25
|
+
The doc comment says a cron task or queue worker has no browser, so "any vendor
|
|
26
|
+
id they produced would be invented" (`cookie-analytics-identity.ts:24-25`).
|
|
27
|
+
|
|
28
|
+
**What this rules out:** treating any present cookie value as consent, and
|
|
29
|
+
inventing a vendor identity for a non-browser caller.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: CORS defaults to any origin and refuses the wildcard when credentials are on
|
|
4
|
+
description: The cors middleware's origin defaults to *, and a resolved wildcard origin combined with credentials throws when the middleware is built
|
|
5
|
+
tags: core, middleware, cors, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# CORS defaults to any origin and refuses the wildcard when credentials are on
|
|
9
|
+
|
|
10
|
+
In `packages/core/src/middleware/cors.ts`, `origin` defaults to `'*'` (`:29`) and
|
|
11
|
+
`credentials` defaults to `false` (`:33`). The factory throws when the resolved
|
|
12
|
+
origin is `'*'` and credentials are on (`:36-40`). Because the check runs on the
|
|
13
|
+
resolved value, the default origin with `credentials: true` throws too.
|
|
14
|
+
|
|
15
|
+
A `true` origin reflects the request's own origin, or `*` when none is sent
|
|
16
|
+
(`:55-56`). An array origin answers with the request origin only when it is in the
|
|
17
|
+
list (`:57-61`). Any other request origin is left out of the response, with a debug
|
|
18
|
+
log (`:62-65`).
|
|
19
|
+
|
|
20
|
+
**What this rules out:** a wildcard origin together with credentials.
|
|
21
|
+
`questions/does-cors-origin-true-reflect-any-origin-with-credentials.md` records
|
|
22
|
+
the `true` case, which this check does not refuse.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Credential backends are checked by one conformance suite that proves per-user isolation
|
|
4
|
+
description: defineCredentialServiceTests asserts that one user's credential is invisible to another and that KEK rotation re-wraps stored values, for any backend that supplies a credential service
|
|
5
|
+
tags: core, credentials, testing, isolation
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Credential backends are checked by one conformance suite that proves per-user isolation
|
|
9
|
+
|
|
10
|
+
`defineCredentialServiceTests` in `packages/core/src/testing/service-tests/credential-service-tests.ts`
|
|
11
|
+
is the shared conformance suite for `credentialService`. Its doc comment says it
|
|
12
|
+
"runs only when a backend supplies one" (`credential-service-tests.ts:6`), so a
|
|
13
|
+
backend without a credential service is not checked by it.
|
|
14
|
+
|
|
15
|
+
Per-user isolation is asserted directly. A value set for `user-1` is read back
|
|
16
|
+
only for `user-1`, and `user-2` gets its own value (`credential-service-tests.ts:53-62`).
|
|
17
|
+
A delete for one user leaves the other user's credential in place
|
|
18
|
+
(`credential-service-tests.ts:64-72`). `getAll` for one user returns that user's
|
|
19
|
+
credentials and omits another user's (`credential-service-tests.ts:85-95`), and
|
|
20
|
+
returns `{}` for an unknown user (`credential-service-tests.ts:97-101`).
|
|
21
|
+
|
|
22
|
+
KEK rotation is asserted too. After rotation to a new key version, a credential
|
|
23
|
+
written under the old key is still readable, and the new key alone can read it
|
|
24
|
+
(`credential-service-tests.ts:103-133`). Rotation without a `previousKey` throws
|
|
25
|
+
(`credential-service-tests.ts:135-141`).
|
|
26
|
+
|
|
27
|
+
**What this rules out:** a credential backend that returns one user's stored value
|
|
28
|
+
to another user, or that rotates keys without re-wrapping existing credentials,
|
|
29
|
+
while still passing the suite.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Each stage has one virtual user key pair, and the app holds only the public half
|
|
4
|
+
description: The deployer holds the private key, the app reads the public key from an unreserved variable, and users never see either
|
|
5
|
+
tags: services
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Each stage has one virtual user key pair, and the app holds only the public half
|
|
9
|
+
|
|
10
|
+
| Environment | Private key held by | Public key reaches the app as |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| `pikku dev` | Generated on first start into `<runtimeDir>/virtual-user/signing.key` (mode 0600, a `.gitignore` of `*` beside it); the CLI reads it there | `process.env.VIRTUAL_USER_VERIFY_KEY`, set before services are built |
|
|
13
|
+
| Pikku Studio Cloud stage | Pikku Studio Cloud's backend, one pair per stage | the stage variable `VIRTUAL_USER_VERIFY_KEY`, set by Pikku Studio Cloud |
|
|
14
|
+
| Open source deploy | The deployer's machine, as `VIRTUAL_USER_SIGNING_KEY` in the environment of whoever runs the CLI | the variable `VIRTUAL_USER_VERIFY_KEY`, set by the deployer |
|
|
15
|
+
|
|
16
|
+
The app reads it with `variables.get('VIRTUAL_USER_VERIFY_KEY')`: the SPKI DER, base64
|
|
17
|
+
on one line (a PEM is also accepted). A stage without it registers no statement
|
|
18
|
+
endpoint, and the CLI's error for that says which variable to set.
|
|
19
|
+
|
|
20
|
+
**One pair per stage** so a statement for one stage verifies nowhere else; the
|
|
21
|
+
audience check is a second line, not the first. **Unreserved names**: neither starts
|
|
22
|
+
with `PIKKU_` or `PIKKUSTUDIO_`, which a platform may refuse to let an app or a deployer
|
|
23
|
+
set. **Users never manage keys**: `pikku dev` makes them and a deploy supplies them.
|
|
24
|
+
|
|
25
|
+
**What this rules out:** one key shared across stages, a private key stored in the
|
|
26
|
+
app's variables or secrets, and a key a person has to generate or paste to run
|
|
27
|
+
scenarios locally.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: HMAC verification fails closed on empty or malformed inputs
|
|
4
|
+
description: verifyHmacSignature returns false for an empty secret, a missing signature, an unknown algorithm, an empty key, a malformed hex signature or an empty signature
|
|
5
|
+
tags: core, security, webhooks
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# HMAC verification fails closed on empty or malformed inputs
|
|
9
|
+
|
|
10
|
+
`verifyHmacSignature` in `packages/core/src/utils/hmac.ts:142-161` returns `false`
|
|
11
|
+
in each of these cases:
|
|
12
|
+
|
|
13
|
+
- an empty secret or a missing signature (`:150`);
|
|
14
|
+
- an algorithm with no entry in `HMAC_HASH` (`:153`);
|
|
15
|
+
- a secret that decodes to an empty key (`:155`);
|
|
16
|
+
- a hex encoding whose signature does not match `^(?:[0-9a-fA-F]{2})+$` (`:156-158`);
|
|
17
|
+
- a signature that decodes to zero bytes (`:161`).
|
|
18
|
+
|
|
19
|
+
The key is imported with WebCrypto `importKey` (`:162-166`). The doc comment at
|
|
20
|
+
`:139-140` says the comparison is `crypto.subtle.verify('HMAC', ...)`, which runs
|
|
21
|
+
in the platform's constant-time implementation. `timingSafeStringEqual`
|
|
22
|
+
(`:52-63`) is a separate helper for opaque tokens and is not used for HMACs.
|
|
23
|
+
|
|
24
|
+
**What this rules out:** treating an empty secret, or a signature that does not
|
|
25
|
+
decode, as a match.
|
|
@@ -9,63 +9,127 @@ description: Who may do what in core — gates, defaults, and the failures they
|
|
|
9
9
|
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 bearer token checked against a secret is read per request, and an unreadable secret means no session](a-bearer-token-checked-against-a-secret-reads-it-per-request.md) — The secretId form of authBearer reads the secret on every request; a missing or failing read leaves the request without a session rather than raising an error
|
|
13
|
+
- [A call without a session raises MissingSessionError, and a missing scope raises MissingScopeError](a-call-without-a-session-raises-missing-session-not-forbidden.md) — The function runner separates the no-session refusal from the scope and permission refusals, so a caller can tell a missing login from a denied grant
|
|
14
|
+
- [A cookie the caller sets wins over the jar's copy of the same name](a-caller-cookie-wins-over-the-jar-for-the-same-name.md) — createCookieJar sends the caller's cookie for a name and drops the jar's copy of that name, so one request never carries the same cookie twice
|
|
15
|
+
- [A channel lifecycle acts as the session the socket upgrade established](a-channel-lifecycle-acts-as-the-socket-upgrade-session.md) — onConnect and onDisconnect run with the userSession captured during the socket upgrade, both in the wire they receive and as the session service passed to runPikkuFunc
|
|
16
|
+
- [A channel requires a session unless it says auth: false](a-channel-requires-a-session-unless-it-says-auth-false.md) — The channel-level requirement is auth !== false, and a message handler's own auth, when it is set, decides for that message in either direction
|
|
17
|
+
- [A credential's read path follows its declared type, never a missing value](a-credential-read-path-follows-its-declared-type-not-absence.md) — The credential wire service reads a wire credential only from the user's store and a singleton only from the deployment, so an absent user value never falls back to the deployment's token
|
|
18
|
+
- [A credential's wire or singleton type is a literal the inspector checks at declaration](a-credential-type-is-a-literal-the-inspector-checks-at-declaration.md) — defineCredential is a no-op read by AST, so its type must be a literal 'wire' or 'singleton' and the inspector refuses any other value
|
|
13
19
|
- [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
|
|
20
|
+
- [A first-party tracking cookie is written only after its purposes are consented](a-first-party-cookie-is-written-only-after-its-purposes-consent.md) — mintCookie writes a new cookie only when every purpose it requires is true in the consent record, and writes nothing on a wire with no HTTP response
|
|
14
21
|
- [A function never receives the secret service](a-function-never-receives-the-secret-service.md) — Every function-, permission- and auth-facing services type is bounded by SecretlessServices, so reaching for `secrets` in a function body is a type error rather than a lint
|
|
15
22
|
- [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
|
|
16
23
|
- [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
|
|
24
|
+
- [A public function says so with auth: false](a-public-function-says-so-with-auth-false.md) — An exposed sessionless function that writes auth: false is public on purpose and PKU574 stays quiet; one that leaves auth out still warns
|
|
25
|
+
- [A readonly session reaches only functions declared readonly](a-readonly-session-reaches-only-readonly-functions.md) — A session flagged readonly is refused by every function not declared readonly, checked straight after the session is required and before scopes
|
|
26
|
+
- [A scaffold flag says a surface exists, not who may call it](a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md) — scaffold.<feature> is boolean | { path } — the auth field is gone, because the function runner already enforces each generated function's own auth, wiring, scopes and addon
|
|
27
|
+
- [A scheduled virtual-user run needs no admin at run time](a-scheduled-virtual-user-run-needs-no-admin-at-run-time.md) — An admin may start a virtual-user run, but the run itself never depends on an admin being signed in or on an admin's credential
|
|
28
|
+
- [A scoped credential service is limited to the names its package declared](a-scoped-credential-view-is-limited-to-declared-names.md) — ScopedCredentialService refuses any credential name outside the package's declared set, filters getAll to that set, and refuses to enumerate users
|
|
29
|
+
- [A scoped secret view reads only its declared keys and never writes](a-scoped-secret-view-reads-only-declared-keys-and-never-writes.md) — ScopedSecretService checks every read against an allowed-key set and refuses all writes, unlike the credential service that stays writable
|
|
30
|
+
- [A secret may only be sent to the hosts its declaration names](a-secret-is-sent-only-to-the-hosts-it-declares.md) — assertSecretAllowedForHost refuses a host outside a secret's allowedHosts, and refuses any host at all for a secret with no allowedHosts when the project requires them
|
|
31
|
+
- [A scenario step can name an identity no virtual user holds, through per-call headers](a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md) — ScenarioInvokeOptions.headers lets a step send an impersonation or shim header on one call, and a virtual-user run never gets that option
|
|
17
32
|
- [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
|
|
33
|
+
- [Stopping an agent run by its id skips the ownership check; interruptAgent is the checked path](a-stopped-run-by-id-skips-the-ownership-check.md) — signalRunInterrupt is exported and performs no authorisation, while interruptAgent asserts the run's resource owner before it signals
|
|
34
|
+
- [A vault secret redacts when serialized and throws when coerced to a string](a-vault-secret-redacts-on-serialization-and-throws-on-coercion.md) — SecretValue serializes to a redacted marker so logs stay honest without crashing, but string coercion throws, and reveal() is the only way to read the value
|
|
35
|
+
- [Virtual users are offered calls by declared scope, and approval-gated calls are denied by default](a-virtual-user-catalogue-entry-is-filtered-by-scope-and-approval-is-denied-by-default.md) — The catalogue type filters by the declared scopes of each RPC rather than by permission functions, and the catalogue filter drops approval-required entries unless the caller opts in
|
|
36
|
+
- [A virtual user's sign-in is refused unless the environment is known and allowed](a-virtual-user-environment-fails-closed.md) — Virtual user provisioning, sync and sign-in refuse when PIKKU_ENV is unresolved, unknown, production without the accountable disposition, or outside the virtual user's environments, and production environments are omitted by default
|
|
18
37
|
- [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
|
|
19
|
-
- [A
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
38
|
+
- [A virtual user never runs against production, whatever its disposition](a-virtual-user-never-runs-against-production.md) — startVirtualUserRun and an enabling writeVirtualUserSchedule both refuse with a 403 in production, including for the accountable disposition; an unresolved environment counts as production
|
|
39
|
+
- [A virtual user reads its roles from the app's own RPC before better-auth's user.role](a-virtual-user-reads-its-roles-from-the-apps-own-rpc-before-better-auth.md) — sessionRoles asks getMyScopes first, then get-session, and returns null rather than an empty list when the stage reports no roles
|
|
40
|
+
- [A virtual-user run treats an unresolved PIKKU_ENV as production](a-virtual-user-run-treats-an-unresolved-environment-as-production.md) — The virtual-user scaffold decides "production" from the configured PIKKU_ENV environment, counts an unresolved one as production, and falls back to NODE_ENV only when no environments are configured
|
|
41
|
+
- [A virtual user's scopes are expanded from its roles once, at run preparation](a-virtual-user-scopes-are-expanded-from-its-roles-once-at-run-preparation.md) — A virtual user's scope set is computed from its declared roles against the defineSystemRole definitions when the run is prepared, and that set is the oracle for scope drift
|
|
42
|
+
- [A virtual user whose granted roles drift from its declaration refuses to run](a-virtual-user-whose-roles-drift-from-the-declaration-refuses-to-run.md) — verifyVirtualUserRoles compares declared and granted roles in both directions before a run, and roleMismatchMessage produces the refusal text; a provider login is never runnable
|
|
43
|
+
- [A workflow run is read by its owner, and answered by whoever its gate declares](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; who may answer an approval gate is the gate's own declaration, and a run with no recorded owner has no ownership to enforce
|
|
24
44
|
- [Addon auth and tags only tighten, and resolve where the function runs](addon-auth-and-tags-only-tighten.md) — wireAddon auth and tags are applied in runPikkuFunc like scopes, but auth:false is ignored and tags resolve against the consuming app's tag groups rather than the addon package's
|
|
25
45
|
- [Addon auth and tag gates apply wherever the function runs, including inside the addon](addon-config-gates-apply-only-at-the-namespaced-rpc-boundary.md) — wireAddon's auth and tags moved from the namespaced RPC boundary into runPikkuFunc, so they also apply to direct wirings and to bare intra-addon calls
|
|
26
46
|
- [Addon scopes are resolved where the function runs](addon-scopes-are-resolved-where-the-function-runs.md) — wireAddon scopes are merged inside runPikkuFunc rather than at namespace resolution, because most wirings reach an addon function without ever resolving a namespace
|
|
27
|
-
- [
|
|
28
|
-
- [Only a Symbol-branded framework result can request tool approval](
|
|
29
|
-
- [Credential requests are trusted only when Symbol-branded](
|
|
30
|
-
- [An agent requires a session only when auth is true, but always enforces scopes and permissions](
|
|
31
|
-
- [An agent ownership failure never echoes the resource it refused](
|
|
32
|
-
- [Resuming a suspended agent run re-runs the agent's authorization gate](
|
|
33
|
-
- [Agent thread ownership fails closed when there is no principal](
|
|
34
|
-
- [An agent thread key is always prefixed with the trusted principal](
|
|
35
|
-
- [Agent tool permission filtering reads the live function config, not the metadata](
|
|
47
|
+
- [Addon screen scopes hide a screen and do not enforce access](addon-screen-scopes-hide-and-do-not-enforce.md) — The scopes on a defineScreens entry state what a viewer must hold to see the screen; the addon's own functions are the access check
|
|
48
|
+
- [Only a Symbol-branded framework result can request tool approval](agent-approval-forwarding-requires-a-symbol-brand.md) — Approval markers are trusted from the APPROVAL_REQUIRED Symbol on a forwardsApproval tool, never from a JSON key an LLM could emit
|
|
49
|
+
- [Credential requests are trusted only when Symbol-branded](agent-credential-requests-are-symbol-branded.md) — The string key is a wire field; the Symbol is the capability, and only core can mint it
|
|
50
|
+
- [An agent requires a session only when auth is true, but always enforces scopes and permissions](agent-gate-requires-a-session-only-when-auth-is-true.md) — Agents follow pikkuSessionlessFunc semantics so crons and queue workers can run them; scopes are an AND gate checked before any permission I/O
|
|
51
|
+
- [An agent ownership failure never echoes the resource it refused](agent-ownership-failures-never-echo-the-resource.md) — assertResourceOwner throws a bare ForbiddenError so the error cannot be used as an existence oracle, at the cost of thinner debugging output
|
|
52
|
+
- [Resuming a suspended agent run re-runs the agent's authorization gate](agent-resume-re-runs-the-authorization-gate.md) — Ownership of the run is not enough — a grant revoked while the run was suspended must block the approval
|
|
53
|
+
- [Agent thread ownership fails closed when there is no principal](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
|
|
54
|
+
- [An agent thread key is always prefixed with the trusted principal](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
|
|
55
|
+
- [Agent tool permission filtering reads the live function config, not the metadata](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
|
|
56
|
+
- [An addon header can only narrow the session, and an unknown addon is refused](an-addon-header-can-only-narrow-the-session.md) — addon-scope-cap intersects the session's scopes with the named addon's role, so naming an addon never widens access, and a name the host does not know is a 403
|
|
57
|
+
- [An addon instance reads only the secrets and credentials it is named](an-addon-instance-reads-only-the-secrets-and-credentials-it-is-named.md) — An addon's secret and credential views are its declared names plus the consuming app's grants and override keys; the global flags opt out of scoping
|
|
58
|
+
- [An addon may reach only itself and the addons it declares in uses](an-addon-may-reach-only-itself-and-the-addons-it-uses.md) — An RPC call that arrives tagged with an addon header is refused unless the target is the caller or one of the caller's declared uses, and an unknown addon name is refused outright
|
|
36
59
|
- [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
|
|
60
|
+
- [An API key is JWT-decoded into the session, from a header or a query parameter](an-api-key-is-jwt-decoded-into-the-session.md) — authAPIKey reads x-api-key or the apiKey query parameter, decodes it as a JWT, and sets the session only when no session exists yet
|
|
61
|
+
- [An approval answer outlives the run it answered](an-approval-answer-outlives-the-run-it-answered.md) — Run state holds a decision only while the gate is open, so the settled answer carries decidedBy/decidedAt into the step result and every attempt is written to the audit sink, which has no foreign key to the run
|
|
37
62
|
- [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
|
|
63
|
+
- [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
|
|
64
|
+
- [An external virtual user caller presents one signed statement](an-external-virtual-user-caller-presents-one-signed-statement.md) — Callers outside the app sign an EdDSA statement naming the virtual user; the app verifies it against a public key and accepts each statement once
|
|
65
|
+
- [An inbound webhook source dispatches only requests verified against its credential](an-inbound-webhook-source-dispatches-only-verified-requests.md) — A webhook source with verify refuses a body-carrying request that fails its check, fails closed on an empty secret, and does not dispatch events from a body-less request
|
|
66
|
+
- [An MCP call with no HTTP request stays anonymous by transport design](an-mcp-call-with-no-http-request-stays-anonymous.md) — MCP calls that arrive without an HTTP request, such as stdio, reach the function with no session because auth middleware reads the session only from the request
|
|
38
67
|
- [An MCP refusal is decided before dispatch, not raised from the tool](an-mcp-refusal-is-decided-before-dispatch.md) — mcpTargetRequiresSession lets the transport answer 401 with an OAuth challenge, because the MCP response streams and the status is gone by the time the runner refuses
|
|
39
68
|
- [An OAuth2 credential is read by its account row, not by its provider name](an-oauth2-credential-is-read-by-its-account-row.md) — better-auth 1.7.5 selects an account by row id under a strict body schema, so BetterAuthCredentialService resolves the row through the internal adapter before asking for a token
|
|
40
|
-
- [An
|
|
69
|
+
- [An omitted userId on CredentialService addresses the platform credential](an-omitted-user-id-addresses-the-platform-credential.md) — The CredentialService contract reads and writes the platform-level credential whenever a per-credential call omits userId, and the better-auth implementation defaults its platform owner to pikku-platform
|
|
41
70
|
- [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
|
|
71
|
+
- [Analytics identity is stamped from the session and resolvers, never from the event body](analytics-identity-is-stamped-from-the-session-not-the-event.md) — Each analytics record takes userId and orgId from the wire session, pikkuUserId from the wire, and the rest from the identity resolvers, not from the event payload
|
|
72
|
+
- [App access is a scope tree synthesised from the virtual users that name an app](app-access-is-a-scope-tree-synthesised-from-virtual-users.md) — buildAppScopeDefinition creates one app:<name> scope per app any virtual user names, so which frontend a person may sign into is a runtime-revocable grant rather than a column per app
|
|
73
|
+
- [The approval policy decides who answers a tool call](approval-policy-decides-who-answers-a-tool-call.md) — The conversation approvals option picks either a fixed answer or the virtual user's model for each pending tool call, and a call with no answer is denied
|
|
74
|
+
- [auth false on a session-required function warns and still requires a session](auth-false-on-a-session-function-warns-and-still-requires-one.md) — Writing auth: false on a function that is not sessionless does not open it; the runner logs a warning and still throws MissingSessionError without a session
|
|
75
|
+
- [Bearer token hashes are unpeppered SHA-256](bearer-token-hashes-are-unpeppered-sha256.md) — hashToken stores bearer tokens as plain SHA-256 hex because they are high-entropy random values; a low-entropy secret is not a hashed column
|
|
76
|
+
- [Consent is read from a cookie, and a recognised denial value counts as a refusal](consent-is-read-from-a-cookie-and-a-recognised-denial-counts-as-refusal.md) — cookieAnalyticsIdentity grants a consent purpose only when its cookie is present and not one of a fixed set of denial values, and resolves no identity off a non-HTTP wire
|
|
42
77
|
- [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
|
|
43
78
|
- [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
|
|
44
79
|
- [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
|
|
80
|
+
- [CORS defaults to any origin and refuses the wildcard when credentials are on](cors-defaults-to-any-origin-and-refuses-the-wildcard-with-credentials.md) — The cors middleware's origin defaults to *, and a resolved wildcard origin combined with credentials throws when the middleware is built
|
|
81
|
+
- [Credential backends are checked by one conformance suite that proves per-user isolation](credential-backends-share-one-conformance-suite-for-per-user-isolation.md) — defineCredentialServiceTests asserts that one user's credential is invisible to another and that KEK rotation re-wraps stored values, for any backend that supplies a credential service
|
|
82
|
+
- [Each stage has one virtual user key pair, and the app holds only the public half](each-stage-has-one-virtual-user-key-pair.md) — The deployer holds the private key, the app reads the public key from an unreserved variable, and users never see either
|
|
45
83
|
- [Gateway handlers run through the function runner gate](gateway-handlers-run-through-the-function-runner-gate.md) — A gateway's handler is registered as a real pikku function and invoked via runPikkuFunc, because calling it directly skips auth, scopes and permissions
|
|
46
84
|
- [Gateway middleware sessions must be bridged onto the wire](gateway-middleware-sessions-must-be-bridged-onto-the-wire.md) — Gateway middleware calling wire.setSession writes to a session service the handler's invocation never reads, so the session is copied onto wire.session before the gate runs
|
|
47
85
|
- [Global permissions and function permissions are independent gates](global-permissions-and-function-permissions-are-independent-gates.md) — Globals AND together and can only narrow access; a function's own group ORs internally and is never satisfied by a global
|
|
86
|
+
- [HMAC verification fails closed on empty or malformed inputs](hmac-verification-fails-closed-on-empty-inputs.md) — verifyHmacSignature returns false for an empty secret, a missing signature, an unknown algorithm, an empty key, a malformed hex signature or an empty signature
|
|
48
87
|
- [HTTP error detail is withheld from clients in production](http-error-detail-is-withheld-from-clients-in-production.md) — 5xx bodies carry only a trace id in production; exposeErrors can widen that in development but never in production
|
|
49
88
|
- [HTTP request bodies are bounded before they are buffered](http-request-bodies-are-bounded-before-they-are-buffered.md) — Content-Length is rejected up front and the stream is measured as it arrives, because that header is optional and attacker-controlled
|
|
89
|
+
- [Local content refuses private buckets under the public segment and paths outside the root](local-content-refuses-private-buckets-under-the-public-segment.md) — The local content path resolver returns no target for a private bucket whose first segment is the public URL segment, for NUL bytes, and for any path resolving outside its visibility root
|
|
90
|
+
- [Mail reaches a virtual user run only after sender and link-origin allowlisting](mail-is-allowlisted-before-a-virtual-user-run-sees-it.md) — applyMailboxAllowlist keeps only mail from listed senders and drops links to unlisted origins; the engine ships no mailbox, so every implementation must call this helper
|
|
50
91
|
- [MCP internal error details are double-gated on production](mcp-internal-error-details-are-double-gated-on-production.md) — exposeErrors is checked again against isProduction() at throw time so an explicit true cannot leak stack traces from a production build
|
|
92
|
+
- [MCP resources, tools and prompts take sessionless functions, because an MCP client is not a logged-in user](mcp-resources-tools-and-prompts-take-sessionless-functions.md) — The MCP wiring types default each func to CorePikkuFunctionSessionless, so an MCP entry point is declared without a session type; whether a session is required is decided per function by the runner
|
|
93
|
+
- [Origin-locked routes accept only an exact origin match and refuse a missing one](origin-locked-routes-match-the-origin-exactly.md) — requireOrigin rejects a request whose Origin, or Referer when Origin is absent, is not the host's own origin or a configured origin, compared by equality
|
|
94
|
+
- [Open-source auth works without Pikku Studio Cloud](oss-auth-works-without-pikku-studio-cloud.md) — Every virtual-user sign-in path works in an app that runs no Pikku Studio Cloud code; the signing key is the project's own, never a cloud-issued credential
|
|
95
|
+
- [Outbound webhooks use an allowlist when one is configured, and block private hosts otherwise](outbound-webhooks-use-an-allowlist-when-configured.md) — The webhook delivery reads allowedHosts from config and passes it to safeFetch; without an allowlist safeFetch refuses private hosts, and an allowlist replaces that check
|
|
51
96
|
- [Passphrases are stretched, key material is expanded](passphrases-are-stretched-key-material-is-expanded.md) — PBKDF2 guards low-entropy human input; HKDF derives from high-entropy secrets, and the two are never swapped
|
|
52
97
|
- [Auth filtering requires live permission functions, never their metadata](permission-auth-filtering-requires-live-permission-functions.md) — checkAuthPermissions collects pikkuAuth-branded predicates off the real config; passing metadata would let every gated tool through
|
|
53
|
-
- [
|
|
98
|
+
- [Core carries virtual user roles as data and provisioning grants them](pikku-carries-virtual-user-scopes-as-data-and-the-app-grants-them.md) — roles on a resolved virtual user are transported by core, never applied there — provisionVirtualUsers grants them in the running stage, through the scope service the app wired
|
|
54
99
|
- [Queue job identities are signed at enqueue](queue-job-identities-are-signed-at-enqueue.md) — A job's pikkuUserId is an HMAC claim bound to the queue and payload; an unverifiable claim is dropped, never trusted
|
|
55
100
|
- [Queue jobs carry the producer's pikku user id](queue-jobs-carry-the-producers-pikku-user-id.md) — A job's pikkuUserId is trusted as identity by the worker, so enqueue rights are effectively act-as-user rights
|
|
56
101
|
- [Remote addon tokens are client credentials, not mesh trust](remote-addon-tokens-are-client-credentials-not-mesh-trust.md) — wireRemoteAddon authenticates as a client to a hosted library and fails closed on an empty token; it never uses PIKKU_REMOTE_SECRET
|
|
57
|
-
- [A
|
|
58
|
-
- [A scaffolded surface is authenticated unless the config opts out in writing](scaffold-features-are-authenticated-unless-opted-out.md) — SUPERSEDED; retains why the legacy 'auth' | 'no-auth' strings are refused rather than coerced
|
|
102
|
+
- [A scaffolded surface is authenticated unless the config opts out in writing](scaffold-features-are-authenticated-unless-opted-out.md) — scaffold.<feature> became boolean | { auth, path }, where true means authenticated — going public requires typing { auth: false }, and the legacy 'auth' | 'no-auth' strings are refused rather than coerced
|
|
59
103
|
- [Scenario-step functions are never externally invocable over RPC](scenario-step-functions-are-never-externally-invocable.md) — rpcExposed requires expose and rejects scenarioStep, so test steps stay reachable only from inside a scenario run
|
|
60
104
|
- [Scope resolution happens at the session boundary and scope sync never deletes](scope-resolution-happens-at-the-session-boundary-and-sync-never-deletes.md) — ScopeService is called when a session is built, never by the function runner, and syncScopes only ever adds — revoking is an explicit operation
|
|
61
|
-
- [
|
|
105
|
+
- [Scoped content access is confined to a root folder, with longest-prefix grants outside it](scoped-content-access-is-a-root-folder-with-longest-prefix-grants.md) — ScopedContentService keeps an addon inside its root folder, lets it reach the wider store only through read or write grants matched by longest prefix, and refuses signURL and copyFile
|
|
62
106
|
- [Self-authentication is declared, not detected](self-authentication-is-declared-not-detected.md) — A function that authorizes callers in its own body says so with selfAuthenticated; codegen never tries to infer it
|
|
107
|
+
- [Contexts that must not hold secrets get a services bag whose secrets accessor throws](services-without-secrets-get-a-throwing-secrets-accessor.md) — withoutSecrets replaces the secrets entry with a non-enumerable getter that throws, so code outside the permitted contexts cannot reach the secret service
|
|
108
|
+
- [Shared guards refuse to mutate or shadow a system role, in every store](shared-guards-refuse-to-mutate-or-shadow-a-system-role.md) — system-role-guard holds the guards every ScopeService must call before changing a role, and roleLockReason gives the console a reason to show for a locked role
|
|
63
109
|
- [Signed content URLs bind the request path and verify fail-closed](signed-content-urls-bind-the-request-path.md) — A signature that only carries timestamps authorizes every asset, and a verifier with no key must refuse rather than allow
|
|
110
|
+
- [Signed uploads are verified by the same check as signed reads](signed-uploads-are-verified-by-the-same-check-as-signed-reads.md) — The local content handler refuses an upload whose presigned URL does not verify, using the same exported verifier that signed reads use
|
|
111
|
+
- [System role scopes must be declared with defineScope, or the build fails](system-role-scopes-must-be-declared-with-define-scope.md) — A system role may grant only scope ids that defineScope declares, and the bare wildcard is refused; both are critical inspector diagnostics, so a role cannot grant a scope no function checks
|
|
112
|
+
- [System roles are never deleted by a deploy, but their scope set follows the declaration, and the admin API cannot change them](system-roles-are-additive-and-immutable.md) — syncSystemRoles keeps a role row whose declaration has gone (marked declared false) but replaces a declared role's scope set; deleteRole and setRoleScopes refuse system roles
|
|
113
|
+
- [The channel RPC does not put the caller's session on the wire](the-channel-rpc-does-not-send-the-callers-session.md) — ChannelDeploymentService.invoke accepts a session only to satisfy the DeploymentService shape and never sends it, so the peer runs the capability as itself
|
|
114
|
+
- [The remote jobs inbox rejects every caller when its secret is unset](the-remote-jobs-inbox-fails-closed-without-a-secret.md) — remoteJobsSecret refuses all requests when the dispatch secret variable is empty, and compares the header in constant time when it is set
|
|
115
|
+
- [The remote RPC gate fails closed and binds a token to one function](the-remote-rpc-gate-fails-closed-and-binds-the-token-to-its-function.md) — pikkuRemoteAuthMiddleware refuses a remote-RPC request unless the mesh token is audience pikku-remote, names the requested function, and carries a session that decrypts under the mesh secret
|
|
116
|
+
- [The workflow status stream re-checks run ownership on every poll and closes when access is lost](the-workflow-status-stream-rechecks-ownership-on-every-poll.md) — streamWorkflowRunStatus asserts the run owner on each poll rather than once when the stream opens, and closes the channel when that check throws
|
|
117
|
+
- [Two credentials resolving to one key must agree on whether they are per-user or singleton](two-credentials-on-one-key-must-share-a-read-mode.md) — buildCredentialResolutions throws when two credentials resolve to the same stored key but would be read in different modes, so one cannot silently serve the other's value
|
|
118
|
+
- [Verified bearer claims are pass-through data that core never fills](verified-bearer-claims-are-pass-through-data-that-core-never-fills.md) — PikkuHTTP.authInfo is a typed slot for claims a host has already verified; core declares it and no core source assigns it from request headers
|
|
119
|
+
- [The virtual user catalogue excludes any RPC scoped under virtualUser](virtual-user-catalogue-excludes-the-machinery-that-runs-virtual-users.md) — deriveCatalogue drops every function whose scope begins with virtualUser, so a virtual user holding those scopes cannot start runs, read findings or set schedules through the catalogue
|
|
120
|
+
- [A virtual user's missing approval decision defaults to denied](virtual-user-conversation-missing-approval-decisions-default-to-denied.md) — Every pending tool call gets an explicit decision; an id the virtual user LLM omitted is denied, so a dropped field can never read as consent
|
|
121
|
+
- [Virtual user run records are privileged, and reads are gated behind their own scope](virtual-user-run-records-are-gated-behind-a-read-scope.md) — The generated read RPCs carry virtualUser:read while starting a run carries virtualUser:run, because adversarial findings are working exploits with live ids
|
|
122
|
+
- [Virtual user sign-in endpoints do not exist in production](virtual-user-sign-in-does-not-exist-in-production.md) — The plugin registers the statement, in-process and switcher endpoints only when the sign-in gate is open at construction, and creating a row is dev-only
|
|
123
|
+
- [Virtual user sign-in has one door, reached two ways](virtual-user-sign-in-has-one-door.md) — Only the app's virtual user sign-in handler creates a virtual user session; runs reach it in-process and everything else reaches it with a signed statement
|
|
124
|
+
- [Virtual user sign-in is proven by Set-Cookie, not a non-empty jar](virtual-user-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md) — HttpVirtualUser tracks its own signedIn flag and both VirtualUserSignIn implementations require the sign-in response itself to set a cookie, because a populated jar proves nothing
|
|
125
|
+
- [Virtual user sign-in only works for virtual-user-flagged users](virtual-user-sign-in-only-works-for-virtual-user-flagged-users.md) — A virtual user statement mints sessions for user rows flagged virtual user and nothing else, so holding one never impersonates a real user
|
|
64
126
|
- [Webhook bodies are signed before they are enqueued](webhook-bodies-are-signed-before-they-are-enqueued.md) — QueueWebhookService computes the HMAC at enqueue time so the signing key never travels in the queue payload
|
|
65
|
-
- [
|
|
127
|
+
- [wireAddon expose selects the RPC surface, and a list may widen it](wire-addon-expose-selects-the-rpc-surface.md) — The consuming app decides which addon functions rpc.exposed reaches per instance — unset keeps the addon's own expose, false closes the instance, a list names exactly what is callable — and the deploy analyzer applies the same rule
|
|
66
128
|
- [An approval decision is stored raw and validated on replay, and an invalid one closes the gate rather than failing the run](workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md) — The schema only exists inside the workflow body, and letting an external payload fail the run would let any caller kill a workflow
|
|
129
|
+
- [Workflow helper functions are registered sessionless by declaration](workflow-helper-functions-are-sessionless-by-declaration.md) — Queue-worker helpers and the workflow sleeper get meta with sessionless true, so the runner requires a session for them only where their own auth flag says so
|
|
67
130
|
- [A queued workflow step rehydrates its session from the persisted run wire](workflow-queued-steps-rehydrate-their-session-from-the-run-wire.md) — The queue job payload is just `{ runId }`, so without threading `pikkuUserId` an authed step sees no session and throws
|
|
68
|
-
- [Scenario sessions are isolated per
|
|
69
|
-
- [A scenario step is never registered as a callable RPC and never dispatched on the queue](workflow-scenario-steps-are-never-network-invocable.md) — A step drives a browser and holds
|
|
70
|
-
|
|
131
|
+
- [Scenario sessions are isolated per virtual user and reset between scenarios](workflow-scenario-sessions-are-isolated-per-virtual-user-and-per-scenario.md) — One jar per virtual user keeps two virtual users from sharing a session; a browser reset keeps one scenario from leaving the next signed in as somebody else
|
|
132
|
+
- [A scenario step is never registered as a callable RPC and never dispatched on the queue](workflow-scenario-steps-are-never-network-invocable.md) — A step drives a browser and holds a virtual user's session, so exposing it as an RPC would put that reach on the network
|
|
133
|
+
- [A scenario virtual user step always goes over the real transport and never through internal dispatch](workflow-virtual-user-steps-always-use-the-real-transport.md) — Internal dispatch would bypass auth middleware and permissions, turning a scenario into a green health check that proves nothing
|
|
134
|
+
- [Writing a virtual user schedule is gated by its own scope, separate from running one](writing-a-virtual-user-schedule-is-gated-by-its-own-scope.md) — setVirtualUserSchedule carries virtualUser:schedule, because a schedule write spends money on every future tick while a run spends it once with a caller watching
|
|
71
135
|
<!-- /pikku:knowledge-index -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Local content refuses private buckets under the public segment and paths outside the root
|
|
4
|
+
description: The local content path resolver returns no target for a private bucket whose first segment is the public URL segment, for NUL bytes, and for any path resolving outside its visibility root
|
|
5
|
+
tags: core, content, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Local content refuses private buckets under the public segment and paths outside the root
|
|
9
|
+
|
|
10
|
+
The path resolver in `packages/core/src/services/local-content.ts:88-104` returns
|
|
11
|
+
`null` (no target) in four cases:
|
|
12
|
+
|
|
13
|
+
- the visibility is not one of the known visibilities (`:92`);
|
|
14
|
+
- the bucket or key contains a NUL byte (`:93`);
|
|
15
|
+
- the visibility is `private` and the bucket's first path segment is
|
|
16
|
+
`PUBLIC_URL_SEGMENT` (`:94-96`);
|
|
17
|
+
- the scoped path or the final target resolves outside the visibility root
|
|
18
|
+
(`:98-102`). `inside` accepts only the root itself or a path under `root + sep`.
|
|
19
|
+
|
|
20
|
+
**What this rules out:** reaching private content through the public URL segment,
|
|
21
|
+
and a bucket or key with `..` or an absolute component escaping the root.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Mail reaches a virtual user run only after sender and link-origin allowlisting
|
|
4
|
+
description: applyMailboxAllowlist keeps only mail from listed senders and drops links to unlisted origins; the engine ships no mailbox, so every implementation must call this helper
|
|
5
|
+
tags: core, virtual-users, email, prompt-injection
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Mail reaches a virtual user run only after sender and link-origin allowlisting
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/virtual-user/virtual-user-mailbox.ts` defines the `VirtualUserMailbox`
|
|
11
|
+
interface and the allowlist helpers.
|
|
12
|
+
|
|
13
|
+
The interface doc says implementations "**must** apply `MailboxAllowlist` before
|
|
14
|
+
returning anything", and calls this the primary defence against prompt injection,
|
|
15
|
+
because a virtual user's address can be reached by anyone who learns it
|
|
16
|
+
(`virtual-user-mailbox.ts:40-44`). The engine ships no mailbox: capturing mail is
|
|
17
|
+
platform-specific, so pikku "declares the interface and ships no default"
|
|
18
|
+
(`virtual-user-mailbox.ts:36-38`).
|
|
19
|
+
|
|
20
|
+
`applyMailboxAllowlist` filters by sender and then trims each email's links
|
|
21
|
+
(`virtual-user-mailbox.ts:150-156`). The doc comment says implementations should call
|
|
22
|
+
it rather than filter themselves (`virtual-user-mailbox.ts:146-148`).
|
|
23
|
+
|
|
24
|
+
- Sender: an entry with `@` must match the whole address. A bare domain must
|
|
25
|
+
equal the part after the last `@` (`virtual-user-mailbox.ts:91-101`). The comment
|
|
26
|
+
says a bare domain never matches as a substring (`virtual-user-mailbox.ts:97-98`).
|
|
27
|
+
- Links: with no `linkOrigins`, every link is kept (`virtual-user-mailbox.ts:122-124`).
|
|
28
|
+
Otherwise a link is kept only when its origin is listed, and a link that does not
|
|
29
|
+
parse is dropped (`virtual-user-mailbox.ts:134-140`).
|
|
30
|
+
- Links are filtered rather than the whole email rejected, because a legitimate
|
|
31
|
+
email may carry an unrelated unsubscribe link (`virtual-user-mailbox.ts:109-116`).
|
|
32
|
+
|
|
33
|
+
The raw `body` is optional and is not meant to reach a model context
|
|
34
|
+
(`virtual-user-mailbox.ts:25-29`).
|
|
35
|
+
|
|
36
|
+
**What this rules out:** a virtual user run acting on mail from an unlisted sender, and
|
|
37
|
+
following a link that leaves the origins the stage allowed. Nothing in this file
|
|
38
|
+
checks that an implementation actually calls the helper.
|
package/knowledge/decisions/security/mcp-resources-tools-and-prompts-take-sessionless-functions.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: MCP resources, tools and prompts take sessionless functions, because an MCP client is not a logged-in user
|
|
4
|
+
description: The MCP wiring types default each func to CorePikkuFunctionSessionless, so an MCP entry point is declared without a session type; whether a session is required is decided per function by the runner
|
|
5
|
+
tags: core, mcp, auth, sessionless
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MCP resources, tools and prompts take sessionless functions, because an MCP client is not a logged-in user
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/mcp/mcp.types.ts` types the three MCP wirings the same way. Each one's `func` defaults to `CorePikkuFunctionSessionless`:
|
|
11
|
+
|
|
12
|
+
- `CoreMCPResource`: default at line 92, and the `func` field is commented "It is sessionless: an MCP client is not a logged-in user" at line 113.
|
|
13
|
+
- `CoreMCPTool`: default at line 123, and the same comment at line 138.
|
|
14
|
+
- `CoreMCPPrompt`: default at line 150, and the same comment at line 163.
|
|
15
|
+
|
|
16
|
+
So an MCP entry point is declared without a session-carrying function type. The types do not say whether a particular MCP call needs a session. The per-function declaration does that: `mcpTargetRequiresSession`, described in [An MCP refusal is decided before dispatch, not raised from the tool](an-mcp-refusal-is-decided-before-dispatch.md), reads a `pikkuFunc` as always needing a session, and a sessionless function as needing one only where it sets `auth: true`.
|
|
17
|
+
|
|
18
|
+
**What this rules out:** typing an MCP wiring's `func` as a session-carrying function, which would make the MCP surface look as if it always has a logged-in user behind it.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Origin-locked routes accept only an exact origin match and refuse a missing one
|
|
4
|
+
description: requireOrigin rejects a request whose Origin, or Referer when Origin is absent, is not the host's own origin or a configured origin, compared by equality
|
|
5
|
+
tags: core, middleware, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Origin-locked routes accept only an exact origin match and refuse a missing one
|
|
9
|
+
|
|
10
|
+
`isAllowedOrigin` in `packages/core/src/middleware/require-origin.ts:25-33`
|
|
11
|
+
returns false when the request has no origin (`:30`). It then accepts only an
|
|
12
|
+
equal host origin (`:31`) or an equal configured origin after `toOrigin`
|
|
13
|
+
normalisation (`:32`). There is no suffix or prefix match.
|
|
14
|
+
|
|
15
|
+
The middleware takes `Origin` first and falls back to `Referer` (`:59-61`),
|
|
16
|
+
derives the host origin from `host` and `x-forwarded-proto`, which defaults to
|
|
17
|
+
`https` (`:63-65`), and throws `InvalidOriginError` when the check fails
|
|
18
|
+
(`:72-75`). The doc comment at `:36-42` says this is not CORS: it rejects before
|
|
19
|
+
the function runs, and a missing origin is rejected on purpose.
|
|
20
|
+
|
|
21
|
+
**What this rules out:** matching origins by suffix, and accepting a request that
|
|
22
|
+
carries neither `Origin` nor `Referer`.
|