@pikku/core 0.12.141 → 0.12.142
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 +33 -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/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/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/role/define-system-role.d.ts +1 -1
- package/dist/wirings/role/define-system-role.js +1 -1
- 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-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/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 +46 -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 +34 -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
|
@@ -3,7 +3,7 @@ import type { ScenarioArtifact } from './scenario-run.types.js';
|
|
|
3
3
|
* Scenario steps: named, typed units of scenario behaviour.
|
|
4
4
|
*
|
|
5
5
|
* A step's body is an ordinary pikku function, so it may drive a browser, call
|
|
6
|
-
* an RPC as its
|
|
6
|
+
* an RPC as its virtual user, or run a workflow. `given` and `when` differ only in the
|
|
7
7
|
* prose the reporter renders; `then` also changes what runs, because its
|
|
8
8
|
* bindings are witnesses — every declared surface is observed and the
|
|
9
9
|
* observations must agree. See {@link ScenarioSurfaceResolution}.
|
|
@@ -20,7 +20,7 @@ export type ScenarioStepPhase = 'given' | 'when' | 'then';
|
|
|
20
20
|
/**
|
|
21
21
|
* Who acts in a step.
|
|
22
22
|
*
|
|
23
|
-
* `
|
|
23
|
+
* `virtualUser` is a person driving the system through one of its surfaces — the
|
|
24
24
|
* ordinary `pikkuScenarioStep`. `platform` is the app acting on itself ("the
|
|
25
25
|
* platform has expired the trial"), and `addon` is a third-party system acting
|
|
26
26
|
* on it ("Stripe's webhook arrives"), contributed by the addon that wraps that
|
|
@@ -31,9 +31,9 @@ export type ScenarioStepPhase = 'given' | 'when' | 'then';
|
|
|
31
31
|
* trusting a string literal — and so an addon package exports only addon steps,
|
|
32
32
|
* which is checkable.
|
|
33
33
|
*/
|
|
34
|
-
export type ScenarioStepKind = '
|
|
34
|
+
export type ScenarioStepKind = 'virtualUser' | 'platform' | 'addon';
|
|
35
35
|
/**
|
|
36
|
-
* How
|
|
36
|
+
* How a virtual user drives the system for one step.
|
|
37
37
|
*
|
|
38
38
|
* A step declares one implementation per surface it supports, and the runner
|
|
39
39
|
* picks between them — so the same ladder can run through a real browser, over
|
|
@@ -87,10 +87,10 @@ export type ScenarioSurfaceResolution = {
|
|
|
87
87
|
*/
|
|
88
88
|
export interface ScenarioStepOptions {
|
|
89
89
|
/**
|
|
90
|
-
* The
|
|
90
|
+
* The virtual user this step runs as. Required for a step that declares `virtualUser: true`
|
|
91
91
|
* or a `browser` binding; the runner refuses to dispatch one without it.
|
|
92
92
|
*/
|
|
93
|
-
|
|
93
|
+
virtualUser?: unknown;
|
|
94
94
|
/** Overrides the step's own `description` for this call site only. */
|
|
95
95
|
description?: string;
|
|
96
96
|
/** Defaults to 0 for steps — a failed assertion must not be retried. */
|
|
@@ -110,7 +110,7 @@ export interface ScenarioEnvironment {
|
|
|
110
110
|
/**
|
|
111
111
|
* The `scenarioStep` wire, present on every scenario step invocation.
|
|
112
112
|
*
|
|
113
|
-
* The
|
|
113
|
+
* The virtual user a step runs as is not here: it arrives as `wire.virtualUser`, injected
|
|
114
114
|
* by the runner and typed as present exactly on the bindings that declared they
|
|
115
115
|
* need one. Keeping it off this interface is what makes that possible — a wire
|
|
116
116
|
* member can be required per binding, a property of one cannot.
|
|
@@ -178,10 +178,10 @@ export interface ScenarioScreenshotOptions {
|
|
|
178
178
|
fullPage?: boolean;
|
|
179
179
|
}
|
|
180
180
|
/**
|
|
181
|
-
* One
|
|
181
|
+
* One virtual user's browser session, handed to a step as `wire.browser`.
|
|
182
182
|
*
|
|
183
|
-
* Every
|
|
184
|
-
* `screenshot` files each shot under the
|
|
183
|
+
* Every virtual user gets their own, so a two-virtual-user scenario drives two sessions and
|
|
184
|
+
* `screenshot` files each shot under the virtual user it belongs to.
|
|
185
185
|
*
|
|
186
186
|
* Only what every driver can honour is declared here. A driver package adds
|
|
187
187
|
* the rest by declaration-merging onto this interface — `@pikku/playwright`
|
|
@@ -189,21 +189,21 @@ export interface ScenarioScreenshotOptions {
|
|
|
189
189
|
* structural surface keeps working whichever driver runs it.
|
|
190
190
|
*/
|
|
191
191
|
export interface PikkuBrowserWire {
|
|
192
|
-
/** The
|
|
193
|
-
readonly
|
|
192
|
+
/** The virtual user whose browser context this is */
|
|
193
|
+
readonly virtualUser: string;
|
|
194
194
|
goto(url: string): Promise<void>;
|
|
195
195
|
screenshot(name?: string, options?: ScenarioScreenshotOptions): Promise<Uint8Array>;
|
|
196
196
|
}
|
|
197
197
|
/**
|
|
198
|
-
* What one
|
|
198
|
+
* What one virtual user's window looked like at the moment a scenario failed.
|
|
199
199
|
*
|
|
200
200
|
* A browser step fails with a selector timeout that says nothing about *why*
|
|
201
201
|
* the page never rendered. The answer is almost always in the page's own
|
|
202
202
|
* errors, which the driver has been collecting all along.
|
|
203
203
|
*/
|
|
204
204
|
export interface ScenarioBrowserFailure {
|
|
205
|
-
/** The
|
|
206
|
-
|
|
205
|
+
/** The virtual user whose window this is. */
|
|
206
|
+
virtualUser: string;
|
|
207
207
|
/** Where the window was pointed, when the driver can still report it. */
|
|
208
208
|
url?: string;
|
|
209
209
|
/** Path the screenshot was written to; absent when none could be taken. */
|
|
@@ -222,10 +222,10 @@ export interface ScenarioBrowserFailure {
|
|
|
222
222
|
* one that simply offers no isolation and no diagnostics.
|
|
223
223
|
*/
|
|
224
224
|
export interface ScenarioBrowserProvider {
|
|
225
|
-
/** Resolve — creating on first use — the browser session for
|
|
226
|
-
sessionFor(
|
|
225
|
+
/** Resolve — creating on first use — the browser session for a virtual user. */
|
|
226
|
+
sessionFor(virtualUserName: string): Promise<PikkuBrowserWire>;
|
|
227
227
|
/**
|
|
228
|
-
* Discard every
|
|
228
|
+
* Discard every virtual user's per-scenario state — cookies, storage, open pages —
|
|
229
229
|
* while keeping the browser itself. Called between scenarios, so one
|
|
230
230
|
* scenario cannot leave the next signed in as somebody else.
|
|
231
231
|
*/
|
|
@@ -247,23 +247,23 @@ export interface ScenarioBrowserProvider {
|
|
|
247
247
|
*/
|
|
248
248
|
endScenario?(outcome: 'passed' | 'failed'): void;
|
|
249
249
|
/**
|
|
250
|
-
* When this
|
|
250
|
+
* When this virtual user's recording started, as epoch milliseconds.
|
|
251
251
|
*
|
|
252
252
|
* The seam that keeps the video clock out of `@pikku/core`: a driver knows
|
|
253
253
|
* when it opened the context it passed `recordVideo` to, and the runner turns
|
|
254
|
-
* that into a per-step offset. Absent for
|
|
254
|
+
* that into a per-step offset. Absent for a virtual user with no window open, and
|
|
255
255
|
* for a run recording nothing — both of which leave the step's offset off.
|
|
256
256
|
*/
|
|
257
|
-
videoStartedAt?(
|
|
257
|
+
videoStartedAt?(virtualUserName: string): number | undefined;
|
|
258
258
|
/**
|
|
259
|
-
* Mark a browser step starting in this
|
|
259
|
+
* Mark a browser step starting in this virtual user's recording, answering where it
|
|
260
260
|
* falls in the finished video (ms).
|
|
261
261
|
*
|
|
262
262
|
* Preferred over `videoStartedAt` when present: a driver that edits its
|
|
263
263
|
* footage afterwards — holding each step's screen still, say — is the only
|
|
264
264
|
* one that knows how far that moves the step. Undefined when nothing records.
|
|
265
265
|
*/
|
|
266
|
-
markVideoStep?(
|
|
266
|
+
markVideoStep?(virtualUserName: string): number | undefined;
|
|
267
267
|
/**
|
|
268
268
|
* Snapshot every open window for a failed scenario. `label` identifies the
|
|
269
269
|
* scenario in artifact filenames. Never throws: a failure to capture must
|
|
@@ -193,7 +193,7 @@ export type WorkflowsMeta = Record<string, CommonWireMeta & {
|
|
|
193
193
|
*/
|
|
194
194
|
asserts?: boolean;
|
|
195
195
|
skip?: string;
|
|
196
|
-
|
|
196
|
+
virtualUsers?: string[];
|
|
197
197
|
}>;
|
|
198
198
|
export interface WorkflowRuntimeMeta {
|
|
199
199
|
name: string;
|
|
@@ -201,7 +201,7 @@ export interface WorkflowRuntimeMeta {
|
|
|
201
201
|
source: 'dsl' | 'complex' | 'graph' | 'scenario';
|
|
202
202
|
description?: string;
|
|
203
203
|
tags?: string[];
|
|
204
|
-
|
|
204
|
+
virtualUsers?: string[];
|
|
205
205
|
nodes?: Record<string, any>;
|
|
206
206
|
entryNodeIds?: string[];
|
|
207
207
|
graphHash?: string;
|
|
@@ -14,8 +14,6 @@ here first — several of these exist because the obvious simplification was tri
|
|
|
14
14
|
and broke something.
|
|
15
15
|
|
|
16
16
|
<!-- pikku:knowledge-index -->
|
|
17
|
-
|
|
18
17
|
- [internals](internals/index.md) — a rule about how it works under the hood, and why
|
|
19
18
|
- [security](security/index.md) — a rule about who may do what
|
|
20
|
-
|
|
21
19
|
<!-- /pikku:knowledge-index -->
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A credential name declared twice must agree on its schema and its type
|
|
4
|
+
description: validateAndBuildCredentialDefinitionsMeta throws when two declarations of one credential name differ in schema or in type, and otherwise keeps the first declaration
|
|
5
|
+
tags: core, credentials, validation
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A credential name declared twice must agree on its schema and its type
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/credential/validate-credential-definitions.ts` builds
|
|
11
|
+
the credential metadata, keyed by name.
|
|
12
|
+
|
|
13
|
+
When a name repeats, the schema is compared first, but only if both declarations
|
|
14
|
+
resolve to a schema reference (`validate-credential-definitions.ts:19-23`). Two
|
|
15
|
+
references differ when their variable name or source file differs
|
|
16
|
+
(`validate-credential-definitions.ts:24-27`). A difference throws, and the message
|
|
17
|
+
says "Credentials sharing a name must use the same schema"
|
|
18
|
+
(`validate-credential-definitions.ts:28-33`).
|
|
19
|
+
|
|
20
|
+
The `type` field is then compared (`validate-credential-definitions.ts:38-45`).
|
|
21
|
+
The type is `'singleton' | 'wire'` (`credential.types.ts:26`). A different type
|
|
22
|
+
throws with the message "Credentials sharing a name must use the same type"
|
|
23
|
+
(`validate-credential-definitions.ts:43`).
|
|
24
|
+
|
|
25
|
+
When both checks pass, the repeat is skipped and the first declaration stays
|
|
26
|
+
(`validate-credential-definitions.ts:47`).
|
|
27
|
+
|
|
28
|
+
**What this rules out:** two addons declaring the same credential name with
|
|
29
|
+
different schemas or read modes, and a silent choice between them.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: declareScopes lets an app grant a scope whose addon it does not wire
|
|
4
|
+
description: defineScope is a no-op read by the CLI, and declareScopes declares scope ids so a grant is valid without wiring the addon that owns the scope tree
|
|
5
|
+
tags: core, scopes, addons, codegen
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# declareScopes lets an app grant a scope whose addon it does not wire
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/scope/define-scope.ts` has two declaration functions,
|
|
11
|
+
both with empty bodies.
|
|
12
|
+
|
|
13
|
+
`defineScope` is a no-op that "tree-shakes away". The CLI reads the call by AST and
|
|
14
|
+
generates a `ScopeId` union, so a function that names an undeclared scope fails to
|
|
15
|
+
build (`define-scope.ts:3-6`, `33`).
|
|
16
|
+
|
|
17
|
+
`declareScopes` takes a list of ids (`define-scope.ts:53`). Its doc comment says it
|
|
18
|
+
exists so that an app can grant a scope, in a system role or a function's
|
|
19
|
+
`scopes`, whose tree belongs to an addon the app has not wired
|
|
20
|
+
(`define-scope.ts:36-42`). Each id declares that node and its ancestors
|
|
21
|
+
(`define-scope.ts:44`). When the addon is wired, its own description wins, and the
|
|
22
|
+
two calls "never conflict, unlike a second `defineScope` for the same root"
|
|
23
|
+
(`define-scope.ts:44-46`).
|
|
24
|
+
|
|
25
|
+
The example is `declareScopes(['pikku:console'])` (`define-scope.ts:49-51`).
|
|
26
|
+
|
|
27
|
+
**What this rules out:** declaring the same scope root with `defineScope` in two
|
|
28
|
+
places, which the doc says conflicts, and making a grant valid only by wiring the
|
|
29
|
+
addon that owns it.
|
package/knowledge/decisions/internals/a-scheduled-task-writes-its-caller-session-before-it-runs.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A scheduled task writes its caller's session to the session store before it runs
|
|
4
|
+
description: runScheduledTask stores the session it is passed, or nothing, before it looks up the task, so the run uses only a session its caller supplied
|
|
5
|
+
tags: core, scheduler, sessions
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A scheduled task writes its caller's session to the session store before it runs
|
|
9
|
+
|
|
10
|
+
`runScheduledTask` in `packages/core/src/wirings/scheduler/scheduler-runner.ts:70-87`
|
|
11
|
+
takes an optional `session`. It builds a `PikkuSessionService` on the session store
|
|
12
|
+
(`:84`), and when a session was passed it calls `userSession.set(session)` (`:85-87`).
|
|
13
|
+
Nothing else in the function sets a session. The task and its meta are looked up
|
|
14
|
+
afterwards (`:89-96`), so a missing task still leaves the session written.
|
|
15
|
+
|
|
16
|
+
The in-repo caller of this path, `packages/schedule/src/in-memory-scheduler-service.ts:163`,
|
|
17
|
+
passes only `{ name }`, so a cron run has no session.
|
|
18
|
+
|
|
19
|
+
**What this rules out:** a scheduled run taking a session from anywhere other than
|
|
20
|
+
its caller, and any default system session being set by this function.
|
|
21
|
+
`questions/does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with.md` records a
|
|
22
|
+
related gap in the delayed-RPC path.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A secret set in memory shadows the variable of the same name
|
|
4
|
+
description: LocalSecretService reads its in-memory map before the variables service, stores a SecretValue unwrapped, and treats an empty string as absent
|
|
5
|
+
tags: core, secrets, local-dev
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A secret set in memory shadows the variable of the same name
|
|
9
|
+
|
|
10
|
+
`packages/core/src/services/local-secrets.ts` keeps an in-memory map of secrets.
|
|
11
|
+
`getSecret` checks that map first (`local-secrets.ts:25-29`). Only when the key is
|
|
12
|
+
missing or empty does it fall back to the variables service
|
|
13
|
+
(`local-secrets.ts:31-34`), and it throws if neither has the key
|
|
14
|
+
(`local-secrets.ts:35`). `hasSecret` follows the same order (`local-secrets.ts:49-52`).
|
|
15
|
+
|
|
16
|
+
Values are parsed as JSON when possible and kept as the raw string otherwise
|
|
17
|
+
(`local-secrets.ts:13-19`).
|
|
18
|
+
|
|
19
|
+
`setSecret` unwraps a `SecretValue` before storing it (`local-secrets.ts:38-45`).
|
|
20
|
+
The comment gives the reason: storing the wrapper "would serialize it to
|
|
21
|
+
'[secret]'" (`local-secrets.ts:39`).
|
|
22
|
+
|
|
23
|
+
`deleteSecret` removes only the in-memory entry (`local-secrets.ts:56-58`), so a
|
|
24
|
+
variable of the same name becomes visible again after a delete.
|
|
25
|
+
|
|
26
|
+
**What this rules out:** a secret written through `setSecret` being replaced by
|
|
27
|
+
the value of a variable with the same name, and storing a redacted wrapper in
|
|
28
|
+
place of the secret.
|
package/knowledge/decisions/internals/a-virtual-user-acts-through-invokeraw-so-a-refusal-is-data.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user acts through invokeRaw, so a refusal is data it can act on
|
|
4
|
+
description: virtualUserTarget calls virtualUser.invokeRaw, which signs the virtual user in when needed, retries once after a 401, and returns the status instead of throwing
|
|
5
|
+
tags: core, virtual-users
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user acts through invokeRaw, so a refusal is data it can act on
|
|
9
|
+
|
|
10
|
+
`virtualUserTarget` in `packages/core/src/wirings/virtual-user/virtual-user-target.ts`
|
|
11
|
+
is "the only place the engine meets the network" (`virtual-user-target.ts:14`). It
|
|
12
|
+
drives a signed-in virtual user. The doc comment says the virtual user keeps its
|
|
13
|
+
cookies, re-logs in when a session expires mid-run, and reports status as data
|
|
14
|
+
rather than throwing (`virtual-user-target.ts:14-17`).
|
|
15
|
+
|
|
16
|
+
The call goes through `virtualUser.invokeRaw(rpcName, args)` (`virtual-user-target.ts:27`).
|
|
17
|
+
The comment gives the reason: "to a virtual user a 403 is not an error, it is the
|
|
18
|
+
system saying no, which is information it should be allowed to act on"
|
|
19
|
+
(`virtual-user-target.ts:19-22`).
|
|
20
|
+
|
|
21
|
+
In `HttpVirtualUser.invokeRaw`, the virtual user signs in first if it is not signed
|
|
22
|
+
in (`packages/core/src/services/http-virtual-users.ts:117-119`). On a 401 it signs
|
|
23
|
+
in again and retries once (`http-virtual-users.ts:121-126`). The method returns the
|
|
24
|
+
response for every status.
|
|
25
|
+
|
|
26
|
+
The virtual user's target passes no per-call options, so it cannot add a header
|
|
27
|
+
(`virtual-user-target.ts:27`). That is covered in
|
|
28
|
+
[a scenario step can name an identity no virtual user holds, through per-call headers](../security/a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md).
|
|
29
|
+
|
|
30
|
+
**What this rules out:** a virtual user calling `invoke`, which throws on error, and
|
|
31
|
+
a virtual user being given per-call headers. For why `invokeRaw` returns status rather
|
|
32
|
+
than throwing, see [in a scenario a 4xx is data, not an exception](in-a-scenario-a-4xx-is-data-not-an-exception.md).
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: decision
|
|
3
3
|
title: A virtual user cadence is a row, not a timer
|
|
4
|
-
description: How often a
|
|
4
|
+
description: How often a virtual user runs is stored as a due time per virtual user and acted on by a tick the project schedules — pikku never starts a timer, and a run never reschedules itself
|
|
5
5
|
tags: virtual-user, storage, scheduling
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# A virtual user cadence is a row, not a timer
|
|
9
9
|
|
|
10
|
-
A run has a budget; a
|
|
10
|
+
A run has a budget; a virtual user has a cadence. The two get confused because both
|
|
11
11
|
answer "how often", and neither answers the other's question: a budget caps one
|
|
12
12
|
outing, and raising it only buys a more tired user. What tells you about a
|
|
13
13
|
product is the same person coming back over a fortnight.
|
|
14
14
|
|
|
15
|
-
That cadence is one row per
|
|
15
|
+
That cadence is one row per virtual user in `virtualUserSchedule`, holding
|
|
16
16
|
`nextRunAt`. `tickVirtualUserSchedules` acts on whichever rows are due. There is
|
|
17
17
|
no timer, no interval, and no in-memory loop.
|
|
18
18
|
|
|
19
19
|
**Not a timer**, because a process holding the next run in its own heap forgets
|
|
20
|
-
it on the next deploy, and the
|
|
20
|
+
it on the next deploy, and the virtual user silently stops — with nothing anywhere
|
|
21
21
|
saying it used to run. The row survives restarts, and any instance can act on
|
|
22
22
|
it.
|
|
23
23
|
|
|
24
24
|
**Not reschedule-on-completion**, which is the tempting shape: finish a run,
|
|
25
25
|
draw a delay, schedule the next. It has exactly one failure mode and it is
|
|
26
|
-
fatal — a crash between the two ends the
|
|
26
|
+
fatal — a crash between the two ends the virtual user forever, and the evidence is an
|
|
27
27
|
absence. A due time written down before the run starts cannot be lost by the run
|
|
28
28
|
failing.
|
|
29
29
|
|
|
@@ -36,8 +36,8 @@ may not even run schedulers. One line in the project turns it on:
|
|
|
36
36
|
wireScheduler({ name: 'virtualUsers', schedule: '0 * * * *', func: tickVirtualUserSchedules })
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Tick resolution bounds how *late* a due
|
|
40
|
-
|
|
39
|
+
Tick resolution bounds how *late* a due virtual user is, never how often it runs — a
|
|
40
|
+
virtual user due at 09:07 under an hourly tick starts at 10:00. Running the tick more
|
|
41
41
|
often costs one indexed query and changes no cadence.
|
|
42
42
|
|
|
43
43
|
Three rules make a tick safe to run at any resolution, from any number of
|
|
@@ -46,14 +46,14 @@ instances:
|
|
|
46
46
|
- **The due time is written before the run is dispatched**, so a tick that dies
|
|
47
47
|
halfway cannot leave the row due for the next one to pick up again. A dispatch
|
|
48
48
|
that throws therefore waits a full interval, which is the right way round: a
|
|
49
|
-
|
|
50
|
-
- **A
|
|
49
|
+
virtual user failing to start should not be retried every minute for a week.
|
|
50
|
+
- **A virtual user whose previous run is still `running` is skipped, not queued.**
|
|
51
51
|
Two copies of the same user acting at once is not a heavier test, it is a
|
|
52
52
|
different one, and every finding it produces is unreproducible.
|
|
53
|
-
- **A run still `running` after `STALE_RUN_AFTER_MS` is failed and the
|
|
53
|
+
- **A run still `running` after `STALE_RUN_AFTER_MS` is failed and the virtual user
|
|
54
54
|
runs again.** This is where the stranded-record cost of
|
|
55
55
|
[a virtual user run not being a workflow](a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md)
|
|
56
|
-
gets paid: without it, one restart mid-run would block that
|
|
56
|
+
gets paid: without it, one restart mid-run would block that virtual user's schedule
|
|
57
57
|
permanently.
|
|
58
58
|
|
|
59
59
|
The interval is a range (`minIntervalMs`, `maxIntervalMs`), drawn per run. A
|
|
@@ -63,4 +63,4 @@ cron neighbourhood; a real one does not keep an appointment.
|
|
|
63
63
|
**What this rules out:** a `setTimeout` or interval anywhere in the run path;
|
|
64
64
|
the engine scheduling its own next run; a scaffolded scheduled task; a queue
|
|
65
65
|
holding the next run; and a cadence that lives only in a config file, which
|
|
66
|
-
cannot record when the
|
|
66
|
+
cannot record when the virtual user last actually went.
|
package/knowledge/decisions/internals/a-virtual-user-converse-signs-in-before-its-first-turn.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user's converse signs in before its first turn
|
|
4
|
+
description: HttpVirtualUser.converse calls login() before a run when it holds no session, because a public agent route never triggers the 401 retry that would sign the virtual user in
|
|
5
|
+
tags: services, virtual-users, agents
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user's converse signs in before its first turn
|
|
9
|
+
|
|
10
|
+
`HttpVirtualUser.converse` (`packages/core/src/services/http-virtual-users.ts`)
|
|
11
|
+
calls `this.login()` when `signedIn` is false, before it starts the run. Only
|
|
12
|
+
then does it send the agent request.
|
|
13
|
+
|
|
14
|
+
The code comment gives the reason. A public agent route never returns 401, so
|
|
15
|
+
the 401 retry in `sendAgent` never runs and would never sign the virtual user
|
|
16
|
+
in. An agent thread is also minted under a fresh anonymous id per request when
|
|
17
|
+
no one owns it, so turn one could succeed and turn two be refused as somebody
|
|
18
|
+
else's. A virtual user is a real account with real credentials, so conversing as
|
|
19
|
+
nobody is not the intent.
|
|
20
|
+
|
|
21
|
+
**What this rules out:** relying on the 401 retry for `converse`, and running a
|
|
22
|
+
virtual user's conversation unauthenticated when it was meant to sign in.
|
|
23
|
+
Compare [the 401-only retry](scenario-agent-calls-sign-in-on-401-only.md), which is
|
|
24
|
+
how the other agent calls behave.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user's email is computed from its id, with the run id as a sub-address
|
|
4
|
+
description: virtualUserEmail derives the address from the virtual user id and a real domain, appends the run id so concurrent runs get separate inboxes and accounts, and refuses colliding addresses
|
|
5
|
+
tags: core, virtual-users, email, runs
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user's email is computed from its id, with the run id as a sub-address
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/virtual-user/virtual-user-email.ts` computes the address a virtual user
|
|
11
|
+
reads mail at. Nothing is declared by hand.
|
|
12
|
+
|
|
13
|
+
The label is the virtual user id, lower-cased, with runs of non-alphanumerics turned
|
|
14
|
+
into `-` and leading or trailing dashes removed (`virtual-user-email.ts:19-30`). A
|
|
15
|
+
virtual user id with no letters or digits left throws (`virtual-user-email.ts:24-28`).
|
|
16
|
+
|
|
17
|
+
The domain must contain a dot and no `@` (`virtual-user-email.ts:49-54`). The error
|
|
18
|
+
asks for "a domain the stage actually receives mail on" (`virtual-user-email.ts:52`).
|
|
19
|
+
|
|
20
|
+
With a `runId`, the address is `label+runIdLabel@domain` (`virtual-user-email.ts:55-56`).
|
|
21
|
+
The doc comment says that without the suffix, two concurrent runs share "an inbox
|
|
22
|
+
_and_ a user row, and run B reads run A's magic link" (`virtual-user-email.ts:35-37`).
|
|
23
|
+
Without a `runId`, the bare form is returned (`virtual-user-email.ts:40-41`).
|
|
24
|
+
|
|
25
|
+
`virtualUserEmails` throws when two virtual users compute the same address
|
|
26
|
+
(`virtual-user-email.ts:76-82`). The comment says they "would share a user row and read
|
|
27
|
+
each other's email" (`virtual-user-email.ts:79-80`).
|
|
28
|
+
|
|
29
|
+
All of one virtual user's logins share one address (`virtual-user-email.ts:62-66`). The
|
|
30
|
+
comment gives the reason: better-auth keeps `email` on the `user` and defaults
|
|
31
|
+
`allowDifferentEmails` to false.
|
|
32
|
+
|
|
33
|
+
**What this rules out:** two concurrent runs sharing an inbox, and two virtual users
|
|
34
|
+
mapping to one account.
|
package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md
CHANGED
|
@@ -30,7 +30,7 @@ unit: there is no in-process promise to leave running, and `executeVirtualUserRu
|
|
|
30
30
|
— sessionless, unexposed, wired to nothing — is not a function any unit can be
|
|
31
31
|
reached at. The RPC resolves to nothing, the rejection is swallowed by the
|
|
32
32
|
`catch` that exists to stop it taking the process down, and the run parks at
|
|
33
|
-
`running` with zero steps and no error anywhere. That is what a
|
|
33
|
+
`running` with zero steps and no error anywhere. That is what a cloud stage did.
|
|
34
34
|
|
|
35
35
|
So the queue is not bought for durability. It is bought because **a trigger is
|
|
36
36
|
what makes a function deployable**: `wireQueueWorker` puts `executeVirtualUserRun`
|
|
@@ -56,7 +56,7 @@ wants the same exploration.
|
|
|
56
56
|
|
|
57
57
|
Where that rule is actually applied is
|
|
58
58
|
[the schedule tick](a-virtual-user-cadence-is-a-row-not-a-timer.md), which has
|
|
59
|
-
to: a record stuck at `running` would otherwise block its
|
|
59
|
+
to: a record stuck at `running` would otherwise block its virtual user's cadence
|
|
60
60
|
forever.
|
|
61
61
|
|
|
62
62
|
**What this rules out:** dispatching the run through `startWorkflow`; awaiting
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A reachable agent is offered by its registry key, filtered by the virtual user's scopes
|
|
4
|
+
description: reachableAgents offers an agent to a virtual user under the key it is registered with, and only when the virtual user holds the agent's declared scopes; the server still enforces access
|
|
5
|
+
tags: core, virtual-users, agents, scopes
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A reachable agent is offered by its registry key, filtered by the virtual user's scopes
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/virtual-user/virtual-user-agents.ts` decides which agents
|
|
11
|
+
a virtual user is told about.
|
|
12
|
+
|
|
13
|
+
An agent with no declared scopes is offered to everybody (`virtual-user-agents.ts:54-55`).
|
|
14
|
+
The doc comment says undeclared is not the same as denied (`virtual-user-agents.ts:45-46`).
|
|
15
|
+
Otherwise the agent is offered only when the virtual user's scopes satisfy the agent's
|
|
16
|
+
scopes (`virtual-user-agents.ts:57`, `hasScopes`).
|
|
17
|
+
|
|
18
|
+
The name offered is the registry key, not the agent's display name. The code uses
|
|
19
|
+
`name: id` (`virtual-user-agents.ts:59-60`), and the doc comment says the display
|
|
20
|
+
name "is frequently something else entirely", and offering it makes the run die
|
|
21
|
+
on a 500 (`virtual-user-agents.ts:30-37`).
|
|
22
|
+
|
|
23
|
+
The doc comment says this filter narrows only what is offered. The server decides
|
|
24
|
+
who may talk to which agent (`virtual-user-agents.ts:39-44`).
|
|
25
|
+
|
|
26
|
+
**What this rules out:** offering an agent name the server cannot resolve, and
|
|
27
|
+
treating the offer list as the access check.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An OAuth2 credential implies its app client secret, optional and overridable by declaration
|
|
4
|
+
description: deriveOAuth2AppSecrets creates the app client id and secret for each OAuth2 credential, marked optional, unless the project already declares that secret id
|
|
5
|
+
tags: core, secrets, credentials, oauth2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An OAuth2 credential implies its app client secret, optional and overridable by declaration
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/secret/derive-oauth2-app-secrets.ts` adds the app's
|
|
11
|
+
client id and secret for each OAuth2 credential a project declares. The doc comment
|
|
12
|
+
says the pair has the same shape every time, so writing it by hand "bought nothing"
|
|
13
|
+
and was often skipped (`derive-oauth2-app-secrets.ts:7-12`).
|
|
14
|
+
|
|
15
|
+
- A secret id that is already declared is skipped, so an explicit declaration
|
|
16
|
+
wins and keeps its own description or `docsUrl` (`derive-oauth2-app-secrets.ts:25`,
|
|
17
|
+
`31`, and the comment at `derive-oauth2-app-secrets.ts:14-15`).
|
|
18
|
+
- The derived secret has `optional: true` (`derive-oauth2-app-secrets.ts:38-39`).
|
|
19
|
+
The comment says an addon that also authenticates by API key "must still deploy
|
|
20
|
+
without an OAuth app configured" (`derive-oauth2-app-secrets.ts:17-19`).
|
|
21
|
+
- The OAuth2 settings (token secret, URLs, scopes, PKCE, extra params) are copied
|
|
22
|
+
from the credential (`derive-oauth2-app-secrets.ts:41-49`).
|
|
23
|
+
|
|
24
|
+
**What this rules out:** requiring a deployment to configure an OAuth app for a
|
|
25
|
+
credential that is not used, and a derived secret overriding one the author
|
|
26
|
+
declared.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Analytics identity resolvers run in declared order so consent is read before a cookie is minted
|
|
4
|
+
description: composeAnalyticsIdentity threads each resolver's output into the next, so a cookie-minting resolver sees the consent that an earlier resolver read
|
|
5
|
+
tags: core, analytics, consent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Analytics identity resolvers run in declared order so consent is read before a cookie is minted
|
|
9
|
+
|
|
10
|
+
`composeAnalyticsIdentity` in `packages/core/src/analytics/compose-analytics-identity.ts:22-35`
|
|
11
|
+
runs the resolvers in the order given. Each one receives the accumulated result of
|
|
12
|
+
the ones before it (`:26`, `:29`), and its output is merged over that result (`:31-33`).
|
|
13
|
+
|
|
14
|
+
The doc comment at `:8-15` states the ordering rule: a resolver that mints a cookie
|
|
15
|
+
may do so only once consent has been read, so the consent reader has to come first.
|
|
16
|
+
A later resolver wins a key it sets (`:17-18`).
|
|
17
|
+
|
|
18
|
+
The cookie-side rule is in `a-first-party-cookie-is-written-only-after-its-purposes-consent.md`.
|
|
19
|
+
That note does not record the order in which the resolvers are composed.
|
|
20
|
+
|
|
21
|
+
**What this rules out:** running a cookie-minting resolver ahead of the consent
|
|
22
|
+
reader, and merging independent resolver results so that no resolver sees another's output.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Feature flag declarations refuse an empty anyOf and conflicting scopes
|
|
4
|
+
description: validateAndBuildFeatureFlagDefinitionsMeta throws on an anyOf that is an empty list, and on a repeated flag name whose scopes differ between declarations
|
|
5
|
+
tags: core, flags, validation
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Feature flag declarations refuse an empty anyOf and conflicting scopes
|
|
9
|
+
|
|
10
|
+
`validateAndBuildFeatureFlagDefinitionsMeta` in
|
|
11
|
+
`packages/core/src/wirings/flag/validate-flag-definitions.ts:70-98` checks each
|
|
12
|
+
declaration in two ways.
|
|
13
|
+
|
|
14
|
+
An `anyOf` that is an empty array throws (`:78-84`). The message says an empty
|
|
15
|
+
list satisfies nobody, and that a pure switch should omit `anyOf`.
|
|
16
|
+
|
|
17
|
+
When a flag name is declared twice, the two `anyOf` lists are compared as sorted
|
|
18
|
+
JSON (`:86-90`). A difference throws (`:91-97`), and the message says flags sharing
|
|
19
|
+
a name must name the same scopes.
|
|
20
|
+
|
|
21
|
+
**What this rules out:** a flag that no session can satisfy, and two declarations
|
|
22
|
+
of one flag that disagree about who may see it.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Feature flags fail open on availability and never enforce access
|
|
4
|
+
description: A flag with no config row is available by default, anyOf decides only the advisory capable half, and the runner's assertFeatureAvailable checks availability alone
|
|
5
|
+
tags: core, flags, authorization
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Feature flags fail open on availability and never enforce access
|
|
9
|
+
|
|
10
|
+
`resolveFlag` in `packages/core/src/wirings/flag/resolve-flag.ts:54-95`
|
|
11
|
+
separates two halves. `capable` is true when `anyOf` is absent, and otherwise
|
|
12
|
+
comes from `hasAnyScope` on the session's scopes (`:68-69`). If the flag has no
|
|
13
|
+
config row, the result is `available: true` (`:72-74`). The doc comment at `:54-59`
|
|
14
|
+
says this fail-open is safe only because `capable` comes from the session and
|
|
15
|
+
`scopes:` is not changed by a flag.
|
|
16
|
+
|
|
17
|
+
`FlagState` in `packages/core/src/wirings/flag/flag.types.ts:97-104` says the same
|
|
18
|
+
thing: `available` is the only half the runner enforces, and `capable` is advisory
|
|
19
|
+
and "protects nothing". `flag.types.ts:14-18` says `anyOf` is an OR over scopes,
|
|
20
|
+
deliberately the opposite of `scopes`, which is an AND.
|
|
21
|
+
|
|
22
|
+
`assertFeatureAvailable` in `packages/core/src/wirings/flag/assert-feature-available.ts`
|
|
23
|
+
calls `resolveFlag` and throws only when `available` is false (`:28-30`). It does
|
|
24
|
+
nothing when no feature-flag source is registered (`:24-26`). The doc comment at
|
|
25
|
+
`:10-12` says enforcing `capable` here would make a second authorization path.
|
|
26
|
+
|
|
27
|
+
**What this rules out:** using a flag as an access check, and a store outage
|
|
28
|
+
turning every flagged feature off.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Flag declaration sync rewrites only the description and anyOf
|
|
4
|
+
description: The FeatureFlagStore contract re-syncs a flag's description and anyOf on deploy and leaves enabled, rollout and overrides to the operator
|
|
5
|
+
tags: core, flags, services
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Flag declaration sync rewrites only the description and anyOf
|
|
9
|
+
|
|
10
|
+
The `FeatureFlagStore` interface in `packages/core/src/services/feature-flag-service.ts:79-90`
|
|
11
|
+
declares `syncFlags`. Its doc comment says a flag's `description` and `anyOf` are
|
|
12
|
+
re-synced on each deploy, because that is the declaration's whole content. It also
|
|
13
|
+
says `enabled`, the rollout and every override belong to the operator and are never
|
|
14
|
+
touched.
|
|
15
|
+
|
|
16
|
+
Lines `:80-83` say a removed declaration leaves its row marked `declared: false`
|
|
17
|
+
rather than deleting the operator's switch.
|
|
18
|
+
|
|
19
|
+
Core defines only the interface here. The implementing store is not in this
|
|
20
|
+
package, so the rule is the contract's, and was not verified against an
|
|
21
|
+
implementation.
|
|
22
|
+
|
|
23
|
+
**What this rules out:** a deploy overwriting an operator's enabled state, rollout
|
|
24
|
+
or overrides, and deleting a flag row because its declaration was removed.
|
|
@@ -7,15 +7,15 @@ tags: services
|
|
|
7
7
|
|
|
8
8
|
# In a scenario a 4xx is data, not an exception
|
|
9
9
|
|
|
10
|
-
`postScenarioJson` (`packages/core/src/services/
|
|
11
|
-
`
|
|
10
|
+
`postScenarioJson` (`packages/core/src/services/virtual-users-service.ts`) and
|
|
11
|
+
`ScenarioVirtualUser.invokeRaw` return a `ScenarioHttpResponse` for every status. Only
|
|
12
12
|
`invoke` throws, and it throws with a truncated body — which is why a scenario
|
|
13
13
|
asserting on a refusal must use `invokeRaw`.
|
|
14
14
|
|
|
15
15
|
A permissions or scopes scenario _expects_ the 403. The status is the assertion,
|
|
16
16
|
and the body usually names the scope that was missing, so both have to survive as
|
|
17
17
|
data rather than being reduced to a thrown `Error`. These helpers also exist
|
|
18
|
-
because every scenario reaching past
|
|
18
|
+
because every scenario reaching past a virtual user was hand-writing the same
|
|
19
19
|
`content-type`, `JSON.stringify` and drain — and the copies had drifted: several
|
|
20
20
|
returned `res.json()`, which discards the status entirely and throws outright on
|
|
21
21
|
an empty body or an HTML error page.
|