From 7d96eb9895f973d533f344fd402e990f64fa36c1 Mon Sep 17 00:00:00 2001 From: Fabro Date: Wed, 3 Jun 2026 13:49:47 -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 | 551 ++- stages/005-implement@1/diff.patch | 3063 +++++++++++++++++ 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 | 28 + 6 files changed, 3966 insertions(+), 21 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 5cdcfa70f..ab3baf6e9 100644 --- a/run.json +++ b/run.json @@ -505,7 +505,7 @@ "kind": "running" }, "status_updated_at": "2026-06-03T16:15:56.545535Z", - "last_event_at": "2026-06-03T17:38:19.015190Z", + "last_event_at": "2026-06-03T17:49:47.287517Z", "pending_control": null, "checkpoints": [ { @@ -789,9 +789,9 @@ } }, { - "seq": 0, + "seq": 1371, "checkpoint": { - "timestamp": "2026-06-03T17:38:19.285342Z", + "timestamp": "2026-06-03T17:38:30.074693Z", "current_node": "implement", "completed_nodes": [ "start", @@ -802,31 +802,181 @@ ], "node_retries": {}, "context_values": { - "internal.work_dir": "/home/daytona/workspace/fabro", - "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", - "internal.retry_count.preflight_compile": 0, - "thread.preflight_compile.current_node": "preflight_lint", - "last_response": "Implemented the plan in two commits.\n\nCommits:\n- `26fdc23a9 refactor(auth): remove anonymous principal`\n- `5fea2c5ad Make run provenance and creator total`\n\nSummary:\n- Removed anonymous principal repr", - "last_stage": "implement", - "failure_signature": "", - "outcome": "succeeded", - "internal.thread_id": "preflight_lint", "internal.run_id": "01KT749TNV1MD2PYSHX1PB9905", - "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", - "failure_class": "", - "current_node": "implement", + "outcome": "succeeded", + "thread.toolchain.current_node": "preflight_compile", + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", "internal.fidelity": "compact", + "internal.retry_count.preflight_lint": 0, + "failure_signature": "", + "current_node": "implement", + "failure_class": "", + "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.thread_id": "preflight_lint", + "internal.work_dir": "/home/daytona/workspace/fabro", + "internal.node_visit_count": 1, + "thread.preflight_compile.current_node": "preflight_lint", + "thread.preflight_lint.current_node": "implement", + "thread.start.current_node": "toolchain", + "internal.retry_count.toolchain": 0, + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", "response.implement": "Implemented the plan in two commits.\n\nCommits:\n- `26fdc23a9 refactor(auth): remove anonymous principal`\n- `5fea2c5ad Make run provenance and creator total`\n\nSummary:\n- Removed anonymous principal representation from auth/API/frontend.\n- Request auth now represents absent/invalid auth as `None`.\n- HTTP logs always emit `principal_kind`, using `\"none\"` for absent principal.\n- Made run creator/provenance total across Rust types, workflow events, store projections, server create/retry flows, OpenAPI, generated TS client, frontend data/UI, demo mode, and tests.\n- Added gated `fabro-types::test_support` helpers.\n- Regenerated Rust OpenAPI types and TypeScript API client.\n- Working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\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 lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Final sweeps for anonymous principal / nullable creator-provenance remnants; only unrelated matches remained (`crossorigin=\"anonymous\"`, todo/auth subject fields).\n- Manual unauthenticated protected endpoint check:\n - `/api/v1/runs` returned `401`\n - HTTP log included `principal_kind=\"none\"` and `auth_status=\"missing\"`\n- Manual authenticated demo-mode API check:\n - `X-Fabro-Demo: 1` run list returned synthetic creator:\n - `login: \"demo\"`\n - `auth_method: \"dev_token\"`\n - `identity: { issuer: \"fabro:demo\", subject: \"demo\" }`\n\nNot run:\n- Full browser-driven UI create/retry manual flow; this terminal environment has no interactive browser/auth session.", + "last_response": "Implemented the plan in two commits.\n\nCommits:\n- `26fdc23a9 refactor(auth): remove anonymous principal`\n- `5fea2c5ad Make run provenance and creator total`\n\nSummary:\n- Removed anonymous principal repr", + "graph.rankdir": "LR", + "last_stage": "implement", + "internal.retry_count.start": 0, + "internal.retry_count.implement": 0, + "internal.retry_count.preflight_compile": 0 + }, + "node_outcomes": { + "start": { + "status": "succeeded", + "usage": null + }, + "implement": { + "status": "succeeded", + "context_updates": { + "response.implement": "Implemented the plan in two commits.\n\nCommits:\n- `26fdc23a9 refactor(auth): remove anonymous principal`\n- `5fea2c5ad Make run provenance and creator total`\n\nSummary:\n- Removed anonymous principal representation from auth/API/frontend.\n- Request auth now represents absent/invalid auth as `None`.\n- HTTP logs always emit `principal_kind`, using `\"none\"` for absent principal.\n- Made run creator/provenance total across Rust types, workflow events, store projections, server create/retry flows, OpenAPI, generated TS client, frontend data/UI, demo mode, and tests.\n- Added gated `fabro-types::test_support` helpers.\n- Regenerated Rust OpenAPI types and TypeScript API client.\n- Working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\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 lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Final sweeps for anonymous principal / nullable creator-provenance remnants; only unrelated matches remained (`crossorigin=\"anonymous\"`, todo/auth subject fields).\n- Manual unauthenticated protected endpoint check:\n - `/api/v1/runs` returned `401`\n - HTTP log included `principal_kind=\"none\"` and `auth_status=\"missing\"`\n- Manual authenticated demo-mode API check:\n - `X-Fabro-Demo: 1` run list returned synthetic creator:\n - `login: \"demo\"`\n - `auth_method: \"dev_token\"`\n - `identity: { issuer: \"fabro:demo\", subject: \"demo\" }`\n\nNot run:\n- Full browser-driven UI create/retry manual flow; this terminal environment has no interactive browser/auth session.", + "last_response": "Implemented the plan in two commits.\n\nCommits:\n- `26fdc23a9 refactor(auth): remove anonymous principal`\n- `5fea2c5ad Make run provenance and creator total`\n\nSummary:\n- Removed anonymous principal repr", + "last_stage": "implement" + }, + "notes": "Stage completed: implement", + "usage": { + "input": { + "usage": { + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "tokens": { + "input_tokens": 5739560, + "output_tokens": 57767, + "reasoning_tokens": 17492, + "cache_read_tokens": 31580672, + "cache_write_tokens": 0 + } + }, + "facts": { + "algorithm": "openai" + } + }, + "total_usd_micros": 46745906 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs" + ], + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 2484520, + "tool_time_ms": 1823286, + "active_time_ms": 4307806 + } + }, + "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": 148390, + "active_time_ms": 148390 + } + }, + "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": 1368, + "active_time_ms": 1368 + } + }, + "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": 127905, + "active_time_ms": 127905 + } + } + }, + "next_node_id": "simplify_opus", + "git_commit_sha": "68c319282ea49517b7bba00d03b2020519e7394d", + "node_visits": { + "implement": 1, + "toolchain": 1, + "preflight_lint": 1, + "preflight_compile": 1, + "start": 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..476224728 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.test.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.test.tsx\n@@ -6,6 +6,7 @@ import {\n RunSummaryPanelView,\n type RunSummaryPanelViewProps,\n } from \"./run-summary-panel\";\n+import { TEST_PRINCIPAL } from \"../lib/test-fixtures\";\n \n function instanceText(instance: TestRenderer.ReactTestInstance): string {\n const parts: string[] = [];\n@@ -53,7 +54,7 @@ function cellAfterLabel(\n function makeRun(overrides: Record = {}) {\n return {\n id: \"run_1\",\n- created_by: null,\n+ created_by: TEST_PRINCIPAL,\n diff: null,\n billing: null,\n ...overrides,\n@@ -73,7 +74,7 @@ describe(\"RunSummaryPanelView\", () => {\n \n test(\"shows unavailable copy for missing 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@@ -252,7 +253,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..aa7ddd87b 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);\n@@ -133,7 +133,7 @@ export function RunSummaryPanelView({\n {created.label}\n \n ) : (\n- \n+ \n )}\n \n \ndiff --git a/apps/fabro-web/app/components/runs-list/run-table-row.tsx b/apps/fabro-web/app/components/runs-list/run-table-row.tsx\nindex 4fdb82777..8fc722bd6 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@@ -36,6 +36,7 @@ export function RunTableRow({\n const lifecycleLabel = listLifecycleStatusLabel(run);\n const statusDisplay = columnStatusDisplay[run.status];\n const show = (col: ToggleableColumn) => !hiddenColumns.has(col);\n+ const creatorDisplay = principalDisplay(run.createdBy);\n \n return (\n \n@@ -54,14 +55,9 @@ export function RunTableRow({\n \n {show(\"created_by\") && (\n \n- {run.createdBy && (() => {\n- const display = principalDisplay(run.createdBy);\n- return (\n- \n- {display.glyph}\n- \n- );\n- })()}\n+ \n+ {creatorDisplay.glyph}\n+ \n \n )}\n {show(\"repo\") && (\ndiff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts\nindex 98586b2c4..feb2ba568 100644\n--- a/apps/fabro-web/app/data/runs.test.ts\n+++ b/apps/fabro-web/app/data/runs.test.ts\n@@ -8,6 +8,7 @@ import {\n mapRunToRunItem,\n runStatusDisplay,\n } from \"./runs\";\n+import { TEST_PRINCIPAL } from \"../lib/test-fixtures\";\n \n function makeRun(overrides: Partial = {}): Run {\n return {\n@@ -17,7 +18,7 @@ function makeRun(overrides: Partial = {}): Run {\n workflow: { slug: \"fix_build\", name: \"Fix Build\", graph_name: \"FixBuild\", node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"myrepo\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: 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..d88fdd3af 100644\n--- a/apps/fabro-web/app/lib/run-actions.test.ts\n+++ b/apps/fabro-web/app/lib/run-actions.test.ts\n@@ -24,6 +24,7 @@ import {\n unarchiveRuns,\n } from \"./run-actions\";\n import { generatedAxios } from \"./api-client\";\n+import { TEST_PRINCIPAL } from \"./test-fixtures\";\n \n type StubResponseInit = {\n status: number;\n@@ -47,7 +48,7 @@ function makeRun(status: RunStatus, archived = false): Run {\n workflow: { slug: \"fix_build\", name: \"Fix Build\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: null,\n- created_by: null,\n+ created_by: TEST_PRINCIPAL,\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/lib/test-fixtures.ts b/apps/fabro-web/app/lib/test-fixtures.ts\nnew file mode 100644\nindex 000000000..d05afc2a0\n--- /dev/null\n+++ b/apps/fabro-web/app/lib/test-fixtures.ts\n@@ -0,0 +1,8 @@\n+import type { Principal } from \"@qltysh/fabro-api-client\";\n+\n+export const TEST_PRINCIPAL = {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+} satisfies Principal;\ndiff --git a/apps/fabro-web/app/routes/automations-new.test.tsx b/apps/fabro-web/app/routes/automations-new.test.tsx\nindex 47f471abf..32a88dccc 100644\n--- a/apps/fabro-web/app/routes/automations-new.test.tsx\n+++ b/apps/fabro-web/app/routes/automations-new.test.tsx\n@@ -4,6 +4,7 @@ import TestRenderer, { act } from \"react-test-renderer\";\n import { createMemoryRouter, RouterProvider } from \"react-router\";\n \n import { ToastProvider } from \"../components/toast\";\n+import { TEST_PRINCIPAL } from \"../lib/test-fixtures\";\n import { setupReactTestEnv } from \"../lib/test-utils\";\n \n let currentRun: any = null;\n@@ -120,7 +121,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..a850cc0b5 100644\n--- a/apps/fabro-web/app/routes/run-detail.test.ts\n+++ b/apps/fabro-web/app/routes/run-detail.test.ts\n@@ -13,6 +13,7 @@ import {\n \n import { ToastProvider } from \"../components/toast\";\n import { DemoModeProvider } from \"../lib/demo-mode\";\n+import { TEST_PRINCIPAL } from \"../lib/test-fixtures\";\n \n let currentRunSummary: any = null;\n let currentRunState: any = null;\n@@ -221,7 +222,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..40a51614e 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@@ -5,6 +5,7 @@ import { MemoryRouter, Route, Routes } from \"react-router\";\n import { toast as sonnerToast } from \"sonner\";\n \n import { ToastProvider } from \"../components/toast\";\n+import { TEST_PRINCIPAL } from \"../lib/test-fixtures\";\n \n let currentFilesPayload: any = null;\n let currentCommitsPayload: any = null;\n@@ -51,7 +52,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..01eda20fc 100644\n--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx\n@@ -5,6 +5,7 @@ import type { PaginatedRunList, Run } from \"@qltysh/fabro-api-client\";\n \n import { ToastProvider } from \"../components/toast\";\n import { CHILD_RUNS_LIST_PREFERENCES_STORAGE_KEY } from \"../components/runs-list/preferences\";\n+import { TEST_PRINCIPAL } from \"../lib/test-fixtures\";\n import { setupReactTestEnv } from \"../lib/test-utils\";\n \n class MemoryStorage {\n@@ -35,7 +36,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..babef285b 100644\n--- a/apps/fabro-web/app/routes/runs.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.test.tsx\n@@ -11,6 +11,7 @@ import {\n shouldRefreshBoardForEvent,\n } from \"./runs\";\n import { summarizeBatchLifecycleAction } from \"../components/runs-list/batch-lifecycle\";\n+import { TEST_PRINCIPAL } from \"../lib/test-fixtures\";\n \n function boardRun(id: string, column: BoardColumn, questionText?: string): Run {\n const status =\n@@ -34,7 +35,7 @@ function boardRun(id: string, column: BoardColumn, questionText?: string): Run {\n workflow: { slug: \"test\", name: \"Test\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"repo\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: 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..22d0b66d1 100644\n--- a/docs/internal/logging-strategy.md\n+++ b/docs/internal/logging-strategy.md\n@@ -118,11 +118,11 @@ Fields are key-value pairs that make events queryable. Include enough context th\n | `error` | Error value on failure |\n | `path` | File system path |\n | `duration_ms` | Elapsed time in milliseconds |\n-| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) |\n+| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) |\n | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) |\n | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests |\n \n-For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`).\n+For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`). Requests without a principal use `principal_kind=\"none\"`; `auth_status` distinguishes missing, invalid, expired, and authenticated auth state.\n \n Server auth intentionally exposes a mutable `RequestAuth` context slot for public auth routes and guard extractors such as `RequiredUser` / `RequireRunScoped` for protected routes. There is no loose `RequestPrincipal` extractor; route-facing extractors should enforce the route's auth contract while the slot supplies the final HTTP log fields.\n | `input_tokens` | Token count for LLM input |\ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex 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..298dad17c 100644\n--- a/lib/crates/fabro-api/Cargo.toml\n+++ b/lib/crates/fabro-api/Cargo.toml\n@@ -34,3 +34,6 @@ serde_json = \"1\"\n serde_yaml = \"0.9\"\n prettyplease = \"0.2\"\n syn = \"2\"\n+\n+[dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\ndiff --git a/lib/crates/fabro-api/tests/principal_round_trip.rs b/lib/crates/fabro-api/tests/principal_round_trip.rs\nindex ca1180e60..ac2b2c258 100644\n--- a/lib/crates/fabro-api/tests/principal_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/principal_round_trip.rs\n@@ -118,7 +118,6 @@ fn principal_round_trips_every_variant_through_api_type() {\n Principal::System {\n system_kind: SystemActorKind::Watchdog,\n },\n- Principal::Anonymous,\n ];\n \n for principal in variants {\n@@ -140,9 +139,9 @@ fn run_provenance_subject_round_trips_as_principal() {\n name: Some(\"fabro-cli\".to_string()),\n version: Some(\"0.1.0\".to_string()),\n }),\n- subject: Some(Principal::Worker {\n+ subject: Principal::Worker {\n run_id: fixtures::RUN_1,\n- }),\n+ },\n };\n let json = serde_json::to_value(&provenance).unwrap();\n \ndiff --git a/lib/crates/fabro-api/tests/run_event_round_trip.rs b/lib/crates/fabro-api/tests/run_event_round_trip.rs\nindex dc2c9297f..18611c2e4 100644\n--- a/lib/crates/fabro-api/tests/run_event_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs\n@@ -1,7 +1,9 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunEvent as ApiRunEvent;\n-use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures};\n+use fabro_types::{\n+ Graph, RunEvent, WorkflowSettings, fixtures, test_support as types_test_support,\n+};\n use serde_json::{Value, json};\n \n #[test]\n@@ -20,7 +22,8 @@ fn run_event_round_trips_run_created() {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/fabro/run-1\",\n- \"source_directory\": \"/tmp/fabro/run-1\"\n+ \"source_directory\": \"/tmp/fabro/run-1\",\n+ \"provenance\": types_test_support::test_run_provenance()\n }\n });\n \n@@ -39,6 +42,7 @@ fn run_event_round_trips_run_created_with_web_url() {\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/fabro/run-1\",\n \"source_directory\": \"/tmp/fabro/run-1\",\n+ \"provenance\": types_test_support::test_run_provenance(),\n \"web_url\": format!(\"http://localhost:3000/runs/{}\", fixtures::RUN_1)\n }\n });\ndiff --git a/lib/crates/fabro-api/tests/run_projection_round_trip.rs b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\nindex 9a2a0f310..a4e7d6622 100644\n--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n@@ -1,7 +1,9 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunProjection as ApiRunProjection;\n-use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings};\n+use fabro_types::{\n+ Graph, RunProjection, RunSpec, WorkflowSettings, test_support as types_test_support,\n+};\n use serde_json::json;\n \n #[test]\n@@ -137,7 +139,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: 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..4212cb4b4 100644\n--- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs\n@@ -12,7 +12,7 @@ use fabro_types::{\n AskFabro, AskFabroUnavailableReason, AutomationRef, DiffSummary, PullRequestLink,\n RepositoryProvider, RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary,\n RunId, RunLifecycle, RunLinks, RunOrigin, RunRunnableSource, RunSize, RunTimestamps, RunTiming,\n- WorkflowRef, fixtures,\n+ WorkflowRef, fixtures, test_support as types_test_support,\n };\n use serde_json::json;\n \n@@ -88,7 +88,7 @@ fn run_summary_json_matches_openapi_shape() {\n origin_url: None,\n provider: RepositoryProvider::Unknown,\n }),\n- created_by: None,\n+ created_by: types_test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"team\".to_string(), \"core\".to_string())]),\n lifecycle: RunLifecycle {\n@@ -161,7 +161,7 @@ fn run_summary_json_matches_openapi_shape() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n- \"created_by\": null,\n+ \"created_by\": types_test_support::test_principal(),\n \"origin\": {\n \"kind\": \"api\"\n },\n@@ -253,6 +253,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": types_test_support::test_principal(),\n \"models\": [],\n \"timestamps\": {\n \"created_at\": \"2026-04-20T12:00:00Z\",\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..9f0e814d9 100644\n--- a/lib/crates/fabro-cli/src/commands/run/attach.rs\n+++ b/lib/crates/fabro-cli/src/commands/run/attach.rs\n@@ -822,6 +822,7 @@ mod tests {\n )]\n \n use fabro_interview::{Answer, AnswerValue};\n+ use fabro_types::test_support as types_test_support;\n use fabro_util::terminal::Styles;\n use httpmock::MockServer;\n \n@@ -841,7 +842,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: 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..8f619de4f 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs\n@@ -21,7 +21,7 @@ use fabro_config::daemon::ServerDaemon;\n use fabro_config::{Storage, envfile};\n use fabro_store::EventEnvelope;\n use fabro_test::{TestContext, expect_reqwest_status};\n-use fabro_types::{RunId, StageId};\n+use fabro_types::{RunId, StageId, test_support as types_test_support};\n use httpmock::{Mock, MockServer};\n use serde_json::Value;\n use shlex::try_quote;\n@@ -165,6 +165,7 @@ pub(crate) fn remote_run_summary_json(\n ) -> Value {\n serde_json::json!({\n \"id\": run_id,\n+ \"children_count\": 0,\n \"title\": goal,\n \"goal\": goal,\n \"workflow\": {\n@@ -172,11 +173,13 @@ pub(crate) fn remote_run_summary_json(\n \"name\": workflow_name,\n \"graph_name\": null\n },\n+ \"automation\": null,\n \"repository\": {\n \"name\": \"repo\",\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": types_test_support::test_principal(),\n \"origin\": {\n \"kind\": \"api\"\n },\n@@ -199,6 +202,9 @@ pub(crate) fn remote_run_summary_json(\n },\n \"timing\": null,\n \"billing\": null,\n+ \"ask_fabro\": {\n+ \"available\": false\n+ },\n \"diff\": null,\n \"pull_request\": null,\n \"current_question\": null,\ndiff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs\nindex 80b65e4aa..336f06d2b 100644\n--- a/lib/crates/fabro-cli/tests/it/support/mod.rs\n+++ b/lib/crates/fabro-cli/tests/it/support/mod.rs\n@@ -9,7 +9,7 @@ pub(crate) use auth_harness::{\n pub(crate) use auth_tokens::{TEST_SESSION_SECRET, issue_test_github_jwt, issue_test_worker_jwt};\n use fabro_store::EventEnvelope;\n use fabro_test::{EnvVars, TestContext, preserve_coverage_env};\n-use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings};\n+use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings, test_support as types_test_support};\n \n pub(crate) fn run_output_filters(context: &TestContext) -> Vec<(String, String)> {\n let mut filters = context.filters();\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: 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..a34f0b8a1 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 as types_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: 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-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..39a9cf4e3 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,23 @@ 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\")\n+ .expect(\"hard-coded 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 +1181,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..3b6ce4c2c 100644\n--- a/lib/crates/fabro-server/src/run_files.rs\n+++ b/lib/crates/fabro-server/src/run_files.rs\n@@ -1717,7 +1717,7 @@ fn count_flags(data: &[FileDiff]) -> (u64, u64, u64, u64) {\n mod tests {\n use std::sync::atomic::{AtomicUsize, Ordering};\n \n- use fabro_types::{CommandTermination, RunId};\n+ use fabro_types::{CommandTermination, RunId, test_support as types_test_support};\n use tokio::time::{Duration, sleep};\n \n use super::*;\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: 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..a0e9b4c39 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@@ -193,6 +195,7 @@ pub(crate) fn validate_prepared_manifest(\n pub(crate) fn create_run_input(\n prepared: PreparedManifest,\n configured_providers: Vec,\n+ provenance: RunProvenance,\n web_url: Option,\n ) -> CreateRunInput {\n CreateRunInput {\n@@ -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..5ec8d0364 100644\n--- a/lib/crates/fabro-server/src/server/handler/events.rs\n+++ b/lib/crates/fabro-server/src/server/handler/events.rs\n@@ -535,7 +535,7 @@ mod stage_events_tests {\n use axum::body::{Body, to_bytes};\n use axum::http::{Request, StatusCode, header};\n use fabro_store::EventPayload;\n- use fabro_types::{Graph, RunId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, WorkflowSettings, test_support as types_test_support};\n use fabro_workflow::event as workflow_event;\n use http_body_util::BodyExt;\n use serde_json::json;\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: 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..6a559db9a 100644\n--- a/lib/crates/fabro-server/src/server/handler/pair.rs\n+++ b/lib/crates/fabro-server/src/server/handler/pair.rs\n@@ -850,7 +850,7 @@ mod tests {\n use fabro_types::run_event::AgentMessageProps;\n use fabro_types::{\n BilledTokenCounts, EventEnvelope, Graph, PairMessageId, RunEvent, StageId,\n- WorkflowSettings, fixtures,\n+ WorkflowSettings, fixtures, test_support as types_test_support,\n };\n use fabro_workflow::event as workflow_event;\n use tower::ServiceExt;\n@@ -1024,7 +1024,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..2c5097d32 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 ready_provider_ids.clone(),\n+ provenance,\n web_url.clone(),\n );\n create_input.run_id = Some(run_id);\n- create_input.provenance = Some(run_provenance(&headers, &actor));\n create_input.submitted_manifest_bytes = Some(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..46277a401 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,7 +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::{Graph, RunId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, WorkflowSettings, test_support as types_test_support};\n use serde_json::{Value, json};\n use tower::ServiceExt;\n \n@@ -1339,6 +1339,7 @@ mod retrieve_sandbox_tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": types_test_support::test_run_provenance(),\n },\n }),\n run_id,\ndiff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/crates/fabro-server/src/server/handler/sessions.rs\nindex bad48c6d4..b45422386 100644\n--- a/lib/crates/fabro-server/src/server/handler/sessions.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sessions.rs\n@@ -1506,6 +1506,7 @@ mod tests {\n use fabro_agent::config::ToolAccess;\n use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource};\n use fabro_llm::types::{ToolCall, ToolDefinition};\n+ use fabro_types::test_support as types_test_support;\n \n use super::*;\n \n@@ -1700,7 +1701,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: 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..098d66c37 100644\n--- a/lib/crates/fabro-server/src/server/tests.rs\n+++ b/lib/crates/fabro-server/src/server/tests.rs\n@@ -29,7 +29,7 @@ use fabro_types::{\n SandboxProviderKind, StageContextWindowBreakdownItem, StageContextWindowCategory,\n StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness,\n StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind,\n- WorkflowSettings, fixtures,\n+ WorkflowSettings, fixtures, test_support as types_test_support,\n };\n use fabro_util::check_report::CheckStatus;\n use fabro_workflow::records::CheckpointExt;\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: 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: 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: 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: 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: 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: 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..43eb4cf26 100644\n--- a/lib/crates/fabro-server/tests/it/api/run_files.rs\n+++ b/lib/crates/fabro-server/tests/it/api/run_files.rs\n@@ -14,7 +14,9 @@ use axum::body::Body;\n use axum::http::{Request, StatusCode};\n use fabro_server::test_support::test_app_state_with_store;\n use fabro_store::{ArtifactStore, Database};\n-use fabro_types::{Graph, RunId, SandboxProviderKind, WorkflowSettings};\n+use fabro_types::{\n+ Graph, RunId, SandboxProviderKind, WorkflowSettings, test_support as types_test_support,\n+};\n use fabro_workflow::event as workflow_event;\n use fabro_workflow::run_status::SuccessReason;\n use object_store::memory::InMemory as MemoryObjectStore;\n@@ -69,7 +71,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: 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..6e7291561 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 as types_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: types_test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1439,7 +1435,7 @@ mod tests {\n }\n \n #[test]\n- fn legacy_run_created_projects_retried_from_none() {\n+ fn run_created_without_retry_projects_retried_from_none() {\n let event = test_raw_event(\n 1,\n \"run.created\",\n@@ -1652,7 +1648,7 @@ mod tests {\n \"run_id\": fixtures::RUN_1,\n \"event\": event,\n \"node_id\": node_id,\n- \"properties\": properties,\n+ \"properties\": test_event_properties(event, properties),\n }))\n .unwrap(),\n }\n@@ -1673,12 +1669,25 @@ mod tests {\n \"run_id\": fixtures::RUN_1,\n \"event\": event,\n \"node_id\": node_id,\n- \"properties\": properties,\n+ \"properties\": test_event_properties(event, properties),\n }))\n .unwrap(),\n }\n }\n \n+ fn test_event_properties(event: &str, properties: &serde_json::Value) -> serde_json::Value {\n+ let mut properties = properties.clone();\n+ if event == \"run.created\" {\n+ let object = properties\n+ .as_object_mut()\n+ .expect(\"run.created test properties should be a JSON object\");\n+ object\n+ .entry(\"provenance\".to_string())\n+ .or_insert_with(|| json!(types_test_support::test_run_provenance()));\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 +1831,7 @@ mod tests {\n \"repo_origin_url\": null,\n \"base_branch\": null,\n \"labels\": {},\n- \"provenance\": null,\n+ \"provenance\": types_test_support::test_run_provenance(),\n \"manifest_blob\": null,\n \"definition_blob\": null,\n \"git\": null,\n@@ -2851,7 +2860,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: types_test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2877,7 +2886,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: types_test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2930,7 +2939,7 @@ mod tests {\n }\n \n #[test]\n- fn legacy_run_created_without_title_infers_projection_title() {\n+ fn run_created_without_title_infers_projection_title_from_goal() {\n let event = test_raw_event(\n 1,\n \"run.created\",\n@@ -3016,7 +3025,8 @@ mod tests {\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\",\n- \"manifest_blob\": manifest_blob\n+ \"manifest_blob\": manifest_blob,\n+ \"provenance\": types_test_support::test_run_provenance()\n }\n }))\n .unwrap(),\ndiff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs\nindex c6406fd8d..b7d2be8d3 100644\n--- a/lib/crates/fabro-store/src/slate/mod.rs\n+++ b/lib/crates/fabro-store/src/slate/mod.rs\n@@ -472,7 +472,7 @@ mod tests {\n use chrono::{DateTime, Utc};\n use fabro_types::{\n AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId,\n- SuccessReason, WorkflowSettings,\n+ SuccessReason, WorkflowSettings, test_support as types_test_support,\n };\n use futures::TryStreamExt;\n use object_store::memory::InMemory;\n@@ -542,7 +542,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: types_test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\n@@ -601,6 +601,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@@ -627,6 +628,7 @@ mod tests {\n \"git\": run_spec.git,\n \"labels\": run_spec.labels,\n \"parent_id\": parent_id,\n+ \"provenance\": run_spec.provenance,\n }),\n ))\n .await\n@@ -1300,6 +1302,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..8ff532352 100644\n--- a/lib/crates/fabro-store/src/slate/run_store.rs\n+++ b/lib/crates/fabro-store/src/slate/run_store.rs\n@@ -667,7 +667,9 @@ mod tests {\n use std::sync::Arc;\n use std::time::Duration;\n \n- use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings};\n+ use fabro_types::{\n+ Graph, RunId, SessionId, StageId, WorkflowSettings, test_support as types_test_support,\n+ };\n use object_store::memory::InMemory;\n use serde_json::json;\n \n@@ -723,6 +725,7 @@ mod tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": types_test_support::test_run_provenance(),\n },\n }),\n run_id,\ndiff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs\nindex 6584a36cd..db597eef1 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 as types_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: types_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..2a0b19cda 100644\n--- a/lib/crates/fabro-tool/src/common.rs\n+++ b/lib/crates/fabro-tool/src/common.rs\n@@ -307,7 +307,10 @@ fn format_tool_error(err: &anyhow::Error) -> String {\n #[cfg(test)]\n mod tests {\n use chrono::{TimeZone, Utc};\n- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n+ use fabro_types::{\n+ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef,\n+ test_support as types_test_support,\n+ };\n \n use super::*;\n \n@@ -413,7 +416,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: types_test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs\nindex 8488f8f0b..0abee98f8 100644\n--- a/lib/crates/fabro-tool/src/create.rs\n+++ b/lib/crates/fabro-tool/src/create.rs\n@@ -508,7 +508,7 @@ mod tests {\n use fabro_api::types;\n use fabro_types::{\n EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus,\n- RunTimestamps, WorkflowRef,\n+ RunTimestamps, WorkflowRef, test_support as types_test_support,\n };\n use schemars::SchemaGenerator;\n use serde_json::json;\n@@ -902,7 +902,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: types_test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/interact.rs b/lib/crates/fabro-tool/src/interact.rs\nindex 34023120d..fc833ca3c 100644\n--- a/lib/crates/fabro-tool/src/interact.rs\n+++ b/lib/crates/fabro-tool/src/interact.rs\n@@ -453,7 +453,7 @@ mod tests {\n use chrono::{TimeZone, Utc};\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection,\n- RunStatus, RunTimestamps, WorkflowRef,\n+ RunStatus, RunTimestamps, WorkflowRef, test_support as types_test_support,\n };\n use serde_json::json;\n \n@@ -690,7 +690,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: types_test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs\nindex 0df7e391a..04d8b5d79 100644\n--- a/lib/crates/fabro-tool/src/search.rs\n+++ b/lib/crates/fabro-tool/src/search.rs\n@@ -293,7 +293,10 @@ mod tests {\n use std::collections::HashMap;\n \n use chrono::{TimeZone, Utc};\n- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n+ use fabro_types::{\n+ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef,\n+ test_support as types_test_support,\n+ };\n \n use super::*;\n \n@@ -444,7 +447,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: types_test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"group\".to_string(), group.to_string())]),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs\nindex 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..41c8a6546 100644\n--- a/lib/crates/fabro-types/src/run_event/mod.rs\n+++ b/lib/crates/fabro-types/src/run_event/mod.rs\n@@ -933,7 +933,7 @@ mod tests {\n use super::*;\n use crate::{\n AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings,\n- fixtures,\n+ fixtures, test_support as types_test_support,\n };\n \n fn user_principal(login: &str) -> Principal {\n@@ -1017,7 +1017,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\": types_test_support::test_run_provenance()\n }\n });\n \n@@ -1038,7 +1039,8 @@ mod tests {\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\",\n- \"manifest_blob\": RunBlobId::new(br#\"{\"version\":1}\"#).to_string()\n+ \"manifest_blob\": RunBlobId::new(br#\"{\"version\":1}\"#).to_string(),\n+ \"provenance\": types_test_support::test_run_provenance()\n }\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..40130ed9c 100644\n--- a/lib/crates/fabro-types/src/run_projection.rs\n+++ b/lib/crates/fabro-types/src/run_projection.rs\n@@ -681,7 +681,7 @@ mod title_tests {\n \n use chrono::Utc;\n \n- use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings};\n+ use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support};\n \n fn projection_with_goal(goal: Option<&str>) -> RunProjection {\n let mut graph = Graph::new(\"test\");\n@@ -700,7 +700,7 @@ mod title_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@@ -752,7 +752,7 @@ mod iter_stages_tests {\n use serde_json::json;\n \n use super::RunProjection;\n- use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings};\n+ use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings, test_support};\n \n fn seq(n: u32) -> NonZeroU32 {\n NonZeroU32::new(n).unwrap()\n@@ -770,7 +770,7 @@ mod iter_stages_tests {\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs\nindex 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..04850ba68 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,20 @@ 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::{\n+ AutomationRef, EventBody, Principal, RunProvenance, SystemActorKind, TurnId, WorkflowSettings,\n+ fixtures,\n+};\n+\n+fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::System {\n+ system_kind: SystemActorKind::Engine,\n+ },\n+ }\n+}\n \n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n@@ -32,7 +45,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_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 +110,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_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -133,6 +146,7 @@ fn run_created_props_defaults_additive_fields_for_legacy_events() {\n \"title\": null,\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"ship\"),\n+ \"provenance\": test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\"\n });\ndiff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs\nindex b05dca05e..79b05186a 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,17 @@ 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::{Principal, RunProvenance, SystemActorKind, WorkflowSettings, fixtures};\n+\n+fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::System {\n+ system_kind: SystemActorKind::Engine,\n+ },\n+ }\n+}\n \n fn sample_run_spec() -> RunSpec {\n let settings = WorkflowSettings {\n@@ -27,7 +37,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_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..afa3398ee 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,19 @@ 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::{\n+ AutomationRef, Principal, RunProvenance, SystemActorKind, WorkflowSettings, fixtures,\n+};\n+\n+fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::System {\n+ system_kind: SystemActorKind::Engine,\n+ },\n+ }\n+}\n \n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n@@ -27,7 +39,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_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\n@@ -80,7 +92,8 @@ fn run_spec_defaults_automation_for_legacy_specs() {\n \"run_id\": fixtures::RUN_1,\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"ship\"),\n- \"labels\": {}\n+ \"labels\": {},\n+ \"provenance\": test_run_provenance()\n });\n \n let record: RunSpec = serde_json::from_value(json).expect(\"legacy spec should deserialize\");\ndiff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs\nindex d9e2867ab..b6e122af7 100644\n--- a/lib/crates/fabro-workflow/src/billing_rollup.rs\n+++ b/lib/crates/fabro-workflow/src/billing_rollup.rs\n@@ -166,6 +166,7 @@ mod tests {\n use fabro_types::{\n AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion,\n StageOutcome, WorkflowSettings, first_event_seq, fixtures,\n+ test_support as types_test_support,\n };\n \n use super::billing_rollup_from_projection;\n@@ -353,7 +354,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: 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..4f3a35450 100644\n--- a/lib/crates/fabro-workflow/src/event/sink.rs\n+++ b/lib/crates/fabro-workflow/src/event/sink.rs\n@@ -213,6 +213,7 @@ mod tests {\n use std::sync::Arc;\n \n use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures};\n+ use fabro_types::test_support as types_test_support;\n use tokio::sync::Mutex as AsyncMutex;\n \n use super::*;\n@@ -244,7 +245,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..efe5bb514 100644\n--- a/lib/crates/fabro-workflow/src/git.rs\n+++ b/lib/crates/fabro-workflow/src/git.rs\n@@ -343,7 +343,9 @@ mod tests {\n \n use fabro_dump::RunDump;\n use fabro_store::Database;\n- use fabro_types::{CommandTermination, StageModelUsage, fixtures};\n+ use fabro_types::{\n+ CommandTermination, StageModelUsage, fixtures, test_support as types_test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -469,7 +471,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..4f2ab446e 100644\n--- a/lib/crates/fabro-workflow/src/handler/agent.rs\n+++ b/lib/crates/fabro-workflow/src/handler/agent.rs\n@@ -429,7 +429,7 @@ mod tests {\n use fabro_graphviz::graph::AttrValue;\n use fabro_model::{ReasoningEffort, Speed};\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support as types_test_support};\n use object_store::memory::InMemory;\n use tempfile::TempDir;\n \n@@ -484,7 +484,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..a74e8878d 100644\n--- a/lib/crates/fabro-workflow/src/handler/command.rs\n+++ b/lib/crates/fabro-workflow/src/handler/command.rs\n@@ -228,7 +228,10 @@ mod tests {\n use bytes::Bytes;\n use fabro_graphviz::graph::AttrValue;\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures};\n+ use fabro_types::{\n+ Graph, RunProjection, RunSpec, WorkflowSettings, fixtures,\n+ test_support as types_test_support,\n+ };\n use object_store::memory::InMemory;\n use tokio::sync::Mutex;\n \n@@ -256,7 +259,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: types_test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -357,7 +360,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..7d8d2d570 100644\n--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs\n+++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs\n@@ -1600,6 +1600,7 @@ mod tests {\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin,\n RunPairStatusResponse, RunProjection, RunStatus, RunTimestamps, SuccessReason, WorkflowRef,\n+ test_support as types_test_support,\n };\n use fabro_vault::{SecretType, Vault};\n use futures::stream;\n@@ -2133,7 +2134,7 @@ reasoning = false\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: 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..fb4406c48 100644\n--- a/lib/crates/fabro-workflow/src/handler/parallel.rs\n+++ b/lib/crates/fabro-workflow/src/handler/parallel.rs\n@@ -692,7 +692,7 @@ mod tests {\n \n use fabro_graphviz::graph::{AttrValue, Edge};\n use fabro_store::{Database, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support as types_test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -728,7 +728,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..43b155ed7 100644\n--- a/lib/crates/fabro-workflow/src/handler/prompt.rs\n+++ b/lib/crates/fabro-workflow/src/handler/prompt.rs\n@@ -225,7 +225,7 @@ mod tests {\n use fabro_graphviz::graph::AttrValue;\n use fabro_model::{ReasoningEffort, Speed};\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support as types_test_support};\n use object_store::memory::InMemory;\n use tempfile::TempDir;\n \n@@ -283,7 +283,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..5cb33b33d 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs\n@@ -598,7 +598,10 @@ mod tests {\n use fabro_model::Catalog;\n use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection};\n use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};\n- use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures};\n+ use fabro_types::{\n+ EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures,\n+ test_support as types_test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -736,7 +739,7 @@ mod tests {\n workflow_slug: Some(\"metadata\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..e6bda8f22 100644\n--- a/lib/crates/fabro-workflow/src/operations/archive.rs\n+++ b/lib/crates/fabro-workflow/src/operations/archive.rs\n@@ -136,7 +136,10 @@ mod tests {\n use std::time::Duration;\n \n use fabro_store::Database;\n- use fabro_types::{FailureReason, RunId, SuccessReason, TerminalStatus, fixtures};\n+ use fabro_types::{\n+ FailureReason, RunId, SuccessReason, TerminalStatus, fixtures,\n+ test_support as types_test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -226,7 +229,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..da2ce241a 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@@ -422,7 +422,7 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunMode;\n- use fabro_types::{WorkflowSettings, fixtures};\n+ use fabro_types::{WorkflowSettings, fixtures, test_support as types_test_support};\n use fabro_util::error::collect_chain;\n use fabro_validate::Severity;\n use object_store::local::LocalFileSystem;\n@@ -1115,7 +1115,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: 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: 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: 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: 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: 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..eeae16ba0 100644\n--- a/lib/crates/fabro-workflow/src/operations/fork.rs\n+++ b/lib/crates/fabro-workflow/src/operations/fork.rs\n@@ -284,7 +284,7 @@ mod tests {\n \n use fabro_graphviz::graph::Graph;\n use fabro_store::{Database, RunProjectionReducer};\n- use fabro_types::{StageId, WorkflowSettings, fixtures};\n+ use fabro_types::{StageId, WorkflowSettings, fixtures, test_support as types_test_support};\n use object_store::memory::InMemory;\n \n use super::*;\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: 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..15f0e9361 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@@ -123,6 +123,7 @@ mod tests {\n AuthMethod, DirtyStatus, FailureReason, ForkSourceRef, GitContext, Graph, IdpIdentity,\n PreRunPushOutcome, Principal, PullRequestLink, RunBlobId, RunRunnableSource,\n RunServerProvenance, RunTiming, UserPrincipal, WorkflowSettings, fixtures,\n+ test_support as types_test_support,\n };\n use object_store::memory::InMemory;\n \n@@ -152,7 +153,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 +192,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 +369,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 +403,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 +457,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 +511,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: types_test_support::test_run_provenance(),\n web_url: None,\n })\n .await\n@@ -535,7 +529,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: types_test_support::test_run_provenance(),\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..230a9d8ce 100644\n--- a/lib/crates/fabro-workflow/src/operations/start.rs\n+++ b/lib/crates/fabro-workflow/src/operations/start.rs\n@@ -1129,7 +1129,10 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::settings::run::RunMode;\n use fabro_types::settings::{InterpString, ModelRef};\n- use fabro_types::{BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures};\n+ use fabro_types::{\n+ BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures,\n+ test_support as types_test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -1438,7 +1441,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: types_test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1860,7 +1863,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: 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..c9ea7110f 100644\n--- a/lib/crates/fabro-workflow/src/operations/timeline.rs\n+++ b/lib/crates/fabro-workflow/src/operations/timeline.rs\n@@ -204,6 +204,7 @@ mod tests {\n use chrono::Utc;\n use fabro_types::{\n Checkpoint, CheckpointRecord, Graph, RunDiff, RunSpec, WorkflowSettings, fixtures,\n+ test_support as types_test_support,\n };\n \n use super::*;\n@@ -248,7 +249,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: 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..bd42ba7ac 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n@@ -18,7 +18,10 @@ use fabro_interview::AutoApproveInterviewer;\n use fabro_sandbox::SandboxSpec;\n use fabro_store::Database;\n use fabro_types::settings::run::RunModelControls;\n-use fabro_types::{Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref};\n+use fabro_types::{\n+ Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref,\n+ test_support as types_test_support,\n+};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -165,7 +168,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: types_test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -208,7 +211,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: 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..4b9c68589 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n@@ -651,7 +651,7 @@ mod tests {\n use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};\n use fabro_types::{\n BilledTokenCounts, EventBody, RunBlobId, RunEvent, RunId, RunSpec, StageCompletion,\n- WorkflowSettings, first_event_seq, fixtures,\n+ WorkflowSettings, first_event_seq, fixtures, test_support as types_test_support,\n };\n use object_store::memory::InMemory;\n \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: 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: 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..04cd78793 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n@@ -648,7 +648,9 @@ mod tests {\n use fabro_sandbox::SandboxSpec;\n use fabro_store::Database;\n use fabro_types::settings::run::RunModelControls;\n- use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures};\n+ use fabro_types::{\n+ EventBody, RunEvent, RunId, WorkflowSettings, fixtures, test_support as types_test_support,\n+ };\n use fabro_vault::{SecretType, Vault};\n use object_store::memory::InMemory;\n use tokio::fs::{create_dir_all, write};\n@@ -773,7 +775,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: 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..2da6979f9 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs\n@@ -59,7 +59,7 @@ mod tests {\n \n use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};\n use fabro_store::{Database, RunDatabase};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support as types_test_support};\n use object_store::memory::InMemory;\n \n use super::*;\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: 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..f5987aaae 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n@@ -680,7 +680,7 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::{\n BilledTokenCounts, RunProjection, RunSpec, SuccessReason, WorkflowSettings,\n- first_event_seq, fixtures,\n+ first_event_seq, fixtures, test_support as types_test_support,\n };\n use fabro_vault::{SecretType, Vault};\n use futures::stream;\n@@ -823,7 +823,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: 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: 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: 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: 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: 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: types_test_support::test_run_provenance(),\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: 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: 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/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs\nindex e7b0badfc..c1560d6ac 100644\n--- a/lib/crates/fabro-workflow/src/run_lookup.rs\n+++ b/lib/crates/fabro-workflow/src/run_lookup.rs\n@@ -457,7 +457,7 @@ mod tests {\n \n use fabro_graphviz::graph::Graph;\n use fabro_store::Database;\n- use fabro_types::{RunStatus, WorkflowSettings, fixtures};\n+ use fabro_types::{RunStatus, WorkflowSettings, fixtures, test_support as types_test_support};\n use object_store::memory::InMemory;\n \n use super::scan_runs_combined;\n@@ -491,7 +491,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: 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..88231af66 100644\n--- a/lib/crates/fabro-workflow/src/run_metadata.rs\n+++ b/lib/crates/fabro-workflow/src/run_metadata.rs\n@@ -537,7 +537,10 @@ mod tests {\n use std::sync::Arc;\n \n use fabro_store::RunProjection;\n- use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings};\n+ use fabro_types::{\n+ DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings,\n+ test_support as types_test_support,\n+ };\n use git2::{ErrorClass, ErrorCode};\n \n use super::*;\n@@ -639,7 +642,7 @@ mod tests {\n push_outcome: PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: 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..c2a2eb21b 100644\n--- a/lib/crates/fabro-workflow/src/runtime_store.rs\n+++ b/lib/crates/fabro-workflow/src/runtime_store.rs\n@@ -120,7 +120,9 @@ mod tests {\n use fabro_graphviz::graph::Graph;\n use fabro_store::Database;\n use fabro_types::run_event::RunSubmittedProps;\n- use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures};\n+ use fabro_types::{\n+ EventBody, RunEvent, WorkflowSettings, fixtures, test_support as types_test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::RunStoreHandle;\n@@ -148,7 +150,7 @@ mod tests {\n source_directory: Some(\"/tmp/test\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: types_test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -170,7 +172,7 @@ mod tests {\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..1acf410a4 100644\n--- a/lib/crates/fabro-workflow/src/test_support.rs\n+++ b/lib/crates/fabro-workflow/src/test_support.rs\n@@ -175,7 +175,13 @@ async fn initialized(\n workflow_slug: run_options.workflow_slug.clone(),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ },\n manifest_blob: None,\n git: run_options.pre_run_git.clone(),\n fork_source_ref: run_options.fork_source_ref.clone(),\ndiff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex 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..31263d33e 100644\n--- a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n+++ b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n@@ -1,4 +1,4 @@\n-import type { AutomationRef, Principal, PrincipalSystem, RunSpec } from \"../src\";\n+import type { AutomationRef, Principal, PrincipalSystem, Run, RunSpec } from \"../src\";\n \n type AssertFalse = T;\n type AssertExtends = true;\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@@ -42,12 +40,16 @@ export function systemKind(principal: PrincipalSystem): string {\n }\n }\n \n-type Provenance = NonNullable;\n-type Subject = NonNullable;\n+type Provenance = RunSpec[\"provenance\"];\n+type Subject = Provenance[\"subject\"];\n+type Creator = Run[\"created_by\"];\n \n type SubjectIsNotAny = AssertFalse>;\n type SubjectExtendsPrincipal = AssertExtends;\n type PrincipalExtendsSubject = AssertExtends;\n+type CreatorIsNotAny = AssertFalse>;\n+type CreatorExtendsPrincipal = AssertExtends;\n+type PrincipalExtendsCreator = AssertExtends;\n \n type Automation = NonNullable;\n type AutomationExtendsRef = AssertExtends;\n@@ -69,6 +71,9 @@ const _automationTriggerId: AutomationTriggerId = \"schedule_1\";\n void (null as unknown as SubjectIsNotAny);\n void (null as unknown as SubjectExtendsPrincipal);\n void (null as unknown as PrincipalExtendsSubject);\n+void (null as unknown as CreatorIsNotAny);\n+void (null as unknown as CreatorExtendsPrincipal);\n+void (null as unknown as PrincipalExtendsCreator);\n void (null as unknown as AutomationExtendsRef);\n void _principalSubject;\n void _automation;\n", + "summary": { + "files_changed": 98, + "additions": 456, + "deletions": 344 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-06-03T17:49:47.389471Z", + "current_node": "simplify_opus", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "implement", + "simplify_opus" + ], + "node_retries": {}, + "context_values": { + "internal.retry_count.start": 0, + "last_response": "## Summary\n\nThree review agents (reuse, quality, efficiency) inspected the two-commit change (Anonymous principal removal + total run provenance). Applied fixes:\n\n**Efficiency**\n- `apps/fabro-web/app/", + "outcome": "succeeded", + "internal.run_id": "01KT749TNV1MD2PYSHX1PB9905", "internal.retry_count.implement": 0, "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_lint.current_node": "implement", + "response.simplify_opus": "## Summary\n\nThree review agents (reuse, quality, efficiency) inspected the two-commit change (Anonymous principal removal + total run provenance). Applied fixes:\n\n**Efficiency**\n- `apps/fabro-web/app/components/runs-list/run-table-row.tsx` — `principalDisplay(run.createdBy)` was being computed on every row render even when the `created_by` column was hidden. Moved the call inside the `show(\"created_by\") &&` guard.\n\n**Quality**\n- `lib/crates/fabro-server/src/principal_middleware.rs` — `principal_without_log_unused_fields` had an awkward `Option<&Principal> → Option` signature with redundant `None`/`Some(clone)` arms. Simplified to `&Principal → Principal` and let the caller `.as_ref().map(...)`.\n- `apps/fabro-web/app/components/run-summary-panel.test.tsx` — Test \"shows unavailable copy for missing run fields after load\" asserted \"Created by\" rendered \"Ttest\", which is not an unavailable copy. Split into a separate `renders creator from run.created_by` test.\n- `apps/fabro-web/app/components/run-summary-panel.tsx` — Removed dead `created ? ... : ` branch that suggested `created_by` could still be null after a run loads; now skeleton while loading, principal display once loaded.\n\n**Reuse**\n- `lib/crates/fabro-types/Cargo.toml` — Added self dev-dep `fabro-types = { path = \".\", features = [\"test-support\"] }` to make the new `test_support::test_run_provenance()` available to integration tests.\n- `lib/crates/fabro-types/tests/{run_event_serde,run_spec_serde,run_spec_methods}.rs` — Deleted three byte-identical local `test_run_provenance()` copies and the now-unused `Principal`/`RunProvenance`/`SystemActorKind` imports; all three now call the canonical helper.\n\n**Skipped (false positive or out of scope)**\n- Demo principal `LazyLock` (already idiomatic; clone is unavoidable given owned `Run.created_by`).\n- Gate-helper boilerplate consolidation in `principal_middleware.rs` (pre-existing pattern, out of scope).\n- `fabro-workflow/src/test_support.rs` inline `RunProvenance` literal (its `test_support` module is not feature-gated, so it cannot depend on `fabro-types`'s feature-gated helper without a broader refactor).\n- `cmd/inspect.rs` inline JSON principal (low value).\n\n**Verification**\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` ✅\n- `cargo nextest run -p fabro-types -p fabro-server` (1036 + server tests passed) ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cd apps/fabro-web && bun test` (509 passed) ✅", "thread.toolchain.current_node": "preflight_compile", - "thread.start.current_node": "toolchain", "internal.retry_count.toolchain": 0, - "internal.retry_count.preflight_lint": 0, + "internal.retry_count.preflight_compile": 0, "graph.rankdir": "LR", - "internal.node_visit_count": 1, - "internal.retry_count.start": 0 + "thread.preflight_compile.current_node": "preflight_lint", + "thread.implement.current_node": "simplify_opus", + "failure_signature": "", + "internal.thread_id": "implement", + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "failure_class": "", + "current_node": "simplify_opus", + "internal.fidelity": "compact", + "response.implement": "Implemented the plan in two commits.\n\nCommits:\n- `26fdc23a9 refactor(auth): remove anonymous principal`\n- `5fea2c5ad Make run provenance and creator total`\n\nSummary:\n- Removed anonymous principal representation from auth/API/frontend.\n- Request auth now represents absent/invalid auth as `None`.\n- HTTP logs always emit `principal_kind`, using `\"none\"` for absent principal.\n- Made run creator/provenance total across Rust types, workflow events, store projections, server create/retry flows, OpenAPI, generated TS client, frontend data/UI, demo mode, and tests.\n- Added gated `fabro-types::test_support` helpers.\n- Regenerated Rust OpenAPI types and TypeScript API client.\n- Working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\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 lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Final sweeps for anonymous principal / nullable creator-provenance remnants; only unrelated matches remained (`crossorigin=\"anonymous\"`, todo/auth subject fields).\n- Manual unauthenticated protected endpoint check:\n - `/api/v1/runs` returned `401`\n - HTTP log included `principal_kind=\"none\"` and `auth_status=\"missing\"`\n- Manual authenticated demo-mode API check:\n - `X-Fabro-Demo: 1` run list returned synthetic creator:\n - `login: \"demo\"`\n - `auth_method: \"dev_token\"`\n - `identity: { issuer: \"fabro:demo\", subject: \"demo\" }`\n\nNot run:\n- Full browser-driven UI create/retry manual flow; this terminal environment has no interactive browser/auth session.", + "internal.retry_count.preflight_lint": 0, + "last_stage": "simplify_opus", + "thread.start.current_node": "toolchain", + "internal.retry_count.simplify_opus": 0, + "internal.work_dir": "/home/daytona/workspace/fabro", + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "internal.node_visit_count": 1 }, "node_outcomes": { "start": { @@ -900,6 +1050,54 @@ "active_time_ms": 4307806 } }, + "simplify_opus": { + "status": "succeeded", + "context_updates": { + "response.simplify_opus": "## Summary\n\nThree review agents (reuse, quality, efficiency) inspected the two-commit change (Anonymous principal removal + total run provenance). Applied fixes:\n\n**Efficiency**\n- `apps/fabro-web/app/components/runs-list/run-table-row.tsx` — `principalDisplay(run.createdBy)` was being computed on every row render even when the `created_by` column was hidden. Moved the call inside the `show(\"created_by\") &&` guard.\n\n**Quality**\n- `lib/crates/fabro-server/src/principal_middleware.rs` — `principal_without_log_unused_fields` had an awkward `Option<&Principal> → Option` signature with redundant `None`/`Some(clone)` arms. Simplified to `&Principal → Principal` and let the caller `.as_ref().map(...)`.\n- `apps/fabro-web/app/components/run-summary-panel.test.tsx` — Test \"shows unavailable copy for missing run fields after load\" asserted \"Created by\" rendered \"Ttest\", which is not an unavailable copy. Split into a separate `renders creator from run.created_by` test.\n- `apps/fabro-web/app/components/run-summary-panel.tsx` — Removed dead `created ? ... : ` branch that suggested `created_by` could still be null after a run loads; now skeleton while loading, principal display once loaded.\n\n**Reuse**\n- `lib/crates/fabro-types/Cargo.toml` — Added self dev-dep `fabro-types = { path = \".\", features = [\"test-support\"] }` to make the new `test_support::test_run_provenance()` available to integration tests.\n- `lib/crates/fabro-types/tests/{run_event_serde,run_spec_serde,run_spec_methods}.rs` — Deleted three byte-identical local `test_run_provenance()` copies and the now-unused `Principal`/`RunProvenance`/`SystemActorKind` imports; all three now call the canonical helper.\n\n**Skipped (false positive or out of scope)**\n- Demo principal `LazyLock` (already idiomatic; clone is unavoidable given owned `Run.created_by`).\n- Gate-helper boilerplate consolidation in `principal_middleware.rs` (pre-existing pattern, out of scope).\n- `fabro-workflow/src/test_support.rs` inline `RunProvenance` literal (its `test_support` module is not feature-gated, so it cannot depend on `fabro-types`'s feature-gated helper without a broader refactor).\n- `cmd/inspect.rs` inline JSON principal (low value).\n\n**Verification**\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` ✅\n- `cargo nextest run -p fabro-types -p fabro-server` (1036 + server tests passed) ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cd apps/fabro-web && bun test` (509 passed) ✅", + "last_stage": "simplify_opus", + "last_response": "## Summary\n\nThree review agents (reuse, quality, efficiency) inspected the two-commit change (Anonymous principal removal + total run provenance). Applied fixes:\n\n**Efficiency**\n- `apps/fabro-web/app/" + }, + "notes": "Stage completed: simplify_opus", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 62748, + "output_tokens": 17602, + "reasoning_tokens": 0, + "cache_read_tokens": 2613488, + "cache_write_tokens": 154249 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 154249, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 3024590 + }, + "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/run-summary-panel.tsx", + "/home/daytona/workspace/fabro/apps/fabro-web/app/components/runs-list/run-table-row.tsx", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/principal_middleware.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/Cargo.toml", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_event_serde.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_methods.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_serde.rs" + ], + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 288909, + "tool_time_ms": 383497, + "active_time_ms": 672406 + } + }, "toolchain": { "status": "succeeded", "context_updates": { @@ -915,9 +1113,10 @@ } } }, - "next_node_id": "simplify_opus", + "next_node_id": "simplify_gpt", "node_visits": { "toolchain": 1, + "simplify_opus": 1, "preflight_compile": 1, "implement": 1, "start": 1, @@ -1087,7 +1286,12 @@ "first_event_seq": 52, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Stage completed: implement", + "failure_reason": null, + "timestamp": "2026-06-03T17:38:19.283757Z" + }, "provider_used": { "mode": "agent", "provider": "openai", @@ -1101,6 +1305,12 @@ "output": null, "started_at": "2026-06-03T16:21:09.745532Z", "handler": "agent", + "timing": { + "wall_time_ms": 4629514, + "inference_time_ms": 2484520, + "tool_time_ms": 1823286, + "active_time_ms": 4307806 + }, "usage": { "input_tokens": 5739560, "output_tokens": 57767, @@ -1312,6 +1522,305 @@ ], "warnings": [] }, + "state": "succeeded" + }, + "simplify_opus@1": { + "first_event_seq": 1374, + "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-03T17:38:30.078150Z", + "handler": "agent", + "usage": { + "input_tokens": 62748, + "output_tokens": 17602, + "total_tokens": 2848087, + "reasoning_tokens": 0, + "cache_read_tokens": 2613488, + "cache_write_tokens": 154249, + "total_usd_micros": 3024590 + }, + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "todos": { + "kind": "anthropic_tasks", + "list_id": "anthropic_tasks:7d8b91b9-5b7e-4db3-9bfd-48f9435f262f", + "items": [ + { + "id": "1", + "status": "completed", + "order": 0, + "subject": "Fix RunTableRow: move principalDisplay inside show(\"created_by\") guard", + "description": "In apps/fabro-web/app/components/runs-list/run-table-row.tsx, principalDisplay(run.createdBy) is computed unconditionally per row, even when the created_by column is hidden. This is a regression vs. pre-diff. Move the computation inside the {show(\"created_by\") && ...} block.", + "active_form": "Fixing RunTableRow principalDisplay guard" + }, + { + "id": "2", + "status": "completed", + "order": 1, + "subject": "Simplify principal_without_log_unused_fields signature", + "description": "In lib/crates/fabro-server/src/principal_middleware.rs, principal_without_log_unused_fields currently takes Option<&Principal> and returns Option, with the None and Some(non-User) arms being boilerplate. Change to fn(&Principal) -> Principal and let the caller .as_ref().map(...).", + "active_form": "Simplifying principal_without_log_unused_fields" + }, + { + "id": "3", + "status": "completed", + "order": 2, + "subject": "Fix misleading test name in run-summary-panel.test.tsx", + "description": "The test \"shows unavailable copy for missing run fields after load\" asserts that \"Created by\" renders \"Ttest\" — that's not an unavailable copy. Split the Created by assertion into its own test and keep this one focused on Changes/Cost.", + "active_form": "Fixing misleading test name" + }, + { + "id": "4", + "status": "completed", + "order": 3, + "subject": "Remove dead EmptyValue branch for Created by in run-summary-panel.tsx", + "description": "created can only be null when run itself is null, but runLoading already handles that branch. The (created ? ... : ) ternary's EmptyValue arm is dead code that suggests created_by can still be null. Replace with the loaded principal display.", + "active_form": "Removing dead EmptyValue branch" + }, + { + "id": "5", + "status": "completed", + "order": 4, + "subject": "Consolidate duplicate test_run_provenance helper in run_event_serde.rs", + "description": "The diff added another inline test_run_provenance() copy in lib/crates/fabro-types/tests/run_event_serde.rs duplicating fabro_types::test_support::test_run_provenance(). Add a dev-dep self-listing with the test-support feature and use the shared helper.", + "active_form": "Consolidating duplicate test_run_provenance" + } + ] + }, + "subagents": [ + { + "agent_id": "d618a33c", + "depth": 1, + "task": "You are performing a CODE REUSE review on a recent series of changes in this repo. The full diff is saved at /tmp/all_diff.patch (3063 lines). Read it first to understand the scope.\n\nContext: The change does two things:\n1. Removes `Principal::Anonymous` variant; `Principal` now means \"who acted\" and missing auth is represented as `Option` in middleware.\n2. Makes run provenance total: `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, `Event::RunCreated.provenance` are now non-optional. Also adds a new test-support module at `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()`.\n\nYour job:\n1. Read /tmp/all_diff.patch in full.\n2. For each meaningful change, look for existing utilities or helpers in the repo that could replace newly written code. Common locations: `lib/crates/fabro-types/src/`, test_support modules in other crates (like `lib/crates/fabro-workflow/src/test_support.rs`), and adjacent helpers.\n3. Flag duplicates: new functions that duplicate existing functionality (e.g. principal/provenance constructors that already exist somewhere).\n4. Flag inline logic that could use an existing utility.\n\nSpecifically check:\n- Does `test_principal()` / `test_run_provenance()` in `lib/crates/fabro-types/src/test_support.rs` duplicate something that already exists (e.g. in `fabro-workflow/src/test_support.rs`, `fabro-server` test helpers, `fabro-types/src/fixtures.rs`)?\n- Are there multiple in-repo places now constructing the same demo/test principal that should use the shared helper?\n- Are there places where the diff inlines a `RunProvenance { subject: ..., source: None, request_id: None }` literal that should call `test_run_provenance()` or some constructor?\n- Is the new demo principal in `fabro-server/src/demo/mod.rs` redundant with something else? Could it use a constructor helper?\n\nUse `grep` and `read_file` tools to verify before reporting. Be precise: cite file:line for both the new code and the existing helper that should replace it.\n\nReport findings as a list. For each finding include:\n- File path and line(s) in the diff\n- Existing helper/utility that should be used\n- Concrete suggested change\n\nIf the code is already clean, say so explicitly.", + "status": { + "kind": "closed" + } + }, + { + "agent_id": "5a99f008", + "depth": 1, + "task": "You are performing a CODE QUALITY review on a recent series of changes in this repo. The full diff is saved at /tmp/all_diff.patch (3063 lines). Read it first to understand the scope.\n\nContext: The change does two things:\n1. Removes `Principal::Anonymous` variant; `Principal` now means \"who acted\" and missing auth is represented as `Option` in middleware.\n2. Makes run provenance total: `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, `Event::RunCreated.provenance` are now non-optional. Also adds a new test-support module at `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()`.\n\nYour job: Review the diff for hacky patterns:\n\n1. **Redundant state**: state duplicating existing state, derived values cached\n2. **Parameter sprawl**: new parameters added rather than restructuring\n3. **Copy-paste with slight variation**: near-duplicate code that should be unified\n4. **Leaky abstractions**: exposing internals that should be encapsulated\n5. **Stringly-typed code**: raw strings where constants/enums exist\n6. **Unnecessary JSX nesting / wrapper elements**\n7. **Unnecessary comments**: comments narrating WHAT or referencing the task — keep only non-obvious WHY\n\nSpecifically check:\n- Tests that previously asserted `created_by: None` or `provenance: None` — were they DELETED rather than dumbly updated to assert `Some(...)` and now testing nothing useful?\n- Are there now-meaningless \"round trip\" tests for provenance/created_by that just construct a value and check it deserializes (low value)?\n- Are there leftover comments referencing \"anonymous principal\" or referencing the task?\n- Are pattern matches using `Option` cleanly (e.g., not awkwardly converting to Option and back)?\n- In `principal_middleware.rs`, are the gate helpers clean? Or do they repeat the same `None => auth_rejection(...)` boilerplate that could be a single helper?\n- In `server.rs` HTTP logging, is the match on `Option` clean?\n- Did any wrapper JSX components get added in `apps/fabro-web` that are now unnecessary because the inner value is non-null?\n- Are there `.clone()` calls on the new total `Principal`/`RunProvenance` that could be avoided?\n- Are there leftover `Default` impls or `#[serde(default)]` attributes that should have been removed?\n- Are there test fixtures duplicating the new `test_principal()` / `test_run_provenance()` shape inline?\n- Look at how the demo principal in `fabro-server/src/demo/mod.rs` is defined — is it clean and idiomatic?\n\nUse `grep` and `read_file` to verify findings. Cite file:line.\n\nReport findings as a list with concrete fixes. If clean, say so.", + "status": { + "kind": "closed" + } + }, + { + "agent_id": "5de40841", + "depth": 1, + "task": "You are performing an EFFICIENCY review on a recent series of changes in this repo. The full diff is saved at /tmp/all_diff.patch (3063 lines). Read it first to understand the scope.\n\nContext: The change does two things:\n1. Removes `Principal::Anonymous` variant; `Principal` now means \"who acted\" and missing auth is represented as `Option` in middleware.\n2. Makes run provenance total: `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, `Event::RunCreated.provenance` are now non-optional.\n\nYour job: Review the diff for efficiency:\n\n1. **Unnecessary work**: redundant computations, repeated reads, N+1 patterns\n2. **Missed concurrency**: independent ops that could run in parallel\n3. **Hot-path bloat**: new blocking work added to startup or per-request/render paths\n4. **Recurring no-op updates**: state updates in loops/intervals\n5. **Unnecessary existence checks** before operating (TOCTOU)\n6. **Memory**: unbounded structures, missing cleanup\n7. **Overly broad operations**\n\nSpecifically check:\n- In `lib/crates/fabro-server/src/demo/mod.rs`, the new `DEMO_PRINCIPAL` LazyLock: is it constructed once and cloned, or is it constructed on every call? Are there clones happening per-request that could be avoided?\n- The new `test_principal()` / `test_run_provenance()` functions in `fabro-types/src/test_support.rs`: do they reconstruct values on every call when they could be `LazyLock`s? (For tests, this is usually fine, but check.)\n- Any `Principal` or `RunProvenance` `.clone()` calls in hot paths that became more expensive once the values became non-optional (e.g. workflow operations, event conversion, projections)?\n- Are there now redundant `provenance.clone()` + `provenance.subject.clone()` patterns that could share one clone?\n- Any per-request synchronous principal constructions in middleware?\n- The match in `server.rs` for HTTP logging: any redundant computation across arms?\n\nUse `grep` and `read_file` to verify. Cite file:line.\n\nReport findings as a list with concrete fixes. If clean, say so.", + "status": { + "kind": "closed" + } + } + ], + "permission_level": "full", + "agent_tools": [ + { + "name": "AskUserQuestion", + "description": "Ask the human one or more questions and wait for their answers before continuing this stage.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "TaskCreate", + "description": "Create pending tasks in the current session. Use concise subjects, descriptions, optional activeForm text, and metadata. Check TaskList first to avoid duplicate tasks.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": true + }, + { + "name": "TaskGet", + "description": "Get one task by taskId, including subject, status, description, owner, blockedBy, and blocks.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "TaskList", + "description": "List tasks for the current session, including status, owner, and blocking dependencies. Use TaskGet with a taskId for full description and dependency details.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": true + }, + { + "name": "TaskUpdate", + "description": "Update an existing task's status, text, owner, metadata, or dependencies. Valid statuses are pending, in_progress, completed, and deleted. After completing a task, call TaskList to find newly unblocked work.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": true + }, + { + "name": "close_agent", + "description": "Close a running subagent that is no longer needed.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": true + }, + { + "name": "edit_file", + "description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.", + "source": { + "kind": "native" + }, + "category": "write", + "invoked": true + }, + { + "name": "glob", + "description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.", + "source": { + "kind": "native" + }, + "category": "read", + "invoked": true + }, + { + "name": "grep", + "description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.", + "source": { + "kind": "native" + }, + "category": "read", + "invoked": true + }, + { + "name": "read_file", + "description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.", + "source": { + "kind": "native" + }, + "category": "read", + "invoked": true + }, + { + "name": "send_input", + "description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": false + }, + { + "name": "shell", + "description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.", + "source": { + "kind": "native" + }, + "category": "shell", + "invoked": true + }, + { + "name": "spawn_agent", + "description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": true + }, + { + "name": "wait", + "description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": true + }, + { + "name": "web_fetch", + "description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "web_search", + "description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "name": "write_file", + "description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.", + "source": { + "kind": "native" + }, + "category": "write", + "invoked": false + } + ], + "context_window": { + "provider": "anthropic", + "model": "claude-opus-4-7", + "context_window_tokens": 1000000, + "input_tokens": 74932, + "usage_percent": 7.4932, + "count_method": "response_usage_scaled_breakdown", + "staleness": "live", + "generated_at": "2026-06-03T17:49:47.286311Z", + "event_seq": 1868, + "breakdown": [ + { + "category": "system_prompt", + "tokens": 2448, + "usage_percent": 0.2448 + }, + { + "category": "tools", + "tokens": 2840, + "usage_percent": 0.284 + }, + { + "category": "memory", + "tokens": 6038, + "usage_percent": 0.6038 + }, + { + "category": "conversation", + "tokens": 63598, + "usage_percent": 6.3598 + }, + { + "category": "other", + "tokens": 8, + "usage_percent": 0.0008 + } + ], + "warnings": [] + }, "state": "running" }, "preflight_lint@1": { diff --git a/stages/005-implement@1/diff.patch b/stages/005-implement@1/diff.patch new file mode 100644 index 000000000..bea58d2d3 --- /dev/null +++ b/stages/005-implement@1/diff.patch @@ -0,0 +1,3063 @@ +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..476224728 100644 +--- a/apps/fabro-web/app/components/run-summary-panel.test.tsx ++++ b/apps/fabro-web/app/components/run-summary-panel.test.tsx +@@ -6,6 +6,7 @@ import { + RunSummaryPanelView, + type RunSummaryPanelViewProps, + } from "./run-summary-panel"; ++import { TEST_PRINCIPAL } from "../lib/test-fixtures"; + + function instanceText(instance: TestRenderer.ReactTestInstance): string { + const parts: string[] = []; +@@ -53,7 +54,7 @@ function cellAfterLabel( + function makeRun(overrides: Record = {}) { + return { + id: "run_1", +- created_by: null, ++ created_by: TEST_PRINCIPAL, + diff: null, + billing: null, + ...overrides, +@@ -73,7 +74,7 @@ describe("RunSummaryPanelView", () => { + + test("shows unavailable copy for missing 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); + }); +@@ -252,7 +253,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..aa7ddd87b 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); +@@ -133,7 +133,7 @@ export function RunSummaryPanelView({ + {created.label} + + ) : ( +- ++ + )} + + +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..8fc722bd6 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 +@@ -36,6 +36,7 @@ export function RunTableRow({ + const lifecycleLabel = listLifecycleStatusLabel(run); + const statusDisplay = columnStatusDisplay[run.status]; + const show = (col: ToggleableColumn) => !hiddenColumns.has(col); ++ const creatorDisplay = principalDisplay(run.createdBy); + + return ( + +@@ -54,14 +55,9 @@ export function RunTableRow({ + + {show("created_by") && ( + +- {run.createdBy && (() => { +- const display = principalDisplay(run.createdBy); +- return ( +- +- {display.glyph} +- +- ); +- })()} ++ ++ {creatorDisplay.glyph} ++ + + )} + {show("repo") && ( +diff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts +index 98586b2c4..feb2ba568 100644 +--- a/apps/fabro-web/app/data/runs.test.ts ++++ b/apps/fabro-web/app/data/runs.test.ts +@@ -8,6 +8,7 @@ import { + mapRunToRunItem, + runStatusDisplay, + } from "./runs"; ++import { TEST_PRINCIPAL } from "../lib/test-fixtures"; + + function makeRun(overrides: Partial = {}): Run { + return { +@@ -17,7 +18,7 @@ function makeRun(overrides: Partial = {}): Run { + workflow: { slug: "fix_build", name: "Fix Build", graph_name: "FixBuild", node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: "myrepo", origin_url: null, provider: "unknown" }, +- created_by: null, ++ created_by: 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..d88fdd3af 100644 +--- a/apps/fabro-web/app/lib/run-actions.test.ts ++++ b/apps/fabro-web/app/lib/run-actions.test.ts +@@ -24,6 +24,7 @@ import { + unarchiveRuns, + } from "./run-actions"; + import { generatedAxios } from "./api-client"; ++import { TEST_PRINCIPAL } from "./test-fixtures"; + + type StubResponseInit = { + status: number; +@@ -47,7 +48,7 @@ function makeRun(status: RunStatus, archived = false): Run { + workflow: { slug: "fix_build", name: "Fix Build", graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: null, +- created_by: null, ++ created_by: TEST_PRINCIPAL, + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/lib/test-fixtures.ts b/apps/fabro-web/app/lib/test-fixtures.ts +new file mode 100644 +index 000000000..d05afc2a0 +--- /dev/null ++++ b/apps/fabro-web/app/lib/test-fixtures.ts +@@ -0,0 +1,8 @@ ++import type { Principal } from "@qltysh/fabro-api-client"; ++ ++export const TEST_PRINCIPAL = { ++ kind: "user", ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token", ++} satisfies Principal; +diff --git a/apps/fabro-web/app/routes/automations-new.test.tsx b/apps/fabro-web/app/routes/automations-new.test.tsx +index 47f471abf..32a88dccc 100644 +--- a/apps/fabro-web/app/routes/automations-new.test.tsx ++++ b/apps/fabro-web/app/routes/automations-new.test.tsx +@@ -4,6 +4,7 @@ import TestRenderer, { act } from "react-test-renderer"; + import { createMemoryRouter, RouterProvider } from "react-router"; + + import { ToastProvider } from "../components/toast"; ++import { TEST_PRINCIPAL } from "../lib/test-fixtures"; + import { setupReactTestEnv } from "../lib/test-utils"; + + let currentRun: any = null; +@@ -120,7 +121,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..a850cc0b5 100644 +--- a/apps/fabro-web/app/routes/run-detail.test.ts ++++ b/apps/fabro-web/app/routes/run-detail.test.ts +@@ -13,6 +13,7 @@ import { + + import { ToastProvider } from "../components/toast"; + import { DemoModeProvider } from "../lib/demo-mode"; ++import { TEST_PRINCIPAL } from "../lib/test-fixtures"; + + let currentRunSummary: any = null; + let currentRunState: any = null; +@@ -221,7 +222,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..40a51614e 100644 +--- a/apps/fabro-web/app/routes/run-files.render.test.tsx ++++ b/apps/fabro-web/app/routes/run-files.render.test.tsx +@@ -5,6 +5,7 @@ import { MemoryRouter, Route, Routes } from "react-router"; + import { toast as sonnerToast } from "sonner"; + + import { ToastProvider } from "../components/toast"; ++import { TEST_PRINCIPAL } from "../lib/test-fixtures"; + + let currentFilesPayload: any = null; + let currentCommitsPayload: any = null; +@@ -51,7 +52,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..01eda20fc 100644 +--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx ++++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx +@@ -5,6 +5,7 @@ import type { PaginatedRunList, Run } from "@qltysh/fabro-api-client"; + + import { ToastProvider } from "../components/toast"; + import { CHILD_RUNS_LIST_PREFERENCES_STORAGE_KEY } from "../components/runs-list/preferences"; ++import { TEST_PRINCIPAL } from "../lib/test-fixtures"; + import { setupReactTestEnv } from "../lib/test-utils"; + + class MemoryStorage { +@@ -35,7 +36,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..babef285b 100644 +--- a/apps/fabro-web/app/routes/runs.test.tsx ++++ b/apps/fabro-web/app/routes/runs.test.tsx +@@ -11,6 +11,7 @@ import { + shouldRefreshBoardForEvent, + } from "./runs"; + import { summarizeBatchLifecycleAction } from "../components/runs-list/batch-lifecycle"; ++import { TEST_PRINCIPAL } from "../lib/test-fixtures"; + + function boardRun(id: string, column: BoardColumn, questionText?: string): Run { + const status = +@@ -34,7 +35,7 @@ function boardRun(id: string, column: BoardColumn, questionText?: string): Run { + workflow: { slug: "test", name: "Test", graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: "repo", origin_url: null, provider: "unknown" }, +- created_by: null, ++ created_by: TEST_PRINCIPAL, + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md +index 63f6f8f54..22d0b66d1 100644 +--- a/docs/internal/logging-strategy.md ++++ b/docs/internal/logging-strategy.md +@@ -118,11 +118,11 @@ Fields are key-value pairs that make events queryable. Include enough context th + | `error` | Error value on failure | + | `path` | File system path | + | `duration_ms` | Elapsed time in milliseconds | +-| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) | ++| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) | + | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) | + | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests | + +-For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`). ++For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`). Requests without a principal use `principal_kind="none"`; `auth_status` distinguishes missing, invalid, expired, and authenticated auth state. + + Server auth intentionally exposes a mutable `RequestAuth` context slot for public auth routes and guard extractors such as `RequiredUser` / `RequireRunScoped` for protected routes. There is no loose `RequestPrincipal` extractor; route-facing extractors should enforce the route's auth contract while the slot supplies the final HTTP log fields. + | `input_tokens` | Token count for LLM input | +diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml +index 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..298dad17c 100644 +--- a/lib/crates/fabro-api/Cargo.toml ++++ b/lib/crates/fabro-api/Cargo.toml +@@ -34,3 +34,6 @@ serde_json = "1" + serde_yaml = "0.9" + prettyplease = "0.2" + syn = "2" ++ ++[dev-dependencies] ++fabro-types = { path = "../fabro-types", features = ["test-support"] } +diff --git a/lib/crates/fabro-api/tests/principal_round_trip.rs b/lib/crates/fabro-api/tests/principal_round_trip.rs +index ca1180e60..ac2b2c258 100644 +--- a/lib/crates/fabro-api/tests/principal_round_trip.rs ++++ b/lib/crates/fabro-api/tests/principal_round_trip.rs +@@ -118,7 +118,6 @@ fn principal_round_trips_every_variant_through_api_type() { + Principal::System { + system_kind: SystemActorKind::Watchdog, + }, +- Principal::Anonymous, + ]; + + for principal in variants { +@@ -140,9 +139,9 @@ fn run_provenance_subject_round_trips_as_principal() { + name: Some("fabro-cli".to_string()), + version: Some("0.1.0".to_string()), + }), +- subject: Some(Principal::Worker { ++ subject: Principal::Worker { + run_id: fixtures::RUN_1, +- }), ++ }, + }; + let json = serde_json::to_value(&provenance).unwrap(); + +diff --git a/lib/crates/fabro-api/tests/run_event_round_trip.rs b/lib/crates/fabro-api/tests/run_event_round_trip.rs +index dc2c9297f..18611c2e4 100644 +--- a/lib/crates/fabro-api/tests/run_event_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs +@@ -1,7 +1,9 @@ + use std::any::{TypeId, type_name}; + + use fabro_api::types::RunEvent as ApiRunEvent; +-use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures}; ++use fabro_types::{ ++ Graph, RunEvent, WorkflowSettings, fixtures, test_support as types_test_support, ++}; + use serde_json::{Value, json}; + + #[test] +@@ -20,7 +22,8 @@ fn run_event_round_trips_run_created() { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/fabro/run-1", +- "source_directory": "/tmp/fabro/run-1" ++ "source_directory": "/tmp/fabro/run-1", ++ "provenance": types_test_support::test_run_provenance() + } + }); + +@@ -39,6 +42,7 @@ fn run_event_round_trips_run_created_with_web_url() { + "graph": Graph::new("test"), + "run_dir": "/tmp/fabro/run-1", + "source_directory": "/tmp/fabro/run-1", ++ "provenance": types_test_support::test_run_provenance(), + "web_url": format!("http://localhost:3000/runs/{}", fixtures::RUN_1) + } + }); +diff --git a/lib/crates/fabro-api/tests/run_projection_round_trip.rs b/lib/crates/fabro-api/tests/run_projection_round_trip.rs +index 9a2a0f310..a4e7d6622 100644 +--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs +@@ -1,7 +1,9 @@ + use std::any::{TypeId, type_name}; + + use fabro_api::types::RunProjection as ApiRunProjection; +-use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings}; ++use fabro_types::{ ++ Graph, RunProjection, RunSpec, WorkflowSettings, test_support as types_test_support, ++}; + use serde_json::json; + + #[test] +@@ -137,7 +139,7 @@ fn run_spec_json() -> serde_json::Value { + automation: None, + source_directory: None, + labels: std::collections::HashMap::new(), +- provenance: None, ++ provenance: 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..4212cb4b4 100644 +--- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs +@@ -12,7 +12,7 @@ use fabro_types::{ + AskFabro, AskFabroUnavailableReason, AutomationRef, DiffSummary, PullRequestLink, + RepositoryProvider, RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary, + RunId, RunLifecycle, RunLinks, RunOrigin, RunRunnableSource, RunSize, RunTimestamps, RunTiming, +- WorkflowRef, fixtures, ++ WorkflowRef, fixtures, test_support as types_test_support, + }; + use serde_json::json; + +@@ -88,7 +88,7 @@ fn run_summary_json_matches_openapi_shape() { + origin_url: None, + provider: RepositoryProvider::Unknown, + }), +- created_by: None, ++ created_by: types_test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::from([("team".to_string(), "core".to_string())]), + lifecycle: RunLifecycle { +@@ -161,7 +161,7 @@ fn run_summary_json_matches_openapi_shape() { + "origin_url": null, + "provider": "unknown" + }, +- "created_by": null, ++ "created_by": types_test_support::test_principal(), + "origin": { + "kind": "api" + }, +@@ -253,6 +253,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() { + "origin_url": null, + "provider": "unknown" + }, ++ "created_by": types_test_support::test_principal(), + "models": [], + "timestamps": { + "created_at": "2026-04-20T12:00:00Z", +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..9f0e814d9 100644 +--- a/lib/crates/fabro-cli/src/commands/run/attach.rs ++++ b/lib/crates/fabro-cli/src/commands/run/attach.rs +@@ -822,6 +822,7 @@ mod tests { + )] + + use fabro_interview::{Answer, AnswerValue}; ++ use fabro_types::test_support as types_test_support; + use fabro_util::terminal::Styles; + use httpmock::MockServer; + +@@ -841,7 +842,7 @@ mod tests { + automation: None, + source_directory: None, + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: 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..8f619de4f 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs +@@ -21,7 +21,7 @@ use fabro_config::daemon::ServerDaemon; + use fabro_config::{Storage, envfile}; + use fabro_store::EventEnvelope; + use fabro_test::{TestContext, expect_reqwest_status}; +-use fabro_types::{RunId, StageId}; ++use fabro_types::{RunId, StageId, test_support as types_test_support}; + use httpmock::{Mock, MockServer}; + use serde_json::Value; + use shlex::try_quote; +@@ -165,6 +165,7 @@ pub(crate) fn remote_run_summary_json( + ) -> Value { + serde_json::json!({ + "id": run_id, ++ "children_count": 0, + "title": goal, + "goal": goal, + "workflow": { +@@ -172,11 +173,13 @@ pub(crate) fn remote_run_summary_json( + "name": workflow_name, + "graph_name": null + }, ++ "automation": null, + "repository": { + "name": "repo", + "origin_url": null, + "provider": "unknown" + }, ++ "created_by": types_test_support::test_principal(), + "origin": { + "kind": "api" + }, +@@ -199,6 +202,9 @@ pub(crate) fn remote_run_summary_json( + }, + "timing": null, + "billing": null, ++ "ask_fabro": { ++ "available": false ++ }, + "diff": null, + "pull_request": null, + "current_question": null, +diff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs +index 80b65e4aa..336f06d2b 100644 +--- a/lib/crates/fabro-cli/tests/it/support/mod.rs ++++ b/lib/crates/fabro-cli/tests/it/support/mod.rs +@@ -9,7 +9,7 @@ pub(crate) use auth_harness::{ + pub(crate) use auth_tokens::{TEST_SESSION_SECRET, issue_test_github_jwt, issue_test_worker_jwt}; + use fabro_store::EventEnvelope; + use fabro_test::{EnvVars, TestContext, preserve_coverage_env}; +-use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings}; ++use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings, test_support as types_test_support}; + + pub(crate) fn run_output_filters(context: &TestContext) -> Vec<(String, String)> { + let mut filters = context.filters(); +@@ -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: 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..a34f0b8a1 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 as types_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: types_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..39a9cf4e3 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,23 @@ 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("hard-coded demo identity should be valid"), ++ "demo".to_string(), ++ AuthMethod::DevToken, ++ ) ++ }); ++ + fn labels(entries: &[(&str, &str)]) -> HashMap { + entries + .iter() +@@ -1171,7 +1181,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..3b6ce4c2c 100644 +--- a/lib/crates/fabro-server/src/run_files.rs ++++ b/lib/crates/fabro-server/src/run_files.rs +@@ -1717,7 +1717,7 @@ fn count_flags(data: &[FileDiff]) -> (u64, u64, u64, u64) { + mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + +- use fabro_types::{CommandTermination, RunId}; ++ use fabro_types::{CommandTermination, RunId, test_support as types_test_support}; + use tokio::time::{Duration, sleep}; + + use super::*; +@@ -2389,7 +2389,7 @@ index 1111111..2222222 160000 + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: 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..a0e9b4c39 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; +@@ -193,6 +195,7 @@ pub(crate) fn validate_prepared_manifest( + pub(crate) fn create_run_input( + prepared: PreparedManifest, + configured_providers: Vec, ++ provenance: RunProvenance, + web_url: Option, + ) -> CreateRunInput { + CreateRunInput { +@@ -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..5ec8d0364 100644 +--- a/lib/crates/fabro-server/src/server/handler/events.rs ++++ b/lib/crates/fabro-server/src/server/handler/events.rs +@@ -535,7 +535,7 @@ mod stage_events_tests { + use axum::body::{Body, to_bytes}; + use axum::http::{Request, StatusCode, header}; + use fabro_store::EventPayload; +- use fabro_types::{Graph, RunId, WorkflowSettings}; ++ use fabro_types::{Graph, RunId, WorkflowSettings, test_support as types_test_support}; + use fabro_workflow::event as workflow_event; + use http_body_util::BodyExt; + use serde_json::json; +@@ -570,7 +570,7 @@ mod stage_events_tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..6a559db9a 100644 +--- a/lib/crates/fabro-server/src/server/handler/pair.rs ++++ b/lib/crates/fabro-server/src/server/handler/pair.rs +@@ -850,7 +850,7 @@ mod tests { + use fabro_types::run_event::AgentMessageProps; + use fabro_types::{ + BilledTokenCounts, EventEnvelope, Graph, PairMessageId, RunEvent, StageId, +- WorkflowSettings, fixtures, ++ WorkflowSettings, fixtures, test_support as types_test_support, + }; + use fabro_workflow::event as workflow_event; + use tower::ServiceExt; +@@ -1024,7 +1024,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..2c5097d32 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(), + ready_provider_ids.clone(), ++ provenance, + web_url.clone(), + ); + create_input.run_id = Some(run_id); +- create_input.provenance = Some(run_provenance(&headers, &actor)); + create_input.submitted_manifest_bytes = Some(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..46277a401 100644 +--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs ++++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs +@@ -1299,7 +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::{Graph, RunId, WorkflowSettings}; ++ use fabro_types::{Graph, RunId, WorkflowSettings, test_support as types_test_support}; + use serde_json::{Value, json}; + use tower::ServiceExt; + +@@ -1339,6 +1339,7 @@ mod retrieve_sandbox_tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/test", ++ "provenance": types_test_support::test_run_provenance(), + }, + }), + run_id, +diff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/crates/fabro-server/src/server/handler/sessions.rs +index bad48c6d4..b45422386 100644 +--- a/lib/crates/fabro-server/src/server/handler/sessions.rs ++++ b/lib/crates/fabro-server/src/server/handler/sessions.rs +@@ -1506,6 +1506,7 @@ mod tests { + use fabro_agent::config::ToolAccess; + use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource}; + use fabro_llm::types::{ToolCall, ToolDefinition}; ++ use fabro_types::test_support as types_test_support; + + use super::*; + +@@ -1700,7 +1701,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: 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..098d66c37 100644 +--- a/lib/crates/fabro-server/src/server/tests.rs ++++ b/lib/crates/fabro-server/src/server/tests.rs +@@ -29,7 +29,7 @@ use fabro_types::{ + SandboxProviderKind, StageContextWindowBreakdownItem, StageContextWindowCategory, + StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, + StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind, +- WorkflowSettings, fixtures, ++ WorkflowSettings, fixtures, test_support as types_test_support, + }; + use fabro_util::check_report::CheckStatus; + use fabro_workflow::records::CheckpointExt; +@@ -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: 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: 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: 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: 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: 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: 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..43eb4cf26 100644 +--- a/lib/crates/fabro-server/tests/it/api/run_files.rs ++++ b/lib/crates/fabro-server/tests/it/api/run_files.rs +@@ -14,7 +14,9 @@ use axum::body::Body; + use axum::http::{Request, StatusCode}; + use fabro_server::test_support::test_app_state_with_store; + use fabro_store::{ArtifactStore, Database}; +-use fabro_types::{Graph, RunId, SandboxProviderKind, WorkflowSettings}; ++use fabro_types::{ ++ Graph, RunId, SandboxProviderKind, WorkflowSettings, test_support as types_test_support, ++}; + use fabro_workflow::event as workflow_event; + use fabro_workflow::run_status::SuccessReason; + use object_store::memory::InMemory as MemoryObjectStore; +@@ -69,7 +71,7 @@ async fn append_completed_run_with_final_patch( + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..6e7291561 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 as types_test_support, + }; + use serde_json::json; + +@@ -1358,7 +1354,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: types_test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -1439,7 +1435,7 @@ mod tests { + } + + #[test] +- fn legacy_run_created_projects_retried_from_none() { ++ fn run_created_without_retry_projects_retried_from_none() { + let event = test_raw_event( + 1, + "run.created", +@@ -1652,7 +1648,7 @@ mod tests { + "run_id": fixtures::RUN_1, + "event": event, + "node_id": node_id, +- "properties": properties, ++ "properties": test_event_properties(event, properties), + })) + .unwrap(), + } +@@ -1673,12 +1669,25 @@ mod tests { + "run_id": fixtures::RUN_1, + "event": event, + "node_id": node_id, +- "properties": properties, ++ "properties": test_event_properties(event, properties), + })) + .unwrap(), + } + } + ++ fn test_event_properties(event: &str, properties: &serde_json::Value) -> serde_json::Value { ++ let mut properties = properties.clone(); ++ if event == "run.created" { ++ let object = properties ++ .as_object_mut() ++ .expect("run.created test properties should be a JSON object"); ++ object ++ .entry("provenance".to_string()) ++ .or_insert_with(|| json!(types_test_support::test_run_provenance())); ++ } ++ properties ++ } ++ + #[test] + fn live_run_timing_returns_none_before_run_starts() { + let state = initialized_projection(); +@@ -1822,7 +1831,7 @@ mod tests { + "repo_origin_url": null, + "base_branch": null, + "labels": {}, +- "provenance": null, ++ "provenance": types_test_support::test_run_provenance(), + "manifest_blob": null, + "definition_blob": null, + "git": null, +@@ -2851,7 +2860,7 @@ mod tests { + source_directory: Some("/tmp/repo".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: types_test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -2877,7 +2886,7 @@ mod tests { + source_directory: Some("/tmp/repo".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: types_test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -2930,7 +2939,7 @@ mod tests { + } + + #[test] +- fn legacy_run_created_without_title_infers_projection_title() { ++ fn run_created_without_title_infers_projection_title_from_goal() { + let event = test_raw_event( + 1, + "run.created", +@@ -3016,7 +3025,8 @@ mod tests { + "labels": {}, + "run_dir": "/tmp/run", + "source_directory": "/tmp/run", +- "manifest_blob": manifest_blob ++ "manifest_blob": manifest_blob, ++ "provenance": types_test_support::test_run_provenance() + } + })) + .unwrap(), +diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs +index c6406fd8d..b7d2be8d3 100644 +--- a/lib/crates/fabro-store/src/slate/mod.rs ++++ b/lib/crates/fabro-store/src/slate/mod.rs +@@ -472,7 +472,7 @@ mod tests { + use chrono::{DateTime, Utc}; + use fabro_types::{ + AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId, +- SuccessReason, WorkflowSettings, ++ SuccessReason, WorkflowSettings, test_support as types_test_support, + }; + use futures::TryStreamExt; + use object_store::memory::InMemory; +@@ -542,7 +542,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: types_test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(fabro_types::GitContext { +@@ -601,6 +601,7 @@ mod tests { + "run_dir": format!("/tmp/{label}"), + "git": run_spec.git, + "labels": run_spec.labels, ++ "provenance": run_spec.provenance, + }), + )) + .await +@@ -627,6 +628,7 @@ mod tests { + "git": run_spec.git, + "labels": run_spec.labels, + "parent_id": parent_id, ++ "provenance": run_spec.provenance, + }), + )) + .await +@@ -1300,6 +1302,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..8ff532352 100644 +--- a/lib/crates/fabro-store/src/slate/run_store.rs ++++ b/lib/crates/fabro-store/src/slate/run_store.rs +@@ -667,7 +667,9 @@ mod tests { + use std::sync::Arc; + use std::time::Duration; + +- use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings}; ++ use fabro_types::{ ++ Graph, RunId, SessionId, StageId, WorkflowSettings, test_support as types_test_support, ++ }; + use object_store::memory::InMemory; + use serde_json::json; + +@@ -723,6 +725,7 @@ mod tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/test", ++ "provenance": types_test_support::test_run_provenance(), + }, + }), + run_id, +diff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs +index 6584a36cd..db597eef1 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 as types_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: types_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..2a0b19cda 100644 +--- a/lib/crates/fabro-tool/src/common.rs ++++ b/lib/crates/fabro-tool/src/common.rs +@@ -307,7 +307,10 @@ fn format_tool_error(err: &anyhow::Error) -> String { + #[cfg(test)] + mod tests { + use chrono::{TimeZone, Utc}; +- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef}; ++ use fabro_types::{ ++ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, ++ test_support as types_test_support, ++ }; + + use super::*; + +@@ -413,7 +416,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: types_test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs +index 8488f8f0b..0abee98f8 100644 +--- a/lib/crates/fabro-tool/src/create.rs ++++ b/lib/crates/fabro-tool/src/create.rs +@@ -508,7 +508,7 @@ mod tests { + use fabro_api::types; + use fabro_types::{ + EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus, +- RunTimestamps, WorkflowRef, ++ RunTimestamps, WorkflowRef, test_support as types_test_support, + }; + use schemars::SchemaGenerator; + use serde_json::json; +@@ -902,7 +902,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: types_test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/interact.rs b/lib/crates/fabro-tool/src/interact.rs +index 34023120d..fc833ca3c 100644 +--- a/lib/crates/fabro-tool/src/interact.rs ++++ b/lib/crates/fabro-tool/src/interact.rs +@@ -453,7 +453,7 @@ mod tests { + use chrono::{TimeZone, Utc}; + use fabro_types::{ + EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection, +- RunStatus, RunTimestamps, WorkflowRef, ++ RunStatus, RunTimestamps, WorkflowRef, test_support as types_test_support, + }; + use serde_json::json; + +@@ -690,7 +690,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: types_test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs +index 0df7e391a..04d8b5d79 100644 +--- a/lib/crates/fabro-tool/src/search.rs ++++ b/lib/crates/fabro-tool/src/search.rs +@@ -293,7 +293,10 @@ mod tests { + use std::collections::HashMap; + + use chrono::{TimeZone, Utc}; +- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef}; ++ use fabro_types::{ ++ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, ++ test_support as types_test_support, ++ }; + + use super::*; + +@@ -444,7 +447,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: types_test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::from([("group".to_string(), group.to_string())]), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs +index 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..41c8a6546 100644 +--- a/lib/crates/fabro-types/src/run_event/mod.rs ++++ b/lib/crates/fabro-types/src/run_event/mod.rs +@@ -933,7 +933,7 @@ mod tests { + use super::*; + use crate::{ + AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings, +- fixtures, ++ fixtures, test_support as types_test_support, + }; + + fn user_principal(login: &str) -> Principal { +@@ -1017,7 +1017,8 @@ mod tests { + "graph": graph, + "labels": {}, + "run_dir": "/tmp/run", +- "source_directory": "/tmp/run" ++ "source_directory": "/tmp/run", ++ "provenance": types_test_support::test_run_provenance() + } + }); + +@@ -1038,7 +1039,8 @@ mod tests { + "labels": {}, + "run_dir": "/tmp/run", + "source_directory": "/tmp/run", +- "manifest_blob": RunBlobId::new(br#"{"version":1}"#).to_string() ++ "manifest_blob": RunBlobId::new(br#"{"version":1}"#).to_string(), ++ "provenance": types_test_support::test_run_provenance() + } + }); + +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..40130ed9c 100644 +--- a/lib/crates/fabro-types/src/run_projection.rs ++++ b/lib/crates/fabro-types/src/run_projection.rs +@@ -681,7 +681,7 @@ mod title_tests { + + use chrono::Utc; + +- use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings}; ++ use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support}; + + fn projection_with_goal(goal: Option<&str>) -> RunProjection { + let mut graph = Graph::new("test"); +@@ -700,7 +700,7 @@ mod title_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, +@@ -752,7 +752,7 @@ mod iter_stages_tests { + use serde_json::json; + + use super::RunProjection; +- use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings}; ++ use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings, test_support}; + + fn seq(n: u32) -> NonZeroU32 { + NonZeroU32::new(n).unwrap() +@@ -770,7 +770,7 @@ mod iter_stages_tests { + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs +index 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..04850ba68 100644 +--- a/lib/crates/fabro-types/tests/run_event_serde.rs ++++ b/lib/crates/fabro-types/tests/run_event_serde.rs +@@ -6,7 +6,20 @@ 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, Principal, RunProvenance, SystemActorKind, TurnId, WorkflowSettings, ++ fixtures, ++}; ++ ++fn test_run_provenance() -> RunProvenance { ++ RunProvenance { ++ server: None, ++ client: None, ++ subject: Principal::System { ++ system_kind: SystemActorKind::Engine, ++ }, ++ } ++} + + fn templated_settings() -> WorkflowSettings { + let mut settings = WorkflowSettings::default(); +@@ -32,7 +45,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_run_provenance(), + manifest_blob: None, + git: Some(GitContext { + origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), +@@ -97,7 +110,7 @@ fn run_created_props_omits_web_url_when_absent() { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -133,6 +146,7 @@ fn run_created_props_defaults_additive_fields_for_legacy_events() { + "title": null, + "settings": WorkflowSettings::default(), + "graph": Graph::new("ship"), ++ "provenance": test_run_provenance(), + "labels": {}, + "run_dir": "/tmp/run" + }); +diff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs +index b05dca05e..79b05186a 100644 +--- a/lib/crates/fabro-types/tests/run_spec_methods.rs ++++ b/lib/crates/fabro-types/tests/run_spec_methods.rs +@@ -3,7 +3,17 @@ 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::{Principal, RunProvenance, SystemActorKind, WorkflowSettings, fixtures}; ++ ++fn test_run_provenance() -> RunProvenance { ++ RunProvenance { ++ server: None, ++ client: None, ++ subject: Principal::System { ++ system_kind: SystemActorKind::Engine, ++ }, ++ } ++} + + fn sample_run_spec() -> RunSpec { + let settings = WorkflowSettings { +@@ -27,7 +37,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_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..afa3398ee 100644 +--- a/lib/crates/fabro-types/tests/run_spec_serde.rs ++++ b/lib/crates/fabro-types/tests/run_spec_serde.rs +@@ -4,7 +4,19 @@ 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, Principal, RunProvenance, SystemActorKind, WorkflowSettings, fixtures, ++}; ++ ++fn test_run_provenance() -> RunProvenance { ++ RunProvenance { ++ server: None, ++ client: None, ++ subject: Principal::System { ++ system_kind: SystemActorKind::Engine, ++ }, ++ } ++} + + fn templated_settings() -> WorkflowSettings { + let mut settings = WorkflowSettings::default(); +@@ -27,7 +39,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_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(GitContext { +@@ -80,7 +92,8 @@ fn run_spec_defaults_automation_for_legacy_specs() { + "run_id": fixtures::RUN_1, + "settings": WorkflowSettings::default(), + "graph": Graph::new("ship"), +- "labels": {} ++ "labels": {}, ++ "provenance": test_run_provenance() + }); + + let record: RunSpec = serde_json::from_value(json).expect("legacy spec should deserialize"); +diff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs +index d9e2867ab..b6e122af7 100644 +--- a/lib/crates/fabro-workflow/src/billing_rollup.rs ++++ b/lib/crates/fabro-workflow/src/billing_rollup.rs +@@ -166,6 +166,7 @@ mod tests { + use fabro_types::{ + AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion, + StageOutcome, WorkflowSettings, first_event_seq, fixtures, ++ test_support as types_test_support, + }; + + use super::billing_rollup_from_projection; +@@ -353,7 +354,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: 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..4f3a35450 100644 +--- a/lib/crates/fabro-workflow/src/event/sink.rs ++++ b/lib/crates/fabro-workflow/src/event/sink.rs +@@ -213,6 +213,7 @@ mod tests { + use std::sync::Arc; + + use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures}; ++ use fabro_types::test_support as types_test_support; + use tokio::sync::Mutex as AsyncMutex; + + use super::*; +@@ -244,7 +245,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..efe5bb514 100644 +--- a/lib/crates/fabro-workflow/src/git.rs ++++ b/lib/crates/fabro-workflow/src/git.rs +@@ -343,7 +343,9 @@ mod tests { + + use fabro_dump::RunDump; + use fabro_store::Database; +- use fabro_types::{CommandTermination, StageModelUsage, fixtures}; ++ use fabro_types::{ ++ CommandTermination, StageModelUsage, fixtures, test_support as types_test_support, ++ }; + use object_store::memory::InMemory; + + use super::*; +@@ -469,7 +471,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..4f2ab446e 100644 +--- a/lib/crates/fabro-workflow/src/handler/agent.rs ++++ b/lib/crates/fabro-workflow/src/handler/agent.rs +@@ -429,7 +429,7 @@ mod tests { + use fabro_graphviz::graph::AttrValue; + use fabro_model::{ReasoningEffort, Speed}; + use fabro_store::{Database, RunDatabase, StageId}; +- use fabro_types::fixtures; ++ use fabro_types::{fixtures, test_support as types_test_support}; + use object_store::memory::InMemory; + use tempfile::TempDir; + +@@ -484,7 +484,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..a74e8878d 100644 +--- a/lib/crates/fabro-workflow/src/handler/command.rs ++++ b/lib/crates/fabro-workflow/src/handler/command.rs +@@ -228,7 +228,10 @@ mod tests { + use bytes::Bytes; + use fabro_graphviz::graph::AttrValue; + use fabro_store::{Database, RunDatabase, StageId}; +- use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures}; ++ use fabro_types::{ ++ Graph, RunProjection, RunSpec, WorkflowSettings, fixtures, ++ test_support as types_test_support, ++ }; + use object_store::memory::InMemory; + use tokio::sync::Mutex; + +@@ -256,7 +259,7 @@ mod tests { + automation: None, + source_directory: None, + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: types_test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -357,7 +360,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..7d8d2d570 100644 +--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs ++++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs +@@ -1600,6 +1600,7 @@ mod tests { + use fabro_types::{ + EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, + RunPairStatusResponse, RunProjection, RunStatus, RunTimestamps, SuccessReason, WorkflowRef, ++ test_support as types_test_support, + }; + use fabro_vault::{SecretType, Vault}; + use futures::stream; +@@ -2133,7 +2134,7 @@ reasoning = false + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: 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..fb4406c48 100644 +--- a/lib/crates/fabro-workflow/src/handler/parallel.rs ++++ b/lib/crates/fabro-workflow/src/handler/parallel.rs +@@ -692,7 +692,7 @@ mod tests { + + use fabro_graphviz::graph::{AttrValue, Edge}; + use fabro_store::{Database, StageId}; +- use fabro_types::fixtures; ++ use fabro_types::{fixtures, test_support as types_test_support}; + use object_store::memory::InMemory; + + use super::*; +@@ -728,7 +728,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..43b155ed7 100644 +--- a/lib/crates/fabro-workflow/src/handler/prompt.rs ++++ b/lib/crates/fabro-workflow/src/handler/prompt.rs +@@ -225,7 +225,7 @@ mod tests { + use fabro_graphviz::graph::AttrValue; + use fabro_model::{ReasoningEffort, Speed}; + use fabro_store::{Database, RunDatabase, StageId}; +- use fabro_types::fixtures; ++ use fabro_types::{fixtures, test_support as types_test_support}; + use object_store::memory::InMemory; + use tempfile::TempDir; + +@@ -283,7 +283,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..5cb33b33d 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs +@@ -598,7 +598,10 @@ mod tests { + use fabro_model::Catalog; + use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection}; + use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; +- use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures}; ++ use fabro_types::{ ++ EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures, ++ test_support as types_test_support, ++ }; + use object_store::memory::InMemory; + + use super::*; +@@ -736,7 +739,7 @@ mod tests { + workflow_slug: Some("metadata".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..e6bda8f22 100644 +--- a/lib/crates/fabro-workflow/src/operations/archive.rs ++++ b/lib/crates/fabro-workflow/src/operations/archive.rs +@@ -136,7 +136,10 @@ mod tests { + use std::time::Duration; + + use fabro_store::Database; +- use fabro_types::{FailureReason, RunId, SuccessReason, TerminalStatus, fixtures}; ++ use fabro_types::{ ++ FailureReason, RunId, SuccessReason, TerminalStatus, fixtures, ++ test_support as types_test_support, ++ }; + use object_store::memory::InMemory; + + use super::*; +@@ -226,7 +229,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..da2ce241a 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, + } +@@ -422,7 +422,7 @@ mod tests { + use fabro_store::Database; + use fabro_types::settings::InterpString; + use fabro_types::settings::run::RunMode; +- use fabro_types::{WorkflowSettings, fixtures}; ++ use fabro_types::{WorkflowSettings, fixtures, test_support as types_test_support}; + use fabro_util::error::collect_chain; + use fabro_validate::Severity; + use object_store::local::LocalFileSystem; +@@ -1115,7 +1115,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: 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: 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: 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: 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: 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..eeae16ba0 100644 +--- a/lib/crates/fabro-workflow/src/operations/fork.rs ++++ b/lib/crates/fabro-workflow/src/operations/fork.rs +@@ -284,7 +284,7 @@ mod tests { + + use fabro_graphviz::graph::Graph; + use fabro_store::{Database, RunProjectionReducer}; +- use fabro_types::{StageId, WorkflowSettings, fixtures}; ++ use fabro_types::{StageId, WorkflowSettings, fixtures, test_support as types_test_support}; + use object_store::memory::InMemory; + + use super::*; +@@ -383,7 +383,7 @@ mod tests { + workflow_slug: Some("fork-source".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..15f0e9361 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, + } + +@@ -123,6 +123,7 @@ mod tests { + AuthMethod, DirtyStatus, FailureReason, ForkSourceRef, GitContext, Graph, IdpIdentity, + PreRunPushOutcome, Principal, PullRequestLink, RunBlobId, RunRunnableSource, + RunServerProvenance, RunTiming, UserPrincipal, WorkflowSettings, fixtures, ++ test_support as types_test_support, + }; + use object_store::memory::InMemory; + +@@ -152,7 +153,7 @@ mod tests { + version: "test".to_string(), + }), + client: None, +- subject: Some(actor(login)), ++ subject: actor(login), + } + } + +@@ -191,7 +192,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 +369,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 +403,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 +457,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 +511,7 @@ mod tests { + let err = retry_run(&store, &RetryRunInput { + source_run_id: run_id, + new_run_id: RunId::new(), +- provenance: None, ++ provenance: types_test_support::test_run_provenance(), + web_url: None, + }) + .await +@@ -535,7 +529,7 @@ mod tests { + let err = retry_run(&store, &RetryRunInput { + source_run_id: fixtures::RUN_1, + new_run_id: RunId::new(), +- provenance: None, ++ provenance: types_test_support::test_run_provenance(), + 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..230a9d8ce 100644 +--- a/lib/crates/fabro-workflow/src/operations/start.rs ++++ b/lib/crates/fabro-workflow/src/operations/start.rs +@@ -1129,7 +1129,10 @@ mod tests { + use fabro_store::Database; + use fabro_types::settings::run::RunMode; + use fabro_types::settings::{InterpString, ModelRef}; +- use fabro_types::{BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures}; ++ use fabro_types::{ ++ BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures, ++ test_support as types_test_support, ++ }; + use object_store::memory::InMemory; + + use super::*; +@@ -1438,7 +1441,7 @@ reasoning = false + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: types_test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1860,7 +1863,7 @@ reasoning = false + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: 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..c9ea7110f 100644 +--- a/lib/crates/fabro-workflow/src/operations/timeline.rs ++++ b/lib/crates/fabro-workflow/src/operations/timeline.rs +@@ -204,6 +204,7 @@ mod tests { + use chrono::Utc; + use fabro_types::{ + Checkpoint, CheckpointRecord, Graph, RunDiff, RunSpec, WorkflowSettings, fixtures, ++ test_support as types_test_support, + }; + + use super::*; +@@ -248,7 +249,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: 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..bd42ba7ac 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +@@ -18,7 +18,10 @@ use fabro_interview::AutoApproveInterviewer; + use fabro_sandbox::SandboxSpec; + use fabro_store::Database; + use fabro_types::settings::run::RunModelControls; +-use fabro_types::{Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref}; ++use fabro_types::{ ++ Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref, ++ test_support as types_test_support, ++}; + use object_store::memory::InMemory; + + use super::*; +@@ -165,7 +168,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: types_test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -208,7 +211,7 @@ async fn seed_created_and_starting( + workflow_slug: run_options.workflow_slug.clone(), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..4b9c68589 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs +@@ -651,7 +651,7 @@ mod tests { + use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase}; + use fabro_types::{ + BilledTokenCounts, EventBody, RunBlobId, RunEvent, RunId, RunSpec, StageCompletion, +- WorkflowSettings, first_event_seq, fixtures, ++ WorkflowSettings, first_event_seq, fixtures, test_support as types_test_support, + }; + use object_store::memory::InMemory; + +@@ -739,7 +739,7 @@ mod tests { + workflow_slug: Some("metadata".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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: 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..04cd78793 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs +@@ -648,7 +648,9 @@ mod tests { + use fabro_sandbox::SandboxSpec; + use fabro_store::Database; + use fabro_types::settings::run::RunModelControls; +- use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures}; ++ use fabro_types::{ ++ EventBody, RunEvent, RunId, WorkflowSettings, fixtures, test_support as types_test_support, ++ }; + use fabro_vault::{SecretType, Vault}; + use object_store::memory::InMemory; + use tokio::fs::{create_dir_all, write}; +@@ -773,7 +775,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: 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..2da6979f9 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs +@@ -59,7 +59,7 @@ mod tests { + + use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node}; + use fabro_store::{Database, RunDatabase}; +- use fabro_types::fixtures; ++ use fabro_types::{fixtures, test_support as types_test_support}; + use object_store::memory::InMemory; + + use super::*; +@@ -148,7 +148,7 @@ mod tests { + ("env".to_string(), "test".to_string()), + ("team".to_string(), "workflow".to_string()), + ]), +- provenance: None, ++ provenance: 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..f5987aaae 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +@@ -680,7 +680,7 @@ mod tests { + use fabro_store::Database; + use fabro_types::{ + BilledTokenCounts, RunProjection, RunSpec, SuccessReason, WorkflowSettings, +- first_event_seq, fixtures, ++ first_event_seq, fixtures, test_support as types_test_support, + }; + use fabro_vault::{SecretType, Vault}; + use futures::stream; +@@ -823,7 +823,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: 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: 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: 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: 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: 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: types_test_support::test_run_provenance(), + 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: 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: types_test_support::test_run_provenance(), + 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..c1560d6ac 100644 +--- a/lib/crates/fabro-workflow/src/run_lookup.rs ++++ b/lib/crates/fabro-workflow/src/run_lookup.rs +@@ -457,7 +457,7 @@ mod tests { + + use fabro_graphviz::graph::Graph; + use fabro_store::Database; +- use fabro_types::{RunStatus, WorkflowSettings, fixtures}; ++ use fabro_types::{RunStatus, WorkflowSettings, fixtures, test_support as types_test_support}; + use object_store::memory::InMemory; + + use super::scan_runs_combined; +@@ -491,7 +491,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: 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..88231af66 100644 +--- a/lib/crates/fabro-workflow/src/run_metadata.rs ++++ b/lib/crates/fabro-workflow/src/run_metadata.rs +@@ -537,7 +537,10 @@ mod tests { + use std::sync::Arc; + + use fabro_store::RunProjection; +- use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings}; ++ use fabro_types::{ ++ DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings, ++ test_support as types_test_support, ++ }; + use git2::{ErrorClass, ErrorCode}; + + use super::*; +@@ -639,7 +642,7 @@ mod tests { + push_outcome: PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: 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..c2a2eb21b 100644 +--- a/lib/crates/fabro-workflow/src/runtime_store.rs ++++ b/lib/crates/fabro-workflow/src/runtime_store.rs +@@ -120,7 +120,9 @@ mod tests { + use fabro_graphviz::graph::Graph; + use fabro_store::Database; + use fabro_types::run_event::RunSubmittedProps; +- use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures}; ++ use fabro_types::{ ++ EventBody, RunEvent, WorkflowSettings, fixtures, test_support as types_test_support, ++ }; + use object_store::memory::InMemory; + + use super::RunStoreHandle; +@@ -148,7 +150,7 @@ mod tests { + source_directory: Some("/tmp/test".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: types_test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -170,7 +172,7 @@ mod tests { + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: 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..1acf410a4 100644 +--- a/lib/crates/fabro-workflow/src/test_support.rs ++++ b/lib/crates/fabro-workflow/src/test_support.rs +@@ -175,7 +175,13 @@ async fn initialized( + workflow_slug: run_options.workflow_slug.clone(), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::RunProvenance { ++ server: None, ++ client: None, ++ subject: fabro_types::Principal::System { ++ system_kind: fabro_types::SystemActorKind::Engine, ++ }, ++ }, + manifest_blob: None, + git: run_options.pre_run_git.clone(), + fork_source_ref: run_options.fork_source_ref.clone(), +diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +index 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..31263d33e 100644 +--- a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts ++++ b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts +@@ -1,4 +1,4 @@ +-import type { AutomationRef, Principal, PrincipalSystem, RunSpec } from "../src"; ++import type { AutomationRef, Principal, PrincipalSystem, Run, RunSpec } from "../src"; + + type AssertFalse = T; + type AssertExtends = true; +@@ -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": +@@ -42,12 +40,16 @@ export function systemKind(principal: PrincipalSystem): string { + } + } + +-type Provenance = NonNullable; +-type Subject = NonNullable; ++type Provenance = RunSpec["provenance"]; ++type Subject = Provenance["subject"]; ++type Creator = Run["created_by"]; + + type SubjectIsNotAny = AssertFalse>; + type SubjectExtendsPrincipal = AssertExtends; + type PrincipalExtendsSubject = AssertExtends; ++type CreatorIsNotAny = AssertFalse>; ++type CreatorExtendsPrincipal = AssertExtends; ++type PrincipalExtendsCreator = AssertExtends; + + type Automation = NonNullable; + type AutomationExtendsRef = AssertExtends; +@@ -69,6 +71,9 @@ const _automationTriggerId: AutomationTriggerId = "schedule_1"; + void (null as unknown as SubjectIsNotAny); + void (null as unknown as SubjectExtendsPrincipal); + void (null as unknown as PrincipalExtendsSubject); ++void (null as unknown as CreatorIsNotAny); ++void (null as unknown as CreatorExtendsPrincipal); ++void (null as unknown as PrincipalExtendsCreator); + void (null as unknown as AutomationExtendsRef); + void _principalSubject; + void _automation; diff --git a/stages/005-implement@1/status.json b/stages/005-implement@1/status.json new file mode 100644 index 000000000..b74569f86 --- /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-03T17:38:19.283757Z" +} \ 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..3e259c7ae --- /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, 5.7m tokens in / 75.3k 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..dac941304 --- /dev/null +++ b/stages/006-simplify_opus@1/response.md @@ -0,0 +1,28 @@ +## Summary + +Three review agents (reuse, quality, efficiency) inspected the two-commit change (Anonymous principal removal + total run provenance). Applied fixes: + +**Efficiency** +- `apps/fabro-web/app/components/runs-list/run-table-row.tsx` — `principalDisplay(run.createdBy)` was being computed on every row render even when the `created_by` column was hidden. Moved the call inside the `show("created_by") &&` guard. + +**Quality** +- `lib/crates/fabro-server/src/principal_middleware.rs` — `principal_without_log_unused_fields` had an awkward `Option<&Principal> → Option` signature with redundant `None`/`Some(clone)` arms. Simplified to `&Principal → Principal` and let the caller `.as_ref().map(...)`. +- `apps/fabro-web/app/components/run-summary-panel.test.tsx` — Test "shows unavailable copy for missing run fields after load" asserted "Created by" rendered "Ttest", which is not an unavailable copy. Split into a separate `renders creator from run.created_by` test. +- `apps/fabro-web/app/components/run-summary-panel.tsx` — Removed dead `created ? ... : ` branch that suggested `created_by` could still be null after a run loads; now skeleton while loading, principal display once loaded. + +**Reuse** +- `lib/crates/fabro-types/Cargo.toml` — Added self dev-dep `fabro-types = { path = ".", features = ["test-support"] }` to make the new `test_support::test_run_provenance()` available to integration tests. +- `lib/crates/fabro-types/tests/{run_event_serde,run_spec_serde,run_spec_methods}.rs` — Deleted three byte-identical local `test_run_provenance()` copies and the now-unused `Principal`/`RunProvenance`/`SystemActorKind` imports; all three now call the canonical helper. + +**Skipped (false positive or out of scope)** +- Demo principal `LazyLock` (already idiomatic; clone is unavoidable given owned `Run.created_by`). +- Gate-helper boilerplate consolidation in `principal_middleware.rs` (pre-existing pattern, out of scope). +- `fabro-workflow/src/test_support.rs` inline `RunProvenance` literal (its `test_support` module is not feature-gated, so it cannot depend on `fabro-types`'s feature-gated helper without a broader refactor). +- `cmd/inspect.rs` inline JSON principal (low value). + +**Verification** +- `cargo +nightly-2026-04-14 fmt --check --all` ✅ +- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` ✅ +- `cargo nextest run -p fabro-types -p fabro-server` (1036 + server tests passed) ✅ +- `cd apps/fabro-web && bun run typecheck` ✅ +- `cd apps/fabro-web && bun test` (509 passed) ✅ \ No newline at end of file