From e578d9877e65fc86939732354881b965849da153 Mon Sep 17 00:00:00 2001 From: Fabro Date: Mon, 8 Jun 2026 16:53:24 -0400 Subject: [PATCH] =?UTF-8?q?checkpoint=20=E2=9A=92=EF=B8=8F=20Generated=20w?= =?UTF-8?q?ith=20[Fabro](https://fabro.sh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.json | 519 +++- stages/005-implement@1/diff.patch | 2693 +++++++++++++++++ stages/005-implement@1/status.json | 6 + stages/006-simplify_opus@1/prompt.md | 334 ++ stages/006-simplify_opus@1/provider_used.json | 5 + stages/006-simplify_opus@1/response.md | 19 + 6 files changed, 3553 insertions(+), 23 deletions(-) create mode 100644 stages/005-implement@1/diff.patch create mode 100644 stages/005-implement@1/status.json create mode 100644 stages/006-simplify_opus@1/prompt.md create mode 100644 stages/006-simplify_opus@1/provider_used.json create mode 100644 stages/006-simplify_opus@1/response.md diff --git a/run.json b/run.json index 427d2ac58..99e9572f9 100644 --- a/run.json +++ b/run.json @@ -505,7 +505,7 @@ "kind": "running" }, "status_updated_at": "2026-06-08T18:57:33.473581Z", - "last_event_at": "2026-06-08T20:34:04.850153Z", + "last_event_at": "2026-06-08T20:53:20.852756Z", "pending_control": null, "checkpoints": [ { @@ -789,9 +789,9 @@ } }, { - "seq": 0, + "seq": 1560, "checkpoint": { - "timestamp": "2026-06-08T20:34:15.136441Z", + "timestamp": "2026-06-08T20:34:26.305840Z", "current_node": "implement", "completed_nodes": [ "start", @@ -802,31 +802,181 @@ ], "node_retries": {}, "context_values": { - "thread.preflight_compile.current_node": "preflight_lint", - "internal.retry_count.preflight_lint": 0, - "internal.thread_id": "preflight_lint", - "internal.run_id": "01KTM9H228G0Z1ATDMZ10GGS4W", - "internal.retry_count.start": 0, - "current_node": "implement", - "failure_class": "", - "failure_signature": "", - "response.implement": "Implemented the plan in the requested two commits, in order:\n\n1. `199c93c5e Remove anonymous principal actor`\n - Removed `Principal::Anonymous`.\n - Request auth now represents no actor as `None`.\n - HTTP logs use `principal_kind=\"none\"` for absent principals.\n - Updated OpenAPI/generated TS client/frontend/tests accordingly.\n\n2. `fffe671dd Make run provenance and creator total`\n - Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance non-optional end-to-end.\n - Removed null/default/fallback provenance handling.\n - Updated create/retry/projection flows to require and propagate total provenance.\n - Added gated `fabro-types` test support helpers.\n - Added synthetic demo principal.\n - Updated OpenAPI and regenerated generated clients.\n - Updated frontend models/rendering/tests for total `created_by`.\n\nFinal repo state is clean.\n\nValidation completed:\n- `cargo build -p fabro-api`\n- TypeScript API client generation with local JRE\n- `cargo check -q --workspace --all-targets`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace --no-fail-fast`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- Final grep sweep for stale anonymous/null provenance traces; remaining hits are unrelated non-run-provenance `subject` fields.\n\nManual browser/server end-to-end checks from the plan were not run in this terminal environment.", "last_stage": "implement", - "last_response": "Implemented the plan in the requested two commits, in order:\n\n1. `199c93c5e Remove anonymous principal actor`\n - Removed `Principal::Anonymous`.\n - Request auth now represents no actor as `None`.\n", - "internal.retry_count.preflight_compile": 0, - "internal.fidelity": "compact", - "thread.start.current_node": "toolchain", - "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", - "internal.retry_count.toolchain": 0, "thread.preflight_lint.current_node": "implement", "thread.toolchain.current_node": "preflight_compile", - "internal.retry_count.implement": 0, + "thread.start.current_node": "toolchain", + "response.implement": "Implemented the plan in the requested two commits, in order:\n\n1. `199c93c5e Remove anonymous principal actor`\n - Removed `Principal::Anonymous`.\n - Request auth now represents no actor as `None`.\n - HTTP logs use `principal_kind=\"none\"` for absent principals.\n - Updated OpenAPI/generated TS client/frontend/tests accordingly.\n\n2. `fffe671dd Make run provenance and creator total`\n - Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance non-optional end-to-end.\n - Removed null/default/fallback provenance handling.\n - Updated create/retry/projection flows to require and propagate total provenance.\n - Added gated `fabro-types` test support helpers.\n - Added synthetic demo principal.\n - Updated OpenAPI and regenerated generated clients.\n - Updated frontend models/rendering/tests for total `created_by`.\n\nFinal repo state is clean.\n\nValidation completed:\n- `cargo build -p fabro-api`\n- TypeScript API client generation with local JRE\n- `cargo check -q --workspace --all-targets`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace --no-fail-fast`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- Final grep sweep for stale anonymous/null provenance traces; remaining hits are unrelated non-run-provenance `subject` fields.\n\nManual browser/server end-to-end checks from the plan were not run in this terminal environment.", + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "internal.retry_count.toolchain": 0, + "internal.thread_id": "preflight_lint", + "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", + "thread.preflight_compile.current_node": "preflight_lint", + "failure_signature": "", + "internal.retry_count.preflight_compile": 0, + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", "graph.rankdir": "LR", + "internal.node_visit_count": 1, + "last_response": "Implemented the plan in the requested two commits, in order:\n\n1. `199c93c5e Remove anonymous principal actor`\n - Removed `Principal::Anonymous`.\n - Request auth now represents no actor as `None`.\n", + "outcome": "succeeded", + "current_node": "implement", + "internal.fidelity": "compact", + "internal.retry_count.implement": 0, + "internal.retry_count.preflight_lint": 0, + "internal.retry_count.start": 0, + "internal.work_dir": "/home/daytona/workspace/fabro", + "internal.run_id": "01KTM9H228G0Z1ATDMZ10GGS4W", + "failure_class": "" + }, + "node_outcomes": { + "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": 152673, + "active_time_ms": 152673 + } + }, + "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": 161865, + "active_time_ms": 161865 + } + }, + "implement": { + "status": "succeeded", + "context_updates": { + "response.implement": "Implemented the plan in the requested two commits, in order:\n\n1. `199c93c5e Remove anonymous principal actor`\n - Removed `Principal::Anonymous`.\n - Request auth now represents no actor as `None`.\n - HTTP logs use `principal_kind=\"none\"` for absent principals.\n - Updated OpenAPI/generated TS client/frontend/tests accordingly.\n\n2. `fffe671dd Make run provenance and creator total`\n - Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance non-optional end-to-end.\n - Removed null/default/fallback provenance handling.\n - Updated create/retry/projection flows to require and propagate total provenance.\n - Added gated `fabro-types` test support helpers.\n - Added synthetic demo principal.\n - Updated OpenAPI and regenerated generated clients.\n - Updated frontend models/rendering/tests for total `created_by`.\n\nFinal repo state is clean.\n\nValidation completed:\n- `cargo build -p fabro-api`\n- TypeScript API client generation with local JRE\n- `cargo check -q --workspace --all-targets`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace --no-fail-fast`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- Final grep sweep for stale anonymous/null provenance traces; remaining hits are unrelated non-run-provenance `subject` fields.\n\nManual browser/server end-to-end checks from the plan were not run in this terminal environment.", + "last_response": "Implemented the plan in the requested two commits, in order:\n\n1. `199c93c5e Remove anonymous principal actor`\n - Removed `Principal::Anonymous`.\n - Request auth now represents no actor as `None`.\n", + "last_stage": "implement" + }, + "notes": "Stage completed: implement", + "usage": { + "input": { + "usage": { + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "tokens": { + "input_tokens": 3764533, + "output_tokens": 47720, + "reasoning_tokens": 15703, + "cache_read_tokens": 44680704, + "cache_write_tokens": 0 + } + }, + "facts": { + "algorithm": "openai" + } + }, + "total_usd_micros": 43065707 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs" + ], + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 2723291, + "tool_time_ms": 2405987, + "active_time_ms": 5129278 + } + }, + "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": 1440, + "active_time_ms": 1440 + } + }, + "start": { + "status": "succeeded", + "usage": null + } + }, + "next_node_id": "simplify_opus", + "git_commit_sha": "01c9960461775c92f30783b2fbb5905409d359d5", + "node_visits": { + "implement": 1, + "preflight_lint": 1, + "start": 1, + "preflight_compile": 1, + "toolchain": 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 52e79cf0f..abc5d6e7c 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@@ -34,6 +34,14 @@ function render(props: Partial = {}) {\n return tree!;\n }\n \n+const TEST_PRINCIPAL = {\n+ kind: \"user\" as const,\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\" as const,\n+ avatar_url: null,\n+};\n+\n function cellAfterLabel(\n tree: TestRenderer.ReactTestRenderer,\n label: string,\n@@ -53,7 +61,7 @@ function cellAfterLabel(\n function makeRun(overrides: Record = {}) {\n return {\n id: \"run_1\",\n- created_by: null,\n+ created_by: TEST_PRINCIPAL,\n diff: null,\n billing: null,\n ...overrides,\n@@ -71,9 +79,9 @@ 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, \"Created by\"))).toBe(\"Ttest\");\n expect(instanceText(cellAfterLabel(tree, \"Changes\"))).toBe(EMPTY_VALUE);\n expect(instanceText(cellAfterLabel(tree, \"Cost\"))).toBe(EMPTY_VALUE);\n });\n@@ -226,7 +234,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@@ -240,7 +248,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@@ -252,7 +260,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 20a08af5b..177270630 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -116,7 +116,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 const sandboxKind = sandboxLifecycleKind(run?.sandbox);\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 4fdb82777..5fd17dad1 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@@ -54,7 +54,7 @@ export function RunTableRow({\n \n {show(\"created_by\") && (\n \n- {run.createdBy && (() => {\n+ {(() => {\n const display = principalDisplay(run.createdBy);\n return (\n \ndiff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts\nindex 98586b2c4..69f2dbe81 100644\n--- a/apps/fabro-web/app/data/runs.test.ts\n+++ b/apps/fabro-web/app/data/runs.test.ts\n@@ -9,6 +9,14 @@ import {\n runStatusDisplay,\n } from \"./runs\";\n \n+const TEST_PRINCIPAL = {\n+ kind: \"user\" as const,\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\" as const,\n+ avatar_url: null,\n+};\n+\n function makeRun(overrides: Partial = {}): Run {\n return {\n id: \"01ABC\",\n@@ -17,7 +25,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: TEST_PRINCIPAL,\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 2b277b6d0..3bc9779de 100644\n--- a/apps/fabro-web/app/data/runs.ts\n+++ b/apps/fabro-web/app/data/runs.ts\n@@ -41,7 +41,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 ffb208aa7..c5cb1245c 100644\n--- a/apps/fabro-web/app/lib/run-actions.test.ts\n+++ b/apps/fabro-web/app/lib/run-actions.test.ts\n@@ -39,6 +39,14 @@ type CapturedRequest = {\n \n const originalAdapter = generatedAxios.defaults.adapter;\n \n+const TEST_PRINCIPAL = {\n+ kind: \"user\" as const,\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\" as const,\n+ avatar_url: null,\n+};\n+\n function makeRun(status: RunStatus, archived = false): Run {\n return {\n id: \"run-1\",\n@@ -47,7 +55,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: TEST_PRINCIPAL,\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/automations-new.test.tsx b/apps/fabro-web/app/routes/automations-new.test.tsx\nindex 47f471abf..2a767fcea 100644\n--- a/apps/fabro-web/app/routes/automations-new.test.tsx\n+++ b/apps/fabro-web/app/routes/automations-new.test.tsx\n@@ -101,6 +101,14 @@ mock.module(\"swr\", () => ({\n const { default: AutomationsNew } = await import(\"./automations-new\");\n mock.restore();\n \n+const TEST_PRINCIPAL = {\n+ kind: \"user\" as const,\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\" as const,\n+ avatar_url: null,\n+};\n+\n function makeRun(overrides: Record = {}) {\n return {\n id: \"run_1\",\n@@ -120,7 +128,7 @@ function makeRun(overrides: Record = {}) {\n origin_url: \"https://github.com/fallback/repo.git\",\n provider: \"github\",\n },\n- created_by: null,\n+ created_by: TEST_PRINCIPAL,\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts\nindex 45f288bf5..cb9906196 100644\n--- a/apps/fabro-web/app/routes/run-detail.test.ts\n+++ b/apps/fabro-web/app/routes/run-detail.test.ts\n@@ -20,6 +20,14 @@ let currentQuestions: any[] = [];\n let deleteRunApiResult: Promise | null = null;\n const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];\n \n+const TEST_PRINCIPAL = {\n+ kind: \"user\" as const,\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\" as const,\n+ avatar_url: null,\n+};\n+\n const deleteRunApiMock = mock((_id: string) =>\n deleteRunApiResult ?? Promise.resolve({}),\n );\n@@ -221,7 +229,7 @@ function makeRunSummary({\n workflow: { slug: \"default\", name: \"Default\", graph_name: null, node_count: 0, edge_count: 0 },\n automation,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: TEST_PRINCIPAL,\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 457f4bd41..b6d95b70c 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@@ -18,6 +18,14 @@ const virtualizerCalls: any[] = [];\n const providerCalls: any[] = [];\n const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];\n \n+const TEST_PRINCIPAL = {\n+ kind: \"user\" as const,\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\" as const,\n+ avatar_url: null,\n+};\n+\n mock.module(\"@pierre/diffs/react\", () => ({\n MultiFileDiff: (props: any) => {\n multiFileDiffCalls.push(props);\n@@ -51,7 +59,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: TEST_PRINCIPAL,\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 dd5120af8..e2be07d12 100644\n--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx\n@@ -7,6 +7,14 @@ import { ToastProvider } from \"../components/toast\";\n import { CHILD_RUNS_LIST_PREFERENCES_STORAGE_KEY } from \"../components/runs-list/preferences\";\n import { setupReactTestEnv } from \"../lib/test-utils\";\n \n+const TEST_PRINCIPAL = {\n+ kind: \"user\" as const,\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\" as const,\n+ avatar_url: null,\n+};\n+\n class MemoryStorage {\n values = new Map();\n \n@@ -35,7 +43,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: TEST_PRINCIPAL,\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..edc7e388f 100644\n--- a/apps/fabro-web/app/routes/runs.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.test.tsx\n@@ -12,6 +12,14 @@ import {\n } from \"./runs\";\n import { summarizeBatchLifecycleAction } from \"../components/runs-list/batch-lifecycle\";\n \n+const TEST_PRINCIPAL = {\n+ kind: \"user\" as const,\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\" as const,\n+ avatar_url: null,\n+};\n+\n function boardRun(id: string, column: BoardColumn, questionText?: string): Run {\n const status =\n column === \"blocked\"\n@@ -34,7 +42,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: TEST_PRINCIPAL,\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md\nindex 63f6f8f54..013904b50 100644\n--- a/docs/internal/logging-strategy.md\n+++ b/docs/internal/logging-strategy.md\n@@ -118,7 +118,7 @@ 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.); `none` means no request principal was established |\n | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) |\n | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests |\n \ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex 35d7511e5..bfb3e66a1 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -8992,6 +8992,8 @@ components:\n \n RunProvenance:\n type: object\n+ required:\n+ - subject\n properties:\n server:\n oneOf:\n@@ -9002,9 +9004,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@@ -9014,7 +9014,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@@ -9024,7 +9023,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@@ -9114,15 +9112,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@@ -10250,6 +10239,7 @@ components:\n - run_id\n - settings\n - graph\n+ - provenance\n properties:\n run_id:\n type: string\n@@ -10273,9 +10263,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@@ -10587,9 +10575,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 284a1956f..e7ab4cfe8 100644\n--- a/lib/crates/fabro-api/Cargo.toml\n+++ b/lib/crates/fabro-api/Cargo.toml\n@@ -27,6 +27,9 @@ serde.workspace = true\n serde_json.workspace = true\n uuid = { workspace = true, features = [\"serde\"] }\n \n+[dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\n+\n [build-dependencies]\n openapiv3 = \"2\"\n progenitor = \"0.13\"\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 dc2c9297f..95ab7e558 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,6 +1,7 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunEvent as ApiRunEvent;\n+use fabro_types::test_support::test_run_provenance;\n use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures};\n use serde_json::{Value, json};\n \n@@ -19,6 +20,7 @@ fn run_event_round_trips_run_created() {\n \"properties\": {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_run_provenance(),\n \"run_dir\": \"/tmp/fabro/run-1\",\n \"source_directory\": \"/tmp/fabro/run-1\"\n }\n@@ -37,6 +39,7 @@ fn run_event_round_trips_run_created_with_web_url() {\n \"properties\": {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_run_provenance(),\n \"run_dir\": \"/tmp/fabro/run-1\",\n \"source_directory\": \"/tmp/fabro/run-1\",\n \"web_url\": format!(\"http://localhost:3000/runs/{}\", fixtures::RUN_1)\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 9a2a0f310..4ab846a7f 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@@ -137,7 +137,7 @@ fn run_spec_json() -> serde_json::Value {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::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 798448941..24c97c52a 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@@ -8,6 +8,7 @@ use fabro_api::types::{\n RunRunnableSource as ApiRunRunnableSource, RunSize as ApiRunSize,\n };\n use fabro_types::status::{RunStatus, SuccessReason};\n+use fabro_types::test_support::test_principal;\n use fabro_types::{\n AskFabro, AskFabroUnavailableReason, AutomationRef, DiffSummary, PullRequestLink,\n RepositoryProvider, RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary,\n@@ -88,7 +89,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_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"team\".to_string(), \"core\".to_string())]),\n lifecycle: RunLifecycle {\n@@ -161,7 +162,7 @@ fn run_summary_json_matches_openapi_shape() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n- \"created_by\": null,\n+ \"created_by\": test_principal(),\n \"origin\": {\n \"kind\": \"api\"\n },\n@@ -253,6 +254,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": test_principal(),\n \"models\": [],\n \"timestamps\": {\n \"created_at\": \"2026-04-20T12:00:00Z\",\n@@ -275,6 +277,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n assert_eq!(summary.workflow.edge_count, 0);\n assert_eq!(summary.goal, \"ship it\");\n assert_eq!(summary.title, \"ship it\");\n+ assert_eq!(summary.created_by, test_principal());\n assert_eq!(summary.labels, HashMap::new());\n assert_eq!(summary.source_directory, None);\n assert_eq!(\ndiff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml\nindex 266f41244..4850b26fc 100644\n--- a/lib/crates/fabro-cli/Cargo.toml\n+++ b/lib/crates/fabro-cli/Cargo.toml\n@@ -128,6 +128,7 @@ temp-env = \"0.3\"\n httpmock = \"0.8\"\n fabro-test = { workspace = true }\n fabro-macros = { path = \"../fabro-macros\" }\n+fabro-types = { path = \"../fabro-types\", features = [\"clap\", \"test-support\"] }\n hkdf.workspace = true\n reqwest = { workspace = true, features = [\"cookies\"] }\n tokio = { workspace = true, features = [\"test-util\", \"macros\"] }\ndiff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs\nindex 77c5c113a..5e49adf42 100644\n--- a/lib/crates/fabro-cli/src/commands/run/attach.rs\n+++ b/lib/crates/fabro-cli/src/commands/run/attach.rs\n@@ -841,7 +841,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::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 ec0d191e3..1934d8c2a 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,18 @@ 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+ \"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 275ff9437..4625cb81c 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,7 @@ pub(crate) fn remote_run_summary_json(\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": fabro_types::test_support::test_principal(),\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 80b65e4aa..0e3122606 100644\n--- a/lib/crates/fabro-cli/tests/it/support/mod.rs\n+++ b/lib/crates/fabro-cli/tests/it/support/mod.rs\n@@ -49,7 +49,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s\n automation: None,\n source_directory: Some(\"/srv/repo\".to_string()),\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::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 397d55a3f..b4da2215c 100644\n--- a/lib/crates/fabro-dump/src/lib.rs\n+++ b/lib/crates/fabro-dump/src/lib.rs\n@@ -476,6 +476,7 @@ mod tests {\n Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunSandboxInstance,\n RunSandboxPlan, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage,\n StageOutcome, StartRecord, SuccessReason, WorkflowSettings, first_event_seq, fixtures,\n+ test_support,\n };\n use futures::executor;\n \n@@ -498,7 +499,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 987c2631b..3a9d6e055 100644\n--- a/lib/crates/fabro-server/src/auth/cli_flow.rs\n+++ b/lib/crates/fabro-server/src/auth/cli_flow.rs\n@@ -1671,11 +1671,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@@ -2080,7 +2080,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@@ -2273,8 +2273,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 d189b2a21..2825aa348 100644\n--- a/lib/crates/fabro-server/src/demo/mod.rs\n+++ b/lib/crates/fabro-server/src/demo/mod.rs\n@@ -1081,7 +1081,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@@ -1092,13 +1092,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(\"static 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@@ -1171,7 +1180,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 f9e8c385c..2012b74c7 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@@ -76,7 +76,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@@ -87,7 +87,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@@ -98,7 +98,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@@ -125,7 +125,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@@ -148,7 +148,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@@ -172,22 +172,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@@ -402,7 +403,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@@ -412,7 +413,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@@ -428,11 +429,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@@ -443,9 +444,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@@ -453,8 +454,8 @@ fn require_worker_or_user_for_run(\n fn require_worker_for_run(slot: &AuthContextSlot, route_run_id: &RunId) -> Result<(), ApiError> {\n let context = slot.0.lock().expect(\"auth context lock poisoned\");\n match &context.principal {\n- Principal::Worker { run_id } if run_id == route_run_id => Ok(()),\n- Principal::Worker { .. } | Principal::User(_) => Err(ApiError::forbidden()),\n+ Some(Principal::Worker { run_id }) if run_id == route_run_id => Ok(()),\n+ Some(Principal::Worker { .. } | Principal::User(_)) => Err(ApiError::forbidden()),\n _ => Err(auth_rejection(context.auth_status, context.auth_error_code)),\n }\n }\n@@ -465,13 +466,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@@ -687,7 +688,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@@ -727,7 +728,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@@ -746,7 +747,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@@ -825,7 +826,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 1f358821b..e49567162 100644\n--- a/lib/crates/fabro-server/src/run_files.rs\n+++ b/lib/crates/fabro-server/src/run_files.rs\n@@ -2389,7 +2389,7 @@ index 1111111..2222222 160000\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::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 4feb22133..f5a09ab61 100644\n--- a/lib/crates/fabro-server/src/run_manifest.rs\n+++ b/lib/crates/fabro-server/src/run_manifest.rs\n@@ -27,7 +27,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@@ -192,6 +194,7 @@ pub(crate) fn validate_prepared_manifest(\n \n pub(crate) fn create_run_input(\n prepared: PreparedManifest,\n+ provenance: RunProvenance,\n configured_providers: Vec,\n web_url: Option,\n ) -> CreateRunInput {\n@@ -209,7 +212,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 7bff4968f..4710c40ca 100644\n--- a/lib/crates/fabro-server/src/server.rs\n+++ b/lib/crates/fabro-server/src/server.rs\n@@ -1876,7 +1876,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@@ -1914,27 +1917,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 180c04bdd..ff2fabcdc 100644\n--- a/lib/crates/fabro-server/src/server/handler/events.rs\n+++ b/lib/crates/fabro-server/src/server/handler/events.rs\n@@ -570,7 +570,7 @@ mod stage_events_tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 06beb7c0b..5df7e72c5 100644\n--- a/lib/crates/fabro-server/src/server/handler/lifecycle.rs\n+++ b/lib/crates/fabro-server/src/server/handler/lifecycle.rs\n@@ -894,7 +894,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 7b673fad8..73bf6f374 100644\n--- a/lib/crates/fabro-server/src/server/handler/pair.rs\n+++ b/lib/crates/fabro-server/src/server/handler/pair.rs\n@@ -1024,7 +1024,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 fca9fdc3c..41fdc4070 100644\n--- a/lib/crates/fabro-server/src/server/handler/runs.rs\n+++ b/lib/crates/fabro-server/src/server/handler/runs.rs\n@@ -687,13 +687,14 @@ pub(crate) async fn create_run_from_manifest(\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+ provenance,\n ready_provider_ids.clone(),\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(submitted_manifest_bytes);\n create_input.automation = automation;\n \n@@ -864,7 +865,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 61d8b7ee9..d3e77d7da 100644\n--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs\n@@ -1299,6 +1299,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::test_support::test_run_provenance;\n use fabro_types::{Graph, RunId, WorkflowSettings};\n use serde_json::{Value, json};\n use tower::ServiceExt;\n@@ -1339,6 +1340,7 @@ mod retrieve_sandbox_tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": 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 bad48c6d4..cd8c07579 100644\n--- a/lib/crates/fabro-server/src/server/handler/sessions.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sessions.rs\n@@ -1700,7 +1700,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::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 363884738..418ffc098 100644\n--- a/lib/crates/fabro-server/src/server/tests.rs\n+++ b/lib/crates/fabro-server/src/server/tests.rs\n@@ -4022,7 +4022,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -4076,7 +4076,7 @@ async fn create_slack_notification_run(\n workflow_slug: workflow_slug.map(str::to_string),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -5083,7 +5083,7 @@ async fn list_run_stages_distinguishes_visits() {\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -6140,7 +6140,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: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -9943,15 +9943,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@@ -12310,7 +12305,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -13062,7 +13057,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 033e46db6..e231599b2 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 a3bf7ad76..01d762a73 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@@ -69,7 +69,7 @@ async fn append_completed_run_with_final_patch(\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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..c81bca853 100644\n--- a/lib/crates/fabro-store/Cargo.toml\n+++ b/lib/crates/fabro-store/Cargo.toml\n@@ -32,6 +32,7 @@ futures.workspace = true\n uuid.workspace = true\n \n [dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\n tokio = { workspace = true, features = [\"test-util\", \"macros\"] }\n tempfile = \"3\"\n ulid.workspace = true\ndiff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs\nindex cd6fa0640..d525906bc 100644\n--- a/lib/crates/fabro-store/src/run_state.rs\n+++ b/lib/crates/fabro-store/src/run_state.rs\n@@ -922,11 +922,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@@ -1276,7 +1272,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@@ -1358,7 +1354,7 @@ mod tests {\n automation: 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@@ -1644,6 +1640,7 @@ mod tests {\n properties: &serde_json::Value,\n node_id: Option<&str>,\n ) -> EventEnvelope {\n+ let properties = run_created_properties(event, properties);\n EventEnvelope {\n seq,\n event: RunEvent::from_value(json!({\n@@ -1665,6 +1662,7 @@ mod tests {\n properties: &serde_json::Value,\n node_id: Option<&str>,\n ) -> EventEnvelope {\n+ let properties = run_created_properties(event, properties);\n EventEnvelope {\n seq,\n event: RunEvent::from_value(json!({\n@@ -1679,6 +1677,19 @@ mod tests {\n }\n }\n \n+ fn run_created_properties(event: &str, properties: &serde_json::Value) -> serde_json::Value {\n+ let mut properties = properties.clone();\n+ if event == \"run.created\" && properties.get(\"provenance\").is_none() {\n+ if let Some(object) = properties.as_object_mut() {\n+ object.insert(\n+ \"provenance\".to_string(),\n+ serde_json::to_value(test_support::test_run_provenance()).unwrap(),\n+ );\n+ }\n+ }\n+ properties\n+ }\n+\n #[test]\n fn live_run_timing_returns_none_before_run_starts() {\n let state = initialized_projection();\n@@ -1822,7 +1833,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@@ -2851,7 +2862,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@@ -2877,7 +2888,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@@ -3016,6 +3027,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 c6406fd8d..568f07fac 100644\n--- a/lib/crates/fabro-store/src/slate/mod.rs\n+++ b/lib/crates/fabro-store/src/slate/mod.rs\n@@ -470,6 +470,7 @@ fn active_run_from(\n #[cfg(test)]\n mod tests {\n use chrono::{DateTime, Utc};\n+ use fabro_types::test_support::test_run_provenance;\n use fabro_types::{\n AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId,\n SuccessReason, WorkflowSettings,\n@@ -542,7 +543,7 @@ mod tests {\n automation: None,\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_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\n@@ -601,6 +602,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@@ -626,6 +628,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@@ -1300,6 +1303,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..013e8aba2 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,6 +667,7 @@ mod tests {\n use std::sync::Arc;\n use std::time::Duration;\n \n+ use fabro_types::test_support::test_run_provenance;\n use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings};\n use object_store::memory::InMemory;\n use serde_json::json;\n@@ -723,6 +724,7 @@ mod tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": 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 6584a36cd..ffc7fbb03 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, RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime,\n RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage, StageOutcome, StartRecord,\n- WorkflowSettings, first_event_seq, fixtures,\n+ WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -22,7 +22,7 @@ fn sample_run_spec() -> RunSpec {\n automation: None,\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..1595dc28f 100644\n--- a/lib/crates/fabro-tool/src/common.rs\n+++ b/lib/crates/fabro-tool/src/common.rs\n@@ -307,6 +307,7 @@ fn format_tool_error(err: &anyhow::Error) -> String {\n #[cfg(test)]\n mod tests {\n use chrono::{TimeZone, Utc};\n+ use fabro_types::test_support::test_principal;\n use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n \n use super::*;\n@@ -413,7 +414,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: 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..8988d1a4e 100644\n--- a/lib/crates/fabro-tool/src/create.rs\n+++ b/lib/crates/fabro-tool/src/create.rs\n@@ -506,6 +506,7 @@ mod tests {\n use async_trait::async_trait;\n use chrono::{TimeZone, Utc};\n use fabro_api::types;\n+ use fabro_types::test_support::test_principal;\n use fabro_types::{\n EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus,\n RunTimestamps, WorkflowRef,\n@@ -902,7 +903,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: 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..9f7957d15 100644\n--- a/lib/crates/fabro-tool/src/interact.rs\n+++ b/lib/crates/fabro-tool/src/interact.rs\n@@ -451,6 +451,7 @@ mod tests {\n \n use async_trait::async_trait;\n use chrono::{TimeZone, Utc};\n+ use fabro_types::test_support::test_principal;\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection,\n RunStatus, RunTimestamps, WorkflowRef,\n@@ -690,7 +691,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: 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..379806d07 100644\n--- a/lib/crates/fabro-tool/src/search.rs\n+++ b/lib/crates/fabro-tool/src/search.rs\n@@ -293,6 +293,7 @@ mod tests {\n use std::collections::HashMap;\n \n use chrono::{TimeZone, Utc};\n+ use fabro_types::test_support::test_principal;\n use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n \n use super::*;\n@@ -444,7 +445,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: 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 f1a8c8b39..cf2605429 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 2b27d0a06..2f1671fe6 100644\n--- a/lib/crates/fabro-types/src/run.rs\n+++ b/lib/crates/fabro-types/src/run.rs\n@@ -24,14 +24,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@@ -93,8 +92,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 52cbb83a9..a5acb6795 100644\n--- a/lib/crates/fabro-types/src/run_event/mod.rs\n+++ b/lib/crates/fabro-types/src/run_event/mod.rs\n@@ -931,6 +931,7 @@ mod tests {\n use serde_json::json;\n \n use super::*;\n+ use crate::test_support::test_run_provenance;\n use crate::{\n AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings,\n fixtures,\n@@ -1017,7 +1018,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_run_provenance()\n }\n });\n \n@@ -1038,6 +1040,7 @@ mod tests {\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\",\n+ \"provenance\": 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 e3023997a..06077171d 100644\n--- a/lib/crates/fabro-types/src/run_event/run.rs\n+++ b/lib/crates/fabro-types/src/run_event/run.rs\n@@ -30,8 +30,7 @@ pub struct RunCreatedProps {\n pub automation: 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 4b52dcc96..47dffdcf7 100644\n--- a/lib/crates/fabro-types/src/run_projection.rs\n+++ b/lib/crates/fabro-types/src/run_projection.rs\n@@ -681,6 +681,7 @@ mod title_tests {\n \n use chrono::Utc;\n \n+ use crate::test_support::test_run_provenance;\n use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings};\n \n fn projection_with_goal(goal: Option<&str>) -> RunProjection {\n@@ -700,7 +701,7 @@ mod title_tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -752,6 +753,7 @@ mod iter_stages_tests {\n use serde_json::json;\n \n use super::RunProjection;\n+ use crate::test_support::test_run_provenance;\n use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings};\n \n fn seq(n: u32) -> NonZeroU32 {\n@@ -770,7 +772,7 @@ mod iter_stages_tests {\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: 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 fd35dc2a4..3c4321052 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..994813974\n--- /dev/null\n+++ b/lib/crates/fabro-types/src/test_support.rs\n@@ -0,0 +1,19 @@\n+use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance};\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: None,\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 49af80c50..687bc57ff 100644\n--- a/lib/crates/fabro-types/tests/run_event_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_event_serde.rs\n@@ -6,7 +6,7 @@ use fabro_types::run_event::run::{RunCreatedProps, RunParentLinkedProps, RunPare\n use fabro_types::run_event::{RunSessionTurnFailedCode, RunSessionTurnFailedProps};\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n-use fabro_types::{AutomationRef, EventBody, TurnId, WorkflowSettings, fixtures};\n+use fabro_types::{AutomationRef, EventBody, TurnId, WorkflowSettings, fixtures, test_support};\n \n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n@@ -32,7 +32,7 @@ fn run_created_props_round_trip_templated_settings() {\n trigger_id: Some(\"schedule_1\".to_string()),\n }),\n db_prefix: Some(\"run_\".to_string()),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: Some(GitContext {\n origin_url: \"https://github.com/fabro-sh/fabro.git\".to_string(),\n@@ -97,7 +97,7 @@ fn run_created_props_omits_web_url_when_absent() {\n workflow_slug: None,\n automation: 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@@ -127,22 +127,6 @@ fn run_created_props_omits_web_url_when_absent() {\n assert_eq!(round_trip.retried_from, None);\n }\n \n-#[test]\n-fn run_created_props_defaults_additive_fields_for_legacy_events() {\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- });\n-\n- let props: RunCreatedProps =\n- serde_json::from_value(json).expect(\"legacy props should deserialize\");\n- assert_eq!(props.retried_from, None);\n- assert_eq!(props.automation, None);\n-}\n-\n #[test]\n fn run_parent_events_round_trip_parent_ids() {\n let linked = EventBody::RunParentLinked(RunParentLinkedProps {\ndiff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs\nindex b05dca05e..6eb9e105c 100644\n--- a/lib/crates/fabro-types/tests/run_spec_methods.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_methods.rs\n@@ -3,7 +3,7 @@ use std::collections::HashMap;\n use fabro_types::graph::Graph;\n use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec};\n use fabro_types::settings::{ProjectNamespace, WorkflowNamespace};\n-use fabro_types::{WorkflowSettings, fixtures};\n+use fabro_types::{WorkflowSettings, fixtures, test_support};\n \n fn sample_run_spec() -> RunSpec {\n let settings = WorkflowSettings {\n@@ -27,7 +27,7 @@ fn sample_run_spec() -> RunSpec {\n automation: None,\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_support::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 89b09ccbd..437afad5f 100644\n--- a/lib/crates/fabro-types/tests/run_spec_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_serde.rs\n@@ -4,7 +4,7 @@ use fabro_types::graph::Graph;\n use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunSpec};\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n-use fabro_types::{AutomationRef, WorkflowSettings, fixtures};\n+use fabro_types::{AutomationRef, WorkflowSettings, fixtures, test_support};\n \n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n@@ -27,7 +27,7 @@ fn run_spec_round_trips_templated_settings() {\n }),\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_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\n@@ -75,15 +75,16 @@ fn run_spec_round_trips_templated_settings() {\n }\n \n #[test]\n-fn run_spec_defaults_automation_for_legacy_specs() {\n+fn run_spec_defaults_automation_when_field_absent() {\n let json = serde_json::json!({\n \"run_id\": fixtures::RUN_1,\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"ship\"),\n- \"labels\": {}\n+ \"labels\": {},\n+ \"provenance\": test_support::test_run_provenance()\n });\n \n- let record: RunSpec = serde_json::from_value(json).expect(\"legacy spec should deserialize\");\n+ let record: RunSpec = serde_json::from_value(json).expect(\"run spec should deserialize\");\n \n assert_eq!(record.automation, None);\n }\ndiff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs\nindex d9e2867ab..73391c9bc 100644\n--- a/lib/crates/fabro-workflow/src/billing_rollup.rs\n+++ b/lib/crates/fabro-workflow/src/billing_rollup.rs\n@@ -353,7 +353,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::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 4c1f91228..5f0fc9959 100644\n--- a/lib/crates/fabro-workflow/src/event/convert.rs\n+++ b/lib/crates/fabro-workflow/src/event/convert.rs\n@@ -2339,7 +2339,7 @@ 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 let automation = AutomationRef {\n id: \"nightly\".to_string(),\n@@ -2348,25 +2348,25 @@ mod tests {\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- automation: Some(automation.clone()),\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+ automation: Some(automation.clone()),\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 886868804..0b5afb1c7 100644\n--- a/lib/crates/fabro-workflow/src/event/events.rs\n+++ b/lib/crates/fabro-workflow/src/event/events.rs\n@@ -41,8 +41,7 @@ pub enum Event {\n automation: 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 7b65fc99c..4acc5265a 100644\n--- a/lib/crates/fabro-workflow/src/event/sink.rs\n+++ b/lib/crates/fabro-workflow/src/event/sink.rs\n@@ -244,7 +244,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 643d49b58..0f0613fe8 100644\n--- a/lib/crates/fabro-workflow/src/git.rs\n+++ b/lib/crates/fabro-workflow/src/git.rs\n@@ -469,7 +469,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 3a120e8fc..3b048374b 100644\n--- a/lib/crates/fabro-workflow/src/handler/agent.rs\n+++ b/lib/crates/fabro-workflow/src/handler/agent.rs\n@@ -484,7 +484,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 f10ebafde..2e59a954b 100644\n--- a/lib/crates/fabro-workflow/src/handler/command.rs\n+++ b/lib/crates/fabro-workflow/src/handler/command.rs\n@@ -256,7 +256,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -357,7 +357,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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..8520a0c93 100644\n--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs\n+++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs\n@@ -2133,7 +2133,7 @@ reasoning = false\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: fabro_types::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 b52478bac..6772db8fb 100644\n--- a/lib/crates/fabro-workflow/src/handler/parallel.rs\n+++ b/lib/crates/fabro-workflow/src/handler/parallel.rs\n@@ -728,7 +728,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 1c2a82267..d88c9e875 100644\n--- a/lib/crates/fabro-workflow/src/handler/prompt.rs\n+++ b/lib/crates/fabro-workflow/src/handler/prompt.rs\n@@ -283,7 +283,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 ccd24632d..233044930 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs\n@@ -736,7 +736,7 @@ mod tests {\n workflow_slug: Some(\"metadata\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 db4b0ccf9..2e0112a61 100644\n--- a/lib/crates/fabro-workflow/src/operations/archive.rs\n+++ b/lib/crates/fabro-workflow/src/operations/archive.rs\n@@ -226,7 +226,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 8bf16d115..f68c3545b 100644\n--- a/lib/crates/fabro-workflow/src/operations/create.rs\n+++ b/lib/crates/fabro-workflow/src/operations/create.rs\n@@ -45,7 +45,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@@ -72,7 +72,7 @@ struct PersistCreateOptions {\n automation: Option,\n git: Option,\n fork_source_ref: Option,\n- provenance: Option,\n+ provenance: RunProvenance,\n configured_providers: Vec,\n catalog: Arc,\n }\n@@ -1115,7 +1115,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1183,7 +1183,7 @@ mod tests {\n }),\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1295,7 +1295,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1341,7 +1341,7 @@ mod tests {\n }),\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1414,7 +1414,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1467,7 +1467,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@@ -1476,12 +1476,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@@ -1494,7 +1494,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@@ -1502,7 +1502,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 2bf7e144f..006f960fa 100644\n--- a/lib/crates/fabro-workflow/src/operations/fork.rs\n+++ b/lib/crates/fabro-workflow/src/operations/fork.rs\n@@ -383,7 +383,7 @@ mod tests {\n workflow_slug: Some(\"fork-source\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 be7793d3e..86dff030b 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@@ -152,7 +152,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@@ -191,7 +191,7 @@ mod tests {\n workflow_slug: Some(\"retry-source\".to_string()),\n automation: None,\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@@ -368,7 +368,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@@ -402,14 +402,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@@ -463,7 +456,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: None,\n })\n .await\n@@ -517,7 +510,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@@ -535,7 +528,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 8083350be..3ac123a9a 100644\n--- a/lib/crates/fabro-workflow/src/operations/start.rs\n+++ b/lib/crates/fabro-workflow/src/operations/start.rs\n@@ -1438,7 +1438,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1860,7 +1860,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::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 68e0ee959..bfe0da88d 100644\n--- a/lib/crates/fabro-workflow/src/operations/timeline.rs\n+++ b/lib/crates/fabro-workflow/src/operations/timeline.rs\n@@ -248,7 +248,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::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 d05398556..0deb440e5 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n@@ -165,7 +165,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: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -208,7 +208,7 @@ async fn seed_created_and_starting(\n workflow_slug: run_options.workflow_slug.clone(),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 c677c1007..3b6344422 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n@@ -739,7 +739,7 @@ mod tests {\n workflow_slug: Some(\"metadata\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -856,7 +856,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::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 cc1b0777a..b2894e2b6 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n@@ -773,7 +773,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::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 7a8c896e4..64ef56a2e 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs\n@@ -148,7 +148,7 @@ mod tests {\n (\"env\".to_string(), \"test\".to_string()),\n (\"team\".to_string(), \"workflow\".to_string()),\n ]),\n- provenance: None,\n+ provenance: fabro_types::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 c02376988..c620e3685 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n@@ -823,7 +823,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1148,7 +1148,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1219,7 +1219,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1575,7 +1575,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: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1704,7 +1704,7 @@ mod tests {\n source_directory: Some(\"/tmp/project\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1722,7 +1722,7 @@ mod tests {\n workflow_slug: run_spec.workflow_slug.clone(),\n automation: 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,\n@@ -1875,7 +1875,7 @@ mod tests {\n source_directory: None,\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1893,7 +1893,7 @@ mod tests {\n workflow_slug: None,\n automation: 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 e7b0badfc..e62e17fbe 100644\n--- a/lib/crates/fabro-workflow/src/run_lookup.rs\n+++ b/lib/crates/fabro-workflow/src/run_lookup.rs\n@@ -491,7 +491,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::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 b11c9667c..40c42a05f 100644\n--- a/lib/crates/fabro-workflow/src/run_metadata.rs\n+++ b/lib/crates/fabro-workflow/src/run_metadata.rs\n@@ -639,7 +639,7 @@ mod tests {\n push_outcome: PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::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 0dafbfdad..da3cfb099 100644\n--- a/lib/crates/fabro-workflow/src/runtime_store.rs\n+++ b/lib/crates/fabro-workflow/src/runtime_store.rs\n@@ -148,7 +148,7 @@ mod tests {\n source_directory: Some(\"/tmp/test\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -170,7 +170,7 @@ mod tests {\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::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 e72a0ee1b..3996dc406 100644\n--- a/lib/crates/fabro-workflow/src/test_support.rs\n+++ b/lib/crates/fabro-workflow/src/test_support.rs\n@@ -10,6 +10,7 @@ use fabro_graphviz::graph::Graph as GvGraph;\n use fabro_interview::AutoApproveInterviewer;\n use fabro_model::Catalog;\n use fabro_store::{ArtifactStore, Database, RunProjection};\n+use fabro_types::{AuthMethod, IdpIdentity, Principal, RunProvenance};\n use object_store::local::LocalFileSystem;\n \n use crate::artifact_upload::ArtifactSink;\n@@ -53,6 +54,18 @@ async fn execute_and_emit_terminal(initialized: InitializedState) -> Executed {\n executed\n }\n \n+fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: 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+\n /// Construct a fully-populated `BilledModelUsage` for tests. Centralised so\n /// callers don't keep rebuilding the same JSON skeleton.\n #[must_use]\n@@ -175,7 +188,7 @@ async fn initialized(\n workflow_slug: run_options.workflow_slug.clone(),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex 5e97116d9..7839b95b6 100644\n--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n+++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n@@ -271,7 +271,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 87b9c189c..af8ae77e0 100644\n--- a/lib/packages/fabro-api-client/src/models/index.ts\n+++ b/lib/packages/fabro-api-client/src/models/index.ts\n@@ -244,7 +244,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 f1e5adab6..6abad2884 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@@ -41,7 +41,7 @@ export interface RunSpec {\n 'automation'?: AutomationRef | 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 114b1e640..5fff8b8f4 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": 384, + "deletions": 308 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-06-08T20:53:24.646344Z", + "current_node": "simplify_opus", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "implement", + "simplify_opus" + ], + "node_retries": {}, + "context_values": { + "thread.preflight_compile.current_node": "preflight_lint", + "internal.thread_id": "implement", + "internal.run_id": "01KTM9H228G0Z1ATDMZ10GGS4W", + "thread.implement.current_node": "simplify_opus", + "failure_class": "", + "internal.retry_count.simplify_opus": 0, + "internal.retry_count.preflight_compile": 0, + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "thread.preflight_lint.current_node": "implement", + "thread.toolchain.current_node": "preflight_compile", + "internal.retry_count.preflight_lint": 0, + "graph.rankdir": "LR", + "response.implement": "Implemented the plan in the requested two commits, in order:\n\n1. `199c93c5e Remove anonymous principal actor`\n - Removed `Principal::Anonymous`.\n - Request auth now represents no actor as `None`.\n - HTTP logs use `principal_kind=\"none\"` for absent principals.\n - Updated OpenAPI/generated TS client/frontend/tests accordingly.\n\n2. `fffe671dd Make run provenance and creator total`\n - Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance non-optional end-to-end.\n - Removed null/default/fallback provenance handling.\n - Updated create/retry/projection flows to require and propagate total provenance.\n - Added gated `fabro-types` test support helpers.\n - Added synthetic demo principal.\n - Updated OpenAPI and regenerated generated clients.\n - Updated frontend models/rendering/tests for total `created_by`.\n\nFinal repo state is clean.\n\nValidation completed:\n- `cargo build -p fabro-api`\n- TypeScript API client generation with local JRE\n- `cargo check -q --workspace --all-targets`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace --no-fail-fast`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- Final grep sweep for stale anonymous/null provenance traces; remaining hits are unrelated non-run-provenance `subject` fields.\n\nManual browser/server end-to-end checks from the plan were not run in this terminal environment.", "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.work_dir": "/home/daytona/workspace/fabro", "internal.node_visit_count": 1, + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "internal.retry_count.start": 0, + "response.simplify_opus": "All checks green. Summary of cleanup:\n\n**Fixed:**\n\n1. **Duplicate `TEST_PRINCIPAL` literal across 8 frontend test files** — extracted to `apps/fabro-web/app/lib/test-fixtures.ts` (typed as `Principal` from the API client). All 8 test files now import it instead of redeclaring.\n\n2. **Stale assertion + misleading test name** in `run-summary-panel.test.tsx` — removed the `expect(... \"Created by\"))).toBe(\"Ttest\")` line that no longer fit the \"shows unavailable copy\" semantics, and dropped the leftover \"optional\" wording from the test title.\n\n3. **Leftover IIFE wrapper** in `run-table-row.tsx` — hoisted `principalDisplay(run.createdBy)` to a `const createdByDisplay = …` at the top of the component and inlined the `` directly. Removed the no-longer-needed self-invoking-function shape that existed only for the old `run.createdBy && …` guard.\n\n4. **`run_created_properties` compat-shim** in `fabro-store/src/run_state.rs` — removed. The eight `test_raw_event(… \"run.created\" …)` JSON fixtures now include `\"provenance\": test_support::test_run_provenance()` explicitly, so the conditional injection of provenance no longer hides untouched fixtures.\n\n**Skipped (false positive / out-of-scope):**\n\n- The duplicate local `test_run_provenance()` in `fabro-workflow/src/test_support.rs` looked redundant, but the workflow crate exposes `pub mod test_support;` *unconditionally* while `fabro_types::test_support` is feature-gated and only enabled as a dev-dependency. Replacing the local copy broke the library build, so the duplicate stays.\n- Demo `LazyLock` — correct as-is.\n- `Option` in `principal_middleware.rs` — intentional per the plan (\"missing/invalid auth is represented as absent request principal\").\n\n**Verified:** `cargo check --workspace`, `cargo clippy --workspace --all-targets -D warnings`, `cargo fmt --check`, `cargo nextest run --workspace` (6500 pass), `bun run typecheck`, `bun test` (508 pass).", + "failure_signature": "", + "current_node": "simplify_opus", + "last_response": "All checks green. Summary of cleanup:\n\n**Fixed:**\n\n1. **Duplicate `TEST_PRINCIPAL` literal across 8 frontend test files** — extracted to `apps/fabro-web/app/lib/test-fixtures.ts` (typed as `Principa", + "internal.fidelity": "compact", + "thread.start.current_node": "toolchain", + "internal.retry_count.toolchain": 0, + "internal.retry_count.implement": 0, "outcome": "succeeded", - "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n " + "last_stage": "simplify_opus" }, "node_outcomes": { "implement": { @@ -882,6 +1032,58 @@ "active_time_ms": 152673 } }, + "simplify_opus": { + "status": "succeeded", + "context_updates": { + "last_response": "All checks green. Summary of cleanup:\n\n**Fixed:**\n\n1. **Duplicate `TEST_PRINCIPAL` literal across 8 frontend test files** — extracted to `apps/fabro-web/app/lib/test-fixtures.ts` (typed as `Principa", + "last_stage": "simplify_opus", + "response.simplify_opus": "All checks green. Summary of cleanup:\n\n**Fixed:**\n\n1. **Duplicate `TEST_PRINCIPAL` literal across 8 frontend test files** — extracted to `apps/fabro-web/app/lib/test-fixtures.ts` (typed as `Principal` from the API client). All 8 test files now import it instead of redeclaring.\n\n2. **Stale assertion + misleading test name** in `run-summary-panel.test.tsx` — removed the `expect(... \"Created by\"))).toBe(\"Ttest\")` line that no longer fit the \"shows unavailable copy\" semantics, and dropped the leftover \"optional\" wording from the test title.\n\n3. **Leftover IIFE wrapper** in `run-table-row.tsx` — hoisted `principalDisplay(run.createdBy)` to a `const createdByDisplay = …` at the top of the component and inlined the `` directly. Removed the no-longer-needed self-invoking-function shape that existed only for the old `run.createdBy && …` guard.\n\n4. **`run_created_properties` compat-shim** in `fabro-store/src/run_state.rs` — removed. The eight `test_raw_event(… \"run.created\" …)` JSON fixtures now include `\"provenance\": test_support::test_run_provenance()` explicitly, so the conditional injection of provenance no longer hides untouched fixtures.\n\n**Skipped (false positive / out-of-scope):**\n\n- The duplicate local `test_run_provenance()` in `fabro-workflow/src/test_support.rs` looked redundant, but the workflow crate exposes `pub mod test_support;` *unconditionally* while `fabro_types::test_support` is feature-gated and only enabled as a dev-dependency. Replacing the local copy broke the library build, so the duplicate stays.\n- Demo `LazyLock` — correct as-is.\n- `Option` in `principal_middleware.rs` — intentional per the plan (\"missing/invalid auth is represented as absent request principal\").\n\n**Verified:** `cargo check --workspace`, `cargo clippy --workspace --all-targets -D warnings`, `cargo fmt --check`, `cargo nextest run --workspace` (6500 pass), `bun run typecheck`, `bun test` (508 pass)." + }, + "notes": "Stage completed: simplify_opus", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 87930, + "output_tokens": 28686, + "reasoning_tokens": 0, + "cache_read_tokens": 5074609, + "cache_write_tokens": 633736 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 633736, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 7654954 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/apps/fabro-web/app/components/run-summary-panel.test.tsx", + "/home/daytona/workspace/fabro/apps/fabro-web/app/components/runs-list/run-table-row.tsx", + "/home/daytona/workspace/fabro/apps/fabro-web/app/data/runs.test.ts", + "/home/daytona/workspace/fabro/apps/fabro-web/app/lib/run-actions.test.ts", + "/home/daytona/workspace/fabro/apps/fabro-web/app/lib/test-fixtures.ts", + "/home/daytona/workspace/fabro/apps/fabro-web/app/routes/automations-new.test.tsx", + "/home/daytona/workspace/fabro/apps/fabro-web/app/routes/run-detail.test.ts", + "/home/daytona/workspace/fabro/apps/fabro-web/app/routes/run-files.render.test.tsx", + "/home/daytona/workspace/fabro/apps/fabro-web/app/routes/runs.preferences.test.tsx", + "/home/daytona/workspace/fabro/apps/fabro-web/app/routes/runs.test.tsx", + "/home/daytona/workspace/fabro/lib/crates/fabro-store/src/run_state.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/test_support.rs" + ], + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 478800, + "tool_time_ms": 637303, + "active_time_ms": 1116103 + } + }, "start": { "status": "succeeded", "usage": null @@ -915,8 +1117,9 @@ } } }, - "next_node_id": "simplify_opus", + "next_node_id": "simplify_gpt", "node_visits": { + "simplify_opus": 1, "preflight_compile": 1, "implement": 1, "toolchain": 1, @@ -1101,7 +1304,12 @@ "first_event_seq": 50, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Stage completed: implement", + "failure_reason": null, + "timestamp": "2026-06-08T20:34:15.135832Z" + }, "provider_used": { "mode": "agent", "provider": "openai", @@ -1115,6 +1323,12 @@ "output": null, "started_at": "2026-06-08T19:03:38.492981Z", "handler": "agent", + "timing": { + "wall_time_ms": 5436597, + "inference_time_ms": 2723291, + "tool_time_ms": 2405987, + "active_time_ms": 5129278 + }, "usage": { "input_tokens": 3764533, "output_tokens": 47720, @@ -1344,7 +1558,7 @@ ], "warnings": [] }, - "state": "running" + "state": "succeeded" }, "start@1": { "first_event_seq": 16, @@ -1379,6 +1593,265 @@ "cache_write_tokens": 0 }, "state": "succeeded" + }, + "simplify_opus@1": { + "first_event_seq": 1563, + "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-06-08T20:34:26.307225Z", + "handler": "agent", + "usage": { + "input_tokens": 87930, + "output_tokens": 28686, + "total_tokens": 5824961, + "reasoning_tokens": 0, + "cache_read_tokens": 5074609, + "cache_write_tokens": 633736, + "total_usd_micros": 7654954 + }, + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "subagents": [ + { + "agent_id": "058fc78c", + "depth": 1, + "task": "You are reviewing a code change for CODE REUSE issues only.\n\nThe full diff is in /tmp/changes.diff (2693 lines). Read it with `cat /tmp/changes.diff` (or in chunks if needed).\n\nContext: This change makes `Principal::Anonymous` go away and makes `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance non-optional/total. A `test_support` module was added to `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()` helpers.\n\nThe repo root is /home/daytona/workspace/fabro.\n\nYour job: identify CODE REUSE issues in the diff.\n\nFor each change:\n\n1. Search for existing utilities and helpers (use grep/glob) that could replace newly written code. Look for similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.\n3. 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.\n\nSpecific things to look for in this diff:\n- The new `test_principal()` / `test_run_provenance()` helpers — is there an existing `Principal::user(...)` constructor or RunProvenance::new constructor we should use?\n- Demo mode's `DEMO_PRINCIPAL` constant — is there already a similar pattern somewhere?\n- Multiple call sites that now construct provenance inline — could a helper be reused?\n- Any newly added principal-construction snippets in test files that duplicate the test_support helper.\n- The frontend principal-display: any redundant rendering code now that the anonymous case is gone.\n\nDo NOT make any code changes. Just report:\n- File path and a brief description of the issue\n- Suggested existing utility to use\n- Why this would be better\n\nBe concrete and concise. Skip non-issues. If everything looks clean, say so.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 56 + } + }, + { + "agent_id": "0d710db0", + "depth": 1, + "task": "You are reviewing a code change for CODE QUALITY issues only.\n\nThe full diff is in /tmp/changes.diff (2693 lines). Read it with `cat /tmp/changes.diff` (or in chunks if needed).\n\nContext: This change makes `Principal::Anonymous` go away and makes `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance non-optional/total. A `test_support` module was added to `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()` helpers.\n\nThe repo root is /home/daytona/workspace/fabro.\n\nYour job: identify CODE QUALITY issues in the diff.\n\nLook for:\n1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction\n4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n6. Unnecessary JSX nesting: wrapper Boxes/elements that add no layout value\n7. Unnecessary comments: comments explaining WHAT the code does, narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY\n\nSpecifically check:\n- `lib/crates/fabro-types/src/test_support.rs` for quality of the new helpers (Issuer/login string consistency, fake-but-unique values)\n- The `run_provenance(...)` calls in server handlers — is the helper signature reasonable now that subject is non-optional?\n- Whether `created_by: test_principal()` and `provenance: test_run_provenance()` repeats are wrapped in fluent fixture builders anywhere\n- Any leftover `Option`-juggling in code that should now be straightforward direct usage (mismatched unwraps, redundant `.clone()`, `Some(...)` wrapping that's now meaningless)\n- Comments left over that narrate the change (\"now non-optional\", \"since X removed\", etc.)\n- Test deserializations that previously tested null/legacy tolerance but are now meaningless\n\nDo NOT make any code changes. Just report:\n- File path + line range\n- A brief description of the issue\n- Suggested fix\n\nBe concrete and concise. Skip non-issues. If everything looks clean, say so.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 58 + } + }, + { + "agent_id": "a8965bdd", + "depth": 1, + "task": "You are reviewing a code change for EFFICIENCY issues only.\n\nThe full diff is in /tmp/changes.diff (2693 lines). Read it with `cat /tmp/changes.diff` (or in chunks if needed).\n\nContext: This change makes `Principal::Anonymous` go away and makes `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance non-optional/total. A `test_support` module was added to `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()` helpers.\n\nThe repo root is /home/daytona/workspace/fabro.\n\nYour job: identify EFFICIENCY issues in the diff.\n\nLook for:\n1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. Missed concurrency: independent operations run sequentially when they could run in parallel\n3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths\n4. Recurring no-op updates: state/store updates inside polling loops, intervals, or event handlers\n5. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU)\n6. Memory: unbounded data structures, missing cleanup, event listener leaks\n7. Overly broad operations: reading entire files when only a portion is needed\n\nSpecifically check:\n- `test_principal()` and `test_run_provenance()` helpers — do they allocate strings every call that could be `LazyLock`'d, or is per-call allocation fine for test code?\n- `DEMO_PRINCIPAL` in `lib/crates/fabro-server/src/demo/mod.rs` — is the `LazyLock` setup correct? Are unnecessary clones added?\n- `run_provenance()` helper in server handlers — extra clones, extra work per request?\n- Any `.clone()` of `Principal` / `RunProvenance` added in hot paths (request handling, projection building, event conversion) that could be avoided by passing references\n- The `Event::RunCreated`/`RunCreatedProps` conversions — moves vs clones\n\nDo NOT make any code changes. Just report:\n- File path + line range\n- A brief description of the issue\n- Suggested fix\n\nBe concrete and concise. Skip non-issues. If everything looks clean, say so.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 48 + } + } + ], + "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": false + }, + { + "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": false + }, + { + "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": false + }, + { + "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": true + } + ], + "context_window": { + "provider": "anthropic", + "model": "claude-opus-4-7", + "context_window_tokens": 1000000, + "input_tokens": 100588, + "usage_percent": 10.0588, + "count_method": "response_usage_scaled_breakdown", + "staleness": "live", + "generated_at": "2026-06-08T20:53:20.852482Z", + "event_seq": 2075, + "breakdown": [ + { + "category": "system_prompt", + "tokens": 2309, + "usage_percent": 0.2309 + }, + { + "category": "tools", + "tokens": 2684, + "usage_percent": 0.2684 + }, + { + "category": "memory", + "tokens": 5705, + "usage_percent": 0.5705 + }, + { + "category": "conversation", + "tokens": 89882, + "usage_percent": 8.9882 + }, + { + "category": "other", + "tokens": 8, + "usage_percent": 0.0008 + } + ], + "warnings": [] + }, + "state": "running" } } } \ No newline at end of file diff --git a/stages/005-implement@1/diff.patch b/stages/005-implement@1/diff.patch new file mode 100644 index 000000000..30e4446a0 --- /dev/null +++ b/stages/005-implement@1/diff.patch @@ -0,0 +1,2693 @@ +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 52e79cf0f..abc5d6e7c 100644 +--- a/apps/fabro-web/app/components/run-summary-panel.test.tsx ++++ b/apps/fabro-web/app/components/run-summary-panel.test.tsx +@@ -34,6 +34,14 @@ function render(props: Partial = {}) { + return tree!; + } + ++const TEST_PRINCIPAL = { ++ kind: "user" as const, ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token" as const, ++ avatar_url: null, ++}; ++ + function cellAfterLabel( + tree: TestRenderer.ReactTestRenderer, + label: string, +@@ -53,7 +61,7 @@ function cellAfterLabel( + function makeRun(overrides: Record = {}) { + return { + id: "run_1", +- created_by: null, ++ created_by: TEST_PRINCIPAL, + diff: null, + billing: null, + ...overrides, +@@ -71,9 +79,9 @@ 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, "Created by"))).toBe("Ttest"); + expect(instanceText(cellAfterLabel(tree, "Changes"))).toBe(EMPTY_VALUE); + expect(instanceText(cellAfterLabel(tree, "Cost"))).toBe(EMPTY_VALUE); + }); +@@ -226,7 +234,7 @@ describe("RunSummaryPanelView", () => { + kind: "user", + identity: { issuer: "github", subject: "1" }, + login: "brynary", +- auth_method: "oauth", ++ auth_method: "github", + }, + }), + }); +@@ -240,7 +248,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", + }, + }), +@@ -252,7 +260,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 20a08af5b..177270630 100644 +--- a/apps/fabro-web/app/components/run-summary-panel.tsx ++++ b/apps/fabro-web/app/components/run-summary-panel.tsx +@@ -116,7 +116,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); + const sandboxKind = sandboxLifecycleKind(run?.sandbox); +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 4fdb82777..5fd17dad1 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 +@@ -54,7 +54,7 @@ export function RunTableRow({ + + {show("created_by") && ( + +- {run.createdBy && (() => { ++ {(() => { + const display = principalDisplay(run.createdBy); + return ( + +diff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts +index 98586b2c4..69f2dbe81 100644 +--- a/apps/fabro-web/app/data/runs.test.ts ++++ b/apps/fabro-web/app/data/runs.test.ts +@@ -9,6 +9,14 @@ import { + runStatusDisplay, + } from "./runs"; + ++const TEST_PRINCIPAL = { ++ kind: "user" as const, ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token" as const, ++ avatar_url: null, ++}; ++ + function makeRun(overrides: Partial = {}): Run { + return { + id: "01ABC", +@@ -17,7 +25,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: TEST_PRINCIPAL, + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts +index 2b277b6d0..3bc9779de 100644 +--- a/apps/fabro-web/app/data/runs.ts ++++ b/apps/fabro-web/app/data/runs.ts +@@ -41,7 +41,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 ffb208aa7..c5cb1245c 100644 +--- a/apps/fabro-web/app/lib/run-actions.test.ts ++++ b/apps/fabro-web/app/lib/run-actions.test.ts +@@ -39,6 +39,14 @@ type CapturedRequest = { + + const originalAdapter = generatedAxios.defaults.adapter; + ++const TEST_PRINCIPAL = { ++ kind: "user" as const, ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token" as const, ++ avatar_url: null, ++}; ++ + function makeRun(status: RunStatus, archived = false): Run { + return { + id: "run-1", +@@ -47,7 +55,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: TEST_PRINCIPAL, + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/routes/automations-new.test.tsx b/apps/fabro-web/app/routes/automations-new.test.tsx +index 47f471abf..2a767fcea 100644 +--- a/apps/fabro-web/app/routes/automations-new.test.tsx ++++ b/apps/fabro-web/app/routes/automations-new.test.tsx +@@ -101,6 +101,14 @@ mock.module("swr", () => ({ + const { default: AutomationsNew } = await import("./automations-new"); + mock.restore(); + ++const TEST_PRINCIPAL = { ++ kind: "user" as const, ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token" as const, ++ avatar_url: null, ++}; ++ + function makeRun(overrides: Record = {}) { + return { + id: "run_1", +@@ -120,7 +128,7 @@ function makeRun(overrides: Record = {}) { + origin_url: "https://github.com/fallback/repo.git", + provider: "github", + }, +- created_by: null, ++ created_by: TEST_PRINCIPAL, + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts +index 45f288bf5..cb9906196 100644 +--- a/apps/fabro-web/app/routes/run-detail.test.ts ++++ b/apps/fabro-web/app/routes/run-detail.test.ts +@@ -20,6 +20,14 @@ let currentQuestions: any[] = []; + let deleteRunApiResult: Promise | null = null; + const mountedRenderers: TestRenderer.ReactTestRenderer[] = []; + ++const TEST_PRINCIPAL = { ++ kind: "user" as const, ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token" as const, ++ avatar_url: null, ++}; ++ + const deleteRunApiMock = mock((_id: string) => + deleteRunApiResult ?? Promise.resolve({}), + ); +@@ -221,7 +229,7 @@ function makeRunSummary({ + workflow: { slug: "default", name: "Default", graph_name: null, node_count: 0, edge_count: 0 }, + automation, + repository: { name: "fabro", origin_url: null, provider: "unknown" }, +- created_by: null, ++ created_by: TEST_PRINCIPAL, + 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 457f4bd41..b6d95b70c 100644 +--- a/apps/fabro-web/app/routes/run-files.render.test.tsx ++++ b/apps/fabro-web/app/routes/run-files.render.test.tsx +@@ -18,6 +18,14 @@ const virtualizerCalls: any[] = []; + const providerCalls: any[] = []; + const mountedRenderers: TestRenderer.ReactTestRenderer[] = []; + ++const TEST_PRINCIPAL = { ++ kind: "user" as const, ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token" as const, ++ avatar_url: null, ++}; ++ + mock.module("@pierre/diffs/react", () => ({ + MultiFileDiff: (props: any) => { + multiFileDiffCalls.push(props); +@@ -51,7 +59,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: TEST_PRINCIPAL, + 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 dd5120af8..e2be07d12 100644 +--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx ++++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx +@@ -7,6 +7,14 @@ import { ToastProvider } from "../components/toast"; + import { CHILD_RUNS_LIST_PREFERENCES_STORAGE_KEY } from "../components/runs-list/preferences"; + import { setupReactTestEnv } from "../lib/test-utils"; + ++const TEST_PRINCIPAL = { ++ kind: "user" as const, ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token" as const, ++ avatar_url: null, ++}; ++ + class MemoryStorage { + values = new Map(); + +@@ -35,7 +43,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: TEST_PRINCIPAL, + 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..edc7e388f 100644 +--- a/apps/fabro-web/app/routes/runs.test.tsx ++++ b/apps/fabro-web/app/routes/runs.test.tsx +@@ -12,6 +12,14 @@ import { + } from "./runs"; + import { summarizeBatchLifecycleAction } from "../components/runs-list/batch-lifecycle"; + ++const TEST_PRINCIPAL = { ++ kind: "user" as const, ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token" as const, ++ avatar_url: null, ++}; ++ + function boardRun(id: string, column: BoardColumn, questionText?: string): Run { + const status = + column === "blocked" +@@ -34,7 +42,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: TEST_PRINCIPAL, + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md +index 63f6f8f54..013904b50 100644 +--- a/docs/internal/logging-strategy.md ++++ b/docs/internal/logging-strategy.md +@@ -118,7 +118,7 @@ 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.); `none` means no request principal was established | + | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) | + | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests | + +diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml +index 35d7511e5..bfb3e66a1 100644 +--- a/docs/public/api-reference/fabro-api.yaml ++++ b/docs/public/api-reference/fabro-api.yaml +@@ -8992,6 +8992,8 @@ components: + + RunProvenance: + type: object ++ required: ++ - subject + properties: + server: + oneOf: +@@ -9002,9 +9004,7 @@ components: + - $ref: "#/components/schemas/RunClientProvenance" + - type: "null" + subject: +- oneOf: +- - $ref: "#/components/schemas/Principal" +- - type: "null" ++ $ref: "#/components/schemas/Principal" + + Principal: + oneOf: +@@ -9014,7 +9014,6 @@ components: + - $ref: "#/components/schemas/PrincipalSlack" + - $ref: "#/components/schemas/PrincipalAgent" + - $ref: "#/components/schemas/PrincipalSystem" +- - $ref: "#/components/schemas/PrincipalAnonymous" + discriminator: + propertyName: kind + mapping: +@@ -9024,7 +9023,6 @@ components: + slack: "#/components/schemas/PrincipalSlack" + agent: "#/components/schemas/PrincipalAgent" + system: "#/components/schemas/PrincipalSystem" +- anonymous: "#/components/schemas/PrincipalAnonymous" + + PrincipalUser: + type: object +@@ -9114,15 +9112,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 +@@ -10250,6 +10239,7 @@ components: + - run_id + - settings + - graph ++ - provenance + properties: + run_id: + type: string +@@ -10273,9 +10263,7 @@ components: + additionalProperties: + type: string + provenance: +- oneOf: +- - $ref: "#/components/schemas/RunProvenance" +- - type: "null" ++ $ref: "#/components/schemas/RunProvenance" + manifest_blob: + type: ["string", "null"] + definition_blob: +@@ -10587,9 +10575,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 284a1956f..e7ab4cfe8 100644 +--- a/lib/crates/fabro-api/Cargo.toml ++++ b/lib/crates/fabro-api/Cargo.toml +@@ -27,6 +27,9 @@ serde.workspace = true + serde_json.workspace = true + uuid = { workspace = true, features = ["serde"] } + ++[dev-dependencies] ++fabro-types = { path = "../fabro-types", features = ["test-support"] } ++ + [build-dependencies] + openapiv3 = "2" + progenitor = "0.13" +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 dc2c9297f..95ab7e558 100644 +--- a/lib/crates/fabro-api/tests/run_event_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs +@@ -1,6 +1,7 @@ + use std::any::{TypeId, type_name}; + + use fabro_api::types::RunEvent as ApiRunEvent; ++use fabro_types::test_support::test_run_provenance; + use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures}; + use serde_json::{Value, json}; + +@@ -19,6 +20,7 @@ fn run_event_round_trips_run_created() { + "properties": { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), ++ "provenance": test_run_provenance(), + "run_dir": "/tmp/fabro/run-1", + "source_directory": "/tmp/fabro/run-1" + } +@@ -37,6 +39,7 @@ fn run_event_round_trips_run_created_with_web_url() { + "properties": { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), ++ "provenance": test_run_provenance(), + "run_dir": "/tmp/fabro/run-1", + "source_directory": "/tmp/fabro/run-1", + "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 9a2a0f310..4ab846a7f 100644 +--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs +@@ -137,7 +137,7 @@ fn run_spec_json() -> serde_json::Value { + automation: None, + source_directory: None, + labels: std::collections::HashMap::new(), +- provenance: None, ++ provenance: fabro_types::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 798448941..24c97c52a 100644 +--- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs +@@ -8,6 +8,7 @@ use fabro_api::types::{ + RunRunnableSource as ApiRunRunnableSource, RunSize as ApiRunSize, + }; + use fabro_types::status::{RunStatus, SuccessReason}; ++use fabro_types::test_support::test_principal; + use fabro_types::{ + AskFabro, AskFabroUnavailableReason, AutomationRef, DiffSummary, PullRequestLink, + RepositoryProvider, RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary, +@@ -88,7 +89,7 @@ fn run_summary_json_matches_openapi_shape() { + origin_url: None, + provider: RepositoryProvider::Unknown, + }), +- created_by: None, ++ created_by: test_principal(), + origin: RunOrigin::default(), + labels: HashMap::from([("team".to_string(), "core".to_string())]), + lifecycle: RunLifecycle { +@@ -161,7 +162,7 @@ fn run_summary_json_matches_openapi_shape() { + "origin_url": null, + "provider": "unknown" + }, +- "created_by": null, ++ "created_by": test_principal(), + "origin": { + "kind": "api" + }, +@@ -253,6 +254,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() { + "origin_url": null, + "provider": "unknown" + }, ++ "created_by": test_principal(), + "models": [], + "timestamps": { + "created_at": "2026-04-20T12:00:00Z", +@@ -275,6 +277,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() { + assert_eq!(summary.workflow.edge_count, 0); + assert_eq!(summary.goal, "ship it"); + assert_eq!(summary.title, "ship it"); ++ assert_eq!(summary.created_by, test_principal()); + assert_eq!(summary.labels, HashMap::new()); + assert_eq!(summary.source_directory, None); + assert_eq!( +diff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml +index 266f41244..4850b26fc 100644 +--- a/lib/crates/fabro-cli/Cargo.toml ++++ b/lib/crates/fabro-cli/Cargo.toml +@@ -128,6 +128,7 @@ temp-env = "0.3" + httpmock = "0.8" + fabro-test = { workspace = true } + fabro-macros = { path = "../fabro-macros" } ++fabro-types = { path = "../fabro-types", features = ["clap", "test-support"] } + hkdf.workspace = true + reqwest = { workspace = true, features = ["cookies"] } + tokio = { workspace = true, features = ["test-util", "macros"] } +diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs +index 77c5c113a..5e49adf42 100644 +--- a/lib/crates/fabro-cli/src/commands/run/attach.rs ++++ b/lib/crates/fabro-cli/src/commands/run/attach.rs +@@ -841,7 +841,7 @@ mod tests { + automation: None, + source_directory: None, + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: fabro_types::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 ec0d191e3..1934d8c2a 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +@@ -221,7 +221,18 @@ fn inspect_resolves_selector_via_server_endpoint() { + "attrs": {} + }, + "workflow_slug": "remote-workflow", +- "source_directory": "/srv/repo" ++ "source_directory": "/srv/repo", ++ "provenance": { ++ "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 275ff9437..4625cb81c 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs +@@ -177,6 +177,7 @@ pub(crate) fn remote_run_summary_json( + "origin_url": null, + "provider": "unknown" + }, ++ "created_by": fabro_types::test_support::test_principal(), + "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 80b65e4aa..0e3122606 100644 +--- a/lib/crates/fabro-cli/tests/it/support/mod.rs ++++ b/lib/crates/fabro-cli/tests/it/support/mod.rs +@@ -49,7 +49,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s + automation: None, + source_directory: Some("/srv/repo".to_string()), + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: fabro_types::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 397d55a3f..b4da2215c 100644 +--- a/lib/crates/fabro-dump/src/lib.rs ++++ b/lib/crates/fabro-dump/src/lib.rs +@@ -476,6 +476,7 @@ mod tests { + Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunSandboxInstance, + RunSandboxPlan, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage, + StageOutcome, StartRecord, SuccessReason, WorkflowSettings, first_event_seq, fixtures, ++ test_support, + }; + use futures::executor; + +@@ -498,7 +499,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 987c2631b..3a9d6e055 100644 +--- a/lib/crates/fabro-server/src/auth/cli_flow.rs ++++ b/lib/crates/fabro-server/src/auth/cli_flow.rs +@@ -1671,11 +1671,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] +@@ -2080,7 +2080,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, +@@ -2273,8 +2273,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 d189b2a21..2825aa348 100644 +--- a/lib/crates/fabro-server/src/demo/mod.rs ++++ b/lib/crates/fabro-server/src/demo/mod.rs +@@ -1081,7 +1081,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::*; +@@ -1092,13 +1092,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("static demo identity should be valid"), ++ "demo".to_string(), ++ AuthMethod::DevToken, ++ ) ++ }); ++ + fn labels(entries: &[(&str, &str)]) -> HashMap { + entries + .iter() +@@ -1171,7 +1180,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 f9e8c385c..2012b74c7 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, +@@ -76,7 +76,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, +@@ -87,7 +87,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, +@@ -98,7 +98,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, +@@ -125,7 +125,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, +@@ -148,7 +148,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, + } +@@ -172,22 +172,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, + } + } + +@@ -402,7 +403,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)), + } + } +@@ -412,7 +413,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, +@@ -428,11 +429,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)), + } + } +@@ -443,9 +444,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)), + } + } +@@ -453,8 +454,8 @@ fn require_worker_or_user_for_run( + fn require_worker_for_run(slot: &AuthContextSlot, route_run_id: &RunId) -> Result<(), ApiError> { + let context = slot.0.lock().expect("auth context lock poisoned"); + match &context.principal { +- Principal::Worker { run_id } if run_id == route_run_id => Ok(()), +- Principal::Worker { .. } | Principal::User(_) => Err(ApiError::forbidden()), ++ Some(Principal::Worker { run_id }) if run_id == route_run_id => Ok(()), ++ Some(Principal::Worker { .. } | Principal::User(_)) => Err(ApiError::forbidden()), + _ => Err(auth_rejection(context.auth_status, context.auth_error_code)), + } + } +@@ -465,13 +466,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)), + } + } +@@ -687,7 +688,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()); + } + +@@ -727,7 +728,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()); + } + +@@ -746,7 +747,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()); + } + +@@ -825,7 +826,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 1f358821b..e49567162 100644 +--- a/lib/crates/fabro-server/src/run_files.rs ++++ b/lib/crates/fabro-server/src/run_files.rs +@@ -2389,7 +2389,7 @@ index 1111111..2222222 160000 + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: fabro_types::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 4feb22133..f5a09ab61 100644 +--- a/lib/crates/fabro-server/src/run_manifest.rs ++++ b/lib/crates/fabro-server/src/run_manifest.rs +@@ -27,7 +27,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; +@@ -192,6 +194,7 @@ pub(crate) fn validate_prepared_manifest( + + pub(crate) fn create_run_input( + prepared: PreparedManifest, ++ provenance: RunProvenance, + configured_providers: Vec, + web_url: Option, + ) -> CreateRunInput { +@@ -209,7 +212,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 7bff4968f..4710c40ca 100644 +--- a/lib/crates/fabro-server/src/server.rs ++++ b/lib/crates/fabro-server/src/server.rs +@@ -1876,7 +1876,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 { +@@ -1914,27 +1917,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 180c04bdd..ff2fabcdc 100644 +--- a/lib/crates/fabro-server/src/server/handler/events.rs ++++ b/lib/crates/fabro-server/src/server/handler/events.rs +@@ -570,7 +570,7 @@ mod stage_events_tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 06beb7c0b..5df7e72c5 100644 +--- a/lib/crates/fabro-server/src/server/handler/lifecycle.rs ++++ b/lib/crates/fabro-server/src/server/handler/lifecycle.rs +@@ -894,7 +894,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 7b673fad8..73bf6f374 100644 +--- a/lib/crates/fabro-server/src/server/handler/pair.rs ++++ b/lib/crates/fabro-server/src/server/handler/pair.rs +@@ -1024,7 +1024,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 fca9fdc3c..41fdc4070 100644 +--- a/lib/crates/fabro-server/src/server/handler/runs.rs ++++ b/lib/crates/fabro-server/src/server/handler/runs.rs +@@ -687,13 +687,14 @@ pub(crate) async fn create_run_from_manifest( + .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(), ++ provenance, + ready_provider_ids.clone(), + web_url.clone(), + ); + create_input.run_id = Some(run_id); +- create_input.provenance = Some(run_provenance(&headers, &actor)); + create_input.submitted_manifest_bytes = Some(submitted_manifest_bytes); + create_input.automation = automation; + +@@ -864,7 +865,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 61d8b7ee9..d3e77d7da 100644 +--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs ++++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs +@@ -1299,6 +1299,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::test_support::test_run_provenance; + use fabro_types::{Graph, RunId, WorkflowSettings}; + use serde_json::{Value, json}; + use tower::ServiceExt; +@@ -1339,6 +1340,7 @@ mod retrieve_sandbox_tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/test", ++ "provenance": 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 bad48c6d4..cd8c07579 100644 +--- a/lib/crates/fabro-server/src/server/handler/sessions.rs ++++ b/lib/crates/fabro-server/src/server/handler/sessions.rs +@@ -1700,7 +1700,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: fabro_types::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 363884738..418ffc098 100644 +--- a/lib/crates/fabro-server/src/server/tests.rs ++++ b/lib/crates/fabro-server/src/server/tests.rs +@@ -4022,7 +4022,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -4076,7 +4076,7 @@ async fn create_slack_notification_run( + workflow_slug: workflow_slug.map(str::to_string), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -5083,7 +5083,7 @@ async fn list_run_stages_distinguishes_visits() { + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -6140,7 +6140,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: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -9943,15 +9943,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 +@@ -12310,7 +12305,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -13062,7 +13057,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() { + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 033e46db6..e231599b2 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 a3bf7ad76..01d762a73 100644 +--- a/lib/crates/fabro-server/tests/it/api/run_files.rs ++++ b/lib/crates/fabro-server/tests/it/api/run_files.rs +@@ -69,7 +69,7 @@ async fn append_completed_run_with_final_patch( + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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..c81bca853 100644 +--- a/lib/crates/fabro-store/Cargo.toml ++++ b/lib/crates/fabro-store/Cargo.toml +@@ -32,6 +32,7 @@ futures.workspace = true + uuid.workspace = true + + [dev-dependencies] ++fabro-types = { path = "../fabro-types", features = ["test-support"] } + tokio = { workspace = true, features = ["test-util", "macros"] } + tempfile = "3" + ulid.workspace = true +diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs +index cd6fa0640..d525906bc 100644 +--- a/lib/crates/fabro-store/src/run_state.rs ++++ b/lib/crates/fabro-store/src/run_state.rs +@@ -922,11 +922,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); +@@ -1276,7 +1272,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; + +@@ -1358,7 +1354,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -1644,6 +1640,7 @@ mod tests { + properties: &serde_json::Value, + node_id: Option<&str>, + ) -> EventEnvelope { ++ let properties = run_created_properties(event, properties); + EventEnvelope { + seq, + event: RunEvent::from_value(json!({ +@@ -1665,6 +1662,7 @@ mod tests { + properties: &serde_json::Value, + node_id: Option<&str>, + ) -> EventEnvelope { ++ let properties = run_created_properties(event, properties); + EventEnvelope { + seq, + event: RunEvent::from_value(json!({ +@@ -1679,6 +1677,19 @@ mod tests { + } + } + ++ fn run_created_properties(event: &str, properties: &serde_json::Value) -> serde_json::Value { ++ let mut properties = properties.clone(); ++ if event == "run.created" && properties.get("provenance").is_none() { ++ if let Some(object) = properties.as_object_mut() { ++ object.insert( ++ "provenance".to_string(), ++ serde_json::to_value(test_support::test_run_provenance()).unwrap(), ++ ); ++ } ++ } ++ properties ++ } ++ + #[test] + fn live_run_timing_returns_none_before_run_starts() { + let state = initialized_projection(); +@@ -1822,7 +1833,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, +@@ -2851,7 +2862,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, +@@ -2877,7 +2888,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, +@@ -3016,6 +3027,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 c6406fd8d..568f07fac 100644 +--- a/lib/crates/fabro-store/src/slate/mod.rs ++++ b/lib/crates/fabro-store/src/slate/mod.rs +@@ -470,6 +470,7 @@ fn active_run_from( + #[cfg(test)] + mod tests { + use chrono::{DateTime, Utc}; ++ use fabro_types::test_support::test_run_provenance; + use fabro_types::{ + AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId, + SuccessReason, WorkflowSettings, +@@ -542,7 +543,7 @@ mod tests { + automation: None, + source_directory: Some(format!("/tmp/{label}")), + labels: std::collections::HashMap::from([("team".to_string(), "infra".to_string())]), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(fabro_types::GitContext { +@@ -601,6 +602,7 @@ mod tests { + "run_dir": format!("/tmp/{label}"), + "git": run_spec.git, + "labels": run_spec.labels, ++ "provenance": run_spec.provenance, + }), + )) + .await +@@ -626,6 +628,7 @@ mod tests { + "run_dir": format!("/tmp/{label}"), + "git": run_spec.git, + "labels": run_spec.labels, ++ "provenance": run_spec.provenance, + "parent_id": parent_id, + }), + )) +@@ -1300,6 +1303,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..013e8aba2 100644 +--- a/lib/crates/fabro-store/src/slate/run_store.rs ++++ b/lib/crates/fabro-store/src/slate/run_store.rs +@@ -667,6 +667,7 @@ mod tests { + use std::sync::Arc; + use std::time::Duration; + ++ use fabro_types::test_support::test_run_provenance; + use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings}; + use object_store::memory::InMemory; + use serde_json::json; +@@ -723,6 +724,7 @@ mod tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/test", ++ "provenance": 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 6584a36cd..ffc7fbb03 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, RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime, + RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage, StageOutcome, StartRecord, +- WorkflowSettings, first_event_seq, fixtures, ++ WorkflowSettings, first_event_seq, fixtures, test_support, + }; + use serde_json::json; + +@@ -22,7 +22,7 @@ fn sample_run_spec() -> RunSpec { + automation: None, + 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..1595dc28f 100644 +--- a/lib/crates/fabro-tool/src/common.rs ++++ b/lib/crates/fabro-tool/src/common.rs +@@ -307,6 +307,7 @@ fn format_tool_error(err: &anyhow::Error) -> String { + #[cfg(test)] + mod tests { + use chrono::{TimeZone, Utc}; ++ use fabro_types::test_support::test_principal; + use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef}; + + use super::*; +@@ -413,7 +414,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: 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..8988d1a4e 100644 +--- a/lib/crates/fabro-tool/src/create.rs ++++ b/lib/crates/fabro-tool/src/create.rs +@@ -506,6 +506,7 @@ mod tests { + use async_trait::async_trait; + use chrono::{TimeZone, Utc}; + use fabro_api::types; ++ use fabro_types::test_support::test_principal; + use fabro_types::{ + EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus, + RunTimestamps, WorkflowRef, +@@ -902,7 +903,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: 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..9f7957d15 100644 +--- a/lib/crates/fabro-tool/src/interact.rs ++++ b/lib/crates/fabro-tool/src/interact.rs +@@ -451,6 +451,7 @@ mod tests { + + use async_trait::async_trait; + use chrono::{TimeZone, Utc}; ++ use fabro_types::test_support::test_principal; + use fabro_types::{ + EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection, + RunStatus, RunTimestamps, WorkflowRef, +@@ -690,7 +691,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: 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..379806d07 100644 +--- a/lib/crates/fabro-tool/src/search.rs ++++ b/lib/crates/fabro-tool/src/search.rs +@@ -293,6 +293,7 @@ mod tests { + use std::collections::HashMap; + + use chrono::{TimeZone, Utc}; ++ use fabro_types::test_support::test_principal; + use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef}; + + use super::*; +@@ -444,7 +445,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: 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 f1a8c8b39..cf2605429 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 2b27d0a06..2f1671fe6 100644 +--- a/lib/crates/fabro-types/src/run.rs ++++ b/lib/crates/fabro-types/src/run.rs +@@ -24,14 +24,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)] +@@ -93,8 +92,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 52cbb83a9..a5acb6795 100644 +--- a/lib/crates/fabro-types/src/run_event/mod.rs ++++ b/lib/crates/fabro-types/src/run_event/mod.rs +@@ -931,6 +931,7 @@ mod tests { + use serde_json::json; + + use super::*; ++ use crate::test_support::test_run_provenance; + use crate::{ + AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings, + fixtures, +@@ -1017,7 +1018,8 @@ mod tests { + "graph": graph, + "labels": {}, + "run_dir": "/tmp/run", +- "source_directory": "/tmp/run" ++ "source_directory": "/tmp/run", ++ "provenance": test_run_provenance() + } + }); + +@@ -1038,6 +1040,7 @@ mod tests { + "labels": {}, + "run_dir": "/tmp/run", + "source_directory": "/tmp/run", ++ "provenance": 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 e3023997a..06077171d 100644 +--- a/lib/crates/fabro-types/src/run_event/run.rs ++++ b/lib/crates/fabro-types/src/run_event/run.rs +@@ -30,8 +30,7 @@ pub struct RunCreatedProps { + pub automation: 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 4b52dcc96..47dffdcf7 100644 +--- a/lib/crates/fabro-types/src/run_projection.rs ++++ b/lib/crates/fabro-types/src/run_projection.rs +@@ -681,6 +681,7 @@ mod title_tests { + + use chrono::Utc; + ++ use crate::test_support::test_run_provenance; + use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings}; + + fn projection_with_goal(goal: Option<&str>) -> RunProjection { +@@ -700,7 +701,7 @@ mod title_tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -752,6 +753,7 @@ mod iter_stages_tests { + use serde_json::json; + + use super::RunProjection; ++ use crate::test_support::test_run_provenance; + use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings}; + + fn seq(n: u32) -> NonZeroU32 { +@@ -770,7 +772,7 @@ mod iter_stages_tests { + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: 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 fd35dc2a4..3c4321052 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..994813974 +--- /dev/null ++++ b/lib/crates/fabro-types/src/test_support.rs +@@ -0,0 +1,19 @@ ++use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance}; ++ ++#[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: None, ++ 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 49af80c50..687bc57ff 100644 +--- a/lib/crates/fabro-types/tests/run_event_serde.rs ++++ b/lib/crates/fabro-types/tests/run_event_serde.rs +@@ -6,7 +6,7 @@ use fabro_types::run_event::run::{RunCreatedProps, RunParentLinkedProps, RunPare + use fabro_types::run_event::{RunSessionTurnFailedCode, RunSessionTurnFailedProps}; + use fabro_types::settings::InterpString; + use fabro_types::settings::run::RunGoal; +-use fabro_types::{AutomationRef, EventBody, TurnId, WorkflowSettings, fixtures}; ++use fabro_types::{AutomationRef, EventBody, TurnId, WorkflowSettings, fixtures, test_support}; + + fn templated_settings() -> WorkflowSettings { + let mut settings = WorkflowSettings::default(); +@@ -32,7 +32,7 @@ fn run_created_props_round_trip_templated_settings() { + trigger_id: Some("schedule_1".to_string()), + }), + db_prefix: Some("run_".to_string()), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: Some(GitContext { + origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), +@@ -97,7 +97,7 @@ fn run_created_props_omits_web_url_when_absent() { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -127,22 +127,6 @@ fn run_created_props_omits_web_url_when_absent() { + assert_eq!(round_trip.retried_from, None); + } + +-#[test] +-fn run_created_props_defaults_additive_fields_for_legacy_events() { +- let json = serde_json::json!({ +- "title": null, +- "settings": WorkflowSettings::default(), +- "graph": Graph::new("ship"), +- "labels": {}, +- "run_dir": "/tmp/run" +- }); +- +- let props: RunCreatedProps = +- serde_json::from_value(json).expect("legacy props should deserialize"); +- assert_eq!(props.retried_from, None); +- assert_eq!(props.automation, None); +-} +- + #[test] + fn run_parent_events_round_trip_parent_ids() { + let linked = EventBody::RunParentLinked(RunParentLinkedProps { +diff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs +index b05dca05e..6eb9e105c 100644 +--- a/lib/crates/fabro-types/tests/run_spec_methods.rs ++++ b/lib/crates/fabro-types/tests/run_spec_methods.rs +@@ -3,7 +3,7 @@ use std::collections::HashMap; + use fabro_types::graph::Graph; + use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec}; + use fabro_types::settings::{ProjectNamespace, WorkflowNamespace}; +-use fabro_types::{WorkflowSettings, fixtures}; ++use fabro_types::{WorkflowSettings, fixtures, test_support}; + + fn sample_run_spec() -> RunSpec { + let settings = WorkflowSettings { +@@ -27,7 +27,7 @@ fn sample_run_spec() -> RunSpec { + automation: None, + source_directory: Some("/Users/client/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(GitContext { +diff --git a/lib/crates/fabro-types/tests/run_spec_serde.rs b/lib/crates/fabro-types/tests/run_spec_serde.rs +index 89b09ccbd..437afad5f 100644 +--- a/lib/crates/fabro-types/tests/run_spec_serde.rs ++++ b/lib/crates/fabro-types/tests/run_spec_serde.rs +@@ -4,7 +4,7 @@ use fabro_types::graph::Graph; + use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunSpec}; + use fabro_types::settings::InterpString; + use fabro_types::settings::run::RunGoal; +-use fabro_types::{AutomationRef, WorkflowSettings, fixtures}; ++use fabro_types::{AutomationRef, WorkflowSettings, fixtures, test_support}; + + fn templated_settings() -> WorkflowSettings { + let mut settings = WorkflowSettings::default(); +@@ -27,7 +27,7 @@ fn run_spec_round_trips_templated_settings() { + }), + source_directory: Some("/Users/client/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(GitContext { +@@ -75,15 +75,16 @@ fn run_spec_round_trips_templated_settings() { + } + + #[test] +-fn run_spec_defaults_automation_for_legacy_specs() { ++fn run_spec_defaults_automation_when_field_absent() { + let json = serde_json::json!({ + "run_id": fixtures::RUN_1, + "settings": WorkflowSettings::default(), + "graph": Graph::new("ship"), +- "labels": {} ++ "labels": {}, ++ "provenance": test_support::test_run_provenance() + }); + +- let record: RunSpec = serde_json::from_value(json).expect("legacy spec should deserialize"); ++ let record: RunSpec = serde_json::from_value(json).expect("run spec should deserialize"); + + assert_eq!(record.automation, None); + } +diff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs +index d9e2867ab..73391c9bc 100644 +--- a/lib/crates/fabro-workflow/src/billing_rollup.rs ++++ b/lib/crates/fabro-workflow/src/billing_rollup.rs +@@ -353,7 +353,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::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 4c1f91228..5f0fc9959 100644 +--- a/lib/crates/fabro-workflow/src/event/convert.rs ++++ b/lib/crates/fabro-workflow/src/event/convert.rs +@@ -2339,7 +2339,7 @@ mod tests { + let provenance = RunProvenance { + server: None, + client: None, +- subject: Some(user_principal("alice")), ++ subject: user_principal("alice"), + }; + let automation = AutomationRef { + id: "nightly".to_string(), +@@ -2348,25 +2348,25 @@ mod tests { + }; + + 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, +- automation: Some(automation.clone()), +- 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, ++ automation: Some(automation.clone()), ++ 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 886868804..0b5afb1c7 100644 +--- a/lib/crates/fabro-workflow/src/event/events.rs ++++ b/lib/crates/fabro-workflow/src/event/events.rs +@@ -41,8 +41,7 @@ pub enum Event { + automation: 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 7b65fc99c..4acc5265a 100644 +--- a/lib/crates/fabro-workflow/src/event/sink.rs ++++ b/lib/crates/fabro-workflow/src/event/sink.rs +@@ -244,7 +244,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 643d49b58..0f0613fe8 100644 +--- a/lib/crates/fabro-workflow/src/git.rs ++++ b/lib/crates/fabro-workflow/src/git.rs +@@ -469,7 +469,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 3a120e8fc..3b048374b 100644 +--- a/lib/crates/fabro-workflow/src/handler/agent.rs ++++ b/lib/crates/fabro-workflow/src/handler/agent.rs +@@ -484,7 +484,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 f10ebafde..2e59a954b 100644 +--- a/lib/crates/fabro-workflow/src/handler/command.rs ++++ b/lib/crates/fabro-workflow/src/handler/command.rs +@@ -256,7 +256,7 @@ mod tests { + automation: None, + source_directory: None, + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -357,7 +357,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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..8520a0c93 100644 +--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs ++++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs +@@ -2133,7 +2133,7 @@ reasoning = false + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: fabro_types::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 b52478bac..6772db8fb 100644 +--- a/lib/crates/fabro-workflow/src/handler/parallel.rs ++++ b/lib/crates/fabro-workflow/src/handler/parallel.rs +@@ -728,7 +728,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 1c2a82267..d88c9e875 100644 +--- a/lib/crates/fabro-workflow/src/handler/prompt.rs ++++ b/lib/crates/fabro-workflow/src/handler/prompt.rs +@@ -283,7 +283,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 ccd24632d..233044930 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs +@@ -736,7 +736,7 @@ mod tests { + workflow_slug: Some("metadata".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 db4b0ccf9..2e0112a61 100644 +--- a/lib/crates/fabro-workflow/src/operations/archive.rs ++++ b/lib/crates/fabro-workflow/src/operations/archive.rs +@@ -226,7 +226,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 8bf16d115..f68c3545b 100644 +--- a/lib/crates/fabro-workflow/src/operations/create.rs ++++ b/lib/crates/fabro-workflow/src/operations/create.rs +@@ -45,7 +45,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 +@@ -72,7 +72,7 @@ struct PersistCreateOptions { + automation: Option, + git: Option, + fork_source_ref: Option, +- provenance: Option, ++ provenance: RunProvenance, + configured_providers: Vec, + catalog: Arc, + } +@@ -1115,7 +1115,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1183,7 +1183,7 @@ mod tests { + }), + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1295,7 +1295,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1341,7 +1341,7 @@ mod tests { + }), + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1414,7 +1414,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1467,7 +1467,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(), + }), +@@ -1476,12 +1476,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, + }, +@@ -1494,7 +1494,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!( +@@ -1502,7 +1502,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 2bf7e144f..006f960fa 100644 +--- a/lib/crates/fabro-workflow/src/operations/fork.rs ++++ b/lib/crates/fabro-workflow/src/operations/fork.rs +@@ -383,7 +383,7 @@ mod tests { + workflow_slug: Some("fork-source".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 be7793d3e..86dff030b 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, + } + +@@ -152,7 +152,7 @@ mod tests { + version: "test".to_string(), + }), + client: None, +- subject: Some(actor(login)), ++ subject: actor(login), + } + } + +@@ -191,7 +191,7 @@ mod tests { + workflow_slug: Some("retry-source".to_string()), + automation: None, + db_prefix: None, +- provenance: Some(provenance("source-user")), ++ provenance: provenance("source-user"), + manifest_blob, + git: Some(git_context()), + fork_source_ref, +@@ -368,7 +368,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 +@@ -402,14 +402,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") +@@ -463,7 +456,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: None, + }) + .await +@@ -517,7 +510,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 +@@ -535,7 +528,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 8083350be..3ac123a9a 100644 +--- a/lib/crates/fabro-workflow/src/operations/start.rs ++++ b/lib/crates/fabro-workflow/src/operations/start.rs +@@ -1438,7 +1438,7 @@ reasoning = false + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1860,7 +1860,7 @@ reasoning = false + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::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 68e0ee959..bfe0da88d 100644 +--- a/lib/crates/fabro-workflow/src/operations/timeline.rs ++++ b/lib/crates/fabro-workflow/src/operations/timeline.rs +@@ -248,7 +248,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::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 d05398556..0deb440e5 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +@@ -165,7 +165,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: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -208,7 +208,7 @@ async fn seed_created_and_starting( + workflow_slug: run_options.workflow_slug.clone(), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 c677c1007..3b6344422 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs +@@ -739,7 +739,7 @@ mod tests { + workflow_slug: Some("metadata".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -856,7 +856,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::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 cc1b0777a..b2894e2b6 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs +@@ -773,7 +773,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::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 7a8c896e4..64ef56a2e 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs +@@ -148,7 +148,7 @@ mod tests { + ("env".to_string(), "test".to_string()), + ("team".to_string(), "workflow".to_string()), + ]), +- provenance: None, ++ provenance: fabro_types::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 c02376988..c620e3685 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +@@ -823,7 +823,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -1148,7 +1148,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1219,7 +1219,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1575,7 +1575,7 @@ mod tests { + source_directory: Some(tmp.path().display().to_string()), + git: None, + labels: std::collections::HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1704,7 +1704,7 @@ mod tests { + source_directory: Some("/tmp/project".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1722,7 +1722,7 @@ mod tests { + workflow_slug: run_spec.workflow_slug.clone(), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: run_spec.provenance.clone(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -1875,7 +1875,7 @@ mod tests { + source_directory: None, + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1893,7 +1893,7 @@ mod tests { + workflow_slug: None, + automation: 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 e7b0badfc..e62e17fbe 100644 +--- a/lib/crates/fabro-workflow/src/run_lookup.rs ++++ b/lib/crates/fabro-workflow/src/run_lookup.rs +@@ -491,7 +491,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::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 b11c9667c..40c42a05f 100644 +--- a/lib/crates/fabro-workflow/src/run_metadata.rs ++++ b/lib/crates/fabro-workflow/src/run_metadata.rs +@@ -639,7 +639,7 @@ mod tests { + push_outcome: PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::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 0dafbfdad..da3cfb099 100644 +--- a/lib/crates/fabro-workflow/src/runtime_store.rs ++++ b/lib/crates/fabro-workflow/src/runtime_store.rs +@@ -148,7 +148,7 @@ mod tests { + source_directory: Some("/tmp/test".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -170,7 +170,7 @@ mod tests { + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::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 e72a0ee1b..3996dc406 100644 +--- a/lib/crates/fabro-workflow/src/test_support.rs ++++ b/lib/crates/fabro-workflow/src/test_support.rs +@@ -10,6 +10,7 @@ use fabro_graphviz::graph::Graph as GvGraph; + use fabro_interview::AutoApproveInterviewer; + use fabro_model::Catalog; + use fabro_store::{ArtifactStore, Database, RunProjection}; ++use fabro_types::{AuthMethod, IdpIdentity, Principal, RunProvenance}; + use object_store::local::LocalFileSystem; + + use crate::artifact_upload::ArtifactSink; +@@ -53,6 +54,18 @@ async fn execute_and_emit_terminal(initialized: InitializedState) -> Executed { + executed + } + ++fn test_run_provenance() -> RunProvenance { ++ RunProvenance { ++ server: None, ++ client: None, ++ subject: Principal::user( ++ IdpIdentity::new("fabro:test", "test-user").expect("test identity should be valid"), ++ "test".to_string(), ++ AuthMethod::DevToken, ++ ), ++ } ++} ++ + /// Construct a fully-populated `BilledModelUsage` for tests. Centralised so + /// callers don't keep rebuilding the same JSON skeleton. + #[must_use] +@@ -175,7 +188,7 @@ async fn initialized( + workflow_slug: run_options.workflow_slug.clone(), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +index 5e97116d9..7839b95b6 100644 +--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES ++++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +@@ -271,7 +271,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 87b9c189c..af8ae77e0 100644 +--- a/lib/packages/fabro-api-client/src/models/index.ts ++++ b/lib/packages/fabro-api-client/src/models/index.ts +@@ -244,7 +244,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 f1e5adab6..6abad2884 100644 +--- a/lib/packages/fabro-api-client/src/models/run-spec.ts ++++ b/lib/packages/fabro-api-client/src/models/run-spec.ts +@@ -41,7 +41,7 @@ export interface RunSpec { + 'automation'?: AutomationRef | 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 114b1e640..5fff8b8f4 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..14c991447 --- /dev/null +++ b/stages/005-implement@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Stage completed: implement", + "failure_reason": null, + "timestamp": "2026-06-08T20:34:15.135832Z" +} \ 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..0bdf22f12 --- /dev/null +++ b/stages/006-simplify_opus@1/prompt.md @@ -0,0 +1,334 @@ +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, 3.8m tokens in / 63.4k out + - Files: /home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs + + +# Simplify: Code Review and Cleanup + +Review all changed files 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_NAME} 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. Look for 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. + +### 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 +6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior +7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds) + +### 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. **Recurring no-op updates**: state/store updates inside polling loops, intervals, or event handlers that fire unconditionally — add a change-detection guard so downstream consumers aren't notified when nothing changed. Also: if a wrapper function takes an updater/reducer callback, verify it honors same-reference returns (or whatever the "no change" signal is) — otherwise callers' early-return no-ops are silently defeated +5. **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error +6. **Memory**: unbounded data structures, missing cleanup, event listener leaks +7. **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..7d3a0ffab --- /dev/null +++ b/stages/006-simplify_opus@1/response.md @@ -0,0 +1,19 @@ +All checks green. Summary of cleanup: + +**Fixed:** + +1. **Duplicate `TEST_PRINCIPAL` literal across 8 frontend test files** — extracted to `apps/fabro-web/app/lib/test-fixtures.ts` (typed as `Principal` from the API client). All 8 test files now import it instead of redeclaring. + +2. **Stale assertion + misleading test name** in `run-summary-panel.test.tsx` — removed the `expect(... "Created by"))).toBe("Ttest")` line that no longer fit the "shows unavailable copy" semantics, and dropped the leftover "optional" wording from the test title. + +3. **Leftover IIFE wrapper** in `run-table-row.tsx` — hoisted `principalDisplay(run.createdBy)` to a `const createdByDisplay = …` at the top of the component and inlined the `` directly. Removed the no-longer-needed self-invoking-function shape that existed only for the old `run.createdBy && …` guard. + +4. **`run_created_properties` compat-shim** in `fabro-store/src/run_state.rs` — removed. The eight `test_raw_event(… "run.created" …)` JSON fixtures now include `"provenance": test_support::test_run_provenance()` explicitly, so the conditional injection of provenance no longer hides untouched fixtures. + +**Skipped (false positive / out-of-scope):** + +- The duplicate local `test_run_provenance()` in `fabro-workflow/src/test_support.rs` looked redundant, but the workflow crate exposes `pub mod test_support;` *unconditionally* while `fabro_types::test_support` is feature-gated and only enabled as a dev-dependency. Replacing the local copy broke the library build, so the duplicate stays. +- Demo `LazyLock` — correct as-is. +- `Option` in `principal_middleware.rs` — intentional per the plan ("missing/invalid auth is represented as absent request principal"). + +**Verified:** `cargo check --workspace`, `cargo clippy --workspace --all-targets -D warnings`, `cargo fmt --check`, `cargo nextest run --workspace` (6500 pass), `bun run typecheck`, `bun test` (508 pass). \ No newline at end of file