From aadf63f449a68ef7f6698ccdf5f8d6a46a9c19ad Mon Sep 17 00:00:00 2001 From: Fabro Date: Wed, 3 Jun 2026 10:02:56 -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 | 451 ++- stages/006-simplify_opus@1/diff.patch | 2582 ++++++++++++++++++ stages/006-simplify_opus@1/status.json | 6 + stages/007-simplify_gpt@1/prompt.md | 287 ++ stages/007-simplify_gpt@1/provider_used.json | 5 + 5 files changed, 3303 insertions(+), 28 deletions(-) create mode 100644 stages/006-simplify_opus@1/diff.patch create mode 100644 stages/006-simplify_opus@1/status.json create mode 100644 stages/007-simplify_gpt@1/prompt.md create mode 100644 stages/007-simplify_gpt@1/provider_used.json diff --git a/run.json b/run.json index c177ac177..e9c14086d 100644 --- a/run.json +++ b/run.json @@ -505,7 +505,7 @@ "kind": "running" }, "status_updated_at": "2026-06-03T12:51:16.556285Z", - "last_event_at": "2026-06-03T14:02:48.672607Z", + "last_event_at": "2026-06-03T14:02:55.253396Z", "pending_control": null, "checkpoints": [ { @@ -904,9 +904,9 @@ } }, { - "seq": 0, + "seq": 1486, "checkpoint": { - "timestamp": "2026-06-03T14:02:48.741877Z", + "timestamp": "2026-06-03T14:02:54.927678Z", "current_node": "simplify_opus", "completed_nodes": [ "start", @@ -918,35 +918,53 @@ ], "node_retries": {}, "context_values": { - "internal.thread_id": "implement", - "thread.preflight_compile.current_node": "preflight_lint", - "internal.node_visit_count": 1, - "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", - "current_node": "simplify_opus", - "internal.work_dir": "/home/daytona/workspace/fabro", - "internal.fidelity": "compact", - "graph.rankdir": "LR", - "internal.retry_count.preflight_lint": 0, - "internal.retry_count.toolchain": 0, + "internal.retry_count.implement": 0, + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option`, no nullable OpenAPI fields, no legacy deserialization defaults, and no fallback creator in projection code.\n\nTwo commits, in order.\n\n---\n\n## Commit 1 - Remove `Principal::Anonymous`\n\nBreaking cleanup. `Principal` becomes actor-only. Missing/invalid auth is represented as absent request principal, not as an anonymous principal variant.\n\n### Rust\n\n`lib/crates/fabro-types/src/principal.rs`:\n- Drop `Anonymous`.\n- Drop `Anonymous` arms in `kind()` and `display()`.\n- Delete anonymous serialization/round-trip test coverage.\n\n`lib/crates/fabro-server/src/principal_middleware.rs`:\n- `RequestAuthContext.principal: Principal` -> `Option`.\n- `RequestAuthLogContext.principal: Principal` -> `Option`.\n- `initial()` and `rejected()` set `principal: None`.\n- `authenticated(...)`, `authenticated_worker(...)`, and `authenticated_user(...)` set `principal: Some(...)`.\n- Update `principal_without_log_unused_fields` to preserve `None` and strip user avatar data only inside `Some(Principal::User(...))`.\n- Update all gate helpers to match `Option`:\n - `require_user`\n - `require_authenticated_user`\n - `require_run_management_actor`\n - `require_worker_or_user_for_run`\n - `require_run_management_target`\n- `None` routes to the existing `auth_rejection(context.auth_status, context.auth_error_code)` behavior.\n- `Some(Principal::Worker { .. })` keeps the current forbidden-vs-auth-rejection distinctions.\n- Update tests that assert the initial/rejected principal to assert `None`.\n\n`lib/crates/fabro-server/src/server.rs` HTTP logging:\n- Keep the `principal_kind` field on every HTTP log line.\n- Compute `principal_kind` as `auth_context.principal.as_ref().map(Principal::kind).unwrap_or(\"none\")`.\n- Match `auth_context.principal` as an `Option`:\n - `Some(User(...))`, `Some(Worker { ... })`, `Some(Webhook { ... })`, `Some(Slack { ... })` keep their extra fields.\n - `None | Some(Agent { .. } | System { .. })` emits only the common HTTP fields.\n\n`docs/internal/logging-strategy.md`:\n- Replace the `anonymous` HTTP caller category guidance with `none` for requests that have no principal.\n- Keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state.\n\n### OpenAPI and generated clients\n\n`docs/public/api-reference/fabro-api.yaml`:\n- Remove `PrincipalAnonymous` from the `Principal` `oneOf`.\n- Remove `anonymous` from the `Principal` discriminator mapping.\n- Delete the `PrincipalAnonymous` schema.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nExpected generated cleanup:\n- `lib/packages/fabro-api-client/src/models/principal-anonymous.ts` disappears.\n- `Principal` union no longer includes `{ kind: \"anonymous\" }`.\n- `lib/packages/fabro-api-client/src/models/index.ts` no longer exports `principal-anonymous`.\n\n### Frontend\n\n`apps/fabro-web/app/lib/principal-display.tsx`:\n- Remove the `\"anonymous\"` switch case and unused icon import.\n\n`apps/fabro-web/app/components/run-summary-panel.test.tsx` and API-client exhaustiveness tests:\n- Remove anonymous principal cases.\n\n### Documentation sweep\n\nRemove anonymous-principal references from product/API docs and tests. Be careful not to touch unrelated uses of \"anonymous\" such as telemetry anonymous IDs or Git's `remote_anonymous` API.\n\nUseful sweep:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|kind: 'anonymous'|kind: \\\"anonymous\\\"|anonymous actor|anonymous subject|principal_kind.*anonymous|\\\"anonymous\\\"\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cd apps/fabro-web && bun run typecheck && bun test`\n- Manual: start `fabro server start`, hit a protected endpoint without a token, confirm 401 and an HTTP log with `principal_kind=\"none\"` and `auth_status=\"missing\"`.\n\n---\n\n## Commit 2 - Make run provenance and creator non-optional\n\nFull-chain invariant. Every persisted run has exactly one creator principal. No nullable schema fields, no legacy defaults, no projection fallbacks.\n\n### Core type changes\n\n`lib/crates/fabro-types/src/run_summary.rs`:\n- `Run.created_by: Option` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option` -> `RunProvenance`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]` on `RunSpec.provenance`.\n\n`lib/crates/fabro-types/src/run_event/run.rs`:\n- `RunCreatedProps.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n`lib/crates/fabro-workflow/src/event/events.rs`:\n- `Event::RunCreated.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n### Creation and retry flow\n\n`lib/crates/fabro-workflow/src/operations/create.rs`:\n- `CreateRunInput.provenance: Option` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option` -> `RunProvenance`.\n- `RunSpec { provenance }` stores the total provenance directly.\n- `Event::RunCreated { provenance }` emits total provenance directly.\n\n`lib/crates/fabro-server/src/server/handler/runs.rs`:\n- `run_provenance(headers, subject)` returns `RunProvenance { subject: subject.clone(), ... }`.\n- Build provenance before creating `CreateRunInput`.\n\n`lib/crates/fabro-server/src/run_manifest.rs`:\n- Change `create_run_input(...)` to accept `provenance: RunProvenance` and set it directly, or stop using the helper for the final `CreateRunInput` construction. Do not create a temporary input with missing provenance.\n\n`lib/crates/fabro-workflow/src/operations/retry.rs`:\n- `RetryRunInput.provenance: Option` -> `RunProvenance`.\n- `retry_run(...)` writes the new run's `run.created` event with total provenance.\n\n`lib/crates/fabro-server/src/server/handler/lifecycle.rs`:\n- Pass `run_provenance(&headers, &actor)` directly into `RetryRunInput`.\n\n### Event conversion and projections\n\n`lib/crates/fabro-workflow/src/event/convert.rs`:\n- Convert `Event::RunCreated.provenance` into `RunCreatedProps.provenance` directly.\n- Remove `Some(...)` wrapping for run-created provenance.\n\n`lib/crates/fabro-workflow/src/event/stored_fields.rs`:\n- `Event::RunCreated { provenance, .. }` sets `actor: Some(provenance.subject.clone())`.\n\n`lib/crates/fabro-store/src/run_state.rs`:\n- `projection_from_created(...)` builds `RunSpec { provenance: props.provenance.clone(), ... }`.\n- `build_summary(...)` sets `created_by: state.spec.provenance.subject.clone()`.\n- Delete or rewrite tests that deserialize projections with `\"provenance\": null`.\n\n`lib/crates/fabro-types/src/run_projection.rs` and projection tests:\n- Replace all test `RunSpec` literals with total provenance.\n- Remove tests whose only purpose is legacy/null provenance tolerance.\n\n### OpenAPI\n\n`docs/public/api-reference/fabro-api.yaml`:\n- `Run.created_by` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunProvenance.required` includes `subject`.\n- `RunProvenance.subject` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunSpec.required` includes `provenance`.\n- `RunSpec.provenance` references `RunProvenance` directly. Remove `oneOf [..., null]`.\n- If `run.created` event properties are represented separately in the spec, make that event provenance required and non-nullable too.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nDo not hand-edit generated client files.\n\n### Demo mode\n\n`lib/crates/fabro-server/src/demo/mod.rs`:\n- Add a clearly synthetic demo principal using `AuthMethod::DevToken`, not GitHub:\n ```rust\n static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| {\n Principal::user(\n IdpIdentity::new(\"fabro:demo\", \"demo\").unwrap(),\n \"demo\".to_string(),\n AuthMethod::DevToken,\n )\n });\n ```\n- Replace `created_by: None` with `created_by: DEMO_PRINCIPAL.clone()`.\n- If demo creates any full `RunSpec` or `run.created` event data, give it `RunProvenance { subject: DEMO_PRINCIPAL.clone(), ... }`.\n\n### Test support\n\nDo not add fake auth helpers to `fabro_types::fixtures`; that module is run-id constants.\n\nUse the existing `fabro-types` `test-support` feature:\n- Add `#[cfg(any(test, feature = \"test-support\"))] pub mod test_support;` in `lib/crates/fabro-types/src/lib.rs` if it does not already exist.\n- Add `lib/crates/fabro-types/src/test_support.rs` with:\n - `test_principal() -> Principal`\n - `test_run_provenance() -> RunProvenance`\n- Use an obviously fake dev-token identity, e.g. issuer `fabro:test`, subject `test-user`, login `test`.\n- In crates that need the helper from integration tests or cross-crate tests, dual-list `fabro-types` in `dev-dependencies` with `features = [\"test-support\"]`, following existing repo patterns.\n\nUpdate all constructors:\n- Replace `provenance: None` in `RunSpec`, `CreateRunInput`, `RetryRunInput`, `Event::RunCreated`, and `RunCreatedProps` literals with `test_run_provenance()` or a locally meaningful provenance.\n- Replace `subject: Some(...)` with `subject: ...`.\n- Replace `subject: None` only when it is actually `RunProvenance.subject`; leave unrelated todo/commit/message `subject` fields alone.\n- Replace `created_by: None` / `created_by: null` with `test_principal()` or a frontend TS principal fixture.\n- Delete tests that assert nullable or omitted creator/provenance behavior.\n\nRepresentative Rust areas:\n- `lib/crates/fabro-store/src/run_state.rs`\n- `lib/crates/fabro-store/tests/serializable_projection.rs`\n- `lib/crates/fabro-workflow/src/operations/{create,retry,start}.rs`\n- `lib/crates/fabro-workflow/src/event/{convert,sink,stored_fields}.rs`\n- `lib/crates/fabro-workflow/src/handler/**`\n- `lib/crates/fabro-workflow/src/pipeline/**`\n- `lib/crates/fabro-workflow/src/run_{lookup,metadata}.rs`\n- `lib/crates/fabro-server/src/server/tests.rs`\n- `lib/crates/fabro-server/src/server/handler/**`\n- `lib/crates/fabro-server/tests/it/**`\n- `lib/crates/fabro-cli/tests/it/support/mod.rs`\n- `lib/crates/fabro-dump/src/lib.rs`\n- `lib/crates/fabro-tool/src/{common,create,interact,search}.rs`\n- `lib/crates/fabro-api/tests/{principal_round_trip,run_summary_round_trip,run_projection_round_trip,run_event_round_trip}.rs`\n- `lib/crates/fabro-types/tests/{run_spec_serde,run_spec_methods,run_event_serde}.rs`\n\nRepresentative TypeScript areas:\n- `apps/fabro-web/app/**` tests with `created_by: null`\n- `apps/fabro-web/app/data/runs.ts`\n- `apps/fabro-web/app/components/run-summary-panel.tsx`\n- `apps/fabro-web/app/components/runs-list/**`\n- `lib/packages/fabro-api-client/tests/principal-exhaustive.ts`\n\nUseful sweep after edits:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|principal-anonymous|kind: ['\\\"]anonymous|created_by:\\\\s*(None|null)|provenance:\\\\s*None|subject:\\\\s*Some\\\\(|subject:\\\\s*None\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\nReview each hit. The only acceptable remaining matches should be unrelated uses of \"anonymous\" and unrelated non-principal `subject` fields.\n\n### Frontend\n\n`apps/fabro-web/app/components/run-summary-panel.tsx`:\n- `run?.created_by` may still be guarded by `run` loading state, but `created_by` itself is non-null once `run` exists.\n- Pass `run.created_by` directly to `principalDisplay(...)` inside loaded-run branches.\n\n`apps/fabro-web/app/data/runs.ts` and run-list components:\n- Treat `createdBy` as a total principal in UI data derived from a loaded API run.\n- Remove empty/fallback rendering that only existed for missing creator data.\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Manual end-to-end:\n - `fabro server start`\n - `cd apps/fabro-web && bun run dev`\n - Authenticate and create a run through the UI.\n - Confirm `/api/v1/runs/:id` has non-null `created_by`.\n - Confirm `/api/v1/runs/:id/state` has non-null `spec.provenance.subject`.\n - Retry a failed run and confirm the retried run has the retrying user as creator.\n - Hit demo mode with `X-Fabro-Demo: 1` and confirm the run summary renders the synthetic `demo` dev-token user.\n", + "internal.retry_count.preflight_compile": 0, + "internal.retry_count.simplify_opus": 0, "response.simplify_opus": "Both commits are done. Summary:\n\n**Commit 1** (`refactor: remove Principal::Anonymous, use Option for absent auth`): Dropped `Principal::Anonymous` variant. Auth middleware now uses `Option` for request context. HTTP logs emit `principal_kind=\"none\"` for unauthenticated requests. Updated OpenAPI spec, regenerated TS client, removed frontend anonymous icon/case.\n\n**Commit 2** (`refactor: make Run.created_by, RunSpec.provenance, and RunProvenance.subject total`): Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, `RunCreatedProps.provenance`, and `Event::RunCreated.provenance` non-optional. Threaded actual `RunProvenance` through `CreateRunInput`, `RetryRunInput`, and `PersistCreateOptions`. Added `fabro_types::test_support::{test_principal, test_run_provenance}` behind the existing `test-support` feature for tests. Added a clearly synthetic `DEMO_PRINCIPAL` (issuer `fabro:demo`, dev-token) in demo mode. Updated OpenAPI spec to require `subject`/`provenance`/`created_by`. Regenerated the TS client and updated the frontend to treat `createdBy` as total inside loaded-run branches. Backward-compatibility tests for null provenance/creator were either deleted or updated to include the total provenance.\n\nVerification:\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` — 6499 passed, 0 failed, 181 skipped ✓\n- `cd apps/fabro-web && bun run typecheck` ✓\n- `cd apps/fabro-web && bun test` — 508 passed, 0 failed ✓\n- `cd apps/fabro-web && bun run build` ✓\n- `cd lib/packages/fabro-api-client && bun run typecheck` ✓\n\nManual end-to-end verification (server start, UI run creation, demo header) was not exercised in this run.", - "outcome": "succeeded", + "internal.fidelity": "compact", + "thread.preflight_compile.current_node": "preflight_lint", + "thread.preflight_lint.current_node": "implement", "internal.run_id": "01KT6RK0RM223N7PJQ47XXTKVJ", - "thread.toolchain.current_node": "preflight_compile", - "last_stage": "simplify_opus", + "internal.retry_count.toolchain": 0, + "current_node": "simplify_opus", + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", "failure_signature": "", "internal.retry_count.start": 0, - "internal.retry_count.preflight_compile": 0, - "last_response": "Both commits are done. Summary:\n\n**Commit 1** (`refactor: remove Principal::Anonymous, use Option for absent auth`): Dropped `Principal::Anonymous` variant. Auth middleware now uses `Option", "failure_class": "", - "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", - "thread.preflight_lint.current_node": "implement", - "thread.implement.current_node": "simplify_opus", - "internal.retry_count.implement": 0, "thread.start.current_node": "toolchain", - "graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option`, no nullable OpenAPI fields, no legacy deserialization defaults, and no fallback creator in projection code.\n\nTwo commits, in order.\n\n---\n\n## Commit 1 - Remove `Principal::Anonymous`\n\nBreaking cleanup. `Principal` becomes actor-only. Missing/invalid auth is represented as absent request principal, not as an anonymous principal variant.\n\n### Rust\n\n`lib/crates/fabro-types/src/principal.rs`:\n- Drop `Anonymous`.\n- Drop `Anonymous` arms in `kind()` and `display()`.\n- Delete anonymous serialization/round-trip test coverage.\n\n`lib/crates/fabro-server/src/principal_middleware.rs`:\n- `RequestAuthContext.principal: Principal` -> `Option`.\n- `RequestAuthLogContext.principal: Principal` -> `Option`.\n- `initial()` and `rejected()` set `principal: None`.\n- `authenticated(...)`, `authenticated_worker(...)`, and `authenticated_user(...)` set `principal: Some(...)`.\n- Update `principal_without_log_unused_fields` to preserve `None` and strip user avatar data only inside `Some(Principal::User(...))`.\n- Update all gate helpers to match `Option`:\n - `require_user`\n - `require_authenticated_user`\n - `require_run_management_actor`\n - `require_worker_or_user_for_run`\n - `require_run_management_target`\n- `None` routes to the existing `auth_rejection(context.auth_status, context.auth_error_code)` behavior.\n- `Some(Principal::Worker { .. })` keeps the current forbidden-vs-auth-rejection distinctions.\n- Update tests that assert the initial/rejected principal to assert `None`.\n\n`lib/crates/fabro-server/src/server.rs` HTTP logging:\n- Keep the `principal_kind` field on every HTTP log line.\n- Compute `principal_kind` as `auth_context.principal.as_ref().map(Principal::kind).unwrap_or(\"none\")`.\n- Match `auth_context.principal` as an `Option`:\n - `Some(User(...))`, `Some(Worker { ... })`, `Some(Webhook { ... })`, `Some(Slack { ... })` keep their extra fields.\n - `None | Some(Agent { .. } | System { .. })` emits only the common HTTP fields.\n\n`docs/internal/logging-strategy.md`:\n- Replace the `anonymous` HTTP caller category guidance with `none` for requests that have no principal.\n- Keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state.\n\n### OpenAPI and generated clients\n\n`docs/public/api-reference/fabro-api.yaml`:\n- Remove `PrincipalAnonymous` from the `Principal` `oneOf`.\n- Remove `anonymous` from the `Principal` discriminator mapping.\n- Delete the `PrincipalAnonymous` schema.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nExpected generated cleanup:\n- `lib/packages/fabro-api-client/src/models/principal-anonymous.ts` disappears.\n- `Principal` union no longer includes `{ kind: \"anonymous\" }`.\n- `lib/packages/fabro-api-client/src/models/index.ts` no longer exports `principal-anonymous`.\n\n### Frontend\n\n`apps/fabro-web/app/lib/principal-display.tsx`:\n- Remove the `\"anonymous\"` switch case and unused icon import.\n\n`apps/fabro-web/app/components/run-summary-panel.test.tsx` and API-client exhaustiveness tests:\n- Remove anonymous principal cases.\n\n### Documentation sweep\n\nRemove anonymous-principal references from product/API docs and tests. Be careful not to touch unrelated uses of \"anonymous\" such as telemetry anonymous IDs or Git's `remote_anonymous` API.\n\nUseful sweep:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|kind: 'anonymous'|kind: \\\"anonymous\\\"|anonymous actor|anonymous subject|principal_kind.*anonymous|\\\"anonymous\\\"\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cd apps/fabro-web && bun run typecheck && bun test`\n- Manual: start `fabro server start`, hit a protected endpoint without a token, confirm 401 and an HTTP log with `principal_kind=\"none\"` and `auth_status=\"missing\"`.\n\n---\n\n## Commit 2 - Make run provenance and creator non-optional\n\nFull-chain invariant. Every persisted run has exactly one creator principal. No nullable schema fields, no legacy defaults, no projection fallbacks.\n\n### Core type changes\n\n`lib/crates/fabro-types/src/run_summary.rs`:\n- `Run.created_by: Option` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option` -> `RunProvenance`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]` on `RunSpec.provenance`.\n\n`lib/crates/fabro-types/src/run_event/run.rs`:\n- `RunCreatedProps.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n`lib/crates/fabro-workflow/src/event/events.rs`:\n- `Event::RunCreated.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n### Creation and retry flow\n\n`lib/crates/fabro-workflow/src/operations/create.rs`:\n- `CreateRunInput.provenance: Option` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option` -> `RunProvenance`.\n- `RunSpec { provenance }` stores the total provenance directly.\n- `Event::RunCreated { provenance }` emits total provenance directly.\n\n`lib/crates/fabro-server/src/server/handler/runs.rs`:\n- `run_provenance(headers, subject)` returns `RunProvenance { subject: subject.clone(), ... }`.\n- Build provenance before creating `CreateRunInput`.\n\n`lib/crates/fabro-server/src/run_manifest.rs`:\n- Change `create_run_input(...)` to accept `provenance: RunProvenance` and set it directly, or stop using the helper for the final `CreateRunInput` construction. Do not create a temporary input with missing provenance.\n\n`lib/crates/fabro-workflow/src/operations/retry.rs`:\n- `RetryRunInput.provenance: Option` -> `RunProvenance`.\n- `retry_run(...)` writes the new run's `run.created` event with total provenance.\n\n`lib/crates/fabro-server/src/server/handler/lifecycle.rs`:\n- Pass `run_provenance(&headers, &actor)` directly into `RetryRunInput`.\n\n### Event conversion and projections\n\n`lib/crates/fabro-workflow/src/event/convert.rs`:\n- Convert `Event::RunCreated.provenance` into `RunCreatedProps.provenance` directly.\n- Remove `Some(...)` wrapping for run-created provenance.\n\n`lib/crates/fabro-workflow/src/event/stored_fields.rs`:\n- `Event::RunCreated { provenance, .. }` sets `actor: Some(provenance.subject.clone())`.\n\n`lib/crates/fabro-store/src/run_state.rs`:\n- `projection_from_created(...)` builds `RunSpec { provenance: props.provenance.clone(), ... }`.\n- `build_summary(...)` sets `created_by: state.spec.provenance.subject.clone()`.\n- Delete or rewrite tests that deserialize projections with `\"provenance\": null`.\n\n`lib/crates/fabro-types/src/run_projection.rs` and projection tests:\n- Replace all test `RunSpec` literals with total provenance.\n- Remove tests whose only purpose is legacy/null provenance tolerance.\n\n### OpenAPI\n\n`docs/public/api-reference/fabro-api.yaml`:\n- `Run.created_by` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunProvenance.required` includes `subject`.\n- `RunProvenance.subject` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunSpec.required` includes `provenance`.\n- `RunSpec.provenance` references `RunProvenance` directly. Remove `oneOf [..., null]`.\n- If `run.created` event properties are represented separately in the spec, make that event provenance required and non-nullable too.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nDo not hand-edit generated client files.\n\n### Demo mode\n\n`lib/crates/fabro-server/src/demo/mod.rs`:\n- Add a clearly synthetic demo principal using `AuthMethod::DevToken`, not GitHub:\n ```rust\n static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| {\n Principal::user(\n IdpIdentity::new(\"fabro:demo\", \"demo\").unwrap(),\n \"demo\".to_string(),\n AuthMethod::DevToken,\n )\n });\n ```\n- Replace `created_by: None` with `created_by: DEMO_PRINCIPAL.clone()`.\n- If demo creates any full `RunSpec` or `run.created` event data, give it `RunProvenance { subject: DEMO_PRINCIPAL.clone(), ... }`.\n\n### Test support\n\nDo not add fake auth helpers to `fabro_types::fixtures`; that module is run-id constants.\n\nUse the existing `fabro-types` `test-support` feature:\n- Add `#[cfg(any(test, feature = \"test-support\"))] pub mod test_support;` in `lib/crates/fabro-types/src/lib.rs` if it does not already exist.\n- Add `lib/crates/fabro-types/src/test_support.rs` with:\n - `test_principal() -> Principal`\n - `test_run_provenance() -> RunProvenance`\n- Use an obviously fake dev-token identity, e.g. issuer `fabro:test`, subject `test-user`, login `test`.\n- In crates that need the helper from integration tests or cross-crate tests, dual-list `fabro-types` in `dev-dependencies` with `features = [\"test-support\"]`, following existing repo patterns.\n\nUpdate all constructors:\n- Replace `provenance: None` in `RunSpec`, `CreateRunInput`, `RetryRunInput`, `Event::RunCreated`, and `RunCreatedProps` literals with `test_run_provenance()` or a locally meaningful provenance.\n- Replace `subject: Some(...)` with `subject: ...`.\n- Replace `subject: None` only when it is actually `RunProvenance.subject`; leave unrelated todo/commit/message `subject` fields alone.\n- Replace `created_by: None` / `created_by: null` with `test_principal()` or a frontend TS principal fixture.\n- Delete tests that assert nullable or omitted creator/provenance behavior.\n\nRepresentative Rust areas:\n- `lib/crates/fabro-store/src/run_state.rs`\n- `lib/crates/fabro-store/tests/serializable_projection.rs`\n- `lib/crates/fabro-workflow/src/operations/{create,retry,start}.rs`\n- `lib/crates/fabro-workflow/src/event/{convert,sink,stored_fields}.rs`\n- `lib/crates/fabro-workflow/src/handler/**`\n- `lib/crates/fabro-workflow/src/pipeline/**`\n- `lib/crates/fabro-workflow/src/run_{lookup,metadata}.rs`\n- `lib/crates/fabro-server/src/server/tests.rs`\n- `lib/crates/fabro-server/src/server/handler/**`\n- `lib/crates/fabro-server/tests/it/**`\n- `lib/crates/fabro-cli/tests/it/support/mod.rs`\n- `lib/crates/fabro-dump/src/lib.rs`\n- `lib/crates/fabro-tool/src/{common,create,interact,search}.rs`\n- `lib/crates/fabro-api/tests/{principal_round_trip,run_summary_round_trip,run_projection_round_trip,run_event_round_trip}.rs`\n- `lib/crates/fabro-types/tests/{run_spec_serde,run_spec_methods,run_event_serde}.rs`\n\nRepresentative TypeScript areas:\n- `apps/fabro-web/app/**` tests with `created_by: null`\n- `apps/fabro-web/app/data/runs.ts`\n- `apps/fabro-web/app/components/run-summary-panel.tsx`\n- `apps/fabro-web/app/components/runs-list/**`\n- `lib/packages/fabro-api-client/tests/principal-exhaustive.ts`\n\nUseful sweep after edits:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|principal-anonymous|kind: ['\\\"]anonymous|created_by:\\\\s*(None|null)|provenance:\\\\s*None|subject:\\\\s*Some\\\\(|subject:\\\\s*None\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\nReview each hit. The only acceptable remaining matches should be unrelated uses of \"anonymous\" and unrelated non-principal `subject` fields.\n\n### Frontend\n\n`apps/fabro-web/app/components/run-summary-panel.tsx`:\n- `run?.created_by` may still be guarded by `run` loading state, but `created_by` itself is non-null once `run` exists.\n- Pass `run.created_by` directly to `principalDisplay(...)` inside loaded-run branches.\n\n`apps/fabro-web/app/data/runs.ts` and run-list components:\n- Treat `createdBy` as a total principal in UI data derived from a loaded API run.\n- Remove empty/fallback rendering that only existed for missing creator data.\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Manual end-to-end:\n - `fabro server start`\n - `cd apps/fabro-web && bun run dev`\n - Authenticate and create a run through the UI.\n - Confirm `/api/v1/runs/:id` has non-null `created_by`.\n - Confirm `/api/v1/runs/:id/state` has non-null `spec.provenance.subject`.\n - Retry a failed run and confirm the retried run has the retrying user as creator.\n - Hit demo mode with `X-Fabro-Demo: 1` and confirm the run summary renders the synthetic `demo` dev-token user.\n", - "internal.retry_count.simplify_opus": 0 + "thread.toolchain.current_node": "preflight_compile", + "last_stage": "simplify_opus", + "internal.node_visit_count": 1, + "outcome": "succeeded", + "internal.thread_id": "implement", + "graph.rankdir": "LR", + "internal.retry_count.preflight_lint": 0, + "last_response": "Both commits are done. Summary:\n\n**Commit 1** (`refactor: remove Principal::Anonymous, use Option for absent auth`): Dropped `Principal::Anonymous` variant. Auth middleware now uses `Option", + "thread.implement.current_node": "simplify_opus", + "internal.work_dir": "/home/daytona/workspace/fabro" }, "node_outcomes": { + "start": { + "status": "succeeded", + "usage": null + }, + "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": 136821, + "active_time_ms": 136821 + } + }, "simplify_opus": { "status": "succeeded", "context_updates": { @@ -1039,6 +1057,114 @@ "active_time_ms": 3970713 } }, + "implement": { + "status": "failed", + "failure": { + "message": "LLM error: Authentication error for openai: Your authentication token has been invalidated. Please try signing in again.", + "category": "deterministic", + "signature": "api_deterministic|openai|authentication" + }, + "usage": null + }, + "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": 154024, + "active_time_ms": 154024 + } + }, + "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": 1328, + "active_time_ms": 1328 + } + } + }, + "next_node_id": "simplify_gpt", + "git_commit_sha": "60c0a2af61a32abc2a152faa7b91046a58e52976", + "loop_failure_signatures": { + "implement|deterministic|api_deterministic|openai|authentication": 1 + }, + "node_visits": { + "start": 1, + "preflight_compile": 1, + "toolchain": 1, + "implement": 1, + "preflight_lint": 1, + "simplify_opus": 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..d1f03ff59 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@@ -53,7 +53,12 @@ function cellAfterLabel(\n function makeRun(overrides: Record = {}) {\n return {\n id: \"run_1\",\n- created_by: null,\n+ created_by: {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\n diff: null,\n billing: null,\n ...overrides,\n@@ -73,7 +78,6 @@ 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, \"Changes\"))).toBe(EMPTY_VALUE);\n expect(instanceText(cellAfterLabel(tree, \"Cost\"))).toBe(EMPTY_VALUE);\n });\n@@ -252,7 +256,7 @@ describe(\"RunSummaryPanelView\", () => {\n });\n \n test(\"renders non-user actor with kind label\", () => {\n- for (const kind of [\"agent\", \"system\", \"slack\", \"webhook\", \"worker\", \"anonymous\"]) {\n+ for (const kind of [\"agent\", \"system\", \"slack\", \"webhook\", \"worker\"]) {\n const tree = render({ run: makeRun({ created_by: { kind } as any }) });\n expect(instanceText(cellAfterLabel(tree, \"Created by\"))).toContain(kind);\n }\ndiff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx\nindex 20a08af5b..177270630 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -116,7 +116,7 @@ export function RunSummaryPanelView({\n artifactsCount,\n artifactsLoading,\n }: RunSummaryPanelViewProps) {\n- const created = run?.created_by ? principalDisplay(run.created_by) : null;\n+ const created = run ? principalDisplay(run.created_by) : null;\n const diff = run?.diff ?? null;\n const cost = formatUsdMicros(run?.billing?.total_usd_micros);\n const sandboxKind = sandboxLifecycleKind(run?.sandbox);\ndiff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts\nindex 98586b2c4..0f94de98b 100644\n--- a/apps/fabro-web/app/data/runs.test.ts\n+++ b/apps/fabro-web/app/data/runs.test.ts\n@@ -17,7 +17,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: { kind: \"user\", identity: { issuer: \"fabro:test\", subject: \"test-user\" }, login: \"test\", auth_method: \"dev_token\" },\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..d49aac4d4 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..c3d8d73a8 100644\n--- a/apps/fabro-web/app/lib/run-actions.test.ts\n+++ b/apps/fabro-web/app/lib/run-actions.test.ts\n@@ -47,7 +47,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: { kind: \"user\", identity: { issuer: \"fabro:test\", subject: \"test-user\" }, login: \"test\", auth_method: \"dev_token\" },\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/automations-new.test.tsx b/apps/fabro-web/app/routes/automations-new.test.tsx\nindex 47f471abf..240cf7c85 100644\n--- a/apps/fabro-web/app/routes/automations-new.test.tsx\n+++ b/apps/fabro-web/app/routes/automations-new.test.tsx\n@@ -120,7 +120,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: { kind: \"user\", identity: { issuer: \"fabro:test\", subject: \"test-user\" }, login: \"test\", auth_method: \"dev_token\" },\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..391c50ee3 100644\n--- a/apps/fabro-web/app/routes/run-detail.test.ts\n+++ b/apps/fabro-web/app/routes/run-detail.test.ts\n@@ -221,7 +221,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: { kind: \"user\", identity: { issuer: \"fabro:test\", subject: \"test-user\" }, login: \"test\", auth_method: \"dev_token\" },\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..c0c89a246 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@@ -51,7 +51,7 @@ mock.module(\"../lib/queries\", () => ({\n workflow: { slug: \"default\", name: \"Default\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: { kind: \"user\", identity: { issuer: \"fabro:test\", subject: \"test-user\" }, login: \"test\", auth_method: \"dev_token\" },\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..c13226b0c 100644\n--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx\n@@ -35,7 +35,7 @@ function run(id: string, repo = \"qlty/fabro\", workflow = \"release\"): Run {\n workflow: { slug: workflow, name: workflow, graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: repo, origin_url: null, provider: \"github\" },\n- created_by: null,\n+ created_by: { kind: \"user\", identity: { issuer: \"fabro:test\", subject: \"test-user\" }, login: \"test\", auth_method: \"dev_token\" },\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..92404782c 100644\n--- a/apps/fabro-web/app/routes/runs.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.test.tsx\n@@ -34,7 +34,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: { kind: \"user\", identity: { issuer: \"fabro:test\", subject: \"test-user\" }, login: \"test\", auth_method: \"dev_token\" },\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md\nindex 63f6f8f54..728a7c031 100644\n--- a/docs/internal/logging-strategy.md\n+++ b/docs/internal/logging-strategy.md\n@@ -118,7 +118,7 @@ Fields are key-value pairs that make events queryable. Include enough context th\n | `error` | Error value on failure |\n | `path` | File system path |\n | `duration_ms` | Elapsed time in milliseconds |\n-| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) |\n+| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) |\n | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) |\n | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests |\n \ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex 35d7511e5..bfb3e66a1 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -8992,6 +8992,8 @@ components:\n \n RunProvenance:\n type: object\n+ required:\n+ - subject\n properties:\n server:\n oneOf:\n@@ -9002,9 +9004,7 @@ components:\n - $ref: \"#/components/schemas/RunClientProvenance\"\n - type: \"null\"\n subject:\n- oneOf:\n- - $ref: \"#/components/schemas/Principal\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/Principal\"\n \n Principal:\n oneOf:\n@@ -9014,7 +9014,6 @@ components:\n - $ref: \"#/components/schemas/PrincipalSlack\"\n - $ref: \"#/components/schemas/PrincipalAgent\"\n - $ref: \"#/components/schemas/PrincipalSystem\"\n- - $ref: \"#/components/schemas/PrincipalAnonymous\"\n discriminator:\n propertyName: kind\n mapping:\n@@ -9024,7 +9023,6 @@ components:\n slack: \"#/components/schemas/PrincipalSlack\"\n agent: \"#/components/schemas/PrincipalAgent\"\n system: \"#/components/schemas/PrincipalSystem\"\n- anonymous: \"#/components/schemas/PrincipalAnonymous\"\n \n PrincipalUser:\n type: object\n@@ -9114,15 +9112,6 @@ components:\n system_kind:\n $ref: \"#/components/schemas/SystemActorKind\"\n \n- PrincipalAnonymous:\n- type: object\n- required:\n- - kind\n- properties:\n- kind:\n- type: string\n- enum: [anonymous]\n-\n RunEvent:\n description: >\n Internal RunEvent-compatible JSON payload. The server validates this\n@@ -10250,6 +10239,7 @@ components:\n - run_id\n - settings\n - graph\n+ - provenance\n properties:\n run_id:\n type: string\n@@ -10273,9 +10263,7 @@ components:\n additionalProperties:\n type: string\n provenance:\n- oneOf:\n- - $ref: \"#/components/schemas/RunProvenance\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/RunProvenance\"\n manifest_blob:\n type: [\"string\", \"null\"]\n definition_blob:\n@@ -10587,9 +10575,7 @@ components:\n - $ref: \"#/components/schemas/RepositoryRef\"\n - type: \"null\"\n created_by:\n- oneOf:\n- - $ref: \"#/components/schemas/Principal\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/Principal\"\n origin:\n $ref: \"#/components/schemas/RunOrigin\"\n labels:\ndiff --git a/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..370c33c29 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@@ -9,6 +9,17 @@ fn run_event_reuses_canonical_type() {\n assert_same_type::();\n }\n \n+fn test_provenance_json() -> Value {\n+ json!({\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ })\n+}\n+\n #[test]\n fn run_event_round_trips_run_created() {\n let value = json!({\n@@ -20,7 +31,8 @@ fn run_event_round_trips_run_created() {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/fabro/run-1\",\n- \"source_directory\": \"/tmp/fabro/run-1\"\n+ \"source_directory\": \"/tmp/fabro/run-1\",\n+ \"provenance\": test_provenance_json()\n }\n });\n \n@@ -39,6 +51,7 @@ fn run_event_round_trips_run_created_with_web_url() {\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/fabro/run-1\",\n \"source_directory\": \"/tmp/fabro/run-1\",\n+ \"provenance\": test_provenance_json(),\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..4ab846a7f 100644\n--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n@@ -137,7 +137,7 @@ fn run_spec_json() -> serde_json::Value {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-api/tests/run_summary_round_trip.rs b/lib/crates/fabro-api/tests/run_summary_round_trip.rs\nindex 798448941..aabe4889f 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@@ -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: fabro_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,12 @@ fn run_summary_json_matches_openapi_shape() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n- \"created_by\": null,\n+ \"created_by\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ },\n \"origin\": {\n \"kind\": \"api\"\n },\n@@ -253,6 +258,12 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ },\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..44dcc0f37 100644\n--- a/lib/crates/fabro-cli/Cargo.toml\n+++ b/lib/crates/fabro-cli/Cargo.toml\n@@ -119,6 +119,7 @@ assert_cmd = \"2\"\n fabro-acp = { path = \"../fabro-acp\", features = [\"test-support\"] }\n fabro-build-support = { path = \"../build-support\" }\n fabro-server = { path = \"../fabro-server\", features = [\"test-support\"] }\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\n insta = { workspace = true, features = [\"filters\"] }\n paste = \"1\"\n predicates = \"3\"\ndiff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs\nindex 77c5c113a..5e49adf42 100644\n--- a/lib/crates/fabro-cli/src/commands/run/attach.rs\n+++ b/lib/crates/fabro-cli/src/commands/run/attach.rs\n@@ -841,7 +841,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\nindex ec0d191e3..1934d8c2a 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\n@@ -221,7 +221,18 @@ fn inspect_resolves_selector_via_server_endpoint() {\n \"attrs\": {}\n },\n \"workflow_slug\": \"remote-workflow\",\n- \"source_directory\": \"/srv/repo\"\n+ \"source_directory\": \"/srv/repo\",\n+ \"provenance\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": {\n+ \"issuer\": \"fabro:test\",\n+ \"subject\": \"test-user\"\n+ },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\n },\n \"start_record\": null,\n \"conclusion\": null,\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs\nindex 275ff9437..a4f6f1516 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs\n@@ -177,6 +177,12 @@ pub(crate) fn remote_run_summary_json(\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ },\n \"origin\": {\n \"kind\": \"api\"\n },\ndiff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs\nindex 80b65e4aa..0e3122606 100644\n--- a/lib/crates/fabro-cli/tests/it/support/mod.rs\n+++ b/lib/crates/fabro-cli/tests/it/support/mod.rs\n@@ -49,7 +49,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s\n automation: None,\n source_directory: Some(\"/srv/repo\".to_string()),\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-dump/src/lib.rs b/lib/crates/fabro-dump/src/lib.rs\nindex 397d55a3f..455e7290b 100644\n--- a/lib/crates/fabro-dump/src/lib.rs\n+++ b/lib/crates/fabro-dump/src/lib.rs\n@@ -472,6 +472,7 @@ mod tests {\n use fabro_store::{RunProjection, StageId};\n use fabro_types::graph::Graph;\n use fabro_types::run::RunSpec;\n+ use fabro_types::test_support::test_run_provenance;\n use fabro_types::{\n Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunSandboxInstance,\n RunSandboxPlan, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage,\n@@ -498,7 +499,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_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..618bcfae1 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,10 +1092,19 @@ 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+ static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| {\n+ Principal::user(\n+ IdpIdentity::new(\"fabro:demo\", \"demo\").expect(\"demo identity should parse\"),\n+ \"demo\".to_string(),\n+ AuthMethod::DevToken,\n+ )\n+ });\n+\n use super::ts;\n use crate::server::run_stage_from_stage_id;\n \n@@ -1171,7 +1180,7 @@ mod runs {\n repo_origin_url,\n source_directory.as_deref(),\n )),\n- created_by: None,\n+ created_by: DEMO_PRINCIPAL.clone(),\n origin: RunOrigin::default(),\n labels: labels(entries),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-server/src/principal_middleware.rs b/lib/crates/fabro-server/src/principal_middleware.rs\nindex f9e8c385c..06a0ba257 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,7 +172,10 @@ 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: context\n+ .principal\n+ .as_ref()\n+ .map(principal_without_log_unused_fields),\n auth_status: context.auth_status,\n auth_error_code: context.auth_error_code,\n }\n@@ -402,7 +405,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 +415,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 +431,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 +446,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 +456,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 +468,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 +690,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 +730,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 +749,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 +828,7 @@ mod tests {\n \n assert_eq!(context.auth_status, AuthStatus::Missing);\n assert_eq!(context.auth_error_code, None);\n- assert_eq!(context.principal, Principal::Anonymous);\n+ assert_eq!(context.principal, None);\n }\n \n #[test]\ndiff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/crates/fabro-server/src/run_files.rs\nindex 1f358821b..e49567162 100644\n--- a/lib/crates/fabro-server/src/run_files.rs\n+++ b/lib/crates/fabro-server/src/run_files.rs\n@@ -2389,7 +2389,7 @@ index 1111111..2222222 160000\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs\nindex 4feb22133..729e5666f 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@@ -194,6 +196,7 @@ pub(crate) fn create_run_input(\n prepared: PreparedManifest,\n configured_providers: Vec,\n web_url: Option,\n+ provenance: RunProvenance,\n ) -> CreateRunInput {\n CreateRunInput {\n workflow: WorkflowInput::Bundled(prepared.workflow_input),\n@@ -209,7 +212,7 @@ pub(crate) fn create_run_input(\n git: prepared.git,\n fork_source_ref: None,\n parent_id: prepared.parent_id,\n- provenance: None,\n+ provenance,\n configured_providers,\n web_url,\n }\ndiff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs\nindex 7bff4968f..4710c40ca 100644\n--- a/lib/crates/fabro-server/src/server.rs\n+++ b/lib/crates/fabro-server/src/server.rs\n@@ -1876,7 +1876,10 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n let status = response.status().as_u16();\n let latency_ms = start.elapsed().as_millis();\n let auth_context = auth_slot.log_snapshot();\n- let principal_kind = auth_context.principal.kind();\n+ let principal_kind = auth_context\n+ .principal\n+ .as_ref()\n+ .map_or(\"none\", Principal::kind);\n let auth_status = auth_context.auth_status.as_str();\n \n macro_rules! emit_http_log {\n@@ -1914,27 +1917,27 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n macro_rules! emit_principal_http_log {\n ($level:ident) => {{\n match &auth_context.principal {\n- Principal::User(user) => emit_http_log!(\n+ Some(Principal::User(user)) => emit_http_log!(\n $level,\n user_auth_method = user.auth_method.as_str(),\n idp_issuer = user.identity.issuer(),\n idp_subject = user.identity.subject(),\n login = user.login.as_str(),\n ),\n- Principal::Worker { run_id } => {\n+ Some(Principal::Worker { run_id }) => {\n emit_http_log!($level, run_id = run_id.to_string().as_str(),)\n }\n- Principal::Webhook { delivery_id } => {\n+ Some(Principal::Webhook { delivery_id }) => {\n emit_http_log!($level, delivery_id = delivery_id.as_str(),)\n }\n- Principal::Slack {\n+ Some(Principal::Slack {\n team_id, user_id, ..\n- } => emit_http_log!(\n+ }) => emit_http_log!(\n $level,\n team_id = team_id.as_str(),\n user_id = user_id.as_str(),\n ),\n- Principal::Agent { .. } | Principal::System { .. } | Principal::Anonymous => {\n+ None | Some(Principal::Agent { .. } | Principal::System { .. }) => {\n emit_http_log!($level)\n }\n }\ndiff --git a/lib/crates/fabro-server/src/server/handler/events.rs b/lib/crates/fabro-server/src/server/handler/events.rs\nindex 180c04bdd..ff2fabcdc 100644\n--- a/lib/crates/fabro-server/src/server/handler/events.rs\n+++ b/lib/crates/fabro-server/src/server/handler/events.rs\n@@ -570,7 +570,7 @@ mod stage_events_tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/server/handler/lifecycle.rs b/lib/crates/fabro-server/src/server/handler/lifecycle.rs\nindex 06beb7c0b..5df7e72c5 100644\n--- a/lib/crates/fabro-server/src/server/handler/lifecycle.rs\n+++ b/lib/crates/fabro-server/src/server/handler/lifecycle.rs\n@@ -894,7 +894,7 @@ async fn retry_run(\n let input = operations::RetryRunInput {\n source_run_id: id,\n new_run_id,\n- provenance: Some(run_provenance(&headers, &actor)),\n+ provenance: run_provenance(&headers, &actor),\n web_url: state.run_web_url(&new_run_id),\n };\n match Box::pin(operations::retry_run(&state.store, &input)).await {\ndiff --git a/lib/crates/fabro-server/src/server/handler/pair.rs b/lib/crates/fabro-server/src/server/handler/pair.rs\nindex 7b673fad8..73bf6f374 100644\n--- a/lib/crates/fabro-server/src/server/handler/pair.rs\n+++ b/lib/crates/fabro-server/src/server/handler/pair.rs\n@@ -1024,7 +1024,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs\nindex fca9fdc3c..5b8f06ef4 100644\n--- a/lib/crates/fabro-server/src/server/handler/runs.rs\n+++ b/lib/crates/fabro-server/src/server/handler/runs.rs\n@@ -691,9 +691,9 @@ pub(crate) async fn create_run_from_manifest(\n prepared.clone(),\n ready_provider_ids.clone(),\n web_url.clone(),\n+ run_provenance(&headers, &actor),\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 +864,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..3f3c1853f 100644\n--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs\n@@ -1339,6 +1339,14 @@ mod retrieve_sandbox_tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\n },\n }),\n run_id,\ndiff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/crates/fabro-server/src/server/handler/sessions.rs\nindex bad48c6d4..cd8c07579 100644\n--- a/lib/crates/fabro-server/src/server/handler/sessions.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sessions.rs\n@@ -1700,7 +1700,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs\nindex 363884738..418ffc098 100644\n--- a/lib/crates/fabro-server/src/server/tests.rs\n+++ b/lib/crates/fabro-server/src/server/tests.rs\n@@ -4022,7 +4022,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -4076,7 +4076,7 @@ async fn create_slack_notification_run(\n workflow_slug: workflow_slug.map(str::to_string),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -5083,7 +5083,7 @@ async fn list_run_stages_distinguishes_visits() {\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -6140,7 +6140,7 @@ async fn create_completed_run_ready_for_pull_request(\n source_directory: Some(\"/tmp/project\".to_string()),\n git: git.clone(),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -9943,15 +9943,10 @@ async fn run_tool_worker_token_can_use_client_backend_routes_across_runs() {\n .unwrap()\n .expect(\"created run should be cached\");\n assert_eq!(\n- cached\n- .projection\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.as_ref()),\n- Some(&Principal::Worker {\n+ cached.projection.spec.provenance.subject,\n+ Principal::Worker {\n run_id: parent_run_id,\n- }),\n+ },\n );\n \n let response = app\n@@ -12310,7 +12305,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -13062,7 +13057,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs\nindex 033e46db6..e231599b2 100644\n--- a/lib/crates/fabro-server/src/web_auth.rs\n+++ b/lib/crates/fabro-server/src/web_auth.rs\n@@ -1365,7 +1365,7 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert!(matches!(contexts[0].principal, Principal::User(_)));\n+ assert!(matches!(contexts[0].principal, Some(Principal::User(_))));\n assert_eq!(contexts[1].auth_status, AuthStatus::Invalid);\n assert_eq!(\n contexts[1].auth_error_code,\ndiff --git a/lib/crates/fabro-server/tests/it/api/run_files.rs b/lib/crates/fabro-server/tests/it/api/run_files.rs\nindex a3bf7ad76..01d762a73 100644\n--- a/lib/crates/fabro-server/tests/it/api/run_files.rs\n+++ b/lib/crates/fabro-server/tests/it/api/run_files.rs\n@@ -69,7 +69,7 @@ async fn append_completed_run_with_final_patch(\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-store/Cargo.toml b/lib/crates/fabro-store/Cargo.toml\nindex 016b55d3d..c81bca853 100644\n--- a/lib/crates/fabro-store/Cargo.toml\n+++ b/lib/crates/fabro-store/Cargo.toml\n@@ -32,6 +32,7 @@ futures.workspace = true\n uuid.workspace = true\n \n [dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\n tokio = { workspace = true, features = [\"test-util\", \"macros\"] }\n tempfile = \"3\"\n ulid.workspace = true\ndiff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs\nindex cd6fa0640..3325955c0 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@@ -1267,6 +1263,7 @@ mod tests {\n StagePromptProps, StageRetryingProps, StageStartedProps,\n };\n use fabro_types::settings::run::{DockerfileSource, EnvironmentProvider};\n+ use fabro_types::test_support::test_run_provenance;\n use fabro_types::{\n AgentBackend, AutomationRef, BilledModelUsage, BilledTokenCounts, BlockedReason,\n Checkpoint, CheckpointRecord, CommandTermination, EventBody, FailureCategory,\n@@ -1358,7 +1355,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1638,12 +1635,35 @@ mod tests {\n assert!(sandbox.get(\"failure\").is_none());\n }\n \n+ fn inject_required_run_created_fields(\n+ event: &str,\n+ properties: &serde_json::Value,\n+ ) -> serde_json::Value {\n+ if event != \"run.created\" || !properties.is_object() {\n+ return properties.clone();\n+ }\n+ let mut props = properties.clone();\n+ let obj = props.as_object_mut().expect(\"properties must be an object\");\n+ obj.entry(\"provenance\".to_string()).or_insert_with(|| {\n+ json!({\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ })\n+ });\n+ props\n+ }\n+\n fn test_raw_event(\n seq: u32,\n event: &str,\n properties: &serde_json::Value,\n node_id: Option<&str>,\n ) -> EventEnvelope {\n+ let properties = inject_required_run_created_fields(event, properties);\n EventEnvelope {\n seq,\n event: RunEvent::from_value(json!({\n@@ -1665,6 +1685,7 @@ mod tests {\n properties: &serde_json::Value,\n node_id: Option<&str>,\n ) -> EventEnvelope {\n+ let properties = inject_required_run_created_fields(event, properties);\n EventEnvelope {\n seq,\n event: RunEvent::from_value(json!({\n@@ -1822,7 +1843,14 @@ mod tests {\n \"repo_origin_url\": null,\n \"base_branch\": null,\n \"labels\": {},\n- \"provenance\": null,\n+ \"provenance\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ },\n \"manifest_blob\": null,\n \"definition_blob\": null,\n \"git\": null,\n@@ -2851,7 +2879,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2877,7 +2905,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -3016,7 +3044,15 @@ 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\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\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..568f07fac 100644\n--- a/lib/crates/fabro-store/src/slate/mod.rs\n+++ b/lib/crates/fabro-store/src/slate/mod.rs\n@@ -470,6 +470,7 @@ fn active_run_from(\n #[cfg(test)]\n mod tests {\n use chrono::{DateTime, Utc};\n+ use fabro_types::test_support::test_run_provenance;\n use fabro_types::{\n AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId,\n SuccessReason, WorkflowSettings,\n@@ -542,7 +543,7 @@ mod tests {\n automation: None,\n source_directory: Some(format!(\"/tmp/{label}\")),\n labels: std::collections::HashMap::from([(\"team\".to_string(), \"infra\".to_string())]),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\n@@ -601,6 +602,7 @@ mod tests {\n \"run_dir\": format!(\"/tmp/{label}\"),\n \"git\": run_spec.git,\n \"labels\": run_spec.labels,\n+ \"provenance\": run_spec.provenance,\n }),\n ))\n .await\n@@ -626,6 +628,7 @@ mod tests {\n \"run_dir\": format!(\"/tmp/{label}\"),\n \"git\": run_spec.git,\n \"labels\": run_spec.labels,\n+ \"provenance\": run_spec.provenance,\n \"parent_id\": parent_id,\n }),\n ))\n@@ -1300,6 +1303,7 @@ mod tests {\n \"run_dir\": \"/tmp/run-2\",\n \"git\": run_spec[\"git\"],\n \"labels\": run_spec[\"labels\"],\n+ \"provenance\": run_spec[\"provenance\"],\n },\n }))\n .unwrap(),\ndiff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs\nindex 1718cb662..d2f5d8961 100644\n--- a/lib/crates/fabro-store/src/slate/run_store.rs\n+++ b/lib/crates/fabro-store/src/slate/run_store.rs\n@@ -723,6 +723,14 @@ mod tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\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..eda1995c4 100644\n--- a/lib/crates/fabro-store/tests/serializable_projection.rs\n+++ b/lib/crates/fabro-store/tests/serializable_projection.rs\n@@ -4,6 +4,7 @@ use chrono::{TimeZone, Utc};\n use fabro_store::{RunProjection, SerializableProjection, StageId};\n use fabro_types::graph::Graph;\n use fabro_types::run::RunSpec;\n+use fabro_types::test_support::test_run_provenance;\n use fabro_types::{\n BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord,\n QuestionType, RunDiff, RunSandbox, RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime,\n@@ -22,7 +23,7 @@ fn sample_run_spec() -> RunSpec {\n automation: None,\n source_directory: Some(\"/tmp/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\ndiff --git a/lib/crates/fabro-tool/Cargo.toml b/lib/crates/fabro-tool/Cargo.toml\nindex 08b6df7cf..c50ea6a38 100644\n--- a/lib/crates/fabro-tool/Cargo.toml\n+++ b/lib/crates/fabro-tool/Cargo.toml\n@@ -29,4 +29,5 @@ tokio.workspace = true\n toml.workspace = true\n \n [dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\n tempfile = \"3\"\ndiff --git a/lib/crates/fabro-tool/src/common.rs b/lib/crates/fabro-tool/src/common.rs\nindex 9dd64599a..1595dc28f 100644\n--- a/lib/crates/fabro-tool/src/common.rs\n+++ b/lib/crates/fabro-tool/src/common.rs\n@@ -307,6 +307,7 @@ fn format_tool_error(err: &anyhow::Error) -> String {\n #[cfg(test)]\n mod tests {\n use chrono::{TimeZone, Utc};\n+ use fabro_types::test_support::test_principal;\n use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n \n use super::*;\n@@ -413,7 +414,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs\nindex 8488f8f0b..8988d1a4e 100644\n--- a/lib/crates/fabro-tool/src/create.rs\n+++ b/lib/crates/fabro-tool/src/create.rs\n@@ -506,6 +506,7 @@ mod tests {\n use async_trait::async_trait;\n use chrono::{TimeZone, Utc};\n use fabro_api::types;\n+ use fabro_types::test_support::test_principal;\n use fabro_types::{\n EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus,\n RunTimestamps, WorkflowRef,\n@@ -902,7 +903,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/interact.rs b/lib/crates/fabro-tool/src/interact.rs\nindex 34023120d..9f7957d15 100644\n--- a/lib/crates/fabro-tool/src/interact.rs\n+++ b/lib/crates/fabro-tool/src/interact.rs\n@@ -451,6 +451,7 @@ mod tests {\n \n use async_trait::async_trait;\n use chrono::{TimeZone, Utc};\n+ use fabro_types::test_support::test_principal;\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection,\n RunStatus, RunTimestamps, WorkflowRef,\n@@ -690,7 +691,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs\nindex 0df7e391a..379806d07 100644\n--- a/lib/crates/fabro-tool/src/search.rs\n+++ b/lib/crates/fabro-tool/src/search.rs\n@@ -293,6 +293,7 @@ mod tests {\n use std::collections::HashMap;\n \n use chrono::{TimeZone, Utc};\n+ use fabro_types::test_support::test_principal;\n use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n \n use super::*;\n@@ -444,7 +445,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"group\".to_string(), group.to_string())]),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs\nindex f1a8c8b39..cf2605429 100644\n--- a/lib/crates/fabro-types/src/lib.rs\n+++ b/lib/crates/fabro-types/src/lib.rs\n@@ -44,6 +44,8 @@ pub mod start;\n pub mod status;\n pub mod steering;\n pub mod system_integrations;\n+#[cfg(any(test, feature = \"test-support\"))]\n+pub mod test_support;\n pub mod timing;\n pub mod todo;\n pub mod transcript;\ndiff --git a/lib/crates/fabro-types/src/principal.rs b/lib/crates/fabro-types/src/principal.rs\nindex 2c0ff3807..4f1962ea2 100644\n--- a/lib/crates/fabro-types/src/principal.rs\n+++ b/lib/crates/fabro-types/src/principal.rs\n@@ -39,7 +39,6 @@ pub enum Principal {\n System {\n system_kind: SystemActorKind,\n },\n- Anonymous,\n }\n \n #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, IntoStaticStr)]\n@@ -97,7 +96,6 @@ impl Principal {\n Self::Slack { .. } => \"slack\",\n Self::Agent { .. } => \"agent\",\n Self::System { .. } => \"system\",\n- Self::Anonymous => \"anonymous\",\n }\n }\n \n@@ -123,7 +121,6 @@ impl Principal {\n } => session_id.clone(),\n Self::Agent { .. } => \"agent\".to_string(),\n Self::System { system_kind } => format!(\"system:{system_kind}\"),\n- Self::Anonymous => \"anonymous\".to_string(),\n }\n }\n }\n@@ -291,11 +288,6 @@ mod tests {\n });\n }\n \n- #[test]\n- fn round_trips_anonymous_variant() {\n- assert_round_trip(&Principal::Anonymous);\n- }\n-\n #[test]\n fn auth_method_as_str_matches_serde() {\n assert_eq!(AuthMethod::Github.as_str(), \"github\");\ndiff --git a/lib/crates/fabro-types/src/run.rs b/lib/crates/fabro-types/src/run.rs\nindex 2b27d0a06..2f1671fe6 100644\n--- a/lib/crates/fabro-types/src/run.rs\n+++ b/lib/crates/fabro-types/src/run.rs\n@@ -24,14 +24,13 @@ pub struct RunClientProvenance {\n pub version: Option,\n }\n \n-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n pub struct RunProvenance {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub server: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub client: Option,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub subject: Option,\n+ pub subject: Principal,\n }\n \n #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n@@ -93,8 +92,7 @@ pub struct RunSpec {\n pub source_directory: Option,\n #[serde(default, skip_serializing_if = \"HashMap::is_empty\")]\n pub labels: HashMap,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub provenance: Option,\n+ pub provenance: RunProvenance,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub manifest_blob: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\ndiff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs\nindex 52cbb83a9..9ce7b7f85 100644\n--- a/lib/crates/fabro-types/src/run_event/mod.rs\n+++ b/lib/crates/fabro-types/src/run_event/mod.rs\n@@ -1017,7 +1017,15 @@ mod tests {\n \"graph\": graph,\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n- \"source_directory\": \"/tmp/run\"\n+ \"source_directory\": \"/tmp/run\",\n+ \"provenance\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\n }\n });\n \n@@ -1038,7 +1046,15 @@ 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\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\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..47dffdcf7 100644\n--- a/lib/crates/fabro-types/src/run_projection.rs\n+++ b/lib/crates/fabro-types/src/run_projection.rs\n@@ -681,6 +681,7 @@ mod title_tests {\n \n use chrono::Utc;\n \n+ use crate::test_support::test_run_provenance;\n use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings};\n \n fn projection_with_goal(goal: Option<&str>) -> RunProjection {\n@@ -700,7 +701,7 @@ mod title_tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -752,6 +753,7 @@ mod iter_stages_tests {\n use serde_json::json;\n \n use super::RunProjection;\n+ use crate::test_support::test_run_provenance;\n use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings};\n \n fn seq(n: u32) -> NonZeroU32 {\n@@ -770,7 +772,7 @@ mod iter_stages_tests {\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs\nindex fd35dc2a4..3c4321052 100644\n--- a/lib/crates/fabro-types/src/run_summary.rs\n+++ b/lib/crates/fabro-types/src/run_summary.rs\n@@ -52,8 +52,7 @@ pub struct Run {\n pub automation: Option,\n #[serde(default)]\n pub repository: Option,\n- #[serde(default)]\n- pub created_by: Option,\n+ pub created_by: Principal,\n pub origin: RunOrigin,\n pub labels: HashMap,\n pub lifecycle: RunLifecycle,\ndiff --git a/lib/crates/fabro-types/src/test_support.rs b/lib/crates/fabro-types/src/test_support.rs\nnew file mode 100644\nindex 000000000..4067144bb\n--- /dev/null\n+++ b/lib/crates/fabro-types/src/test_support.rs\n@@ -0,0 +1,28 @@\n+//! Test-only helpers for constructing total `Principal` and `RunProvenance`\n+//! values without requiring callers to assemble fake identity bits inline.\n+//!\n+//! Only available behind `#[cfg(test)]` or the `test-support` feature. Do not\n+//! use these in production code or release builds.\n+\n+use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance};\n+\n+/// A clearly synthetic dev-token principal for tests.\n+#[must_use]\n+pub fn test_principal() -> Principal {\n+ Principal::user(\n+ IdpIdentity::new(\"fabro:test\", \"test-user\").expect(\"test identity should parse\"),\n+ \"test\".to_string(),\n+ AuthMethod::DevToken,\n+ )\n+}\n+\n+/// A `RunProvenance` with a synthetic test subject and no server/client\n+/// provenance fields.\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..0bfa07da0 100644\n--- a/lib/crates/fabro-types/tests/run_event_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_event_serde.rs\n@@ -1,12 +1,27 @@\n use std::collections::BTreeMap;\n \n use fabro_types::graph::Graph;\n-use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome};\n+use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunProvenance};\n use fabro_types::run_event::run::{RunCreatedProps, RunParentLinkedProps, RunParentUnlinkedProps};\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+ AuthMethod, AutomationRef, EventBody, IdpIdentity, Principal, TurnId, WorkflowSettings,\n+ fixtures,\n+};\n+\n+fn test_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::user(\n+ IdpIdentity::new(\"fabro:test\", \"test-user\").expect(\"test identity should parse\"),\n+ \"test\".to_string(),\n+ AuthMethod::DevToken,\n+ ),\n+ }\n+}\n \n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n@@ -32,7 +47,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_provenance(),\n manifest_blob: None,\n git: Some(GitContext {\n origin_url: \"https://github.com/fabro-sh/fabro.git\".to_string(),\n@@ -97,7 +112,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_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -134,7 +149,15 @@ fn run_created_props_defaults_additive_fields_for_legacy_events() {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"ship\"),\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\n });\n \n let props: RunCreatedProps =\ndiff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs\nindex b05dca05e..de069034e 100644\n--- a/lib/crates/fabro-types/tests/run_spec_methods.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_methods.rs\n@@ -1,9 +1,21 @@\n use std::collections::HashMap;\n \n use fabro_types::graph::Graph;\n-use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec};\n+use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunProvenance, RunSpec};\n use fabro_types::settings::{ProjectNamespace, WorkflowNamespace};\n-use fabro_types::{WorkflowSettings, fixtures};\n+use fabro_types::{AuthMethod, IdpIdentity, Principal, WorkflowSettings, fixtures};\n+\n+fn test_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::user(\n+ IdpIdentity::new(\"fabro:test\", \"test-user\").expect(\"test identity should parse\"),\n+ \"test\".to_string(),\n+ AuthMethod::DevToken,\n+ ),\n+ }\n+}\n \n fn sample_run_spec() -> RunSpec {\n let settings = WorkflowSettings {\n@@ -27,7 +39,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_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..0f5318eea 100644\n--- a/lib/crates/fabro-types/tests/run_spec_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_serde.rs\n@@ -1,10 +1,24 @@\n use std::collections::HashMap;\n \n use fabro_types::graph::Graph;\n-use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunSpec};\n+use fabro_types::run::{\n+ DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunProvenance, RunSpec,\n+};\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n-use fabro_types::{AutomationRef, WorkflowSettings, fixtures};\n+use fabro_types::{AuthMethod, AutomationRef, IdpIdentity, Principal, WorkflowSettings, fixtures};\n+\n+fn test_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::user(\n+ IdpIdentity::new(\"fabro:test\", \"test-user\").expect(\"test identity should parse\"),\n+ \"test\".to_string(),\n+ AuthMethod::DevToken,\n+ ),\n+ }\n+}\n \n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n@@ -27,7 +41,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_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\n@@ -80,7 +94,15 @@ 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\": {\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": { \"issuer\": \"fabro:test\", \"subject\": \"test-user\" },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\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..73391c9bc 100644\n--- a/lib/crates/fabro-workflow/src/billing_rollup.rs\n+++ b/lib/crates/fabro-workflow/src/billing_rollup.rs\n@@ -353,7 +353,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs\nindex 4c1f91228..5f0fc9959 100644\n--- a/lib/crates/fabro-workflow/src/event/convert.rs\n+++ b/lib/crates/fabro-workflow/src/event/convert.rs\n@@ -2339,7 +2339,7 @@ mod tests {\n let provenance = RunProvenance {\n server: None,\n client: None,\n- subject: Some(user_principal(\"alice\")),\n+ subject: user_principal(\"alice\"),\n };\n let automation = AutomationRef {\n id: \"nightly\".to_string(),\n@@ -2348,25 +2348,25 @@ mod tests {\n };\n \n let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated {\n- run_id: fixtures::RUN_1,\n- title: None,\n- settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),\n- graph: serde_json::to_value(Graph::new(\"test\")).unwrap(),\n- workflow_source: None,\n- workflow_config: None,\n- labels: BTreeMap::default(),\n- run_dir: \"/tmp/run\".to_string(),\n+ run_id: fixtures::RUN_1,\n+ title: None,\n+ settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),\n+ graph: serde_json::to_value(Graph::new(\"test\")).unwrap(),\n+ workflow_source: None,\n+ workflow_config: None,\n+ labels: BTreeMap::default(),\n+ run_dir: \"/tmp/run\".to_string(),\n source_directory: Some(\"/tmp/run\".to_string()),\n- workflow_slug: None,\n- automation: Some(automation.clone()),\n- db_prefix: None,\n- provenance: Some(provenance),\n- manifest_blob: None,\n- git: None,\n- fork_source_ref: None,\n- retried_from: None,\n- parent_id: None,\n- web_url: None,\n+ workflow_slug: None,\n+ automation: Some(automation.clone()),\n+ db_prefix: None,\n+ provenance,\n+ manifest_blob: None,\n+ git: None,\n+ fork_source_ref: None,\n+ retried_from: None,\n+ parent_id: None,\n+ web_url: None,\n });\n let actor = stored.actor.as_ref().expect(\"actor set\");\n assert_eq!(actor, &user_principal(\"alice\"));\ndiff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs\nindex 886868804..0b5afb1c7 100644\n--- a/lib/crates/fabro-workflow/src/event/events.rs\n+++ b/lib/crates/fabro-workflow/src/event/events.rs\n@@ -41,8 +41,7 @@ pub enum Event {\n automation: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n db_prefix: Option,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- provenance: Option,\n+ provenance: RunProvenance,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n manifest_blob: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\ndiff --git a/lib/crates/fabro-workflow/src/event/sink.rs b/lib/crates/fabro-workflow/src/event/sink.rs\nindex 7b65fc99c..4acc5265a 100644\n--- a/lib/crates/fabro-workflow/src/event/sink.rs\n+++ b/lib/crates/fabro-workflow/src/event/sink.rs\n@@ -244,7 +244,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/event/stored_fields.rs b/lib/crates/fabro-workflow/src/event/stored_fields.rs\nindex 94fba25ad..4ada17b67 100644\n--- a/lib/crates/fabro-workflow/src/event/stored_fields.rs\n+++ b/lib/crates/fabro-workflow/src/event/stored_fields.rs\n@@ -57,7 +57,7 @@ pub(super) fn stored_event_fields(event: &Event, scope: Option<&StageScope>) ->\n fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields {\n match event {\n Event::RunCreated { provenance, .. } => StoredEventFields {\n- actor: provenance.as_ref().and_then(|p| p.subject.clone()),\n+ actor: Some(provenance.subject.clone()),\n ..StoredEventFields::default()\n },\n Event::RunCancelRequested { actor }\ndiff --git a/lib/crates/fabro-workflow/src/git.rs b/lib/crates/fabro-workflow/src/git.rs\nindex 643d49b58..0f0613fe8 100644\n--- a/lib/crates/fabro-workflow/src/git.rs\n+++ b/lib/crates/fabro-workflow/src/git.rs\n@@ -469,7 +469,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs\nindex 3a120e8fc..3b048374b 100644\n--- a/lib/crates/fabro-workflow/src/handler/agent.rs\n+++ b/lib/crates/fabro-workflow/src/handler/agent.rs\n@@ -484,7 +484,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/command.rs b/lib/crates/fabro-workflow/src/handler/command.rs\nindex f10ebafde..2e59a954b 100644\n--- a/lib/crates/fabro-workflow/src/handler/command.rs\n+++ b/lib/crates/fabro-workflow/src/handler/command.rs\n@@ -256,7 +256,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -357,7 +357,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs\nindex 91753f4c9..8520a0c93 100644\n--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs\n+++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs\n@@ -2133,7 +2133,7 @@ reasoning = false\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: fabro_types::test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs\nindex b52478bac..6772db8fb 100644\n--- a/lib/crates/fabro-workflow/src/handler/parallel.rs\n+++ b/lib/crates/fabro-workflow/src/handler/parallel.rs\n@@ -728,7 +728,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs\nindex 1c2a82267..d88c9e875 100644\n--- a/lib/crates/fabro-workflow/src/handler/prompt.rs\n+++ b/lib/crates/fabro-workflow/src/handler/prompt.rs\n@@ -283,7 +283,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs\nindex ccd24632d..233044930 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs\n@@ -736,7 +736,7 @@ mod tests {\n workflow_slug: Some(\"metadata\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/operations/archive.rs b/lib/crates/fabro-workflow/src/operations/archive.rs\nindex db4b0ccf9..2e0112a61 100644\n--- a/lib/crates/fabro-workflow/src/operations/archive.rs\n+++ b/lib/crates/fabro-workflow/src/operations/archive.rs\n@@ -226,7 +226,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs\nindex 8bf16d115..d66ac48cb 100644\n--- a/lib/crates/fabro-workflow/src/operations/create.rs\n+++ b/lib/crates/fabro-workflow/src/operations/create.rs\n@@ -45,7 +45,7 @@ pub struct CreateRunInput {\n pub git: Option,\n pub fork_source_ref: Option,\n pub parent_id: Option,\n- pub provenance: Option,\n+ pub provenance: RunProvenance,\n pub configured_providers: Vec,\n /// Public URL where this run can be viewed in the web UI, when the server\n /// has the web UI enabled. Recorded on the `run.created` event so attach\n@@ -72,7 +72,7 @@ struct PersistCreateOptions {\n automation: Option,\n git: Option,\n fork_source_ref: Option,\n- provenance: Option,\n+ provenance: RunProvenance,\n configured_providers: Vec,\n catalog: Arc,\n }\n@@ -1115,7 +1115,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1183,7 +1183,7 @@ mod tests {\n }),\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1295,7 +1295,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1341,7 +1341,7 @@ mod tests {\n }),\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1414,7 +1414,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1467,7 +1467,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: Some(fabro_types::RunProvenance {\n+ provenance: fabro_types::RunProvenance {\n server: Some(fabro_types::RunServerProvenance {\n version: \"0.9.0\".to_string(),\n }),\n@@ -1476,12 +1476,12 @@ mod tests {\n name: Some(\"fabro-cli\".to_string()),\n version: Some(\"0.9.0\".to_string()),\n }),\n- subject: Some(fabro_types::Principal::user(\n+ subject: fabro_types::Principal::user(\n fabro_types::IdpIdentity::new(\"https://github.com\", \"12345\").unwrap(),\n \"octocat\".to_string(),\n fabro_types::AuthMethod::Github,\n- )),\n- }),\n+ ),\n+ },\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1493,8 +1493,7 @@ mod tests {\n \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 = state.spec.provenance;\n \n assert_eq!(provenance.server.unwrap().version, \"0.9.0\");\n assert_eq!(\n@@ -1502,7 +1501,7 @@ mod tests {\n Some(\"fabro-cli\")\n );\n assert_eq!(\n- provenance.subject.unwrap(),\n+ provenance.subject,\n fabro_types::Principal::user(\n fabro_types::IdpIdentity::new(\"https://github.com\", \"12345\").unwrap(),\n \"octocat\".to_string(),\ndiff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs\nindex 2bf7e144f..006f960fa 100644\n--- a/lib/crates/fabro-workflow/src/operations/fork.rs\n+++ b/lib/crates/fabro-workflow/src/operations/fork.rs\n@@ -383,7 +383,7 @@ mod tests {\n workflow_slug: Some(\"fork-source\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: Some(fabro_types::GitContext {\n origin_url: \"https://github.com/example/repo.git\".to_string(),\ndiff --git a/lib/crates/fabro-workflow/src/operations/retry.rs b/lib/crates/fabro-workflow/src/operations/retry.rs\nindex be7793d3e..6d3cbdc3c 100644\n--- a/lib/crates/fabro-workflow/src/operations/retry.rs\n+++ b/lib/crates/fabro-workflow/src/operations/retry.rs\n@@ -12,7 +12,7 @@ use crate::event::{self, Event};\n pub struct RetryRunInput {\n pub source_run_id: RunId,\n pub new_run_id: RunId,\n- pub provenance: Option,\n+ pub provenance: RunProvenance,\n pub web_url: Option,\n }\n \n@@ -152,7 +152,7 @@ mod tests {\n version: \"test\".to_string(),\n }),\n client: None,\n- subject: Some(actor(login)),\n+ subject: actor(login),\n }\n }\n \n@@ -191,7 +191,7 @@ mod tests {\n workflow_slug: Some(\"retry-source\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: Some(provenance(\"source-user\")),\n+ provenance: provenance(\"source-user\"),\n manifest_blob,\n git: Some(git_context()),\n fork_source_ref,\n@@ -368,7 +368,7 @@ mod tests {\n let outcome = retry_run(&store, &RetryRunInput {\n source_run_id,\n new_run_id: RunId::new(),\n- provenance: Some(provenance(\"retry-user\")),\n+ provenance: provenance(\"retry-user\"),\n web_url: Some(\"http://localhost:3000/runs/retry\".to_string()),\n })\n .await\n@@ -402,14 +402,7 @@ mod tests {\n assert_eq!(retry_state.spec.manifest_blob, manifest_blob);\n assert_eq!(retry_state.spec.definition_blob, definition_blob);\n assert_eq!(retry_state.spec.fork_source_ref, Some(fork_source_ref));\n- assert_eq!(\n- retry_state\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.as_ref()),\n- Some(&actor(\"retry-user\"))\n- );\n+ assert_eq!(retry_state.spec.provenance.subject, actor(\"retry-user\"));\n assert_eq!(\n retry_state.web_url.as_deref(),\n Some(\"http://localhost:3000/runs/retry\")\n@@ -463,7 +456,7 @@ mod tests {\n let outcome = retry_run(&store, &RetryRunInput {\n source_run_id,\n new_run_id: RunId::new(),\n- provenance: Some(provenance(\"retry-user\")),\n+ provenance: provenance(\"retry-user\"),\n web_url: None,\n })\n .await\n@@ -517,7 +510,7 @@ mod tests {\n let err = retry_run(&store, &RetryRunInput {\n source_run_id: run_id,\n new_run_id: RunId::new(),\n- provenance: None,\n+ provenance: provenance(\"retry-user\"),\n web_url: None,\n })\n .await\n@@ -535,7 +528,7 @@ mod tests {\n let err = retry_run(&store, &RetryRunInput {\n source_run_id: fixtures::RUN_1,\n new_run_id: RunId::new(),\n- provenance: None,\n+ provenance: provenance(\"retry-user\"),\n web_url: None,\n })\n .await\ndiff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs\nindex 8083350be..3ac123a9a 100644\n--- a/lib/crates/fabro-workflow/src/operations/start.rs\n+++ b/lib/crates/fabro-workflow/src/operations/start.rs\n@@ -1438,7 +1438,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1860,7 +1860,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\ndiff --git a/lib/crates/fabro-workflow/src/operations/timeline.rs b/lib/crates/fabro-workflow/src/operations/timeline.rs\nindex 68e0ee959..bfe0da88d 100644\n--- a/lib/crates/fabro-workflow/src/operations/timeline.rs\n+++ b/lib/crates/fabro-workflow/src/operations/timeline.rs\n@@ -248,7 +248,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\nindex d05398556..0deb440e5 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n@@ -165,7 +165,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -208,7 +208,7 @@ async fn seed_created_and_starting(\n workflow_slug: run_options.workflow_slug.clone(),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: run_options.pre_run_git.clone(),\n fork_source_ref: run_options.fork_source_ref.clone(),\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\nindex c677c1007..3b6344422 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n@@ -739,7 +739,7 @@ mod tests {\n workflow_slug: Some(\"metadata\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -856,7 +856,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\nindex cc1b0777a..b2894e2b6 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n@@ -773,7 +773,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs\nindex 7a8c896e4..64ef56a2e 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs\n@@ -148,7 +148,7 @@ mod tests {\n (\"env\".to_string(), \"test\".to_string()),\n (\"team\".to_string(), \"workflow\".to_string()),\n ]),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\nindex c02376988..79433cb79 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n@@ -823,7 +823,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1148,7 +1148,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1219,7 +1219,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1575,7 +1575,7 @@ mod tests {\n source_directory: Some(tmp.path().display().to_string()),\n git: None,\n labels: std::collections::HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1704,7 +1704,7 @@ mod tests {\n source_directory: Some(\"/tmp/project\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1722,7 +1722,7 @@ mod tests {\n workflow_slug: run_spec.workflow_slug.clone(),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_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: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1893,7 +1893,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs\nindex e7b0badfc..e62e17fbe 100644\n--- a/lib/crates/fabro-workflow/src/run_lookup.rs\n+++ b/lib/crates/fabro-workflow/src/run_lookup.rs\n@@ -491,7 +491,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/run_metadata.rs b/lib/crates/fabro-workflow/src/run_metadata.rs\nindex b11c9667c..40c42a05f 100644\n--- a/lib/crates/fabro-workflow/src/run_metadata.rs\n+++ b/lib/crates/fabro-workflow/src/run_metadata.rs\n@@ -639,7 +639,7 @@ mod tests {\n push_outcome: PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs\nindex 0dafbfdad..da3cfb099 100644\n--- a/lib/crates/fabro-workflow/src/runtime_store.rs\n+++ b/lib/crates/fabro-workflow/src/runtime_store.rs\n@@ -148,7 +148,7 @@ mod tests {\n source_directory: Some(\"/tmp/test\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -170,7 +170,7 @@ mod tests {\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs\nindex e72a0ee1b..c138e4e6e 100644\n--- a/lib/crates/fabro-workflow/src/test_support.rs\n+++ b/lib/crates/fabro-workflow/src/test_support.rs\n@@ -10,8 +10,21 @@ use fabro_graphviz::graph::Graph as GvGraph;\n use fabro_interview::AutoApproveInterviewer;\n use fabro_model::Catalog;\n use fabro_store::{ArtifactStore, Database, RunProjection};\n+use fabro_types::{AuthMethod, IdpIdentity, Principal, RunProvenance};\n use object_store::local::LocalFileSystem;\n \n+fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::user(\n+ IdpIdentity::new(\"fabro:test\", \"test-user\").expect(\"test identity should parse\"),\n+ \"test\".to_string(),\n+ AuthMethod::DevToken,\n+ ),\n+ }\n+}\n+\n use crate::artifact_upload::ArtifactSink;\n use crate::error::{Error, Result};\n use crate::event::{Emitter, Event, StoreProgressLogger, append_event};\n@@ -175,7 +188,7 @@ async fn initialized(\n workflow_slug: run_options.workflow_slug.clone(),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n git: run_options.pre_run_git.clone(),\n fork_source_ref: run_options.fork_source_ref.clone(),\ndiff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex 5e97116d9..7839b95b6 100644\n--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n+++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n@@ -271,7 +271,6 @@ models/preflight-workflow-summary.ts\n models/preview-url-request.ts\n models/preview-url-response.ts\n models/principal-agent.ts\n-models/principal-anonymous.ts\n models/principal-slack.ts\n models/principal-system.ts\n models/principal-user.ts\ndiff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts\nindex 87b9c189c..af8ae77e0 100644\n--- a/lib/packages/fabro-api-client/src/models/index.ts\n+++ b/lib/packages/fabro-api-client/src/models/index.ts\n@@ -244,7 +244,6 @@ export * from './preview-url-request';\n export * from './preview-url-response';\n export * from './principal';\n export * from './principal-agent';\n-export * from './principal-anonymous';\n export * from './principal-slack';\n export * from './principal-system';\n export * from './principal-user';\ndiff --git a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts b/lib/packages/fabro-api-client/src/models/principal-anonymous.ts\ndeleted file mode 100644\nindex daac61df0..000000000\n--- a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts\n+++ /dev/null\n@@ -1,25 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-\n-export interface PrincipalAnonymous {\n- 'kind': PrincipalAnonymousKindEnum;\n-}\n-\n-export const PrincipalAnonymousKindEnum = {\n- ANONYMOUS: 'anonymous'\n-} as const;\n-\n-export type PrincipalAnonymousKindEnum = typeof PrincipalAnonymousKindEnum[keyof typeof PrincipalAnonymousKindEnum];\ndiff --git a/lib/packages/fabro-api-client/src/models/principal.ts b/lib/packages/fabro-api-client/src/models/principal.ts\nindex e3597295d..08b5422df 100644\n--- a/lib/packages/fabro-api-client/src/models/principal.ts\n+++ b/lib/packages/fabro-api-client/src/models/principal.ts\n@@ -24,9 +24,6 @@ import type { IdpIdentity } from './idp-identity';\n import type { PrincipalAgent } from './principal-agent';\n // May contain unused imports in some cases\n // @ts-ignore\n-import type { PrincipalAnonymous } from './principal-anonymous';\n-// May contain unused imports in some cases\n-// @ts-ignore\n import type { PrincipalSlack } from './principal-slack';\n // May contain unused imports in some cases\n // @ts-ignore\n@@ -47,4 +44,4 @@ import type { SystemActorKind } from './system-actor-kind';\n /**\n * @type Principal\n */\n-export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'anonymous' } & PrincipalAnonymous | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker;\n+export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker;\ndiff --git a/lib/packages/fabro-api-client/src/models/run-provenance.ts b/lib/packages/fabro-api-client/src/models/run-provenance.ts\nindex 7276857f8..59fa7a063 100644\n--- a/lib/packages/fabro-api-client/src/models/run-provenance.ts\n+++ b/lib/packages/fabro-api-client/src/models/run-provenance.ts\n@@ -26,5 +26,5 @@ import type { RunServerProvenance } from './run-server-provenance';\n export interface RunProvenance {\n 'server'?: RunServerProvenance | null;\n 'client'?: RunClientProvenance | null;\n- 'subject'?: Principal | null;\n+ 'subject': Principal;\n }\ndiff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts\nindex f1e5adab6..6abad2884 100644\n--- a/lib/packages/fabro-api-client/src/models/run-spec.ts\n+++ b/lib/packages/fabro-api-client/src/models/run-spec.ts\n@@ -41,7 +41,7 @@ export interface RunSpec {\n 'automation'?: AutomationRef | null;\n 'source_directory'?: string | null;\n 'labels'?: { [key: string]: string; };\n- 'provenance'?: RunProvenance | null;\n+ 'provenance': RunProvenance;\n 'manifest_blob'?: string | null;\n 'definition_blob'?: string | null;\n 'git'?: GitContext | null;\ndiff --git a/lib/packages/fabro-api-client/src/models/run.ts b/lib/packages/fabro-api-client/src/models/run.ts\nindex 1ed6c6c26..a4ade38a3 100644\n--- a/lib/packages/fabro-api-client/src/models/run.ts\n+++ b/lib/packages/fabro-api-client/src/models/run.ts\n@@ -83,7 +83,7 @@ export interface Run {\n 'workflow': WorkflowRef;\n 'automation': AutomationRef | null;\n 'repository': RepositoryRef | null;\n- 'created_by': Principal | null;\n+ 'created_by': Principal;\n 'origin': RunOrigin;\n 'labels': { [key: string]: string; };\n 'lifecycle': RunLifecycle;\ndiff --git a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\nindex 114b1e640..5fff8b8f4 100644\n--- a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n+++ b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n@@ -12,8 +12,6 @@ export function principalKind(principal: Principal): string {\n switch (principal.kind) {\n case \"agent\":\n return \"agent\";\n- case \"anonymous\":\n- return \"anonymous\";\n case \"slack\":\n return \"slack\";\n case \"system\":\n", + "summary": { + "files_changed": 95, + "additions": 456, + "deletions": 286 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-06-03T14:02:55.491146Z", + "current_node": "simplify_gpt", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "implement", + "simplify_opus", + "simplify_gpt" + ], + "node_retries": {}, + "context_values": { + "internal.retry_count.simplify_gpt": 0, + "internal.thread_id": "simplify_opus", + "thread.preflight_compile.current_node": "preflight_lint", + "internal.node_visit_count": 1, + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "current_node": "simplify_gpt", + "internal.work_dir": "/home/daytona/workspace/fabro", + "internal.fidelity": "compact", + "graph.rankdir": "LR", + "internal.retry_count.preflight_lint": 0, + "internal.retry_count.toolchain": 0, + "response.simplify_opus": "Both commits are done. Summary:\n\n**Commit 1** (`refactor: remove Principal::Anonymous, use Option for absent auth`): Dropped `Principal::Anonymous` variant. Auth middleware now uses `Option` for request context. HTTP logs emit `principal_kind=\"none\"` for unauthenticated requests. Updated OpenAPI spec, regenerated TS client, removed frontend anonymous icon/case.\n\n**Commit 2** (`refactor: make Run.created_by, RunSpec.provenance, and RunProvenance.subject total`): Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, `RunCreatedProps.provenance`, and `Event::RunCreated.provenance` non-optional. Threaded actual `RunProvenance` through `CreateRunInput`, `RetryRunInput`, and `PersistCreateOptions`. Added `fabro_types::test_support::{test_principal, test_run_provenance}` behind the existing `test-support` feature for tests. Added a clearly synthetic `DEMO_PRINCIPAL` (issuer `fabro:demo`, dev-token) in demo mode. Updated OpenAPI spec to require `subject`/`provenance`/`created_by`. Regenerated the TS client and updated the frontend to treat `createdBy` as total inside loaded-run branches. Backward-compatibility tests for null provenance/creator were either deleted or updated to include the total provenance.\n\nVerification:\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` — 6499 passed, 0 failed, 181 skipped ✓\n- `cd apps/fabro-web && bun run typecheck` ✓\n- `cd apps/fabro-web && bun test` — 508 passed, 0 failed ✓\n- `cd apps/fabro-web && bun run build` ✓\n- `cd lib/packages/fabro-api-client && bun run typecheck` ✓\n\nManual end-to-end verification (server start, UI run creation, demo header) was not exercised in this run.", + "outcome": "failed", + "internal.run_id": "01KT6RK0RM223N7PJQ47XXTKVJ", + "thread.toolchain.current_node": "preflight_compile", + "last_stage": "simplify_opus", + "failure_signature": "simplify_gpt|deterministic|api_deterministic|openai|authentication", + "internal.retry_count.start": 0, + "internal.retry_count.preflight_compile": 0, + "last_response": "Both commits are done. Summary:\n\n**Commit 1** (`refactor: remove Principal::Anonymous, use Option for absent auth`): Dropped `Principal::Anonymous` variant. Auth middleware now uses `Option", + "failure_class": "deterministic", + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "thread.preflight_lint.current_node": "implement", + "thread.implement.current_node": "simplify_opus", + "internal.retry_count.implement": 0, + "thread.simplify_opus.current_node": "simplify_gpt", + "thread.start.current_node": "toolchain", + "graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option`, no nullable OpenAPI fields, no legacy deserialization defaults, and no fallback creator in projection code.\n\nTwo commits, in order.\n\n---\n\n## Commit 1 - Remove `Principal::Anonymous`\n\nBreaking cleanup. `Principal` becomes actor-only. Missing/invalid auth is represented as absent request principal, not as an anonymous principal variant.\n\n### Rust\n\n`lib/crates/fabro-types/src/principal.rs`:\n- Drop `Anonymous`.\n- Drop `Anonymous` arms in `kind()` and `display()`.\n- Delete anonymous serialization/round-trip test coverage.\n\n`lib/crates/fabro-server/src/principal_middleware.rs`:\n- `RequestAuthContext.principal: Principal` -> `Option`.\n- `RequestAuthLogContext.principal: Principal` -> `Option`.\n- `initial()` and `rejected()` set `principal: None`.\n- `authenticated(...)`, `authenticated_worker(...)`, and `authenticated_user(...)` set `principal: Some(...)`.\n- Update `principal_without_log_unused_fields` to preserve `None` and strip user avatar data only inside `Some(Principal::User(...))`.\n- Update all gate helpers to match `Option`:\n - `require_user`\n - `require_authenticated_user`\n - `require_run_management_actor`\n - `require_worker_or_user_for_run`\n - `require_run_management_target`\n- `None` routes to the existing `auth_rejection(context.auth_status, context.auth_error_code)` behavior.\n- `Some(Principal::Worker { .. })` keeps the current forbidden-vs-auth-rejection distinctions.\n- Update tests that assert the initial/rejected principal to assert `None`.\n\n`lib/crates/fabro-server/src/server.rs` HTTP logging:\n- Keep the `principal_kind` field on every HTTP log line.\n- Compute `principal_kind` as `auth_context.principal.as_ref().map(Principal::kind).unwrap_or(\"none\")`.\n- Match `auth_context.principal` as an `Option`:\n - `Some(User(...))`, `Some(Worker { ... })`, `Some(Webhook { ... })`, `Some(Slack { ... })` keep their extra fields.\n - `None | Some(Agent { .. } | System { .. })` emits only the common HTTP fields.\n\n`docs/internal/logging-strategy.md`:\n- Replace the `anonymous` HTTP caller category guidance with `none` for requests that have no principal.\n- Keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state.\n\n### OpenAPI and generated clients\n\n`docs/public/api-reference/fabro-api.yaml`:\n- Remove `PrincipalAnonymous` from the `Principal` `oneOf`.\n- Remove `anonymous` from the `Principal` discriminator mapping.\n- Delete the `PrincipalAnonymous` schema.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nExpected generated cleanup:\n- `lib/packages/fabro-api-client/src/models/principal-anonymous.ts` disappears.\n- `Principal` union no longer includes `{ kind: \"anonymous\" }`.\n- `lib/packages/fabro-api-client/src/models/index.ts` no longer exports `principal-anonymous`.\n\n### Frontend\n\n`apps/fabro-web/app/lib/principal-display.tsx`:\n- Remove the `\"anonymous\"` switch case and unused icon import.\n\n`apps/fabro-web/app/components/run-summary-panel.test.tsx` and API-client exhaustiveness tests:\n- Remove anonymous principal cases.\n\n### Documentation sweep\n\nRemove anonymous-principal references from product/API docs and tests. Be careful not to touch unrelated uses of \"anonymous\" such as telemetry anonymous IDs or Git's `remote_anonymous` API.\n\nUseful sweep:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|kind: 'anonymous'|kind: \\\"anonymous\\\"|anonymous actor|anonymous subject|principal_kind.*anonymous|\\\"anonymous\\\"\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cd apps/fabro-web && bun run typecheck && bun test`\n- Manual: start `fabro server start`, hit a protected endpoint without a token, confirm 401 and an HTTP log with `principal_kind=\"none\"` and `auth_status=\"missing\"`.\n\n---\n\n## Commit 2 - Make run provenance and creator non-optional\n\nFull-chain invariant. Every persisted run has exactly one creator principal. No nullable schema fields, no legacy defaults, no projection fallbacks.\n\n### Core type changes\n\n`lib/crates/fabro-types/src/run_summary.rs`:\n- `Run.created_by: Option` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option` -> `RunProvenance`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]` on `RunSpec.provenance`.\n\n`lib/crates/fabro-types/src/run_event/run.rs`:\n- `RunCreatedProps.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n`lib/crates/fabro-workflow/src/event/events.rs`:\n- `Event::RunCreated.provenance: Option` -> `RunProvenance`.\n- Drop default/skip serialization attributes for provenance.\n\n### Creation and retry flow\n\n`lib/crates/fabro-workflow/src/operations/create.rs`:\n- `CreateRunInput.provenance: Option` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option` -> `RunProvenance`.\n- `RunSpec { provenance }` stores the total provenance directly.\n- `Event::RunCreated { provenance }` emits total provenance directly.\n\n`lib/crates/fabro-server/src/server/handler/runs.rs`:\n- `run_provenance(headers, subject)` returns `RunProvenance { subject: subject.clone(), ... }`.\n- Build provenance before creating `CreateRunInput`.\n\n`lib/crates/fabro-server/src/run_manifest.rs`:\n- Change `create_run_input(...)` to accept `provenance: RunProvenance` and set it directly, or stop using the helper for the final `CreateRunInput` construction. Do not create a temporary input with missing provenance.\n\n`lib/crates/fabro-workflow/src/operations/retry.rs`:\n- `RetryRunInput.provenance: Option` -> `RunProvenance`.\n- `retry_run(...)` writes the new run's `run.created` event with total provenance.\n\n`lib/crates/fabro-server/src/server/handler/lifecycle.rs`:\n- Pass `run_provenance(&headers, &actor)` directly into `RetryRunInput`.\n\n### Event conversion and projections\n\n`lib/crates/fabro-workflow/src/event/convert.rs`:\n- Convert `Event::RunCreated.provenance` into `RunCreatedProps.provenance` directly.\n- Remove `Some(...)` wrapping for run-created provenance.\n\n`lib/crates/fabro-workflow/src/event/stored_fields.rs`:\n- `Event::RunCreated { provenance, .. }` sets `actor: Some(provenance.subject.clone())`.\n\n`lib/crates/fabro-store/src/run_state.rs`:\n- `projection_from_created(...)` builds `RunSpec { provenance: props.provenance.clone(), ... }`.\n- `build_summary(...)` sets `created_by: state.spec.provenance.subject.clone()`.\n- Delete or rewrite tests that deserialize projections with `\"provenance\": null`.\n\n`lib/crates/fabro-types/src/run_projection.rs` and projection tests:\n- Replace all test `RunSpec` literals with total provenance.\n- Remove tests whose only purpose is legacy/null provenance tolerance.\n\n### OpenAPI\n\n`docs/public/api-reference/fabro-api.yaml`:\n- `Run.created_by` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunProvenance.required` includes `subject`.\n- `RunProvenance.subject` references `Principal` directly. Remove `oneOf [..., null]`.\n- `RunSpec.required` includes `provenance`.\n- `RunSpec.provenance` references `RunProvenance` directly. Remove `oneOf [..., null]`.\n- If `run.created` event properties are represented separately in the spec, make that event provenance required and non-nullable too.\n\nRegenerate:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n\nDo not hand-edit generated client files.\n\n### Demo mode\n\n`lib/crates/fabro-server/src/demo/mod.rs`:\n- Add a clearly synthetic demo principal using `AuthMethod::DevToken`, not GitHub:\n ```rust\n static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| {\n Principal::user(\n IdpIdentity::new(\"fabro:demo\", \"demo\").unwrap(),\n \"demo\".to_string(),\n AuthMethod::DevToken,\n )\n });\n ```\n- Replace `created_by: None` with `created_by: DEMO_PRINCIPAL.clone()`.\n- If demo creates any full `RunSpec` or `run.created` event data, give it `RunProvenance { subject: DEMO_PRINCIPAL.clone(), ... }`.\n\n### Test support\n\nDo not add fake auth helpers to `fabro_types::fixtures`; that module is run-id constants.\n\nUse the existing `fabro-types` `test-support` feature:\n- Add `#[cfg(any(test, feature = \"test-support\"))] pub mod test_support;` in `lib/crates/fabro-types/src/lib.rs` if it does not already exist.\n- Add `lib/crates/fabro-types/src/test_support.rs` with:\n - `test_principal() -> Principal`\n - `test_run_provenance() -> RunProvenance`\n- Use an obviously fake dev-token identity, e.g. issuer `fabro:test`, subject `test-user`, login `test`.\n- In crates that need the helper from integration tests or cross-crate tests, dual-list `fabro-types` in `dev-dependencies` with `features = [\"test-support\"]`, following existing repo patterns.\n\nUpdate all constructors:\n- Replace `provenance: None` in `RunSpec`, `CreateRunInput`, `RetryRunInput`, `Event::RunCreated`, and `RunCreatedProps` literals with `test_run_provenance()` or a locally meaningful provenance.\n- Replace `subject: Some(...)` with `subject: ...`.\n- Replace `subject: None` only when it is actually `RunProvenance.subject`; leave unrelated todo/commit/message `subject` fields alone.\n- Replace `created_by: None` / `created_by: null` with `test_principal()` or a frontend TS principal fixture.\n- Delete tests that assert nullable or omitted creator/provenance behavior.\n\nRepresentative Rust areas:\n- `lib/crates/fabro-store/src/run_state.rs`\n- `lib/crates/fabro-store/tests/serializable_projection.rs`\n- `lib/crates/fabro-workflow/src/operations/{create,retry,start}.rs`\n- `lib/crates/fabro-workflow/src/event/{convert,sink,stored_fields}.rs`\n- `lib/crates/fabro-workflow/src/handler/**`\n- `lib/crates/fabro-workflow/src/pipeline/**`\n- `lib/crates/fabro-workflow/src/run_{lookup,metadata}.rs`\n- `lib/crates/fabro-server/src/server/tests.rs`\n- `lib/crates/fabro-server/src/server/handler/**`\n- `lib/crates/fabro-server/tests/it/**`\n- `lib/crates/fabro-cli/tests/it/support/mod.rs`\n- `lib/crates/fabro-dump/src/lib.rs`\n- `lib/crates/fabro-tool/src/{common,create,interact,search}.rs`\n- `lib/crates/fabro-api/tests/{principal_round_trip,run_summary_round_trip,run_projection_round_trip,run_event_round_trip}.rs`\n- `lib/crates/fabro-types/tests/{run_spec_serde,run_spec_methods,run_event_serde}.rs`\n\nRepresentative TypeScript areas:\n- `apps/fabro-web/app/**` tests with `created_by: null`\n- `apps/fabro-web/app/data/runs.ts`\n- `apps/fabro-web/app/components/run-summary-panel.tsx`\n- `apps/fabro-web/app/components/runs-list/**`\n- `lib/packages/fabro-api-client/tests/principal-exhaustive.ts`\n\nUseful sweep after edits:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|principal-anonymous|kind: ['\\\"]anonymous|created_by:\\\\s*(None|null)|provenance:\\\\s*None|subject:\\\\s*Some\\\\(|subject:\\\\s*None\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\nReview each hit. The only acceptable remaining matches should be unrelated uses of \"anonymous\" and unrelated non-principal `subject` fields.\n\n### Frontend\n\n`apps/fabro-web/app/components/run-summary-panel.tsx`:\n- `run?.created_by` may still be guarded by `run` loading state, but `created_by` itself is non-null once `run` exists.\n- Pass `run.created_by` directly to `principalDisplay(...)` inside loaded-run branches.\n\n`apps/fabro-web/app/data/runs.ts` and run-list components:\n- Treat `createdBy` as a total principal in UI data derived from a loaded API run.\n- Remove empty/fallback rendering that only existed for missing creator data.\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Manual end-to-end:\n - `fabro server start`\n - `cd apps/fabro-web && bun run dev`\n - Authenticate and create a run through the UI.\n - Confirm `/api/v1/runs/:id` has non-null `created_by`.\n - Confirm `/api/v1/runs/:id/state` has non-null `spec.provenance.subject`.\n - Retry a failed run and confirm the retried run has the retrying user as creator.\n - Hit demo mode with `X-Fabro-Demo: 1` and confirm the run summary renders the synthetic `demo` dev-token user.\n", + "internal.retry_count.simplify_opus": 0 + }, + "node_outcomes": { "implement": { "status": "failed", "failure": { @@ -1052,6 +1178,98 @@ "status": "succeeded", "usage": null }, + "simplify_opus": { + "status": "succeeded", + "context_updates": { + "last_stage": "simplify_opus", + "last_response": "Both commits are done. Summary:\n\n**Commit 1** (`refactor: remove Principal::Anonymous, use Option for absent auth`): Dropped `Principal::Anonymous` variant. Auth middleware now uses `Option", + "response.simplify_opus": "Both commits are done. Summary:\n\n**Commit 1** (`refactor: remove Principal::Anonymous, use Option for absent auth`): Dropped `Principal::Anonymous` variant. Auth middleware now uses `Option` for request context. HTTP logs emit `principal_kind=\"none\"` for unauthenticated requests. Updated OpenAPI spec, regenerated TS client, removed frontend anonymous icon/case.\n\n**Commit 2** (`refactor: make Run.created_by, RunSpec.provenance, and RunProvenance.subject total`): Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, `RunCreatedProps.provenance`, and `Event::RunCreated.provenance` non-optional. Threaded actual `RunProvenance` through `CreateRunInput`, `RetryRunInput`, and `PersistCreateOptions`. Added `fabro_types::test_support::{test_principal, test_run_provenance}` behind the existing `test-support` feature for tests. Added a clearly synthetic `DEMO_PRINCIPAL` (issuer `fabro:demo`, dev-token) in demo mode. Updated OpenAPI spec to require `subject`/`provenance`/`created_by`. Regenerated the TS client and updated the frontend to treat `createdBy` as total inside loaded-run branches. Backward-compatibility tests for null provenance/creator were either deleted or updated to include the total provenance.\n\nVerification:\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` — 6499 passed, 0 failed, 181 skipped ✓\n- `cd apps/fabro-web && bun run typecheck` ✓\n- `cd apps/fabro-web && bun test` — 508 passed, 0 failed ✓\n- `cd apps/fabro-web && bun run build` ✓\n- `cd lib/packages/fabro-api-client && bun run typecheck` ✓\n\nManual end-to-end verification (server start, UI run creation, demo header) was not exercised in this run." + }, + "notes": "Stage completed: simplify_opus", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 316731, + "output_tokens": 99246, + "reasoning_tokens": 0, + "cache_read_tokens": 72603628, + "cache_write_tokens": 8610746 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 8610746, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 94183781 + }, + "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/data/runs.ts", + "/home/daytona/workspace/fabro/apps/fabro-web/app/lib/principal-display.tsx", + "/home/daytona/workspace/fabro/docs/internal/logging-strategy.md", + "/home/daytona/workspace/fabro/docs/public/api-reference/fabro-api.yaml", + "/home/daytona/workspace/fabro/lib/crates/fabro-api/Cargo.toml", + "/home/daytona/workspace/fabro/lib/crates/fabro-api/tests/principal_round_trip.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-api/tests/run_event_round_trip.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-api/tests/run_summary_round_trip.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-cli/Cargo.toml", + "/home/daytona/workspace/fabro/lib/crates/fabro-cli/tests/it/cmd/inspect.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-cli/tests/it/cmd/support.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-dump/src/lib.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/auth/cli_flow.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/demo/mod.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/principal_middleware.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/run_manifest.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/lifecycle.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/runs.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/sandbox.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/tests.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/web_auth.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-store/Cargo.toml", + "/home/daytona/workspace/fabro/lib/crates/fabro-store/src/run_state.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-store/src/slate/mod.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-store/src/slate/run_store.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-store/tests/serializable_projection.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-tool/Cargo.toml", + "/home/daytona/workspace/fabro/lib/crates/fabro-tool/src/common.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-tool/src/create.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-tool/src/interact.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-tool/src/search.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/lib.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/principal.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/mod.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/run.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_projection.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_summary.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_event_serde.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_methods.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_serde.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/convert.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/events.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/stored_fields.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/operations/create.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/operations/retry.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/test_support.rs", + "/home/daytona/workspace/fabro/lib/packages/fabro-api-client/tests/principal-exhaustive.ts" + ], + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 2040215, + "tool_time_ms": 1930498, + "active_time_ms": 3970713 + } + }, "toolchain": { "status": "succeeded", "context_updates": { @@ -1080,6 +1298,15 @@ "active_time_ms": 136821 } }, + "simplify_gpt": { + "status": "failed", + "failure": { + "message": "LLM error: Authentication error for openai: Your authentication token has been invalidated. Please try signing in again.", + "category": "deterministic", + "signature": "api_deterministic|openai|authentication" + }, + "usage": null + }, "preflight_lint": { "status": "succeeded", "context_updates": { @@ -1095,14 +1322,15 @@ } } }, - "next_node_id": "simplify_gpt", + "next_node_id": "verify", "node_visits": { "preflight_lint": 1, "toolchain": 1, "preflight_compile": 1, "start": 1, "implement": 1, - "simplify_opus": 1 + "simplify_opus": 1, + "simplify_gpt": 1 } }, "diff": {} @@ -1432,6 +1660,162 @@ }, "state": "succeeded" }, + "simplify_gpt@1": { + "first_event_seq": 1489, + "prompt": null, + "response": null, + "completion": null, + "provider_used": { + "mode": "agent", + "provider": "openai", + "model": "gpt-5.5" + }, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "started_at": "2026-06-03T14:02:54.931852Z", + "handler": "agent", + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + "reasoning_tokens": 0, + "cache_read_tokens": 0, + "cache_write_tokens": 0 + }, + "permission_level": "full", + "agent_tools": [ + { + "name": "apply_patch", + "description": "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.", + "source": { + "kind": "native" + }, + "category": "write", + "invoked": false + }, + { + "name": "close_agent", + "description": "Close a running subagent that is no longer needed.", + "source": { + "kind": "native" + }, + "category": "subagent", + "invoked": false + }, + { + "name": "glob", + "description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.", + "source": { + "kind": "native" + }, + "category": "read", + "invoked": false + }, + { + "name": "grep", + "description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.", + "source": { + "kind": "native" + }, + "category": "read", + "invoked": false + }, + { + "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": false + }, + { + "name": "request_user_input", + "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": "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": false + }, + { + "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": false + }, + { + "name": "update_plan", + "description": "Update the multi-step plan for the current task. Submit the entire plan; existing steps are reconciled by exact step text.", + "source": { + "kind": "native" + }, + "category": "other", + "invoked": false + }, + { + "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": false + }, + { + "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 + } + ], + "state": "running" + }, "preflight_lint@1": { "first_event_seq": 41, "prompt": null, @@ -1484,7 +1868,12 @@ "first_event_seq": 68, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Stage completed: simplify_opus", + "failure_reason": null, + "timestamp": "2026-06-03T14:02:48.741229Z" + }, "provider_used": { "mode": "agent", "provider": "anthropic", @@ -1497,6 +1886,12 @@ "output": null, "started_at": "2026-06-03T12:56:31.527704Z", "handler": "agent", + "timing": { + "wall_time_ms": 3977185, + "inference_time_ms": 2040215, + "tool_time_ms": 1930498, + "active_time_ms": 3970713 + }, "usage": { "input_tokens": 316731, "output_tokens": 99246, @@ -1737,7 +2132,7 @@ ], "warnings": [] }, - "state": "running" + "state": "succeeded" } } } \ No newline at end of file diff --git a/stages/006-simplify_opus@1/diff.patch b/stages/006-simplify_opus@1/diff.patch new file mode 100644 index 000000000..b950baf57 --- /dev/null +++ b/stages/006-simplify_opus@1/diff.patch @@ -0,0 +1,2582 @@ +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..d1f03ff59 100644 +--- a/apps/fabro-web/app/components/run-summary-panel.test.tsx ++++ b/apps/fabro-web/app/components/run-summary-panel.test.tsx +@@ -53,7 +53,12 @@ function cellAfterLabel( + function makeRun(overrides: Record = {}) { + return { + id: "run_1", +- created_by: null, ++ created_by: { ++ kind: "user", ++ identity: { issuer: "fabro:test", subject: "test-user" }, ++ login: "test", ++ auth_method: "dev_token", ++ }, + diff: null, + billing: null, + ...overrides, +@@ -73,7 +78,6 @@ 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, "Changes"))).toBe(EMPTY_VALUE); + expect(instanceText(cellAfterLabel(tree, "Cost"))).toBe(EMPTY_VALUE); + }); +@@ -252,7 +256,7 @@ describe("RunSummaryPanelView", () => { + }); + + test("renders non-user actor with kind label", () => { +- for (const kind of ["agent", "system", "slack", "webhook", "worker", "anonymous"]) { ++ for (const kind of ["agent", "system", "slack", "webhook", "worker"]) { + const tree = render({ run: makeRun({ created_by: { kind } as any }) }); + expect(instanceText(cellAfterLabel(tree, "Created by"))).toContain(kind); + } +diff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx +index 20a08af5b..177270630 100644 +--- a/apps/fabro-web/app/components/run-summary-panel.tsx ++++ b/apps/fabro-web/app/components/run-summary-panel.tsx +@@ -116,7 +116,7 @@ export function RunSummaryPanelView({ + artifactsCount, + artifactsLoading, + }: RunSummaryPanelViewProps) { +- const created = run?.created_by ? principalDisplay(run.created_by) : null; ++ const created = run ? principalDisplay(run.created_by) : null; + const diff = run?.diff ?? null; + const cost = formatUsdMicros(run?.billing?.total_usd_micros); + const sandboxKind = sandboxLifecycleKind(run?.sandbox); +diff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts +index 98586b2c4..0f94de98b 100644 +--- a/apps/fabro-web/app/data/runs.test.ts ++++ b/apps/fabro-web/app/data/runs.test.ts +@@ -17,7 +17,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: { kind: "user", identity: { issuer: "fabro:test", subject: "test-user" }, login: "test", auth_method: "dev_token" }, + 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..d49aac4d4 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..c3d8d73a8 100644 +--- a/apps/fabro-web/app/lib/run-actions.test.ts ++++ b/apps/fabro-web/app/lib/run-actions.test.ts +@@ -47,7 +47,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: { kind: "user", identity: { issuer: "fabro:test", subject: "test-user" }, login: "test", auth_method: "dev_token" }, + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/apps/fabro-web/app/routes/automations-new.test.tsx b/apps/fabro-web/app/routes/automations-new.test.tsx +index 47f471abf..240cf7c85 100644 +--- a/apps/fabro-web/app/routes/automations-new.test.tsx ++++ b/apps/fabro-web/app/routes/automations-new.test.tsx +@@ -120,7 +120,7 @@ function makeRun(overrides: Record = {}) { + origin_url: "https://github.com/fallback/repo.git", + provider: "github", + }, +- created_by: null, ++ created_by: { kind: "user", identity: { issuer: "fabro:test", subject: "test-user" }, login: "test", auth_method: "dev_token" }, + 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..391c50ee3 100644 +--- a/apps/fabro-web/app/routes/run-detail.test.ts ++++ b/apps/fabro-web/app/routes/run-detail.test.ts +@@ -221,7 +221,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: { kind: "user", identity: { issuer: "fabro:test", subject: "test-user" }, login: "test", auth_method: "dev_token" }, + 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..c0c89a246 100644 +--- a/apps/fabro-web/app/routes/run-files.render.test.tsx ++++ b/apps/fabro-web/app/routes/run-files.render.test.tsx +@@ -51,7 +51,7 @@ mock.module("../lib/queries", () => ({ + workflow: { slug: "default", name: "Default", graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: "fabro", origin_url: null, provider: "unknown" }, +- created_by: null, ++ created_by: { kind: "user", identity: { issuer: "fabro:test", subject: "test-user" }, login: "test", auth_method: "dev_token" }, + 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..c13226b0c 100644 +--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx ++++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx +@@ -35,7 +35,7 @@ function run(id: string, repo = "qlty/fabro", workflow = "release"): Run { + workflow: { slug: workflow, name: workflow, graph_name: null, node_count: 0, edge_count: 0 }, + automation: null, + repository: { name: repo, origin_url: null, provider: "github" }, +- created_by: null, ++ created_by: { kind: "user", identity: { issuer: "fabro:test", subject: "test-user" }, login: "test", auth_method: "dev_token" }, + 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..92404782c 100644 +--- a/apps/fabro-web/app/routes/runs.test.tsx ++++ b/apps/fabro-web/app/routes/runs.test.tsx +@@ -34,7 +34,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: { kind: "user", identity: { issuer: "fabro:test", subject: "test-user" }, login: "test", auth_method: "dev_token" }, + origin: { kind: "api" }, + labels: {}, + lifecycle: { +diff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md +index 63f6f8f54..728a7c031 100644 +--- a/docs/internal/logging-strategy.md ++++ b/docs/internal/logging-strategy.md +@@ -118,7 +118,7 @@ Fields are key-value pairs that make events queryable. Include enough context th + | `error` | Error value on failure | + | `path` | File system path | + | `duration_ms` | Elapsed time in milliseconds | +-| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) | ++| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) | + | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) | + | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests | + +diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml +index 35d7511e5..bfb3e66a1 100644 +--- a/docs/public/api-reference/fabro-api.yaml ++++ b/docs/public/api-reference/fabro-api.yaml +@@ -8992,6 +8992,8 @@ components: + + RunProvenance: + type: object ++ required: ++ - subject + properties: + server: + oneOf: +@@ -9002,9 +9004,7 @@ components: + - $ref: "#/components/schemas/RunClientProvenance" + - type: "null" + subject: +- oneOf: +- - $ref: "#/components/schemas/Principal" +- - type: "null" ++ $ref: "#/components/schemas/Principal" + + Principal: + oneOf: +@@ -9014,7 +9014,6 @@ components: + - $ref: "#/components/schemas/PrincipalSlack" + - $ref: "#/components/schemas/PrincipalAgent" + - $ref: "#/components/schemas/PrincipalSystem" +- - $ref: "#/components/schemas/PrincipalAnonymous" + discriminator: + propertyName: kind + mapping: +@@ -9024,7 +9023,6 @@ components: + slack: "#/components/schemas/PrincipalSlack" + agent: "#/components/schemas/PrincipalAgent" + system: "#/components/schemas/PrincipalSystem" +- anonymous: "#/components/schemas/PrincipalAnonymous" + + PrincipalUser: + type: object +@@ -9114,15 +9112,6 @@ components: + system_kind: + $ref: "#/components/schemas/SystemActorKind" + +- PrincipalAnonymous: +- type: object +- required: +- - kind +- properties: +- kind: +- type: string +- enum: [anonymous] +- + RunEvent: + description: > + Internal RunEvent-compatible JSON payload. The server validates this +@@ -10250,6 +10239,7 @@ components: + - run_id + - settings + - graph ++ - provenance + properties: + run_id: + type: string +@@ -10273,9 +10263,7 @@ components: + additionalProperties: + type: string + provenance: +- oneOf: +- - $ref: "#/components/schemas/RunProvenance" +- - type: "null" ++ $ref: "#/components/schemas/RunProvenance" + manifest_blob: + type: ["string", "null"] + definition_blob: +@@ -10587,9 +10575,7 @@ components: + - $ref: "#/components/schemas/RepositoryRef" + - type: "null" + created_by: +- oneOf: +- - $ref: "#/components/schemas/Principal" +- - type: "null" ++ $ref: "#/components/schemas/Principal" + origin: + $ref: "#/components/schemas/RunOrigin" + labels: +diff --git a/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..370c33c29 100644 +--- a/lib/crates/fabro-api/tests/run_event_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs +@@ -9,6 +9,17 @@ fn run_event_reuses_canonical_type() { + assert_same_type::(); + } + ++fn test_provenance_json() -> Value { ++ json!({ ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ }) ++} ++ + #[test] + fn run_event_round_trips_run_created() { + let value = json!({ +@@ -20,7 +31,8 @@ fn run_event_round_trips_run_created() { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/fabro/run-1", +- "source_directory": "/tmp/fabro/run-1" ++ "source_directory": "/tmp/fabro/run-1", ++ "provenance": test_provenance_json() + } + }); + +@@ -39,6 +51,7 @@ fn run_event_round_trips_run_created_with_web_url() { + "graph": Graph::new("test"), + "run_dir": "/tmp/fabro/run-1", + "source_directory": "/tmp/fabro/run-1", ++ "provenance": test_provenance_json(), + "web_url": format!("http://localhost:3000/runs/{}", fixtures::RUN_1) + } + }); +diff --git a/lib/crates/fabro-api/tests/run_projection_round_trip.rs b/lib/crates/fabro-api/tests/run_projection_round_trip.rs +index 9a2a0f310..4ab846a7f 100644 +--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs +@@ -137,7 +137,7 @@ fn run_spec_json() -> serde_json::Value { + automation: None, + source_directory: None, + labels: std::collections::HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-api/tests/run_summary_round_trip.rs b/lib/crates/fabro-api/tests/run_summary_round_trip.rs +index 798448941..aabe4889f 100644 +--- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs ++++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs +@@ -88,7 +88,7 @@ fn run_summary_json_matches_openapi_shape() { + origin_url: None, + provider: RepositoryProvider::Unknown, + }), +- created_by: None, ++ created_by: fabro_types::test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::from([("team".to_string(), "core".to_string())]), + lifecycle: RunLifecycle { +@@ -161,7 +161,12 @@ fn run_summary_json_matches_openapi_shape() { + "origin_url": null, + "provider": "unknown" + }, +- "created_by": null, ++ "created_by": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ }, + "origin": { + "kind": "api" + }, +@@ -253,6 +258,12 @@ fn run_summary_deserializes_when_optional_fields_are_absent() { + "origin_url": null, + "provider": "unknown" + }, ++ "created_by": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ }, + "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..44dcc0f37 100644 +--- a/lib/crates/fabro-cli/Cargo.toml ++++ b/lib/crates/fabro-cli/Cargo.toml +@@ -119,6 +119,7 @@ assert_cmd = "2" + fabro-acp = { path = "../fabro-acp", features = ["test-support"] } + fabro-build-support = { path = "../build-support" } + fabro-server = { path = "../fabro-server", features = ["test-support"] } ++fabro-types = { path = "../fabro-types", features = ["test-support"] } + insta = { workspace = true, features = ["filters"] } + paste = "1" + predicates = "3" +diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs +index 77c5c113a..5e49adf42 100644 +--- a/lib/crates/fabro-cli/src/commands/run/attach.rs ++++ b/lib/crates/fabro-cli/src/commands/run/attach.rs +@@ -841,7 +841,7 @@ mod tests { + automation: None, + source_directory: None, + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +index ec0d191e3..1934d8c2a 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +@@ -221,7 +221,18 @@ fn inspect_resolves_selector_via_server_endpoint() { + "attrs": {} + }, + "workflow_slug": "remote-workflow", +- "source_directory": "/srv/repo" ++ "source_directory": "/srv/repo", ++ "provenance": { ++ "subject": { ++ "kind": "user", ++ "identity": { ++ "issuer": "fabro:test", ++ "subject": "test-user" ++ }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + }, + "start_record": null, + "conclusion": null, +diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs +index 275ff9437..a4f6f1516 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs +@@ -177,6 +177,12 @@ pub(crate) fn remote_run_summary_json( + "origin_url": null, + "provider": "unknown" + }, ++ "created_by": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ }, + "origin": { + "kind": "api" + }, +diff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs +index 80b65e4aa..0e3122606 100644 +--- a/lib/crates/fabro-cli/tests/it/support/mod.rs ++++ b/lib/crates/fabro-cli/tests/it/support/mod.rs +@@ -49,7 +49,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s + automation: None, + source_directory: Some("/srv/repo".to_string()), + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-dump/src/lib.rs b/lib/crates/fabro-dump/src/lib.rs +index 397d55a3f..455e7290b 100644 +--- a/lib/crates/fabro-dump/src/lib.rs ++++ b/lib/crates/fabro-dump/src/lib.rs +@@ -472,6 +472,7 @@ mod tests { + use fabro_store::{RunProjection, StageId}; + use fabro_types::graph::Graph; + use fabro_types::run::RunSpec; ++ use fabro_types::test_support::test_run_provenance; + use fabro_types::{ + Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunSandboxInstance, + RunSandboxPlan, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage, +@@ -498,7 +499,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::from([("team".to_string(), "platform".to_string())]), +- provenance: None, ++ provenance: test_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..618bcfae1 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,10 +1092,19 @@ 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, + }; + ++ static DEMO_PRINCIPAL: LazyLock = LazyLock::new(|| { ++ Principal::user( ++ IdpIdentity::new("fabro:demo", "demo").expect("demo identity should parse"), ++ "demo".to_string(), ++ AuthMethod::DevToken, ++ ) ++ }); ++ + use super::ts; + use crate::server::run_stage_from_stage_id; + +@@ -1171,7 +1180,7 @@ mod runs { + repo_origin_url, + source_directory.as_deref(), + )), +- created_by: None, ++ created_by: DEMO_PRINCIPAL.clone(), + origin: RunOrigin::default(), + labels: labels(entries), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-server/src/principal_middleware.rs b/lib/crates/fabro-server/src/principal_middleware.rs +index f9e8c385c..06a0ba257 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,7 +172,10 @@ 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: context ++ .principal ++ .as_ref() ++ .map(principal_without_log_unused_fields), + auth_status: context.auth_status, + auth_error_code: context.auth_error_code, + } +@@ -402,7 +405,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 +415,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 +431,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 +446,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 +456,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 +468,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 +690,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 +730,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 +749,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 +828,7 @@ mod tests { + + assert_eq!(context.auth_status, AuthStatus::Missing); + assert_eq!(context.auth_error_code, None); +- assert_eq!(context.principal, Principal::Anonymous); ++ assert_eq!(context.principal, None); + } + + #[test] +diff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/crates/fabro-server/src/run_files.rs +index 1f358821b..e49567162 100644 +--- a/lib/crates/fabro-server/src/run_files.rs ++++ b/lib/crates/fabro-server/src/run_files.rs +@@ -2389,7 +2389,7 @@ index 1111111..2222222 160000 + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs +index 4feb22133..729e5666f 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; +@@ -194,6 +196,7 @@ pub(crate) fn create_run_input( + prepared: PreparedManifest, + configured_providers: Vec, + web_url: Option, ++ provenance: RunProvenance, + ) -> CreateRunInput { + CreateRunInput { + workflow: WorkflowInput::Bundled(prepared.workflow_input), +@@ -209,7 +212,7 @@ pub(crate) fn create_run_input( + git: prepared.git, + fork_source_ref: None, + parent_id: prepared.parent_id, +- provenance: None, ++ provenance, + configured_providers, + web_url, + } +diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs +index 7bff4968f..4710c40ca 100644 +--- a/lib/crates/fabro-server/src/server.rs ++++ b/lib/crates/fabro-server/src/server.rs +@@ -1876,7 +1876,10 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp + let status = response.status().as_u16(); + let latency_ms = start.elapsed().as_millis(); + let auth_context = auth_slot.log_snapshot(); +- let principal_kind = auth_context.principal.kind(); ++ let principal_kind = auth_context ++ .principal ++ .as_ref() ++ .map_or("none", Principal::kind); + let auth_status = auth_context.auth_status.as_str(); + + macro_rules! emit_http_log { +@@ -1914,27 +1917,27 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp + macro_rules! emit_principal_http_log { + ($level:ident) => {{ + match &auth_context.principal { +- Principal::User(user) => emit_http_log!( ++ Some(Principal::User(user)) => emit_http_log!( + $level, + user_auth_method = user.auth_method.as_str(), + idp_issuer = user.identity.issuer(), + idp_subject = user.identity.subject(), + login = user.login.as_str(), + ), +- Principal::Worker { run_id } => { ++ Some(Principal::Worker { run_id }) => { + emit_http_log!($level, run_id = run_id.to_string().as_str(),) + } +- Principal::Webhook { delivery_id } => { ++ Some(Principal::Webhook { delivery_id }) => { + emit_http_log!($level, delivery_id = delivery_id.as_str(),) + } +- Principal::Slack { ++ Some(Principal::Slack { + team_id, user_id, .. +- } => emit_http_log!( ++ }) => emit_http_log!( + $level, + team_id = team_id.as_str(), + user_id = user_id.as_str(), + ), +- Principal::Agent { .. } | Principal::System { .. } | Principal::Anonymous => { ++ None | Some(Principal::Agent { .. } | Principal::System { .. }) => { + emit_http_log!($level) + } + } +diff --git a/lib/crates/fabro-server/src/server/handler/events.rs b/lib/crates/fabro-server/src/server/handler/events.rs +index 180c04bdd..ff2fabcdc 100644 +--- a/lib/crates/fabro-server/src/server/handler/events.rs ++++ b/lib/crates/fabro-server/src/server/handler/events.rs +@@ -570,7 +570,7 @@ mod stage_events_tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-server/src/server/handler/lifecycle.rs b/lib/crates/fabro-server/src/server/handler/lifecycle.rs +index 06beb7c0b..5df7e72c5 100644 +--- a/lib/crates/fabro-server/src/server/handler/lifecycle.rs ++++ b/lib/crates/fabro-server/src/server/handler/lifecycle.rs +@@ -894,7 +894,7 @@ async fn retry_run( + let input = operations::RetryRunInput { + source_run_id: id, + new_run_id, +- provenance: Some(run_provenance(&headers, &actor)), ++ provenance: run_provenance(&headers, &actor), + web_url: state.run_web_url(&new_run_id), + }; + match Box::pin(operations::retry_run(&state.store, &input)).await { +diff --git a/lib/crates/fabro-server/src/server/handler/pair.rs b/lib/crates/fabro-server/src/server/handler/pair.rs +index 7b673fad8..73bf6f374 100644 +--- a/lib/crates/fabro-server/src/server/handler/pair.rs ++++ b/lib/crates/fabro-server/src/server/handler/pair.rs +@@ -1024,7 +1024,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs +index fca9fdc3c..5b8f06ef4 100644 +--- a/lib/crates/fabro-server/src/server/handler/runs.rs ++++ b/lib/crates/fabro-server/src/server/handler/runs.rs +@@ -691,9 +691,9 @@ pub(crate) async fn create_run_from_manifest( + prepared.clone(), + ready_provider_ids.clone(), + web_url.clone(), ++ run_provenance(&headers, &actor), + ); + 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 +864,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..3f3c1853f 100644 +--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs ++++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs +@@ -1339,6 +1339,14 @@ mod retrieve_sandbox_tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/test", ++ "provenance": { ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + }, + }), + run_id, +diff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/crates/fabro-server/src/server/handler/sessions.rs +index bad48c6d4..cd8c07579 100644 +--- a/lib/crates/fabro-server/src/server/handler/sessions.rs ++++ b/lib/crates/fabro-server/src/server/handler/sessions.rs +@@ -1700,7 +1700,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs +index 363884738..418ffc098 100644 +--- a/lib/crates/fabro-server/src/server/tests.rs ++++ b/lib/crates/fabro-server/src/server/tests.rs +@@ -4022,7 +4022,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -4076,7 +4076,7 @@ async fn create_slack_notification_run( + workflow_slug: workflow_slug.map(str::to_string), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -5083,7 +5083,7 @@ async fn list_run_stages_distinguishes_visits() { + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -6140,7 +6140,7 @@ async fn create_completed_run_ready_for_pull_request( + source_directory: Some("/tmp/project".to_string()), + git: git.clone(), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -9943,15 +9943,10 @@ async fn run_tool_worker_token_can_use_client_backend_routes_across_runs() { + .unwrap() + .expect("created run should be cached"); + assert_eq!( +- cached +- .projection +- .spec +- .provenance +- .as_ref() +- .and_then(|provenance| provenance.subject.as_ref()), +- Some(&Principal::Worker { ++ cached.projection.spec.provenance.subject, ++ Principal::Worker { + run_id: parent_run_id, +- }), ++ }, + ); + + let response = app +@@ -12310,7 +12305,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -13062,7 +13057,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() { + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs +index 033e46db6..e231599b2 100644 +--- a/lib/crates/fabro-server/src/web_auth.rs ++++ b/lib/crates/fabro-server/src/web_auth.rs +@@ -1365,7 +1365,7 @@ client_id = "github-client-id" + + let contexts = captured.lock().expect("captured auth contexts").clone(); + assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated); +- assert!(matches!(contexts[0].principal, Principal::User(_))); ++ assert!(matches!(contexts[0].principal, Some(Principal::User(_)))); + assert_eq!(contexts[1].auth_status, AuthStatus::Invalid); + assert_eq!( + contexts[1].auth_error_code, +diff --git a/lib/crates/fabro-server/tests/it/api/run_files.rs b/lib/crates/fabro-server/tests/it/api/run_files.rs +index a3bf7ad76..01d762a73 100644 +--- a/lib/crates/fabro-server/tests/it/api/run_files.rs ++++ b/lib/crates/fabro-server/tests/it/api/run_files.rs +@@ -69,7 +69,7 @@ async fn append_completed_run_with_final_patch( + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-store/Cargo.toml b/lib/crates/fabro-store/Cargo.toml +index 016b55d3d..c81bca853 100644 +--- a/lib/crates/fabro-store/Cargo.toml ++++ b/lib/crates/fabro-store/Cargo.toml +@@ -32,6 +32,7 @@ futures.workspace = true + uuid.workspace = true + + [dev-dependencies] ++fabro-types = { path = "../fabro-types", features = ["test-support"] } + tokio = { workspace = true, features = ["test-util", "macros"] } + tempfile = "3" + ulid.workspace = true +diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs +index cd6fa0640..3325955c0 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); +@@ -1267,6 +1263,7 @@ mod tests { + StagePromptProps, StageRetryingProps, StageStartedProps, + }; + use fabro_types::settings::run::{DockerfileSource, EnvironmentProvider}; ++ use fabro_types::test_support::test_run_provenance; + use fabro_types::{ + AgentBackend, AutomationRef, BilledModelUsage, BilledTokenCounts, BlockedReason, + Checkpoint, CheckpointRecord, CommandTermination, EventBody, FailureCategory, +@@ -1358,7 +1355,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -1638,12 +1635,35 @@ mod tests { + assert!(sandbox.get("failure").is_none()); + } + ++ fn inject_required_run_created_fields( ++ event: &str, ++ properties: &serde_json::Value, ++ ) -> serde_json::Value { ++ if event != "run.created" || !properties.is_object() { ++ return properties.clone(); ++ } ++ let mut props = properties.clone(); ++ let obj = props.as_object_mut().expect("properties must be an object"); ++ obj.entry("provenance".to_string()).or_insert_with(|| { ++ json!({ ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ }) ++ }); ++ props ++ } ++ + fn test_raw_event( + seq: u32, + event: &str, + properties: &serde_json::Value, + node_id: Option<&str>, + ) -> EventEnvelope { ++ let properties = inject_required_run_created_fields(event, properties); + EventEnvelope { + seq, + event: RunEvent::from_value(json!({ +@@ -1665,6 +1685,7 @@ mod tests { + properties: &serde_json::Value, + node_id: Option<&str>, + ) -> EventEnvelope { ++ let properties = inject_required_run_created_fields(event, properties); + EventEnvelope { + seq, + event: RunEvent::from_value(json!({ +@@ -1822,7 +1843,14 @@ mod tests { + "repo_origin_url": null, + "base_branch": null, + "labels": {}, +- "provenance": null, ++ "provenance": { ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ }, + "manifest_blob": null, + "definition_blob": null, + "git": null, +@@ -2851,7 +2879,7 @@ mod tests { + source_directory: Some("/tmp/repo".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -2877,7 +2905,7 @@ mod tests { + source_directory: Some("/tmp/repo".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -3016,7 +3044,15 @@ mod tests { + "labels": {}, + "run_dir": "/tmp/run", + "source_directory": "/tmp/run", +- "manifest_blob": manifest_blob ++ "manifest_blob": manifest_blob, ++ "provenance": { ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + } + })) + .unwrap(), +diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs +index c6406fd8d..568f07fac 100644 +--- a/lib/crates/fabro-store/src/slate/mod.rs ++++ b/lib/crates/fabro-store/src/slate/mod.rs +@@ -470,6 +470,7 @@ fn active_run_from( + #[cfg(test)] + mod tests { + use chrono::{DateTime, Utc}; ++ use fabro_types::test_support::test_run_provenance; + use fabro_types::{ + AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId, + SuccessReason, WorkflowSettings, +@@ -542,7 +543,7 @@ mod tests { + automation: None, + source_directory: Some(format!("/tmp/{label}")), + labels: std::collections::HashMap::from([("team".to_string(), "infra".to_string())]), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(fabro_types::GitContext { +@@ -601,6 +602,7 @@ mod tests { + "run_dir": format!("/tmp/{label}"), + "git": run_spec.git, + "labels": run_spec.labels, ++ "provenance": run_spec.provenance, + }), + )) + .await +@@ -626,6 +628,7 @@ mod tests { + "run_dir": format!("/tmp/{label}"), + "git": run_spec.git, + "labels": run_spec.labels, ++ "provenance": run_spec.provenance, + "parent_id": parent_id, + }), + )) +@@ -1300,6 +1303,7 @@ mod tests { + "run_dir": "/tmp/run-2", + "git": run_spec["git"], + "labels": run_spec["labels"], ++ "provenance": run_spec["provenance"], + }, + })) + .unwrap(), +diff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs +index 1718cb662..d2f5d8961 100644 +--- a/lib/crates/fabro-store/src/slate/run_store.rs ++++ b/lib/crates/fabro-store/src/slate/run_store.rs +@@ -723,6 +723,14 @@ mod tests { + "settings": WorkflowSettings::default(), + "graph": Graph::new("test"), + "run_dir": "/tmp/test", ++ "provenance": { ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + }, + }), + run_id, +diff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs +index 6584a36cd..eda1995c4 100644 +--- a/lib/crates/fabro-store/tests/serializable_projection.rs ++++ b/lib/crates/fabro-store/tests/serializable_projection.rs +@@ -4,6 +4,7 @@ use chrono::{TimeZone, Utc}; + use fabro_store::{RunProjection, SerializableProjection, StageId}; + use fabro_types::graph::Graph; + use fabro_types::run::RunSpec; ++use fabro_types::test_support::test_run_provenance; + use fabro_types::{ + BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord, + QuestionType, RunDiff, RunSandbox, RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime, +@@ -22,7 +23,7 @@ fn sample_run_spec() -> RunSpec { + automation: None, + source_directory: Some("/tmp/project".to_string()), + labels: HashMap::from([("team".to_string(), "platform".to_string())]), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(fabro_types::GitContext { +diff --git a/lib/crates/fabro-tool/Cargo.toml b/lib/crates/fabro-tool/Cargo.toml +index 08b6df7cf..c50ea6a38 100644 +--- a/lib/crates/fabro-tool/Cargo.toml ++++ b/lib/crates/fabro-tool/Cargo.toml +@@ -29,4 +29,5 @@ tokio.workspace = true + toml.workspace = true + + [dev-dependencies] ++fabro-types = { path = "../fabro-types", features = ["test-support"] } + tempfile = "3" +diff --git a/lib/crates/fabro-tool/src/common.rs b/lib/crates/fabro-tool/src/common.rs +index 9dd64599a..1595dc28f 100644 +--- a/lib/crates/fabro-tool/src/common.rs ++++ b/lib/crates/fabro-tool/src/common.rs +@@ -307,6 +307,7 @@ fn format_tool_error(err: &anyhow::Error) -> String { + #[cfg(test)] + mod tests { + use chrono::{TimeZone, Utc}; ++ use fabro_types::test_support::test_principal; + use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef}; + + use super::*; +@@ -413,7 +414,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs +index 8488f8f0b..8988d1a4e 100644 +--- a/lib/crates/fabro-tool/src/create.rs ++++ b/lib/crates/fabro-tool/src/create.rs +@@ -506,6 +506,7 @@ mod tests { + use async_trait::async_trait; + use chrono::{TimeZone, Utc}; + use fabro_api::types; ++ use fabro_types::test_support::test_principal; + use fabro_types::{ + EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus, + RunTimestamps, WorkflowRef, +@@ -902,7 +903,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/interact.rs b/lib/crates/fabro-tool/src/interact.rs +index 34023120d..9f7957d15 100644 +--- a/lib/crates/fabro-tool/src/interact.rs ++++ b/lib/crates/fabro-tool/src/interact.rs +@@ -451,6 +451,7 @@ mod tests { + + use async_trait::async_trait; + use chrono::{TimeZone, Utc}; ++ use fabro_types::test_support::test_principal; + use fabro_types::{ + EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection, + RunStatus, RunTimestamps, WorkflowRef, +@@ -690,7 +691,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs +index 0df7e391a..379806d07 100644 +--- a/lib/crates/fabro-tool/src/search.rs ++++ b/lib/crates/fabro-tool/src/search.rs +@@ -293,6 +293,7 @@ mod tests { + use std::collections::HashMap; + + use chrono::{TimeZone, Utc}; ++ use fabro_types::test_support::test_principal; + use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef}; + + use super::*; +@@ -444,7 +445,7 @@ mod tests { + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: test_principal(), + origin: RunOrigin::default(), + labels: HashMap::from([("group".to_string(), group.to_string())]), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs +index f1a8c8b39..cf2605429 100644 +--- a/lib/crates/fabro-types/src/lib.rs ++++ b/lib/crates/fabro-types/src/lib.rs +@@ -44,6 +44,8 @@ pub mod start; + pub mod status; + pub mod steering; + pub mod system_integrations; ++#[cfg(any(test, feature = "test-support"))] ++pub mod test_support; + pub mod timing; + pub mod todo; + pub mod transcript; +diff --git a/lib/crates/fabro-types/src/principal.rs b/lib/crates/fabro-types/src/principal.rs +index 2c0ff3807..4f1962ea2 100644 +--- a/lib/crates/fabro-types/src/principal.rs ++++ b/lib/crates/fabro-types/src/principal.rs +@@ -39,7 +39,6 @@ pub enum Principal { + System { + system_kind: SystemActorKind, + }, +- Anonymous, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, IntoStaticStr)] +@@ -97,7 +96,6 @@ impl Principal { + Self::Slack { .. } => "slack", + Self::Agent { .. } => "agent", + Self::System { .. } => "system", +- Self::Anonymous => "anonymous", + } + } + +@@ -123,7 +121,6 @@ impl Principal { + } => session_id.clone(), + Self::Agent { .. } => "agent".to_string(), + Self::System { system_kind } => format!("system:{system_kind}"), +- Self::Anonymous => "anonymous".to_string(), + } + } + } +@@ -291,11 +288,6 @@ mod tests { + }); + } + +- #[test] +- fn round_trips_anonymous_variant() { +- assert_round_trip(&Principal::Anonymous); +- } +- + #[test] + fn auth_method_as_str_matches_serde() { + assert_eq!(AuthMethod::Github.as_str(), "github"); +diff --git a/lib/crates/fabro-types/src/run.rs b/lib/crates/fabro-types/src/run.rs +index 2b27d0a06..2f1671fe6 100644 +--- a/lib/crates/fabro-types/src/run.rs ++++ b/lib/crates/fabro-types/src/run.rs +@@ -24,14 +24,13 @@ pub struct RunClientProvenance { + pub version: Option, + } + +-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] ++#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct RunProvenance { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client: Option, +- #[serde(default, skip_serializing_if = "Option::is_none")] +- pub subject: Option, ++ pub subject: Principal, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +@@ -93,8 +92,7 @@ pub struct RunSpec { + pub source_directory: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub labels: HashMap, +- #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provenance: Option, ++ pub provenance: RunProvenance, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub manifest_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +diff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs +index 52cbb83a9..9ce7b7f85 100644 +--- a/lib/crates/fabro-types/src/run_event/mod.rs ++++ b/lib/crates/fabro-types/src/run_event/mod.rs +@@ -1017,7 +1017,15 @@ mod tests { + "graph": graph, + "labels": {}, + "run_dir": "/tmp/run", +- "source_directory": "/tmp/run" ++ "source_directory": "/tmp/run", ++ "provenance": { ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + } + }); + +@@ -1038,7 +1046,15 @@ 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": { ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + } + }); + +diff --git a/lib/crates/fabro-types/src/run_event/run.rs b/lib/crates/fabro-types/src/run_event/run.rs +index e3023997a..06077171d 100644 +--- a/lib/crates/fabro-types/src/run_event/run.rs ++++ b/lib/crates/fabro-types/src/run_event/run.rs +@@ -30,8 +30,7 @@ pub struct RunCreatedProps { + pub automation: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub db_prefix: Option, +- #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provenance: Option, ++ pub provenance: RunProvenance, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub manifest_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +diff --git a/lib/crates/fabro-types/src/run_projection.rs b/lib/crates/fabro-types/src/run_projection.rs +index 4b52dcc96..47dffdcf7 100644 +--- a/lib/crates/fabro-types/src/run_projection.rs ++++ b/lib/crates/fabro-types/src/run_projection.rs +@@ -681,6 +681,7 @@ mod title_tests { + + use chrono::Utc; + ++ use crate::test_support::test_run_provenance; + use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings}; + + fn projection_with_goal(goal: Option<&str>) -> RunProjection { +@@ -700,7 +701,7 @@ mod title_tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -752,6 +753,7 @@ mod iter_stages_tests { + use serde_json::json; + + use super::RunProjection; ++ use crate::test_support::test_run_provenance; + use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings}; + + fn seq(n: u32) -> NonZeroU32 { +@@ -770,7 +772,7 @@ mod iter_stages_tests { + automation: None, + source_directory: None, + labels: HashMap::default(), +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs +index fd35dc2a4..3c4321052 100644 +--- a/lib/crates/fabro-types/src/run_summary.rs ++++ b/lib/crates/fabro-types/src/run_summary.rs +@@ -52,8 +52,7 @@ pub struct Run { + pub automation: Option, + #[serde(default)] + pub repository: Option, +- #[serde(default)] +- pub created_by: Option, ++ pub created_by: Principal, + pub origin: RunOrigin, + pub labels: HashMap, + pub lifecycle: RunLifecycle, +diff --git a/lib/crates/fabro-types/src/test_support.rs b/lib/crates/fabro-types/src/test_support.rs +new file mode 100644 +index 000000000..4067144bb +--- /dev/null ++++ b/lib/crates/fabro-types/src/test_support.rs +@@ -0,0 +1,28 @@ ++//! Test-only helpers for constructing total `Principal` and `RunProvenance` ++//! values without requiring callers to assemble fake identity bits inline. ++//! ++//! Only available behind `#[cfg(test)]` or the `test-support` feature. Do not ++//! use these in production code or release builds. ++ ++use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance}; ++ ++/// A clearly synthetic dev-token principal for tests. ++#[must_use] ++pub fn test_principal() -> Principal { ++ Principal::user( ++ IdpIdentity::new("fabro:test", "test-user").expect("test identity should parse"), ++ "test".to_string(), ++ AuthMethod::DevToken, ++ ) ++} ++ ++/// A `RunProvenance` with a synthetic test subject and no server/client ++/// provenance fields. ++#[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..0bfa07da0 100644 +--- a/lib/crates/fabro-types/tests/run_event_serde.rs ++++ b/lib/crates/fabro-types/tests/run_event_serde.rs +@@ -1,12 +1,27 @@ + use std::collections::BTreeMap; + + use fabro_types::graph::Graph; +-use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome}; ++use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunProvenance}; + use fabro_types::run_event::run::{RunCreatedProps, RunParentLinkedProps, RunParentUnlinkedProps}; + 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::{ ++ AuthMethod, AutomationRef, EventBody, IdpIdentity, Principal, TurnId, WorkflowSettings, ++ fixtures, ++}; ++ ++fn test_provenance() -> RunProvenance { ++ RunProvenance { ++ server: None, ++ client: None, ++ subject: Principal::user( ++ IdpIdentity::new("fabro:test", "test-user").expect("test identity should parse"), ++ "test".to_string(), ++ AuthMethod::DevToken, ++ ), ++ } ++} + + fn templated_settings() -> WorkflowSettings { + let mut settings = WorkflowSettings::default(); +@@ -32,7 +47,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_provenance(), + manifest_blob: None, + git: Some(GitContext { + origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), +@@ -97,7 +112,7 @@ fn run_created_props_omits_web_url_when_absent() { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: test_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -134,7 +149,15 @@ fn run_created_props_defaults_additive_fields_for_legacy_events() { + "settings": WorkflowSettings::default(), + "graph": Graph::new("ship"), + "labels": {}, +- "run_dir": "/tmp/run" ++ "run_dir": "/tmp/run", ++ "provenance": { ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + }); + + let props: RunCreatedProps = +diff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs +index b05dca05e..de069034e 100644 +--- a/lib/crates/fabro-types/tests/run_spec_methods.rs ++++ b/lib/crates/fabro-types/tests/run_spec_methods.rs +@@ -1,9 +1,21 @@ + use std::collections::HashMap; + + use fabro_types::graph::Graph; +-use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec}; ++use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunProvenance, RunSpec}; + use fabro_types::settings::{ProjectNamespace, WorkflowNamespace}; +-use fabro_types::{WorkflowSettings, fixtures}; ++use fabro_types::{AuthMethod, IdpIdentity, Principal, WorkflowSettings, fixtures}; ++ ++fn test_provenance() -> RunProvenance { ++ RunProvenance { ++ server: None, ++ client: None, ++ subject: Principal::user( ++ IdpIdentity::new("fabro:test", "test-user").expect("test identity should parse"), ++ "test".to_string(), ++ AuthMethod::DevToken, ++ ), ++ } ++} + + fn sample_run_spec() -> RunSpec { + let settings = WorkflowSettings { +@@ -27,7 +39,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_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..0f5318eea 100644 +--- a/lib/crates/fabro-types/tests/run_spec_serde.rs ++++ b/lib/crates/fabro-types/tests/run_spec_serde.rs +@@ -1,10 +1,24 @@ + use std::collections::HashMap; + + use fabro_types::graph::Graph; +-use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunSpec}; ++use fabro_types::run::{ ++ DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunProvenance, RunSpec, ++}; + use fabro_types::settings::InterpString; + use fabro_types::settings::run::RunGoal; +-use fabro_types::{AutomationRef, WorkflowSettings, fixtures}; ++use fabro_types::{AuthMethod, AutomationRef, IdpIdentity, Principal, WorkflowSettings, fixtures}; ++ ++fn test_provenance() -> RunProvenance { ++ RunProvenance { ++ server: None, ++ client: None, ++ subject: Principal::user( ++ IdpIdentity::new("fabro:test", "test-user").expect("test identity should parse"), ++ "test".to_string(), ++ AuthMethod::DevToken, ++ ), ++ } ++} + + fn templated_settings() -> WorkflowSettings { + let mut settings = WorkflowSettings::default(); +@@ -27,7 +41,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_provenance(), + manifest_blob: None, + definition_blob: None, + git: Some(GitContext { +@@ -80,7 +94,15 @@ fn run_spec_defaults_automation_for_legacy_specs() { + "run_id": fixtures::RUN_1, + "settings": WorkflowSettings::default(), + "graph": Graph::new("ship"), +- "labels": {} ++ "labels": {}, ++ "provenance": { ++ "subject": { ++ "kind": "user", ++ "identity": { "issuer": "fabro:test", "subject": "test-user" }, ++ "login": "test", ++ "auth_method": "dev_token" ++ } ++ } + }); + + 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..73391c9bc 100644 +--- a/lib/crates/fabro-workflow/src/billing_rollup.rs ++++ b/lib/crates/fabro-workflow/src/billing_rollup.rs +@@ -353,7 +353,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs +index 4c1f91228..5f0fc9959 100644 +--- a/lib/crates/fabro-workflow/src/event/convert.rs ++++ b/lib/crates/fabro-workflow/src/event/convert.rs +@@ -2339,7 +2339,7 @@ mod tests { + let provenance = RunProvenance { + server: None, + client: None, +- subject: Some(user_principal("alice")), ++ subject: user_principal("alice"), + }; + let automation = AutomationRef { + id: "nightly".to_string(), +@@ -2348,25 +2348,25 @@ mod tests { + }; + + let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated { +- run_id: fixtures::RUN_1, +- title: None, +- settings: serde_json::to_value(WorkflowSettings::default()).unwrap(), +- graph: serde_json::to_value(Graph::new("test")).unwrap(), +- workflow_source: None, +- workflow_config: None, +- labels: BTreeMap::default(), +- run_dir: "/tmp/run".to_string(), ++ run_id: fixtures::RUN_1, ++ title: None, ++ settings: serde_json::to_value(WorkflowSettings::default()).unwrap(), ++ graph: serde_json::to_value(Graph::new("test")).unwrap(), ++ workflow_source: None, ++ workflow_config: None, ++ labels: BTreeMap::default(), ++ run_dir: "/tmp/run".to_string(), + source_directory: Some("/tmp/run".to_string()), +- workflow_slug: None, +- automation: Some(automation.clone()), +- db_prefix: None, +- provenance: Some(provenance), +- manifest_blob: None, +- git: None, +- fork_source_ref: None, +- retried_from: None, +- parent_id: None, +- web_url: None, ++ workflow_slug: None, ++ automation: Some(automation.clone()), ++ db_prefix: None, ++ provenance, ++ manifest_blob: None, ++ git: None, ++ fork_source_ref: None, ++ retried_from: None, ++ parent_id: None, ++ web_url: None, + }); + let actor = stored.actor.as_ref().expect("actor set"); + assert_eq!(actor, &user_principal("alice")); +diff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs +index 886868804..0b5afb1c7 100644 +--- a/lib/crates/fabro-workflow/src/event/events.rs ++++ b/lib/crates/fabro-workflow/src/event/events.rs +@@ -41,8 +41,7 @@ pub enum Event { + automation: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + db_prefix: Option, +- #[serde(default, skip_serializing_if = "Option::is_none")] +- provenance: Option, ++ provenance: RunProvenance, + #[serde(default, skip_serializing_if = "Option::is_none")] + manifest_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +diff --git a/lib/crates/fabro-workflow/src/event/sink.rs b/lib/crates/fabro-workflow/src/event/sink.rs +index 7b65fc99c..4acc5265a 100644 +--- a/lib/crates/fabro-workflow/src/event/sink.rs ++++ b/lib/crates/fabro-workflow/src/event/sink.rs +@@ -244,7 +244,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/event/stored_fields.rs b/lib/crates/fabro-workflow/src/event/stored_fields.rs +index 94fba25ad..4ada17b67 100644 +--- a/lib/crates/fabro-workflow/src/event/stored_fields.rs ++++ b/lib/crates/fabro-workflow/src/event/stored_fields.rs +@@ -57,7 +57,7 @@ pub(super) fn stored_event_fields(event: &Event, scope: Option<&StageScope>) -> + fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields { + match event { + Event::RunCreated { provenance, .. } => StoredEventFields { +- actor: provenance.as_ref().and_then(|p| p.subject.clone()), ++ actor: Some(provenance.subject.clone()), + ..StoredEventFields::default() + }, + Event::RunCancelRequested { actor } +diff --git a/lib/crates/fabro-workflow/src/git.rs b/lib/crates/fabro-workflow/src/git.rs +index 643d49b58..0f0613fe8 100644 +--- a/lib/crates/fabro-workflow/src/git.rs ++++ b/lib/crates/fabro-workflow/src/git.rs +@@ -469,7 +469,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs +index 3a120e8fc..3b048374b 100644 +--- a/lib/crates/fabro-workflow/src/handler/agent.rs ++++ b/lib/crates/fabro-workflow/src/handler/agent.rs +@@ -484,7 +484,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/handler/command.rs b/lib/crates/fabro-workflow/src/handler/command.rs +index f10ebafde..2e59a954b 100644 +--- a/lib/crates/fabro-workflow/src/handler/command.rs ++++ b/lib/crates/fabro-workflow/src/handler/command.rs +@@ -256,7 +256,7 @@ mod tests { + automation: None, + source_directory: None, + labels: std::collections::HashMap::default(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -357,7 +357,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs +index 91753f4c9..8520a0c93 100644 +--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs ++++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs +@@ -2133,7 +2133,7 @@ reasoning = false + }, + automation: None, + repository: None, +- created_by: None, ++ created_by: fabro_types::test_support::test_principal(), + origin: RunOrigin::default(), + labels: HashMap::new(), + lifecycle: RunLifecycle { +diff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs +index b52478bac..6772db8fb 100644 +--- a/lib/crates/fabro-workflow/src/handler/parallel.rs ++++ b/lib/crates/fabro-workflow/src/handler/parallel.rs +@@ -728,7 +728,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs +index 1c2a82267..d88c9e875 100644 +--- a/lib/crates/fabro-workflow/src/handler/prompt.rs ++++ b/lib/crates/fabro-workflow/src/handler/prompt.rs +@@ -283,7 +283,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs +index ccd24632d..233044930 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs +@@ -736,7 +736,7 @@ mod tests { + workflow_slug: Some("metadata".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/operations/archive.rs b/lib/crates/fabro-workflow/src/operations/archive.rs +index db4b0ccf9..2e0112a61 100644 +--- a/lib/crates/fabro-workflow/src/operations/archive.rs ++++ b/lib/crates/fabro-workflow/src/operations/archive.rs +@@ -226,7 +226,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs +index 8bf16d115..d66ac48cb 100644 +--- a/lib/crates/fabro-workflow/src/operations/create.rs ++++ b/lib/crates/fabro-workflow/src/operations/create.rs +@@ -45,7 +45,7 @@ pub struct CreateRunInput { + pub git: Option, + pub fork_source_ref: Option, + pub parent_id: Option, +- pub provenance: Option, ++ pub provenance: RunProvenance, + pub configured_providers: Vec, + /// Public URL where this run can be viewed in the web UI, when the server + /// has the web UI enabled. Recorded on the `run.created` event so attach +@@ -72,7 +72,7 @@ struct PersistCreateOptions { + automation: Option, + git: Option, + fork_source_ref: Option, +- provenance: Option, ++ provenance: RunProvenance, + configured_providers: Vec, + catalog: Arc, + } +@@ -1115,7 +1115,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1183,7 +1183,7 @@ mod tests { + }), + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1295,7 +1295,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1341,7 +1341,7 @@ mod tests { + }), + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1414,7 +1414,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1467,7 +1467,7 @@ mod tests { + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: Some(fabro_types::RunProvenance { ++ provenance: fabro_types::RunProvenance { + server: Some(fabro_types::RunServerProvenance { + version: "0.9.0".to_string(), + }), +@@ -1476,12 +1476,12 @@ mod tests { + name: Some("fabro-cli".to_string()), + version: Some("0.9.0".to_string()), + }), +- subject: Some(fabro_types::Principal::user( ++ subject: fabro_types::Principal::user( + fabro_types::IdpIdentity::new("https://github.com", "12345").unwrap(), + "octocat".to_string(), + fabro_types::AuthMethod::Github, +- )), +- }), ++ ), ++ }, + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1493,8 +1493,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 = state.spec.provenance; + + assert_eq!(provenance.server.unwrap().version, "0.9.0"); + assert_eq!( +@@ -1502,7 +1501,7 @@ mod tests { + Some("fabro-cli") + ); + assert_eq!( +- provenance.subject.unwrap(), ++ provenance.subject, + fabro_types::Principal::user( + fabro_types::IdpIdentity::new("https://github.com", "12345").unwrap(), + "octocat".to_string(), +diff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs +index 2bf7e144f..006f960fa 100644 +--- a/lib/crates/fabro-workflow/src/operations/fork.rs ++++ b/lib/crates/fabro-workflow/src/operations/fork.rs +@@ -383,7 +383,7 @@ mod tests { + workflow_slug: Some("fork-source".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: Some(fabro_types::GitContext { + origin_url: "https://github.com/example/repo.git".to_string(), +diff --git a/lib/crates/fabro-workflow/src/operations/retry.rs b/lib/crates/fabro-workflow/src/operations/retry.rs +index be7793d3e..6d3cbdc3c 100644 +--- a/lib/crates/fabro-workflow/src/operations/retry.rs ++++ b/lib/crates/fabro-workflow/src/operations/retry.rs +@@ -12,7 +12,7 @@ use crate::event::{self, Event}; + pub struct RetryRunInput { + pub source_run_id: RunId, + pub new_run_id: RunId, +- pub provenance: Option, ++ pub provenance: RunProvenance, + pub web_url: Option, + } + +@@ -152,7 +152,7 @@ mod tests { + version: "test".to_string(), + }), + client: None, +- subject: Some(actor(login)), ++ subject: actor(login), + } + } + +@@ -191,7 +191,7 @@ mod tests { + workflow_slug: Some("retry-source".to_string()), + automation: None, + db_prefix: None, +- provenance: Some(provenance("source-user")), ++ provenance: provenance("source-user"), + manifest_blob, + git: Some(git_context()), + fork_source_ref, +@@ -368,7 +368,7 @@ mod tests { + let outcome = retry_run(&store, &RetryRunInput { + source_run_id, + new_run_id: RunId::new(), +- provenance: Some(provenance("retry-user")), ++ provenance: provenance("retry-user"), + web_url: Some("http://localhost:3000/runs/retry".to_string()), + }) + .await +@@ -402,14 +402,7 @@ mod tests { + assert_eq!(retry_state.spec.manifest_blob, manifest_blob); + assert_eq!(retry_state.spec.definition_blob, definition_blob); + assert_eq!(retry_state.spec.fork_source_ref, Some(fork_source_ref)); +- assert_eq!( +- retry_state +- .spec +- .provenance +- .as_ref() +- .and_then(|provenance| provenance.subject.as_ref()), +- Some(&actor("retry-user")) +- ); ++ assert_eq!(retry_state.spec.provenance.subject, actor("retry-user")); + assert_eq!( + retry_state.web_url.as_deref(), + Some("http://localhost:3000/runs/retry") +@@ -463,7 +456,7 @@ mod tests { + let outcome = retry_run(&store, &RetryRunInput { + source_run_id, + new_run_id: RunId::new(), +- provenance: Some(provenance("retry-user")), ++ provenance: provenance("retry-user"), + web_url: None, + }) + .await +@@ -517,7 +510,7 @@ mod tests { + let err = retry_run(&store, &RetryRunInput { + source_run_id: run_id, + new_run_id: RunId::new(), +- provenance: None, ++ provenance: provenance("retry-user"), + web_url: None, + }) + .await +@@ -535,7 +528,7 @@ mod tests { + let err = retry_run(&store, &RetryRunInput { + source_run_id: fixtures::RUN_1, + new_run_id: RunId::new(), +- provenance: None, ++ provenance: provenance("retry-user"), + web_url: None, + }) + .await +diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs +index 8083350be..3ac123a9a 100644 +--- a/lib/crates/fabro-workflow/src/operations/start.rs ++++ b/lib/crates/fabro-workflow/src/operations/start.rs +@@ -1438,7 +1438,7 @@ reasoning = false + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +@@ -1860,7 +1860,7 @@ reasoning = false + git: None, + fork_source_ref: None, + parent_id: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + configured_providers: Vec::new(), + web_url: None, + }, +diff --git a/lib/crates/fabro-workflow/src/operations/timeline.rs b/lib/crates/fabro-workflow/src/operations/timeline.rs +index 68e0ee959..bfe0da88d 100644 +--- a/lib/crates/fabro-workflow/src/operations/timeline.rs ++++ b/lib/crates/fabro-workflow/src/operations/timeline.rs +@@ -248,7 +248,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +index d05398556..0deb440e5 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +@@ -165,7 +165,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -208,7 +208,7 @@ async fn seed_created_and_starting( + workflow_slug: run_options.workflow_slug.clone(), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: run_options.pre_run_git.clone(), + fork_source_ref: run_options.fork_source_ref.clone(), +diff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs +index c677c1007..3b6344422 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs +@@ -739,7 +739,7 @@ mod tests { + workflow_slug: Some("metadata".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +@@ -856,7 +856,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +diff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs +index cc1b0777a..b2894e2b6 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs +@@ -773,7 +773,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs +index 7a8c896e4..64ef56a2e 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs +@@ -148,7 +148,7 @@ mod tests { + ("env".to_string(), "test".to_string()), + ("team".to_string(), "workflow".to_string()), + ]), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +index c02376988..79433cb79 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +@@ -823,7 +823,7 @@ mod tests { + automation: None, + source_directory: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + git: None, +@@ -1148,7 +1148,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1219,7 +1219,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1575,7 +1575,7 @@ mod tests { + source_directory: Some(tmp.path().display().to_string()), + git: None, + labels: std::collections::HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1704,7 +1704,7 @@ mod tests { + source_directory: Some("/tmp/project".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1722,7 +1722,7 @@ mod tests { + workflow_slug: run_spec.workflow_slug.clone(), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_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: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -1893,7 +1893,7 @@ mod tests { + workflow_slug: None, + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_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..e62e17fbe 100644 +--- a/lib/crates/fabro-workflow/src/run_lookup.rs ++++ b/lib/crates/fabro-workflow/src/run_lookup.rs +@@ -491,7 +491,7 @@ mod tests { + push_outcome: fabro_types::PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/run_metadata.rs b/lib/crates/fabro-workflow/src/run_metadata.rs +index b11c9667c..40c42a05f 100644 +--- a/lib/crates/fabro-workflow/src/run_metadata.rs ++++ b/lib/crates/fabro-workflow/src/run_metadata.rs +@@ -639,7 +639,7 @@ mod tests { + push_outcome: PreRunPushOutcome::NotAttempted, + }), + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs +index 0dafbfdad..da3cfb099 100644 +--- a/lib/crates/fabro-workflow/src/runtime_store.rs ++++ b/lib/crates/fabro-workflow/src/runtime_store.rs +@@ -148,7 +148,7 @@ mod tests { + source_directory: Some("/tmp/test".to_string()), + git: None, + labels: HashMap::new(), +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + definition_blob: None, + fork_source_ref: None, +@@ -170,7 +170,7 @@ mod tests { + workflow_slug: Some("test".to_string()), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: fabro_types::test_support::test_run_provenance(), + manifest_blob: None, + git: None, + fork_source_ref: None, +diff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs +index e72a0ee1b..c138e4e6e 100644 +--- a/lib/crates/fabro-workflow/src/test_support.rs ++++ b/lib/crates/fabro-workflow/src/test_support.rs +@@ -10,8 +10,21 @@ use fabro_graphviz::graph::Graph as GvGraph; + use fabro_interview::AutoApproveInterviewer; + use fabro_model::Catalog; + use fabro_store::{ArtifactStore, Database, RunProjection}; ++use fabro_types::{AuthMethod, IdpIdentity, Principal, RunProvenance}; + use object_store::local::LocalFileSystem; + ++fn test_run_provenance() -> RunProvenance { ++ RunProvenance { ++ server: None, ++ client: None, ++ subject: Principal::user( ++ IdpIdentity::new("fabro:test", "test-user").expect("test identity should parse"), ++ "test".to_string(), ++ AuthMethod::DevToken, ++ ), ++ } ++} ++ + use crate::artifact_upload::ArtifactSink; + use crate::error::{Error, Result}; + use crate::event::{Emitter, Event, StoreProgressLogger, append_event}; +@@ -175,7 +188,7 @@ async fn initialized( + workflow_slug: run_options.workflow_slug.clone(), + automation: None, + db_prefix: None, +- provenance: None, ++ provenance: test_run_provenance(), + manifest_blob: None, + git: run_options.pre_run_git.clone(), + fork_source_ref: run_options.fork_source_ref.clone(), +diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +index 5e97116d9..7839b95b6 100644 +--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES ++++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +@@ -271,7 +271,6 @@ models/preflight-workflow-summary.ts + models/preview-url-request.ts + models/preview-url-response.ts + models/principal-agent.ts +-models/principal-anonymous.ts + models/principal-slack.ts + models/principal-system.ts + models/principal-user.ts +diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts +index 87b9c189c..af8ae77e0 100644 +--- a/lib/packages/fabro-api-client/src/models/index.ts ++++ b/lib/packages/fabro-api-client/src/models/index.ts +@@ -244,7 +244,6 @@ export * from './preview-url-request'; + export * from './preview-url-response'; + export * from './principal'; + export * from './principal-agent'; +-export * from './principal-anonymous'; + export * from './principal-slack'; + export * from './principal-system'; + export * from './principal-user'; +diff --git a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts b/lib/packages/fabro-api-client/src/models/principal-anonymous.ts +deleted file mode 100644 +index daac61df0..000000000 +--- a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts ++++ /dev/null +@@ -1,25 +0,0 @@ +-/* tslint:disable */ +-/* eslint-disable */ +-/** +- * Fabro Run API +- * HTTP API for managing Fabro workflow run executions. +- * +- * The version of the OpenAPI document: 0.1.0 +- * +- * +- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +- * https://openapi-generator.tech +- * Do not edit the class manually. +- */ +- +- +- +-export interface PrincipalAnonymous { +- 'kind': PrincipalAnonymousKindEnum; +-} +- +-export const PrincipalAnonymousKindEnum = { +- ANONYMOUS: 'anonymous' +-} as const; +- +-export type PrincipalAnonymousKindEnum = typeof PrincipalAnonymousKindEnum[keyof typeof PrincipalAnonymousKindEnum]; +diff --git a/lib/packages/fabro-api-client/src/models/principal.ts b/lib/packages/fabro-api-client/src/models/principal.ts +index e3597295d..08b5422df 100644 +--- a/lib/packages/fabro-api-client/src/models/principal.ts ++++ b/lib/packages/fabro-api-client/src/models/principal.ts +@@ -24,9 +24,6 @@ import type { IdpIdentity } from './idp-identity'; + import type { PrincipalAgent } from './principal-agent'; + // May contain unused imports in some cases + // @ts-ignore +-import type { PrincipalAnonymous } from './principal-anonymous'; +-// May contain unused imports in some cases +-// @ts-ignore + import type { PrincipalSlack } from './principal-slack'; + // May contain unused imports in some cases + // @ts-ignore +@@ -47,4 +44,4 @@ import type { SystemActorKind } from './system-actor-kind'; + /** + * @type Principal + */ +-export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'anonymous' } & PrincipalAnonymous | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker; ++export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker; +diff --git a/lib/packages/fabro-api-client/src/models/run-provenance.ts b/lib/packages/fabro-api-client/src/models/run-provenance.ts +index 7276857f8..59fa7a063 100644 +--- a/lib/packages/fabro-api-client/src/models/run-provenance.ts ++++ b/lib/packages/fabro-api-client/src/models/run-provenance.ts +@@ -26,5 +26,5 @@ import type { RunServerProvenance } from './run-server-provenance'; + export interface RunProvenance { + 'server'?: RunServerProvenance | null; + 'client'?: RunClientProvenance | null; +- 'subject'?: Principal | null; ++ 'subject': Principal; + } +diff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts +index f1e5adab6..6abad2884 100644 +--- a/lib/packages/fabro-api-client/src/models/run-spec.ts ++++ b/lib/packages/fabro-api-client/src/models/run-spec.ts +@@ -41,7 +41,7 @@ export interface RunSpec { + 'automation'?: AutomationRef | null; + 'source_directory'?: string | null; + 'labels'?: { [key: string]: string; }; +- 'provenance'?: RunProvenance | null; ++ 'provenance': RunProvenance; + 'manifest_blob'?: string | null; + 'definition_blob'?: string | null; + 'git'?: GitContext | null; +diff --git a/lib/packages/fabro-api-client/src/models/run.ts b/lib/packages/fabro-api-client/src/models/run.ts +index 1ed6c6c26..a4ade38a3 100644 +--- a/lib/packages/fabro-api-client/src/models/run.ts ++++ b/lib/packages/fabro-api-client/src/models/run.ts +@@ -83,7 +83,7 @@ export interface Run { + 'workflow': WorkflowRef; + 'automation': AutomationRef | null; + 'repository': RepositoryRef | null; +- 'created_by': Principal | null; ++ 'created_by': Principal; + 'origin': RunOrigin; + 'labels': { [key: string]: string; }; + 'lifecycle': RunLifecycle; +diff --git a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts +index 114b1e640..5fff8b8f4 100644 +--- a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts ++++ b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts +@@ -12,8 +12,6 @@ export function principalKind(principal: Principal): string { + switch (principal.kind) { + case "agent": + return "agent"; +- case "anonymous": +- return "anonymous"; + case "slack": + return "slack"; + case "system": diff --git a/stages/006-simplify_opus@1/status.json b/stages/006-simplify_opus@1/status.json new file mode 100644 index 000000000..0dc440c99 --- /dev/null +++ b/stages/006-simplify_opus@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Stage completed: simplify_opus", + "failure_reason": null, + "timestamp": "2026-06-03T14:02:48.741229Z" +} \ No newline at end of file diff --git a/stages/007-simplify_gpt@1/prompt.md b/stages/007-simplify_gpt@1/prompt.md new file mode 100644 index 000000000..82a06633e --- /dev/null +++ b/stages/007-simplify_gpt@1/prompt.md @@ -0,0 +1,287 @@ +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**: failed +- **simplify_opus**: succeeded + - Model: claude-opus-4-7, 316.7k tokens in / 99.2k out + - Files: /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/data/runs.ts, /home/daytona/workspace/fabro/apps/fabro-web/app/lib/principal-display.tsx, /home/daytona/workspace/fabro/docs/internal/logging-strategy.md, /home/daytona/workspace/fabro/docs/public/api-reference/fabro-api.yaml, /home/daytona/workspace/fabro/lib/crates/fabro-api/Cargo.toml, /home/daytona/workspace/fabro/lib/crates/fabro-api/tests/principal_round_trip.rs, /home/daytona/workspace/fabro/lib/crates/fabro-api/tests/run_event_round_trip.rs, /home/daytona/workspace/fabro/lib/crates/fabro-api/tests/run_summary_round_trip.rs, /home/daytona/workspace/fabro/lib/crates/fabro-cli/Cargo.toml, /home/daytona/workspace/fabro/lib/crates/fabro-cli/tests/it/cmd/inspect.rs, /home/daytona/workspace/fabro/lib/crates/fabro-cli/tests/it/cmd/support.rs, /home/daytona/workspace/fabro/lib/crates/fabro-dump/src/lib.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/auth/cli_flow.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/demo/mod.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/principal_middleware.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/run_manifest.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/lifecycle.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/runs.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/sandbox.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/tests.rs, /home/daytona/workspace/fabro/lib/crates/fabro-server/src/web_auth.rs, /home/daytona/workspace/fabro/lib/crates/fabro-store/Cargo.toml, /home/daytona/workspace/fabro/lib/crates/fabro-store/src/run_state.rs, /home/daytona/workspace/fabro/lib/crates/fabro-store/src/slate/mod.rs, /home/daytona/workspace/fabro/lib/crates/fabro-store/src/slate/run_store.rs, /home/daytona/workspace/fabro/lib/crates/fabro-store/tests/serializable_projection.rs, /home/daytona/workspace/fabro/lib/crates/fabro-tool/Cargo.toml, /home/daytona/workspace/fabro/lib/crates/fabro-tool/src/common.rs, /home/daytona/workspace/fabro/lib/crates/fabro-tool/src/create.rs, /home/daytona/workspace/fabro/lib/crates/fabro-tool/src/interact.rs, /home/daytona/workspace/fabro/lib/crates/fabro-tool/src/search.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/lib.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/principal.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/mod.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_event/run.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_projection.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/run_summary.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_event_serde.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_methods.rs, /home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_serde.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/convert.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/events.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/event/stored_fields.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/operations/create.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/operations/retry.rs, /home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/test_support.rs, /home/daytona/workspace/fabro/lib/packages/fabro-api-client/tests/principal-exhaustive.ts + + +Simplify the code diff --git a/stages/007-simplify_gpt@1/provider_used.json b/stages/007-simplify_gpt@1/provider_used.json new file mode 100644 index 000000000..a04162cbf --- /dev/null +++ b/stages/007-simplify_gpt@1/provider_used.json @@ -0,0 +1,5 @@ +{ + "mode": "agent", + "provider": "openai", + "model": "gpt-5.5" +} \ No newline at end of file