diff --git a/run.json b/run.json index 174375852..1fe58f2f1 100644 --- a/run.json +++ b/run.json @@ -503,7 +503,7 @@ "kind": "running" }, "status_updated_at": "2026-05-27T03:13:05.519980Z", - "last_event_at": "2026-05-27T05:06:52.792066Z", + "last_event_at": "2026-05-27T05:24:20.662869Z", "pending_control": null, "checkpoints": [ { @@ -787,9 +787,9 @@ } }, { - "seq": 0, + "seq": 1735, "checkpoint": { - "timestamp": "2026-05-27T05:06:52.897462Z", + "timestamp": "2026-05-27T05:06:57.697233Z", "current_node": "implement", "completed_nodes": [ "start", @@ -800,31 +800,182 @@ ], "node_retries": {}, "context_values": { + "failure_class": "", + "internal.thread_id": "preflight_lint", + "thread.start.current_node": "toolchain", + "internal.retry_count.preflight_lint": 0, + "internal.fidelity": "compact", + "internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A", + "response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.", + "thread.toolchain.current_node": "preflight_compile", + "graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option`, no nullable OpenAPI fields, no legacy deserialization defaults, and no fallback creator in projection code.\n\nTwo commits, in order.\n\n---\n\n## Commit 1 - Remove `Principal::Anonymous`\n\nBreaking cleanup. `Principal` becomes actor-only. Missing/invalid auth is represented as absent request principal, not as an anonymous principal variant.\n\n### Rust\n\n`lib/crates/fabro-types/src/principal.rs`:\n- Drop `Anonymous`.\n- Drop `Anonymous` arms in `kind()` and `display()`.\n- Delete anonymous serialization/round-trip test coverage.\n\n`lib/crates/fabro-server/src/principal_middleware.rs`:\n- `RequestAuthContext.principal: Principal` -> `Option`.\n- `RequestAuthLogContext.principal: Principal` -> `Option`.\n- `initial()` and `rejected()` set `principal: None`.\n- `authenticated(...)`, `authenticated_worker(...)`, and `authenticated_user(...)` set `principal: Some(...)`.\n- Update `principal_without_log_unused_fields` to preserve `None` and strip user avatar data only inside `Some(Principal::User(...))`.\n- Update all gate helpers to match `Option`:\n - `require_user`\n - `require_authenticated_user`\n - `require_run_management_actor`\n - `require_worker_or_user_for_run`\n - `require_run_management_target`\n- `None` routes to the existing `auth_rejection(context.auth_status, context.auth_error_code)` behavior.\n- `Some(Principal::Worker { .. })` keeps the current forbidden-vs-auth-rejection distinctions.\n- Update tests that assert the initial/rejected principal to assert `None`.\n\n`lib/crates/fabro-server/src/server.rs` HTTP logging:\n- Keep the `principal_kind` field on every HTTP log line.\n- Compute `principal_kind` as `auth_context.principal.as_ref().map(Principal::kind).unwrap_or(\"none\")`.\n- Match `auth_context.principal` as an `Option`:\n - `Some(User(...))`, `Some(Worker { ... })`, `Some(Webhook { ... })`, `Some(Slack { ... })` keep their extra fields.\n - `None | Some(Agent { .. } | System { .. })` emits only the common HTTP fields.\n\n`docs/internal/logging-strategy.md`:\n- Replace the `anonymous` HTTP caller category guidance with `none` for requests that have no principal.\n- Keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state.\n\n### OpenAPI and generated clients\n\n`docs/public/api-reference/fabro-api.yaml`:\n- Remove `PrincipalAnonymous` from the `Principal` `oneOf`.\n- Remove `anonymous` from the `Principal` discriminator mapping.\n- Delete the `PrincipalAnonymous` schema.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nExpected generated cleanup:\n- `lib/packages/fabro-api-client/src/models/principal-anonymous.ts` disappears.\n- `Principal` union no longer includes `{ kind: \"anonymous\" }`.\n- `lib/packages/fabro-api-client/src/models/index.ts` no longer exports `principal-anonymous`.\n\n### Frontend\n\n`apps/fabro-web/app/lib/principal-display.tsx`:\n- Remove the `\"anonymous\"` switch case and unused icon import.\n\n`apps/fabro-web/app/components/run-summary-panel.test.tsx` and API-client exhaustiveness tests:\n- Remove anonymous principal cases.\n\n### Documentation sweep\n\nRemove anonymous-principal references from product/API docs and tests. Be careful not to touch unrelated uses of \"anonymous\" such as telemetry anonymous IDs or Git's `remote_anonymous` API.\n\nUseful sweep:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|kind: 'anonymous'|kind: \\\"anonymous\\\"|anonymous actor|anonymous subject|principal_kind.*anonymous|\\\"anonymous\\\"\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cd apps/fabro-web && bun run typecheck && bun test`\n- Manual: start `fabro server start`, hit a protected endpoint without a token, confirm 401 and an HTTP log with `principal_kind=\"none\"` and `auth_status=\"missing\"`.\n\n---\n\n## Commit 2 - Make run provenance and creator non-optional\n\nFull-chain invariant. Every persisted run has exactly one creator principal. No nullable schema fields, no legacy defaults, no projection fallbacks.\n\n### Core type changes\n\n`lib/crates/fabro-types/src/run_summary.rs`:\n- `Run.created_by: Option` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option` -> `RunProvenance`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]` on `RunSpec.provenance`.\n\n`lib/crates/fabro-types/src/run_event/run.rs`:\n- `RunCreatedProps.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n`lib/crates/fabro-workflow/src/event/events.rs`:\n- `Event::RunCreated.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n### Creation and retry flow\n\n`lib/crates/fabro-workflow/src/operations/create.rs`:\n- `CreateRunInput.provenance: Option` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option` -> `RunProvenance`.\n- `RunSpec { provenance }` stores the total provenance directly.\n- `Event::RunCreated { provenance }` emits total provenance directly.\n\n`lib/crates/fabro-server/src/server/handler/runs.rs`:\n- `run_provenance(headers, subject)` returns `RunProvenance { subject: subject.clone(), ... }`.\n- Build provenance before creating `CreateRunInput`.\n\n`lib/crates/fabro-server/src/run_manifest.rs`:\n- Change `create_run_input(...)` to accept `provenance: RunProvenance` and set it directly, or stop using the helper for the final `CreateRunInput` construction. Do not create a temporary input with missing provenance.\n\n`lib/crates/fabro-workflow/src/operations/retry.rs`:\n- `RetryRunInput.provenance: Option` -> `RunProvenance`.\n- `retry_run(...)` writes the new run's `run.created` event with total provenance.\n\n`lib/crates/fabro-server/src/server/handler/lifecycle.rs`:\n- Pass `run_provenance(&headers, &actor)` directly into `RetryRunInput`.\n\n### Event conversion and projections\n\n`lib/crates/fabro-workflow/src/event/convert.rs`:\n- Convert `Event::RunCreated.provenance` into `RunCreatedProps.provenance` directly.\n- Remove `Some(...)` wrapping for run-created provenance.\n\n`lib/crates/fabro-workflow/src/event/stored_fields.rs`:\n- `Event::RunCreated { provenance, .. }` sets `actor: Some(provenance.subject.clone())`.\n\n`lib/crates/fabro-store/src/run_state.rs`:\n- `projection_from_created(...)` builds `RunSpec { provenance: props.provenance.clone(), ... }`.\n- `build_summary(...)` sets `created_by: state.spec.provenance.subject.clone()`.\n- Delete or rewrite tests that deserialize projections with `\"provenance\": null`.\n\n`lib/crates/fabro-types/src/run_projection.rs` and projection tests:\n- Replace all test `RunSpec` literals with total provenance.\n- Remove tests whose only purpose is legacy/null provenance tolerance.\n\n### OpenAPI\n\n`docs/public/api-reference/fabro-api.yaml`:\n- `Run.created_by` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunProvenance.required` includes `subject`.\n- `RunProvenance.subject` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunSpec.required` includes `provenance`.\n- `RunSpec.provenance` references `RunProvenance` directly. Remove `oneOf [..., null]`.\n- If `run.created` event properties are represented separately in the spec, make that event provenance required and non-nullable too.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nDo not hand-edit generated client files.\n\n### Demo mode\n\n`lib/crates/fabro-server/src/demo/mod.rs`:\n- Add a clearly synthetic demo principal using `AuthMethod::DevToken`, not GitHub:\n ```rust\n static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| {\n Principal::user(\n IdpIdentity::new(\"fabro:demo\", \"demo\").unwrap(),\n \"demo\".to_string(),\n AuthMethod::DevToken,\n )\n });\n ```\n- Replace `created_by: None` with `created_by: DEMO_PRINCIPAL.clone()`.\n- If demo creates any full `RunSpec` or `run.created` event data, give it `RunProvenance { subject: DEMO_PRINCIPAL.clone(), ... }`.\n\n### Test support\n\nDo not add fake auth helpers to `fabro_types::fixtures`; that module is run-id constants.\n\nUse the existing `fabro-types` `test-support` feature:\n- Add `#[cfg(any(test, feature = \"test-support\"))] pub mod test_support;` in `lib/crates/fabro-types/src/lib.rs` if it does not already exist.\n- Add `lib/crates/fabro-types/src/test_support.rs` with:\n - `test_principal() -> Principal`\n - `test_run_provenance() -> RunProvenance`\n- Use an obviously fake dev-token identity, e.g. issuer `fabro:test`, subject `test-user`, login `test`.\n- In crates that need the helper from integration tests or cross-crate tests, dual-list `fabro-types` in `dev-dependencies` with `features = [\"test-support\"]`, following existing repo patterns.\n\nUpdate all constructors:\n- Replace `provenance: None` in `RunSpec`, `CreateRunInput`, `RetryRunInput`, `Event::RunCreated`, and `RunCreatedProps` literals with `test_run_provenance()` or a locally meaningful provenance.\n- Replace `subject: Some(...)` with `subject: ...`.\n- Replace `subject: None` only when it is actually `RunProvenance.subject`; leave unrelated todo/commit/message `subject` fields alone.\n- Replace `created_by: None` / `created_by: null` with `test_principal()` or a frontend TS principal fixture.\n- Delete tests that assert nullable or omitted creator/provenance behavior.\n\nRepresentative Rust areas:\n- `lib/crates/fabro-store/src/run_state.rs`\n- `lib/crates/fabro-store/tests/serializable_projection.rs`\n- `lib/crates/fabro-workflow/src/operations/{create,retry,start}.rs`\n- `lib/crates/fabro-workflow/src/event/{convert,sink,stored_fields}.rs`\n- `lib/crates/fabro-workflow/src/handler/**`\n- `lib/crates/fabro-workflow/src/pipeline/**`\n- `lib/crates/fabro-workflow/src/run_{lookup,metadata}.rs`\n- `lib/crates/fabro-server/src/server/tests.rs`\n- `lib/crates/fabro-server/src/server/handler/**`\n- `lib/crates/fabro-server/tests/it/**`\n- `lib/crates/fabro-cli/tests/it/support/mod.rs`\n- `lib/crates/fabro-dump/src/lib.rs`\n- `lib/crates/fabro-tool/src/{common,create,interact,search}.rs`\n- `lib/crates/fabro-api/tests/{principal_round_trip,run_summary_round_trip,run_projection_round_trip,run_event_round_trip}.rs`\n- `lib/crates/fabro-types/tests/{run_spec_serde,run_spec_methods,run_event_serde}.rs`\n\nRepresentative TypeScript areas:\n- `apps/fabro-web/app/**` tests with `created_by: null`\n- `apps/fabro-web/app/data/runs.ts`\n- `apps/fabro-web/app/components/run-summary-panel.tsx`\n- `apps/fabro-web/app/components/runs-list/**`\n- `lib/packages/fabro-api-client/tests/principal-exhaustive.ts`\n\nUseful sweep after edits:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|principal-anonymous|kind: ['\\\"]anonymous|created_by:\\\\s*(None|null)|provenance:\\\\s*None|subject:\\\\s*Some\\\\(|subject:\\\\s*None\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\nReview each hit. The only acceptable remaining matches should be unrelated uses of \"anonymous\" and unrelated non-principal `subject` fields.\n\n### Frontend\n\n`apps/fabro-web/app/components/run-summary-panel.tsx`:\n- `run?.created_by` may still be guarded by `run` loading state, but `created_by` itself is non-null once `run` exists.\n- Pass `run.created_by` directly to `principalDisplay(...)` inside loaded-run branches.\n\n`apps/fabro-web/app/data/runs.ts` and run-list components:\n- Treat `createdBy` as a total principal in UI data derived from a loaded API run.\n- Remove empty/fallback rendering that only existed for missing creator data.\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Manual end-to-end:\n - `fabro server start`\n - `cd apps/fabro-web && bun run dev`\n - Authenticate and create a run through the UI.\n - Confirm `/api/v1/runs/:id` has non-null `created_by`.\n - Confirm `/api/v1/runs/:id/state` has non-null `spec.provenance.subject`.\n - Retry a failed run and confirm the retried run has the retrying user as creator.\n - Hit demo mode with `X-Fabro-Demo: 1` and confirm the run summary renders the synthetic `demo` dev-token user.\n", + "internal.node_visit_count": 1, + "internal.retry_count.start": 0, + "outcome": "succeeded", + "thread.preflight_compile.current_node": "preflight_lint", + "internal.retry_count.preflight_compile": 0, + "internal.work_dir": "/home/daytona/workspace/fabro", + "graph.rankdir": "LR", + "internal.retry_count.toolchain": 0, + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "thread.preflight_lint.current_node": "implement", + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "last_response": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; mis", + "failure_signature": "", + "current_node": "implement", "internal.retry_count.implement": 0, + "last_stage": "implement" + }, + "node_outcomes": { + "toolchain": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c" + }, + "notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", + "usage": null, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 1456, + "active_time_ms": 1456 + } + }, + "preflight_compile": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + }, + "notes": "Script completed: cargo check -q --workspace 2>&1", + "usage": null, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 127963, + "active_time_ms": 127963 + } + }, + "start": { + "status": "succeeded", + "usage": null + }, + "implement": { + "status": "succeeded", + "context_updates": { + "last_stage": "implement", + "response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.", + "last_response": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; mis" + }, + "notes": "Stage completed: implement", + "usage": { + "input": { + "usage": { + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "tokens": { + "input_tokens": 9898125, + "output_tokens": 51088, + "reasoning_tokens": 21958, + "cache_read_tokens": 42159616, + "cache_write_tokens": 0 + } + }, + "facts": { + "algorithm": "openai" + } + }, + "total_usd_micros": 72761813 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/apps/fabro-web/app/lib/test-principal.ts", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs" + ], + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 4850345, + "tool_time_ms": 1392988, + "active_time_ms": 6243333 + } + }, + "preflight_lint": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + }, + "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", + "usage": null, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 138598, + "active_time_ms": 138598 + } + } + }, + "next_node_id": "simplify_opus", + "git_commit_sha": "f12be47f707f3235a97e5bf1814f92084eeb4d45", + "node_visits": { + "preflight_lint": 1, + "preflight_compile": 1, + "toolchain": 1, + "start": 1, + "implement": 1 + } + }, + "diff": { + "patch": "diff --git a/apps/fabro-web/app/components/run-summary-panel.test.tsx b/apps/fabro-web/app/components/run-summary-panel.test.tsx\nindex f93a3fd8a..0a3ff3bbd 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.test.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.test.tsx\n@@ -6,6 +6,7 @@ import {\n RunSummaryPanelView,\n type RunSummaryPanelViewProps,\n } from \"./run-summary-panel\";\n+import { testPrincipal } from \"../lib/test-principal\";\n \n function instanceText(instance: TestRenderer.ReactTestInstance): string {\n const parts: string[] = [];\n@@ -53,7 +54,7 @@ function cellAfterLabel(\n function makeRun(overrides: Record = {}) {\n return {\n id: \"run_1\",\n- created_by: null,\n+ created_by: testPrincipal(),\n diff: null,\n billing: null,\n ...overrides,\n@@ -71,9 +72,8 @@ describe(\"RunSummaryPanelView\", () => {\n }\n });\n \n- test(\"shows unavailable copy for missing run fields after load\", () => {\n+ test(\"shows unavailable copy for missing optional run fields after load\", () => {\n const tree = render({ run: makeRun() });\n- expect(instanceText(cellAfterLabel(tree, \"Created by\"))).toBe(EMPTY_VALUE);\n expect(instanceText(cellAfterLabel(tree, \"Changes\"))).toBe(EMPTY_VALUE);\n expect(instanceText(cellAfterLabel(tree, \"Cost\"))).toBe(EMPTY_VALUE);\n });\n@@ -174,7 +174,7 @@ describe(\"RunSummaryPanelView\", () => {\n kind: \"user\",\n identity: { issuer: \"github\", subject: \"1\" },\n login: \"brynary\",\n- auth_method: \"oauth\",\n+ auth_method: \"github\",\n },\n }),\n });\n@@ -188,7 +188,7 @@ describe(\"RunSummaryPanelView\", () => {\n kind: \"user\",\n identity: { issuer: \"github\", subject: \"1\" },\n login: \"brynary\",\n- auth_method: \"oauth\",\n+ auth_method: \"github\",\n avatar_url: \"https://example.com/brynary.png\",\n },\n }),\n@@ -200,7 +200,7 @@ describe(\"RunSummaryPanelView\", () => {\n });\n \n test(\"renders non-user actor with kind label\", () => {\n- for (const kind of [\"agent\", \"system\", \"slack\", \"webhook\", \"worker\", \"anonymous\"]) {\n+ for (const kind of [\"agent\", \"system\", \"slack\", \"webhook\", \"worker\"]) {\n const tree = render({ run: makeRun({ created_by: { kind } as any }) });\n expect(instanceText(cellAfterLabel(tree, \"Created by\"))).toContain(kind);\n }\ndiff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx\nindex 5d9c78ce4..8e3f6db65 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -92,7 +92,7 @@ export function RunSummaryPanelView({\n artifactsCount,\n artifactsLoading,\n }: RunSummaryPanelViewProps) {\n- const created = run?.created_by ? principalDisplay(run.created_by) : null;\n+ const created = run ? principalDisplay(run.created_by) : null;\n const diff = run?.diff ?? null;\n const cost = formatUsdMicros(run?.billing?.total_usd_micros);\n \ndiff --git a/apps/fabro-web/app/components/runs-list/run-table-row.tsx b/apps/fabro-web/app/components/runs-list/run-table-row.tsx\nindex ca521fb92..6f38e2425 100644\n--- a/apps/fabro-web/app/components/runs-list/run-table-row.tsx\n+++ b/apps/fabro-web/app/components/runs-list/run-table-row.tsx\n@@ -35,6 +35,7 @@ export function RunTableRow({\n }) {\n const lifecycleLabel = listLifecycleStatusLabel(run);\n const statusDisplay = columnStatusDisplay[run.status];\n+ const createdBy = principalDisplay(run.createdBy);\n const show = (col: ToggleableColumn) => !hiddenColumns.has(col);\n \n return (\n@@ -54,14 +55,9 @@ export function RunTableRow({\n \n {show(\"created_by\") && (\n \n- {run.createdBy && (() => {\n- const display = principalDisplay(run.createdBy);\n- return (\n- \n- {display.glyph}\n- \n- );\n- })()}\n+ \n+ {createdBy.glyph}\n+ \n \n )}\n {show(\"repo\") && (\ndiff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts\nindex 98586b2c4..1894ffe27 100644\n--- a/apps/fabro-web/app/data/runs.test.ts\n+++ b/apps/fabro-web/app/data/runs.test.ts\n@@ -8,6 +8,7 @@ import {\n mapRunToRunItem,\n runStatusDisplay,\n } from \"./runs\";\n+import { testPrincipal } from \"../lib/test-principal\";\n \n function makeRun(overrides: Partial = {}): Run {\n return {\n@@ -17,7 +18,7 @@ function makeRun(overrides: Partial = {}): Run {\n workflow: { slug: \"fix_build\", name: \"Fix Build\", graph_name: \"FixBuild\", node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"myrepo\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts\nindex f820b24c1..e5595cf81 100644\n--- a/apps/fabro-web/app/data/runs.ts\n+++ b/apps/fabro-web/app/data/runs.ts\n@@ -40,7 +40,7 @@ export interface RunItem {\n sandboxWorkingDirectory?: string;\n sourceDirectory?: string;\n createdAt?: string;\n- createdBy?: Principal | null;\n+ createdBy: Principal;\n lastEventAt?: string;\n size?: RunSize;\n }\ndiff --git a/apps/fabro-web/app/lib/principal-display.tsx b/apps/fabro-web/app/lib/principal-display.tsx\nindex fa9d4ec16..666f0f64c 100644\n--- a/apps/fabro-web/app/lib/principal-display.tsx\n+++ b/apps/fabro-web/app/lib/principal-display.tsx\n@@ -4,7 +4,6 @@ import {\n ChatBubbleLeftEllipsisIcon,\n Cog6ToothIcon,\n CpuChipIcon,\n- QuestionMarkCircleIcon,\n ServerIcon,\n } from \"@heroicons/react/20/solid\";\n import type { Principal } from \"@qltysh/fabro-api-client\";\n@@ -57,10 +56,5 @@ export function principalDisplay(actor: Principal): PrincipalDisplay {\n return { glyph: principalIconGlyph(), label: \"webhook\" };\n case \"worker\":\n return { glyph: principalIconGlyph(), label: \"worker\" };\n- case \"anonymous\":\n- return {\n- glyph: principalIconGlyph(),\n- label: \"anonymous\",\n- };\n }\n }\ndiff --git a/apps/fabro-web/app/lib/run-actions.test.ts b/apps/fabro-web/app/lib/run-actions.test.ts\nindex 8acde997f..2780ee2ed 100644\n--- a/apps/fabro-web/app/lib/run-actions.test.ts\n+++ b/apps/fabro-web/app/lib/run-actions.test.ts\n@@ -24,6 +24,7 @@ import {\n unarchiveRuns,\n } from \"./run-actions\";\n import { generatedAxios } from \"./api-client\";\n+import { testPrincipal } from \"./test-principal\";\n \n type StubResponseInit = {\n status: number;\n@@ -47,7 +48,7 @@ function makeRun(status: RunStatus, archived = false): Run {\n workflow: { slug: \"fix_build\", name: \"Fix Build\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: null,\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/lib/test-principal.ts b/apps/fabro-web/app/lib/test-principal.ts\nnew file mode 100644\nindex 000000000..7ecf7ad9e\n--- /dev/null\n+++ b/apps/fabro-web/app/lib/test-principal.ts\n@@ -0,0 +1,10 @@\n+import type { Principal } from \"@qltysh/fabro-api-client\";\n+\n+export function testPrincipal(login = \"test\"): Principal {\n+ return {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: `${login}-user` },\n+ login,\n+ auth_method: \"dev_token\",\n+ };\n+}\ndiff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts\nindex e30d38da8..08ecf0411 100644\n--- a/apps/fabro-web/app/routes/run-detail.test.ts\n+++ b/apps/fabro-web/app/routes/run-detail.test.ts\n@@ -178,6 +178,7 @@ import {\n } from \"./run-detail/lifecycle-toasts\";\n \n const { default: RunDetail } = await import(\"./run-detail\");\n+const { testPrincipal } = await import(\"../lib/test-principal\");\n mock.restore();\n type LifecycleToastState = import(\"./run-detail/lifecycle-toasts\").LifecycleToastState;\n type RunDetailActionResult = import(\"./run-detail/lifecycle-toasts\").RunDetailActionResult;\n@@ -209,7 +210,7 @@ function makeRunSummary(\n workflow: { slug: \"default\", name: \"Default\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/run-files.render.test.tsx b/apps/fabro-web/app/routes/run-files.render.test.tsx\nindex 5c9a24722..6a4678d60 100644\n--- a/apps/fabro-web/app/routes/run-files.render.test.tsx\n+++ b/apps/fabro-web/app/routes/run-files.render.test.tsx\n@@ -4,6 +4,7 @@ import TestRenderer, { act } from \"react-test-renderer\";\n import { MemoryRouter, Route, Routes } from \"react-router\";\n \n import { ToastProvider } from \"../components/toast\";\n+import { testPrincipal } from \"../lib/test-principal\";\n \n let currentFilesPayload: any = null;\n let currentCommitsPayload: any = null;\n@@ -50,7 +51,7 @@ mock.module(\"../lib/queries\", () => ({\n workflow: { slug: \"default\", name: \"Default\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/runs.preferences.test.tsx b/apps/fabro-web/app/routes/runs.preferences.test.tsx\nindex 648d8e35a..137a62a78 100644\n--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx\n@@ -4,6 +4,7 @@ import { createMemoryRouter, RouterProvider } from \"react-router\";\n import type { PaginatedRunList, Run } from \"@qltysh/fabro-api-client\";\n \n import { ToastProvider } from \"../components/toast\";\n+import { testPrincipal } from \"../lib/test-principal\";\n import { setupReactTestEnv } from \"../lib/test-utils\";\n \n class MemoryStorage {\n@@ -34,7 +35,7 @@ function run(id: string, repo = \"qlty/fabro\", workflow = \"release\"): Run {\n workflow: { slug: workflow, name: workflow, graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: repo, origin_url: null, provider: \"github\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx\nindex 51483cff2..2e5f65bde 100644\n--- a/apps/fabro-web/app/routes/runs.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.test.tsx\n@@ -11,6 +11,7 @@ import {\n shouldRefreshBoardForEvent,\n } from \"./runs\";\n import { summarizeBatchLifecycleAction } from \"../components/runs-list/batch-lifecycle\";\n+import { testPrincipal } from \"../lib/test-principal\";\n \n function boardRun(id: string, column: BoardColumn, questionText?: string): Run {\n const status =\n@@ -34,7 +35,7 @@ function boardRun(id: string, column: BoardColumn, questionText?: string): Run {\n workflow: { slug: \"test\", name: \"Test\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"repo\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md\nindex 63f6f8f54..0a7c712e6 100644\n--- a/docs/internal/logging-strategy.md\n+++ b/docs/internal/logging-strategy.md\n@@ -118,11 +118,11 @@ Fields are key-value pairs that make events queryable. Include enough context th\n | `error` | Error value on failure |\n | `path` | File system path |\n | `duration_ms` | Elapsed time in milliseconds |\n-| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) |\n+| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) |\n | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) |\n | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests |\n \n-For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`).\n+For HTTP request logs, use the request principal projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`). Requests with no principal use `principal_kind=\"none\"`; keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state.\n \n Server auth intentionally exposes a mutable `RequestAuth` context slot for public auth routes and guard extractors such as `RequiredUser` / `RequireRunScoped` for protected routes. There is no loose `RequestPrincipal` extractor; route-facing extractors should enforce the route's auth contract while the slot supplies the final HTTP log fields.\n | `input_tokens` | Token count for LLM input |\ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex b39bd5f1a..8a49fb17a 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -7863,6 +7863,8 @@ components:\n \n RunProvenance:\n type: object\n+ required:\n+ - subject\n properties:\n server:\n oneOf:\n@@ -7873,9 +7875,7 @@ components:\n - $ref: \"#/components/schemas/RunClientProvenance\"\n - type: \"null\"\n subject:\n- oneOf:\n- - $ref: \"#/components/schemas/Principal\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/Principal\"\n \n Principal:\n oneOf:\n@@ -7885,7 +7885,6 @@ components:\n - $ref: \"#/components/schemas/PrincipalSlack\"\n - $ref: \"#/components/schemas/PrincipalAgent\"\n - $ref: \"#/components/schemas/PrincipalSystem\"\n- - $ref: \"#/components/schemas/PrincipalAnonymous\"\n discriminator:\n propertyName: kind\n mapping:\n@@ -7895,7 +7894,6 @@ components:\n slack: \"#/components/schemas/PrincipalSlack\"\n agent: \"#/components/schemas/PrincipalAgent\"\n system: \"#/components/schemas/PrincipalSystem\"\n- anonymous: \"#/components/schemas/PrincipalAnonymous\"\n \n PrincipalUser:\n type: object\n@@ -7985,15 +7983,6 @@ components:\n system_kind:\n $ref: \"#/components/schemas/SystemActorKind\"\n \n- PrincipalAnonymous:\n- type: object\n- required:\n- - kind\n- properties:\n- kind:\n- type: string\n- enum: [anonymous]\n-\n RunEvent:\n description: >\n Internal RunEvent-compatible JSON payload. The server validates this\n@@ -9121,6 +9110,7 @@ components:\n - run_id\n - settings\n - graph\n+ - provenance\n properties:\n run_id:\n type: string\n@@ -9140,9 +9130,7 @@ components:\n additionalProperties:\n type: string\n provenance:\n- oneOf:\n- - $ref: \"#/components/schemas/RunProvenance\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/RunProvenance\"\n manifest_blob:\n type: [\"string\", \"null\"]\n definition_blob:\n@@ -9454,9 +9442,7 @@ components:\n - $ref: \"#/components/schemas/RepositoryRef\"\n - type: \"null\"\n created_by:\n- oneOf:\n- - $ref: \"#/components/schemas/Principal\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/Principal\"\n origin:\n $ref: \"#/components/schemas/RunOrigin\"\n labels:\ndiff --git a/docs/public/changelog/2026-05-02.mdx b/docs/public/changelog/2026-05-02.mdx\nindex 48a501a59..2d55f8d05 100644\n--- a/docs/public/changelog/2026-05-02.mdx\n+++ b/docs/public/changelog/2026-05-02.mdx\n@@ -11,7 +11,7 @@ The dock listens to interview events and refreshes as questions arrive, so a par\n \n ## Principal attribution and auth routing\n \n-Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, agents, and anonymous actors. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from.\n+Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, and agents. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from.\n \n This also closes attribution gaps across web, CLI, worker-token, Slack, and human-interview paths. Runs created or advanced through different surfaces now preserve who or what took the action more consistently.\n \n@@ -19,7 +19,7 @@ This also closes attribution gaps across web, CLI, worker-token, Slack, and huma\n \n \n - Run specs now include client and server provenance shapes\n-- Run events use unified principal shapes for user, worker, system, Slack, webhook, agent, and anonymous subjects\n+- Run events use unified principal shapes for user, worker, system, Slack, webhook, and agent subjects\n \n \n \ndiff --git a/lib/crates/fabro-api/Cargo.toml b/lib/crates/fabro-api/Cargo.toml\nindex 8b347f032..459e6bfda 100644\n--- a/lib/crates/fabro-api/Cargo.toml\n+++ b/lib/crates/fabro-api/Cargo.toml\n@@ -32,3 +32,6 @@ serde_json = \"1\"\n serde_yaml = \"0.9\"\n prettyplease = \"0.2\"\n syn = \"2\"\n+\n+[dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\ndiff --git a/lib/crates/fabro-api/tests/principal_round_trip.rs b/lib/crates/fabro-api/tests/principal_round_trip.rs\nindex ca1180e60..ac2b2c258 100644\n--- a/lib/crates/fabro-api/tests/principal_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/principal_round_trip.rs\n@@ -118,7 +118,6 @@ fn principal_round_trips_every_variant_through_api_type() {\n Principal::System {\n system_kind: SystemActorKind::Watchdog,\n },\n- Principal::Anonymous,\n ];\n \n for principal in variants {\n@@ -140,9 +139,9 @@ fn run_provenance_subject_round_trips_as_principal() {\n name: Some(\"fabro-cli\".to_string()),\n version: Some(\"0.1.0\".to_string()),\n }),\n- subject: Some(Principal::Worker {\n+ subject: Principal::Worker {\n run_id: fixtures::RUN_1,\n- }),\n+ },\n };\n let json = serde_json::to_value(&provenance).unwrap();\n \ndiff --git a/lib/crates/fabro-api/tests/run_event_round_trip.rs b/lib/crates/fabro-api/tests/run_event_round_trip.rs\nindex e39f3d8f4..8e1cf5d36 100644\n--- a/lib/crates/fabro-api/tests/run_event_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs\n@@ -1,7 +1,7 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunEvent as ApiRunEvent;\n-use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures};\n+use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures, test_support};\n use serde_json::{Value, json};\n \n #[test]\n@@ -20,7 +20,8 @@ fn run_event_round_trips_run_created() {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/fabro/run-1\",\n- \"source_directory\": \"/tmp/fabro/run-1\"\n+ \"source_directory\": \"/tmp/fabro/run-1\",\n+ \"provenance\": test_support::test_run_provenance()\n }\n });\n \n@@ -39,6 +40,7 @@ fn run_event_round_trips_run_created_with_web_url() {\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/fabro/run-1\",\n \"source_directory\": \"/tmp/fabro/run-1\",\n+ \"provenance\": test_support::test_run_provenance(),\n \"web_url\": format!(\"http://localhost:3000/runs/{}\", fixtures::RUN_1)\n }\n });\ndiff --git a/lib/crates/fabro-api/tests/run_projection_round_trip.rs b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\nindex 64a00df91..30295d76a 100644\n--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n@@ -1,7 +1,7 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunProjection as ApiRunProjection;\n-use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings};\n+use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, test_support};\n use serde_json::json;\n \n #[test]\n@@ -130,7 +130,7 @@ fn run_spec_json() -> serde_json::Value {\n workflow_slug: None,\n source_directory: None,\n labels: std::collections::HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-api/tests/run_summary_round_trip.rs b/lib/crates/fabro-api/tests/run_summary_round_trip.rs\nindex 21d237434..1e94c7b1b 100644\n--- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs\n@@ -12,7 +12,7 @@ use fabro_types::{\n AskFabro, AskFabroUnavailableReason, DiffSummary, PullRequestLink, RepositoryProvider,\n RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary, RunId, RunLifecycle,\n RunLinks, RunOrigin, RunRunnableSource, RunSize, RunTimestamps, RunTiming, WorkflowRef,\n- fixtures,\n+ fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -83,7 +83,7 @@ fn run_summary_json_matches_openapi_shape() {\n origin_url: None,\n provider: RepositoryProvider::Unknown,\n }),\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"team\".to_string(), \"core\".to_string())]),\n lifecycle: RunLifecycle {\n@@ -152,7 +152,15 @@ fn run_summary_json_matches_openapi_shape() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n- \"created_by\": null,\n+ \"created_by\": {\n+ \"kind\": \"user\",\n+ \"identity\": {\n+ \"issuer\": \"fabro:test\",\n+ \"subject\": \"test-user\"\n+ },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ },\n \"origin\": {\n \"kind\": \"api\"\n },\n@@ -244,6 +252,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": test_support::test_principal(),\n \"models\": [],\n \"timestamps\": {\n \"created_at\": \"2026-04-20T12:00:00Z\",\n@@ -276,6 +285,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n provider: RepositoryProvider::Unknown,\n })\n );\n+ assert_eq!(summary.created_by, test_support::test_principal());\n assert_eq!(summary.timestamps.started_at, None);\n assert_eq!(summary.timestamps.created_at, created_at);\n assert_eq!(summary.timestamps.last_event_at, None);\ndiff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml\nindex 4bff98f8d..9568ac22a 100644\n--- a/lib/crates/fabro-cli/Cargo.toml\n+++ b/lib/crates/fabro-cli/Cargo.toml\n@@ -129,3 +129,4 @@ fabro-test = { workspace = true }\n fabro-macros = { path = \"../fabro-macros\" }\n hkdf.workspace = true\n reqwest = { workspace = true, features = [\"cookies\"] }\n+fabro-types = { path = \"../fabro-types\", features = [\"clap\", \"test-support\"] }\ndiff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs\nindex c8ad2fff2..8dbb31b35 100644\n--- a/lib/crates/fabro-cli/src/commands/run/attach.rs\n+++ b/lib/crates/fabro-cli/src/commands/run/attach.rs\n@@ -822,6 +822,7 @@ mod tests {\n )]\n \n use fabro_interview::{Answer, AnswerValue};\n+ use fabro_types::test_support;\n use fabro_util::terminal::Styles;\n use httpmock::MockServer;\n \n@@ -840,7 +841,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\nindex ebcf1a2a0..f426c167b 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\n@@ -221,7 +221,21 @@ fn inspect_resolves_selector_via_server_endpoint() {\n \"attrs\": {}\n },\n \"workflow_slug\": \"remote-workflow\",\n- \"source_directory\": \"/srv/repo\"\n+ \"source_directory\": \"/srv/repo\",\n+ \"provenance\": {\n+ \"server\": {\n+ \"version\": \"test\"\n+ },\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": {\n+ \"issuer\": \"fabro:test\",\n+ \"subject\": \"test-user\"\n+ },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\n },\n \"start_record\": null,\n \"conclusion\": null,\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs\nindex 9ac2e7673..d8572ac19 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs\n@@ -177,6 +177,15 @@ pub(crate) fn remote_run_summary_json(\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": {\n+ \"kind\": \"user\",\n+ \"identity\": {\n+ \"issuer\": \"fabro:test\",\n+ \"subject\": \"test-user\"\n+ },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ },\n \"origin\": {\n \"kind\": \"api\"\n },\ndiff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs\nindex 7c7c59a22..41150f3f2 100644\n--- a/lib/crates/fabro-cli/tests/it/support/mod.rs\n+++ b/lib/crates/fabro-cli/tests/it/support/mod.rs\n@@ -9,7 +9,7 @@ pub(crate) use auth_harness::{\n pub(crate) use auth_tokens::{TEST_SESSION_SECRET, issue_test_github_jwt, issue_test_worker_jwt};\n use fabro_store::EventEnvelope;\n use fabro_test::{EnvVars, TestContext, preserve_coverage_env};\n-use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings};\n+use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings, test_support};\n \n pub(crate) fn run_output_filters(context: &TestContext) -> Vec<(String, String)> {\n let mut filters = context.filters();\n@@ -48,7 +48,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s\n workflow_slug: Some(\"remote-workflow\".to_string()),\n source_directory: Some(\"/srv/repo\".to_string()),\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-dump/src/lib.rs b/lib/crates/fabro-dump/src/lib.rs\nindex d44e01215..ae3598666 100644\n--- a/lib/crates/fabro-dump/src/lib.rs\n+++ b/lib/crates/fabro-dump/src/lib.rs\n@@ -475,7 +475,7 @@ mod tests {\n use fabro_types::{\n Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunStatus,\n SandboxProviderKind, StageCompletion, StageModelUsage, StageOutcome, StartRecord,\n- SuccessReason, WorkflowSettings, first_event_seq, fixtures,\n+ SuccessReason, WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n use futures::executor;\n \n@@ -497,7 +497,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/crates/fabro-server/src/auth/cli_flow.rs\nindex 5e6a9f9cc..e45b933de 100644\n--- a/lib/crates/fabro-server/src/auth/cli_flow.rs\n+++ b/lib/crates/fabro-server/src/auth/cli_flow.rs\n@@ -1667,11 +1667,11 @@ client_id = \"github-client-id\"\n let [first, second, third] = <[RequestAuthContext; 3]>::try_from(contexts)\n .expect(\"expected three captured auth contexts\");\n assert_eq!(first.auth_status, AuthStatus::Authenticated);\n- assert_eq!(first.principal.display(), \"octocat\");\n+ assert_eq!(first.principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(second.auth_status, AuthStatus::Authenticated);\n- assert_eq!(second.principal.display(), \"octocat\");\n+ assert_eq!(second.principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(third.auth_status, AuthStatus::Authenticated);\n- assert_eq!(third.principal.display(), \"octocat\");\n+ assert_eq!(third.principal.as_ref().unwrap().display(), \"octocat\");\n }\n \n #[tokio::test]\n@@ -2076,7 +2076,7 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert_eq!(contexts[0].principal.display(), \"octocat\");\n+ assert_eq!(contexts[0].principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(contexts[1].auth_status, AuthStatus::Invalid);\n assert_eq!(\n contexts[1].auth_error_code,\n@@ -2269,8 +2269,8 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert_eq!(contexts[0].principal.display(), \"octocat\");\n- let Principal::User(user) = &contexts[0].principal else {\n+ assert_eq!(contexts[0].principal.as_ref().unwrap().display(), \"octocat\");\n+ let Some(Principal::User(user)) = &contexts[0].principal else {\n panic!(\"expected user principal\");\n };\n assert_eq!(\ndiff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs\nindex 42e19a97c..b731cfd2e 100644\n--- a/lib/crates/fabro-server/src/demo/mod.rs\n+++ b/lib/crates/fabro-server/src/demo/mod.rs\n@@ -1080,7 +1080,7 @@ fn ts(s: &str) -> DateTime {\n \n mod runs {\n use std::collections::HashMap;\n- use std::sync::OnceLock;\n+ use std::sync::{LazyLock, OnceLock};\n use std::time::Duration;\n \n use fabro_api::types::*;\n@@ -1091,13 +1091,22 @@ mod runs {\n };\n use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace};\n use fabro_types::{\n- PendingReason, RepositoryRef, RunBillingSummary, RunId, RunLifecycle, RunLinks, RunOrigin,\n- RunSize, RunTimestamps, StageId, WorkflowRef, WorkflowSettings,\n+ AuthMethod, IdpIdentity, PendingReason, Principal, RepositoryRef, RunBillingSummary, RunId,\n+ RunLifecycle, RunLinks, RunOrigin, RunSize, RunTimestamps, StageId, WorkflowRef,\n+ WorkflowSettings,\n };\n \n use super::ts;\n use crate::server::run_stage_from_stage_id;\n \n+ static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| {\n+ Principal::user(\n+ IdpIdentity::new(\"fabro:demo\", \"demo\").expect(\"demo identity should be valid\"),\n+ \"demo\".to_string(),\n+ AuthMethod::DevToken,\n+ )\n+ });\n+\n fn labels(entries: &[(&str, &str)]) -> HashMap {\n entries\n .iter()\n@@ -1170,7 +1179,7 @@ mod runs {\n repo_origin_url,\n source_directory.as_deref(),\n )),\n- created_by: None,\n+ created_by: DEMO_PRINCIPAL.clone(),\n origin: RunOrigin::default(),\n labels: labels(entries),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-server/src/principal_middleware.rs b/lib/crates/fabro-server/src/principal_middleware.rs\nindex acc9eb250..b1741e190 100644\n--- a/lib/crates/fabro-server/src/principal_middleware.rs\n+++ b/lib/crates/fabro-server/src/principal_middleware.rs\n@@ -19,7 +19,7 @@ use crate::worker_token::{self, WORKER_TOKEN_KID, WorkerScopeSet};\n \n #[derive(Clone, Debug)]\n pub(crate) struct RequestAuthContext {\n- pub principal: Principal,\n+ pub principal: Option,\n pub auth_status: AuthStatus,\n pub auth_error_code: Option,\n pub user_profile: Option,\n@@ -74,7 +74,7 @@ impl RequestAuthContext {\n #[must_use]\n pub(crate) fn initial() -> Self {\n Self {\n- principal: Principal::Anonymous,\n+ principal: None,\n auth_status: AuthStatus::Missing,\n auth_error_code: None,\n user_profile: None,\n@@ -85,7 +85,7 @@ impl RequestAuthContext {\n #[must_use]\n pub(crate) fn authenticated(principal: Principal, user_profile: Option) -> Self {\n Self {\n- principal,\n+ principal: Some(principal),\n auth_status: AuthStatus::Authenticated,\n auth_error_code: None,\n user_profile,\n@@ -96,7 +96,7 @@ impl RequestAuthContext {\n #[must_use]\n pub(crate) fn authenticated_worker(run_id: RunId, scopes: WorkerScopeSet) -> Self {\n Self {\n- principal: Principal::Worker { run_id },\n+ principal: Some(Principal::Worker { run_id }),\n auth_status: AuthStatus::Authenticated,\n auth_error_code: None,\n user_profile: None,\n@@ -123,7 +123,7 @@ impl RequestAuthContext {\n #[must_use]\n pub(crate) fn rejected(status: AuthStatus, code: Option) -> Self {\n Self {\n- principal: Principal::Anonymous,\n+ principal: None,\n auth_status: status,\n auth_error_code: code,\n user_profile: None,\n@@ -146,7 +146,7 @@ impl AuthStatus {\n \n #[derive(Clone, Debug)]\n pub(crate) struct RequestAuthLogContext {\n- pub principal: Principal,\n+ pub principal: Option,\n pub auth_status: AuthStatus,\n pub auth_error_code: Option,\n }\n@@ -170,22 +170,23 @@ impl AuthContextSlot {\n pub(crate) fn log_snapshot(&self) -> RequestAuthLogContext {\n let context = self.0.lock().expect(\"auth context lock poisoned\");\n RequestAuthLogContext {\n- principal: principal_without_log_unused_fields(&context.principal),\n+ principal: principal_without_log_unused_fields(context.principal.as_ref()),\n auth_status: context.auth_status,\n auth_error_code: context.auth_error_code,\n }\n }\n }\n \n-fn principal_without_log_unused_fields(principal: &Principal) -> Principal {\n+fn principal_without_log_unused_fields(principal: Option<&Principal>) -> Option {\n match principal {\n- Principal::User(user) => Principal::User(UserPrincipal {\n+ Some(Principal::User(user)) => Some(Principal::User(UserPrincipal {\n identity: user.identity.clone(),\n login: user.login.clone(),\n auth_method: user.auth_method,\n avatar_url: None,\n- }),\n- principal => principal.clone(),\n+ })),\n+ Some(principal) => Some(principal.clone()),\n+ None => None,\n }\n }\n \n@@ -370,7 +371,7 @@ fn auth_slot_from_parts(parts: &Parts) -> AuthContextSlot {\n pub(crate) fn require_user(slot: &AuthContextSlot) -> Result {\n let context = slot.0.lock().expect(\"auth context lock poisoned\");\n match &context.principal {\n- Principal::User(user) => Ok(user.clone()),\n+ Some(Principal::User(user)) => Ok(user.clone()),\n _ => Err(auth_rejection(context.auth_status, context.auth_error_code)),\n }\n }\n@@ -380,7 +381,7 @@ pub(crate) fn require_authenticated_user(\n ) -> Result {\n let context = slot.snapshot();\n match context.principal {\n- Principal::User(principal) => {\n+ Some(Principal::User(principal)) => {\n let Some(profile) = context.user_profile else {\n return Err(ApiError::new(\n StatusCode::INTERNAL_SERVER_ERROR,\n@@ -396,11 +397,11 @@ pub(crate) fn require_authenticated_user(\n pub(crate) fn require_run_management_actor(slot: &AuthContextSlot) -> Result {\n let context = slot.0.lock().expect(\"auth context lock poisoned\");\n match &context.principal {\n- Principal::User(user) => Ok(Principal::User(user.clone())),\n- Principal::Worker { run_id } if context.worker_scopes.has_agent_run_tools() => {\n+ Some(Principal::User(user)) => Ok(Principal::User(user.clone())),\n+ Some(Principal::Worker { run_id }) if context.worker_scopes.has_agent_run_tools() => {\n Ok(Principal::Worker { run_id: *run_id })\n }\n- Principal::Worker { .. } => Err(ApiError::forbidden()),\n+ Some(Principal::Worker { .. }) => Err(ApiError::forbidden()),\n _ => Err(auth_rejection(context.auth_status, context.auth_error_code)),\n }\n }\n@@ -411,9 +412,9 @@ fn require_worker_or_user_for_run(\n ) -> Result<(), ApiError> {\n let context = slot.0.lock().expect(\"auth context lock poisoned\");\n match &context.principal {\n- Principal::User(_) => Ok(()),\n- Principal::Worker { run_id } if run_id == route_run_id => Ok(()),\n- Principal::Worker { .. } => Err(ApiError::forbidden()),\n+ Some(Principal::User(_)) => Ok(()),\n+ Some(Principal::Worker { run_id }) if run_id == route_run_id => Ok(()),\n+ Some(Principal::Worker { .. }) => Err(ApiError::forbidden()),\n _ => Err(auth_rejection(context.auth_status, context.auth_error_code)),\n }\n }\n@@ -424,13 +425,13 @@ fn require_run_management_target(\n ) -> Result {\n let context = slot.0.lock().expect(\"auth context lock poisoned\");\n match &context.principal {\n- Principal::User(user) => Ok(Principal::User(user.clone())),\n- Principal::Worker { run_id }\n+ Some(Principal::User(user)) => Ok(Principal::User(user.clone())),\n+ Some(Principal::Worker { run_id })\n if run_id == route_run_id || context.worker_scopes.has_agent_run_tools() =>\n {\n Ok(Principal::Worker { run_id: *run_id })\n }\n- Principal::Worker { .. } => Err(ApiError::forbidden()),\n+ Some(Principal::Worker { .. }) => Err(ApiError::forbidden()),\n _ => Err(auth_rejection(context.auth_status, context.auth_error_code)),\n }\n }\n@@ -646,7 +647,7 @@ mod tests {\n let context = classify_request(&request, state.as_ref());\n \n assert_eq!(context.auth_status, AuthStatus::Authenticated);\n- assert!(matches!(context.principal, Principal::User(_)));\n+ assert!(matches!(context.principal, Some(Principal::User(_))));\n assert!(context.user_profile.is_some());\n }\n \n@@ -686,7 +687,7 @@ mod tests {\n let context = classify_request(&request, state.as_ref());\n \n assert_eq!(context.auth_status, AuthStatus::Authenticated);\n- assert_eq!(context.principal, Principal::Worker { run_id });\n+ assert_eq!(context.principal, Some(Principal::Worker { run_id }));\n assert!(!context.worker_scopes.has_agent_run_tools());\n }\n \n@@ -705,7 +706,7 @@ mod tests {\n let context = classify_request(&request, state.as_ref());\n \n assert_eq!(context.auth_status, AuthStatus::Authenticated);\n- assert_eq!(context.principal, Principal::Worker { run_id });\n+ assert_eq!(context.principal, Some(Principal::Worker { run_id }));\n assert!(context.worker_scopes.has_agent_run_tools());\n }\n \n@@ -784,7 +785,7 @@ mod tests {\n \n assert_eq!(context.auth_status, AuthStatus::Missing);\n assert_eq!(context.auth_error_code, None);\n- assert_eq!(context.principal, Principal::Anonymous);\n+ assert_eq!(context.principal, None);\n }\n \n #[test]\ndiff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/crates/fabro-server/src/run_files.rs\nindex 53657282a..082f13025 100644\n--- a/lib/crates/fabro-server/src/run_files.rs\n+++ b/lib/crates/fabro-server/src/run_files.rs\n@@ -1715,7 +1715,7 @@ fn count_flags(data: &[FileDiff]) -> (u64, u64, u64, u64) {\n mod tests {\n use std::sync::atomic::{AtomicUsize, Ordering};\n \n- use fabro_types::{CommandTermination, RunId};\n+ use fabro_types::{CommandTermination, RunId, test_support};\n use tokio::time::{Duration, sleep};\n \n use super::*;\n@@ -2386,7 +2386,7 @@ index 1111111..2222222 160000\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs\nindex a004872cb..1760d76cf 100644\n--- a/lib/crates/fabro-server/src/run_manifest.rs\n+++ b/lib/crates/fabro-server/src/run_manifest.rs\n@@ -26,7 +26,9 @@ use fabro_static::EnvVars;\n use fabro_types::settings::cli::OutputVerbosity;\n use fabro_types::settings::interp::InterpString;\n use fabro_types::settings::run::{EnvironmentProvider, RunGoal, RunNamespace};\n-use fabro_types::{ManifestPath, RunId, SandboxProviderKind, ServerSettings, WorkflowSettings};\n+use fabro_types::{\n+ ManifestPath, RunId, RunProvenance, SandboxProviderKind, ServerSettings, WorkflowSettings,\n+};\n use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};\n use fabro_validate::Severity;\n use fabro_workflow::Error as WorkflowError;\n@@ -201,6 +203,7 @@ pub(crate) fn validate_prepared_manifest(\n pub(crate) fn create_run_input(\n prepared: PreparedManifest,\n configured_providers: Vec,\n+ provenance: RunProvenance,\n web_url: Option,\n ) -> CreateRunInput {\n CreateRunInput {\n@@ -216,7 +219,7 @@ pub(crate) fn create_run_input(\n git: prepared.git,\n fork_source_ref: None,\n parent_id: prepared.parent_id,\n- provenance: None,\n+ provenance,\n configured_providers,\n web_url,\n }\ndiff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs\nindex b0bb8432c..25316c66a 100644\n--- a/lib/crates/fabro-server/src/server.rs\n+++ b/lib/crates/fabro-server/src/server.rs\n@@ -1764,7 +1764,10 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n let status = response.status().as_u16();\n let latency_ms = start.elapsed().as_millis();\n let auth_context = auth_slot.log_snapshot();\n- let principal_kind = auth_context.principal.kind();\n+ let principal_kind = auth_context\n+ .principal\n+ .as_ref()\n+ .map_or(\"none\", Principal::kind);\n let auth_status = auth_context.auth_status.as_str();\n \n macro_rules! emit_http_log {\n@@ -1802,27 +1805,27 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n macro_rules! emit_principal_http_log {\n ($level:ident) => {{\n match &auth_context.principal {\n- Principal::User(user) => emit_http_log!(\n+ Some(Principal::User(user)) => emit_http_log!(\n $level,\n user_auth_method = user.auth_method.as_str(),\n idp_issuer = user.identity.issuer(),\n idp_subject = user.identity.subject(),\n login = user.login.as_str(),\n ),\n- Principal::Worker { run_id } => {\n+ Some(Principal::Worker { run_id }) => {\n emit_http_log!($level, run_id = run_id.to_string().as_str(),)\n }\n- Principal::Webhook { delivery_id } => {\n+ Some(Principal::Webhook { delivery_id }) => {\n emit_http_log!($level, delivery_id = delivery_id.as_str(),)\n }\n- Principal::Slack {\n+ Some(Principal::Slack {\n team_id, user_id, ..\n- } => emit_http_log!(\n+ }) => emit_http_log!(\n $level,\n team_id = team_id.as_str(),\n user_id = user_id.as_str(),\n ),\n- Principal::Agent { .. } | Principal::System { .. } | Principal::Anonymous => {\n+ None | Some(Principal::Agent { .. } | Principal::System { .. }) => {\n emit_http_log!($level)\n }\n }\ndiff --git a/lib/crates/fabro-server/src/server/handler/events.rs b/lib/crates/fabro-server/src/server/handler/events.rs\nindex 8e7806522..562e3d5fc 100644\n--- a/lib/crates/fabro-server/src/server/handler/events.rs\n+++ b/lib/crates/fabro-server/src/server/handler/events.rs\n@@ -535,7 +535,7 @@ mod stage_events_tests {\n use axum::body::{Body, to_bytes};\n use axum::http::{Request, StatusCode, header};\n use fabro_store::EventPayload;\n- use fabro_types::{Graph, RunId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, WorkflowSettings, test_support};\n use fabro_workflow::event as workflow_event;\n use http_body_util::BodyExt;\n use serde_json::json;\n@@ -569,7 +569,7 @@ mod stage_events_tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/server/handler/lifecycle.rs b/lib/crates/fabro-server/src/server/handler/lifecycle.rs\nindex 0c2697133..2d55e9b33 100644\n--- a/lib/crates/fabro-server/src/server/handler/lifecycle.rs\n+++ b/lib/crates/fabro-server/src/server/handler/lifecycle.rs\n@@ -864,7 +864,7 @@ async fn retry_run(\n let input = operations::RetryRunInput {\n source_run_id: id,\n new_run_id,\n- provenance: Some(run_provenance(&headers, &actor)),\n+ provenance: run_provenance(&headers, &actor),\n web_url: state.run_web_url(&new_run_id),\n };\n match Box::pin(operations::retry_run(&state.store, &input)).await {\ndiff --git a/lib/crates/fabro-server/src/server/handler/pair.rs b/lib/crates/fabro-server/src/server/handler/pair.rs\nindex e43f4e6f8..ea64e05ab 100644\n--- a/lib/crates/fabro-server/src/server/handler/pair.rs\n+++ b/lib/crates/fabro-server/src/server/handler/pair.rs\n@@ -850,7 +850,7 @@ mod tests {\n use fabro_types::run_event::AgentMessageProps;\n use fabro_types::{\n BilledTokenCounts, EventEnvelope, Graph, PairMessageId, RunEvent, StageId,\n- WorkflowSettings, fixtures,\n+ WorkflowSettings, fixtures, test_support,\n };\n use fabro_workflow::event as workflow_event;\n use tower::ServiceExt;\n@@ -1023,7 +1023,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs\nindex 090d1ba5d..e581c8048 100644\n--- a/lib/crates/fabro-server/src/server/handler/runs.rs\n+++ b/lib/crates/fabro-server/src/server/handler/runs.rs\n@@ -641,13 +641,14 @@ async fn create_run(\n .as_ref()\n .map(LlmClientResult::provider_ids)\n .unwrap_or_default();\n+ let provenance = run_provenance(&headers, &actor);\n let mut create_input = run_manifest::create_run_input(\n prepared.clone(),\n ready_provider_ids.clone(),\n+ provenance,\n web_url.clone(),\n );\n create_input.run_id = Some(run_id);\n- create_input.provenance = Some(run_provenance(&headers, &actor));\n create_input.submitted_manifest_bytes = Some(body.to_vec());\n \n let storage_root = match resolve_interp_string(&state.server_settings().server.storage.root) {\n@@ -817,7 +818,7 @@ pub(super) fn run_provenance(headers: &HeaderMap, subject: &Principal) -> RunPro\n version: FABRO_VERSION.to_string(),\n }),\n client: run_client_provenance(headers),\n- subject: Some(subject.clone()),\n+ subject: subject.clone(),\n }\n }\n \ndiff --git a/lib/crates/fabro-server/src/server/handler/sandbox.rs b/lib/crates/fabro-server/src/server/handler/sandbox.rs\nindex bfba9c242..be7d22023 100644\n--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs\n@@ -1307,7 +1307,7 @@ FABRO_PROC_NET_TCP /proc/net/tcp6\n mod retrieve_sandbox_tests {\n use axum::body::{Body, to_bytes};\n use axum::http::{Request, StatusCode};\n- use fabro_types::{Graph, RunId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, WorkflowSettings, test_support};\n use serde_json::{Value, json};\n use tower::ServiceExt;\n \n@@ -1347,6 +1347,7 @@ mod retrieve_sandbox_tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": test_support::test_run_provenance(),\n },\n }),\n run_id,\ndiff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/crates/fabro-server/src/server/handler/sessions.rs\nindex 1791ee57b..5aa30cfed 100644\n--- a/lib/crates/fabro-server/src/server/handler/sessions.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sessions.rs\n@@ -1508,6 +1508,7 @@ mod tests {\n use fabro_agent::config::ToolAccess;\n use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource};\n use fabro_llm::types::{ToolCall, ToolDefinition};\n+ use fabro_types::test_support;\n \n use super::*;\n \n@@ -1701,7 +1702,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs\nindex ba4520d87..62d4331c1 100644\n--- a/lib/crates/fabro-server/src/server/tests.rs\n+++ b/lib/crates/fabro-server/src/server/tests.rs\n@@ -24,7 +24,7 @@ use fabro_types::{\n SandboxProviderKind, StageContextWindowBreakdownItem, StageContextWindowCategory,\n StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness,\n StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind,\n- WorkflowSettings, fixtures,\n+ WorkflowSettings, fixtures, test_support,\n };\n use fabro_util::check_report::CheckStatus;\n use fabro_workflow::records::CheckpointExt;\n@@ -3367,7 +3367,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -3412,7 +3412,7 @@ async fn create_slack_notification_run(\n source_directory: None,\n workflow_slug: workflow_slug.map(str::to_string),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -4418,7 +4418,7 @@ async fn list_run_stages_distinguishes_visits() {\n source_directory: None,\n workflow_slug: Some(\"test\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -5470,7 +5470,7 @@ async fn create_completed_run_ready_for_pull_request(\n source_directory: Some(\"/tmp/project\".to_string()),\n git: git.clone(),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -9272,15 +9272,10 @@ async fn run_tool_worker_token_can_use_client_backend_routes_across_runs() {\n .unwrap()\n .expect(\"created run should be cached\");\n assert_eq!(\n- cached\n- .projection\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.as_ref()),\n- Some(&Principal::Worker {\n+ cached.projection.spec.provenance.subject,\n+ Principal::Worker {\n run_id: parent_run_id,\n- }),\n+ },\n );\n \n let response = app\n@@ -11604,7 +11599,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId\n source_directory: Some(\"/tmp/fabro-run\".to_string()),\n workflow_slug: Some(\"test\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -12353,7 +12348,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {\n source_directory: Some(\"/tmp/fabro-run\".to_string()),\n workflow_slug: Some(\"test\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs\nindex 5f10aba10..9ecd7b68a 100644\n--- a/lib/crates/fabro-server/src/web_auth.rs\n+++ b/lib/crates/fabro-server/src/web_auth.rs\n@@ -1365,7 +1365,7 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert!(matches!(contexts[0].principal, Principal::User(_)));\n+ assert!(matches!(contexts[0].principal, Some(Principal::User(_))));\n assert_eq!(contexts[1].auth_status, AuthStatus::Invalid);\n assert_eq!(\n contexts[1].auth_error_code,\ndiff --git a/lib/crates/fabro-server/tests/it/api/run_files.rs b/lib/crates/fabro-server/tests/it/api/run_files.rs\nindex e820b00f9..5af21eb92 100644\n--- a/lib/crates/fabro-server/tests/it/api/run_files.rs\n+++ b/lib/crates/fabro-server/tests/it/api/run_files.rs\n@@ -14,7 +14,7 @@ use axum::body::Body;\n use axum::http::{Request, StatusCode};\n use fabro_server::test_support::test_app_state_with_store;\n use fabro_store::{ArtifactStore, Database};\n-use fabro_types::{Graph, RunId, WorkflowSettings};\n+use fabro_types::{Graph, RunId, WorkflowSettings, test_support};\n use fabro_workflow::event as workflow_event;\n use fabro_workflow::run_status::SuccessReason;\n use object_store::memory::InMemory as MemoryObjectStore;\n@@ -68,7 +68,7 @@ async fn append_completed_run_with_final_patch(\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-store/Cargo.toml b/lib/crates/fabro-store/Cargo.toml\nindex 016b55d3d..860ffbf7c 100644\n--- a/lib/crates/fabro-store/Cargo.toml\n+++ b/lib/crates/fabro-store/Cargo.toml\n@@ -36,3 +36,4 @@ tokio = { workspace = true, features = [\"test-util\", \"macros\"] }\n tempfile = \"3\"\n ulid.workspace = true\n insta = { workspace = true }\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\ndiff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs\nindex 552891d2e..619e12fb4 100644\n--- a/lib/crates/fabro-store/src/run_state.rs\n+++ b/lib/crates/fabro-store/src/run_state.rs\n@@ -902,11 +902,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run {\n })\n .map(|(_, record)| record.question.clone());\n let models = run_models(state);\n- let created_by = state\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.clone());\n+ let created_by = state.spec.provenance.subject.clone();\n let source_directory = state.spec.source_directory.clone();\n let repo_origin_url = state.spec.git.as_ref().map(|git| git.origin_url.clone());\n let start_time = state.start.as_ref().map(|start| start.start_time);\n@@ -1255,7 +1251,7 @@ mod tests {\n StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod,\n StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning,\n StageModelUsage, StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings,\n- first_event_seq, fixtures,\n+ first_event_seq, fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -1336,7 +1332,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1372,7 +1368,7 @@ mod tests {\n }\n \n #[test]\n- fn legacy_run_created_projects_retried_from_none() {\n+ fn run_created_projects_retried_from_none() {\n let event = test_raw_event(\n 1,\n \"run.created\",\n@@ -1380,7 +1376,8 @@ mod tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }),\n None,\n );\n@@ -1577,7 +1574,7 @@ mod tests {\n \"repo_origin_url\": null,\n \"base_branch\": null,\n \"labels\": {},\n- \"provenance\": null,\n+ \"provenance\": test_support::test_run_provenance(),\n \"manifest_blob\": null,\n \"definition_blob\": null,\n \"git\": null,\n@@ -2605,7 +2602,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2630,7 +2627,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2669,7 +2666,8 @@ mod tests {\n \"attrs\": { \"goal\": { \"String\": \"Goal title\" } }\n },\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }),\n None,\n );\n@@ -2683,7 +2681,7 @@ mod tests {\n }\n \n #[test]\n- fn legacy_run_created_without_title_infers_projection_title() {\n+ fn run_created_without_title_infers_projection_title() {\n let event = test_raw_event(\n 1,\n \"run.created\",\n@@ -2696,7 +2694,8 @@ mod tests {\n \"attrs\": { \"goal\": { \"String\": \"## Plan: Legacy title\\n\\nDetails\" } }\n },\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }),\n None,\n );\n@@ -2725,7 +2724,8 @@ mod tests {\n \"attrs\": { \"goal\": { \"String\": \"Goal title\" } }\n },\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }),\n None,\n ),\n@@ -2769,6 +2769,7 @@ mod tests {\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance(),\n \"manifest_blob\": manifest_blob\n }\n }))\ndiff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs\nindex 784868ace..4f1432319 100644\n--- a/lib/crates/fabro-store/src/slate/mod.rs\n+++ b/lib/crates/fabro-store/src/slate/mod.rs\n@@ -472,7 +472,7 @@ mod tests {\n use chrono::{DateTime, Utc};\n use fabro_types::{\n AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId,\n- SuccessReason, WorkflowSettings,\n+ SuccessReason, WorkflowSettings, test_support,\n };\n use futures::TryStreamExt;\n use object_store::memory::InMemory;\n@@ -541,7 +541,7 @@ mod tests {\n workflow_slug: Some(\"night-sky\".to_string()),\n source_directory: Some(format!(\"/tmp/{label}\")),\n labels: std::collections::HashMap::from([(\"team\".to_string(), \"infra\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\n@@ -600,6 +600,7 @@ mod tests {\n \"run_dir\": format!(\"/tmp/{label}\"),\n \"git\": run_spec.git,\n \"labels\": run_spec.labels,\n+ \"provenance\": run_spec.provenance,\n }),\n ))\n .await\n@@ -625,6 +626,7 @@ mod tests {\n \"run_dir\": format!(\"/tmp/{label}\"),\n \"git\": run_spec.git,\n \"labels\": run_spec.labels,\n+ \"provenance\": run_spec.provenance,\n \"parent_id\": parent_id,\n }),\n ))\n@@ -1299,6 +1301,7 @@ mod tests {\n \"run_dir\": \"/tmp/run-2\",\n \"git\": run_spec[\"git\"],\n \"labels\": run_spec[\"labels\"],\n+ \"provenance\": run_spec[\"provenance\"],\n },\n }))\n .unwrap(),\ndiff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs\nindex 1718cb662..94116fa94 100644\n--- a/lib/crates/fabro-store/src/slate/run_store.rs\n+++ b/lib/crates/fabro-store/src/slate/run_store.rs\n@@ -667,7 +667,7 @@ mod tests {\n use std::sync::Arc;\n use std::time::Duration;\n \n- use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings, test_support};\n use object_store::memory::InMemory;\n use serde_json::json;\n \n@@ -723,6 +723,7 @@ mod tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": test_support::test_run_provenance(),\n },\n }),\n run_id,\ndiff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs\nindex f6aa256fd..12ee8c932 100644\n--- a/lib/crates/fabro-store/tests/serializable_projection.rs\n+++ b/lib/crates/fabro-store/tests/serializable_projection.rs\n@@ -8,7 +8,7 @@ use fabro_types::{\n BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord,\n QuestionType, RunDiff, RunSandbox, RunSandboxRuntime, RunStatus, SandboxProviderKind,\n StageCompletion, StageModelUsage, StageOutcome, StartRecord, WorkflowSettings, first_event_seq,\n- fixtures,\n+ fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -21,7 +21,7 @@ fn sample_run_spec() -> RunSpec {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/tmp/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\ndiff --git a/lib/crates/fabro-tool/Cargo.toml b/lib/crates/fabro-tool/Cargo.toml\nindex 08b6df7cf..c50ea6a38 100644\n--- a/lib/crates/fabro-tool/Cargo.toml\n+++ b/lib/crates/fabro-tool/Cargo.toml\n@@ -29,4 +29,5 @@ tokio.workspace = true\n toml.workspace = true\n \n [dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\n tempfile = \"3\"\ndiff --git a/lib/crates/fabro-tool/src/common.rs b/lib/crates/fabro-tool/src/common.rs\nindex 9dd64599a..159ec426d 100644\n--- a/lib/crates/fabro-tool/src/common.rs\n+++ b/lib/crates/fabro-tool/src/common.rs\n@@ -307,7 +307,9 @@ fn format_tool_error(err: &anyhow::Error) -> String {\n #[cfg(test)]\n mod tests {\n use chrono::{TimeZone, Utc};\n- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n+ use fabro_types::{\n+ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support,\n+ };\n \n use super::*;\n \n@@ -413,7 +415,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs\nindex 8488f8f0b..57e41acd5 100644\n--- a/lib/crates/fabro-tool/src/create.rs\n+++ b/lib/crates/fabro-tool/src/create.rs\n@@ -508,7 +508,7 @@ mod tests {\n use fabro_api::types;\n use fabro_types::{\n EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus,\n- RunTimestamps, WorkflowRef,\n+ RunTimestamps, WorkflowRef, test_support,\n };\n use schemars::SchemaGenerator;\n use serde_json::json;\n@@ -902,7 +902,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/interact.rs b/lib/crates/fabro-tool/src/interact.rs\nindex 34023120d..503147ece 100644\n--- a/lib/crates/fabro-tool/src/interact.rs\n+++ b/lib/crates/fabro-tool/src/interact.rs\n@@ -453,7 +453,7 @@ mod tests {\n use chrono::{TimeZone, Utc};\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection,\n- RunStatus, RunTimestamps, WorkflowRef,\n+ RunStatus, RunTimestamps, WorkflowRef, test_support,\n };\n use serde_json::json;\n \n@@ -690,7 +690,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs\nindex 0df7e391a..e3d0162c6 100644\n--- a/lib/crates/fabro-tool/src/search.rs\n+++ b/lib/crates/fabro-tool/src/search.rs\n@@ -293,7 +293,9 @@ mod tests {\n use std::collections::HashMap;\n \n use chrono::{TimeZone, Utc};\n- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n+ use fabro_types::{\n+ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support,\n+ };\n \n use super::*;\n \n@@ -444,7 +446,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"group\".to_string(), group.to_string())]),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs\nindex 318f982ad..339d1a0dc 100644\n--- a/lib/crates/fabro-types/src/lib.rs\n+++ b/lib/crates/fabro-types/src/lib.rs\n@@ -44,6 +44,8 @@ pub mod start;\n pub mod status;\n pub mod steering;\n pub mod system_integrations;\n+#[cfg(any(test, feature = \"test-support\"))]\n+pub mod test_support;\n pub mod timing;\n pub mod todo;\n pub mod transcript;\ndiff --git a/lib/crates/fabro-types/src/principal.rs b/lib/crates/fabro-types/src/principal.rs\nindex 2c0ff3807..4f1962ea2 100644\n--- a/lib/crates/fabro-types/src/principal.rs\n+++ b/lib/crates/fabro-types/src/principal.rs\n@@ -39,7 +39,6 @@ pub enum Principal {\n System {\n system_kind: SystemActorKind,\n },\n- Anonymous,\n }\n \n #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, IntoStaticStr)]\n@@ -97,7 +96,6 @@ impl Principal {\n Self::Slack { .. } => \"slack\",\n Self::Agent { .. } => \"agent\",\n Self::System { .. } => \"system\",\n- Self::Anonymous => \"anonymous\",\n }\n }\n \n@@ -123,7 +121,6 @@ impl Principal {\n } => session_id.clone(),\n Self::Agent { .. } => \"agent\".to_string(),\n Self::System { system_kind } => format!(\"system:{system_kind}\"),\n- Self::Anonymous => \"anonymous\".to_string(),\n }\n }\n }\n@@ -291,11 +288,6 @@ mod tests {\n });\n }\n \n- #[test]\n- fn round_trips_anonymous_variant() {\n- assert_round_trip(&Principal::Anonymous);\n- }\n-\n #[test]\n fn auth_method_as_str_matches_serde() {\n assert_eq!(AuthMethod::Github.as_str(), \"github\");\ndiff --git a/lib/crates/fabro-types/src/run.rs b/lib/crates/fabro-types/src/run.rs\nindex 269db43ee..afb2a1f24 100644\n--- a/lib/crates/fabro-types/src/run.rs\n+++ b/lib/crates/fabro-types/src/run.rs\n@@ -23,14 +23,13 @@ pub struct RunClientProvenance {\n pub version: Option,\n }\n \n-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n pub struct RunProvenance {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub server: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub client: Option,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub subject: Option,\n+ pub subject: Principal,\n }\n \n #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n@@ -90,8 +89,7 @@ pub struct RunSpec {\n pub source_directory: Option,\n #[serde(default, skip_serializing_if = \"HashMap::is_empty\")]\n pub labels: HashMap,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub provenance: Option,\n+ pub provenance: RunProvenance,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub manifest_blob: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\ndiff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs\nindex 01ed04f4d..e57b5ed0a 100644\n--- a/lib/crates/fabro-types/src/run_event/mod.rs\n+++ b/lib/crates/fabro-types/src/run_event/mod.rs\n@@ -961,7 +961,7 @@ mod tests {\n use super::*;\n use crate::{\n AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings,\n- fixtures,\n+ fixtures, test_support,\n };\n \n fn user_principal(login: &str) -> Principal {\n@@ -1045,7 +1045,8 @@ mod tests {\n \"graph\": graph,\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n- \"source_directory\": \"/tmp/run\"\n+ \"source_directory\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }\n });\n \n@@ -1066,6 +1067,7 @@ mod tests {\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance(),\n \"manifest_blob\": RunBlobId::new(br#\"{\"version\":1}\"#).to_string()\n }\n });\ndiff --git a/lib/crates/fabro-types/src/run_event/run.rs b/lib/crates/fabro-types/src/run_event/run.rs\nindex fa189171f..be16946e2 100644\n--- a/lib/crates/fabro-types/src/run_event/run.rs\n+++ b/lib/crates/fabro-types/src/run_event/run.rs\n@@ -28,8 +28,7 @@ pub struct RunCreatedProps {\n pub workflow_slug: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub db_prefix: Option,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub provenance: Option,\n+ pub provenance: RunProvenance,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub manifest_blob: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\ndiff --git a/lib/crates/fabro-types/src/run_projection.rs b/lib/crates/fabro-types/src/run_projection.rs\nindex 3bba6d43e..b667f4fc8 100644\n--- a/lib/crates/fabro-types/src/run_projection.rs\n+++ b/lib/crates/fabro-types/src/run_projection.rs\n@@ -680,7 +680,7 @@ mod title_tests {\n \n use chrono::Utc;\n \n- use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings};\n+ use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support};\n \n fn projection_with_goal(goal: Option<&str>) -> RunProjection {\n let mut graph = Graph::new(\"test\");\n@@ -698,7 +698,7 @@ mod title_tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -750,7 +750,7 @@ mod iter_stages_tests {\n use serde_json::json;\n \n use super::RunProjection;\n- use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings};\n+ use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings, test_support};\n \n fn seq(n: u32) -> NonZeroU32 {\n NonZeroU32::new(n).unwrap()\n@@ -767,7 +767,7 @@ mod iter_stages_tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs\nindex 1cde31614..509303d3c 100644\n--- a/lib/crates/fabro-types/src/run_summary.rs\n+++ b/lib/crates/fabro-types/src/run_summary.rs\n@@ -52,8 +52,7 @@ pub struct Run {\n pub automation: Option,\n #[serde(default)]\n pub repository: Option,\n- #[serde(default)]\n- pub created_by: Option,\n+ pub created_by: Principal,\n pub origin: RunOrigin,\n pub labels: HashMap,\n pub lifecycle: RunLifecycle,\ndiff --git a/lib/crates/fabro-types/src/test_support.rs b/lib/crates/fabro-types/src/test_support.rs\nnew file mode 100644\nindex 000000000..9db7eff91\n--- /dev/null\n+++ b/lib/crates/fabro-types/src/test_support.rs\n@@ -0,0 +1,21 @@\n+use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance, RunServerProvenance};\n+\n+#[must_use]\n+pub fn test_principal() -> Principal {\n+ Principal::user(\n+ IdpIdentity::new(\"fabro:test\", \"test-user\").expect(\"test identity should be valid\"),\n+ \"test\".to_string(),\n+ AuthMethod::DevToken,\n+ )\n+}\n+\n+#[must_use]\n+pub fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: Some(RunServerProvenance {\n+ version: \"test\".to_string(),\n+ }),\n+ client: None,\n+ subject: test_principal(),\n+ }\n+}\ndiff --git a/lib/crates/fabro-types/tests/run_event_serde.rs b/lib/crates/fabro-types/tests/run_event_serde.rs\nindex 8f2df1972..533d21f15 100644\n--- a/lib/crates/fabro-types/tests/run_event_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_event_serde.rs\n@@ -8,6 +8,16 @@ use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n use fabro_types::{EventBody, TurnId, WorkflowSettings, fixtures};\n \n+fn test_run_provenance() -> fabro_types::RunProvenance {\n+ fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ }\n+}\n+\n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n settings.run.goal = Some(RunGoal::Inline(InterpString::parse(\"Ship {{ env.TASK }}\")));\n@@ -27,7 +37,7 @@ fn run_created_props_round_trip_templated_settings() {\n source_directory: Some(\"/Users/client/project\".to_string()),\n workflow_slug: Some(\"demo\".to_string()),\n db_prefix: Some(\"run_\".to_string()),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n git: Some(GitContext {\n origin_url: \"https://github.com/fabro-sh/fabro.git\".to_string(),\n@@ -89,7 +99,7 @@ fn run_created_props_omits_web_url_when_absent() {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -120,17 +130,17 @@ fn run_created_props_omits_web_url_when_absent() {\n }\n \n #[test]\n-fn run_created_props_defaults_retried_from_for_legacy_events() {\n+fn run_created_props_defaults_retried_from_when_absent() {\n let json = serde_json::json!({\n \"title\": null,\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"ship\"),\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_run_provenance()\n });\n \n- let props: RunCreatedProps =\n- serde_json::from_value(json).expect(\"legacy props should deserialize\");\n+ let props: RunCreatedProps = serde_json::from_value(json).expect(\"props should deserialize\");\n assert_eq!(props.retried_from, None);\n }\n \ndiff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs\nindex f5e8cf25f..ef29ce763 100644\n--- a/lib/crates/fabro-types/tests/run_spec_methods.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_methods.rs\n@@ -5,6 +5,16 @@ use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec};\n use fabro_types::settings::{ProjectNamespace, WorkflowNamespace};\n use fabro_types::{WorkflowSettings, fixtures};\n \n+fn test_run_provenance() -> fabro_types::RunProvenance {\n+ fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ }\n+}\n+\n fn sample_run_spec() -> RunSpec {\n let settings = WorkflowSettings {\n project: ProjectNamespace {\n@@ -26,7 +36,7 @@ fn sample_run_spec() -> RunSpec {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\ndiff --git a/lib/crates/fabro-types/tests/run_spec_serde.rs b/lib/crates/fabro-types/tests/run_spec_serde.rs\nindex f6278ff34..d45c56ac8 100644\n--- a/lib/crates/fabro-types/tests/run_spec_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_serde.rs\n@@ -6,6 +6,16 @@ use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n use fabro_types::{WorkflowSettings, fixtures};\n \n+fn test_run_provenance() -> fabro_types::RunProvenance {\n+ fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ }\n+}\n+\n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n settings.run.goal = Some(RunGoal::Inline(InterpString::parse(\"Ship {{ env.TASK }}\")));\n@@ -22,7 +32,7 @@ fn run_spec_round_trips_templated_settings() {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\ndiff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs\nindex feceb0e12..e6fb39136 100644\n--- a/lib/crates/fabro-workflow/src/billing_rollup.rs\n+++ b/lib/crates/fabro-workflow/src/billing_rollup.rs\n@@ -165,7 +165,7 @@ mod tests {\n use fabro_model::{Catalog, ModelRef, ProviderId};\n use fabro_types::{\n AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion,\n- StageOutcome, WorkflowSettings, first_event_seq, fixtures,\n+ StageOutcome, WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n \n use super::billing_rollup_from_projection;\n@@ -352,7 +352,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs\nindex 684af5164..17eb5d157 100644\n--- a/lib/crates/fabro-workflow/src/event/convert.rs\n+++ b/lib/crates/fabro-workflow/src/event/convert.rs\n@@ -2403,28 +2403,28 @@ mod tests {\n let provenance = RunProvenance {\n server: None,\n client: None,\n- subject: Some(user_principal(\"alice\")),\n+ subject: user_principal(\"alice\"),\n };\n \n let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated {\n- run_id: fixtures::RUN_1,\n- title: None,\n- settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),\n- graph: serde_json::to_value(Graph::new(\"test\")).unwrap(),\n- workflow_source: None,\n- workflow_config: None,\n- labels: BTreeMap::default(),\n- run_dir: \"/tmp/run\".to_string(),\n+ run_id: fixtures::RUN_1,\n+ title: None,\n+ settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),\n+ graph: serde_json::to_value(Graph::new(\"test\")).unwrap(),\n+ workflow_source: None,\n+ workflow_config: None,\n+ labels: BTreeMap::default(),\n+ run_dir: \"/tmp/run\".to_string(),\n source_directory: Some(\"/tmp/run\".to_string()),\n- workflow_slug: None,\n- db_prefix: None,\n- provenance: Some(provenance),\n- manifest_blob: None,\n- git: None,\n- fork_source_ref: None,\n- retried_from: None,\n- parent_id: None,\n- web_url: None,\n+ workflow_slug: None,\n+ db_prefix: None,\n+ provenance,\n+ manifest_blob: None,\n+ git: None,\n+ fork_source_ref: None,\n+ retried_from: None,\n+ parent_id: None,\n+ web_url: None,\n });\n let actor = stored.actor.as_ref().expect(\"actor set\");\n assert_eq!(actor, &user_principal(\"alice\"));\ndiff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs\nindex cf458c121..5a70af52c 100644\n--- a/lib/crates/fabro-workflow/src/event/events.rs\n+++ b/lib/crates/fabro-workflow/src/event/events.rs\n@@ -39,8 +39,7 @@ pub enum Event {\n workflow_slug: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n db_prefix: Option,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- provenance: Option,\n+ provenance: RunProvenance,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n manifest_blob: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\ndiff --git a/lib/crates/fabro-workflow/src/event/sink.rs b/lib/crates/fabro-workflow/src/event/sink.rs\nindex 967f0570a..2c49372e4 100644\n--- a/lib/crates/fabro-workflow/src/event/sink.rs\n+++ b/lib/crates/fabro-workflow/src/event/sink.rs\n@@ -212,7 +212,7 @@ impl StoreProgressLogger {\n mod tests {\n use std::sync::Arc;\n \n- use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures};\n+ use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures, test_support};\n use tokio::sync::Mutex as AsyncMutex;\n \n use super::*;\n@@ -243,7 +243,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/event/stored_fields.rs b/lib/crates/fabro-workflow/src/event/stored_fields.rs\nindex 94fba25ad..4ada17b67 100644\n--- a/lib/crates/fabro-workflow/src/event/stored_fields.rs\n+++ b/lib/crates/fabro-workflow/src/event/stored_fields.rs\n@@ -57,7 +57,7 @@ pub(super) fn stored_event_fields(event: &Event, scope: Option<&StageScope>) ->\n fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields {\n match event {\n Event::RunCreated { provenance, .. } => StoredEventFields {\n- actor: provenance.as_ref().and_then(|p| p.subject.clone()),\n+ actor: Some(provenance.subject.clone()),\n ..StoredEventFields::default()\n },\n Event::RunCancelRequested { actor }\ndiff --git a/lib/crates/fabro-workflow/src/git.rs b/lib/crates/fabro-workflow/src/git.rs\nindex f48683dd0..177f09079 100644\n--- a/lib/crates/fabro-workflow/src/git.rs\n+++ b/lib/crates/fabro-workflow/src/git.rs\n@@ -343,7 +343,7 @@ mod tests {\n \n use fabro_dump::RunDump;\n use fabro_store::Database;\n- use fabro_types::{CommandTermination, StageModelUsage, fixtures};\n+ use fabro_types::{CommandTermination, StageModelUsage, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -468,7 +468,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs\nindex 648f00c4b..0f46cdfed 100644\n--- a/lib/crates/fabro-workflow/src/handler/agent.rs\n+++ b/lib/crates/fabro-workflow/src/handler/agent.rs\n@@ -429,7 +429,7 @@ mod tests {\n use fabro_graphviz::graph::AttrValue;\n use fabro_model::{ReasoningEffort, Speed};\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n use tempfile::TempDir;\n \n@@ -483,7 +483,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/command.rs b/lib/crates/fabro-workflow/src/handler/command.rs\nindex dde68dd5d..10ad51ce0 100644\n--- a/lib/crates/fabro-workflow/src/handler/command.rs\n+++ b/lib/crates/fabro-workflow/src/handler/command.rs\n@@ -228,7 +228,7 @@ mod tests {\n use bytes::Bytes;\n use fabro_graphviz::graph::AttrValue;\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures};\n+ use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n use tokio::sync::Mutex;\n \n@@ -255,7 +255,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -355,7 +355,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs\nindex 91753f4c9..499cdd784 100644\n--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs\n+++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs\n@@ -1600,6 +1600,7 @@ mod tests {\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin,\n RunPairStatusResponse, RunProjection, RunStatus, RunTimestamps, SuccessReason, WorkflowRef,\n+ test_support,\n };\n use fabro_vault::{SecretType, Vault};\n use futures::stream;\n@@ -2133,7 +2134,7 @@ reasoning = false\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs\nindex 7c85042cc..e9b4d4fe3 100644\n--- a/lib/crates/fabro-workflow/src/handler/parallel.rs\n+++ b/lib/crates/fabro-workflow/src/handler/parallel.rs\n@@ -692,7 +692,7 @@ mod tests {\n \n use fabro_graphviz::graph::{AttrValue, Edge};\n use fabro_store::{Database, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -727,7 +727,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs\nindex 8d5b2fdf6..5c3ab7c11 100644\n--- a/lib/crates/fabro-workflow/src/handler/prompt.rs\n+++ b/lib/crates/fabro-workflow/src/handler/prompt.rs\n@@ -225,7 +225,7 @@ mod tests {\n use fabro_graphviz::graph::AttrValue;\n use fabro_model::{ReasoningEffort, Speed};\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n use tempfile::TempDir;\n \n@@ -282,7 +282,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs\nindex fefaafa8c..e3cfeff89 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs\n@@ -598,7 +598,7 @@ mod tests {\n use fabro_model::Catalog;\n use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection};\n use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};\n- use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -735,7 +735,7 @@ mod tests {\n source_directory: Some(\"/tmp/project\".to_string()),\n workflow_slug: Some(\"metadata\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/operations/archive.rs b/lib/crates/fabro-workflow/src/operations/archive.rs\nindex bb3693398..4393af607 100644\n--- a/lib/crates/fabro-workflow/src/operations/archive.rs\n+++ b/lib/crates/fabro-workflow/src/operations/archive.rs\n@@ -136,7 +136,9 @@ mod tests {\n use std::time::Duration;\n \n use fabro_store::Database;\n- use fabro_types::{FailureReason, RunId, SuccessReason, TerminalStatus, fixtures};\n+ use fabro_types::{\n+ FailureReason, RunId, SuccessReason, TerminalStatus, fixtures, test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -225,7 +227,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs\nindex 0460a1874..6b7233484 100644\n--- a/lib/crates/fabro-workflow/src/operations/create.rs\n+++ b/lib/crates/fabro-workflow/src/operations/create.rs\n@@ -44,7 +44,7 @@ pub struct CreateRunInput {\n pub git: Option,\n pub fork_source_ref: Option,\n pub parent_id: Option,\n- pub provenance: Option,\n+ pub provenance: RunProvenance,\n pub configured_providers: Vec,\n /// Public URL where this run can be viewed in the web UI, when the server\n /// has the web UI enabled. Recorded on the `run.created` event so attach\n@@ -70,7 +70,7 @@ struct PersistCreateOptions {\n source_directory: Option,\n git: Option,\n fork_source_ref: Option,\n- provenance: Option,\n+ provenance: RunProvenance,\n configured_providers: Vec,\n catalog: Arc,\n }\n@@ -415,7 +415,7 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunMode;\n- use fabro_types::{WorkflowSettings, fixtures};\n+ use fabro_types::{WorkflowSettings, fixtures, test_support};\n use fabro_util::error::collect_chain;\n use fabro_validate::Severity;\n use object_store::local::LocalFileSystem;\n@@ -1099,7 +1099,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1166,7 +1166,7 @@ mod tests {\n }),\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1277,7 +1277,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1322,7 +1322,7 @@ mod tests {\n }),\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1389,7 +1389,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1435,7 +1435,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: Some(fabro_types::RunProvenance {\n+ provenance: fabro_types::RunProvenance {\n server: Some(fabro_types::RunServerProvenance {\n version: \"0.9.0\".to_string(),\n }),\n@@ -1444,12 +1444,12 @@ mod tests {\n name: Some(\"fabro-cli\".to_string()),\n version: Some(\"0.9.0\".to_string()),\n }),\n- subject: Some(fabro_types::Principal::user(\n+ subject: fabro_types::Principal::user(\n fabro_types::IdpIdentity::new(\"https://github.com\", \"12345\").unwrap(),\n \"octocat\".to_string(),\n fabro_types::AuthMethod::Github,\n- )),\n- }),\n+ ),\n+ },\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1462,7 +1462,7 @@ mod tests {\n let run_store = store.open_run_reader(&created.run_id).await.unwrap();\n let state = run_store.state().await.unwrap();\n let run = state.spec;\n- let provenance = run.provenance.expect(\"provenance should be projected\");\n+ let provenance = run.provenance;\n \n assert_eq!(provenance.server.unwrap().version, \"0.9.0\");\n assert_eq!(\n@@ -1470,7 +1470,7 @@ mod tests {\n Some(\"fabro-cli\")\n );\n assert_eq!(\n- provenance.subject.unwrap(),\n+ provenance.subject,\n fabro_types::Principal::user(\n fabro_types::IdpIdentity::new(\"https://github.com\", \"12345\").unwrap(),\n \"octocat\".to_string(),\ndiff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs\nindex 513975d71..eb3a6720e 100644\n--- a/lib/crates/fabro-workflow/src/operations/fork.rs\n+++ b/lib/crates/fabro-workflow/src/operations/fork.rs\n@@ -283,7 +283,7 @@ mod tests {\n \n use fabro_graphviz::graph::Graph;\n use fabro_store::{Database, RunProjectionReducer};\n- use fabro_types::{StageId, WorkflowSettings, fixtures};\n+ use fabro_types::{StageId, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -381,7 +381,7 @@ mod tests {\n source_directory: Some(\"/client/source\".to_string()),\n workflow_slug: Some(\"fork-source\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: Some(fabro_types::GitContext {\n origin_url: \"https://github.com/example/repo.git\".to_string(),\ndiff --git a/lib/crates/fabro-workflow/src/operations/retry.rs b/lib/crates/fabro-workflow/src/operations/retry.rs\nindex 6551687c5..f71b90626 100644\n--- a/lib/crates/fabro-workflow/src/operations/retry.rs\n+++ b/lib/crates/fabro-workflow/src/operations/retry.rs\n@@ -12,7 +12,7 @@ use crate::event::{self, Event};\n pub struct RetryRunInput {\n pub source_run_id: RunId,\n pub new_run_id: RunId,\n- pub provenance: Option,\n+ pub provenance: RunProvenance,\n pub web_url: Option,\n }\n \n@@ -149,7 +149,7 @@ mod tests {\n version: \"test\".to_string(),\n }),\n client: None,\n- subject: Some(actor(login)),\n+ subject: actor(login),\n }\n }\n \n@@ -187,7 +187,7 @@ mod tests {\n source_directory: Some(\"/workspace/source\".to_string()),\n workflow_slug: Some(\"retry-source\".to_string()),\n db_prefix: None,\n- provenance: Some(provenance(\"source-user\")),\n+ provenance: provenance(\"source-user\"),\n manifest_blob,\n git: Some(git_context()),\n fork_source_ref,\n@@ -345,7 +345,7 @@ mod tests {\n let outcome = retry_run(&store, &RetryRunInput {\n source_run_id,\n new_run_id: RunId::new(),\n- provenance: Some(provenance(\"retry-user\")),\n+ provenance: provenance(\"retry-user\"),\n web_url: Some(\"http://localhost:3000/runs/retry\".to_string()),\n })\n .await\n@@ -379,14 +379,7 @@ mod tests {\n assert_eq!(retry_state.spec.manifest_blob, manifest_blob);\n assert_eq!(retry_state.spec.definition_blob, definition_blob);\n assert_eq!(retry_state.spec.fork_source_ref, Some(fork_source_ref));\n- assert_eq!(\n- retry_state\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.as_ref()),\n- Some(&actor(\"retry-user\"))\n- );\n+ assert_eq!(&retry_state.spec.provenance.subject, &actor(\"retry-user\"));\n assert_eq!(\n retry_state.web_url.as_deref(),\n Some(\"http://localhost:3000/runs/retry\")\n@@ -469,7 +462,7 @@ mod tests {\n let err = retry_run(&store, &RetryRunInput {\n source_run_id: run_id,\n new_run_id: RunId::new(),\n- provenance: None,\n+ provenance: provenance(\"retry-user\"),\n web_url: None,\n })\n .await\n@@ -487,7 +480,7 @@ mod tests {\n let err = retry_run(&store, &RetryRunInput {\n source_run_id: fixtures::RUN_1,\n new_run_id: RunId::new(),\n- provenance: None,\n+ provenance: provenance(\"retry-user\"),\n web_url: None,\n })\n .await\ndiff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs\nindex a962cd0e5..adf2741d7 100644\n--- a/lib/crates/fabro-workflow/src/operations/start.rs\n+++ b/lib/crates/fabro-workflow/src/operations/start.rs\n@@ -1112,7 +1112,9 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::settings::run::RunMode;\n use fabro_types::settings::{InterpString, ModelRef};\n- use fabro_types::{BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures};\n+ use fabro_types::{\n+ BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures, test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -1416,7 +1418,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1837,7 +1839,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\ndiff --git a/lib/crates/fabro-workflow/src/operations/timeline.rs b/lib/crates/fabro-workflow/src/operations/timeline.rs\nindex 2170dc28a..fdfdea119 100644\n--- a/lib/crates/fabro-workflow/src/operations/timeline.rs\n+++ b/lib/crates/fabro-workflow/src/operations/timeline.rs\n@@ -204,6 +204,7 @@ mod tests {\n use chrono::Utc;\n use fabro_types::{\n Checkpoint, CheckpointRecord, Graph, RunDiff, RunSpec, WorkflowSettings, fixtures,\n+ test_support,\n };\n \n use super::*;\n@@ -247,7 +248,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\nindex cef35cc39..39392007b 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n@@ -18,7 +18,9 @@ use fabro_interview::AutoApproveInterviewer;\n use fabro_sandbox::SandboxSpec;\n use fabro_store::Database;\n use fabro_types::settings::run::RunModelControls;\n-use fabro_types::{Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref};\n+use fabro_types::{\n+ Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref, test_support,\n+};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -164,7 +166,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -207,7 +209,7 @@ async fn seed_created_and_starting(\n source_directory: Some(std::env::current_dir().unwrap().display().to_string()),\n workflow_slug: run_options.workflow_slug.clone(),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: run_options.pre_run_git.clone(),\n fork_source_ref: run_options.fork_source_ref.clone(),\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\nindex 05d6db1d0..36b3b7452 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n@@ -651,7 +651,7 @@ mod tests {\n use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};\n use fabro_types::{\n BilledTokenCounts, EventBody, RunBlobId, RunEvent, RunId, RunSpec, StageCompletion,\n- WorkflowSettings, first_event_seq, fixtures,\n+ WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n use object_store::memory::InMemory;\n \n@@ -738,7 +738,7 @@ mod tests {\n source_directory: Some(\"/tmp/project\".to_string()),\n workflow_slug: Some(\"metadata\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -854,7 +854,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\nindex 761a58e42..b8b8e5147 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n@@ -764,7 +764,7 @@ mod tests {\n use fabro_sandbox::SandboxSpec;\n use fabro_store::Database;\n use fabro_types::settings::run::RunModelControls;\n- use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures, test_support};\n use fabro_vault::{SecretType, Vault};\n use object_store::memory::InMemory;\n use tokio::fs::{create_dir_all, write};\n@@ -888,7 +888,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs\nindex ee6150696..93da4da2c 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs\n@@ -59,7 +59,7 @@ mod tests {\n \n use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};\n use fabro_store::{Database, RunDatabase};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -147,7 +147,7 @@ mod tests {\n (\"env\".to_string(), \"test\".to_string()),\n (\"team\".to_string(), \"workflow\".to_string()),\n ]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\nindex 92f4a0bc3..41e1f3434 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n@@ -680,7 +680,7 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::{\n BilledTokenCounts, RunProjection, RunSpec, SuccessReason, WorkflowSettings,\n- first_event_seq, fixtures,\n+ first_event_seq, fixtures, test_support,\n };\n use fabro_vault::{SecretType, Vault};\n use futures::stream;\n@@ -822,7 +822,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1146,7 +1146,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1215,7 +1215,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1569,7 +1569,7 @@ mod tests {\n source_directory: Some(tmp.path().display().to_string()),\n git: None,\n labels: std::collections::HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1696,7 +1696,7 @@ mod tests {\n source_directory: Some(\"/tmp/project\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1713,7 +1713,7 @@ mod tests {\n source_directory: run_spec.source_directory.clone(),\n workflow_slug: run_spec.workflow_slug.clone(),\n db_prefix: None,\n- provenance: None,\n+ provenance: run_spec.provenance.clone(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -1865,7 +1865,7 @@ mod tests {\n source_directory: None,\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1882,7 +1882,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: run_spec.provenance.clone(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs\nindex 5d8cdeb3b..750d2d5ea 100644\n--- a/lib/crates/fabro-workflow/src/run_lookup.rs\n+++ b/lib/crates/fabro-workflow/src/run_lookup.rs\n@@ -457,7 +457,7 @@ mod tests {\n \n use fabro_graphviz::graph::Graph;\n use fabro_store::Database;\n- use fabro_types::{RunStatus, WorkflowSettings, fixtures};\n+ use fabro_types::{RunStatus, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::scan_runs_combined;\n@@ -490,7 +490,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/run_metadata.rs b/lib/crates/fabro-workflow/src/run_metadata.rs\nindex 9d679d0b4..f8606a307 100644\n--- a/lib/crates/fabro-workflow/src/run_metadata.rs\n+++ b/lib/crates/fabro-workflow/src/run_metadata.rs\n@@ -537,7 +537,9 @@ mod tests {\n use std::sync::Arc;\n \n use fabro_store::RunProjection;\n- use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings};\n+ use fabro_types::{\n+ DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings, test_support,\n+ };\n use git2::{ErrorClass, ErrorCode};\n \n use super::*;\n@@ -638,7 +640,7 @@ mod tests {\n push_outcome: PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs\nindex 0f590c70f..e3b464abb 100644\n--- a/lib/crates/fabro-workflow/src/runtime_store.rs\n+++ b/lib/crates/fabro-workflow/src/runtime_store.rs\n@@ -120,7 +120,7 @@ mod tests {\n use fabro_graphviz::graph::Graph;\n use fabro_store::Database;\n use fabro_types::run_event::RunSubmittedProps;\n- use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::RunStoreHandle;\n@@ -147,7 +147,7 @@ mod tests {\n source_directory: Some(\"/tmp/test\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -168,7 +168,7 @@ mod tests {\n source_directory: Some(\"/tmp/test\".to_string()),\n workflow_slug: Some(\"test\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs\nindex c587e49fa..c54affbdc 100644\n--- a/lib/crates/fabro-workflow/src/test_support.rs\n+++ b/lib/crates/fabro-workflow/src/test_support.rs\n@@ -174,7 +174,13 @@ async fn initialized(\n source_directory: Some(sandbox.working_directory().to_string()),\n workflow_slug: run_options.workflow_slug.clone(),\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ },\n manifest_blob: None,\n git: run_options.pre_run_git.clone(),\n fork_source_ref: run_options.fork_source_ref.clone(),\ndiff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex 8e4fa7d1d..03e4463f9 100644\n--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n+++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n@@ -256,7 +256,6 @@ models/preflight-workflow-summary.ts\n models/preview-url-request.ts\n models/preview-url-response.ts\n models/principal-agent.ts\n-models/principal-anonymous.ts\n models/principal-slack.ts\n models/principal-system.ts\n models/principal-user.ts\ndiff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts\nindex 4a208f5e3..2cca8d321 100644\n--- a/lib/packages/fabro-api-client/src/models/index.ts\n+++ b/lib/packages/fabro-api-client/src/models/index.ts\n@@ -232,7 +232,6 @@ export * from './preview-url-request';\n export * from './preview-url-response';\n export * from './principal';\n export * from './principal-agent';\n-export * from './principal-anonymous';\n export * from './principal-slack';\n export * from './principal-system';\n export * from './principal-user';\ndiff --git a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts b/lib/packages/fabro-api-client/src/models/principal-anonymous.ts\ndeleted file mode 100644\nindex daac61df0..000000000\n--- a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts\n+++ /dev/null\n@@ -1,25 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-\n-export interface PrincipalAnonymous {\n- 'kind': PrincipalAnonymousKindEnum;\n-}\n-\n-export const PrincipalAnonymousKindEnum = {\n- ANONYMOUS: 'anonymous'\n-} as const;\n-\n-export type PrincipalAnonymousKindEnum = typeof PrincipalAnonymousKindEnum[keyof typeof PrincipalAnonymousKindEnum];\ndiff --git a/lib/packages/fabro-api-client/src/models/principal.ts b/lib/packages/fabro-api-client/src/models/principal.ts\nindex e3597295d..08b5422df 100644\n--- a/lib/packages/fabro-api-client/src/models/principal.ts\n+++ b/lib/packages/fabro-api-client/src/models/principal.ts\n@@ -24,9 +24,6 @@ import type { IdpIdentity } from './idp-identity';\n import type { PrincipalAgent } from './principal-agent';\n // May contain unused imports in some cases\n // @ts-ignore\n-import type { PrincipalAnonymous } from './principal-anonymous';\n-// May contain unused imports in some cases\n-// @ts-ignore\n import type { PrincipalSlack } from './principal-slack';\n // May contain unused imports in some cases\n // @ts-ignore\n@@ -47,4 +44,4 @@ import type { SystemActorKind } from './system-actor-kind';\n /**\n * @type Principal\n */\n-export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'anonymous' } & PrincipalAnonymous | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker;\n+export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker;\ndiff --git a/lib/packages/fabro-api-client/src/models/run-provenance.ts b/lib/packages/fabro-api-client/src/models/run-provenance.ts\nindex 7276857f8..59fa7a063 100644\n--- a/lib/packages/fabro-api-client/src/models/run-provenance.ts\n+++ b/lib/packages/fabro-api-client/src/models/run-provenance.ts\n@@ -26,5 +26,5 @@ import type { RunServerProvenance } from './run-server-provenance';\n export interface RunProvenance {\n 'server'?: RunServerProvenance | null;\n 'client'?: RunClientProvenance | null;\n- 'subject'?: Principal | null;\n+ 'subject': Principal;\n }\ndiff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts\nindex 2be7312b7..f86098a2d 100644\n--- a/lib/packages/fabro-api-client/src/models/run-spec.ts\n+++ b/lib/packages/fabro-api-client/src/models/run-spec.ts\n@@ -37,7 +37,7 @@ export interface RunSpec {\n 'workflow_slug'?: string | null;\n 'source_directory'?: string | null;\n 'labels'?: { [key: string]: string; };\n- 'provenance'?: RunProvenance | null;\n+ 'provenance': RunProvenance;\n 'manifest_blob'?: string | null;\n 'definition_blob'?: string | null;\n 'git'?: GitContext | null;\ndiff --git a/lib/packages/fabro-api-client/src/models/run.ts b/lib/packages/fabro-api-client/src/models/run.ts\nindex 1ed6c6c26..a4ade38a3 100644\n--- a/lib/packages/fabro-api-client/src/models/run.ts\n+++ b/lib/packages/fabro-api-client/src/models/run.ts\n@@ -83,7 +83,7 @@ export interface Run {\n 'workflow': WorkflowRef;\n 'automation': AutomationRef | null;\n 'repository': RepositoryRef | null;\n- 'created_by': Principal | null;\n+ 'created_by': Principal;\n 'origin': RunOrigin;\n 'labels': { [key: string]: string; };\n 'lifecycle': RunLifecycle;\ndiff --git a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\nindex 7b6bd4afd..568a21780 100644\n--- a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n+++ b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n@@ -12,8 +12,6 @@ export function principalKind(principal: Principal): string {\n switch (principal.kind) {\n case \"agent\":\n return \"agent\";\n- case \"anonymous\":\n- return \"anonymous\";\n case \"slack\":\n return \"slack\";\n case \"system\":\n", + "summary": { + "files_changed": 97, + "additions": 415, + "deletions": 340 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-05-27T05:24:20.725730Z", + "current_node": "simplify_opus", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "implement", + "simplify_opus" + ], + "node_retries": {}, + "context_values": { + "thread.toolchain.current_node": "preflight_compile", "internal.work_dir": "/home/daytona/workspace/fabro", "internal.node_visit_count": 1, "graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option`, no nullable OpenAPI fields, no legacy deserialization defaults, and no fallback creator in projection code.\n\nTwo commits, in order.\n\n---\n\n## Commit 1 - Remove `Principal::Anonymous`\n\nBreaking cleanup. `Principal` becomes actor-only. Missing/invalid auth is represented as absent request principal, not as an anonymous principal variant.\n\n### Rust\n\n`lib/crates/fabro-types/src/principal.rs`:\n- Drop `Anonymous`.\n- Drop `Anonymous` arms in `kind()` and `display()`.\n- Delete anonymous serialization/round-trip test coverage.\n\n`lib/crates/fabro-server/src/principal_middleware.rs`:\n- `RequestAuthContext.principal: Principal` -> `Option`.\n- `RequestAuthLogContext.principal: Principal` -> `Option`.\n- `initial()` and `rejected()` set `principal: None`.\n- `authenticated(...)`, `authenticated_worker(...)`, and `authenticated_user(...)` set `principal: Some(...)`.\n- Update `principal_without_log_unused_fields` to preserve `None` and strip user avatar data only inside `Some(Principal::User(...))`.\n- Update all gate helpers to match `Option`:\n - `require_user`\n - `require_authenticated_user`\n - `require_run_management_actor`\n - `require_worker_or_user_for_run`\n - `require_run_management_target`\n- `None` routes to the existing `auth_rejection(context.auth_status, context.auth_error_code)` behavior.\n- `Some(Principal::Worker { .. })` keeps the current forbidden-vs-auth-rejection distinctions.\n- Update tests that assert the initial/rejected principal to assert `None`.\n\n`lib/crates/fabro-server/src/server.rs` HTTP logging:\n- Keep the `principal_kind` field on every HTTP log line.\n- Compute `principal_kind` as `auth_context.principal.as_ref().map(Principal::kind).unwrap_or(\"none\")`.\n- Match `auth_context.principal` as an `Option`:\n - `Some(User(...))`, `Some(Worker { ... })`, `Some(Webhook { ... })`, `Some(Slack { ... })` keep their extra fields.\n - `None | Some(Agent { .. } | System { .. })` emits only the common HTTP fields.\n\n`docs/internal/logging-strategy.md`:\n- Replace the `anonymous` HTTP caller category guidance with `none` for requests that have no principal.\n- Keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state.\n\n### OpenAPI and generated clients\n\n`docs/public/api-reference/fabro-api.yaml`:\n- Remove `PrincipalAnonymous` from the `Principal` `oneOf`.\n- Remove `anonymous` from the `Principal` discriminator mapping.\n- Delete the `PrincipalAnonymous` schema.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nExpected generated cleanup:\n- `lib/packages/fabro-api-client/src/models/principal-anonymous.ts` disappears.\n- `Principal` union no longer includes `{ kind: \"anonymous\" }`.\n- `lib/packages/fabro-api-client/src/models/index.ts` no longer exports `principal-anonymous`.\n\n### Frontend\n\n`apps/fabro-web/app/lib/principal-display.tsx`:\n- Remove the `\"anonymous\"` switch case and unused icon import.\n\n`apps/fabro-web/app/components/run-summary-panel.test.tsx` and API-client exhaustiveness tests:\n- Remove anonymous principal cases.\n\n### Documentation sweep\n\nRemove anonymous-principal references from product/API docs and tests. Be careful not to touch unrelated uses of \"anonymous\" such as telemetry anonymous IDs or Git's `remote_anonymous` API.\n\nUseful sweep:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|kind: 'anonymous'|kind: \\\"anonymous\\\"|anonymous actor|anonymous subject|principal_kind.*anonymous|\\\"anonymous\\\"\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cd apps/fabro-web && bun run typecheck && bun test`\n- Manual: start `fabro server start`, hit a protected endpoint without a token, confirm 401 and an HTTP log with `principal_kind=\"none\"` and `auth_status=\"missing\"`.\n\n---\n\n## Commit 2 - Make run provenance and creator non-optional\n\nFull-chain invariant. Every persisted run has exactly one creator principal. No nullable schema fields, no legacy defaults, no projection fallbacks.\n\n### Core type changes\n\n`lib/crates/fabro-types/src/run_summary.rs`:\n- `Run.created_by: Option` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option` -> `RunProvenance`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]` on `RunSpec.provenance`.\n\n`lib/crates/fabro-types/src/run_event/run.rs`:\n- `RunCreatedProps.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n`lib/crates/fabro-workflow/src/event/events.rs`:\n- `Event::RunCreated.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n### Creation and retry flow\n\n`lib/crates/fabro-workflow/src/operations/create.rs`:\n- `CreateRunInput.provenance: Option` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option` -> `RunProvenance`.\n- `RunSpec { provenance }` stores the total provenance directly.\n- `Event::RunCreated { provenance }` emits total provenance directly.\n\n`lib/crates/fabro-server/src/server/handler/runs.rs`:\n- `run_provenance(headers, subject)` returns `RunProvenance { subject: subject.clone(), ... }`.\n- Build provenance before creating `CreateRunInput`.\n\n`lib/crates/fabro-server/src/run_manifest.rs`:\n- Change `create_run_input(...)` to accept `provenance: RunProvenance` and set it directly, or stop using the helper for the final `CreateRunInput` construction. Do not create a temporary input with missing provenance.\n\n`lib/crates/fabro-workflow/src/operations/retry.rs`:\n- `RetryRunInput.provenance: Option` -> `RunProvenance`.\n- `retry_run(...)` writes the new run's `run.created` event with total provenance.\n\n`lib/crates/fabro-server/src/server/handler/lifecycle.rs`:\n- Pass `run_provenance(&headers, &actor)` directly into `RetryRunInput`.\n\n### Event conversion and projections\n\n`lib/crates/fabro-workflow/src/event/convert.rs`:\n- Convert `Event::RunCreated.provenance` into `RunCreatedProps.provenance` directly.\n- Remove `Some(...)` wrapping for run-created provenance.\n\n`lib/crates/fabro-workflow/src/event/stored_fields.rs`:\n- `Event::RunCreated { provenance, .. }` sets `actor: Some(provenance.subject.clone())`.\n\n`lib/crates/fabro-store/src/run_state.rs`:\n- `projection_from_created(...)` builds `RunSpec { provenance: props.provenance.clone(), ... }`.\n- `build_summary(...)` sets `created_by: state.spec.provenance.subject.clone()`.\n- Delete or rewrite tests that deserialize projections with `\"provenance\": null`.\n\n`lib/crates/fabro-types/src/run_projection.rs` and projection tests:\n- Replace all test `RunSpec` literals with total provenance.\n- Remove tests whose only purpose is legacy/null provenance tolerance.\n\n### OpenAPI\n\n`docs/public/api-reference/fabro-api.yaml`:\n- `Run.created_by` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunProvenance.required` includes `subject`.\n- `RunProvenance.subject` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunSpec.required` includes `provenance`.\n- `RunSpec.provenance` references `RunProvenance` directly. Remove `oneOf [..., null]`.\n- If `run.created` event properties are represented separately in the spec, make that event provenance required and non-nullable too.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nDo not hand-edit generated client files.\n\n### Demo mode\n\n`lib/crates/fabro-server/src/demo/mod.rs`:\n- Add a clearly synthetic demo principal using `AuthMethod::DevToken`, not GitHub:\n ```rust\n static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| {\n Principal::user(\n IdpIdentity::new(\"fabro:demo\", \"demo\").unwrap(),\n \"demo\".to_string(),\n AuthMethod::DevToken,\n )\n });\n ```\n- Replace `created_by: None` with `created_by: DEMO_PRINCIPAL.clone()`.\n- If demo creates any full `RunSpec` or `run.created` event data, give it `RunProvenance { subject: DEMO_PRINCIPAL.clone(), ... }`.\n\n### Test support\n\nDo not add fake auth helpers to `fabro_types::fixtures`; that module is run-id constants.\n\nUse the existing `fabro-types` `test-support` feature:\n- Add `#[cfg(any(test, feature = \"test-support\"))] pub mod test_support;` in `lib/crates/fabro-types/src/lib.rs` if it does not already exist.\n- Add `lib/crates/fabro-types/src/test_support.rs` with:\n - `test_principal() -> Principal`\n - `test_run_provenance() -> RunProvenance`\n- Use an obviously fake dev-token identity, e.g. issuer `fabro:test`, subject `test-user`, login `test`.\n- In crates that need the helper from integration tests or cross-crate tests, dual-list `fabro-types` in `dev-dependencies` with `features = [\"test-support\"]`, following existing repo patterns.\n\nUpdate all constructors:\n- Replace `provenance: None` in `RunSpec`, `CreateRunInput`, `RetryRunInput`, `Event::RunCreated`, and `RunCreatedProps` literals with `test_run_provenance()` or a locally meaningful provenance.\n- Replace `subject: Some(...)` with `subject: ...`.\n- Replace `subject: None` only when it is actually `RunProvenance.subject`; leave unrelated todo/commit/message `subject` fields alone.\n- Replace `created_by: None` / `created_by: null` with `test_principal()` or a frontend TS principal fixture.\n- Delete tests that assert nullable or omitted creator/provenance behavior.\n\nRepresentative Rust areas:\n- `lib/crates/fabro-store/src/run_state.rs`\n- `lib/crates/fabro-store/tests/serializable_projection.rs`\n- `lib/crates/fabro-workflow/src/operations/{create,retry,start}.rs`\n- `lib/crates/fabro-workflow/src/event/{convert,sink,stored_fields}.rs`\n- `lib/crates/fabro-workflow/src/handler/**`\n- `lib/crates/fabro-workflow/src/pipeline/**`\n- `lib/crates/fabro-workflow/src/run_{lookup,metadata}.rs`\n- `lib/crates/fabro-server/src/server/tests.rs`\n- `lib/crates/fabro-server/src/server/handler/**`\n- `lib/crates/fabro-server/tests/it/**`\n- `lib/crates/fabro-cli/tests/it/support/mod.rs`\n- `lib/crates/fabro-dump/src/lib.rs`\n- `lib/crates/fabro-tool/src/{common,create,interact,search}.rs`\n- `lib/crates/fabro-api/tests/{principal_round_trip,run_summary_round_trip,run_projection_round_trip,run_event_round_trip}.rs`\n- `lib/crates/fabro-types/tests/{run_spec_serde,run_spec_methods,run_event_serde}.rs`\n\nRepresentative TypeScript areas:\n- `apps/fabro-web/app/**` tests with `created_by: null`\n- `apps/fabro-web/app/data/runs.ts`\n- `apps/fabro-web/app/components/run-summary-panel.tsx`\n- `apps/fabro-web/app/components/runs-list/**`\n- `lib/packages/fabro-api-client/tests/principal-exhaustive.ts`\n\nUseful sweep after edits:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|principal-anonymous|kind: ['\\\"]anonymous|created_by:\\\\s*(None|null)|provenance:\\\\s*None|subject:\\\\s*Some\\\\(|subject:\\\\s*None\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\nReview each hit. The only acceptable remaining matches should be unrelated uses of \"anonymous\" and unrelated non-principal `subject` fields.\n\n### Frontend\n\n`apps/fabro-web/app/components/run-summary-panel.tsx`:\n- `run?.created_by` may still be guarded by `run` loading state, but `created_by` itself is non-null once `run` exists.\n- Pass `run.created_by` directly to `principalDisplay(...)` inside loaded-run branches.\n\n`apps/fabro-web/app/data/runs.ts` and run-list components:\n- Treat `createdBy` as a total principal in UI data derived from a loaded API run.\n- Remove empty/fallback rendering that only existed for missing creator data.\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Manual end-to-end:\n - `fabro server start`\n - `cd apps/fabro-web && bun run dev`\n - Authenticate and create a run through the UI.\n - Confirm `/api/v1/runs/:id` has non-null `created_by`.\n - Confirm `/api/v1/runs/:id/state` has non-null `spec.provenance.subject`.\n - Retry a failed run and confirm the retried run has the retrying user as creator.\n - Hit demo mode with `X-Fabro-Demo: 1` and confirm the run summary renders the synthetic `demo` dev-token user.\n", "internal.retry_count.toolchain": 0, "failure_signature": "", "internal.retry_count.start": 0, - "failure_class": "", "internal.retry_count.preflight_compile": 0, + "thread.preflight_compile.current_node": "preflight_lint", "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", "internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A", - "thread.preflight_compile.current_node": "preflight_lint", - "outcome": "succeeded", - "graph.rankdir": "LR", - "thread.start.current_node": "toolchain", - "internal.retry_count.preflight_lint": 0, - "internal.fidelity": "compact", - "last_stage": "implement", "response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.", - "last_response": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; mis", + "last_response": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_prove", + "graph.rankdir": "LR", + "response.simplify_opus": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`.\n\n2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms.\n\n3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports.\n\n**Findings skipped** with rationale:\n- `Option` → `RequestAuth` sum type: too invasive; the existing shape is readable.\n- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible.\n- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape.\n- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope.\n- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention.\n- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only.\n\n**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`).", + "internal.retry_count.simplify_opus": 0, "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", - "current_node": "implement", "thread.preflight_lint.current_node": "implement", - "internal.thread_id": "preflight_lint", - "thread.toolchain.current_node": "preflight_compile" + "internal.thread_id": "implement", + "last_stage": "simplify_opus", + "failure_class": "", + "thread.implement.current_node": "simplify_opus", + "internal.retry_count.preflight_lint": 0, + "outcome": "succeeded", + "thread.start.current_node": "toolchain", + "internal.fidelity": "compact", + "current_node": "simplify_opus", + "internal.retry_count.implement": 0 }, "node_outcomes": { "implement": { @@ -881,6 +1032,55 @@ "active_time_ms": 1456 } }, + "simplify_opus": { + "status": "succeeded", + "context_updates": { + "last_stage": "simplify_opus", + "last_response": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_prove", + "response.simplify_opus": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`.\n\n2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms.\n\n3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports.\n\n**Findings skipped** with rationale:\n- `Option` → `RequestAuth` sum type: too invasive; the existing shape is readable.\n- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible.\n- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape.\n- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope.\n- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention.\n- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only.\n\n**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`)." + }, + "notes": "Stage completed: simplify_opus", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 69536, + "output_tokens": 25178, + "reasoning_tokens": 0, + "cache_read_tokens": 3797227, + "cache_write_tokens": 362231 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 362231, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 5139686 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/run_manifest.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/runs.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/Cargo.toml", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_event_serde.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_methods.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_serde.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/test_support.rs" + ], + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 403076, + "tool_time_ms": 639117, + "active_time_ms": 1042193 + } + }, "start": { "status": "succeeded", "usage": null @@ -914,10 +1114,11 @@ } } }, - "next_node_id": "simplify_opus", + "next_node_id": "simplify_gpt", "node_visits": { "preflight_compile": 1, "implement": 1, + "simplify_opus": 1, "toolchain": 1, "preflight_lint": 1, "start": 1 @@ -995,11 +1196,305 @@ }, "state": "succeeded" }, + "simplify_opus@1": { + "first_event_seq": 1738, + "prompt": null, + "response": null, + "completion": null, + "provider_used": { + "mode": "agent", + "provider": "anthropic", + "model": "claude-opus-4-7" + }, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "started_at": "2026-05-27T05:06:57.700061Z", + "handler": "agent", + "usage": { + "input_tokens": 69536, + "output_tokens": 25178, + "total_tokens": 4254172, + "reasoning_tokens": 0, + "cache_read_tokens": 3797227, + "cache_write_tokens": 362231, + "total_usd_micros": 5139686 + }, + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "todos": { + "kind": "anthropic_tasks", + "list_id": "anthropic_tasks:58be8615-4937-4eb3-ab2c-ef0327ef19ed", + "items": [ + { + "id": "1", + "status": "completed", + "order": 0, + "subject": "Consolidate test_run_provenance Engine variant", + "description": "Add test_run_provenance_system() to fabro-types::test_support, dual-list fabro-types in its own dev-deps with test-support feature, delete 4 duplicates in fabro-types integration tests and fabro-workflow/src/test_support.rs.", + "active_form": "Consolidating test_run_provenance Engine duplicates" + }, + { + "id": "2", + "status": "completed", + "order": 1, + "subject": "Split HTTP log match arm for principal absence vs stateless", + "description": "Split `None | Some(Principal::Agent { .. } | Principal::System { .. })` in server.rs HTTP log macro into two distinct arms so future fields on Agent/System aren't silently swallowed.", + "active_form": "Splitting HTTP log match arm" + }, + { + "id": "3", + "status": "completed", + "order": 2, + "subject": "Inline create_run_input helper", + "description": "The create_run_input helper in run_manifest.rs has a single caller in handler/runs.rs that immediately post-mutates run_id and submitted_manifest_bytes. Inline it to make the data flow obvious and stop growing positional args.", + "active_form": "Inlining create_run_input helper" + } + ] + }, + "subagents": [ + { + "agent_id": "aa31f620", + "depth": 1, + "task": "You are reviewing a code change for **code reuse opportunities**. The diff is at `/tmp/review.diff` (2945 lines). The repository root is `/home/daytona/workspace/fabro`.\n\nThe change does two things:\n1. Removes `Principal::Anonymous` variant\n2. Makes `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `Event::RunCreated.provenance` non-optional\n\nA new test_support module was added:\n- `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()`\n- `apps/fabro-web/app/lib/test-principal.ts` with a TS principal fixture\n\nCrates that consume `fabro-types` for test fixtures now dual-list `fabro-types` in dev-dependencies with `features = [\"test-support\"]` (fabro-api, fabro-cli, fabro-store, fabro-tool).\n\nYour task:\n1. Read `/tmp/review.diff` thoroughly.\n2. For each new function/helper added in the diff, search the codebase for existing utilities that already do the same thing. Use grep heavily.\n3. Specifically check:\n - Is the new `test_principal()` / `test_run_provenance()` duplicating an existing fixture or builder anywhere?\n - Is the TS `test-principal.ts` duplicating helpers in apps/fabro-web/app/test-utils or similar?\n - Are there inline `Principal::user(...)` constructions in tests/handlers that could just use `test_principal()` (especially in demo/mod.rs)?\n - Does `lib/crates/fabro-server/src/demo/mod.rs` `DEMO_PRINCIPAL` and the test fixture overlap suspiciously?\n - Any duplicate hand-rolled `RunProvenance { subject: ..., ..Default::default() }` literals that could use a helper?\n4. Flag inline logic that could use existing utilities.\n\nReport findings as a concrete list with file paths, line references, and the existing utility name to use. Skip nitpicks; only flag real duplications.\n\nNote: greenfield app — prefer simplicity, no backwards-compat needed.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 52 + } + }, + { + "agent_id": "ec61ae37", + "depth": 1, + "task": "You are reviewing a code change for **code quality** (hacky patterns). The diff is at `/tmp/review.diff` (2945 lines). The repository root is `/home/daytona/workspace/fabro`.\n\nThe change does two things:\n1. Removes `Principal::Anonymous` variant — `Principal` is now actor-only; missing auth becomes `Option` in middleware.\n2. Makes run creator/provenance non-optional: `Run.created_by: Principal`, `RunSpec.provenance: RunProvenance`, `RunProvenance.subject: Principal`, `Event::RunCreated.provenance: RunProvenance`.\n\nA new test_support module was added in `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()`. `apps/fabro-web/app/lib/test-principal.ts` has the TS equivalent.\n\nRead `/tmp/review.diff` and flag hacky patterns:\n\n1. **Redundant state**: Are there fields whose values are always derived from another? E.g. is `Run.created_by` redundant with `RunSpec.provenance.subject` (they should be the same person — both stored)? If yes, flag.\n2. **Parameter sprawl**: New parameters added rather than restructuring. e.g. `create_run_input(provenance: RunProvenance, ...)` vs builder pattern.\n3. **Copy-paste with slight variation**: Are there near-duplicate `RunProvenance { subject: ..., ..Default::default() }` literals across handlers/tests that should share a helper? Look at `run_provenance(headers, subject)` and similar.\n4. **Leaky abstractions**: Look at `principal_middleware.rs` — does the `Option` plumbing leak details that should be encapsulated by an enum like `AuthOutcome { Authenticated(Principal), Missing, Invalid }`? Be opinionated.\n5. **Stringly-typed code**: Any new string literals where a constant/enum already exists? Look especially for \"fabro:demo\", \"fabro:test\", \"demo\", \"test-user\" hardcoded in multiple places.\n6. **`Default` derive removal**: The plan says drop `Default` from `RunProvenance`. Are there places still using `..Default::default()` for `RunProvenance` that should now be explicit?\n7. **Demo principal**: Look at `lib/crates/fabro-server/src/demo/mod.rs` — is `DEMO_PRINCIPAL` using `LazyLock` or just a function? Is it cloned everywhere when an `&Principal` would do?\n8. **Match arm collapsing**: In server.rs HTTP logging, look at the `None | Some(Agent { .. } | System { .. })` arm — is the pattern clean?\n9. **TS test fixture**: Is `apps/fabro-web/app/lib/test-principal.ts` putting test code in a non-test location? Check filename convention (should be `*.test-utils.ts` or in `__tests__`?).\n\nReport concrete findings with file paths and line refs. Be aggressive — this is greenfield. Skip nitpicks but flag real issues.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 50 + } + }, + { + "agent_id": "1b8d560d", + "depth": 1, + "task": "You are reviewing a code change for **efficiency**. The diff is at `/tmp/review.diff` (2945 lines). The repository root is `/home/daytona/workspace/fabro`.\n\nThe change removes `Principal::Anonymous` and makes run creator/provenance fields non-optional.\n\nRead `/tmp/review.diff` and flag efficiency issues:\n\n1. **Unnecessary work / clones**: Was `Option` cheap-to-pass while `Principal` is now always cloned in hot paths (HTTP logging, projections, event conversion)? Look for new `.clone()` calls on `Principal`/`RunProvenance` and ask whether a reference would do. Specifically check:\n - `lib/crates/fabro-server/src/server.rs` HTTP logging\n - `lib/crates/fabro-store/src/run_state.rs` `build_summary`/`projection_from_created`\n - `lib/crates/fabro-workflow/src/event/convert.rs`\n - `lib/crates/fabro-server/src/demo/mod.rs` — `DEMO_PRINCIPAL.clone()` calls\n2. **LazyLock overhead**: Is `static DEMO_PRINCIPAL: LazyLock` justified, or could it be a const/function? `Principal::user(...)` calls `IdpIdentity::new(...).unwrap()` so it can't be const — confirm LazyLock is necessary.\n3. **N+1 / repeated computation**: Any place doing per-request work that should be cached?\n4. **Hot-path bloat**: Is anything new added to request lifecycle (middleware, per-event projection) that does extra work?\n5. **Overly broad operations**: E.g. cloning a full `Principal` (which has a `User` variant carrying avatar URL, login, email...) when only the kind is needed.\n\nLook closely at the server.rs HTTP logging change to ensure `principal_kind` computation isn't doing redundant work.\n\nReport concrete findings with file paths and line refs.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 54 + } + } + ], + "permission_level": "full", + "agent_tools": [ + { + "name": "AskUserQuestion", + "description": "Ask the human one or more questions and wait for their answers before continuing this stage.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "TaskCreate", + "description": "Create pending tasks in the current session. Use concise subjects, descriptions, optional activeForm text, and metadata. Check TaskList first to avoid duplicate tasks.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": true + }, + { + "name": "TaskGet", + "description": "Get one task by taskId, including subject, status, description, owner, blockedBy, and blocks.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "TaskList", + "description": "List tasks for the current session, including status, owner, and blocking dependencies. Use TaskGet with a taskId for full description and dependency details.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "TaskUpdate", + "description": "Update an existing task's status, text, owner, metadata, or dependencies. Valid statuses are pending, in_progress, completed, and deleted. After completing a task, call TaskList to find newly unblocked work.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": true + }, + { + "name": "close_agent", + "description": "Close a running subagent that is no longer needed.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": false + }, + { + "name": "edit_file", + "description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.", + "source": { + "kind": "native" + }, + "category": "write", + "invoked": true + }, + { + "name": "glob", + "description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.", + "source": { + "kind": "native" + }, + "category": "read", + "invoked": true + }, + { + "name": "grep", + "description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.", + "source": { + "kind": "native" + }, + "category": "read", + "invoked": true + }, + { + "name": "read_file", + "description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.", + "source": { + "kind": "native" + }, + "category": "read", + "invoked": true + }, + { + "name": "send_input", + "description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": false + }, + { + "name": "shell", + "description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.", + "source": { + "kind": "native" + }, + "category": "shell", + "invoked": true + }, + { + "name": "spawn_agent", + "description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": true + }, + { + "name": "wait", + "description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": true + }, + { + "name": "web_fetch", + "description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "web_search", + "description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "write_file", + "description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.", + "source": { + "kind": "native" + }, + "category": "write", + "invoked": false + } + ], + "context_window": { + "provider": "anthropic", + "model": "claude-opus-4-7", + "context_window_tokens": 1000000, + "input_tokens": 82014, + "usage_percent": 8.2014, + "count_method": "response_usage_scaled_breakdown", + "staleness": "live", + "generated_at": "2026-05-27T05:24:20.661649Z", + "event_seq": 2244, + "breakdown": [ + { + "category": "system_prompt", + "tokens": 2411, + "usage_percent": 0.2411 + }, + { + "category": "tools", + "tokens": 2791, + "usage_percent": 0.2791 + }, + { + "category": "memory", + "tokens": 5965, + "usage_percent": 0.5965 + }, + { + "category": "conversation", + "tokens": 70839, + "usage_percent": 7.0839 + }, + { + "category": "other", + "tokens": 8, + "usage_percent": 0.0008 + } + ], + "warnings": [] + }, + "state": "running" + }, "implement@1": { "first_event_seq": 51, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Stage completed: implement", + "failure_reason": null, + "timestamp": "2026-05-27T05:06:52.896823Z" + }, "provider_used": { "mode": "agent", "provider": "openai", @@ -1013,6 +1508,12 @@ "output": null, "started_at": "2026-05-27T03:17:49.390830Z", "handler": "agent", + "timing": { + "wall_time_ms": 6543491, + "inference_time_ms": 4850345, + "tool_time_ms": 1392988, + "active_time_ms": 6243333 + }, "usage": { "input_tokens": 9898125, "output_tokens": 51088, @@ -1230,7 +1731,7 @@ ], "warnings": [] }, - "state": "running" + "state": "succeeded" }, "preflight_lint@1": { "first_event_seq": 41, diff --git a/stages/005-implement@1/diff.patch b/stages/005-implement@1/diff.patch new file mode 100644 index 000000000..d6cfe579b --- /dev/null +++ b/stages/005-implement@1/diff.patch @@ -0,0 +1,2945 @@ +diff --git a/apps/fabro-web/app/components/run-summary-panel.test.tsx b/apps/fabro-web/app/components/run-summary-panel.test.tsx +index f93a3fd8a..0a3ff3bbd 100644 +--- a/apps/fabro-web/app/components/run-summary-panel.test.tsx ++++ b/apps/fabro-web/app/components/run-summary-panel.test.tsx +@@ -6,6 +6,7 @@ import { + RunSummaryPanelView, + type RunSummaryPanelViewProps, + } from "./run-summary-panel"; ++import { testPrincipal } from "../lib/test-principal"; + + function instanceText(instance: TestRenderer.ReactTestInstance): string { + const parts: string[] = []; +@@ -53,7 +54,7 @@ function cellAfterLabel( + function makeRun(overrides: Record = {}) { + return { + id: "run_1", +- created_by: null, ++ created_by: testPrincipal(), + diff: null, + billing: null, + ...overrides, +@@ -71,9 +72,8 @@ describe("RunSummaryPanelView", () => { + } + }); + +- test("shows unavailable copy for missing run fields after load", () => { ++ test("shows unavailable copy for missing optional run fields after load", () => { + const tree = render({ run: makeRun() }); +- expect(instanceText(cellAfterLabel(tree, "Created by"))).toBe(EMPTY_VALUE); + expect(instanceText(cellAfterLabel(tree, "Changes"))).toBe(EMPTY_VALUE); + expect(instanceText(cellAfterLabel(tree, "Cost"))).toBe(EMPTY_VALUE); + }); +@@ -174,7 +174,7 @@ describe("RunSummaryPanelView", () => { + kind: "user", + identity: { issuer: "github", subject: "1" }, + login: "brynary", +- auth_method: "oauth", ++ auth_method: "github", + }, + }), + }); +@@ -188,7 +188,7 @@ describe("RunSummaryPanelView", () => { + kind: "user", + identity: { issuer: "github", subject: "1" }, + login: "brynary", +- auth_method: "oauth", ++ auth_method: "github", + avatar_url: "https://example.com/brynary.png", + }, + }), +@@ -200,7 +200,7 @@ describe("RunSummaryPanelView", () => { + }); + + test("renders non-user actor with kind label", () => { +- for (const kind of ["agent", "system", "slack", "webhook", "worker", "anonymous"]) { ++ for (const kind of ["agent", "system", "slack", "webhook", "worker"]) { + const tree = render({ run: makeRun({ created_by: { kind } as any }) }); + expect(instanceText(cellAfterLabel(tree, "Created by"))).toContain(kind); + } +diff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx +index 5d9c78ce4..8e3f6db65 100644 +--- a/apps/fabro-web/app/components/run-summary-panel.tsx ++++ b/apps/fabro-web/app/components/run-summary-panel.tsx +@@ -92,7 +92,7 @@ export function RunSummaryPanelView({ + artifactsCount, + artifactsLoading, + }: RunSummaryPanelViewProps) { +- const created = run?.created_by ? principalDisplay(run.created_by) : null; ++ const created = run ? principalDisplay(run.created_by) : null; + const diff = run?.diff ?? null; + const cost = formatUsdMicros(run?.billing?.total_usd_micros); + +diff --git a/apps/fabro-web/app/components/runs-list/run-table-row.tsx b/apps/fabro-web/app/components/runs-list/run-table-row.tsx +index ca521fb92..6f38e2425 100644 +--- a/apps/fabro-web/app/components/runs-list/run-table-row.tsx ++++ b/apps/fabro-web/app/components/runs-list/run-table-row.tsx +@@ -35,6 +35,7 @@ export function RunTableRow({ + }) { + const lifecycleLabel = listLifecycleStatusLabel(run); + const statusDisplay = columnStatusDisplay[run.status]; ++ const createdBy = principalDisplay(run.createdBy); + const show = (col: ToggleableColumn) => !hiddenColumns.has(col); + + return ( +@@ -54,14 +55,9 @@ export function RunTableRow({ + + {show("created_by") && ( + +- {run.createdBy && (() => { +- const display = principalDisplay(run.createdBy); +- return ( +- +- {display.glyph} +- +- ); +- })()} ++ ++ {createdBy.glyph} ++ + + )} + {show("repo") && ( +diff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts +index 98586b2c4..1894ffe27 100644 +--- a/apps/fabro-web/app/data/runs.test.ts ++++ b/apps/fabro-web/app/data/runs.test.ts +@@ -8,6 +8,7 @@ import { + mapRunToRunItem, + runStatusDisplay, + } from "./runs"; ++import { testPrincipal } from "../lib/test-principal"; + + function makeRun(overrides: Partial = {}): Run { + return { +@@ -17,7 +18,7 @@ function makeRun(overrides: Partial = {}): Run { + workflow: { slug: "fix_build", name: "Fix Build", graph_name: "FixBuild", node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: "myrepo", origin_url: null, provider: "unknown" }, +- created_by: null, ++ created_by: testPrincipal(), + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts +index f820b24c1..e5595cf81 100644 +--- a/apps/fabro-web/app/data/runs.ts ++++ b/apps/fabro-web/app/data/runs.ts +@@ -40,7 +40,7 @@ export interface RunItem { + sandboxWorkingDirectory?: string; + sourceDirectory?: string; + createdAt?: string; +- createdBy?: Principal | null; ++ createdBy: Principal; + lastEventAt?: string; + size?: RunSize; + } +diff --git a/apps/fabro-web/app/lib/principal-display.tsx b/apps/fabro-web/app/lib/principal-display.tsx +index fa9d4ec16..666f0f64c 100644 +--- a/apps/fabro-web/app/lib/principal-display.tsx ++++ b/apps/fabro-web/app/lib/principal-display.tsx +@@ -4,7 +4,6 @@ import { + ChatBubbleLeftEllipsisIcon, + Cog6ToothIcon, + CpuChipIcon, +- QuestionMarkCircleIcon, + ServerIcon, + } from "@heroicons/react/20/solid"; + import type { Principal } from "@qltysh/fabro-api-client"; +@@ -57,10 +56,5 @@ export function principalDisplay(actor: Principal): PrincipalDisplay { + return { glyph: principalIconGlyph(), label: "webhook" }; + case "worker": + return { glyph: principalIconGlyph(), label: "worker" }; +- case "anonymous": +- return { +- glyph: principalIconGlyph(), +- label: "anonymous", +- }; + } + } +diff --git a/apps/fabro-web/app/lib/run-actions.test.ts b/apps/fabro-web/app/lib/run-actions.test.ts +index 8acde997f..2780ee2ed 100644 +--- a/apps/fabro-web/app/lib/run-actions.test.ts ++++ b/apps/fabro-web/app/lib/run-actions.test.ts +@@ -24,6 +24,7 @@ import { + unarchiveRuns, + } from "./run-actions"; + import { generatedAxios } from "./api-client"; ++import { testPrincipal } from "./test-principal"; + + type StubResponseInit = { + status: number; +@@ -47,7 +48,7 @@ function makeRun(status: RunStatus, archived = false): Run { + workflow: { slug: "fix_build", name: "Fix Build", graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: null, +- created_by: null, ++ created_by: testPrincipal(), + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/lib/test-principal.ts b/apps/fabro-web/app/lib/test-principal.ts +new file mode 100644 +index 000000000..7ecf7ad9e +--- /dev/null ++++ b/apps/fabro-web/app/lib/test-principal.ts +@@ -0,0 +1,10 @@ ++import type { Principal } from "@qltysh/fabro-api-client"; ++ ++export function testPrincipal(login = "test"): Principal { ++ return { ++ kind: "user", ++ identity: { issuer: "fabro:test", subject: `${login}-user` }, ++ login, ++ auth_method: "dev_token", ++ }; ++} +diff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts +index e30d38da8..08ecf0411 100644 +--- a/apps/fabro-web/app/routes/run-detail.test.ts ++++ b/apps/fabro-web/app/routes/run-detail.test.ts +@@ -178,6 +178,7 @@ import { + } from "./run-detail/lifecycle-toasts"; + + const { default: RunDetail } = await import("./run-detail"); ++const { testPrincipal } = await import("../lib/test-principal"); + mock.restore(); + type LifecycleToastState = import("./run-detail/lifecycle-toasts").LifecycleToastState; + type RunDetailActionResult = import("./run-detail/lifecycle-toasts").RunDetailActionResult; +@@ -209,7 +210,7 @@ function makeRunSummary( + workflow: { slug: "default", name: "Default", graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: "fabro", origin_url: null, provider: "unknown" }, +- created_by: null, ++ created_by: testPrincipal(), + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/routes/run-files.render.test.tsx b/apps/fabro-web/app/routes/run-files.render.test.tsx +index 5c9a24722..6a4678d60 100644 +--- a/apps/fabro-web/app/routes/run-files.render.test.tsx ++++ b/apps/fabro-web/app/routes/run-files.render.test.tsx +@@ -4,6 +4,7 @@ import TestRenderer, { act } from "react-test-renderer"; + import { MemoryRouter, Route, Routes } from "react-router"; + + import { ToastProvider } from "../components/toast"; ++import { testPrincipal } from "../lib/test-principal"; + + let currentFilesPayload: any = null; + let currentCommitsPayload: any = null; +@@ -50,7 +51,7 @@ mock.module("../lib/queries", () => ({ + workflow: { slug: "default", name: "Default", graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: "fabro", origin_url: null, provider: "unknown" }, +- created_by: null, ++ created_by: testPrincipal(), + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/routes/runs.preferences.test.tsx b/apps/fabro-web/app/routes/runs.preferences.test.tsx +index 648d8e35a..137a62a78 100644 +--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx ++++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx +@@ -4,6 +4,7 @@ import { createMemoryRouter, RouterProvider } from "react-router"; + import type { PaginatedRunList, Run } from "@qltysh/fabro-api-client"; + + import { ToastProvider } from "../components/toast"; ++import { testPrincipal } from "../lib/test-principal"; + import { setupReactTestEnv } from "../lib/test-utils"; + + class MemoryStorage { +@@ -34,7 +35,7 @@ function run(id: string, repo = "qlty/fabro", workflow = "release"): Run { + workflow: { slug: workflow, name: workflow, graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: repo, origin_url: null, provider: "github" }, +- created_by: null, ++ created_by: testPrincipal(), + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx +index 51483cff2..2e5f65bde 100644 +--- a/apps/fabro-web/app/routes/runs.test.tsx ++++ b/apps/fabro-web/app/routes/runs.test.tsx +@@ -11,6 +11,7 @@ import { + shouldRefreshBoardForEvent, + } from "./runs"; + import { summarizeBatchLifecycleAction } from "../components/runs-list/batch-lifecycle"; ++import { testPrincipal } from "../lib/test-principal"; + + function boardRun(id: string, column: BoardColumn, questionText?: string): Run { + const status = +@@ -34,7 +35,7 @@ function boardRun(id: string, column: BoardColumn, questionText?: string): Run { + workflow: { slug: "test", name: "Test", graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: "repo", origin_url: null, provider: "unknown" }, +- created_by: null, ++ created_by: testPrincipal(), + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md +index 63f6f8f54..0a7c712e6 100644 +--- a/docs/internal/logging-strategy.md ++++ b/docs/internal/logging-strategy.md +@@ -118,11 +118,11 @@ Fields are key-value pairs that make events queryable. Include enough context th + | `error` | Error value on failure | + | `path` | File system path | + | `duration_ms` | Elapsed time in milliseconds | +-| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) | ++| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) | + | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) | + | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests | + +-For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`). ++For HTTP request logs, use the request principal projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`). Requests with no principal use `principal_kind="none"`; keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state. + + Server auth intentionally exposes a mutable `RequestAuth` context slot for public auth routes and guard extractors such as `RequiredUser` / `RequireRunScoped` for protected routes. There is no loose `RequestPrincipal` extractor; route-facing extractors should enforce the route's auth contract while the slot supplies the final HTTP log fields. + | `input_tokens` | Token count for LLM input | +diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml +index b39bd5f1a..8a49fb17a 100644 +--- a/docs/public/api-reference/fabro-api.yaml ++++ b/docs/public/api-reference/fabro-api.yaml +@@ -7863,6 +7863,8 @@ components: + + RunProvenance: + type: object ++ required: ++ - subject + properties: + server: + oneOf: +@@ -7873,9 +7875,7 @@ components: + - $ref: "#/components/schemas/RunClientProvenance" + - type: "null" + subject: +- oneOf: +- - $ref: "#/components/schemas/Principal" +- - type: "null" ++ $ref: "#/components/schemas/Principal" + + Principal: + oneOf: +@@ -7885,7 +7885,6 @@ components: + - $ref: "#/components/schemas/PrincipalSlack" + - $ref: "#/components/schemas/PrincipalAgent" + - $ref: "#/components/schemas/PrincipalSystem" +- - $ref: "#/components/schemas/PrincipalAnonymous" + discriminator: + propertyName: kind + mapping: +@@ -7895,7 +7894,6 @@ components: + slack: "#/components/schemas/PrincipalSlack" + agent: "#/components/schemas/PrincipalAgent" + system: "#/components/schemas/PrincipalSystem" +- anonymous: "#/components/schemas/PrincipalAnonymous" + + PrincipalUser: + type: object +@@ -7985,15 +7983,6 @@ components: + system_kind: + $ref: "#/components/schemas/SystemActorKind" + +- PrincipalAnonymous: +- type: object +- required: +- - kind +- properties: +- kind: +- type: string +- enum: [anonymous] +- + RunEvent: + description: > + Internal RunEvent-compatible JSON payload. The server validates this +@@ -9121,6 +9110,7 @@ components: + - run_id + - settings + - graph ++ - provenance + properties: + run_id: + type: string +@@ -9140,9 +9130,7 @@ components: + additionalProperties: + type: string + provenance: +- oneOf: +- - $ref: "#/components/schemas/RunProvenance" +- - type: "null" ++ $ref: "#/components/schemas/RunProvenance" + manifest_blob: + type: ["string", "null"] + definition_blob: +@@ -9454,9 +9442,7 @@ components: + - $ref: "#/components/schemas/RepositoryRef" + - type: "null" + created_by: +- oneOf: +- - $ref: "#/components/schemas/Principal" +- - type: "null" ++ $ref: "#/components/schemas/Principal" + origin: + $ref: "#/components/schemas/RunOrigin" + labels: +diff --git a/docs/public/changelog/2026-05-02.mdx b/docs/public/changelog/2026-05-02.mdx +index 48a501a59..2d55f8d05 100644 +--- a/docs/public/changelog/2026-05-02.mdx ++++ b/docs/public/changelog/2026-05-02.mdx +@@ -11,7 +11,7 @@ The dock listens to interview events and refreshes as questions arrive, so a par + + ## Principal attribution and auth routing + +-Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, agents, and anonymous actors. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from. ++Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, and agents. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from. + + This also closes attribution gaps across web, CLI, worker-token, Slack, and human-interview paths. Runs created or advanced through different surfaces now preserve who or what took the action more consistently. + +@@ -19,7 +19,7 @@ This also closes attribution gaps across web, CLI, worker-token, Slack, and huma + + + - Run specs now include client and server provenance shapes +-- Run events use unified principal shapes for user, worker, system, Slack, webhook, agent, and anonymous subjects ++- Run events use unified principal shapes for user, worker, system, Slack, webhook, and agent subjects + + + +diff --git a/lib/crates/fabro-api/Cargo.toml b/lib/crates/fabro-api/Cargo.toml +index 8b347f032..459e6bfda 100644 +--- a/lib/crates/fabro-api/Cargo.toml ++++ b/lib/crates/fabro-api/Cargo.toml +@@ -32,3 +32,6 @@ serde_json = "1" + serde_yaml = "0.9" + prettyplease = "0.2" + syn = "2" ++ ++[dev-dependencies] ++fabro-types = { path = "../fabro-types", features = ["test-support"] } +diff --git a/lib/crates/fabro-api/tests/principal_round_trip.rs b/lib/crates/fabro-api/tests/principal_round_trip.rs +index ca1180e60..ac2b2c258 100644 +--- a/lib/crates/fabro-api/tests/principal_round_trip.rs ++++ b/lib/crates/fabro-api/tests/principal_round_trip.rs +@@ -118,7 +118,6 @@ fn principal_round_trips_every_variant_through_api_type() { + Principal::System { + system_kind: SystemActorKind::Watchdog, + }, +- Principal::Anonymous, + ]; + + for principal in variants { +@@ -140,9 +139,9 @@ fn run_provenance_subject_round_trips_as_principal() { + name: Some("fabro-cli".to_string()), + version: Some("0.1.0".to_string()), + }), +- subject: Some(Principal::Worker { ++ subject: Principal::Worker { + run_id: fixtures::RUN_1, +- }), ++ }, + }; + let json = serde_json::to_value(&provenance).unwrap(); + +diff --git a/lib/crates/fabro-api/tests/run_event_round_trip.rs b/lib/crates/fabro-api/tests/run_event_round_trip.rs +index e39f3d8f4..8e1cf5d36 100644 +--- a/lib/crates/fabro-api/tests/run_event_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs +@@ -1,7 +1,7 @@ + use std::any::{TypeId, type_name}; + + use fabro_api::types::RunEvent as ApiRunEvent; +-use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures}; ++use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures, test_support}; + use serde_json::{Value, json}; + + #[test] +@@ -20,7 +20,8 @@ fn run_event_round_trips_run_created() { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/fabro/run-1", +- "source_directory": "/tmp/fabro/run-1" ++ "source_directory": "/tmp/fabro/run-1", ++ "provenance": test_support::test_run_provenance() + } + }); + +@@ -39,6 +40,7 @@ fn run_event_round_trips_run_created_with_web_url() { + "graph": Graph::new("test"), + "run_dir": "/tmp/fabro/run-1", + "source_directory": "/tmp/fabro/run-1", ++ "provenance": test_support::test_run_provenance(), + "web_url": format!("http://localhost:3000/runs/{}", fixtures::RUN_1) + } + }); +diff --git a/lib/crates/fabro-api/tests/run_projection_round_trip.rs b/lib/crates/fabro-api/tests/run_projection_round_trip.rs +index 64a00df91..30295d76a 100644 +--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs +@@ -1,7 +1,7 @@ + use std::any::{TypeId, type_name}; + + use fabro_api::types::RunProjection as ApiRunProjection; +-use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings}; ++use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, test_support}; + use serde_json::json; + + #[test] +@@ -130,7 +130,7 @@ fn run_spec_json() -> serde_json::Value { + workflow_slug: None, + source_directory: None, + labels: std::collections::HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-api/tests/run_summary_round_trip.rs b/lib/crates/fabro-api/tests/run_summary_round_trip.rs +index 21d237434..1e94c7b1b 100644 +--- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs +@@ -12,7 +12,7 @@ use fabro_types::{ + AskFabro, AskFabroUnavailableReason, DiffSummary, PullRequestLink, RepositoryProvider, + RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary, RunId, RunLifecycle, + RunLinks, RunOrigin, RunRunnableSource, RunSize, RunTimestamps, RunTiming, WorkflowRef, +- fixtures, ++ fixtures, test_support, + }; + use serde_json::json; + +@@ -83,7 +83,7 @@ fn run_summary_json_matches_openapi_shape() { + origin_url: None, + provider: RepositoryProvider::Unknown, + }), +- created_by: None, ++ created_by: test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::from([("team".to_string(), "core".to_string())]), + lifecycle: RunLifecycle { +@@ -152,7 +152,15 @@ fn run_summary_json_matches_openapi_shape() { + "origin_url": null, + "provider": "unknown" + }, +- "created_by": null, ++ "created_by": { ++ "kind": "user", ++ "identity": { ++ "issuer": "fabro:test", ++ "subject": "test-user" ++ }, ++ "login": "test", ++ "auth_method": "dev_token" ++ }, + "origin": { + "kind": "api" + }, +@@ -244,6 +252,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() { + "origin_url": null, + "provider": "unknown" + }, ++ "created_by": test_support::test_principal(), + "models": [], + "timestamps": { + "created_at": "2026-04-20T12:00:00Z", +@@ -276,6 +285,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() { + provider: RepositoryProvider::Unknown, + }) + ); ++ assert_eq!(summary.created_by, test_support::test_principal()); + assert_eq!(summary.timestamps.started_at, None); + assert_eq!(summary.timestamps.created_at, created_at); + assert_eq!(summary.timestamps.last_event_at, None); +diff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml +index 4bff98f8d..9568ac22a 100644 +--- a/lib/crates/fabro-cli/Cargo.toml ++++ b/lib/crates/fabro-cli/Cargo.toml +@@ -129,3 +129,4 @@ fabro-test = { workspace = true } + fabro-macros = { path = "../fabro-macros" } + hkdf.workspace = true + reqwest = { workspace = true, features = ["cookies"] } ++fabro-types = { path = "../fabro-types", features = ["clap", "test-support"] } +diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs +index c8ad2fff2..8dbb31b35 100644 +--- a/lib/crates/fabro-cli/src/commands/run/attach.rs ++++ b/lib/crates/fabro-cli/src/commands/run/attach.rs +@@ -822,6 +822,7 @@ mod tests { + )] + + use fabro_interview::{Answer, AnswerValue}; ++ use fabro_types::test_support; + use fabro_util::terminal::Styles; + use httpmock::MockServer; + +@@ -840,7 +841,7 @@ mod tests { + workflow_slug: None, + source_directory: None, + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +index ebcf1a2a0..f426c167b 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +@@ -221,7 +221,21 @@ fn inspect_resolves_selector_via_server_endpoint() { + "attrs": {} + }, + "workflow_slug": "remote-workflow", +- "source_directory": "/srv/repo" ++ "source_directory": "/srv/repo", ++ "provenance": { ++ "server": { ++ "version": "test" ++ }, ++ "subject": { ++ "kind": "user", ++ "identity": { ++ "issuer": "fabro:test", ++ "subject": "test-user" ++ }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + }, + "start_record": null, + "conclusion": null, +diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs +index 9ac2e7673..d8572ac19 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs +@@ -177,6 +177,15 @@ pub(crate) fn remote_run_summary_json( + "origin_url": null, + "provider": "unknown" + }, ++ "created_by": { ++ "kind": "user", ++ "identity": { ++ "issuer": "fabro:test", ++ "subject": "test-user" ++ }, ++ "login": "test", ++ "auth_method": "dev_token" ++ }, + "origin": { + "kind": "api" + }, +diff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs +index 7c7c59a22..41150f3f2 100644 +--- a/lib/crates/fabro-cli/tests/it/support/mod.rs ++++ b/lib/crates/fabro-cli/tests/it/support/mod.rs +@@ -9,7 +9,7 @@ pub(crate) use auth_harness::{ + pub(crate) use auth_tokens::{TEST_SESSION_SECRET, issue_test_github_jwt, issue_test_worker_jwt}; + use fabro_store::EventEnvelope; + use fabro_test::{EnvVars, TestContext, preserve_coverage_env}; +-use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings}; ++use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings, test_support}; + + pub(crate) fn run_output_filters(context: &TestContext) -> Vec<(String, String)> { + let mut filters = context.filters(); +@@ -48,7 +48,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s + workflow_slug: Some("remote-workflow".to_string()), + source_directory: Some("/srv/repo".to_string()), + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-dump/src/lib.rs b/lib/crates/fabro-dump/src/lib.rs +index d44e01215..ae3598666 100644 +--- a/lib/crates/fabro-dump/src/lib.rs ++++ b/lib/crates/fabro-dump/src/lib.rs +@@ -475,7 +475,7 @@ mod tests { + use fabro_types::{ + Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunStatus, + SandboxProviderKind, StageCompletion, StageModelUsage, StageOutcome, StartRecord, +- SuccessReason, WorkflowSettings, first_event_seq, fixtures, ++ SuccessReason, WorkflowSettings, first_event_seq, fixtures, test_support, + }; + use futures::executor; + +@@ -497,7 +497,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::from([("team".to_string(), "platform".to_string())]), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/crates/fabro-server/src/auth/cli_flow.rs +index 5e6a9f9cc..e45b933de 100644 +--- a/lib/crates/fabro-server/src/auth/cli_flow.rs ++++ b/lib/crates/fabro-server/src/auth/cli_flow.rs +@@ -1667,11 +1667,11 @@ client_id = "github-client-id" + let [first, second, third] = <[RequestAuthContext; 3]>::try_from(contexts) + .expect("expected three captured auth contexts"); + assert_eq!(first.auth_status, AuthStatus::Authenticated); +- assert_eq!(first.principal.display(), "octocat"); ++ assert_eq!(first.principal.as_ref().unwrap().display(), "octocat"); + assert_eq!(second.auth_status, AuthStatus::Authenticated); +- assert_eq!(second.principal.display(), "octocat"); ++ assert_eq!(second.principal.as_ref().unwrap().display(), "octocat"); + assert_eq!(third.auth_status, AuthStatus::Authenticated); +- assert_eq!(third.principal.display(), "octocat"); ++ assert_eq!(third.principal.as_ref().unwrap().display(), "octocat"); + } + + #[tokio::test] +@@ -2076,7 +2076,7 @@ client_id = "github-client-id" + + let contexts = captured.lock().expect("captured auth contexts").clone(); + assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated); +- assert_eq!(contexts[0].principal.display(), "octocat"); ++ assert_eq!(contexts[0].principal.as_ref().unwrap().display(), "octocat"); + assert_eq!(contexts[1].auth_status, AuthStatus::Invalid); + assert_eq!( + contexts[1].auth_error_code, +@@ -2269,8 +2269,8 @@ client_id = "github-client-id" + + let contexts = captured.lock().expect("captured auth contexts").clone(); + assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated); +- assert_eq!(contexts[0].principal.display(), "octocat"); +- let Principal::User(user) = &contexts[0].principal else { ++ assert_eq!(contexts[0].principal.as_ref().unwrap().display(), "octocat"); ++ let Some(Principal::User(user)) = &contexts[0].principal else { + panic!("expected user principal"); + }; + assert_eq!( +diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs +index 42e19a97c..b731cfd2e 100644 +--- a/lib/crates/fabro-server/src/demo/mod.rs ++++ b/lib/crates/fabro-server/src/demo/mod.rs +@@ -1080,7 +1080,7 @@ fn ts(s: &str) -> DateTime { + + mod runs { + use std::collections::HashMap; +- use std::sync::OnceLock; ++ use std::sync::{LazyLock, OnceLock}; + use std::time::Duration; + + use fabro_api::types::*; +@@ -1091,13 +1091,22 @@ mod runs { + }; + use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace}; + use fabro_types::{ +- PendingReason, RepositoryRef, RunBillingSummary, RunId, RunLifecycle, RunLinks, RunOrigin, +- RunSize, RunTimestamps, StageId, WorkflowRef, WorkflowSettings, ++ AuthMethod, IdpIdentity, PendingReason, Principal, RepositoryRef, RunBillingSummary, RunId, ++ RunLifecycle, RunLinks, RunOrigin, RunSize, RunTimestamps, StageId, WorkflowRef, ++ WorkflowSettings, + }; + + use super::ts; + use crate::server::run_stage_from_stage_id; + ++ static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| { ++ Principal::user( ++ IdpIdentity::new("fabro:demo", "demo").expect("demo identity should be valid"), ++ "demo".to_string(), ++ AuthMethod::DevToken, ++ ) ++ }); ++ + fn labels(entries: &[(&str, &str)]) -> HashMap { + entries + .iter() +@@ -1170,7 +1179,7 @@ mod runs { + repo_origin_url, + source_directory.as_deref(), + )), +- created_by: None, ++ created_by: DEMO_PRINCIPAL.clone(), + origin: RunOrigin::default(), + labels: labels(entries), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-server/src/principal_middleware.rs b/lib/crates/fabro-server/src/principal_middleware.rs +index acc9eb250..b1741e190 100644 +--- a/lib/crates/fabro-server/src/principal_middleware.rs ++++ b/lib/crates/fabro-server/src/principal_middleware.rs +@@ -19,7 +19,7 @@ use crate::worker_token::{self, WORKER_TOKEN_KID, WorkerScopeSet}; + + #[derive(Clone, Debug)] + pub(crate) struct RequestAuthContext { +- pub principal: Principal, ++ pub principal: Option, + pub auth_status: AuthStatus, + pub auth_error_code: Option, + pub user_profile: Option, +@@ -74,7 +74,7 @@ impl RequestAuthContext { + #[must_use] + pub(crate) fn initial() -> Self { + Self { +- principal: Principal::Anonymous, ++ principal: None, + auth_status: AuthStatus::Missing, + auth_error_code: None, + user_profile: None, +@@ -85,7 +85,7 @@ impl RequestAuthContext { + #[must_use] + pub(crate) fn authenticated(principal: Principal, user_profile: Option) -> Self { + Self { +- principal, ++ principal: Some(principal), + auth_status: AuthStatus::Authenticated, + auth_error_code: None, + user_profile, +@@ -96,7 +96,7 @@ impl RequestAuthContext { + #[must_use] + pub(crate) fn authenticated_worker(run_id: RunId, scopes: WorkerScopeSet) -> Self { + Self { +- principal: Principal::Worker { run_id }, ++ principal: Some(Principal::Worker { run_id }), + auth_status: AuthStatus::Authenticated, + auth_error_code: None, + user_profile: None, +@@ -123,7 +123,7 @@ impl RequestAuthContext { + #[must_use] + pub(crate) fn rejected(status: AuthStatus, code: Option) -> Self { + Self { +- principal: Principal::Anonymous, ++ principal: None, + auth_status: status, + auth_error_code: code, + user_profile: None, +@@ -146,7 +146,7 @@ impl AuthStatus { + + #[derive(Clone, Debug)] + pub(crate) struct RequestAuthLogContext { +- pub principal: Principal, ++ pub principal: Option, + pub auth_status: AuthStatus, + pub auth_error_code: Option, + } +@@ -170,22 +170,23 @@ impl AuthContextSlot { + pub(crate) fn log_snapshot(&self) -> RequestAuthLogContext { + let context = self.0.lock().expect("auth context lock poisoned"); + RequestAuthLogContext { +- principal: principal_without_log_unused_fields(&context.principal), ++ principal: principal_without_log_unused_fields(context.principal.as_ref()), + auth_status: context.auth_status, + auth_error_code: context.auth_error_code, + } + } + } + +-fn principal_without_log_unused_fields(principal: &Principal) -> Principal { ++fn principal_without_log_unused_fields(principal: Option<&Principal>) -> Option { + match principal { +- Principal::User(user) => Principal::User(UserPrincipal { ++ Some(Principal::User(user)) => Some(Principal::User(UserPrincipal { + identity: user.identity.clone(), + login: user.login.clone(), + auth_method: user.auth_method, + avatar_url: None, +- }), +- principal => principal.clone(), ++ })), ++ Some(principal) => Some(principal.clone()), ++ None => None, + } + } + +@@ -370,7 +371,7 @@ fn auth_slot_from_parts(parts: &Parts) -> AuthContextSlot { + pub(crate) fn require_user(slot: &AuthContextSlot) -> Result { + let context = slot.0.lock().expect("auth context lock poisoned"); + match &context.principal { +- Principal::User(user) => Ok(user.clone()), ++ Some(Principal::User(user)) => Ok(user.clone()), + _ => Err(auth_rejection(context.auth_status, context.auth_error_code)), + } + } +@@ -380,7 +381,7 @@ pub(crate) fn require_authenticated_user( + ) -> Result { + let context = slot.snapshot(); + match context.principal { +- Principal::User(principal) => { ++ Some(Principal::User(principal)) => { + let Some(profile) = context.user_profile else { + return Err(ApiError::new( + StatusCode::INTERNAL_SERVER_ERROR, +@@ -396,11 +397,11 @@ pub(crate) fn require_authenticated_user( + pub(crate) fn require_run_management_actor(slot: &AuthContextSlot) -> Result { + let context = slot.0.lock().expect("auth context lock poisoned"); + match &context.principal { +- Principal::User(user) => Ok(Principal::User(user.clone())), +- Principal::Worker { run_id } if context.worker_scopes.has_agent_run_tools() => { ++ Some(Principal::User(user)) => Ok(Principal::User(user.clone())), ++ Some(Principal::Worker { run_id }) if context.worker_scopes.has_agent_run_tools() => { + Ok(Principal::Worker { run_id: *run_id }) + } +- Principal::Worker { .. } => Err(ApiError::forbidden()), ++ Some(Principal::Worker { .. }) => Err(ApiError::forbidden()), + _ => Err(auth_rejection(context.auth_status, context.auth_error_code)), + } + } +@@ -411,9 +412,9 @@ fn require_worker_or_user_for_run( + ) -> Result<(), ApiError> { + let context = slot.0.lock().expect("auth context lock poisoned"); + match &context.principal { +- Principal::User(_) => Ok(()), +- Principal::Worker { run_id } if run_id == route_run_id => Ok(()), +- Principal::Worker { .. } => Err(ApiError::forbidden()), ++ Some(Principal::User(_)) => Ok(()), ++ Some(Principal::Worker { run_id }) if run_id == route_run_id => Ok(()), ++ Some(Principal::Worker { .. }) => Err(ApiError::forbidden()), + _ => Err(auth_rejection(context.auth_status, context.auth_error_code)), + } + } +@@ -424,13 +425,13 @@ fn require_run_management_target( + ) -> Result { + let context = slot.0.lock().expect("auth context lock poisoned"); + match &context.principal { +- Principal::User(user) => Ok(Principal::User(user.clone())), +- Principal::Worker { run_id } ++ Some(Principal::User(user)) => Ok(Principal::User(user.clone())), ++ Some(Principal::Worker { run_id }) + if run_id == route_run_id || context.worker_scopes.has_agent_run_tools() => + { + Ok(Principal::Worker { run_id: *run_id }) + } +- Principal::Worker { .. } => Err(ApiError::forbidden()), ++ Some(Principal::Worker { .. }) => Err(ApiError::forbidden()), + _ => Err(auth_rejection(context.auth_status, context.auth_error_code)), + } + } +@@ -646,7 +647,7 @@ mod tests { + let context = classify_request(&request, state.as_ref()); + + assert_eq!(context.auth_status, AuthStatus::Authenticated); +- assert!(matches!(context.principal, Principal::User(_))); ++ assert!(matches!(context.principal, Some(Principal::User(_)))); + assert!(context.user_profile.is_some()); + } + +@@ -686,7 +687,7 @@ mod tests { + let context = classify_request(&request, state.as_ref()); + + assert_eq!(context.auth_status, AuthStatus::Authenticated); +- assert_eq!(context.principal, Principal::Worker { run_id }); ++ assert_eq!(context.principal, Some(Principal::Worker { run_id })); + assert!(!context.worker_scopes.has_agent_run_tools()); + } + +@@ -705,7 +706,7 @@ mod tests { + let context = classify_request(&request, state.as_ref()); + + assert_eq!(context.auth_status, AuthStatus::Authenticated); +- assert_eq!(context.principal, Principal::Worker { run_id }); ++ assert_eq!(context.principal, Some(Principal::Worker { run_id })); + assert!(context.worker_scopes.has_agent_run_tools()); + } + +@@ -784,7 +785,7 @@ mod tests { + + assert_eq!(context.auth_status, AuthStatus::Missing); + assert_eq!(context.auth_error_code, None); +- assert_eq!(context.principal, Principal::Anonymous); ++ assert_eq!(context.principal, None); + } + + #[test] +diff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/crates/fabro-server/src/run_files.rs +index 53657282a..082f13025 100644 +--- a/lib/crates/fabro-server/src/run_files.rs ++++ b/lib/crates/fabro-server/src/run_files.rs +@@ -1715,7 +1715,7 @@ fn count_flags(data: &[FileDiff]) -> (u64, u64, u64, u64) { + mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + +- use fabro_types::{CommandTermination, RunId}; ++ use fabro_types::{CommandTermination, RunId, test_support}; + use tokio::time::{Duration, sleep}; + + use super::*; +@@ -2386,7 +2386,7 @@ index 1111111..2222222 160000 + workflow_slug: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs +index a004872cb..1760d76cf 100644 +--- a/lib/crates/fabro-server/src/run_manifest.rs ++++ b/lib/crates/fabro-server/src/run_manifest.rs +@@ -26,7 +26,9 @@ use fabro_static::EnvVars; + use fabro_types::settings::cli::OutputVerbosity; + use fabro_types::settings::interp::InterpString; + use fabro_types::settings::run::{EnvironmentProvider, RunGoal, RunNamespace}; +-use fabro_types::{ManifestPath, RunId, SandboxProviderKind, ServerSettings, WorkflowSettings}; ++use fabro_types::{ ++ ManifestPath, RunId, RunProvenance, SandboxProviderKind, ServerSettings, WorkflowSettings, ++}; + use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus}; + use fabro_validate::Severity; + use fabro_workflow::Error as WorkflowError; +@@ -201,6 +203,7 @@ pub(crate) fn validate_prepared_manifest( + pub(crate) fn create_run_input( + prepared: PreparedManifest, + configured_providers: Vec, ++ provenance: RunProvenance, + web_url: Option, + ) -> CreateRunInput { + CreateRunInput { +@@ -216,7 +219,7 @@ pub(crate) fn create_run_input( + git: prepared.git, + fork_source_ref: None, + parent_id: prepared.parent_id, +- provenance: None, ++ provenance, + configured_providers, + web_url, + } +diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs +index b0bb8432c..25316c66a 100644 +--- a/lib/crates/fabro-server/src/server.rs ++++ b/lib/crates/fabro-server/src/server.rs +@@ -1764,7 +1764,10 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp + let status = response.status().as_u16(); + let latency_ms = start.elapsed().as_millis(); + let auth_context = auth_slot.log_snapshot(); +- let principal_kind = auth_context.principal.kind(); ++ let principal_kind = auth_context ++ .principal ++ .as_ref() ++ .map_or("none", Principal::kind); + let auth_status = auth_context.auth_status.as_str(); + + macro_rules! emit_http_log { +@@ -1802,27 +1805,27 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp + macro_rules! emit_principal_http_log { + ($level:ident) => {{ + match &auth_context.principal { +- Principal::User(user) => emit_http_log!( ++ Some(Principal::User(user)) => emit_http_log!( + $level, + user_auth_method = user.auth_method.as_str(), + idp_issuer = user.identity.issuer(), + idp_subject = user.identity.subject(), + login = user.login.as_str(), + ), +- Principal::Worker { run_id } => { ++ Some(Principal::Worker { run_id }) => { + emit_http_log!($level, run_id = run_id.to_string().as_str(),) + } +- Principal::Webhook { delivery_id } => { ++ Some(Principal::Webhook { delivery_id }) => { + emit_http_log!($level, delivery_id = delivery_id.as_str(),) + } +- Principal::Slack { ++ Some(Principal::Slack { + team_id, user_id, .. +- } => emit_http_log!( ++ }) => emit_http_log!( + $level, + team_id = team_id.as_str(), + user_id = user_id.as_str(), + ), +- Principal::Agent { .. } | Principal::System { .. } | Principal::Anonymous => { ++ None | Some(Principal::Agent { .. } | Principal::System { .. }) => { + emit_http_log!($level) + } + } +diff --git a/lib/crates/fabro-server/src/server/handler/events.rs b/lib/crates/fabro-server/src/server/handler/events.rs +index 8e7806522..562e3d5fc 100644 +--- a/lib/crates/fabro-server/src/server/handler/events.rs ++++ b/lib/crates/fabro-server/src/server/handler/events.rs +@@ -535,7 +535,7 @@ mod stage_events_tests { + use axum::body::{Body, to_bytes}; + use axum::http::{Request, StatusCode, header}; + use fabro_store::EventPayload; +- use fabro_types::{Graph, RunId, WorkflowSettings}; ++ use fabro_types::{Graph, RunId, WorkflowSettings, test_support}; + use fabro_workflow::event as workflow_event; + use http_body_util::BodyExt; + use serde_json::json; +@@ -569,7 +569,7 @@ mod stage_events_tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-server/src/server/handler/lifecycle.rs b/lib/crates/fabro-server/src/server/handler/lifecycle.rs +index 0c2697133..2d55e9b33 100644 +--- a/lib/crates/fabro-server/src/server/handler/lifecycle.rs ++++ b/lib/crates/fabro-server/src/server/handler/lifecycle.rs +@@ -864,7 +864,7 @@ async fn retry_run( + let input = operations::RetryRunInput { + source_run_id: id, + new_run_id, +- provenance: Some(run_provenance(&headers, &actor)), ++ provenance: run_provenance(&headers, &actor), + web_url: state.run_web_url(&new_run_id), + }; + match Box::pin(operations::retry_run(&state.store, &input)).await { +diff --git a/lib/crates/fabro-server/src/server/handler/pair.rs b/lib/crates/fabro-server/src/server/handler/pair.rs +index e43f4e6f8..ea64e05ab 100644 +--- a/lib/crates/fabro-server/src/server/handler/pair.rs ++++ b/lib/crates/fabro-server/src/server/handler/pair.rs +@@ -850,7 +850,7 @@ mod tests { + use fabro_types::run_event::AgentMessageProps; + use fabro_types::{ + BilledTokenCounts, EventEnvelope, Graph, PairMessageId, RunEvent, StageId, +- WorkflowSettings, fixtures, ++ WorkflowSettings, fixtures, test_support, + }; + use fabro_workflow::event as workflow_event; + use tower::ServiceExt; +@@ -1023,7 +1023,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs +index 090d1ba5d..e581c8048 100644 +--- a/lib/crates/fabro-server/src/server/handler/runs.rs ++++ b/lib/crates/fabro-server/src/server/handler/runs.rs +@@ -641,13 +641,14 @@ async fn create_run( + .as_ref() + .map(LlmClientResult::provider_ids) + .unwrap_or_default(); ++ let provenance = run_provenance(&headers, &actor); + let mut create_input = run_manifest::create_run_input( + prepared.clone(), + ready_provider_ids.clone(), ++ provenance, + web_url.clone(), + ); + create_input.run_id = Some(run_id); +- create_input.provenance = Some(run_provenance(&headers, &actor)); + create_input.submitted_manifest_bytes = Some(body.to_vec()); + + let storage_root = match resolve_interp_string(&state.server_settings().server.storage.root) { +@@ -817,7 +818,7 @@ pub(super) fn run_provenance(headers: &HeaderMap, subject: &Principal) -> RunPro + version: FABRO_VERSION.to_string(), + }), + client: run_client_provenance(headers), +- subject: Some(subject.clone()), ++ subject: subject.clone(), + } + } + +diff --git a/lib/crates/fabro-server/src/server/handler/sandbox.rs b/lib/crates/fabro-server/src/server/handler/sandbox.rs +index bfba9c242..be7d22023 100644 +--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs ++++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs +@@ -1307,7 +1307,7 @@ FABRO_PROC_NET_TCP /proc/net/tcp6 + mod retrieve_sandbox_tests { + use axum::body::{Body, to_bytes}; + use axum::http::{Request, StatusCode}; +- use fabro_types::{Graph, RunId, WorkflowSettings}; ++ use fabro_types::{Graph, RunId, WorkflowSettings, test_support}; + use serde_json::{Value, json}; + use tower::ServiceExt; + +@@ -1347,6 +1347,7 @@ mod retrieve_sandbox_tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/test", ++ "provenance": test_support::test_run_provenance(), + }, + }), + run_id, +diff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/crates/fabro-server/src/server/handler/sessions.rs +index 1791ee57b..5aa30cfed 100644 +--- a/lib/crates/fabro-server/src/server/handler/sessions.rs ++++ b/lib/crates/fabro-server/src/server/handler/sessions.rs +@@ -1508,6 +1508,7 @@ mod tests { + use fabro_agent::config::ToolAccess; + use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource}; + use fabro_llm::types::{ToolCall, ToolDefinition}; ++ use fabro_types::test_support; + + use super::*; + +@@ -1701,7 +1702,7 @@ mod tests { + workflow_slug: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs +index ba4520d87..62d4331c1 100644 +--- a/lib/crates/fabro-server/src/server/tests.rs ++++ b/lib/crates/fabro-server/src/server/tests.rs +@@ -24,7 +24,7 @@ use fabro_types::{ + SandboxProviderKind, StageContextWindowBreakdownItem, StageContextWindowCategory, + StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, + StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind, +- WorkflowSettings, fixtures, ++ WorkflowSettings, fixtures, test_support, + }; + use fabro_util::check_report::CheckStatus; + use fabro_workflow::records::CheckpointExt; +@@ -3367,7 +3367,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -3412,7 +3412,7 @@ async fn create_slack_notification_run( + source_directory: None, + workflow_slug: workflow_slug.map(str::to_string), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -4418,7 +4418,7 @@ async fn list_run_stages_distinguishes_visits() { + source_directory: None, + workflow_slug: Some("test".to_string()), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -5470,7 +5470,7 @@ async fn create_completed_run_ready_for_pull_request( + source_directory: Some("/tmp/project".to_string()), + git: git.clone(), + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -9272,15 +9272,10 @@ async fn run_tool_worker_token_can_use_client_backend_routes_across_runs() { + .unwrap() + .expect("created run should be cached"); + assert_eq!( +- cached +- .projection +- .spec +- .provenance +- .as_ref() +- .and_then(|provenance| provenance.subject.as_ref()), +- Some(&Principal::Worker { ++ cached.projection.spec.provenance.subject, ++ Principal::Worker { + run_id: parent_run_id, +- }), ++ }, + ); + + let response = app +@@ -11604,7 +11599,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId + source_directory: Some("/tmp/fabro-run".to_string()), + workflow_slug: Some("test".to_string()), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -12353,7 +12348,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() { + source_directory: Some("/tmp/fabro-run".to_string()), + workflow_slug: Some("test".to_string()), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs +index 5f10aba10..9ecd7b68a 100644 +--- a/lib/crates/fabro-server/src/web_auth.rs ++++ b/lib/crates/fabro-server/src/web_auth.rs +@@ -1365,7 +1365,7 @@ client_id = "github-client-id" + + let contexts = captured.lock().expect("captured auth contexts").clone(); + assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated); +- assert!(matches!(contexts[0].principal, Principal::User(_))); ++ assert!(matches!(contexts[0].principal, Some(Principal::User(_)))); + assert_eq!(contexts[1].auth_status, AuthStatus::Invalid); + assert_eq!( + contexts[1].auth_error_code, +diff --git a/lib/crates/fabro-server/tests/it/api/run_files.rs b/lib/crates/fabro-server/tests/it/api/run_files.rs +index e820b00f9..5af21eb92 100644 +--- a/lib/crates/fabro-server/tests/it/api/run_files.rs ++++ b/lib/crates/fabro-server/tests/it/api/run_files.rs +@@ -14,7 +14,7 @@ use axum::body::Body; + use axum::http::{Request, StatusCode}; + use fabro_server::test_support::test_app_state_with_store; + use fabro_store::{ArtifactStore, Database}; +-use fabro_types::{Graph, RunId, WorkflowSettings}; ++use fabro_types::{Graph, RunId, WorkflowSettings, test_support}; + use fabro_workflow::event as workflow_event; + use fabro_workflow::run_status::SuccessReason; + use object_store::memory::InMemory as MemoryObjectStore; +@@ -68,7 +68,7 @@ async fn append_completed_run_with_final_patch( + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-store/Cargo.toml b/lib/crates/fabro-store/Cargo.toml +index 016b55d3d..860ffbf7c 100644 +--- a/lib/crates/fabro-store/Cargo.toml ++++ b/lib/crates/fabro-store/Cargo.toml +@@ -36,3 +36,4 @@ tokio = { workspace = true, features = ["test-util", "macros"] } + tempfile = "3" + ulid.workspace = true + insta = { workspace = true } ++fabro-types = { path = "../fabro-types", features = ["test-support"] } +diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs +index 552891d2e..619e12fb4 100644 +--- a/lib/crates/fabro-store/src/run_state.rs ++++ b/lib/crates/fabro-store/src/run_state.rs +@@ -902,11 +902,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { + }) + .map(|(_, record)| record.question.clone()); + let models = run_models(state); +- let created_by = state +- .spec +- .provenance +- .as_ref() +- .and_then(|provenance| provenance.subject.clone()); ++ let created_by = state.spec.provenance.subject.clone(); + let source_directory = state.spec.source_directory.clone(); + let repo_origin_url = state.spec.git.as_ref().map(|git| git.origin_url.clone()); + let start_time = state.start.as_ref().map(|start| start.start_time); +@@ -1255,7 +1251,7 @@ mod tests { + StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, + StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning, + StageModelUsage, StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings, +- first_event_seq, fixtures, ++ first_event_seq, fixtures, test_support, + }; + use serde_json::json; + +@@ -1336,7 +1332,7 @@ mod tests { + workflow_slug: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -1372,7 +1368,7 @@ mod tests { + } + + #[test] +- fn legacy_run_created_projects_retried_from_none() { ++ fn run_created_projects_retried_from_none() { + let event = test_raw_event( + 1, + "run.created", +@@ -1380,7 +1376,8 @@ mod tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "labels": {}, +- "run_dir": "/tmp/run" ++ "run_dir": "/tmp/run", ++ "provenance": test_support::test_run_provenance() + }), + None, + ); +@@ -1577,7 +1574,7 @@ mod tests { + "repo_origin_url": null, + "base_branch": null, + "labels": {}, +- "provenance": null, ++ "provenance": test_support::test_run_provenance(), + "manifest_blob": null, + "definition_blob": null, + "git": null, +@@ -2605,7 +2602,7 @@ mod tests { + source_directory: Some("/tmp/repo".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -2630,7 +2627,7 @@ mod tests { + source_directory: Some("/tmp/repo".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -2669,7 +2666,8 @@ mod tests { + "attrs": { "goal": { "String": "Goal title" } } + }, + "labels": {}, +- "run_dir": "/tmp/run" ++ "run_dir": "/tmp/run", ++ "provenance": test_support::test_run_provenance() + }), + None, + ); +@@ -2683,7 +2681,7 @@ mod tests { + } + + #[test] +- fn legacy_run_created_without_title_infers_projection_title() { ++ fn run_created_without_title_infers_projection_title() { + let event = test_raw_event( + 1, + "run.created", +@@ -2696,7 +2694,8 @@ mod tests { + "attrs": { "goal": { "String": "## Plan: Legacy title\n\nDetails" } } + }, + "labels": {}, +- "run_dir": "/tmp/run" ++ "run_dir": "/tmp/run", ++ "provenance": test_support::test_run_provenance() + }), + None, + ); +@@ -2725,7 +2724,8 @@ mod tests { + "attrs": { "goal": { "String": "Goal title" } } + }, + "labels": {}, +- "run_dir": "/tmp/run" ++ "run_dir": "/tmp/run", ++ "provenance": test_support::test_run_provenance() + }), + None, + ), +@@ -2769,6 +2769,7 @@ mod tests { + "labels": {}, + "run_dir": "/tmp/run", + "source_directory": "/tmp/run", ++ "provenance": test_support::test_run_provenance(), + "manifest_blob": manifest_blob + } + })) +diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs +index 784868ace..4f1432319 100644 +--- a/lib/crates/fabro-store/src/slate/mod.rs ++++ b/lib/crates/fabro-store/src/slate/mod.rs +@@ -472,7 +472,7 @@ mod tests { + use chrono::{DateTime, Utc}; + use fabro_types::{ + AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId, +- SuccessReason, WorkflowSettings, ++ SuccessReason, WorkflowSettings, test_support, + }; + use futures::TryStreamExt; + use object_store::memory::InMemory; +@@ -541,7 +541,7 @@ mod tests { + workflow_slug: Some("night-sky".to_string()), + source_directory: Some(format!("/tmp/{label}")), + labels: std::collections::HashMap::from([("team".to_string(), "infra".to_string())]), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(fabro_types::GitContext { +@@ -600,6 +600,7 @@ mod tests { + "run_dir": format!("/tmp/{label}"), + "git": run_spec.git, + "labels": run_spec.labels, ++ "provenance": run_spec.provenance, + }), + )) + .await +@@ -625,6 +626,7 @@ mod tests { + "run_dir": format!("/tmp/{label}"), + "git": run_spec.git, + "labels": run_spec.labels, ++ "provenance": run_spec.provenance, + "parent_id": parent_id, + }), + )) +@@ -1299,6 +1301,7 @@ mod tests { + "run_dir": "/tmp/run-2", + "git": run_spec["git"], + "labels": run_spec["labels"], ++ "provenance": run_spec["provenance"], + }, + })) + .unwrap(), +diff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs +index 1718cb662..94116fa94 100644 +--- a/lib/crates/fabro-store/src/slate/run_store.rs ++++ b/lib/crates/fabro-store/src/slate/run_store.rs +@@ -667,7 +667,7 @@ mod tests { + use std::sync::Arc; + use std::time::Duration; + +- use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings}; ++ use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings, test_support}; + use object_store::memory::InMemory; + use serde_json::json; + +@@ -723,6 +723,7 @@ mod tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/test", ++ "provenance": test_support::test_run_provenance(), + }, + }), + run_id, +diff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs +index f6aa256fd..12ee8c932 100644 +--- a/lib/crates/fabro-store/tests/serializable_projection.rs ++++ b/lib/crates/fabro-store/tests/serializable_projection.rs +@@ -8,7 +8,7 @@ use fabro_types::{ + BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord, + QuestionType, RunDiff, RunSandbox, RunSandboxRuntime, RunStatus, SandboxProviderKind, + StageCompletion, StageModelUsage, StageOutcome, StartRecord, WorkflowSettings, first_event_seq, +- fixtures, ++ fixtures, test_support, + }; + use serde_json::json; + +@@ -21,7 +21,7 @@ fn sample_run_spec() -> RunSpec { + workflow_slug: Some("demo".to_string()), + source_directory: Some("/tmp/project".to_string()), + labels: HashMap::from([("team".to_string(), "platform".to_string())]), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(fabro_types::GitContext { +diff --git a/lib/crates/fabro-tool/Cargo.toml b/lib/crates/fabro-tool/Cargo.toml +index 08b6df7cf..c50ea6a38 100644 +--- a/lib/crates/fabro-tool/Cargo.toml ++++ b/lib/crates/fabro-tool/Cargo.toml +@@ -29,4 +29,5 @@ tokio.workspace = true + toml.workspace = true + + [dev-dependencies] ++fabro-types = { path = "../fabro-types", features = ["test-support"] } + tempfile = "3" +diff --git a/lib/crates/fabro-tool/src/common.rs b/lib/crates/fabro-tool/src/common.rs +index 9dd64599a..159ec426d 100644 +--- a/lib/crates/fabro-tool/src/common.rs ++++ b/lib/crates/fabro-tool/src/common.rs +@@ -307,7 +307,9 @@ fn format_tool_error(err: &anyhow::Error) -> String { + #[cfg(test)] + mod tests { + use chrono::{TimeZone, Utc}; +- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef}; ++ use fabro_types::{ ++ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support, ++ }; + + use super::*; + +@@ -413,7 +415,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs +index 8488f8f0b..57e41acd5 100644 +--- a/lib/crates/fabro-tool/src/create.rs ++++ b/lib/crates/fabro-tool/src/create.rs +@@ -508,7 +508,7 @@ mod tests { + use fabro_api::types; + use fabro_types::{ + EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus, +- RunTimestamps, WorkflowRef, ++ RunTimestamps, WorkflowRef, test_support, + }; + use schemars::SchemaGenerator; + use serde_json::json; +@@ -902,7 +902,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/interact.rs b/lib/crates/fabro-tool/src/interact.rs +index 34023120d..503147ece 100644 +--- a/lib/crates/fabro-tool/src/interact.rs ++++ b/lib/crates/fabro-tool/src/interact.rs +@@ -453,7 +453,7 @@ mod tests { + use chrono::{TimeZone, Utc}; + use fabro_types::{ + EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection, +- RunStatus, RunTimestamps, WorkflowRef, ++ RunStatus, RunTimestamps, WorkflowRef, test_support, + }; + use serde_json::json; + +@@ -690,7 +690,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs +index 0df7e391a..e3d0162c6 100644 +--- a/lib/crates/fabro-tool/src/search.rs ++++ b/lib/crates/fabro-tool/src/search.rs +@@ -293,7 +293,9 @@ mod tests { + use std::collections::HashMap; + + use chrono::{TimeZone, Utc}; +- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef}; ++ use fabro_types::{ ++ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support, ++ }; + + use super::*; + +@@ -444,7 +446,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::from([("group".to_string(), group.to_string())]), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs +index 318f982ad..339d1a0dc 100644 +--- a/lib/crates/fabro-types/src/lib.rs ++++ b/lib/crates/fabro-types/src/lib.rs +@@ -44,6 +44,8 @@ pub mod start; + pub mod status; + pub mod steering; + pub mod system_integrations; ++#[cfg(any(test, feature = "test-support"))] ++pub mod test_support; + pub mod timing; + pub mod todo; + pub mod transcript; +diff --git a/lib/crates/fabro-types/src/principal.rs b/lib/crates/fabro-types/src/principal.rs +index 2c0ff3807..4f1962ea2 100644 +--- a/lib/crates/fabro-types/src/principal.rs ++++ b/lib/crates/fabro-types/src/principal.rs +@@ -39,7 +39,6 @@ pub enum Principal { + System { + system_kind: SystemActorKind, + }, +- Anonymous, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, IntoStaticStr)] +@@ -97,7 +96,6 @@ impl Principal { + Self::Slack { .. } => "slack", + Self::Agent { .. } => "agent", + Self::System { .. } => "system", +- Self::Anonymous => "anonymous", + } + } + +@@ -123,7 +121,6 @@ impl Principal { + } => session_id.clone(), + Self::Agent { .. } => "agent".to_string(), + Self::System { system_kind } => format!("system:{system_kind}"), +- Self::Anonymous => "anonymous".to_string(), + } + } + } +@@ -291,11 +288,6 @@ mod tests { + }); + } + +- #[test] +- fn round_trips_anonymous_variant() { +- assert_round_trip(&Principal::Anonymous); +- } +- + #[test] + fn auth_method_as_str_matches_serde() { + assert_eq!(AuthMethod::Github.as_str(), "github"); +diff --git a/lib/crates/fabro-types/src/run.rs b/lib/crates/fabro-types/src/run.rs +index 269db43ee..afb2a1f24 100644 +--- a/lib/crates/fabro-types/src/run.rs ++++ b/lib/crates/fabro-types/src/run.rs +@@ -23,14 +23,13 @@ pub struct RunClientProvenance { + pub version: Option, + } + +-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] ++#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct RunProvenance { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client: Option, +- #[serde(default, skip_serializing_if = "Option::is_none")] +- pub subject: Option, ++ pub subject: Principal, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +@@ -90,8 +89,7 @@ pub struct RunSpec { + pub source_directory: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub labels: HashMap, +- #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provenance: Option, ++ pub provenance: RunProvenance, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub manifest_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +diff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs +index 01ed04f4d..e57b5ed0a 100644 +--- a/lib/crates/fabro-types/src/run_event/mod.rs ++++ b/lib/crates/fabro-types/src/run_event/mod.rs +@@ -961,7 +961,7 @@ mod tests { + use super::*; + use crate::{ + AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings, +- fixtures, ++ fixtures, test_support, + }; + + fn user_principal(login: &str) -> Principal { +@@ -1045,7 +1045,8 @@ mod tests { + "graph": graph, + "labels": {}, + "run_dir": "/tmp/run", +- "source_directory": "/tmp/run" ++ "source_directory": "/tmp/run", ++ "provenance": test_support::test_run_provenance() + } + }); + +@@ -1066,6 +1067,7 @@ mod tests { + "labels": {}, + "run_dir": "/tmp/run", + "source_directory": "/tmp/run", ++ "provenance": test_support::test_run_provenance(), + "manifest_blob": RunBlobId::new(br#"{"version":1}"#).to_string() + } + }); +diff --git a/lib/crates/fabro-types/src/run_event/run.rs b/lib/crates/fabro-types/src/run_event/run.rs +index fa189171f..be16946e2 100644 +--- a/lib/crates/fabro-types/src/run_event/run.rs ++++ b/lib/crates/fabro-types/src/run_event/run.rs +@@ -28,8 +28,7 @@ pub struct RunCreatedProps { + pub workflow_slug: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub db_prefix: Option, +- #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provenance: Option, ++ pub provenance: RunProvenance, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub manifest_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +diff --git a/lib/crates/fabro-types/src/run_projection.rs b/lib/crates/fabro-types/src/run_projection.rs +index 3bba6d43e..b667f4fc8 100644 +--- a/lib/crates/fabro-types/src/run_projection.rs ++++ b/lib/crates/fabro-types/src/run_projection.rs +@@ -680,7 +680,7 @@ mod title_tests { + + use chrono::Utc; + +- use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings}; ++ use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support}; + + fn projection_with_goal(goal: Option<&str>) -> RunProjection { + let mut graph = Graph::new("test"); +@@ -698,7 +698,7 @@ mod title_tests { + workflow_slug: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -750,7 +750,7 @@ mod iter_stages_tests { + use serde_json::json; + + use super::RunProjection; +- use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings}; ++ use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings, test_support}; + + fn seq(n: u32) -> NonZeroU32 { + NonZeroU32::new(n).unwrap() +@@ -767,7 +767,7 @@ mod iter_stages_tests { + workflow_slug: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs +index 1cde31614..509303d3c 100644 +--- a/lib/crates/fabro-types/src/run_summary.rs ++++ b/lib/crates/fabro-types/src/run_summary.rs +@@ -52,8 +52,7 @@ pub struct Run { + pub automation: Option, + #[serde(default)] + pub repository: Option, +- #[serde(default)] +- pub created_by: Option, ++ pub created_by: Principal, + pub origin: RunOrigin, + pub labels: HashMap, + pub lifecycle: RunLifecycle, +diff --git a/lib/crates/fabro-types/src/test_support.rs b/lib/crates/fabro-types/src/test_support.rs +new file mode 100644 +index 000000000..9db7eff91 +--- /dev/null ++++ b/lib/crates/fabro-types/src/test_support.rs +@@ -0,0 +1,21 @@ ++use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance, RunServerProvenance}; ++ ++#[must_use] ++pub fn test_principal() -> Principal { ++ Principal::user( ++ IdpIdentity::new("fabro:test", "test-user").expect("test identity should be valid"), ++ "test".to_string(), ++ AuthMethod::DevToken, ++ ) ++} ++ ++#[must_use] ++pub fn test_run_provenance() -> RunProvenance { ++ RunProvenance { ++ server: Some(RunServerProvenance { ++ version: "test".to_string(), ++ }), ++ client: None, ++ subject: test_principal(), ++ } ++} +diff --git a/lib/crates/fabro-types/tests/run_event_serde.rs b/lib/crates/fabro-types/tests/run_event_serde.rs +index 8f2df1972..533d21f15 100644 +--- a/lib/crates/fabro-types/tests/run_event_serde.rs ++++ b/lib/crates/fabro-types/tests/run_event_serde.rs +@@ -8,6 +8,16 @@ use fabro_types::settings::InterpString; + use fabro_types::settings::run::RunGoal; + use fabro_types::{EventBody, TurnId, WorkflowSettings, fixtures}; + ++fn test_run_provenance() -> fabro_types::RunProvenance { ++ fabro_types::RunProvenance { ++ server: None, ++ client: None, ++ subject: fabro_types::Principal::System { ++ system_kind: fabro_types::SystemActorKind::Engine, ++ }, ++ } ++} ++ + fn templated_settings() -> WorkflowSettings { + let mut settings = WorkflowSettings::default(); + settings.run.goal = Some(RunGoal::Inline(InterpString::parse("Ship {{ env.TASK }}"))); +@@ -27,7 +37,7 @@ fn run_created_props_round_trip_templated_settings() { + source_directory: Some("/Users/client/project".to_string()), + workflow_slug: Some("demo".to_string()), + db_prefix: Some("run_".to_string()), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + git: Some(GitContext { + origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), +@@ -89,7 +99,7 @@ fn run_created_props_omits_web_url_when_absent() { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -120,17 +130,17 @@ fn run_created_props_omits_web_url_when_absent() { + } + + #[test] +-fn run_created_props_defaults_retried_from_for_legacy_events() { ++fn run_created_props_defaults_retried_from_when_absent() { + let json = serde_json::json!({ + "title": null, + "settings": WorkflowSettings::default(), + "graph": Graph::new("ship"), + "labels": {}, +- "run_dir": "/tmp/run" ++ "run_dir": "/tmp/run", ++ "provenance": test_run_provenance() + }); + +- let props: RunCreatedProps = +- serde_json::from_value(json).expect("legacy props should deserialize"); ++ let props: RunCreatedProps = serde_json::from_value(json).expect("props should deserialize"); + assert_eq!(props.retried_from, None); + } + +diff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs +index f5e8cf25f..ef29ce763 100644 +--- a/lib/crates/fabro-types/tests/run_spec_methods.rs ++++ b/lib/crates/fabro-types/tests/run_spec_methods.rs +@@ -5,6 +5,16 @@ use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec}; + use fabro_types::settings::{ProjectNamespace, WorkflowNamespace}; + use fabro_types::{WorkflowSettings, fixtures}; + ++fn test_run_provenance() -> fabro_types::RunProvenance { ++ fabro_types::RunProvenance { ++ server: None, ++ client: None, ++ subject: fabro_types::Principal::System { ++ system_kind: fabro_types::SystemActorKind::Engine, ++ }, ++ } ++} ++ + fn sample_run_spec() -> RunSpec { + let settings = WorkflowSettings { + project: ProjectNamespace { +@@ -26,7 +36,7 @@ fn sample_run_spec() -> RunSpec { + workflow_slug: Some("demo".to_string()), + source_directory: Some("/Users/client/project".to_string()), + labels: HashMap::from([("team".to_string(), "platform".to_string())]), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(GitContext { +diff --git a/lib/crates/fabro-types/tests/run_spec_serde.rs b/lib/crates/fabro-types/tests/run_spec_serde.rs +index f6278ff34..d45c56ac8 100644 +--- a/lib/crates/fabro-types/tests/run_spec_serde.rs ++++ b/lib/crates/fabro-types/tests/run_spec_serde.rs +@@ -6,6 +6,16 @@ use fabro_types::settings::InterpString; + use fabro_types::settings::run::RunGoal; + use fabro_types::{WorkflowSettings, fixtures}; + ++fn test_run_provenance() -> fabro_types::RunProvenance { ++ fabro_types::RunProvenance { ++ server: None, ++ client: None, ++ subject: fabro_types::Principal::System { ++ system_kind: fabro_types::SystemActorKind::Engine, ++ }, ++ } ++} ++ + fn templated_settings() -> WorkflowSettings { + let mut settings = WorkflowSettings::default(); + settings.run.goal = Some(RunGoal::Inline(InterpString::parse("Ship {{ env.TASK }}"))); +@@ -22,7 +32,7 @@ fn run_spec_round_trips_templated_settings() { + workflow_slug: Some("demo".to_string()), + source_directory: Some("/Users/client/project".to_string()), + labels: HashMap::from([("team".to_string(), "platform".to_string())]), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(GitContext { +diff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs +index feceb0e12..e6fb39136 100644 +--- a/lib/crates/fabro-workflow/src/billing_rollup.rs ++++ b/lib/crates/fabro-workflow/src/billing_rollup.rs +@@ -165,7 +165,7 @@ mod tests { + use fabro_model::{Catalog, ModelRef, ProviderId}; + use fabro_types::{ + AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion, +- StageOutcome, WorkflowSettings, first_event_seq, fixtures, ++ StageOutcome, WorkflowSettings, first_event_seq, fixtures, test_support, + }; + + use super::billing_rollup_from_projection; +@@ -352,7 +352,7 @@ mod tests { + workflow_slug: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs +index 684af5164..17eb5d157 100644 +--- a/lib/crates/fabro-workflow/src/event/convert.rs ++++ b/lib/crates/fabro-workflow/src/event/convert.rs +@@ -2403,28 +2403,28 @@ mod tests { + let provenance = RunProvenance { + server: None, + client: None, +- subject: Some(user_principal("alice")), ++ subject: user_principal("alice"), + }; + + let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated { +- run_id: fixtures::RUN_1, +- title: None, +- settings: serde_json::to_value(WorkflowSettings::default()).unwrap(), +- graph: serde_json::to_value(Graph::new("test")).unwrap(), +- workflow_source: None, +- workflow_config: None, +- labels: BTreeMap::default(), +- run_dir: "/tmp/run".to_string(), ++ run_id: fixtures::RUN_1, ++ title: None, ++ settings: serde_json::to_value(WorkflowSettings::default()).unwrap(), ++ graph: serde_json::to_value(Graph::new("test")).unwrap(), ++ workflow_source: None, ++ workflow_config: None, ++ labels: BTreeMap::default(), ++ run_dir: "/tmp/run".to_string(), + source_directory: Some("/tmp/run".to_string()), +- workflow_slug: None, +- db_prefix: None, +- provenance: Some(provenance), +- manifest_blob: None, +- git: None, +- fork_source_ref: None, +- retried_from: None, +- parent_id: None, +- web_url: None, ++ workflow_slug: None, ++ db_prefix: None, ++ provenance, ++ manifest_blob: None, ++ git: None, ++ fork_source_ref: None, ++ retried_from: None, ++ parent_id: None, ++ web_url: None, + }); + let actor = stored.actor.as_ref().expect("actor set"); + assert_eq!(actor, &user_principal("alice")); +diff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs +index cf458c121..5a70af52c 100644 +--- a/lib/crates/fabro-workflow/src/event/events.rs ++++ b/lib/crates/fabro-workflow/src/event/events.rs +@@ -39,8 +39,7 @@ pub enum Event { + workflow_slug: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + db_prefix: Option, +- #[serde(default, skip_serializing_if = "Option::is_none")] +- provenance: Option, ++ provenance: RunProvenance, + #[serde(default, skip_serializing_if = "Option::is_none")] + manifest_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +diff --git a/lib/crates/fabro-workflow/src/event/sink.rs b/lib/crates/fabro-workflow/src/event/sink.rs +index 967f0570a..2c49372e4 100644 +--- a/lib/crates/fabro-workflow/src/event/sink.rs ++++ b/lib/crates/fabro-workflow/src/event/sink.rs +@@ -212,7 +212,7 @@ impl StoreProgressLogger { + mod tests { + use std::sync::Arc; + +- use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures}; ++ use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures, test_support}; + use tokio::sync::Mutex as AsyncMutex; + + use super::*; +@@ -243,7 +243,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/event/stored_fields.rs b/lib/crates/fabro-workflow/src/event/stored_fields.rs +index 94fba25ad..4ada17b67 100644 +--- a/lib/crates/fabro-workflow/src/event/stored_fields.rs ++++ b/lib/crates/fabro-workflow/src/event/stored_fields.rs +@@ -57,7 +57,7 @@ pub(super) fn stored_event_fields(event: &Event, scope: Option<&StageScope>) -> + fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields { + match event { + Event::RunCreated { provenance, .. } => StoredEventFields { +- actor: provenance.as_ref().and_then(|p| p.subject.clone()), ++ actor: Some(provenance.subject.clone()), + ..StoredEventFields::default() + }, + Event::RunCancelRequested { actor } +diff --git a/lib/crates/fabro-workflow/src/git.rs b/lib/crates/fabro-workflow/src/git.rs +index f48683dd0..177f09079 100644 +--- a/lib/crates/fabro-workflow/src/git.rs ++++ b/lib/crates/fabro-workflow/src/git.rs +@@ -343,7 +343,7 @@ mod tests { + + use fabro_dump::RunDump; + use fabro_store::Database; +- use fabro_types::{CommandTermination, StageModelUsage, fixtures}; ++ use fabro_types::{CommandTermination, StageModelUsage, fixtures, test_support}; + use object_store::memory::InMemory; + + use super::*; +@@ -468,7 +468,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs +index 648f00c4b..0f46cdfed 100644 +--- a/lib/crates/fabro-workflow/src/handler/agent.rs ++++ b/lib/crates/fabro-workflow/src/handler/agent.rs +@@ -429,7 +429,7 @@ mod tests { + use fabro_graphviz::graph::AttrValue; + use fabro_model::{ReasoningEffort, Speed}; + use fabro_store::{Database, RunDatabase, StageId}; +- use fabro_types::fixtures; ++ use fabro_types::{fixtures, test_support}; + use object_store::memory::InMemory; + use tempfile::TempDir; + +@@ -483,7 +483,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/handler/command.rs b/lib/crates/fabro-workflow/src/handler/command.rs +index dde68dd5d..10ad51ce0 100644 +--- a/lib/crates/fabro-workflow/src/handler/command.rs ++++ b/lib/crates/fabro-workflow/src/handler/command.rs +@@ -228,7 +228,7 @@ mod tests { + use bytes::Bytes; + use fabro_graphviz::graph::AttrValue; + use fabro_store::{Database, RunDatabase, StageId}; +- use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures}; ++ use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures, test_support}; + use object_store::memory::InMemory; + use tokio::sync::Mutex; + +@@ -255,7 +255,7 @@ mod tests { + workflow_slug: None, + source_directory: None, + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -355,7 +355,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs +index 91753f4c9..499cdd784 100644 +--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs ++++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs +@@ -1600,6 +1600,7 @@ mod tests { + use fabro_types::{ + EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, + RunPairStatusResponse, RunProjection, RunStatus, RunTimestamps, SuccessReason, WorkflowRef, ++ test_support, + }; + use fabro_vault::{SecretType, Vault}; + use futures::stream; +@@ -2133,7 +2134,7 @@ reasoning = false + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs +index 7c85042cc..e9b4d4fe3 100644 +--- a/lib/crates/fabro-workflow/src/handler/parallel.rs ++++ b/lib/crates/fabro-workflow/src/handler/parallel.rs +@@ -692,7 +692,7 @@ mod tests { + + use fabro_graphviz::graph::{AttrValue, Edge}; + use fabro_store::{Database, StageId}; +- use fabro_types::fixtures; ++ use fabro_types::{fixtures, test_support}; + use object_store::memory::InMemory; + + use super::*; +@@ -727,7 +727,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs +index 8d5b2fdf6..5c3ab7c11 100644 +--- a/lib/crates/fabro-workflow/src/handler/prompt.rs ++++ b/lib/crates/fabro-workflow/src/handler/prompt.rs +@@ -225,7 +225,7 @@ mod tests { + use fabro_graphviz::graph::AttrValue; + use fabro_model::{ReasoningEffort, Speed}; + use fabro_store::{Database, RunDatabase, StageId}; +- use fabro_types::fixtures; ++ use fabro_types::{fixtures, test_support}; + use object_store::memory::InMemory; + use tempfile::TempDir; + +@@ -282,7 +282,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs +index fefaafa8c..e3cfeff89 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs +@@ -598,7 +598,7 @@ mod tests { + use fabro_model::Catalog; + use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection}; + use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; +- use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures}; ++ use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures, test_support}; + use object_store::memory::InMemory; + + use super::*; +@@ -735,7 +735,7 @@ mod tests { + source_directory: Some("/tmp/project".to_string()), + workflow_slug: Some("metadata".to_string()), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/operations/archive.rs b/lib/crates/fabro-workflow/src/operations/archive.rs +index bb3693398..4393af607 100644 +--- a/lib/crates/fabro-workflow/src/operations/archive.rs ++++ b/lib/crates/fabro-workflow/src/operations/archive.rs +@@ -136,7 +136,9 @@ mod tests { + use std::time::Duration; + + use fabro_store::Database; +- use fabro_types::{FailureReason, RunId, SuccessReason, TerminalStatus, fixtures}; ++ use fabro_types::{ ++ FailureReason, RunId, SuccessReason, TerminalStatus, fixtures, test_support, ++ }; + use object_store::memory::InMemory; + + use super::*; +@@ -225,7 +227,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs +index 0460a1874..6b7233484 100644 +--- a/lib/crates/fabro-workflow/src/operations/create.rs ++++ b/lib/crates/fabro-workflow/src/operations/create.rs +@@ -44,7 +44,7 @@ pub struct CreateRunInput { + pub git: Option, + pub fork_source_ref: Option, + pub parent_id: Option, +- pub provenance: Option, ++ pub provenance: RunProvenance, + pub configured_providers: Vec, + /// Public URL where this run can be viewed in the web UI, when the server + /// has the web UI enabled. Recorded on the `run.created` event so attach +@@ -70,7 +70,7 @@ struct PersistCreateOptions { + source_directory: Option, + git: Option, + fork_source_ref: Option, +- provenance: Option, ++ provenance: RunProvenance, + configured_providers: Vec, + catalog: Arc, + } +@@ -415,7 +415,7 @@ mod tests { + use fabro_store::Database; + use fabro_types::settings::InterpString; + use fabro_types::settings::run::RunMode; +- use fabro_types::{WorkflowSettings, fixtures}; ++ use fabro_types::{WorkflowSettings, fixtures, test_support}; + use fabro_util::error::collect_chain; + use fabro_validate::Severity; + use object_store::local::LocalFileSystem; +@@ -1099,7 +1099,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1166,7 +1166,7 @@ mod tests { + }), + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1277,7 +1277,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1322,7 +1322,7 @@ mod tests { + }), + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1389,7 +1389,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1435,7 +1435,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: Some(fabro_types::RunProvenance { ++ provenance: fabro_types::RunProvenance { + server: Some(fabro_types::RunServerProvenance { + version: "0.9.0".to_string(), + }), +@@ -1444,12 +1444,12 @@ mod tests { + name: Some("fabro-cli".to_string()), + version: Some("0.9.0".to_string()), + }), +- subject: Some(fabro_types::Principal::user( ++ subject: fabro_types::Principal::user( + fabro_types::IdpIdentity::new("https://github.com", "12345").unwrap(), + "octocat".to_string(), + fabro_types::AuthMethod::Github, +- )), +- }), ++ ), ++ }, + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1462,7 +1462,7 @@ mod tests { + let run_store = store.open_run_reader(&created.run_id).await.unwrap(); + let state = run_store.state().await.unwrap(); + let run = state.spec; +- let provenance = run.provenance.expect("provenance should be projected"); ++ let provenance = run.provenance; + + assert_eq!(provenance.server.unwrap().version, "0.9.0"); + assert_eq!( +@@ -1470,7 +1470,7 @@ mod tests { + Some("fabro-cli") + ); + assert_eq!( +- provenance.subject.unwrap(), ++ provenance.subject, + fabro_types::Principal::user( + fabro_types::IdpIdentity::new("https://github.com", "12345").unwrap(), + "octocat".to_string(), +diff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs +index 513975d71..eb3a6720e 100644 +--- a/lib/crates/fabro-workflow/src/operations/fork.rs ++++ b/lib/crates/fabro-workflow/src/operations/fork.rs +@@ -283,7 +283,7 @@ mod tests { + + use fabro_graphviz::graph::Graph; + use fabro_store::{Database, RunProjectionReducer}; +- use fabro_types::{StageId, WorkflowSettings, fixtures}; ++ use fabro_types::{StageId, WorkflowSettings, fixtures, test_support}; + use object_store::memory::InMemory; + + use super::*; +@@ -381,7 +381,7 @@ mod tests { + source_directory: Some("/client/source".to_string()), + workflow_slug: Some("fork-source".to_string()), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: Some(fabro_types::GitContext { + origin_url: "https://github.com/example/repo.git".to_string(), +diff --git a/lib/crates/fabro-workflow/src/operations/retry.rs b/lib/crates/fabro-workflow/src/operations/retry.rs +index 6551687c5..f71b90626 100644 +--- a/lib/crates/fabro-workflow/src/operations/retry.rs ++++ b/lib/crates/fabro-workflow/src/operations/retry.rs +@@ -12,7 +12,7 @@ use crate::event::{self, Event}; + pub struct RetryRunInput { + pub source_run_id: RunId, + pub new_run_id: RunId, +- pub provenance: Option, ++ pub provenance: RunProvenance, + pub web_url: Option, + } + +@@ -149,7 +149,7 @@ mod tests { + version: "test".to_string(), + }), + client: None, +- subject: Some(actor(login)), ++ subject: actor(login), + } + } + +@@ -187,7 +187,7 @@ mod tests { + source_directory: Some("/workspace/source".to_string()), + workflow_slug: Some("retry-source".to_string()), + db_prefix: None, +- provenance: Some(provenance("source-user")), ++ provenance: provenance("source-user"), + manifest_blob, + git: Some(git_context()), + fork_source_ref, +@@ -345,7 +345,7 @@ mod tests { + let outcome = retry_run(&store, &RetryRunInput { + source_run_id, + new_run_id: RunId::new(), +- provenance: Some(provenance("retry-user")), ++ provenance: provenance("retry-user"), + web_url: Some("http://localhost:3000/runs/retry".to_string()), + }) + .await +@@ -379,14 +379,7 @@ mod tests { + assert_eq!(retry_state.spec.manifest_blob, manifest_blob); + assert_eq!(retry_state.spec.definition_blob, definition_blob); + assert_eq!(retry_state.spec.fork_source_ref, Some(fork_source_ref)); +- assert_eq!( +- retry_state +- .spec +- .provenance +- .as_ref() +- .and_then(|provenance| provenance.subject.as_ref()), +- Some(&actor("retry-user")) +- ); ++ assert_eq!(&retry_state.spec.provenance.subject, &actor("retry-user")); + assert_eq!( + retry_state.web_url.as_deref(), + Some("http://localhost:3000/runs/retry") +@@ -469,7 +462,7 @@ mod tests { + let err = retry_run(&store, &RetryRunInput { + source_run_id: run_id, + new_run_id: RunId::new(), +- provenance: None, ++ provenance: provenance("retry-user"), + web_url: None, + }) + .await +@@ -487,7 +480,7 @@ mod tests { + let err = retry_run(&store, &RetryRunInput { + source_run_id: fixtures::RUN_1, + new_run_id: RunId::new(), +- provenance: None, ++ provenance: provenance("retry-user"), + web_url: None, + }) + .await +diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs +index a962cd0e5..adf2741d7 100644 +--- a/lib/crates/fabro-workflow/src/operations/start.rs ++++ b/lib/crates/fabro-workflow/src/operations/start.rs +@@ -1112,7 +1112,9 @@ mod tests { + use fabro_store::Database; + use fabro_types::settings::run::RunMode; + use fabro_types::settings::{InterpString, ModelRef}; +- use fabro_types::{BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures}; ++ use fabro_types::{ ++ BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures, test_support, ++ }; + use object_store::memory::InMemory; + + use super::*; +@@ -1416,7 +1418,7 @@ reasoning = false + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1837,7 +1839,7 @@ reasoning = false + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +diff --git a/lib/crates/fabro-workflow/src/operations/timeline.rs b/lib/crates/fabro-workflow/src/operations/timeline.rs +index 2170dc28a..fdfdea119 100644 +--- a/lib/crates/fabro-workflow/src/operations/timeline.rs ++++ b/lib/crates/fabro-workflow/src/operations/timeline.rs +@@ -204,6 +204,7 @@ mod tests { + use chrono::Utc; + use fabro_types::{ + Checkpoint, CheckpointRecord, Graph, RunDiff, RunSpec, WorkflowSettings, fixtures, ++ test_support, + }; + + use super::*; +@@ -247,7 +248,7 @@ mod tests { + workflow_slug: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +index cef35cc39..39392007b 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +@@ -18,7 +18,9 @@ use fabro_interview::AutoApproveInterviewer; + use fabro_sandbox::SandboxSpec; + use fabro_store::Database; + use fabro_types::settings::run::RunModelControls; +-use fabro_types::{Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref}; ++use fabro_types::{ ++ Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref, test_support, ++}; + use object_store::memory::InMemory; + + use super::*; +@@ -164,7 +166,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -207,7 +209,7 @@ async fn seed_created_and_starting( + source_directory: Some(std::env::current_dir().unwrap().display().to_string()), + workflow_slug: run_options.workflow_slug.clone(), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: run_options.pre_run_git.clone(), + fork_source_ref: run_options.fork_source_ref.clone(), +diff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs +index 05d6db1d0..36b3b7452 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs +@@ -651,7 +651,7 @@ mod tests { + use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; + use fabro_types::{ + BilledTokenCounts, EventBody, RunBlobId, RunEvent, RunId, RunSpec, StageCompletion, +- WorkflowSettings, first_event_seq, fixtures, ++ WorkflowSettings, first_event_seq, fixtures, test_support, + }; + use object_store::memory::InMemory; + +@@ -738,7 +738,7 @@ mod tests { + source_directory: Some("/tmp/project".to_string()), + workflow_slug: Some("metadata".to_string()), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -854,7 +854,7 @@ mod tests { + workflow_slug: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs +index 761a58e42..b8b8e5147 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs +@@ -764,7 +764,7 @@ mod tests { + use fabro_sandbox::SandboxSpec; + use fabro_store::Database; + use fabro_types::settings::run::RunModelControls; +- use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures}; ++ use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures, test_support}; + use fabro_vault::{SecretType, Vault}; + use object_store::memory::InMemory; + use tokio::fs::{create_dir_all, write}; +@@ -888,7 +888,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs +index ee6150696..93da4da2c 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs +@@ -59,7 +59,7 @@ mod tests { + + use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node}; + use fabro_store::{Database, RunDatabase}; +- use fabro_types::fixtures; ++ use fabro_types::{fixtures, test_support}; + use object_store::memory::InMemory; + + use super::*; +@@ -147,7 +147,7 @@ mod tests { + ("env".to_string(), "test".to_string()), + ("team".to_string(), "workflow".to_string()), + ]), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +index 92f4a0bc3..41e1f3434 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +@@ -680,7 +680,7 @@ mod tests { + use fabro_store::Database; + use fabro_types::{ + BilledTokenCounts, RunProjection, RunSpec, SuccessReason, WorkflowSettings, +- first_event_seq, fixtures, ++ first_event_seq, fixtures, test_support, + }; + use fabro_vault::{SecretType, Vault}; + use futures::stream; +@@ -822,7 +822,7 @@ mod tests { + workflow_slug: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -1146,7 +1146,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1215,7 +1215,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1569,7 +1569,7 @@ mod tests { + source_directory: Some(tmp.path().display().to_string()), + git: None, + labels: std::collections::HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1696,7 +1696,7 @@ mod tests { + source_directory: Some("/tmp/project".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1713,7 +1713,7 @@ mod tests { + source_directory: run_spec.source_directory.clone(), + workflow_slug: run_spec.workflow_slug.clone(), + db_prefix: None, +- provenance: None, ++ provenance: run_spec.provenance.clone(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -1865,7 +1865,7 @@ mod tests { + source_directory: None, + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1882,7 +1882,7 @@ mod tests { + source_directory: None, + workflow_slug: None, + db_prefix: None, +- provenance: None, ++ provenance: run_spec.provenance.clone(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs +index 5d8cdeb3b..750d2d5ea 100644 +--- a/lib/crates/fabro-workflow/src/run_lookup.rs ++++ b/lib/crates/fabro-workflow/src/run_lookup.rs +@@ -457,7 +457,7 @@ mod tests { + + use fabro_graphviz::graph::Graph; + use fabro_store::Database; +- use fabro_types::{RunStatus, WorkflowSettings, fixtures}; ++ use fabro_types::{RunStatus, WorkflowSettings, fixtures, test_support}; + use object_store::memory::InMemory; + + use super::scan_runs_combined; +@@ -490,7 +490,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/run_metadata.rs b/lib/crates/fabro-workflow/src/run_metadata.rs +index 9d679d0b4..f8606a307 100644 +--- a/lib/crates/fabro-workflow/src/run_metadata.rs ++++ b/lib/crates/fabro-workflow/src/run_metadata.rs +@@ -537,7 +537,9 @@ mod tests { + use std::sync::Arc; + + use fabro_store::RunProjection; +- use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings}; ++ use fabro_types::{ ++ DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings, test_support, ++ }; + use git2::{ErrorClass, ErrorCode}; + + use super::*; +@@ -638,7 +640,7 @@ mod tests { + push_outcome: PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs +index 0f590c70f..e3b464abb 100644 +--- a/lib/crates/fabro-workflow/src/runtime_store.rs ++++ b/lib/crates/fabro-workflow/src/runtime_store.rs +@@ -120,7 +120,7 @@ mod tests { + use fabro_graphviz::graph::Graph; + use fabro_store::Database; + use fabro_types::run_event::RunSubmittedProps; +- use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures}; ++ use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures, test_support}; + use object_store::memory::InMemory; + + use super::RunStoreHandle; +@@ -147,7 +147,7 @@ mod tests { + source_directory: Some("/tmp/test".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -168,7 +168,7 @@ mod tests { + source_directory: Some("/tmp/test".to_string()), + workflow_slug: Some("test".to_string()), + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs +index c587e49fa..c54affbdc 100644 +--- a/lib/crates/fabro-workflow/src/test_support.rs ++++ b/lib/crates/fabro-workflow/src/test_support.rs +@@ -174,7 +174,13 @@ async fn initialized( + source_directory: Some(sandbox.working_directory().to_string()), + workflow_slug: run_options.workflow_slug.clone(), + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::RunProvenance { ++ server: None, ++ client: None, ++ subject: fabro_types::Principal::System { ++ system_kind: fabro_types::SystemActorKind::Engine, ++ }, ++ }, + manifest_blob: None, + git: run_options.pre_run_git.clone(), + fork_source_ref: run_options.fork_source_ref.clone(), +diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +index 8e4fa7d1d..03e4463f9 100644 +--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES ++++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +@@ -256,7 +256,6 @@ models/preflight-workflow-summary.ts + models/preview-url-request.ts + models/preview-url-response.ts + models/principal-agent.ts +-models/principal-anonymous.ts + models/principal-slack.ts + models/principal-system.ts + models/principal-user.ts +diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts +index 4a208f5e3..2cca8d321 100644 +--- a/lib/packages/fabro-api-client/src/models/index.ts ++++ b/lib/packages/fabro-api-client/src/models/index.ts +@@ -232,7 +232,6 @@ export * from './preview-url-request'; + export * from './preview-url-response'; + export * from './principal'; + export * from './principal-agent'; +-export * from './principal-anonymous'; + export * from './principal-slack'; + export * from './principal-system'; + export * from './principal-user'; +diff --git a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts b/lib/packages/fabro-api-client/src/models/principal-anonymous.ts +deleted file mode 100644 +index daac61df0..000000000 +--- a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts ++++ /dev/null +@@ -1,25 +0,0 @@ +-/* tslint:disable */ +-/* eslint-disable */ +-/** +- * Fabro Run API +- * HTTP API for managing Fabro workflow run executions. +- * +- * The version of the OpenAPI document: 0.1.0 +- * +- * +- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +- * https://openapi-generator.tech +- * Do not edit the class manually. +- */ +- +- +- +-export interface PrincipalAnonymous { +- 'kind': PrincipalAnonymousKindEnum; +-} +- +-export const PrincipalAnonymousKindEnum = { +- ANONYMOUS: 'anonymous' +-} as const; +- +-export type PrincipalAnonymousKindEnum = typeof PrincipalAnonymousKindEnum[keyof typeof PrincipalAnonymousKindEnum]; +diff --git a/lib/packages/fabro-api-client/src/models/principal.ts b/lib/packages/fabro-api-client/src/models/principal.ts +index e3597295d..08b5422df 100644 +--- a/lib/packages/fabro-api-client/src/models/principal.ts ++++ b/lib/packages/fabro-api-client/src/models/principal.ts +@@ -24,9 +24,6 @@ import type { IdpIdentity } from './idp-identity'; + import type { PrincipalAgent } from './principal-agent'; + // May contain unused imports in some cases + // @ts-ignore +-import type { PrincipalAnonymous } from './principal-anonymous'; +-// May contain unused imports in some cases +-// @ts-ignore + import type { PrincipalSlack } from './principal-slack'; + // May contain unused imports in some cases + // @ts-ignore +@@ -47,4 +44,4 @@ import type { SystemActorKind } from './system-actor-kind'; + /** + * @type Principal + */ +-export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'anonymous' } & PrincipalAnonymous | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker; ++export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker; +diff --git a/lib/packages/fabro-api-client/src/models/run-provenance.ts b/lib/packages/fabro-api-client/src/models/run-provenance.ts +index 7276857f8..59fa7a063 100644 +--- a/lib/packages/fabro-api-client/src/models/run-provenance.ts ++++ b/lib/packages/fabro-api-client/src/models/run-provenance.ts +@@ -26,5 +26,5 @@ import type { RunServerProvenance } from './run-server-provenance'; + export interface RunProvenance { + 'server'?: RunServerProvenance | null; + 'client'?: RunClientProvenance | null; +- 'subject'?: Principal | null; ++ 'subject': Principal; + } +diff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts +index 2be7312b7..f86098a2d 100644 +--- a/lib/packages/fabro-api-client/src/models/run-spec.ts ++++ b/lib/packages/fabro-api-client/src/models/run-spec.ts +@@ -37,7 +37,7 @@ export interface RunSpec { + 'workflow_slug'?: string | null; + 'source_directory'?: string | null; + 'labels'?: { [key: string]: string; }; +- 'provenance'?: RunProvenance | null; ++ 'provenance': RunProvenance; + 'manifest_blob'?: string | null; + 'definition_blob'?: string | null; + 'git'?: GitContext | null; +diff --git a/lib/packages/fabro-api-client/src/models/run.ts b/lib/packages/fabro-api-client/src/models/run.ts +index 1ed6c6c26..a4ade38a3 100644 +--- a/lib/packages/fabro-api-client/src/models/run.ts ++++ b/lib/packages/fabro-api-client/src/models/run.ts +@@ -83,7 +83,7 @@ export interface Run { + 'workflow': WorkflowRef; + 'automation': AutomationRef | null; + 'repository': RepositoryRef | null; +- 'created_by': Principal | null; ++ 'created_by': Principal; + 'origin': RunOrigin; + 'labels': { [key: string]: string; }; + 'lifecycle': RunLifecycle; +diff --git a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts +index 7b6bd4afd..568a21780 100644 +--- a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts ++++ b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts +@@ -12,8 +12,6 @@ export function principalKind(principal: Principal): string { + switch (principal.kind) { + case "agent": + return "agent"; +- case "anonymous": +- return "anonymous"; + case "slack": + return "slack"; + case "system": diff --git a/stages/005-implement@1/status.json b/stages/005-implement@1/status.json new file mode 100644 index 000000000..90fdce6f8 --- /dev/null +++ b/stages/005-implement@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Stage completed: implement", + "failure_reason": null, + "timestamp": "2026-05-27T05:06:52.896823Z" +} \ No newline at end of file diff --git a/stages/006-simplify_opus@1/prompt.md b/stages/006-simplify_opus@1/prompt.md new file mode 100644 index 000000000..f54a6e904 --- /dev/null +++ b/stages/006-simplify_opus@1/prompt.md @@ -0,0 +1,335 @@ +Goal: # Plan: Make run actors and provenance total + +## Context + +This is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape. + +`Principal::Anonymous` currently represents "no authenticated actor on this request" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean "who acted." + +Likewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option`, no nullable OpenAPI fields, no legacy deserialization defaults, and no fallback creator in projection code. + +Two commits, in order. + +--- + +## Commit 1 - Remove `Principal::Anonymous` + +Breaking cleanup. `Principal` becomes actor-only. Missing/invalid auth is represented as absent request principal, not as an anonymous principal variant. + +### Rust + +`lib/crates/fabro-types/src/principal.rs`: +- Drop `Anonymous`. +- Drop `Anonymous` arms in `kind()` and `display()`. +- Delete anonymous serialization/round-trip test coverage. + +`lib/crates/fabro-server/src/principal_middleware.rs`: +- `RequestAuthContext.principal: Principal` -> `Option`. +- `RequestAuthLogContext.principal: Principal` -> `Option`. +- `initial()` and `rejected()` set `principal: None`. +- `authenticated(...)`, `authenticated_worker(...)`, and `authenticated_user(...)` set `principal: Some(...)`. +- Update `principal_without_log_unused_fields` to preserve `None` and strip user avatar data only inside `Some(Principal::User(...))`. +- Update all gate helpers to match `Option`: + - `require_user` + - `require_authenticated_user` + - `require_run_management_actor` + - `require_worker_or_user_for_run` + - `require_run_management_target` +- `None` routes to the existing `auth_rejection(context.auth_status, context.auth_error_code)` behavior. +- `Some(Principal::Worker { .. })` keeps the current forbidden-vs-auth-rejection distinctions. +- Update tests that assert the initial/rejected principal to assert `None`. + +`lib/crates/fabro-server/src/server.rs` HTTP logging: +- Keep the `principal_kind` field on every HTTP log line. +- Compute `principal_kind` as `auth_context.principal.as_ref().map(Principal::kind).unwrap_or("none")`. +- Match `auth_context.principal` as an `Option`: + - `Some(User(...))`, `Some(Worker { ... })`, `Some(Webhook { ... })`, `Some(Slack { ... })` keep their extra fields. + - `None | Some(Agent { .. } | System { .. })` emits only the common HTTP fields. + +`docs/internal/logging-strategy.md`: +- Replace the `anonymous` HTTP caller category guidance with `none` for requests that have no principal. +- Keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state. + +### OpenAPI and generated clients + +`docs/public/api-reference/fabro-api.yaml`: +- Remove `PrincipalAnonymous` from the `Principal` `oneOf`. +- Remove `anonymous` from the `Principal` discriminator mapping. +- Delete the `PrincipalAnonymous` schema. + +Regenerate: +- `cargo build -p fabro-api` +- `cd lib/packages/fabro-api-client && bun run generate` + +Expected generated cleanup: +- `lib/packages/fabro-api-client/src/models/principal-anonymous.ts` disappears. +- `Principal` union no longer includes `{ kind: "anonymous" }`. +- `lib/packages/fabro-api-client/src/models/index.ts` no longer exports `principal-anonymous`. + +### Frontend + +`apps/fabro-web/app/lib/principal-display.tsx`: +- Remove the `"anonymous"` switch case and unused icon import. + +`apps/fabro-web/app/components/run-summary-panel.test.tsx` and API-client exhaustiveness tests: +- Remove anonymous principal cases. + +### Documentation sweep + +Remove anonymous-principal references from product/API docs and tests. Be careful not to touch unrelated uses of "anonymous" such as telemetry anonymous IDs or Git's `remote_anonymous` API. + +Useful sweep: +- `rg -n "Principal::Anonymous|PrincipalAnonymous|kind: 'anonymous'|kind: \"anonymous\"|anonymous actor|anonymous subject|principal_kind.*anonymous|\"anonymous\"" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal` + +### Verification + +- `cargo +nightly-2026-04-14 fmt --check --all` +- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` +- `cargo build --workspace` +- `cargo nextest run --workspace` +- `cd apps/fabro-web && bun run typecheck && bun test` +- Manual: start `fabro server start`, hit a protected endpoint without a token, confirm 401 and an HTTP log with `principal_kind="none"` and `auth_status="missing"`. + +--- + +## Commit 2 - Make run provenance and creator non-optional + +Full-chain invariant. Every persisted run has exactly one creator principal. No nullable schema fields, no legacy defaults, no projection fallbacks. + +### Core type changes + +`lib/crates/fabro-types/src/run_summary.rs`: +- `Run.created_by: Option` -> `Principal`. +- Drop `#[serde(default)]`. + +`lib/crates/fabro-types/src/run.rs`: +- `RunProvenance.subject: Option` -> `Principal`. +- Drop `#[serde(default, skip_serializing_if = "Option::is_none")]`. +- Drop `Default` derive on `RunProvenance`. +- `RunSpec.provenance: Option` -> `RunProvenance`. +- Drop `#[serde(default, skip_serializing_if = "Option::is_none")]` on `RunSpec.provenance`. + +`lib/crates/fabro-types/src/run_event/run.rs`: +- `RunCreatedProps.provenance: Option` -> `RunProvenance`. +- Drop default/skip serialization attributes for provenance. + +`lib/crates/fabro-workflow/src/event/events.rs`: +- `Event::RunCreated.provenance: Option` -> `RunProvenance`. +- Drop default/skip serialization attributes for provenance. + +### Creation and retry flow + +`lib/crates/fabro-workflow/src/operations/create.rs`: +- `CreateRunInput.provenance: Option` -> `RunProvenance`. +- `PersistCreateOptions.provenance: Option` -> `RunProvenance`. +- `RunSpec { provenance }` stores the total provenance directly. +- `Event::RunCreated { provenance }` emits total provenance directly. + +`lib/crates/fabro-server/src/server/handler/runs.rs`: +- `run_provenance(headers, subject)` returns `RunProvenance { subject: subject.clone(), ... }`. +- Build provenance before creating `CreateRunInput`. + +`lib/crates/fabro-server/src/run_manifest.rs`: +- Change `create_run_input(...)` to accept `provenance: RunProvenance` and set it directly, or stop using the helper for the final `CreateRunInput` construction. Do not create a temporary input with missing provenance. + +`lib/crates/fabro-workflow/src/operations/retry.rs`: +- `RetryRunInput.provenance: Option` -> `RunProvenance`. +- `retry_run(...)` writes the new run's `run.created` event with total provenance. + +`lib/crates/fabro-server/src/server/handler/lifecycle.rs`: +- Pass `run_provenance(&headers, &actor)` directly into `RetryRunInput`. + +### Event conversion and projections + +`lib/crates/fabro-workflow/src/event/convert.rs`: +- Convert `Event::RunCreated.provenance` into `RunCreatedProps.provenance` directly. +- Remove `Some(...)` wrapping for run-created provenance. + +`lib/crates/fabro-workflow/src/event/stored_fields.rs`: +- `Event::RunCreated { provenance, .. }` sets `actor: Some(provenance.subject.clone())`. + +`lib/crates/fabro-store/src/run_state.rs`: +- `projection_from_created(...)` builds `RunSpec { provenance: props.provenance.clone(), ... }`. +- `build_summary(...)` sets `created_by: state.spec.provenance.subject.clone()`. +- Delete or rewrite tests that deserialize projections with `"provenance": null`. + +`lib/crates/fabro-types/src/run_projection.rs` and projection tests: +- Replace all test `RunSpec` literals with total provenance. +- Remove tests whose only purpose is legacy/null provenance tolerance. + +### OpenAPI + +`docs/public/api-reference/fabro-api.yaml`: +- `Run.created_by` references `Principal` directly. Remove `oneOf [..., null]`. +- `RunProvenance.required` includes `subject`. +- `RunProvenance.subject` references `Principal` directly. Remove `oneOf [..., null]`. +- `RunSpec.required` includes `provenance`. +- `RunSpec.provenance` references `RunProvenance` directly. Remove `oneOf [..., null]`. +- If `run.created` event properties are represented separately in the spec, make that event provenance required and non-nullable too. + +Regenerate: +- `cargo build -p fabro-api` +- `cd lib/packages/fabro-api-client && bun run generate` + +Do not hand-edit generated client files. + +### Demo mode + +`lib/crates/fabro-server/src/demo/mod.rs`: +- Add a clearly synthetic demo principal using `AuthMethod::DevToken`, not GitHub: + ```rust + static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| { + Principal::user( + IdpIdentity::new("fabro:demo", "demo").unwrap(), + "demo".to_string(), + AuthMethod::DevToken, + ) + }); + ``` +- Replace `created_by: None` with `created_by: DEMO_PRINCIPAL.clone()`. +- If demo creates any full `RunSpec` or `run.created` event data, give it `RunProvenance { subject: DEMO_PRINCIPAL.clone(), ... }`. + +### Test support + +Do not add fake auth helpers to `fabro_types::fixtures`; that module is run-id constants. + +Use the existing `fabro-types` `test-support` feature: +- Add `#[cfg(any(test, feature = "test-support"))] pub mod test_support;` in `lib/crates/fabro-types/src/lib.rs` if it does not already exist. +- Add `lib/crates/fabro-types/src/test_support.rs` with: + - `test_principal() -> Principal` + - `test_run_provenance() -> RunProvenance` +- Use an obviously fake dev-token identity, e.g. issuer `fabro:test`, subject `test-user`, login `test`. +- In crates that need the helper from integration tests or cross-crate tests, dual-list `fabro-types` in `dev-dependencies` with `features = ["test-support"]`, following existing repo patterns. + +Update all constructors: +- Replace `provenance: None` in `RunSpec`, `CreateRunInput`, `RetryRunInput`, `Event::RunCreated`, and `RunCreatedProps` literals with `test_run_provenance()` or a locally meaningful provenance. +- Replace `subject: Some(...)` with `subject: ...`. +- Replace `subject: None` only when it is actually `RunProvenance.subject`; leave unrelated todo/commit/message `subject` fields alone. +- Replace `created_by: None` / `created_by: null` with `test_principal()` or a frontend TS principal fixture. +- Delete tests that assert nullable or omitted creator/provenance behavior. + +Representative Rust areas: +- `lib/crates/fabro-store/src/run_state.rs` +- `lib/crates/fabro-store/tests/serializable_projection.rs` +- `lib/crates/fabro-workflow/src/operations/{create,retry,start}.rs` +- `lib/crates/fabro-workflow/src/event/{convert,sink,stored_fields}.rs` +- `lib/crates/fabro-workflow/src/handler/**` +- `lib/crates/fabro-workflow/src/pipeline/**` +- `lib/crates/fabro-workflow/src/run_{lookup,metadata}.rs` +- `lib/crates/fabro-server/src/server/tests.rs` +- `lib/crates/fabro-server/src/server/handler/**` +- `lib/crates/fabro-server/tests/it/**` +- `lib/crates/fabro-cli/tests/it/support/mod.rs` +- `lib/crates/fabro-dump/src/lib.rs` +- `lib/crates/fabro-tool/src/{common,create,interact,search}.rs` +- `lib/crates/fabro-api/tests/{principal_round_trip,run_summary_round_trip,run_projection_round_trip,run_event_round_trip}.rs` +- `lib/crates/fabro-types/tests/{run_spec_serde,run_spec_methods,run_event_serde}.rs` + +Representative TypeScript areas: +- `apps/fabro-web/app/**` tests with `created_by: null` +- `apps/fabro-web/app/data/runs.ts` +- `apps/fabro-web/app/components/run-summary-panel.tsx` +- `apps/fabro-web/app/components/runs-list/**` +- `lib/packages/fabro-api-client/tests/principal-exhaustive.ts` + +Useful sweep after edits: +- `rg -n "Principal::Anonymous|PrincipalAnonymous|principal-anonymous|kind: ['\"]anonymous|created_by:\\s*(None|null)|provenance:\\s*None|subject:\\s*Some\\(|subject:\\s*None" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal` + +Review each hit. The only acceptable remaining matches should be unrelated uses of "anonymous" and unrelated non-principal `subject` fields. + +### Frontend + +`apps/fabro-web/app/components/run-summary-panel.tsx`: +- `run?.created_by` may still be guarded by `run` loading state, but `created_by` itself is non-null once `run` exists. +- Pass `run.created_by` directly to `principalDisplay(...)` inside loaded-run branches. + +`apps/fabro-web/app/data/runs.ts` and run-list components: +- Treat `createdBy` as a total principal in UI data derived from a loaded API run. +- Remove empty/fallback rendering that only existed for missing creator data. + +### Verification + +- `cargo +nightly-2026-04-14 fmt --check --all` +- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` +- `cargo build --workspace` +- `cargo nextest run --workspace` +- `cargo nextest run -p fabro-server` +- `cd apps/fabro-web && bun run typecheck && bun test && bun run build` +- Manual end-to-end: + - `fabro server start` + - `cd apps/fabro-web && bun run dev` + - Authenticate and create a run through the UI. + - Confirm `/api/v1/runs/:id` has non-null `created_by`. + - Confirm `/api/v1/runs/:id/state` has non-null `spec.provenance.subject`. + - Retry a failed run and confirm the retried run has the retrying user as creator. + - Hit demo mode with `X-Fabro-Demo: 1` and confirm the run summary renders the synthetic `demo` dev-token user. + + +## Completed stages +- **toolchain**: succeeded + - Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1` + - Output: + ``` + cargo 1.95.0 (f2d3ce0bd 2026-03-21) + ``` +- **preflight_compile**: succeeded + - Script: `cargo check -q --workspace 2>&1` + - Output: (empty) +- **preflight_lint**: succeeded + - Script: `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1` + - Output: (empty) +- **implement**: succeeded + - Model: gpt-5.5, 9.9m tokens in / 73.0k out + - Files: /home/daytona/workspace/fabro/apps/fabro-web/app/lib/test-principal.ts, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs + + +# Simplify: Code Review and Cleanup + +Review changes vs. origin for reuse, quality, and efficiency. Fix any issues found. + +## Phase 1: Identify Changes + +Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation. + +## Phase 2: Launch Three Review Agents in Parallel + +Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context. + +### Agent 1: Code Reuse Review + +For each change: + +1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones. +2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead. +3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates. + +Note: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it. + +### Agent 2: Code Quality Review + +Review the same changes for hacky patterns: + +1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls +2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones +3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction +4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries +5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase + +Note: This is a greenfield app, so be aggressive in optimizing quality. + +### Agent 3: Efficiency Review + +Review the same changes for efficiency: + +1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns +2. Missed concurrency: independent operations run sequentially when they could run in parallel +3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths +4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error +5. Memory: unbounded data structures, missing cleanup, event listener leaks +6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one + +## Phase 3: Fix Issues + +Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it. + +When done, briefly summarize what was fixed (or confirm the code was already clean). \ No newline at end of file diff --git a/stages/006-simplify_opus@1/provider_used.json b/stages/006-simplify_opus@1/provider_used.json new file mode 100644 index 000000000..672cc50e5 --- /dev/null +++ b/stages/006-simplify_opus@1/provider_used.json @@ -0,0 +1,5 @@ +{ + "mode": "agent", + "provider": "anthropic", + "model": "claude-opus-4-7" +} \ No newline at end of file diff --git a/stages/006-simplify_opus@1/response.md b/stages/006-simplify_opus@1/response.md new file mode 100644 index 000000000..602c2be2e --- /dev/null +++ b/stages/006-simplify_opus@1/response.md @@ -0,0 +1,19 @@ +All checks pass. Summary of cleanup: + +**Fixes applied** based on the three review agents' findings: + +1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`. + +2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms. + +3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports. + +**Findings skipped** with rationale: +- `Option` → `RequestAuth` sum type: too invasive; the existing shape is readable. +- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible. +- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape. +- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope. +- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention. +- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only. + +**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`). \ No newline at end of file