fabro/run.json
Fabro d95ff9fa64 finalize run
⚒️ Generated with [Fabro](https://fabro.sh)
2026-06-03 13:12:23 -04:00

2772 lines
No EOL
417 KiB
JSON

{
"title": "Make run actors and provenance total",
"spec": {
"run_id": "01KT734BXFV007VWXT3G5PFXYA",
"settings": {
"project": {
"name": null,
"description": null,
"metadata": {}
},
"workflow": {
"name": null,
"description": null,
"graph": "workflow.fabro",
"metadata": {}
},
"run": {
"goal": {
"type": "inline",
"value": "# 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<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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"
},
"working_dir": null,
"metadata": {},
"inputs": {},
"model": {
"provider": "anthropic",
"name": "claude-sonnet-4-6",
"fallbacks": [],
"controls": {
"reasoning_effort": null,
"speed": null
}
},
"git": {
"author": null
},
"prepare": {
"commands": [],
"timeout_ms": 300000
},
"execution": {
"mode": "normal",
"approval": "prompt"
},
"checkpoint": {
"exclude_globs": [],
"skip_git_hooks": false
},
"clone": {
"enabled": true
},
"run_branch": {
"enabled": true,
"push": true
},
"meta_branch": {
"enabled": true,
"push": true
},
"environment": {
"id": "fabro-dev",
"provider": "daytona",
"image": {
"docker": null,
"dockerfile": {
"type": "inline",
"value": "FROM ubuntu:24.04\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n curl git ripgrep ca-certificates build-essential pkg-config libssl-dev unzip python3 \\\n xvfb xfce4 xfce4-terminal x11vnc novnc dbus-x11 \\\n libx11-6 libxrandr2 libxext6 libxrender1 libxfixes3 libxss1 libxtst6 libxi6 \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install real Chromium (not the snap stub) via xtradeb PPA\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n software-properties-common curl gnupg \\\n && add-apt-repository -y ppa:xtradeb/apps \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends chromium \\\n && rm -rf /var/lib/apt/lists/*\n\n# Wrapper: Chromium needs --no-sandbox when running as root in a container,\n# and --disable-dev-shm-usage avoids crashes from small /dev/shm\nRUN printf '#!/bin/bash\\nexec /usr/bin/chromium --no-sandbox --disable-dev-shm-usage \"$@\"\\n' \\\n > /usr/local/bin/chromium-wrapper \\\n && chmod +x /usr/local/bin/chromium-wrapper\n\n# Make the wrapper the default in the system .desktop file and via alternatives\nRUN sed -i 's|^Exec=.*|Exec=/usr/local/bin/chromium-wrapper %U|' \\\n /usr/share/applications/chromium.desktop \\\n && update-alternatives --install /usr/bin/x-www-browser x-www-browser \\\n /usr/local/bin/chromium-wrapper 100\n\n# Tell XFCE's exo-open that Chromium is the WebBrowser helper (system-wide)\nRUN mkdir -p /etc/xdg/xfce4 /usr/share/xfce4/helpers \\\n && printf 'WebBrowser=custom-WebBrowser\\n' > /etc/xdg/xfce4/helpers.rc \\\n && printf '[Desktop Entry]\\n\\\nVersion=1.0\\n\\\nType=X-XFCE-Helper\\n\\\nName=Chromium\\n\\\nIcon=chromium\\n\\\nX-XFCE-Category=WebBrowser\\n\\\nX-XFCE-CommandsWithParameter=/usr/local/bin/chromium-wrapper \"%%s\"\\n\\\nX-XFCE-Commands=/usr/local/bin/chromium-wrapper\\n' \\\n > /usr/share/xfce4/helpers/custom-WebBrowser.desktop\n\n# GitHub CLI\nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\\n | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \\\n && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" \\\n | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \\\n && apt-get update && apt-get install -y --no-install-recommends gh \\\n && rm -rf /var/lib/apt/lists/*\n\n# Rust\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\nRUN rustup toolchain install nightly-2026-04-14 --profile minimal --component clippy,rustfmt\nRUN cargo install cargo-nextest --locked\nENV CARGO_INCREMENTAL=0\n\n# Bun\nRUN curl -fsSL https://bun.sh/install | bash\nENV PATH=\"/root/.bun/bin:${PATH}\"\n\nWORKDIR /root\n"
}
},
"resources": {
"cpu": 8,
"memory": "16GB",
"disk": "20GB"
},
"network": {
"mode": "allow_all",
"allow": []
},
"lifecycle": {
"preserve": false,
"stop_on_terminal": true,
"auto_stop": "30m"
},
"labels": {
"repo": "fabro-sh/fabro"
},
"volumes": [],
"env": {}
},
"notifications": {
"feed": {
"enabled": true,
"provider": "slack",
"events": [
"run.started",
"run.completed",
"run.failed"
],
"slack": {
"channel": "#feed-fabro"
}
}
},
"interviews": {
"provider": null,
"slack": null
},
"agent": {
"fabro_tools": false,
"permissions": null,
"mcps": {}
},
"hooks": [],
"scm": {
"provider": null,
"owner": null,
"repository": null,
"github": null
},
"pull_request": {
"enabled": true,
"draft": false,
"auto_merge": false,
"merge_strategy": "squash"
},
"artifacts": {
"include": []
},
"integrations": {
"github": {
"permissions": {}
}
}
}
},
"graph": {
"name": "ImplementPlan",
"nodes": {
"verify": {
"id": "verify",
"attrs": {
"retry_target": {
"String": "fixup"
},
"shape": {
"String": "parallelogram"
},
"model": {
"String": "claude-opus-4-7"
},
"goal_gate": {
"Boolean": true
},
"label": {
"String": "Verify"
},
"script": {
"String": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1"
},
"provider": {
"String": "anthropic"
}
}
},
"preflight_compile": {
"id": "preflight_compile",
"attrs": {
"max_retries": {
"Integer": 0
},
"shape": {
"String": "parallelogram"
},
"script": {
"String": "cargo check -q --workspace 2>&1"
},
"model": {
"String": "claude-opus-4-7"
},
"provider": {
"String": "anthropic"
},
"label": {
"String": "Preflight Compile"
}
}
},
"simplify_opus": {
"id": "simplify_opus",
"attrs": {
"model": {
"String": "claude-opus-4-7"
},
"prompt": {
"String": "# Simplify: Code Review and Cleanup\n\nReview all changed files for reuse, quality, and efficiency. Fix any issues found.\n\n## Phase 1: Identify Changes\n\nRun \\`git diff\\` (or \\`git diff HEAD\\` if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse the ${AGENT_TOOL_NAME} tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. **Search for existing utilities and helpers** that could replace newly written code. Look for similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. **Flag any new function that duplicates existing functionality.** Suggest the existing function to use instead.\n3. **Flag any inline logic that could use an existing utility** — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\n1. **Redundant state**: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. **Parameter sprawl**: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. **Copy-paste with slight variation**: near-duplicate code blocks that should be unified with a shared abstraction\n4. **Leaky abstractions**: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior\n7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. **Unnecessary work**: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. **Missed concurrency**: independent operations run sequentially when they could run in parallel\n3. **Hot-path bloat**: new blocking work added to startup or per-request/per-render hot paths\n4. **Recurring no-op updates**: state/store updates inside polling loops, intervals, or event handlers that fire unconditionally — add a change-detection guard so downstream consumers aren't notified when nothing changed. Also: if a wrapper function takes an updater/reducer callback, verify it honors same-reference returns (or whatever the \"no change\" signal is) — otherwise callers' early-return no-ops are silently defeated\n5. **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n6. **Memory**: unbounded data structures, missing cleanup, event listener leaks\n7. **Overly broad operations**: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean)."
},
"label": {
"String": "Simplify (Opus)"
},
"provider": {
"String": "anthropic"
}
}
},
"implement": {
"id": "implement",
"attrs": {
"reasoning_effort": {
"String": "xhigh"
},
"prompt": {
"String": "Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."
},
"model": {
"String": "gpt-5.5"
},
"label": {
"String": "Implement"
},
"provider": {
"String": "openai"
}
}
},
"simplify_gpt": {
"id": "simplify_gpt",
"attrs": {
"provider": {
"String": "openai"
},
"model": {
"String": "gpt-5.5"
},
"label": {
"String": "Simplify (GPT-55)"
},
"prompt": {
"String": "# Simplify: Code Review and Cleanup\n\nReview all changed files for reuse, quality, and efficiency. Fix any issues found.\n\n## Phase 1: Identify Changes\n\nRun \\`git diff\\` (or \\`git diff HEAD\\` if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse the ${AGENT_TOOL_NAME} tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. **Search for existing utilities and helpers** that could replace newly written code. Look for similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. **Flag any new function that duplicates existing functionality.** Suggest the existing function to use instead.\n3. **Flag any inline logic that could use an existing utility** — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\n1. **Redundant state**: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. **Parameter sprawl**: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. **Copy-paste with slight variation**: near-duplicate code blocks that should be unified with a shared abstraction\n4. **Leaky abstractions**: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior\n7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. **Unnecessary work**: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. **Missed concurrency**: independent operations run sequentially when they could run in parallel\n3. **Hot-path bloat**: new blocking work added to startup or per-request/per-render hot paths\n4. **Recurring no-op updates**: state/store updates inside polling loops, intervals, or event handlers that fire unconditionally — add a change-detection guard so downstream consumers aren't notified when nothing changed. Also: if a wrapper function takes an updater/reducer callback, verify it honors same-reference returns (or whatever the \"no change\" signal is) — otherwise callers' early-return no-ops are silently defeated\n5. **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n6. **Memory**: unbounded data structures, missing cleanup, event listener leaks\n7. **Overly broad operations**: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean)."
}
}
},
"start": {
"id": "start",
"attrs": {
"model": {
"String": "claude-opus-4-7"
},
"label": {
"String": "Start"
},
"shape": {
"String": "Mdiamond"
},
"provider": {
"String": "anthropic"
}
}
},
"fix_lints": {
"id": "fix_lints",
"attrs": {
"label": {
"String": "Fix Lints"
},
"model": {
"String": "claude-opus-4-7"
},
"max_visits": {
"Integer": 3
},
"provider": {
"String": "anthropic"
},
"prompt": {
"String": "The preflight lint step failed. Read the build output from context and fix all clippy lint warnings."
}
}
},
"preflight_lint": {
"id": "preflight_lint",
"attrs": {
"model": {
"String": "claude-opus-4-7"
},
"script": {
"String": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1"
},
"shape": {
"String": "parallelogram"
},
"max_retries": {
"Integer": 0
},
"provider": {
"String": "anthropic"
},
"label": {
"String": "Preflight Lint"
}
}
},
"exit": {
"id": "exit",
"attrs": {
"model": {
"String": "claude-opus-4-7"
},
"shape": {
"String": "Msquare"
},
"provider": {
"String": "anthropic"
},
"label": {
"String": "Exit"
}
}
},
"fixup": {
"id": "fixup",
"attrs": {
"prompt": {
"String": "The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures."
},
"model": {
"String": "claude-opus-4-7"
},
"max_visits": {
"Integer": 3
},
"label": {
"String": "Fixup"
},
"provider": {
"String": "anthropic"
}
}
},
"toolchain": {
"id": "toolchain",
"attrs": {
"shape": {
"String": "parallelogram"
},
"max_retries": {
"Integer": 0
},
"script": {
"String": "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"
},
"label": {
"String": "Toolchain"
},
"provider": {
"String": "anthropic"
},
"model": {
"String": "claude-opus-4-7"
}
}
}
},
"edges": [
{
"from": "start",
"to": "toolchain",
"attrs": {}
},
{
"from": "toolchain",
"to": "preflight_compile",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "toolchain",
"to": "exit",
"attrs": {}
},
{
"from": "preflight_compile",
"to": "preflight_lint",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "preflight_compile",
"to": "exit",
"attrs": {}
},
{
"from": "preflight_lint",
"to": "implement",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "preflight_lint",
"to": "fix_lints",
"attrs": {}
},
{
"from": "fix_lints",
"to": "preflight_lint",
"attrs": {}
},
{
"from": "implement",
"to": "simplify_opus",
"attrs": {}
},
{
"from": "simplify_opus",
"to": "simplify_gpt",
"attrs": {}
},
{
"from": "simplify_gpt",
"to": "verify",
"attrs": {}
},
{
"from": "verify",
"to": "exit",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "verify",
"to": "fixup",
"attrs": {}
},
{
"from": "fixup",
"to": "verify",
"attrs": {}
}
],
"attrs": {
"rankdir": {
"String": "LR"
},
"model_stylesheet": {
"String": "\n * { model: claude-opus-4-7; }\n "
},
"goal": {
"String": "# 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<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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"
}
}
},
"graph_source": "digraph ImplementPlan {\n graph [\n goal=\"Implement and simplify\",\n model_stylesheet=\"\n * { model: claude-opus-4-7; }\n \"\n ]\n rankdir=LR\n\n start [shape=Mdiamond, label=\"Start\"]\n exit [shape=Msquare, label=\"Exit\"]\n\n toolchain [label=\"Toolchain\", shape=parallelogram, 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\", max_retries=0]\n preflight_compile [label=\"Preflight Compile\", shape=parallelogram, script=\"cargo check -q --workspace 2>&1\", max_retries=0]\n preflight_lint [label=\"Preflight Lint\", shape=parallelogram, script=\"cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1\", max_retries=0]\n fix_lints [label=\"Fix Lints\", prompt=\"The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.\", max_visits=3]\n implement [label=\"Implement\", prompt=\"Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.\", model=\"gpt-55\", reasoning_effort=\"xhigh\"]\n simplify_opus [label=\"Simplify (Opus)\", prompt=\"@prompts/simplify.md\"]\n simplify_gpt [label=\"Simplify (GPT-55)\", prompt=\"@prompts/simplify.md\", model=\"gpt-55\"]\n verify [label=\"Verify\", shape=parallelogram, script=\"git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\\\"disabled\\\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1\", goal_gate=true, retry_target=\"fixup\"]\n fixup [label=\"Fixup\", prompt=\"The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.\", max_visits=3]\n\n start -> toolchain\n toolchain -> preflight_compile [condition=\"outcome=succeeded\"]\n toolchain -> exit\n preflight_compile -> preflight_lint [condition=\"outcome=succeeded\"]\n preflight_compile -> exit\n preflight_lint -> implement [condition=\"outcome=succeeded\"]\n preflight_lint -> fix_lints\n fix_lints -> preflight_lint\n implement -> simplify_opus -> simplify_gpt -> verify\n verify -> exit [condition=\"outcome=succeeded\"]\n verify -> fixup\n fixup -> verify\n}\n",
"workflow_slug": "implement-plan",
"source_directory": "/Users/bhelmkamp/p/fabro-sh/fabro",
"provenance": {
"server": {
"version": "0.253.0-nightly.0"
},
"client": {
"user_agent": "fabro-cli/0.253.0-nightly.0",
"name": "fabro-cli",
"version": "0.253.0-nightly.0"
},
"subject": {
"kind": "user",
"identity": {
"issuer": "https://github.com",
"subject": "19"
},
"login": "brynary",
"auth_method": "github",
"avatar_url": "https://avatars.githubusercontent.com/u/19?v=4"
}
},
"manifest_blob": "4c4599cfbcbabf9e6044d98ba0c87c878d794f274c6f7c3a9e0d0d0d8041c6f7",
"definition_blob": "d11a63cf1a622e3d433cd9aedb795d322a2bbb36a43751c76db08282bd87b68c",
"git": {
"origin_url": "https://github.com/fabro-sh/fabro",
"branch": "main",
"sha": "d8c16e5396681880573f7ae4e4beed4c70020824",
"dirty": "dirty",
"push_outcome": {
"type": "not_attempted"
}
}
},
"web_url": "http://127.0.0.1:32276/runs/01KT734BXFV007VWXT3G5PFXYA",
"start": {
"start_time": "2026-06-03T15:55:32.289851Z",
"run_branch": "fabro/run/01KT734BXFV007VWXT3G5PFXYA",
"base_sha": "d8c16e5396681880573f7ae4e4beed4c70020824"
},
"status": {
"kind": "running"
},
"status_updated_at": "2026-06-03T15:55:32.289943Z",
"last_event_at": "2026-06-03T17:12:22.695009Z",
"pending_control": null,
"checkpoints": [
{
"seq": 21,
"checkpoint": {
"timestamp": "2026-06-03T15:55:34.750854Z",
"current_node": "start",
"completed_nodes": [
"start"
],
"node_retries": {},
"context_values": {
"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<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.node_visit_count": 1,
"current_node": "start",
"graph.rankdir": "LR",
"failure_class": "",
"internal.fidelity": "compact",
"internal.retry_count.start": 0,
"internal.run_id": "01KT734BXFV007VWXT3G5PFXYA",
"internal.thread_id": null,
"outcome": "succeeded",
"internal.work_dir": "/home/daytona/workspace/fabro",
"failure_signature": ""
},
"node_outcomes": {
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "toolchain",
"node_visits": {
"start": 1
}
},
"diff": {}
},
{
"seq": 29,
"checkpoint": {
"timestamp": "2026-06-03T15:55:47.014407Z",
"current_node": "toolchain",
"completed_nodes": [
"start",
"toolchain"
],
"node_retries": {},
"context_values": {
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"failure_class": "",
"internal.work_dir": "/home/daytona/workspace/fabro",
"failure_signature": "",
"graph.rankdir": "LR",
"thread.start.current_node": "toolchain",
"internal.node_visit_count": 1,
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c",
"internal.run_id": "01KT734BXFV007VWXT3G5PFXYA",
"internal.retry_count.start": 0,
"graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = LazyLock::new(|| {\n Principal::user(\n IdpIdentity::new(\"fabro:demo\", \"demo\").unwrap(),\n \"demo\".to_string(),\n AuthMethod::DevToken,\n )\n });\n ```\n- Replace `created_by: None` with `created_by: DEMO_PRINCIPAL.clone()`.\n- If demo creates any full `RunSpec` or `run.created` event data, give it `RunProvenance { subject: DEMO_PRINCIPAL.clone(), ... }`.\n\n### Test support\n\nDo not add fake auth helpers to `fabro_types::fixtures`; that module is run-id constants.\n\nUse the existing `fabro-types` `test-support` feature:\n- Add `#[cfg(any(test, feature = \"test-support\"))] pub mod test_support;` in `lib/crates/fabro-types/src/lib.rs` if it does not already exist.\n- Add `lib/crates/fabro-types/src/test_support.rs` with:\n - `test_principal() -> Principal`\n - `test_run_provenance() -> RunProvenance`\n- Use an obviously fake dev-token identity, e.g. issuer `fabro:test`, subject `test-user`, login `test`.\n- In crates that need the helper from integration tests or cross-crate tests, dual-list `fabro-types` in `dev-dependencies` with `features = [\"test-support\"]`, following existing repo patterns.\n\nUpdate all constructors:\n- Replace `provenance: None` in `RunSpec`, `CreateRunInput`, `RetryRunInput`, `Event::RunCreated`, and `RunCreatedProps` literals with `test_run_provenance()` or a locally meaningful provenance.\n- Replace `subject: Some(...)` with `subject: ...`.\n- Replace `subject: None` only when it is actually `RunProvenance.subject`; leave unrelated todo/commit/message `subject` fields alone.\n- Replace `created_by: None` / `created_by: null` with `test_principal()` or a frontend TS principal fixture.\n- Delete tests that assert nullable or omitted creator/provenance behavior.\n\nRepresentative Rust areas:\n- `lib/crates/fabro-store/src/run_state.rs`\n- `lib/crates/fabro-store/tests/serializable_projection.rs`\n- `lib/crates/fabro-workflow/src/operations/{create,retry,start}.rs`\n- `lib/crates/fabro-workflow/src/event/{convert,sink,stored_fields}.rs`\n- `lib/crates/fabro-workflow/src/handler/**`\n- `lib/crates/fabro-workflow/src/pipeline/**`\n- `lib/crates/fabro-workflow/src/run_{lookup,metadata}.rs`\n- `lib/crates/fabro-server/src/server/tests.rs`\n- `lib/crates/fabro-server/src/server/handler/**`\n- `lib/crates/fabro-server/tests/it/**`\n- `lib/crates/fabro-cli/tests/it/support/mod.rs`\n- `lib/crates/fabro-dump/src/lib.rs`\n- `lib/crates/fabro-tool/src/{common,create,interact,search}.rs`\n- `lib/crates/fabro-api/tests/{principal_round_trip,run_summary_round_trip,run_projection_round_trip,run_event_round_trip}.rs`\n- `lib/crates/fabro-types/tests/{run_spec_serde,run_spec_methods,run_event_serde}.rs`\n\nRepresentative TypeScript areas:\n- `apps/fabro-web/app/**` tests with `created_by: null`\n- `apps/fabro-web/app/data/runs.ts`\n- `apps/fabro-web/app/components/run-summary-panel.tsx`\n- `apps/fabro-web/app/components/runs-list/**`\n- `lib/packages/fabro-api-client/tests/principal-exhaustive.ts`\n\nUseful sweep after edits:\n- `rg -n \"Principal::Anonymous|PrincipalAnonymous|principal-anonymous|kind: ['\\\"]anonymous|created_by:\\\\s*(None|null)|provenance:\\\\s*None|subject:\\\\s*Some\\\\(|subject:\\\\s*None\" lib/crates apps/fabro-web lib/packages/fabro-api-client docs/public docs/internal`\n\nReview each hit. The only acceptable remaining matches should be unrelated uses of \"anonymous\" and unrelated non-principal `subject` fields.\n\n### Frontend\n\n`apps/fabro-web/app/components/run-summary-panel.tsx`:\n- `run?.created_by` may still be guarded by `run` loading state, but `created_by` itself is non-null once `run` exists.\n- Pass `run.created_by` directly to `principalDisplay(...)` inside loaded-run branches.\n\n`apps/fabro-web/app/data/runs.ts` and run-list components:\n- Treat `createdBy` as a total principal in UI data derived from a loaded API run.\n- Remove empty/fallback rendering that only existed for missing creator data.\n\n### Verification\n\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\n- Manual end-to-end:\n - `fabro server start`\n - `cd apps/fabro-web && bun run dev`\n - Authenticate and create a run through the UI.\n - Confirm `/api/v1/runs/:id` has non-null `created_by`.\n - Confirm `/api/v1/runs/:id/state` has non-null `spec.provenance.subject`.\n - Retry a failed run and confirm the retried run has the retrying user as creator.\n - Hit demo mode with `X-Fabro-Demo: 1` and confirm the run summary renders the synthetic `demo` dev-token user.\n",
"internal.thread_id": "start",
"internal.retry_count.toolchain": 0,
"current_node": "toolchain",
"outcome": "succeeded",
"internal.fidelity": "compact"
},
"node_outcomes": {
"start": {
"status": "succeeded",
"usage": null
},
"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": 1316,
"active_time_ms": 1316
}
}
},
"next_node_id": "preflight_compile",
"git_commit_sha": "5f90ef02d8e4fa7a9708e2791558bcce63dfe1a6",
"node_visits": {
"toolchain": 1,
"start": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 39,
"checkpoint": {
"timestamp": "2026-06-03T15:58:06.523080Z",
"current_node": "preflight_compile",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile"
],
"node_retries": {},
"context_values": {
"current_node": "preflight_compile",
"graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"graph.rankdir": "LR",
"failure_signature": "",
"internal.retry_count.preflight_compile": 0,
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.thread_id": "toolchain",
"internal.retry_count.start": 0,
"internal.run_id": "01KT734BXFV007VWXT3G5PFXYA",
"thread.start.current_node": "toolchain",
"thread.toolchain.current_node": "preflight_compile",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"internal.fidelity": "compact",
"outcome": "succeeded",
"internal.retry_count.toolchain": 0,
"failure_class": "",
"internal.node_visit_count": 1
},
"node_outcomes": {
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1316,
"active_time_ms": 1316
}
},
"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": 134621,
"active_time_ms": 134621
}
},
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "preflight_lint",
"git_commit_sha": "12be15ef0c60ec5210d94e6473df16349013c09e",
"node_visits": {
"start": 1,
"preflight_compile": 1,
"toolchain": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 49,
"checkpoint": {
"timestamp": "2026-06-03T16:00:43.841586Z",
"current_node": "preflight_lint",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint"
],
"node_retries": {},
"context_values": {
"internal.retry_count.start": 0,
"current_node": "preflight_lint",
"thread.toolchain.current_node": "preflight_compile",
"internal.fidelity": "compact",
"internal.run_id": "01KT734BXFV007VWXT3G5PFXYA",
"thread.preflight_compile.current_node": "preflight_lint",
"failure_class": "",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.thread_id": "preflight_compile",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"thread.start.current_node": "toolchain",
"failure_signature": "",
"internal.retry_count.preflight_compile": 0,
"internal.retry_count.toolchain": 0,
"outcome": "succeeded",
"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<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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_lint": 0,
"graph.rankdir": "LR",
"internal.node_visit_count": 1
},
"node_outcomes": {
"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": 147496,
"active_time_ms": 147496
}
},
"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": 1316,
"active_time_ms": 1316
}
},
"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": 134621,
"active_time_ms": 134621
}
},
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "implement",
"git_commit_sha": "00ad441fa1692c7acef78d12961b59507f8fa299",
"node_visits": {
"preflight_lint": 1,
"toolchain": 1,
"preflight_compile": 1,
"start": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 999,
"checkpoint": {
"timestamp": "2026-06-03T16:56:08.693119Z",
"current_node": "implement",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement"
],
"node_retries": {},
"context_values": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"graph.rankdir": "LR",
"last_response": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Remo",
"thread.toolchain.current_node": "preflight_compile",
"graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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",
"outcome": "succeeded",
"failure_class": "",
"last_stage": "implement",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.retry_count.preflight_lint": 0,
"internal.node_visit_count": 1,
"internal.run_id": "01KT734BXFV007VWXT3G5PFXYA",
"response.implement": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Removed `Principal::Anonymous`; unauthenticated/invalid request state is now represented as `None`, with HTTP logs using `principal_kind=\"none\"`.\n- Made persisted run actors/provenance total:\n - `Run.created_by: Principal`\n - `RunSpec.provenance: RunProvenance`\n - `RunProvenance.subject: Principal`\n - `run.created` provenance required/non-null\n- Updated OpenAPI and regenerated Rust/TS API outputs.\n- Added `fabro-types::test_support` helpers behind `test-support`.\n- Updated server/workflow/store projections, create/retry flows, demo principal, frontend UI/data assumptions, and test fixtures/snapshots.\n- Final working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\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- Final sweeps for anonymous principal and nullable creator/provenance patterns:\n - only unrelated `crossorigin=\"anonymous\"` and unrelated todo/web-auth `subject` fields remain.\n\nNotes:\n- `cargo insta pending-snapshots` could not run because `cargo-insta` is not installed in this environment; I checked for pending `*.snap.new` files and found none.\n- Manual browser/server E2E verification was not performed in this environment.",
"failure_signature": "",
"internal.fidelity": "compact",
"thread.preflight_compile.current_node": "preflight_lint",
"thread.preflight_lint.current_node": "implement",
"current_node": "implement",
"internal.retry_count.toolchain": 0,
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.preflight_compile": 0,
"internal.retry_count.implement": 0,
"internal.retry_count.start": 0,
"internal.thread_id": "preflight_lint",
"thread.start.current_node": "toolchain"
},
"node_outcomes": {
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1316,
"active_time_ms": 1316
}
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"response.implement": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Removed `Principal::Anonymous`; unauthenticated/invalid request state is now represented as `None`, with HTTP logs using `principal_kind=\"none\"`.\n- Made persisted run actors/provenance total:\n - `Run.created_by: Principal`\n - `RunSpec.provenance: RunProvenance`\n - `RunProvenance.subject: Principal`\n - `run.created` provenance required/non-null\n- Updated OpenAPI and regenerated Rust/TS API outputs.\n- Added `fabro-types::test_support` helpers behind `test-support`.\n- Updated server/workflow/store projections, create/retry flows, demo principal, frontend UI/data assumptions, and test fixtures/snapshots.\n- Final working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\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- Final sweeps for anonymous principal and nullable creator/provenance patterns:\n - only unrelated `crossorigin=\"anonymous\"` and unrelated todo/web-auth `subject` fields remain.\n\nNotes:\n- `cargo insta pending-snapshots` could not run because `cargo-insta` is not installed in this environment; I checked for pending `*.snap.new` files and found none.\n- Manual browser/server E2E verification was not performed in this environment.",
"last_response": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Remo"
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 4104889,
"output_tokens": 33716,
"reasoning_tokens": 15110,
"cache_read_tokens": 27674624,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 35826537
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 1761337,
"tool_time_ms": 1443288,
"active_time_ms": 3204625
}
},
"start": {
"status": "succeeded",
"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": 147496,
"active_time_ms": 147496
}
},
"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": 134621,
"active_time_ms": 134621
}
}
},
"next_node_id": "simplify_opus",
"git_commit_sha": "74c81d145a1a27c75e1fb4dd13f283cff9ecde11",
"node_visits": {
"preflight_lint": 1,
"start": 1,
"preflight_compile": 1,
"implement": 1,
"toolchain": 1
}
},
"diff": {
"patch": "diff --git a/apps/fabro-web/app/components/run-summary-panel.test.tsx b/apps/fabro-web/app/components/run-summary-panel.test.tsx\nindex 52e79cf0f..5266f20bd 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<string, any> = {}) {\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@@ -71,9 +76,8 @@ describe(\"RunSummaryPanelView\", () => {\n }\n });\n \n- test(\"shows unavailable copy for missing run fields after load\", () => {\n+ test(\"shows unavailable copy for missing optional run fields after load\", () => {\n const tree = render({ run: makeRun() });\n- expect(instanceText(cellAfterLabel(tree, \"Created by\"))).toBe(EMPTY_VALUE);\n expect(instanceText(cellAfterLabel(tree, \"Changes\"))).toBe(EMPTY_VALUE);\n expect(instanceText(cellAfterLabel(tree, \"Cost\"))).toBe(EMPTY_VALUE);\n });\n@@ -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..78c42efc3 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -116,7 +116,7 @@ export function RunSummaryPanelView({\n artifactsCount,\n artifactsLoading,\n }: RunSummaryPanelViewProps) {\n- const created = run?.created_by ? principalDisplay(run.created_by) : null;\n+ const created = run ? principalDisplay(run.created_by) : null;\n const diff = run?.diff ?? null;\n const cost = formatUsdMicros(run?.billing?.total_usd_micros);\n const sandboxKind = sandboxLifecycleKind(run?.sandbox);\n@@ -133,7 +133,7 @@ export function RunSummaryPanelView({\n <span className={VALUE_CLASS}>{created.label}</span>\n </div>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -151,7 +151,7 @@ export function RunSummaryPanelView({\n </span>\n </div>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \ndiff --git a/apps/fabro-web/app/components/runs-list/run-table-row.tsx b/apps/fabro-web/app/components/runs-list/run-table-row.tsx\nindex 4fdb82777..5fd17dad1 100644\n--- a/apps/fabro-web/app/components/runs-list/run-table-row.tsx\n+++ b/apps/fabro-web/app/components/runs-list/run-table-row.tsx\n@@ -54,7 +54,7 @@ export function RunTableRow({\n </td>\n {show(\"created_by\") && (\n <td className=\"relative z-10 w-8 whitespace-nowrap px-3 py-2.5\">\n- {run.createdBy && (() => {\n+ {(() => {\n const display = principalDisplay(run.createdBy);\n return (\n <Tooltip label={display.label}>\ndiff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts\nindex 98586b2c4..3c4d6b984 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,12 @@ function makeRun(overrides: Partial<Run> = {}): 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: {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts\nindex 2b277b6d0..3bc9779de 100644\n--- a/apps/fabro-web/app/data/runs.ts\n+++ b/apps/fabro-web/app/data/runs.ts\n@@ -41,7 +41,7 @@ export interface RunItem {\n sandboxWorkingDirectory?: string;\n sourceDirectory?: string;\n createdAt?: string;\n- createdBy?: Principal | null;\n+ createdBy: Principal;\n lastEventAt?: string;\n size?: RunSize;\n }\ndiff --git a/apps/fabro-web/app/lib/principal-display.tsx b/apps/fabro-web/app/lib/principal-display.tsx\nindex fa9d4ec16..666f0f64c 100644\n--- a/apps/fabro-web/app/lib/principal-display.tsx\n+++ b/apps/fabro-web/app/lib/principal-display.tsx\n@@ -4,7 +4,6 @@ import {\n ChatBubbleLeftEllipsisIcon,\n Cog6ToothIcon,\n CpuChipIcon,\n- QuestionMarkCircleIcon,\n ServerIcon,\n } from \"@heroicons/react/20/solid\";\n import type { Principal } from \"@qltysh/fabro-api-client\";\n@@ -57,10 +56,5 @@ export function principalDisplay(actor: Principal): PrincipalDisplay {\n return { glyph: principalIconGlyph(<BoltIcon className=\"size-3\" />), label: \"webhook\" };\n case \"worker\":\n return { glyph: principalIconGlyph(<ServerIcon className=\"size-3\" />), label: \"worker\" };\n- case \"anonymous\":\n- return {\n- glyph: principalIconGlyph(<QuestionMarkCircleIcon className=\"size-3\" />),\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..7ba978f81 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,12 @@ 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: {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\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..4bfb3fc8c 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,12 @@ function makeRun(overrides: Record<string, unknown> = {}) {\n origin_url: \"https://github.com/fallback/repo.git\",\n provider: \"github\",\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: { 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..85920628e 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,12 @@ 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: {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\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..68c6258b0 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,12 @@ 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: {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\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..225f95ce3 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,12 @@ 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: {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\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..0207379d5 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,12 @@ 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: {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md\nindex 63f6f8f54..469b0a40a 100644\n--- a/docs/internal/logging-strategy.md\n+++ b/docs/internal/logging-strategy.md\n@@ -118,11 +118,11 @@ Fields are key-value pairs that make events queryable. Include enough context th\n | `error` | Error value on failure |\n | `path` | File system path |\n | `duration_ms` | Elapsed time in milliseconds |\n-| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) |\n+| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) |\n | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) |\n | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests |\n \n-For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`).\n+For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. Requests with no principal use `principal_kind=\"none\"`; `auth_status` distinguishes missing, invalid, expired, and authenticated auth state. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`).\n \n Server auth intentionally exposes a mutable `RequestAuth` context slot for public auth routes and guard extractors such as `RequiredUser` / `RequireRunScoped` for protected routes. There is no loose `RequestPrincipal` extractor; route-facing extractors should enforce the route's auth contract while the slot supplies the final HTTP log fields.\n | `input_tokens` | Token count for LLM input |\ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex 35d7511e5..bfb3e66a1 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -8992,6 +8992,8 @@ components:\n \n RunProvenance:\n type: object\n+ required:\n+ - subject\n properties:\n server:\n oneOf:\n@@ -9002,9 +9004,7 @@ components:\n - $ref: \"#/components/schemas/RunClientProvenance\"\n - type: \"null\"\n subject:\n- oneOf:\n- - $ref: \"#/components/schemas/Principal\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/Principal\"\n \n Principal:\n oneOf:\n@@ -9014,7 +9014,6 @@ components:\n - $ref: \"#/components/schemas/PrincipalSlack\"\n - $ref: \"#/components/schemas/PrincipalAgent\"\n - $ref: \"#/components/schemas/PrincipalSystem\"\n- - $ref: \"#/components/schemas/PrincipalAnonymous\"\n discriminator:\n propertyName: kind\n mapping:\n@@ -9024,7 +9023,6 @@ components:\n slack: \"#/components/schemas/PrincipalSlack\"\n agent: \"#/components/schemas/PrincipalAgent\"\n system: \"#/components/schemas/PrincipalSystem\"\n- anonymous: \"#/components/schemas/PrincipalAnonymous\"\n \n PrincipalUser:\n type: object\n@@ -9114,15 +9112,6 @@ components:\n system_kind:\n $ref: \"#/components/schemas/SystemActorKind\"\n \n- PrincipalAnonymous:\n- type: object\n- required:\n- - kind\n- properties:\n- kind:\n- type: string\n- enum: [anonymous]\n-\n RunEvent:\n description: >\n Internal RunEvent-compatible JSON payload. The server validates this\n@@ -10250,6 +10239,7 @@ components:\n - run_id\n - settings\n - graph\n+ - provenance\n properties:\n run_id:\n type: string\n@@ -10273,9 +10263,7 @@ components:\n additionalProperties:\n type: string\n provenance:\n- oneOf:\n- - $ref: \"#/components/schemas/RunProvenance\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/RunProvenance\"\n manifest_blob:\n type: [\"string\", \"null\"]\n definition_blob:\n@@ -10587,9 +10575,7 @@ components:\n - $ref: \"#/components/schemas/RepositoryRef\"\n - type: \"null\"\n created_by:\n- oneOf:\n- - $ref: \"#/components/schemas/Principal\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/Principal\"\n origin:\n $ref: \"#/components/schemas/RunOrigin\"\n labels:\ndiff --git a/docs/public/changelog/2026-05-02.mdx b/docs/public/changelog/2026-05-02.mdx\nindex 48a501a59..2d55f8d05 100644\n--- a/docs/public/changelog/2026-05-02.mdx\n+++ b/docs/public/changelog/2026-05-02.mdx\n@@ -11,7 +11,7 @@ The dock listens to interview events and refreshes as questions arrive, so a par\n \n ## Principal attribution and auth routing\n \n-Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, agents, and anonymous actors. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from.\n+Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, and agents. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from.\n \n This also closes attribution gaps across web, CLI, worker-token, Slack, and human-interview paths. Runs created or advanced through different surfaces now preserve who or what took the action more consistently.\n \n@@ -19,7 +19,7 @@ This also closes attribution gaps across web, CLI, worker-token, Slack, and huma\n \n <Accordion title=\"API\">\n - Run specs now include client and server provenance shapes\n-- Run events use unified principal shapes for user, worker, system, Slack, webhook, agent, and anonymous subjects\n+- Run events use unified principal shapes for user, worker, system, Slack, webhook, and agent subjects\n </Accordion>\n \n <Accordion title=\"CLI\">\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..a63ea3db1 100644\n--- a/lib/crates/fabro-api/tests/run_event_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs\n@@ -1,7 +1,7 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunEvent as ApiRunEvent;\n-use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures};\n+use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures, test_support};\n use serde_json::{Value, json};\n \n #[test]\n@@ -19,6 +19,7 @@ fn run_event_round_trips_run_created() {\n \"properties\": {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"run_dir\": \"/tmp/fabro/run-1\",\n \"source_directory\": \"/tmp/fabro/run-1\"\n }\n@@ -37,6 +38,7 @@ fn run_event_round_trips_run_created_with_web_url() {\n \"properties\": {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"run_dir\": \"/tmp/fabro/run-1\",\n \"source_directory\": \"/tmp/fabro/run-1\",\n \"web_url\": format!(\"http://localhost:3000/runs/{}\", fixtures::RUN_1)\ndiff --git a/lib/crates/fabro-api/tests/run_projection_round_trip.rs b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\nindex 9a2a0f310..1d641e193 100644\n--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n@@ -1,7 +1,7 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunProjection as ApiRunProjection;\n-use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings};\n+use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, test_support};\n use serde_json::json;\n \n #[test]\n@@ -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: 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..3e020d0e5 100644\n--- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs\n@@ -12,7 +12,7 @@ use fabro_types::{\n AskFabro, AskFabroUnavailableReason, AutomationRef, DiffSummary, PullRequestLink,\n RepositoryProvider, RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary,\n RunId, RunLifecycle, RunLinks, RunOrigin, RunRunnableSource, RunSize, RunTimestamps, RunTiming,\n- WorkflowRef, fixtures,\n+ WorkflowRef, fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -88,7 +88,7 @@ fn run_summary_json_matches_openapi_shape() {\n origin_url: None,\n provider: RepositoryProvider::Unknown,\n }),\n- created_by: None,\n+ created_by: 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,15 @@ fn run_summary_json_matches_openapi_shape() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n- \"created_by\": null,\n+ \"created_by\": {\n+ \"kind\": \"user\",\n+ \"identity\": {\n+ \"issuer\": \"fabro:test\",\n+ \"subject\": \"test-user\"\n+ },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ },\n \"origin\": {\n \"kind\": \"api\"\n },\n@@ -253,6 +261,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": test_support::test_principal(),\n \"models\": [],\n \"timestamps\": {\n \"created_at\": \"2026-04-20T12:00:00Z\",\n@@ -277,6 +286,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n assert_eq!(summary.title, \"ship it\");\n assert_eq!(summary.labels, HashMap::new());\n assert_eq!(summary.source_directory, None);\n+ assert_eq!(summary.created_by, test_support::test_principal());\n assert_eq!(\n summary.repository,\n Some(RepositoryRef {\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..faab25163 100644\n--- a/lib/crates/fabro-cli/src/commands/run/attach.rs\n+++ b/lib/crates/fabro-cli/src/commands/run/attach.rs\n@@ -822,6 +822,7 @@ mod tests {\n )]\n \n use fabro_interview::{Answer, AnswerValue};\n+ use fabro_types::test_support;\n use fabro_util::terminal::Styles;\n use httpmock::MockServer;\n \n@@ -841,7 +842,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\nindex 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..a3635a38a 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs\n@@ -21,7 +21,7 @@ use fabro_config::daemon::ServerDaemon;\n use fabro_config::{Storage, envfile};\n use fabro_store::EventEnvelope;\n use fabro_test::{TestContext, expect_reqwest_status};\n-use fabro_types::{RunId, StageId};\n+use fabro_types::{RunId, StageId, test_support};\n use httpmock::{Mock, MockServer};\n use serde_json::Value;\n use shlex::try_quote;\n@@ -177,6 +177,7 @@ pub(crate) fn remote_run_summary_json(\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": test_support::test_principal(),\n \"origin\": {\n \"kind\": \"api\"\n },\ndiff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs\nindex 80b65e4aa..2f3557044 100644\n--- a/lib/crates/fabro-cli/tests/it/support/mod.rs\n+++ b/lib/crates/fabro-cli/tests/it/support/mod.rs\n@@ -1,3 +1,4 @@\n+use fabro_types::test_support;\n mod auth_harness;\n mod auth_tokens;\n \n@@ -49,7 +50,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: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-dump/src/lib.rs b/lib/crates/fabro-dump/src/lib.rs\nindex 397d55a3f..b4da2215c 100644\n--- a/lib/crates/fabro-dump/src/lib.rs\n+++ b/lib/crates/fabro-dump/src/lib.rs\n@@ -476,6 +476,7 @@ mod tests {\n Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunSandboxInstance,\n RunSandboxPlan, RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage,\n StageOutcome, StartRecord, SuccessReason, WorkflowSettings, first_event_seq, fixtures,\n+ test_support,\n };\n use futures::executor;\n \n@@ -498,7 +499,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/crates/fabro-server/src/auth/cli_flow.rs\nindex 987c2631b..3a9d6e055 100644\n--- a/lib/crates/fabro-server/src/auth/cli_flow.rs\n+++ b/lib/crates/fabro-server/src/auth/cli_flow.rs\n@@ -1671,11 +1671,11 @@ client_id = \"github-client-id\"\n let [first, second, third] = <[RequestAuthContext; 3]>::try_from(contexts)\n .expect(\"expected three captured auth contexts\");\n assert_eq!(first.auth_status, AuthStatus::Authenticated);\n- assert_eq!(first.principal.display(), \"octocat\");\n+ assert_eq!(first.principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(second.auth_status, AuthStatus::Authenticated);\n- assert_eq!(second.principal.display(), \"octocat\");\n+ assert_eq!(second.principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(third.auth_status, AuthStatus::Authenticated);\n- assert_eq!(third.principal.display(), \"octocat\");\n+ assert_eq!(third.principal.as_ref().unwrap().display(), \"octocat\");\n }\n \n #[tokio::test]\n@@ -2080,7 +2080,7 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert_eq!(contexts[0].principal.display(), \"octocat\");\n+ assert_eq!(contexts[0].principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(contexts[1].auth_status, AuthStatus::Invalid);\n assert_eq!(\n contexts[1].auth_error_code,\n@@ -2273,8 +2273,8 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert_eq!(contexts[0].principal.display(), \"octocat\");\n- let Principal::User(user) = &contexts[0].principal else {\n+ assert_eq!(contexts[0].principal.as_ref().unwrap().display(), \"octocat\");\n+ let Some(Principal::User(user)) = &contexts[0].principal else {\n panic!(\"expected user principal\");\n };\n assert_eq!(\ndiff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs\nindex d189b2a21..bdeaafd5a 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<Utc> {\n \n mod runs {\n use std::collections::HashMap;\n- use std::sync::OnceLock;\n+ use std::sync::{LazyLock, OnceLock};\n use std::time::Duration;\n \n use fabro_api::types::*;\n@@ -1092,13 +1092,22 @@ mod runs {\n };\n use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace};\n use fabro_types::{\n- PendingReason, RepositoryRef, RunBillingSummary, RunId, RunLifecycle, RunLinks, RunOrigin,\n- RunSize, RunTimestamps, StageId, WorkflowRef, WorkflowSettings,\n+ AuthMethod, IdpIdentity, PendingReason, Principal, RepositoryRef, RunBillingSummary, RunId,\n+ RunLifecycle, RunLinks, RunOrigin, RunSize, RunTimestamps, StageId, WorkflowRef,\n+ WorkflowSettings,\n };\n \n use super::ts;\n use crate::server::run_stage_from_stage_id;\n \n+ static DEMO_PRINCIPAL: LazyLock<Principal> = LazyLock::new(|| {\n+ Principal::user(\n+ IdpIdentity::new(\"fabro:demo\", \"demo\").expect(\"demo identity should be valid\"),\n+ \"demo\".to_string(),\n+ AuthMethod::DevToken,\n+ )\n+ });\n+\n fn labels(entries: &[(&str, &str)]) -> HashMap<String, String> {\n entries\n .iter()\n@@ -1171,7 +1180,7 @@ mod runs {\n repo_origin_url,\n source_directory.as_deref(),\n )),\n- created_by: None,\n+ created_by: DEMO_PRINCIPAL.clone(),\n origin: RunOrigin::default(),\n labels: labels(entries),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-server/src/principal_middleware.rs b/lib/crates/fabro-server/src/principal_middleware.rs\nindex f9e8c385c..de4307ec7 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<Principal>,\n pub auth_status: AuthStatus,\n pub auth_error_code: Option<AuthErrorCode>,\n pub user_profile: Option<UserProfile>,\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<UserProfile>) -> 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<AuthErrorCode>) -> 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<Principal>,\n pub auth_status: AuthStatus,\n pub auth_error_code: Option<AuthErrorCode>,\n }\n@@ -172,22 +172,22 @@ impl AuthContextSlot {\n pub(crate) fn log_snapshot(&self) -> RequestAuthLogContext {\n let context = self.0.lock().expect(\"auth context lock poisoned\");\n RequestAuthLogContext {\n- principal: principal_without_log_unused_fields(&context.principal),\n+ principal: principal_without_log_unused_fields(context.principal.as_ref()),\n auth_status: context.auth_status,\n auth_error_code: context.auth_error_code,\n }\n }\n }\n \n-fn principal_without_log_unused_fields(principal: &Principal) -> Principal {\n+fn principal_without_log_unused_fields(principal: Option<&Principal>) -> Option<Principal> {\n match principal {\n- Principal::User(user) => Principal::User(UserPrincipal {\n+ Some(Principal::User(user)) => Some(Principal::User(UserPrincipal {\n identity: user.identity.clone(),\n login: user.login.clone(),\n auth_method: user.auth_method,\n avatar_url: None,\n- }),\n- principal => principal.clone(),\n+ })),\n+ principal => principal.cloned(),\n }\n }\n \n@@ -402,7 +402,7 @@ fn auth_slot_from_parts(parts: &Parts) -> AuthContextSlot {\n pub(crate) fn require_user(slot: &AuthContextSlot) -> Result<UserPrincipal, ApiError> {\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 +412,7 @@ pub(crate) fn require_authenticated_user(\n ) -> Result<AuthenticatedUser, ApiError> {\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 +428,11 @@ pub(crate) fn require_authenticated_user(\n pub(crate) fn require_run_management_actor(slot: &AuthContextSlot) -> Result<Principal, ApiError> {\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 +443,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 +453,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 +465,13 @@ fn require_run_management_target(\n ) -> Result<Principal, ApiError> {\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 +687,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 +727,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 +746,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 +825,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..26fd4444a 100644\n--- a/lib/crates/fabro-server/src/run_files.rs\n+++ b/lib/crates/fabro-server/src/run_files.rs\n@@ -1717,7 +1717,7 @@ fn count_flags(data: &[FileDiff]) -> (u64, u64, u64, u64) {\n mod tests {\n use std::sync::atomic::{AtomicUsize, Ordering};\n \n- use fabro_types::{CommandTermination, RunId};\n+ use fabro_types::{CommandTermination, RunId, test_support};\n use tokio::time::{Duration, sleep};\n \n use super::*;\n@@ -2389,7 +2389,7 @@ index 1111111..2222222 160000\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs\nindex 4feb22133..a0e9b4c39 100644\n--- a/lib/crates/fabro-server/src/run_manifest.rs\n+++ b/lib/crates/fabro-server/src/run_manifest.rs\n@@ -27,7 +27,9 @@ use fabro_static::EnvVars;\n use fabro_types::settings::cli::OutputVerbosity;\n use fabro_types::settings::interp::InterpString;\n use fabro_types::settings::run::{EnvironmentProvider, RunGoal, RunNamespace};\n-use fabro_types::{ManifestPath, RunId, SandboxProviderKind, ServerSettings, WorkflowSettings};\n+use fabro_types::{\n+ ManifestPath, RunId, RunProvenance, SandboxProviderKind, ServerSettings, WorkflowSettings,\n+};\n use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};\n use fabro_validate::Severity;\n use fabro_workflow::Error as WorkflowError;\n@@ -193,6 +195,7 @@ pub(crate) fn validate_prepared_manifest(\n pub(crate) fn create_run_input(\n prepared: PreparedManifest,\n configured_providers: Vec<ProviderId>,\n+ provenance: RunProvenance,\n web_url: Option<String>,\n ) -> CreateRunInput {\n CreateRunInput {\n@@ -209,7 +212,7 @@ pub(crate) fn create_run_input(\n git: prepared.git,\n fork_source_ref: None,\n parent_id: prepared.parent_id,\n- provenance: None,\n+ provenance,\n configured_providers,\n web_url,\n }\ndiff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs\nindex 7bff4968f..4710c40ca 100644\n--- a/lib/crates/fabro-server/src/server.rs\n+++ b/lib/crates/fabro-server/src/server.rs\n@@ -1876,7 +1876,10 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n let status = response.status().as_u16();\n let latency_ms = start.elapsed().as_millis();\n let auth_context = auth_slot.log_snapshot();\n- let principal_kind = auth_context.principal.kind();\n+ let principal_kind = auth_context\n+ .principal\n+ .as_ref()\n+ .map_or(\"none\", Principal::kind);\n let auth_status = auth_context.auth_status.as_str();\n \n macro_rules! emit_http_log {\n@@ -1914,27 +1917,27 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n macro_rules! emit_principal_http_log {\n ($level:ident) => {{\n match &auth_context.principal {\n- Principal::User(user) => emit_http_log!(\n+ Some(Principal::User(user)) => emit_http_log!(\n $level,\n user_auth_method = user.auth_method.as_str(),\n idp_issuer = user.identity.issuer(),\n idp_subject = user.identity.subject(),\n login = user.login.as_str(),\n ),\n- Principal::Worker { run_id } => {\n+ Some(Principal::Worker { run_id }) => {\n emit_http_log!($level, run_id = run_id.to_string().as_str(),)\n }\n- Principal::Webhook { delivery_id } => {\n+ Some(Principal::Webhook { delivery_id }) => {\n emit_http_log!($level, delivery_id = delivery_id.as_str(),)\n }\n- Principal::Slack {\n+ Some(Principal::Slack {\n team_id, user_id, ..\n- } => emit_http_log!(\n+ }) => emit_http_log!(\n $level,\n team_id = team_id.as_str(),\n user_id = user_id.as_str(),\n ),\n- Principal::Agent { .. } | Principal::System { .. } | Principal::Anonymous => {\n+ None | Some(Principal::Agent { .. } | Principal::System { .. }) => {\n emit_http_log!($level)\n }\n }\ndiff --git a/lib/crates/fabro-server/src/server/handler/events.rs b/lib/crates/fabro-server/src/server/handler/events.rs\nindex 180c04bdd..fe8f719ef 100644\n--- a/lib/crates/fabro-server/src/server/handler/events.rs\n+++ b/lib/crates/fabro-server/src/server/handler/events.rs\n@@ -535,7 +535,7 @@ mod stage_events_tests {\n use axum::body::{Body, to_bytes};\n use axum::http::{Request, StatusCode, header};\n use fabro_store::EventPayload;\n- use fabro_types::{Graph, RunId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, WorkflowSettings, test_support};\n use fabro_workflow::event as workflow_event;\n use http_body_util::BodyExt;\n use serde_json::json;\n@@ -570,7 +570,7 @@ mod stage_events_tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\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..b5207daab 100644\n--- a/lib/crates/fabro-server/src/server/handler/pair.rs\n+++ b/lib/crates/fabro-server/src/server/handler/pair.rs\n@@ -850,7 +850,7 @@ mod tests {\n use fabro_types::run_event::AgentMessageProps;\n use fabro_types::{\n BilledTokenCounts, EventEnvelope, Graph, PairMessageId, RunEvent, StageId,\n- WorkflowSettings, fixtures,\n+ WorkflowSettings, fixtures, test_support,\n };\n use fabro_workflow::event as workflow_event;\n use tower::ServiceExt;\n@@ -1024,7 +1024,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs\nindex fca9fdc3c..2c5097d32 100644\n--- a/lib/crates/fabro-server/src/server/handler/runs.rs\n+++ b/lib/crates/fabro-server/src/server/handler/runs.rs\n@@ -687,13 +687,14 @@ pub(crate) async fn create_run_from_manifest(\n .as_ref()\n .map(LlmClientResult::provider_ids)\n .unwrap_or_default();\n+ let provenance = run_provenance(&headers, &actor);\n let mut create_input = run_manifest::create_run_input(\n prepared.clone(),\n ready_provider_ids.clone(),\n+ provenance,\n web_url.clone(),\n );\n create_input.run_id = Some(run_id);\n- create_input.provenance = Some(run_provenance(&headers, &actor));\n create_input.submitted_manifest_bytes = Some(submitted_manifest_bytes);\n create_input.automation = automation;\n \n@@ -864,7 +865,7 @@ pub(super) fn run_provenance(headers: &HeaderMap, subject: &Principal) -> RunPro\n version: FABRO_VERSION.to_string(),\n }),\n client: run_client_provenance(headers),\n- subject: Some(subject.clone()),\n+ subject: subject.clone(),\n }\n }\n \ndiff --git a/lib/crates/fabro-server/src/server/handler/sandbox.rs b/lib/crates/fabro-server/src/server/handler/sandbox.rs\nindex 61d8b7ee9..7cd962cb6 100644\n--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs\n@@ -1299,7 +1299,7 @@ FABRO_PROC_NET_TCP /proc/net/tcp6\n mod retrieve_sandbox_tests {\n use axum::body::{Body, to_bytes};\n use axum::http::{Request, StatusCode};\n- use fabro_types::{Graph, RunId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, WorkflowSettings, test_support};\n use serde_json::{Value, json};\n use tower::ServiceExt;\n \n@@ -1338,6 +1338,7 @@ mod retrieve_sandbox_tests {\n \"properties\": {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"run_dir\": \"/tmp/test\",\n },\n }),\ndiff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/crates/fabro-server/src/server/handler/sessions.rs\nindex bad48c6d4..3ac297f07 100644\n--- a/lib/crates/fabro-server/src/server/handler/sessions.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sessions.rs\n@@ -1506,6 +1506,7 @@ mod tests {\n use fabro_agent::config::ToolAccess;\n use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource};\n use fabro_llm::types::{ToolCall, ToolDefinition};\n+ use fabro_types::test_support;\n \n use super::*;\n \n@@ -1700,7 +1701,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: 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..aae20bfc4 100644\n--- a/lib/crates/fabro-server/src/server/tests.rs\n+++ b/lib/crates/fabro-server/src/server/tests.rs\n@@ -29,7 +29,7 @@ use fabro_types::{\n SandboxProviderKind, StageContextWindowBreakdownItem, StageContextWindowCategory,\n StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness,\n StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind,\n- WorkflowSettings, fixtures,\n+ WorkflowSettings, fixtures, test_support,\n };\n use fabro_util::check_report::CheckStatus;\n use fabro_workflow::records::CheckpointExt;\n@@ -4022,7 +4022,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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: 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: 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: 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<AppState>, run_id: RunId\n workflow_slug: Some(\"test\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -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: 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..d2b355b32 100644\n--- a/lib/crates/fabro-server/tests/it/api/run_files.rs\n+++ b/lib/crates/fabro-server/tests/it/api/run_files.rs\n@@ -14,7 +14,7 @@ use axum::body::Body;\n use axum::http::{Request, StatusCode};\n use fabro_server::test_support::test_app_state_with_store;\n use fabro_store::{ArtifactStore, Database};\n-use fabro_types::{Graph, RunId, SandboxProviderKind, WorkflowSettings};\n+use fabro_types::{Graph, RunId, SandboxProviderKind, WorkflowSettings, test_support};\n use fabro_workflow::event as workflow_event;\n use fabro_workflow::run_status::SuccessReason;\n use object_store::memory::InMemory as MemoryObjectStore;\n@@ -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: 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..69ad694aa 100644\n--- a/lib/crates/fabro-store/src/run_state.rs\n+++ b/lib/crates/fabro-store/src/run_state.rs\n@@ -922,11 +922,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run {\n })\n .map(|(_, record)| record.question.clone());\n let models = run_models(state);\n- let created_by = state\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.clone());\n+ let created_by = state.spec.provenance.subject.clone();\n let source_directory = state.spec.source_directory.clone();\n let repo_origin_url = state.spec.git.as_ref().map(|git| git.origin_url.clone());\n let start_time = state.start.as_ref().map(|start| start.start_time);\n@@ -1276,7 +1272,7 @@ mod tests {\n StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod,\n StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning,\n StageModelUsage, StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings,\n- first_event_seq, fixtures,\n+ first_event_seq, fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -1358,7 +1354,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1446,6 +1442,7 @@ mod tests {\n &json!({\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\"\n }),\n@@ -1474,6 +1471,7 @@ mod tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"automation\": automation,\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\"\n }),\n@@ -1497,6 +1495,7 @@ mod tests {\n &json!({\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\"\n }),\n@@ -1519,6 +1518,7 @@ mod tests {\n &json!({\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\"\n }),\n@@ -1596,6 +1596,7 @@ mod tests {\n &json!({\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\"\n }),\n@@ -1822,7 +1823,7 @@ mod tests {\n \"repo_origin_url\": null,\n \"base_branch\": null,\n \"labels\": {},\n- \"provenance\": null,\n+ \"provenance\": test_support::test_run_provenance(),\n \"manifest_blob\": null,\n \"definition_blob\": null,\n \"git\": null,\n@@ -2851,7 +2852,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2877,7 +2878,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2909,6 +2910,7 @@ mod tests {\n &json!({\n \"title\": \"Explicit title\",\n \"settings\": WorkflowSettings::default(),\n+ \"provenance\": test_support::test_run_provenance(),\n \"graph\": {\n \"name\": \"test\",\n \"nodes\": {},\n@@ -2936,6 +2938,7 @@ mod tests {\n \"run.created\",\n &json!({\n \"settings\": WorkflowSettings::default(),\n+ \"provenance\": test_support::test_run_provenance(),\n \"graph\": {\n \"name\": \"test\",\n \"nodes\": {},\n@@ -2965,6 +2968,7 @@ mod tests {\n &json!({\n \"title\": \"Original title\",\n \"settings\": WorkflowSettings::default(),\n+ \"provenance\": test_support::test_run_provenance(),\n \"graph\": {\n \"name\": \"test\",\n \"nodes\": {},\n@@ -3007,6 +3011,7 @@ mod tests {\n \"event\": \"run.created\",\n \"properties\": {\n \"settings\": WorkflowSettings::default(),\n+ \"provenance\": test_support::test_run_provenance(),\n \"graph\": {\n \"name\": \"test\",\n \"nodes\": {},\ndiff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs\nindex c6406fd8d..87c0ec733 100644\n--- a/lib/crates/fabro-store/src/slate/mod.rs\n+++ b/lib/crates/fabro-store/src/slate/mod.rs\n@@ -472,7 +472,7 @@ mod tests {\n use chrono::{DateTime, Utc};\n use fabro_types::{\n AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId,\n- SuccessReason, WorkflowSettings,\n+ SuccessReason, WorkflowSettings, test_support,\n };\n use futures::TryStreamExt;\n use object_store::memory::InMemory;\n@@ -542,7 +542,7 @@ mod tests {\n automation: None,\n source_directory: Some(format!(\"/tmp/{label}\")),\n labels: std::collections::HashMap::from([(\"team\".to_string(), \"infra\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\n@@ -601,6 +601,7 @@ mod tests {\n \"run_dir\": format!(\"/tmp/{label}\"),\n \"git\": run_spec.git,\n \"labels\": run_spec.labels,\n+ \"provenance\": run_spec.provenance,\n }),\n ))\n .await\n@@ -626,6 +627,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 +1302,7 @@ mod tests {\n \"run_dir\": \"/tmp/run-2\",\n \"git\": run_spec[\"git\"],\n \"labels\": run_spec[\"labels\"],\n+ \"provenance\": run_spec[\"provenance\"],\n },\n }))\n .unwrap(),\ndiff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs\nindex 1718cb662..f94a97646 100644\n--- a/lib/crates/fabro-store/src/slate/run_store.rs\n+++ b/lib/crates/fabro-store/src/slate/run_store.rs\n@@ -667,7 +667,7 @@ mod tests {\n use std::sync::Arc;\n use std::time::Duration;\n \n- use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings, test_support};\n use object_store::memory::InMemory;\n use serde_json::json;\n \n@@ -722,6 +722,7 @@ mod tests {\n \"properties\": {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"run_dir\": \"/tmp/test\",\n },\n }),\ndiff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs\nindex 6584a36cd..ffc7fbb03 100644\n--- a/lib/crates/fabro-store/tests/serializable_projection.rs\n+++ b/lib/crates/fabro-store/tests/serializable_projection.rs\n@@ -8,7 +8,7 @@ use fabro_types::{\n BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord,\n QuestionType, RunDiff, RunSandbox, RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime,\n RunStatus, SandboxProviderKind, StageCompletion, StageModelUsage, StageOutcome, StartRecord,\n- WorkflowSettings, first_event_seq, fixtures,\n+ WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -22,7 +22,7 @@ fn sample_run_spec() -> RunSpec {\n automation: None,\n source_directory: Some(\"/tmp/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\ndiff --git a/lib/crates/fabro-tool/Cargo.toml b/lib/crates/fabro-tool/Cargo.toml\nindex 08b6df7cf..c50ea6a38 100644\n--- a/lib/crates/fabro-tool/Cargo.toml\n+++ b/lib/crates/fabro-tool/Cargo.toml\n@@ -29,4 +29,5 @@ tokio.workspace = true\n toml.workspace = true\n \n [dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\n tempfile = \"3\"\ndiff --git a/lib/crates/fabro-tool/src/common.rs b/lib/crates/fabro-tool/src/common.rs\nindex 9dd64599a..159ec426d 100644\n--- a/lib/crates/fabro-tool/src/common.rs\n+++ b/lib/crates/fabro-tool/src/common.rs\n@@ -307,7 +307,9 @@ fn format_tool_error(err: &anyhow::Error) -> String {\n #[cfg(test)]\n mod tests {\n use chrono::{TimeZone, Utc};\n- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n+ use fabro_types::{\n+ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support,\n+ };\n \n use super::*;\n \n@@ -413,7 +415,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs\nindex 8488f8f0b..57e41acd5 100644\n--- a/lib/crates/fabro-tool/src/create.rs\n+++ b/lib/crates/fabro-tool/src/create.rs\n@@ -508,7 +508,7 @@ mod tests {\n use fabro_api::types;\n use fabro_types::{\n EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus,\n- RunTimestamps, WorkflowRef,\n+ RunTimestamps, WorkflowRef, test_support,\n };\n use schemars::SchemaGenerator;\n use serde_json::json;\n@@ -902,7 +902,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/interact.rs b/lib/crates/fabro-tool/src/interact.rs\nindex 34023120d..503147ece 100644\n--- a/lib/crates/fabro-tool/src/interact.rs\n+++ b/lib/crates/fabro-tool/src/interact.rs\n@@ -453,7 +453,7 @@ mod tests {\n use chrono::{TimeZone, Utc};\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection,\n- RunStatus, RunTimestamps, WorkflowRef,\n+ RunStatus, RunTimestamps, WorkflowRef, test_support,\n };\n use serde_json::json;\n \n@@ -690,7 +690,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs\nindex 0df7e391a..e3d0162c6 100644\n--- a/lib/crates/fabro-tool/src/search.rs\n+++ b/lib/crates/fabro-tool/src/search.rs\n@@ -293,7 +293,9 @@ mod tests {\n use std::collections::HashMap;\n \n use chrono::{TimeZone, Utc};\n- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n+ use fabro_types::{\n+ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support,\n+ };\n \n use super::*;\n \n@@ -444,7 +446,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"group\".to_string(), group.to_string())]),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs\nindex 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<String>,\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<RunServerProvenance>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub client: Option<RunClientProvenance>,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub subject: Option<Principal>,\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<String>,\n #[serde(default, skip_serializing_if = \"HashMap::is_empty\")]\n pub labels: HashMap<String, String>,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub provenance: Option<RunProvenance>,\n+ pub provenance: RunProvenance,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub manifest_blob: Option<RunBlobId>,\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..7d5d4a982 100644\n--- a/lib/crates/fabro-types/src/run_event/mod.rs\n+++ b/lib/crates/fabro-types/src/run_event/mod.rs\n@@ -933,7 +933,7 @@ mod tests {\n use super::*;\n use crate::{\n AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings,\n- fixtures,\n+ fixtures, test_support,\n };\n \n fn user_principal(login: &str) -> Principal {\n@@ -1015,6 +1015,7 @@ mod tests {\n \"properties\": {\n \"settings\": settings,\n \"graph\": graph,\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\"\n@@ -1035,6 +1036,7 @@ mod tests {\n \"properties\": {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\",\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<AutomationRef>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub db_prefix: Option<String>,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- pub provenance: Option<RunProvenance>,\n+ pub provenance: RunProvenance,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub manifest_blob: Option<RunBlobId>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\ndiff --git a/lib/crates/fabro-types/src/run_projection.rs b/lib/crates/fabro-types/src/run_projection.rs\nindex 4b52dcc96..40130ed9c 100644\n--- a/lib/crates/fabro-types/src/run_projection.rs\n+++ b/lib/crates/fabro-types/src/run_projection.rs\n@@ -681,7 +681,7 @@ mod title_tests {\n \n use chrono::Utc;\n \n- use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings};\n+ use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support};\n \n fn projection_with_goal(goal: Option<&str>) -> RunProjection {\n let mut graph = Graph::new(\"test\");\n@@ -700,7 +700,7 @@ mod title_tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -752,7 +752,7 @@ mod iter_stages_tests {\n use serde_json::json;\n \n use super::RunProjection;\n- use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings};\n+ use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings, test_support};\n \n fn seq(n: u32) -> NonZeroU32 {\n NonZeroU32::new(n).unwrap()\n@@ -770,7 +770,7 @@ mod iter_stages_tests {\n automation: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs\nindex fd35dc2a4..3c4321052 100644\n--- a/lib/crates/fabro-types/src/run_summary.rs\n+++ b/lib/crates/fabro-types/src/run_summary.rs\n@@ -52,8 +52,7 @@ pub struct Run {\n pub automation: Option<AutomationRef>,\n #[serde(default)]\n pub repository: Option<RepositoryRef>,\n- #[serde(default)]\n- pub created_by: Option<Principal>,\n+ pub created_by: Principal,\n pub origin: RunOrigin,\n pub labels: HashMap<String, String>,\n pub lifecycle: RunLifecycle,\ndiff --git a/lib/crates/fabro-types/src/test_support.rs b/lib/crates/fabro-types/src/test_support.rs\nnew file mode 100644\nindex 000000000..994813974\n--- /dev/null\n+++ b/lib/crates/fabro-types/src/test_support.rs\n@@ -0,0 +1,19 @@\n+use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance};\n+\n+#[must_use]\n+pub fn test_principal() -> Principal {\n+ Principal::user(\n+ IdpIdentity::new(\"fabro:test\", \"test-user\").expect(\"test identity should be valid\"),\n+ \"test\".to_string(),\n+ AuthMethod::DevToken,\n+ )\n+}\n+\n+#[must_use]\n+pub fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: test_principal(),\n+ }\n+}\ndiff --git a/lib/crates/fabro-types/tests/run_event_serde.rs b/lib/crates/fabro-types/tests/run_event_serde.rs\nindex 49af80c50..7bacbc35b 100644\n--- a/lib/crates/fabro-types/tests/run_event_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_event_serde.rs\n@@ -6,7 +6,7 @@ use fabro_types::run_event::run::{RunCreatedProps, RunParentLinkedProps, RunPare\n use fabro_types::run_event::{RunSessionTurnFailedCode, RunSessionTurnFailedProps};\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n-use fabro_types::{AutomationRef, EventBody, TurnId, WorkflowSettings, fixtures};\n+use fabro_types::{AutomationRef, EventBody, TurnId, WorkflowSettings, fixtures, test_support};\n \n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n@@ -32,7 +32,7 @@ fn run_created_props_round_trip_templated_settings() {\n trigger_id: Some(\"schedule_1\".to_string()),\n }),\n db_prefix: Some(\"run_\".to_string()),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: Some(GitContext {\n origin_url: \"https://github.com/fabro-sh/fabro.git\".to_string(),\n@@ -97,7 +97,7 @@ fn run_created_props_omits_web_url_when_absent() {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -128,17 +128,17 @@ fn run_created_props_omits_web_url_when_absent() {\n }\n \n #[test]\n-fn run_created_props_defaults_additive_fields_for_legacy_events() {\n+fn run_created_props_defaults_optional_additive_fields_when_absent() {\n let json = serde_json::json!({\n \"title\": null,\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"ship\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\"\n });\n \n- let props: RunCreatedProps =\n- serde_json::from_value(json).expect(\"legacy props should deserialize\");\n+ let props: RunCreatedProps = serde_json::from_value(json).expect(\"props should deserialize\");\n assert_eq!(props.retried_from, None);\n assert_eq!(props.automation, None);\n }\ndiff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs\nindex b05dca05e..6eb9e105c 100644\n--- a/lib/crates/fabro-types/tests/run_spec_methods.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_methods.rs\n@@ -3,7 +3,7 @@ use std::collections::HashMap;\n use fabro_types::graph::Graph;\n use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec};\n use fabro_types::settings::{ProjectNamespace, WorkflowNamespace};\n-use fabro_types::{WorkflowSettings, fixtures};\n+use fabro_types::{WorkflowSettings, fixtures, test_support};\n \n fn sample_run_spec() -> RunSpec {\n let settings = WorkflowSettings {\n@@ -27,7 +27,7 @@ fn sample_run_spec() -> RunSpec {\n automation: None,\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\ndiff --git a/lib/crates/fabro-types/tests/run_spec_serde.rs b/lib/crates/fabro-types/tests/run_spec_serde.rs\nindex 89b09ccbd..8c4c02a76 100644\n--- a/lib/crates/fabro-types/tests/run_spec_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_serde.rs\n@@ -4,7 +4,7 @@ use fabro_types::graph::Graph;\n use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunSpec};\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n-use fabro_types::{AutomationRef, WorkflowSettings, fixtures};\n+use fabro_types::{AutomationRef, WorkflowSettings, fixtures, test_support};\n \n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n@@ -27,7 +27,7 @@ fn run_spec_round_trips_templated_settings() {\n }),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\n@@ -75,15 +75,16 @@ fn run_spec_round_trips_templated_settings() {\n }\n \n #[test]\n-fn run_spec_defaults_automation_for_legacy_specs() {\n+fn run_spec_defaults_optional_automation_when_absent() {\n let json = serde_json::json!({\n \"run_id\": fixtures::RUN_1,\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"ship\"),\n+ \"provenance\": test_support::test_run_provenance(),\n \"labels\": {}\n });\n \n- let record: RunSpec = serde_json::from_value(json).expect(\"legacy spec should deserialize\");\n+ let record: RunSpec = serde_json::from_value(json).expect(\"spec should deserialize\");\n \n assert_eq!(record.automation, None);\n }\ndiff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs\nindex d9e2867ab..9762b27f4 100644\n--- a/lib/crates/fabro-workflow/src/billing_rollup.rs\n+++ b/lib/crates/fabro-workflow/src/billing_rollup.rs\n@@ -165,7 +165,7 @@ mod tests {\n use fabro_model::{Catalog, ModelRef, ProviderId};\n use fabro_types::{\n AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion,\n- StageOutcome, WorkflowSettings, first_event_seq, fixtures,\n+ StageOutcome, WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n \n use super::billing_rollup_from_projection;\n@@ -353,7 +353,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\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<AutomationRef>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n db_prefix: Option<String>,\n- #[serde(default, skip_serializing_if = \"Option::is_none\")]\n- provenance: Option<RunProvenance>,\n+ provenance: RunProvenance,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n manifest_blob: Option<RunBlobId>,\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..1017b2b29 100644\n--- a/lib/crates/fabro-workflow/src/event/sink.rs\n+++ b/lib/crates/fabro-workflow/src/event/sink.rs\n@@ -213,6 +213,7 @@ mod tests {\n use std::sync::Arc;\n \n use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures};\n+ use fabro_types::test_support;\n use tokio::sync::Mutex as AsyncMutex;\n \n use super::*;\n@@ -244,7 +245,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..8fa23a208 100644\n--- a/lib/crates/fabro-workflow/src/git.rs\n+++ b/lib/crates/fabro-workflow/src/git.rs\n@@ -343,7 +343,7 @@ mod tests {\n \n use fabro_dump::RunDump;\n use fabro_store::Database;\n- use fabro_types::{CommandTermination, StageModelUsage, fixtures};\n+ use fabro_types::{CommandTermination, StageModelUsage, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -469,7 +469,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs\nindex 3a120e8fc..16dbe0230 100644\n--- a/lib/crates/fabro-workflow/src/handler/agent.rs\n+++ b/lib/crates/fabro-workflow/src/handler/agent.rs\n@@ -429,7 +429,7 @@ mod tests {\n use fabro_graphviz::graph::AttrValue;\n use fabro_model::{ReasoningEffort, Speed};\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n use tempfile::TempDir;\n \n@@ -484,7 +484,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/command.rs b/lib/crates/fabro-workflow/src/handler/command.rs\nindex f10ebafde..66838ae63 100644\n--- a/lib/crates/fabro-workflow/src/handler/command.rs\n+++ b/lib/crates/fabro-workflow/src/handler/command.rs\n@@ -228,7 +228,7 @@ mod tests {\n use bytes::Bytes;\n use fabro_graphviz::graph::AttrValue;\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures};\n+ use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n use tokio::sync::Mutex;\n \n@@ -256,7 +256,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -357,7 +357,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs\nindex 91753f4c9..499cdd784 100644\n--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs\n+++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs\n@@ -1600,6 +1600,7 @@ mod tests {\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin,\n RunPairStatusResponse, RunProjection, RunStatus, RunTimestamps, SuccessReason, WorkflowRef,\n+ test_support,\n };\n use fabro_vault::{SecretType, Vault};\n use futures::stream;\n@@ -2133,7 +2134,7 @@ reasoning = false\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs\nindex b52478bac..108a72369 100644\n--- a/lib/crates/fabro-workflow/src/handler/parallel.rs\n+++ b/lib/crates/fabro-workflow/src/handler/parallel.rs\n@@ -692,7 +692,7 @@ mod tests {\n \n use fabro_graphviz::graph::{AttrValue, Edge};\n use fabro_store::{Database, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -728,7 +728,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs\nindex 1c2a82267..48cbeeb32 100644\n--- a/lib/crates/fabro-workflow/src/handler/prompt.rs\n+++ b/lib/crates/fabro-workflow/src/handler/prompt.rs\n@@ -225,7 +225,7 @@ mod tests {\n use fabro_graphviz::graph::AttrValue;\n use fabro_model::{ReasoningEffort, Speed};\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n use tempfile::TempDir;\n \n@@ -283,7 +283,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs\nindex ccd24632d..5133fa602 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs\n@@ -598,7 +598,7 @@ mod tests {\n use fabro_model::Catalog;\n use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection};\n use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};\n- use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -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: 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..cc25ab1a8 100644\n--- a/lib/crates/fabro-workflow/src/operations/archive.rs\n+++ b/lib/crates/fabro-workflow/src/operations/archive.rs\n@@ -136,7 +136,9 @@ mod tests {\n use std::time::Duration;\n \n use fabro_store::Database;\n- use fabro_types::{FailureReason, RunId, SuccessReason, TerminalStatus, fixtures};\n+ use fabro_types::{\n+ FailureReason, RunId, SuccessReason, TerminalStatus, fixtures, test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -226,7 +228,7 @@ mod tests {\n workflow_slug: None,\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs\nindex 8bf16d115..ccd52f9ab 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<GitContext>,\n pub fork_source_ref: Option<ForkSourceRef>,\n pub parent_id: Option<RunId>,\n- pub provenance: Option<RunProvenance>,\n+ pub provenance: RunProvenance,\n pub configured_providers: Vec<ProviderId>,\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<AutomationRef>,\n git: Option<GitContext>,\n fork_source_ref: Option<ForkSourceRef>,\n- provenance: Option<RunProvenance>,\n+ provenance: RunProvenance,\n configured_providers: Vec<ProviderId>,\n catalog: Arc<Catalog>,\n }\n@@ -422,7 +422,7 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunMode;\n- use fabro_types::{WorkflowSettings, fixtures};\n+ use fabro_types::{WorkflowSettings, fixtures, test_support};\n use fabro_util::error::collect_chain;\n use fabro_validate::Severity;\n use object_store::local::LocalFileSystem;\n@@ -1115,7 +1115,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: 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: 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: 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: 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: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1467,7 +1467,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: Some(fabro_types::RunProvenance {\n+ provenance: fabro_types::RunProvenance {\n server: Some(fabro_types::RunServerProvenance {\n version: \"0.9.0\".to_string(),\n }),\n@@ -1476,12 +1476,12 @@ mod tests {\n name: Some(\"fabro-cli\".to_string()),\n version: Some(\"0.9.0\".to_string()),\n }),\n- subject: Some(fabro_types::Principal::user(\n+ subject: fabro_types::Principal::user(\n fabro_types::IdpIdentity::new(\"https://github.com\", \"12345\").unwrap(),\n \"octocat\".to_string(),\n fabro_types::AuthMethod::Github,\n- )),\n- }),\n+ ),\n+ },\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1494,7 +1494,7 @@ mod tests {\n let run_store = store.open_run_reader(&created.run_id).await.unwrap();\n let state = run_store.state().await.unwrap();\n let run = state.spec;\n- let provenance = run.provenance.expect(\"provenance should be projected\");\n+ let provenance = run.provenance;\n \n assert_eq!(provenance.server.unwrap().version, \"0.9.0\");\n assert_eq!(\n@@ -1502,7 +1502,7 @@ mod tests {\n Some(\"fabro-cli\")\n );\n assert_eq!(\n- provenance.subject.unwrap(),\n+ provenance.subject,\n fabro_types::Principal::user(\n fabro_types::IdpIdentity::new(\"https://github.com\", \"12345\").unwrap(),\n \"octocat\".to_string(),\ndiff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs\nindex 2bf7e144f..d2353bdfc 100644\n--- a/lib/crates/fabro-workflow/src/operations/fork.rs\n+++ b/lib/crates/fabro-workflow/src/operations/fork.rs\n@@ -284,7 +284,7 @@ mod tests {\n \n use fabro_graphviz::graph::Graph;\n use fabro_store::{Database, RunProjectionReducer};\n- use fabro_types::{StageId, WorkflowSettings, fixtures};\n+ use fabro_types::{StageId, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -383,7 +383,7 @@ mod tests {\n workflow_slug: Some(\"fork-source\".to_string()),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: 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..e5815efa5 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<RunProvenance>,\n+ pub provenance: RunProvenance,\n pub web_url: Option<String>,\n }\n \n@@ -122,7 +122,7 @@ mod tests {\n use fabro_types::{\n AuthMethod, DirtyStatus, FailureReason, ForkSourceRef, GitContext, Graph, IdpIdentity,\n PreRunPushOutcome, Principal, PullRequestLink, RunBlobId, RunRunnableSource,\n- RunServerProvenance, RunTiming, UserPrincipal, WorkflowSettings, fixtures,\n+ RunServerProvenance, RunTiming, UserPrincipal, WorkflowSettings, fixtures, test_support,\n };\n use object_store::memory::InMemory;\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: test_support::test_run_provenance(),\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: test_support::test_run_provenance(),\n web_url: None,\n })\n .await\ndiff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs\nindex 8083350be..f51781b26 100644\n--- a/lib/crates/fabro-workflow/src/operations/start.rs\n+++ b/lib/crates/fabro-workflow/src/operations/start.rs\n@@ -1129,7 +1129,9 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::settings::run::RunMode;\n use fabro_types::settings::{InterpString, ModelRef};\n- use fabro_types::{BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures};\n+ use fabro_types::{\n+ BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures, test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -1438,7 +1440,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1860,7 +1862,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\ndiff --git a/lib/crates/fabro-workflow/src/operations/timeline.rs b/lib/crates/fabro-workflow/src/operations/timeline.rs\nindex 68e0ee959..b96d10aa4 100644\n--- a/lib/crates/fabro-workflow/src/operations/timeline.rs\n+++ b/lib/crates/fabro-workflow/src/operations/timeline.rs\n@@ -204,6 +204,7 @@ mod tests {\n use chrono::Utc;\n use fabro_types::{\n Checkpoint, CheckpointRecord, Graph, RunDiff, RunSpec, WorkflowSettings, fixtures,\n+ test_support,\n };\n \n use super::*;\n@@ -248,7 +249,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\nindex d05398556..a1ee4e9f9 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n@@ -18,7 +18,9 @@ use fabro_interview::AutoApproveInterviewer;\n use fabro_sandbox::SandboxSpec;\n use fabro_store::Database;\n use fabro_types::settings::run::RunModelControls;\n-use fabro_types::{Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref};\n+use fabro_types::{\n+ Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref, test_support,\n+};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -165,7 +167,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -208,7 +210,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: 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..6e90991dc 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n@@ -651,7 +651,7 @@ mod tests {\n use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};\n use fabro_types::{\n BilledTokenCounts, EventBody, RunBlobId, RunEvent, RunId, RunSpec, StageCompletion,\n- WorkflowSettings, first_event_seq, fixtures,\n+ WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n use object_store::memory::InMemory;\n \n@@ -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: 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: 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..2bfc711f4 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n@@ -648,7 +648,7 @@ mod tests {\n use fabro_sandbox::SandboxSpec;\n use fabro_store::Database;\n use fabro_types::settings::run::RunModelControls;\n- use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures, test_support};\n use fabro_vault::{SecretType, Vault};\n use object_store::memory::InMemory;\n use tokio::fs::{create_dir_all, write};\n@@ -773,7 +773,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs\nindex 7a8c896e4..1cf6dd67a 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs\n@@ -59,7 +59,7 @@ mod tests {\n \n use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};\n use fabro_store::{Database, RunDatabase};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -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: 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..70bf6d81d 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n@@ -680,7 +680,7 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::{\n BilledTokenCounts, RunProjection, RunSpec, SuccessReason, WorkflowSettings,\n- first_event_seq, fixtures,\n+ first_event_seq, fixtures, test_support,\n };\n use fabro_vault::{SecretType, Vault};\n use futures::stream;\n@@ -823,7 +823,7 @@ mod tests {\n automation: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1148,7 +1148,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1219,7 +1219,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -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: 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: 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: 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: 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: 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..01788b89e 100644\n--- a/lib/crates/fabro-workflow/src/run_lookup.rs\n+++ b/lib/crates/fabro-workflow/src/run_lookup.rs\n@@ -457,7 +457,7 @@ mod tests {\n \n use fabro_graphviz::graph::Graph;\n use fabro_store::Database;\n- use fabro_types::{RunStatus, WorkflowSettings, fixtures};\n+ use fabro_types::{RunStatus, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::scan_runs_combined;\n@@ -491,7 +491,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/run_metadata.rs b/lib/crates/fabro-workflow/src/run_metadata.rs\nindex b11c9667c..dd36fd373 100644\n--- a/lib/crates/fabro-workflow/src/run_metadata.rs\n+++ b/lib/crates/fabro-workflow/src/run_metadata.rs\n@@ -537,7 +537,9 @@ mod tests {\n use std::sync::Arc;\n \n use fabro_store::RunProjection;\n- use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings};\n+ use fabro_types::{\n+ DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings, test_support,\n+ };\n use git2::{ErrorClass, ErrorCode};\n \n use super::*;\n@@ -639,7 +641,7 @@ mod tests {\n push_outcome: PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs\nindex 0dafbfdad..761a8c4fd 100644\n--- a/lib/crates/fabro-workflow/src/runtime_store.rs\n+++ b/lib/crates/fabro-workflow/src/runtime_store.rs\n@@ -120,7 +120,7 @@ mod tests {\n use fabro_graphviz::graph::Graph;\n use fabro_store::Database;\n use fabro_types::run_event::RunSubmittedProps;\n- use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::RunStoreHandle;\n@@ -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: 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: 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..55155b528 100644\n--- a/lib/crates/fabro-workflow/src/test_support.rs\n+++ b/lib/crates/fabro-workflow/src/test_support.rs\n@@ -10,6 +10,7 @@ use fabro_graphviz::graph::Graph as GvGraph;\n use fabro_interview::AutoApproveInterviewer;\n use fabro_model::Catalog;\n use fabro_store::{ArtifactStore, Database, RunProjection};\n+use fabro_types::{Principal, RunProvenance, SystemActorKind};\n use object_store::local::LocalFileSystem;\n \n use crate::artifact_upload::ArtifactSink;\n@@ -26,6 +27,16 @@ use crate::run_options::RunOptions;\n use crate::sandbox_git_runtime::SandboxGitRuntime;\n use crate::services::{EngineServices, RunLocations, RunServices};\n \n+fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::System {\n+ system_kind: SystemActorKind::Engine,\n+ },\n+ }\n+}\n+\n /// These helpers stop at EXECUTE, so they emit the terminal event here to\n /// keep test consumers seeing the same end-of-run signal as production\n /// (FINALIZE).\n@@ -175,7 +186,7 @@ async fn initialized(\n workflow_slug: run_options.workflow_slug.clone(),\n automation: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n git: run_options.pre_run_git.clone(),\n fork_source_ref: run_options.fork_source_ref.clone(),\ndiff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex 5e97116d9..7839b95b6 100644\n--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n+++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n@@ -271,7 +271,6 @@ models/preflight-workflow-summary.ts\n models/preview-url-request.ts\n models/preview-url-response.ts\n models/principal-agent.ts\n-models/principal-anonymous.ts\n models/principal-slack.ts\n models/principal-system.ts\n models/principal-user.ts\ndiff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts\nindex 87b9c189c..af8ae77e0 100644\n--- a/lib/packages/fabro-api-client/src/models/index.ts\n+++ b/lib/packages/fabro-api-client/src/models/index.ts\n@@ -244,7 +244,6 @@ export * from './preview-url-request';\n export * from './preview-url-response';\n export * from './principal';\n export * from './principal-agent';\n-export * from './principal-anonymous';\n export * from './principal-slack';\n export * from './principal-system';\n export * from './principal-user';\ndiff --git a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts b/lib/packages/fabro-api-client/src/models/principal-anonymous.ts\ndeleted file mode 100644\nindex daac61df0..000000000\n--- a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts\n+++ /dev/null\n@@ -1,25 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-\n-export interface PrincipalAnonymous {\n- 'kind': PrincipalAnonymousKindEnum;\n-}\n-\n-export const PrincipalAnonymousKindEnum = {\n- ANONYMOUS: 'anonymous'\n-} as const;\n-\n-export type PrincipalAnonymousKindEnum = typeof PrincipalAnonymousKindEnum[keyof typeof PrincipalAnonymousKindEnum];\ndiff --git a/lib/packages/fabro-api-client/src/models/principal.ts b/lib/packages/fabro-api-client/src/models/principal.ts\nindex e3597295d..08b5422df 100644\n--- a/lib/packages/fabro-api-client/src/models/principal.ts\n+++ b/lib/packages/fabro-api-client/src/models/principal.ts\n@@ -24,9 +24,6 @@ import type { IdpIdentity } from './idp-identity';\n import type { PrincipalAgent } from './principal-agent';\n // May contain unused imports in some cases\n // @ts-ignore\n-import type { PrincipalAnonymous } from './principal-anonymous';\n-// May contain unused imports in some cases\n-// @ts-ignore\n import type { PrincipalSlack } from './principal-slack';\n // May contain unused imports in some cases\n // @ts-ignore\n@@ -47,4 +44,4 @@ import type { SystemActorKind } from './system-actor-kind';\n /**\n * @type Principal\n */\n-export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'anonymous' } & PrincipalAnonymous | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker;\n+export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker;\ndiff --git a/lib/packages/fabro-api-client/src/models/run-provenance.ts b/lib/packages/fabro-api-client/src/models/run-provenance.ts\nindex 7276857f8..59fa7a063 100644\n--- a/lib/packages/fabro-api-client/src/models/run-provenance.ts\n+++ b/lib/packages/fabro-api-client/src/models/run-provenance.ts\n@@ -26,5 +26,5 @@ import type { RunServerProvenance } from './run-server-provenance';\n export interface RunProvenance {\n 'server'?: RunServerProvenance | null;\n 'client'?: RunClientProvenance | null;\n- 'subject'?: Principal | null;\n+ 'subject': Principal;\n }\ndiff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts\nindex f1e5adab6..6abad2884 100644\n--- a/lib/packages/fabro-api-client/src/models/run-spec.ts\n+++ b/lib/packages/fabro-api-client/src/models/run-spec.ts\n@@ -41,7 +41,7 @@ export interface RunSpec {\n 'automation'?: AutomationRef | null;\n 'source_directory'?: string | null;\n 'labels'?: { [key: string]: string; };\n- 'provenance'?: RunProvenance | null;\n+ 'provenance': RunProvenance;\n 'manifest_blob'?: string | null;\n 'definition_blob'?: string | null;\n 'git'?: GitContext | null;\ndiff --git a/lib/packages/fabro-api-client/src/models/run.ts b/lib/packages/fabro-api-client/src/models/run.ts\nindex 1ed6c6c26..a4ade38a3 100644\n--- a/lib/packages/fabro-api-client/src/models/run.ts\n+++ b/lib/packages/fabro-api-client/src/models/run.ts\n@@ -83,7 +83,7 @@ export interface Run {\n 'workflow': WorkflowRef;\n 'automation': AutomationRef | null;\n 'repository': RepositoryRef | null;\n- 'created_by': Principal | null;\n+ 'created_by': Principal;\n 'origin': RunOrigin;\n 'labels': { [key: string]: string; };\n 'lifecycle': RunLifecycle;\ndiff --git a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\nindex 114b1e640..5fff8b8f4 100644\n--- a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n+++ b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n@@ -12,8 +12,6 @@ export function principalKind(principal: Principal): string {\n switch (principal.kind) {\n case \"agent\":\n return \"agent\";\n- case \"anonymous\":\n- return \"anonymous\";\n case \"slack\":\n return \"slack\";\n case \"system\":\n",
"summary": {
"files_changed": 97,
"additions": 404,
"deletions": 333
}
}
},
{
"seq": 1376,
"checkpoint": {
"timestamp": "2026-06-03T17:02:47.459985Z",
"current_node": "simplify_opus",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_opus"
],
"node_retries": {},
"context_values": {
"thread.start.current_node": "toolchain",
"last_response": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/f",
"thread.implement.current_node": "simplify_opus",
"internal.retry_count.start": 0,
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.preflight_compile": 0,
"failure_class": "",
"graph.rankdir": "LR",
"internal.node_visit_count": 1,
"internal.retry_count.preflight_lint": 0,
"internal.retry_count.simplify_opus": 0,
"internal.thread_id": "implement",
"failure_signature": "",
"internal.work_dir": "/home/daytona/workspace/fabro",
"response.implement": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Removed `Principal::Anonymous`; unauthenticated/invalid request state is now represented as `None`, with HTTP logs using `principal_kind=\"none\"`.\n- Made persisted run actors/provenance total:\n - `Run.created_by: Principal`\n - `RunSpec.provenance: RunProvenance`\n - `RunProvenance.subject: Principal`\n - `run.created` provenance required/non-null\n- Updated OpenAPI and regenerated Rust/TS API outputs.\n- Added `fabro-types::test_support` helpers behind `test-support`.\n- Updated server/workflow/store projections, create/retry flows, demo principal, frontend UI/data assumptions, and test fixtures/snapshots.\n- Final working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\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- Final sweeps for anonymous principal and nullable creator/provenance patterns:\n - only unrelated `crossorigin=\"anonymous\"` and unrelated todo/web-auth `subject` fields remain.\n\nNotes:\n- `cargo insta pending-snapshots` could not run because `cargo-insta` is not installed in this environment; I checked for pending `*.snap.new` files and found none.\n- Manual browser/server E2E verification was not performed in this environment.",
"thread.preflight_lint.current_node": "implement",
"internal.retry_count.toolchain": 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<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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.fidelity": "compact",
"internal.run_id": "01KT734BXFV007VWXT3G5PFXYA",
"outcome": "succeeded",
"thread.toolchain.current_node": "preflight_compile",
"current_node": "simplify_opus",
"internal.retry_count.implement": 0,
"last_stage": "simplify_opus",
"response.simplify_opus": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/fabro-web/app/routes/run-files.render.test.tsx` (`created_by` fixture had under-indented inner keys) — re-indented to match surrounding nesting.\n- **Inconsistent `<EmptyValue />` indentation** in `apps/fabro-web/app/components/run-summary-panel.tsx` — the two cells touched by the diff had been fixed, but three other cells (Sandbox, Cost, Artifacts) still had the same under-indented `<EmptyValue />`. Made all five consistent.\n\n**Skipped (with rationale):**\n- *Duplicated `test_run_provenance` in `fabro-workflow/src/test_support.rs`* — intentionally uses `Principal::System { Engine }` rather than a user, since workflow-engine tests model the engine itself as the creator. Not a true duplicate.\n- *Unifying `test_principal()` across crates* — existing per-crate helpers use the `https://github.com` issuer with `AuthMethod::Github`, while the new `fabro_types::test_support::test_principal()` deliberately uses `fabro:test`/`DevToken`. Consolidating would require widening the helper API beyond the scope of this PR.\n- *Match arm `None | Some(Principal::Agent { .. } | Principal::System { .. })` in `server.rs`* — exhaustive, concise, and exhaustiveness-checked over `Principal` variants. Splitting would add noise.\n- *Stringly-typed `\"none\"` for `principal_kind`* — used in exactly one place; a named constant would be over-engineering.\n- *`<EmptyValue />` fallback on \"Created by\"* — still reachable when `run` is undefined but not loading (e.g. error states), so not dead code.\n- *Per-request `DEMO_PRINCIPAL.clone()` allocations in demo summaries* — pre-existing pattern, the `LazyLock` already eliminates the per-request `IdpIdentity::new` validation cost. Promoting summaries to a static `Vec<Run>` is a larger refactor unrelated to this PR's scope.\n\nFrontend `typecheck` and `bun test` (508 pass) both green.",
"thread.preflight_compile.current_node": "preflight_lint"
},
"node_outcomes": {
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 134621,
"active_time_ms": 134621
}
},
"simplify_opus": {
"status": "succeeded",
"context_updates": {
"last_response": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/f",
"response.simplify_opus": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/fabro-web/app/routes/run-files.render.test.tsx` (`created_by` fixture had under-indented inner keys) — re-indented to match surrounding nesting.\n- **Inconsistent `<EmptyValue />` indentation** in `apps/fabro-web/app/components/run-summary-panel.tsx` — the two cells touched by the diff had been fixed, but three other cells (Sandbox, Cost, Artifacts) still had the same under-indented `<EmptyValue />`. Made all five consistent.\n\n**Skipped (with rationale):**\n- *Duplicated `test_run_provenance` in `fabro-workflow/src/test_support.rs`* — intentionally uses `Principal::System { Engine }` rather than a user, since workflow-engine tests model the engine itself as the creator. Not a true duplicate.\n- *Unifying `test_principal()` across crates* — existing per-crate helpers use the `https://github.com` issuer with `AuthMethod::Github`, while the new `fabro_types::test_support::test_principal()` deliberately uses `fabro:test`/`DevToken`. Consolidating would require widening the helper API beyond the scope of this PR.\n- *Match arm `None | Some(Principal::Agent { .. } | Principal::System { .. })` in `server.rs`* — exhaustive, concise, and exhaustiveness-checked over `Principal` variants. Splitting would add noise.\n- *Stringly-typed `\"none\"` for `principal_kind`* — used in exactly one place; a named constant would be over-engineering.\n- *`<EmptyValue />` fallback on \"Created by\"* — still reachable when `run` is undefined but not loading (e.g. error states), so not dead code.\n- *Per-request `DEMO_PRINCIPAL.clone()` allocations in demo summaries* — pre-existing pattern, the `LazyLock` already eliminates the per-request `IdpIdentity::new` validation cost. Promoting summaries to a static `Vec<Run>` is a larger refactor unrelated to this PR's scope.\n\nFrontend `typecheck` and `bun test` (508 pass) both green.",
"last_stage": "simplify_opus"
},
"notes": "Stage completed: simplify_opus",
"usage": {
"input": {
"usage": {
"model": {
"provider": "anthropic",
"model_id": "claude-opus-4-7"
},
"tokens": {
"input_tokens": 42982,
"output_tokens": 9623,
"reasoning_tokens": 0,
"cache_read_tokens": 809216,
"cache_write_tokens": 185694
}
},
"facts": {
"algorithm": "anthropic",
"cache_write_5m_tokens": 185694,
"cache_write_1h_tokens": 0
}
},
"total_usd_micros": 2020680
},
"files_touched": [
"/home/daytona/workspace/fabro/apps/fabro-web/app/components/run-summary-panel.tsx",
"/home/daytona/workspace/fabro/apps/fabro-web/app/routes/run-files.render.test.tsx"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 153976,
"tool_time_ms": 236152,
"active_time_ms": 390128
}
},
"start": {
"status": "succeeded",
"usage": null
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"response.implement": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Removed `Principal::Anonymous`; unauthenticated/invalid request state is now represented as `None`, with HTTP logs using `principal_kind=\"none\"`.\n- Made persisted run actors/provenance total:\n - `Run.created_by: Principal`\n - `RunSpec.provenance: RunProvenance`\n - `RunProvenance.subject: Principal`\n - `run.created` provenance required/non-null\n- Updated OpenAPI and regenerated Rust/TS API outputs.\n- Added `fabro-types::test_support` helpers behind `test-support`.\n- Updated server/workflow/store projections, create/retry flows, demo principal, frontend UI/data assumptions, and test fixtures/snapshots.\n- Final working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\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- Final sweeps for anonymous principal and nullable creator/provenance patterns:\n - only unrelated `crossorigin=\"anonymous\"` and unrelated todo/web-auth `subject` fields remain.\n\nNotes:\n- `cargo insta pending-snapshots` could not run because `cargo-insta` is not installed in this environment; I checked for pending `*.snap.new` files and found none.\n- Manual browser/server E2E verification was not performed in this environment.",
"last_response": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Remo"
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 4104889,
"output_tokens": 33716,
"reasoning_tokens": 15110,
"cache_read_tokens": 27674624,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 35826537
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 1761337,
"tool_time_ms": 1443288,
"active_time_ms": 3204625
}
},
"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": 1316,
"active_time_ms": 1316
}
},
"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": 147496,
"active_time_ms": 147496
}
}
},
"next_node_id": "simplify_gpt",
"git_commit_sha": "0551c2e65b645b2cac7e44ba74067615f80a1eb4",
"node_visits": {
"toolchain": 1,
"start": 1,
"preflight_compile": 1,
"implement": 1,
"preflight_lint": 1,
"simplify_opus": 1
}
},
"diff": {
"patch": "diff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx\nindex 78c42efc3..f97496c67 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -163,7 +163,7 @@ export function RunSummaryPanelView({\n ) : sandboxKind ? (\n <SandboxLifecycleValue kind={sandboxKind} />\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -173,7 +173,7 @@ export function RunSummaryPanelView({\n ) : cost != null ? (\n <span className={VALUE_MONO_CLASS}>{cost}</span>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -183,7 +183,7 @@ export function RunSummaryPanelView({\n ) : artifactsCount != null && artifactsCount > 0 ? (\n <span className={VALUE_MONO_CLASS}>{artifactsCount}</span>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \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 68c6258b0..8670e3a26 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@@ -52,11 +52,11 @@ mock.module(\"../lib/queries\", () => ({\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n created_by: {\n- kind: \"user\",\n- identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n- login: \"test\",\n- auth_method: \"dev_token\",\n- },\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\n",
"summary": {
"files_changed": 97,
"additions": 407,
"deletions": 336
}
}
},
{
"seq": 1439,
"checkpoint": {
"timestamp": "2026-06-03T17:03:55.853052Z",
"current_node": "simplify_gpt",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_opus",
"simplify_gpt"
],
"node_retries": {},
"context_values": {
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"response.simplify_opus": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/fabro-web/app/routes/run-files.render.test.tsx` (`created_by` fixture had under-indented inner keys) — re-indented to match surrounding nesting.\n- **Inconsistent `<EmptyValue />` indentation** in `apps/fabro-web/app/components/run-summary-panel.tsx` — the two cells touched by the diff had been fixed, but three other cells (Sandbox, Cost, Artifacts) still had the same under-indented `<EmptyValue />`. Made all five consistent.\n\n**Skipped (with rationale):**\n- *Duplicated `test_run_provenance` in `fabro-workflow/src/test_support.rs`* — intentionally uses `Principal::System { Engine }` rather than a user, since workflow-engine tests model the engine itself as the creator. Not a true duplicate.\n- *Unifying `test_principal()` across crates* — existing per-crate helpers use the `https://github.com` issuer with `AuthMethod::Github`, while the new `fabro_types::test_support::test_principal()` deliberately uses `fabro:test`/`DevToken`. Consolidating would require widening the helper API beyond the scope of this PR.\n- *Match arm `None | Some(Principal::Agent { .. } | Principal::System { .. })` in `server.rs`* — exhaustive, concise, and exhaustiveness-checked over `Principal` variants. Splitting would add noise.\n- *Stringly-typed `\"none\"` for `principal_kind`* — used in exactly one place; a named constant would be over-engineering.\n- *`<EmptyValue />` fallback on \"Created by\"* — still reachable when `run` is undefined but not loading (e.g. error states), so not dead code.\n- *Per-request `DEMO_PRINCIPAL.clone()` allocations in demo summaries* — pre-existing pattern, the `LazyLock` already eliminates the per-request `IdpIdentity::new` validation cost. Promoting summaries to a static `Vec<Run>` is a larger refactor unrelated to this PR's scope.\n\nFrontend `typecheck` and `bun test` (508 pass) both green.",
"current_node": "simplify_gpt",
"thread.preflight_lint.current_node": "implement",
"thread.simplify_opus.current_node": "simplify_gpt",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"response.simplify_gpt": "Review complete.\n\n- Working tree had no uncommitted changes, so I reviewed the latest implementation diff (`9ae80132d..HEAD`).\n- Launched all three review agents in parallel:\n - Code reuse: no issues found.\n - Code quality: no issues found.\n - Efficiency: no issues found.\n- No fixes were needed.\n- Final `git status --short` is clean.",
"graph.rankdir": "LR",
"internal.retry_count.implement": 0,
"internal.retry_count.simplify_gpt": 0,
"internal.retry_count.toolchain": 0,
"thread.start.current_node": "toolchain",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.node_visit_count": 1,
"failure_class": "",
"thread.implement.current_node": "simplify_opus",
"internal.retry_count.simplify_opus": 0,
"internal.fidelity": "compact",
"failure_signature": "",
"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<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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.start": 0,
"internal.retry_count.preflight_lint": 0,
"thread.toolchain.current_node": "preflight_compile",
"response.implement": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Removed `Principal::Anonymous`; unauthenticated/invalid request state is now represented as `None`, with HTTP logs using `principal_kind=\"none\"`.\n- Made persisted run actors/provenance total:\n - `Run.created_by: Principal`\n - `RunSpec.provenance: RunProvenance`\n - `RunProvenance.subject: Principal`\n - `run.created` provenance required/non-null\n- Updated OpenAPI and regenerated Rust/TS API outputs.\n- Added `fabro-types::test_support` helpers behind `test-support`.\n- Updated server/workflow/store projections, create/retry flows, demo principal, frontend UI/data assumptions, and test fixtures/snapshots.\n- Final working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\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- Final sweeps for anonymous principal and nullable creator/provenance patterns:\n - only unrelated `crossorigin=\"anonymous\"` and unrelated todo/web-auth `subject` fields remain.\n\nNotes:\n- `cargo insta pending-snapshots` could not run because `cargo-insta` is not installed in this environment; I checked for pending `*.snap.new` files and found none.\n- Manual browser/server E2E verification was not performed in this environment.",
"internal.run_id": "01KT734BXFV007VWXT3G5PFXYA",
"last_stage": "simplify_gpt",
"last_response": "Review complete.\n\n- Working tree had no uncommitted changes, so I reviewed the latest implementation diff (`9ae80132d..HEAD`).\n- Launched all three review agents in parallel:\n - Code reuse: no issues",
"internal.retry_count.preflight_compile": 0,
"internal.thread_id": "simplify_opus",
"outcome": "succeeded",
"thread.preflight_compile.current_node": "preflight_lint"
},
"node_outcomes": {
"start": {
"status": "succeeded",
"usage": null
},
"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": 1316,
"active_time_ms": 1316
}
},
"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": 134621,
"active_time_ms": 134621
}
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"response.implement": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Removed `Principal::Anonymous`; unauthenticated/invalid request state is now represented as `None`, with HTTP logs using `principal_kind=\"none\"`.\n- Made persisted run actors/provenance total:\n - `Run.created_by: Principal`\n - `RunSpec.provenance: RunProvenance`\n - `RunProvenance.subject: Principal`\n - `run.created` provenance required/non-null\n- Updated OpenAPI and regenerated Rust/TS API outputs.\n- Added `fabro-types::test_support` helpers behind `test-support`.\n- Updated server/workflow/store projections, create/retry flows, demo principal, frontend UI/data assumptions, and test fixtures/snapshots.\n- Final working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\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- Final sweeps for anonymous principal and nullable creator/provenance patterns:\n - only unrelated `crossorigin=\"anonymous\"` and unrelated todo/web-auth `subject` fields remain.\n\nNotes:\n- `cargo insta pending-snapshots` could not run because `cargo-insta` is not installed in this environment; I checked for pending `*.snap.new` files and found none.\n- Manual browser/server E2E verification was not performed in this environment.",
"last_response": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Remo"
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 4104889,
"output_tokens": 33716,
"reasoning_tokens": 15110,
"cache_read_tokens": 27674624,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 35826537
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 1761337,
"tool_time_ms": 1443288,
"active_time_ms": 3204625
}
},
"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": 147496,
"active_time_ms": 147496
}
},
"simplify_gpt": {
"status": "succeeded",
"context_updates": {
"last_response": "Review complete.\n\n- Working tree had no uncommitted changes, so I reviewed the latest implementation diff (`9ae80132d..HEAD`).\n- Launched all three review agents in parallel:\n - Code reuse: no issues",
"response.simplify_gpt": "Review complete.\n\n- Working tree had no uncommitted changes, so I reviewed the latest implementation diff (`9ae80132d..HEAD`).\n- Launched all three review agents in parallel:\n - Code reuse: no issues found.\n - Code quality: no issues found.\n - Efficiency: no issues found.\n- No fixes were needed.\n- Final `git status --short` is clean.",
"last_stage": "simplify_gpt"
},
"notes": "Stage completed: simplify_gpt",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 78198,
"output_tokens": 2542,
"reasoning_tokens": 284,
"cache_read_tokens": 10752,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 481146
},
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 62247,
"tool_time_ms": 1144,
"active_time_ms": 63391
}
},
"simplify_opus": {
"status": "succeeded",
"context_updates": {
"last_response": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/f",
"response.simplify_opus": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/fabro-web/app/routes/run-files.render.test.tsx` (`created_by` fixture had under-indented inner keys) — re-indented to match surrounding nesting.\n- **Inconsistent `<EmptyValue />` indentation** in `apps/fabro-web/app/components/run-summary-panel.tsx` — the two cells touched by the diff had been fixed, but three other cells (Sandbox, Cost, Artifacts) still had the same under-indented `<EmptyValue />`. Made all five consistent.\n\n**Skipped (with rationale):**\n- *Duplicated `test_run_provenance` in `fabro-workflow/src/test_support.rs`* — intentionally uses `Principal::System { Engine }` rather than a user, since workflow-engine tests model the engine itself as the creator. Not a true duplicate.\n- *Unifying `test_principal()` across crates* — existing per-crate helpers use the `https://github.com` issuer with `AuthMethod::Github`, while the new `fabro_types::test_support::test_principal()` deliberately uses `fabro:test`/`DevToken`. Consolidating would require widening the helper API beyond the scope of this PR.\n- *Match arm `None | Some(Principal::Agent { .. } | Principal::System { .. })` in `server.rs`* — exhaustive, concise, and exhaustiveness-checked over `Principal` variants. Splitting would add noise.\n- *Stringly-typed `\"none\"` for `principal_kind`* — used in exactly one place; a named constant would be over-engineering.\n- *`<EmptyValue />` fallback on \"Created by\"* — still reachable when `run` is undefined but not loading (e.g. error states), so not dead code.\n- *Per-request `DEMO_PRINCIPAL.clone()` allocations in demo summaries* — pre-existing pattern, the `LazyLock` already eliminates the per-request `IdpIdentity::new` validation cost. Promoting summaries to a static `Vec<Run>` is a larger refactor unrelated to this PR's scope.\n\nFrontend `typecheck` and `bun test` (508 pass) both green.",
"last_stage": "simplify_opus"
},
"notes": "Stage completed: simplify_opus",
"usage": {
"input": {
"usage": {
"model": {
"provider": "anthropic",
"model_id": "claude-opus-4-7"
},
"tokens": {
"input_tokens": 42982,
"output_tokens": 9623,
"reasoning_tokens": 0,
"cache_read_tokens": 809216,
"cache_write_tokens": 185694
}
},
"facts": {
"algorithm": "anthropic",
"cache_write_5m_tokens": 185694,
"cache_write_1h_tokens": 0
}
},
"total_usd_micros": 2020680
},
"files_touched": [
"/home/daytona/workspace/fabro/apps/fabro-web/app/components/run-summary-panel.tsx",
"/home/daytona/workspace/fabro/apps/fabro-web/app/routes/run-files.render.test.tsx"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 153976,
"tool_time_ms": 236152,
"active_time_ms": 390128
}
}
},
"next_node_id": "verify",
"git_commit_sha": "85aa209cdf7a30576b5357af345139e71bda2a39",
"node_visits": {
"preflight_lint": 1,
"implement": 1,
"simplify_gpt": 1,
"preflight_compile": 1,
"start": 1,
"toolchain": 1,
"simplify_opus": 1
}
},
"diff": {
"summary": {
"files_changed": 97,
"additions": 407,
"deletions": 336
}
}
},
{
"seq": 1449,
"checkpoint": {
"timestamp": "2026-06-03T17:12:22.694599Z",
"current_node": "verify",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_opus",
"simplify_gpt",
"verify"
],
"node_retries": {},
"context_values": {
"internal.thread_id": "simplify_gpt",
"response.simplify_gpt": "Review complete.\n\n- Working tree had no uncommitted changes, so I reviewed the latest implementation diff (`9ae80132d..HEAD`).\n- Launched all three review agents in parallel:\n - Code reuse: no issues found.\n - Code quality: no issues found.\n - Efficiency: no issues found.\n- No fixes were needed.\n- Final `git status --short` is clean.",
"failure_class": "",
"internal.retry_count.simplify_gpt": 0,
"command.output": "blob://sha256/14a657f055256f714ba70d6898170e78b8ad9d9fac61f27d4b1445719ab6b2d5",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.retry_count.simplify_opus": 0,
"thread.preflight_compile.current_node": "preflight_lint",
"thread.start.current_node": "toolchain",
"thread.simplify_gpt.current_node": "verify",
"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<Principal>`, 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<Principal>`.\n- `RequestAuthLogContext.principal: Principal` -> `Option<Principal>`.\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<Principal>`:\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<Principal>`:\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>` -> `Principal`.\n- Drop `#[serde(default)]`.\n\n`lib/crates/fabro-types/src/run.rs`:\n- `RunProvenance.subject: Option<Principal>` -> `Principal`.\n- Drop `#[serde(default, skip_serializing_if = \"Option::is_none\")]`.\n- Drop `Default` derive on `RunProvenance`.\n- `RunSpec.provenance: Option<RunProvenance>` -> `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>` -> `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>` -> `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>` -> `RunProvenance`.\n- `PersistCreateOptions.provenance: Option<RunProvenance>` -> `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>` -> `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<Principal> = 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",
"failure_signature": "",
"outcome": "succeeded",
"internal.retry_count.toolchain": 0,
"thread.preflight_lint.current_node": "implement",
"current_node": "verify",
"last_stage": "simplify_gpt",
"internal.run_id": "01KT734BXFV007VWXT3G5PFXYA",
"last_response": "Review complete.\n\n- Working tree had no uncommitted changes, so I reviewed the latest implementation diff (`9ae80132d..HEAD`).\n- Launched all three review agents in parallel:\n - Code reuse: no issues",
"internal.retry_count.preflight_compile": 0,
"internal.retry_count.implement": 0,
"internal.retry_count.verify": 0,
"thread.implement.current_node": "simplify_opus",
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
"internal.retry_count.start": 0,
"response.simplify_opus": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/fabro-web/app/routes/run-files.render.test.tsx` (`created_by` fixture had under-indented inner keys) — re-indented to match surrounding nesting.\n- **Inconsistent `<EmptyValue />` indentation** in `apps/fabro-web/app/components/run-summary-panel.tsx` — the two cells touched by the diff had been fixed, but three other cells (Sandbox, Cost, Artifacts) still had the same under-indented `<EmptyValue />`. Made all five consistent.\n\n**Skipped (with rationale):**\n- *Duplicated `test_run_provenance` in `fabro-workflow/src/test_support.rs`* — intentionally uses `Principal::System { Engine }` rather than a user, since workflow-engine tests model the engine itself as the creator. Not a true duplicate.\n- *Unifying `test_principal()` across crates* — existing per-crate helpers use the `https://github.com` issuer with `AuthMethod::Github`, while the new `fabro_types::test_support::test_principal()` deliberately uses `fabro:test`/`DevToken`. Consolidating would require widening the helper API beyond the scope of this PR.\n- *Match arm `None | Some(Principal::Agent { .. } | Principal::System { .. })` in `server.rs`* — exhaustive, concise, and exhaustiveness-checked over `Principal` variants. Splitting would add noise.\n- *Stringly-typed `\"none\"` for `principal_kind`* — used in exactly one place; a named constant would be over-engineering.\n- *`<EmptyValue />` fallback on \"Created by\"* — still reachable when `run` is undefined but not loading (e.g. error states), so not dead code.\n- *Per-request `DEMO_PRINCIPAL.clone()` allocations in demo summaries* — pre-existing pattern, the `LazyLock` already eliminates the per-request `IdpIdentity::new` validation cost. Promoting summaries to a static `Vec<Run>` is a larger refactor unrelated to this PR's scope.\n\nFrontend `typecheck` and `bun test` (508 pass) both green.",
"response.implement": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Removed `Principal::Anonymous`; unauthenticated/invalid request state is now represented as `None`, with HTTP logs using `principal_kind=\"none\"`.\n- Made persisted run actors/provenance total:\n - `Run.created_by: Principal`\n - `RunSpec.provenance: RunProvenance`\n - `RunProvenance.subject: Principal`\n - `run.created` provenance required/non-null\n- Updated OpenAPI and regenerated Rust/TS API outputs.\n- Added `fabro-types::test_support` helpers behind `test-support`.\n- Updated server/workflow/store projections, create/retry flows, demo principal, frontend UI/data assumptions, and test fixtures/snapshots.\n- Final working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\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- Final sweeps for anonymous principal and nullable creator/provenance patterns:\n - only unrelated `crossorigin=\"anonymous\"` and unrelated todo/web-auth `subject` fields remain.\n\nNotes:\n- `cargo insta pending-snapshots` could not run because `cargo-insta` is not installed in this environment; I checked for pending `*.snap.new` files and found none.\n- Manual browser/server E2E verification was not performed in this environment.",
"thread.toolchain.current_node": "preflight_compile",
"internal.retry_count.preflight_lint": 0,
"internal.node_visit_count": 1,
"graph.rankdir": "LR",
"thread.simplify_opus.current_node": "simplify_gpt",
"internal.fidelity": "compact"
},
"node_outcomes": {
"start": {
"status": "succeeded",
"usage": null
},
"simplify_opus": {
"status": "succeeded",
"context_updates": {
"last_response": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/f",
"response.simplify_opus": "All frontend tests pass.\n\n## Summary\n\nReviewed both commits with three parallel agents (reuse, quality, efficiency). Findings triaged and applied:\n\n**Fixed:**\n- **Inconsistent indentation** in `apps/fabro-web/app/routes/run-files.render.test.tsx` (`created_by` fixture had under-indented inner keys) — re-indented to match surrounding nesting.\n- **Inconsistent `<EmptyValue />` indentation** in `apps/fabro-web/app/components/run-summary-panel.tsx` — the two cells touched by the diff had been fixed, but three other cells (Sandbox, Cost, Artifacts) still had the same under-indented `<EmptyValue />`. Made all five consistent.\n\n**Skipped (with rationale):**\n- *Duplicated `test_run_provenance` in `fabro-workflow/src/test_support.rs`* — intentionally uses `Principal::System { Engine }` rather than a user, since workflow-engine tests model the engine itself as the creator. Not a true duplicate.\n- *Unifying `test_principal()` across crates* — existing per-crate helpers use the `https://github.com` issuer with `AuthMethod::Github`, while the new `fabro_types::test_support::test_principal()` deliberately uses `fabro:test`/`DevToken`. Consolidating would require widening the helper API beyond the scope of this PR.\n- *Match arm `None | Some(Principal::Agent { .. } | Principal::System { .. })` in `server.rs`* — exhaustive, concise, and exhaustiveness-checked over `Principal` variants. Splitting would add noise.\n- *Stringly-typed `\"none\"` for `principal_kind`* — used in exactly one place; a named constant would be over-engineering.\n- *`<EmptyValue />` fallback on \"Created by\"* — still reachable when `run` is undefined but not loading (e.g. error states), so not dead code.\n- *Per-request `DEMO_PRINCIPAL.clone()` allocations in demo summaries* — pre-existing pattern, the `LazyLock` already eliminates the per-request `IdpIdentity::new` validation cost. Promoting summaries to a static `Vec<Run>` is a larger refactor unrelated to this PR's scope.\n\nFrontend `typecheck` and `bun test` (508 pass) both green.",
"last_stage": "simplify_opus"
},
"notes": "Stage completed: simplify_opus",
"usage": {
"input": {
"usage": {
"model": {
"provider": "anthropic",
"model_id": "claude-opus-4-7"
},
"tokens": {
"input_tokens": 42982,
"output_tokens": 9623,
"reasoning_tokens": 0,
"cache_read_tokens": 809216,
"cache_write_tokens": 185694
}
},
"facts": {
"algorithm": "anthropic",
"cache_write_5m_tokens": 185694,
"cache_write_1h_tokens": 0
}
},
"total_usd_micros": 2020680
},
"files_touched": [
"/home/daytona/workspace/fabro/apps/fabro-web/app/components/run-summary-panel.tsx",
"/home/daytona/workspace/fabro/apps/fabro-web/app/routes/run-files.render.test.tsx"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 153976,
"tool_time_ms": 236152,
"active_time_ms": 390128
}
},
"verify": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/14a657f055256f714ba70d6898170e78b8ad9d9fac61f27d4b1445719ab6b2d5"
},
"notes": "Script completed: git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 483510,
"active_time_ms": 483510
}
},
"simplify_gpt": {
"status": "succeeded",
"context_updates": {
"last_response": "Review complete.\n\n- Working tree had no uncommitted changes, so I reviewed the latest implementation diff (`9ae80132d..HEAD`).\n- Launched all three review agents in parallel:\n - Code reuse: no issues",
"response.simplify_gpt": "Review complete.\n\n- Working tree had no uncommitted changes, so I reviewed the latest implementation diff (`9ae80132d..HEAD`).\n- Launched all three review agents in parallel:\n - Code reuse: no issues found.\n - Code quality: no issues found.\n - Efficiency: no issues found.\n- No fixes were needed.\n- Final `git status --short` is clean.",
"last_stage": "simplify_gpt"
},
"notes": "Stage completed: simplify_gpt",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 78198,
"output_tokens": 2542,
"reasoning_tokens": 284,
"cache_read_tokens": 10752,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 481146
},
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 62247,
"tool_time_ms": 1144,
"active_time_ms": 63391
}
},
"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": 134621,
"active_time_ms": 134621
}
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"response.implement": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Removed `Principal::Anonymous`; unauthenticated/invalid request state is now represented as `None`, with HTTP logs using `principal_kind=\"none\"`.\n- Made persisted run actors/provenance total:\n - `Run.created_by: Principal`\n - `RunSpec.provenance: RunProvenance`\n - `RunProvenance.subject: Principal`\n - `run.created` provenance required/non-null\n- Updated OpenAPI and regenerated Rust/TS API outputs.\n- Added `fabro-types::test_support` helpers behind `test-support`.\n- Updated server/workflow/store projections, create/retry flows, demo principal, frontend UI/data assumptions, and test fixtures/snapshots.\n- Final working tree is clean.\n\nVerification run:\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck && bun test && bun run build`\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- Final sweeps for anonymous principal and nullable creator/provenance patterns:\n - only unrelated `crossorigin=\"anonymous\"` and unrelated todo/web-auth `subject` fields remain.\n\nNotes:\n- `cargo insta pending-snapshots` could not run because `cargo-insta` is not installed in this environment; I checked for pending `*.snap.new` files and found none.\n- Manual browser/server E2E verification was not performed in this environment.",
"last_response": "Implemented the two-commit plan and committed both changes.\n\nCommits:\n- `43a12a3e1 refactor(auth): remove anonymous principal`\n- `9ae80132d refactor(runs): require creator provenance`\n\nSummary:\n- Remo"
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"tokens": {
"input_tokens": 4104889,
"output_tokens": 33716,
"reasoning_tokens": 15110,
"cache_read_tokens": 27674624,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
},
"total_usd_micros": 35826537
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 1761337,
"tool_time_ms": 1443288,
"active_time_ms": 3204625
}
},
"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": 1316,
"active_time_ms": 1316
}
},
"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": 147496,
"active_time_ms": 147496
}
}
},
"next_node_id": "exit",
"git_commit_sha": "bb1a3b17de15b32d73183259a45d164ea46f6a3c",
"node_visits": {
"preflight_lint": 1,
"simplify_opus": 1,
"simplify_gpt": 1,
"preflight_compile": 1,
"start": 1,
"toolchain": 1,
"implement": 1,
"verify": 1
}
},
"diff": {
"patch": "diff --git a/apps/fabro-web/app/routes/run-sandbox/vnc-panel.test.tsx b/apps/fabro-web/app/routes/run-sandbox/vnc-panel.test.tsx\nindex f7bcda6bf..e746780ab 100644\n--- a/apps/fabro-web/app/routes/run-sandbox/vnc-panel.test.tsx\n+++ b/apps/fabro-web/app/routes/run-sandbox/vnc-panel.test.tsx\n@@ -159,6 +159,7 @@ describe(\"VncPanel render\", () => {\n \"https://preview.example.com/sb-1/6080?token=abc\",\n );\n expect(iframes[0]?.props.allow).toContain(\"clipboard-write\");\n+ expect(iframes[0]?.props.sandbox).toContain(\"allow-same-origin\");\n });\n \n test(\"renders an actionable error state for 409 startup failures\", () => {\ndiff --git a/apps/fabro-web/app/routes/run-sandbox/vnc-panel.tsx b/apps/fabro-web/app/routes/run-sandbox/vnc-panel.tsx\nindex 9721a48d4..514b80042 100644\n--- a/apps/fabro-web/app/routes/run-sandbox/vnc-panel.tsx\n+++ b/apps/fabro-web/app/routes/run-sandbox/vnc-panel.tsx\n@@ -246,9 +246,9 @@ function VncBody({\n src={url}\n title=\"Sandbox VNC desktop\"\n // Daytona's signed preview already pins the iframe to the noVNC service;\n- // clipboard + fullscreen let the embedded session feel native.\n+ // noVNC reads localStorage, which requires the child to keep its origin.\n allow=\"clipboard-read; clipboard-write; fullscreen\"\n- sandbox=\"allow-forms allow-pointer-lock allow-scripts\"\n+ sandbox=\"allow-forms allow-pointer-lock allow-same-origin allow-scripts\"\n className=\"size-full border-0\"\n />\n );\n",
"summary": {
"files_changed": 99,
"additions": 410,
"deletions": 338
}
}
}
],
"conclusion": {
"timestamp": "2026-06-03T17:12:22.774893Z",
"status": "succeeded",
"timing": {
"wall_time_ms": 4610392,
"inference_time_ms": 1977560,
"tool_time_ms": 2447527,
"active_time_ms": 4425087
},
"final_git_commit_sha": "bb1a3b17de15b32d73183259a45d164ea46f6a3c",
"stages": [
{
"stage_id": "start",
"stage_label": "start",
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"retries": 0
},
{
"stage_id": "toolchain",
"stage_label": "toolchain",
"timing": {
"wall_time_ms": 1329,
"inference_time_ms": 0,
"tool_time_ms": 1316,
"active_time_ms": 1316
},
"retries": 0
},
{
"stage_id": "preflight_compile",
"stage_label": "preflight_compile",
"timing": {
"wall_time_ms": 134629,
"inference_time_ms": 0,
"tool_time_ms": 134621,
"active_time_ms": 134621
},
"retries": 0
},
{
"stage_id": "preflight_lint",
"stage_label": "preflight_lint",
"timing": {
"wall_time_ms": 147502,
"inference_time_ms": 0,
"tool_time_ms": 147496,
"active_time_ms": 147496
},
"retries": 0
},
{
"stage_id": "implement",
"stage_label": "implement",
"timing": {
"wall_time_ms": 3320136,
"inference_time_ms": 1761337,
"tool_time_ms": 1443288,
"active_time_ms": 3204625
},
"billing_usd_micros": 35826537,
"retries": 0
},
{
"stage_id": "simplify_opus",
"stage_label": "simplify_opus",
"timing": {
"wall_time_ms": 393885,
"inference_time_ms": 153976,
"tool_time_ms": 236152,
"active_time_ms": 390128
},
"billing_usd_micros": 2020680,
"retries": 0
},
{
"stage_id": "simplify_gpt",
"stage_label": "simplify_gpt",
"timing": {
"wall_time_ms": 63759,
"inference_time_ms": 62247,
"tool_time_ms": 1144,
"active_time_ms": 63391
},
"billing_usd_micros": 481146,
"retries": 0
},
{
"stage_id": "verify",
"stage_label": "verify",
"timing": {
"wall_time_ms": 483542,
"inference_time_ms": 0,
"tool_time_ms": 483510,
"active_time_ms": 483510
},
"retries": 0
}
],
"billing": {
"input_tokens": 4226069,
"output_tokens": 45881,
"total_tokens": 32967630,
"reasoning_tokens": 15394,
"cache_read_tokens": 28494592,
"cache_write_tokens": 185694,
"total_usd_micros": 38328363
},
"total_retries": 0,
"diff": {}
},
"sandbox": {
"kind": "ready",
"plan": {
"provider": "daytona"
},
"instance": {
"provider": "daytona",
"snapshot": "fabro-fdb28dec-1233-892c-b9d7-9f88f8353e7a",
"runtime": {
"id": "fabro-01KT734BXFV007VWXT3G5PFXYA",
"working_directory": "/home/daytona/workspace/fabro",
"repo_cloned": true,
"clone_origin_url": "https://github.com/fabro-sh/fabro",
"clone_branch": "main",
"workspace_root": "/home/daytona/workspace",
"repos_root": "/home/daytona/repos",
"primary_repo_path": "/home/daytona/repos/fabro-sh/fabro",
"primary_repo_link": "/home/daytona/workspace/fabro"
}
}
},
"pull_request": null,
"superseded_by": null,
"pending_interviews": {},
"stages": {
"preflight_compile@1": {
"first_event_seq": 32,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: cargo check -q --workspace 2>&1",
"failure_reason": null,
"timestamp": "2026-06-03T15:58:01.646653Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "cargo check -q --workspace 2>&1",
"command": "exec 2>&1\ncargo check -q --workspace 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"exit_code": 0,
"duration_ms": 134621,
"termination": "exited",
"output_bytes": 0,
"live_streaming": false
},
"parallel_results": null,
"output": null,
"output_bytes": 0,
"live_streaming": false,
"termination": "exited",
"started_at": "2026-06-03T15:55:47.016908Z",
"handler": "command",
"timing": {
"wall_time_ms": 134629,
"inference_time_ms": 0,
"tool_time_ms": 134621,
"active_time_ms": 134621
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"start@1": {
"first_event_seq": 18,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": null,
"failure_reason": null,
"timestamp": "2026-06-03T15:55:34.750488Z"
},
"provider_used": null,
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-06-03T15:55:34.750129Z",
"handler": "start",
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"implement@1": {
"first_event_seq": 52,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: implement",
"failure_reason": null,
"timestamp": "2026-06-03T16:56:03.993799Z"
},
"provider_used": {
"mode": "agent",
"provider": "openai",
"model": "gpt-5.5",
"reasoning_effort": "xhigh"
},
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-06-03T16:00:43.843291Z",
"handler": "agent",
"timing": {
"wall_time_ms": 3320136,
"inference_time_ms": 1761337,
"tool_time_ms": 1443288,
"active_time_ms": 3204625
},
"usage": {
"input_tokens": 4104889,
"output_tokens": 33716,
"total_tokens": 31828339,
"reasoning_tokens": 15110,
"cache_read_tokens": 27674624,
"cache_write_tokens": 0,
"total_usd_micros": 35826537
},
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"todos": {
"kind": "openai_plan",
"list_id": "openai_plan:1d6632a1-0f9e-45c3-a707-64534adaa0eb",
"items": [
{
"id": "5c1bad648a1908e6",
"status": "completed",
"order": 0,
"subject": "Commit 1: remove Principal::Anonymous and regenerate clients"
},
{
"id": "bb41a344b8a096c1",
"status": "completed",
"order": 1,
"subject": "Commit 2: make run creator/provenance total across Rust, OpenAPI, generated clients, frontend, demo, and tests"
},
{
"id": "6e61125730ba0b79",
"status": "completed",
"order": 2,
"subject": "Run final sweeps and verification commands"
},
{
"id": "2b83a22a36b6dbb7",
"status": "completed",
"order": 3,
"subject": "Commit commit 2"
}
]
},
"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": true
},
{
"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": true
},
{
"name": "grep",
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "read_file",
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "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": true
},
{
"name": "spawn_agent",
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": 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": true
},
{
"name": "wait",
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": 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": true
}
],
"context_window": {
"provider": "openai",
"model": "gpt-5.5",
"context_window_tokens": 272000,
"input_tokens": 184400,
"usage_percent": 67.79411764705883,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-06-03T16:56:03.667832Z",
"event_seq": 992,
"breakdown": [
{
"category": "system_prompt",
"tokens": 1009,
"usage_percent": 0.3709558823529412
},
{
"category": "tools",
"tokens": 1482,
"usage_percent": 0.5448529411764705
},
{
"category": "memory",
"tokens": 3527,
"usage_percent": 1.2966911764705882
},
{
"category": "conversation",
"tokens": 178375,
"usage_percent": 65.57904411764706
},
{
"category": "other",
"tokens": 7,
"usage_percent": 0.002573529411764706
}
],
"warnings": []
},
"state": "succeeded"
},
"toolchain@1": {
"first_event_seq": 22,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"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",
"failure_reason": null,
"timestamp": "2026-06-03T15:55:36.080990Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"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",
"command": "exec 2>&1\ncommand -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",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c",
"exit_code": 0,
"duration_ms": 1316,
"termination": "exited",
"output_bytes": 36,
"live_streaming": true
},
"parallel_results": null,
"output": null,
"output_bytes": 36,
"live_streaming": true,
"termination": "exited",
"started_at": "2026-06-03T15:55:34.751146Z",
"handler": "command",
"timing": {
"wall_time_ms": 1329,
"inference_time_ms": 0,
"tool_time_ms": 1316,
"active_time_ms": 1316
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"preflight_lint@1": {
"first_event_seq": 42,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"failure_reason": null,
"timestamp": "2026-06-03T16:00:34.027493Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"exit_code": 0,
"duration_ms": 147496,
"termination": "exited",
"output_bytes": 0,
"live_streaming": false
},
"parallel_results": null,
"output": null,
"output_bytes": 0,
"live_streaming": false,
"termination": "exited",
"started_at": "2026-06-03T15:58:06.524157Z",
"handler": "command",
"timing": {
"wall_time_ms": 147502,
"inference_time_ms": 0,
"tool_time_ms": 147496,
"active_time_ms": 147496
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"simplify_opus@1": {
"first_event_seq": 1002,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: simplify_opus",
"failure_reason": null,
"timestamp": "2026-06-03T17:02:42.584463Z"
},
"provider_used": {
"mode": "agent",
"provider": "anthropic",
"model": "claude-opus-4-7"
},
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-06-03T16:56:08.697176Z",
"handler": "agent",
"timing": {
"wall_time_ms": 393885,
"inference_time_ms": 153976,
"tool_time_ms": 236152,
"active_time_ms": 390128
},
"usage": {
"input_tokens": 42982,
"output_tokens": 9623,
"total_tokens": 1047515,
"reasoning_tokens": 0,
"cache_read_tokens": 809216,
"cache_write_tokens": 185694,
"total_usd_micros": 2020680
},
"model": {
"provider": "anthropic",
"model_id": "claude-opus-4-7"
},
"subagents": [
{
"agent_id": "40a50d32",
"depth": 1,
"task": "You are conducting a CODE REUSE REVIEW on a recent set of changes in the Fabro repository.\n\nThe two commits to review are at:\n- 43a12a3e1 refactor(auth): remove anonymous principal\n- 9ae80132d refactor(runs): require creator provenance\n\nRun `git diff 43a12a3e1^ HEAD` to see the changes. (Working dir is /home/daytona/workspace/fabro.) Use the full diff file at /tmp/full.diff if useful.\n\nThe change goals:\n1. Removed `Principal::Anonymous` variant. Auth middleware now uses `Option<Principal>` for \"no authenticated actor.\"\n2. Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `Event::RunCreated.provenance` non-optional (total).\n3. Added test_support helpers `test_principal()` and `test_run_provenance()` in `lib/crates/fabro-types/src/test_support.rs`.\n\nYour task: For each change, look for existing utilities/helpers that could replace newly written code. Specifically:\n\n1. **Search for existing utilities and helpers** that could replace newly written code. Look at common locations (utility directories, shared modules, files adjacent to the changes).\n2. **Flag any new function that duplicates existing functionality.** Suggest the existing function to use instead.\n3. **Flag any inline logic that could use an existing utility.**\n\nPay close attention to:\n- The new `test_support.rs` module in fabro-types - is it duplicating existing fixture helpers (e.g. in `fabro-types/src/fixtures` or other crates' test_support)?\n- The new demo principal in `lib/crates/fabro-server/src/demo/mod.rs` - is there a similar test/demo principal builder elsewhere?\n- The `run_provenance` helper in handler code - is it duplicated across handlers?\n- Use of `Principal::user(...)` constructors in test files - is there a builder pattern in test_support already?\n\nDO NOT make any code changes. Only report findings as a concise list of issues with file:line references and the suggested existing utility to reuse. If something is clean, say so.\n\nOutput format:\n- ISSUE: <description>\n - File: <path:line>\n - Existing utility: <where to find it>\n - Recommendation: <action>\n\nEnd your report with a single line: \"REVIEW COMPLETE.\"",
"status": {
"kind": "completed",
"success": true,
"turns_used": 83
}
},
{
"agent_id": "8560a877",
"depth": 1,
"task": "You are conducting a CODE QUALITY REVIEW on a recent set of changes in the Fabro repository.\n\nThe two commits to review are at:\n- 43a12a3e1 refactor(auth): remove anonymous principal\n- 9ae80132d refactor(runs): require creator provenance\n\nRun `git diff 43a12a3e1^ HEAD` to see the changes. (Working dir is /home/daytona/workspace/fabro.) Use the full diff file at /tmp/full.diff if useful.\n\nThe change goals:\n1. Removed `Principal::Anonymous` variant. Auth middleware now uses `Option<Principal>` for \"no authenticated actor.\"\n2. Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `Event::RunCreated.provenance` non-optional (total).\n3. Added test_support helpers `test_principal()` and `test_run_provenance()` in `lib/crates/fabro-types/src/test_support.rs`.\n\nReview the changes for hacky patterns:\n\n1. **Redundant state**: state that duplicates existing state, cached values that could be derived\n2. **Parameter sprawl**: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. **Copy-paste with slight variation**: near-duplicate code blocks that should be unified\n4. **Leaky abstractions**: exposing internal details that should be encapsulated\n5. **Stringly-typed code**: using raw strings where constants/enums already exist\n6. **Unnecessary JSX nesting**: wrapper elements that add no layout value\n7. **Unnecessary comments**: comments narrating WHAT the code does, narrating the change, or referencing tasks - delete; keep only non-obvious WHY\n\nPay close attention to:\n- `lib/crates/fabro-server/src/server.rs` HTTP logging code (match arms for `Option<Principal>`)\n- `lib/crates/fabro-server/src/principal_middleware.rs` gate helpers\n- The new test_support module and how it's used\n- Any TODO/comment narrating the change in the diff\n- Match patterns that could be simplified\n\nDO NOT make any code changes. Only report findings as a concise list. If something is clean, say so.\n\nOutput format:\n- ISSUE: <description>\n - File: <path:line>\n - Recommendation: <action>\n\nEnd your report with a single line: \"REVIEW COMPLETE.\"",
"status": {
"kind": "completed",
"success": true,
"turns_used": 44
}
},
{
"agent_id": "6c6df762",
"depth": 1,
"task": "You are conducting an EFFICIENCY REVIEW on a recent set of changes in the Fabro repository.\n\nThe two commits to review are at:\n- 43a12a3e1 refactor(auth): remove anonymous principal\n- 9ae80132d refactor(runs): require creator provenance\n\nRun `git diff 43a12a3e1^ HEAD` to see the changes. (Working dir is /home/daytona/workspace/fabro.) Use the full diff file at /tmp/full.diff if useful.\n\nThe change goals:\n1. Removed `Principal::Anonymous` variant. Auth middleware now uses `Option<Principal>` for \"no authenticated actor.\"\n2. Made `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `Event::RunCreated.provenance` non-optional (total).\n3. Added test_support helpers in fabro-types.\n\nReview for efficiency issues:\n\n1. **Unnecessary work**: redundant computations, repeated clones, repeated file reads, duplicate calls\n2. **Missed concurrency**: independent operations run sequentially\n3. **Hot-path bloat**: new blocking work added to per-request hot paths (HTTP middleware, logging, principal extraction)\n4. **Unnecessary clones**: `.clone()` on Principal or RunProvenance where references or moves would work\n5. **Unnecessary existence checks**\n6. **Memory**: unbounded data, missing cleanup\n7. **Overly broad operations**\n\nPay close attention to:\n- The new demo principal `LazyLock<Principal>` and how it's used in `lib/crates/fabro-server/src/demo/mod.rs` - is it cloned excessively?\n- Principal cloning in the HTTP request path (`server.rs`, `principal_middleware.rs`)\n- Provenance subject clones in `run_state.rs` `build_summary` and `projection_from_created`\n- The `run_provenance` helper - is it constructing the same thing multiple times per request?\n- `Principal::clone()` in tests vs direct construction\n\nDO NOT make any code changes. Only report findings as a concise list with file:line references. If something is clean, say so.\n\nOutput format:\n- ISSUE: <description>\n - File: <path:line>\n - Recommendation: <action>\n\nEnd your report with a single line: \"REVIEW COMPLETE.\"",
"status": {
"kind": "completed",
"success": true,
"turns_used": 62
}
}
],
"permission_level": "full",
"agent_tools": [
{
"name": "AskUserQuestion",
"description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "TaskCreate",
"description": "Create pending tasks in the current session. Use concise subjects, descriptions, optional activeForm text, and metadata. Check TaskList first to avoid duplicate tasks.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "TaskGet",
"description": "Get one task by taskId, including subject, status, description, owner, blockedBy, and blocks.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "TaskList",
"description": "List tasks for the current session, including status, owner, and blocking dependencies. Use TaskGet with a taskId for full description and dependency details.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "TaskUpdate",
"description": "Update an existing task's status, text, owner, metadata, or dependencies. Valid statuses are pending, in_progress, completed, and deleted. After completing a task, call TaskList to find newly unblocked work.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "close_agent",
"description": "Close a running subagent that is no longer needed.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": false
},
{
"name": "edit_file",
"description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": true
},
{
"name": "glob",
"description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "grep",
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "read_file",
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "send_input",
"description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": false
},
{
"name": "shell",
"description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
"source": {
"kind": "native"
},
"category": "shell",
"invoked": true
},
{
"name": "spawn_agent",
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "wait",
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "web_fetch",
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "web_search",
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "write_file",
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": false
}
],
"context_window": {
"provider": "anthropic",
"model": "claude-opus-4-7",
"context_window_tokens": 1000000,
"input_tokens": 55197,
"usage_percent": 5.5197,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-06-03T17:02:40.661570Z",
"event_seq": 1369,
"breakdown": [
{
"category": "system_prompt",
"tokens": 2468,
"usage_percent": 0.2468
},
{
"category": "tools",
"tokens": 2863,
"usage_percent": 0.2863
},
{
"category": "memory",
"tokens": 6087,
"usage_percent": 0.6087
},
{
"category": "conversation",
"tokens": 43772,
"usage_percent": 4.3772
},
{
"category": "other",
"tokens": 7,
"usage_percent": 0.0007
}
],
"warnings": []
},
"state": "succeeded"
},
"simplify_gpt@1": {
"first_event_seq": 1379,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: simplify_gpt",
"failure_reason": null,
"timestamp": "2026-06-03T17:03:51.224611Z"
},
"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-03T17:02:47.464964Z",
"handler": "agent",
"timing": {
"wall_time_ms": 63759,
"inference_time_ms": 62247,
"tool_time_ms": 1144,
"active_time_ms": 63391
},
"usage": {
"input_tokens": 78198,
"output_tokens": 2542,
"total_tokens": 91776,
"reasoning_tokens": 284,
"cache_read_tokens": 10752,
"cache_write_tokens": 0,
"total_usd_micros": 481146
},
"model": {
"provider": "openai",
"model_id": "gpt-5.5"
},
"subagents": [
{
"agent_id": "86dab51f",
"depth": 1,
"task": "Code Reuse Review. Review this diff for opportunities to reuse existing utilities/helpers and flag duplicated new functions or inline logic that should use existing utilities. If no issues, say so. Full diff:\n\n```diff\ndiff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx\nindex 78c42efc3..f97496c67 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -163,7 +163,7 @@ export function RunSummaryPanelView({\n ) : sandboxKind ? (\n <SandboxLifecycleValue kind={sandboxKind} />\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -173,7 +173,7 @@ export function RunSummaryPanelView({\n ) : cost != null ? (\n <span className={VALUE_MONO_CLASS}>{cost}</span>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -183,7 +183,7 @@ export function RunSummaryPanelView({\n ) : artifactsCount != null && artifactsCount > 0 ? (\n <span className={VALUE_MONO_CLASS}>{artifactsCount}</span>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \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 68c6258b0..8670e3a26 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@@ -52,11 +52,11 @@ mock.module(\"../lib/queries\", () => ({\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n created_by: {\n- kind: \"user\",\n- identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n- login: \"test\",\n- auth_method: \"dev_token\",\n- },\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\n```",
"status": {
"kind": "completed",
"success": true,
"turns_used": 2
}
},
{
"agent_id": "a322e7d4",
"depth": 1,
"task": "Code Quality Review. Review this diff for hacky patterns: redundant state, parameter sprawl, copy-paste, leaky abstractions, stringly-typed code, unnecessary JSX nesting, and unnecessary comments. If no issues, say so. Full diff:\n\n```diff\ndiff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx\nindex 78c42efc3..f97496c67 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -163,7 +163,7 @@ export function RunSummaryPanelView({\n ) : sandboxKind ? (\n <SandboxLifecycleValue kind={sandboxKind} />\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -173,7 +173,7 @@ export function RunSummaryPanelView({\n ) : cost != null ? (\n <span className={VALUE_MONO_CLASS}>{cost}</span>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -183,7 +183,7 @@ export function RunSummaryPanelView({\n ) : artifactsCount != null && artifactsCount > 0 ? (\n <span className={VALUE_MONO_CLASS}>{artifactsCount}</span>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \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 68c6258b0..8670e3a26 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@@ -52,11 +52,11 @@ mock.module(\"../lib/queries\", () => ({\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n created_by: {\n- kind: \"user\",\n- identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n- login: \"test\",\n- auth_method: \"dev_token\",\n- },\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\n```",
"status": {
"kind": "completed",
"success": true,
"turns_used": 2
}
},
{
"agent_id": "17cd4844",
"depth": 1,
"task": "Efficiency Review. Review this diff for unnecessary work, missed concurrency, hot-path bloat, recurring no-op updates, TOCTOU existence checks, memory issues, or overly broad operations. If no issues, say so. Full diff:\n\n```diff\ndiff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx\nindex 78c42efc3..f97496c67 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -163,7 +163,7 @@ export function RunSummaryPanelView({\n ) : sandboxKind ? (\n <SandboxLifecycleValue kind={sandboxKind} />\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -173,7 +173,7 @@ export function RunSummaryPanelView({\n ) : cost != null ? (\n <span className={VALUE_MONO_CLASS}>{cost}</span>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \n@@ -183,7 +183,7 @@ export function RunSummaryPanelView({\n ) : artifactsCount != null && artifactsCount > 0 ? (\n <span className={VALUE_MONO_CLASS}>{artifactsCount}</span>\n ) : (\n- <EmptyValue />\n+ <EmptyValue />\n )}\n </Cell>\n \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 68c6258b0..8670e3a26 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@@ -52,11 +52,11 @@ mock.module(\"../lib/queries\", () => ({\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n created_by: {\n- kind: \"user\",\n- identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n- login: \"test\",\n- auth_method: \"dev_token\",\n- },\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: \"test-user\" },\n+ login: \"test\",\n+ auth_method: \"dev_token\",\n+ },\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\n```",
"status": {
"kind": "completed",
"success": true,
"turns_used": 2
}
}
],
"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": true
},
{
"name": "spawn_agent",
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "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": true
},
{
"name": "web_fetch",
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "web_search",
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "write_file",
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": false
}
],
"context_window": {
"provider": "openai",
"model": "gpt-5.5",
"context_window_tokens": 272000,
"input_tokens": 14660,
"usage_percent": 5.389705882352941,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-06-03T17:03:51.189147Z",
"event_seq": 1432,
"breakdown": [
{
"category": "system_prompt",
"tokens": 902,
"usage_percent": 0.3316176470588235
},
{
"category": "tools",
"tokens": 1314,
"usage_percent": 0.48308823529411765
},
{
"category": "memory",
"tokens": 3129,
"usage_percent": 1.1503676470588236
},
{
"category": "conversation",
"tokens": 9310,
"usage_percent": 3.422794117647059
},
{
"category": "other",
"tokens": 5,
"usage_percent": 0.001838235294117647
}
],
"warnings": []
},
"state": "succeeded"
},
"verify@1": {
"first_event_seq": 1442,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"failure_reason": null,
"timestamp": "2026-06-03T17:11:59.401885Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"command": "exec 2>&1\ngit fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/14a657f055256f714ba70d6898170e78b8ad9d9fac61f27d4b1445719ab6b2d5",
"exit_code": 0,
"duration_ms": 483510,
"termination": "exited",
"output_bytes": 201705,
"live_streaming": true
},
"parallel_results": null,
"output": null,
"output_bytes": 201705,
"live_streaming": true,
"termination": "exited",
"started_at": "2026-06-03T17:03:55.856785Z",
"handler": "command",
"timing": {
"wall_time_ms": 483542,
"inference_time_ms": 0,
"tool_time_ms": 483510,
"active_time_ms": 483510
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"exit@1": {
"first_event_seq": 1452,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": null,
"failure_reason": null,
"timestamp": "2026-06-03T17:12:22.695009Z"
},
"provider_used": null,
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-06-03T17:12:22.694911Z",
"handler": "exit",
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
}
}
}