mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-11 03:40:05 +00:00
2689 lines
No EOL
425 KiB
JSON
2689 lines
No EOL
425 KiB
JSON
{
|
||
"title": "Remove Principal::Anonymous & make provenance total",
|
||
"spec": {
|
||
"run_id": "01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"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": {
|
||
"ref": "fabro-v12",
|
||
"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": {
|
||
"preflight_compile": {
|
||
"id": "preflight_compile",
|
||
"attrs": {
|
||
"max_retries": {
|
||
"Integer": 0
|
||
},
|
||
"script": {
|
||
"String": "cargo check -q --workspace 2>&1"
|
||
},
|
||
"model": {
|
||
"String": "claude-opus-4-7"
|
||
},
|
||
"shape": {
|
||
"String": "parallelogram"
|
||
},
|
||
"label": {
|
||
"String": "Preflight Compile"
|
||
},
|
||
"provider": {
|
||
"String": "anthropic"
|
||
}
|
||
}
|
||
},
|
||
"exit": {
|
||
"id": "exit",
|
||
"attrs": {
|
||
"label": {
|
||
"String": "Exit"
|
||
},
|
||
"provider": {
|
||
"String": "anthropic"
|
||
},
|
||
"model": {
|
||
"String": "claude-opus-4-7"
|
||
},
|
||
"shape": {
|
||
"String": "Msquare"
|
||
}
|
||
}
|
||
},
|
||
"fixup": {
|
||
"id": "fixup",
|
||
"attrs": {
|
||
"model": {
|
||
"String": "claude-opus-4-7"
|
||
},
|
||
"max_visits": {
|
||
"Integer": 3
|
||
},
|
||
"label": {
|
||
"String": "Fixup"
|
||
},
|
||
"provider": {
|
||
"String": "anthropic"
|
||
},
|
||
"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."
|
||
}
|
||
}
|
||
},
|
||
"preflight_lint": {
|
||
"id": "preflight_lint",
|
||
"attrs": {
|
||
"provider": {
|
||
"String": "anthropic"
|
||
},
|
||
"shape": {
|
||
"String": "parallelogram"
|
||
},
|
||
"model": {
|
||
"String": "claude-opus-4-7"
|
||
},
|
||
"script": {
|
||
"String": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1"
|
||
},
|
||
"label": {
|
||
"String": "Preflight Lint"
|
||
},
|
||
"max_retries": {
|
||
"Integer": 0
|
||
}
|
||
}
|
||
},
|
||
"simplify_opus": {
|
||
"id": "simplify_opus",
|
||
"attrs": {
|
||
"prompt": {
|
||
"String": "# Simplify: Code Review and Cleanup\n\nReview changes vs. origin 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 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. Use Grep to find 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\nNote: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.\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\n\nNote: This is a greenfield app, so be aggressive in optimizing quality.\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. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n5. Memory: unbounded data structures, missing cleanup, event listener leaks\n6. 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)."
|
||
},
|
||
"model": {
|
||
"String": "claude-opus-4-7"
|
||
},
|
||
"label": {
|
||
"String": "Simplify (Opus)"
|
||
},
|
||
"provider": {
|
||
"String": "anthropic"
|
||
}
|
||
}
|
||
},
|
||
"fix_lints": {
|
||
"id": "fix_lints",
|
||
"attrs": {
|
||
"label": {
|
||
"String": "Fix Lints"
|
||
},
|
||
"model": {
|
||
"String": "claude-opus-4-7"
|
||
},
|
||
"prompt": {
|
||
"String": "The preflight lint step failed. Read the build output from context and fix all clippy lint warnings."
|
||
},
|
||
"provider": {
|
||
"String": "anthropic"
|
||
},
|
||
"max_visits": {
|
||
"Integer": 3
|
||
}
|
||
}
|
||
},
|
||
"simplify_gpt": {
|
||
"id": "simplify_gpt",
|
||
"attrs": {
|
||
"model": {
|
||
"String": "gpt-5.5"
|
||
},
|
||
"label": {
|
||
"String": "Simplify (GPT-55)"
|
||
},
|
||
"provider": {
|
||
"String": "openai"
|
||
},
|
||
"prompt": {
|
||
"String": "# Simplify: Code Review and Cleanup\n\nReview changes vs. origin 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 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. Use Grep to find 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\nNote: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.\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\n\nNote: This is a greenfield app, so be aggressive in optimizing quality.\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. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n5. Memory: unbounded data structures, missing cleanup, event listener leaks\n6. 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"
|
||
},
|
||
"shape": {
|
||
"String": "Mdiamond"
|
||
},
|
||
"label": {
|
||
"String": "Start"
|
||
},
|
||
"provider": {
|
||
"String": "anthropic"
|
||
}
|
||
}
|
||
},
|
||
"toolchain": {
|
||
"id": "toolchain",
|
||
"attrs": {
|
||
"provider": {
|
||
"String": "anthropic"
|
||
},
|
||
"label": {
|
||
"String": "Toolchain"
|
||
},
|
||
"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"
|
||
},
|
||
"model": {
|
||
"String": "claude-opus-4-7"
|
||
},
|
||
"shape": {
|
||
"String": "parallelogram"
|
||
},
|
||
"max_retries": {
|
||
"Integer": 0
|
||
}
|
||
}
|
||
},
|
||
"verify": {
|
||
"id": "verify",
|
||
"attrs": {
|
||
"provider": {
|
||
"String": "anthropic"
|
||
},
|
||
"label": {
|
||
"String": "Verify"
|
||
},
|
||
"shape": {
|
||
"String": "parallelogram"
|
||
},
|
||
"goal_gate": {
|
||
"Boolean": true
|
||
},
|
||
"model": {
|
||
"String": "claude-opus-4-7"
|
||
},
|
||
"retry_target": {
|
||
"String": "fixup"
|
||
},
|
||
"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"
|
||
}
|
||
}
|
||
},
|
||
"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."
|
||
},
|
||
"provider": {
|
||
"String": "openai"
|
||
},
|
||
"model": {
|
||
"String": "gpt-5.5"
|
||
},
|
||
"label": {
|
||
"String": "Implement"
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"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": {
|
||
"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"
|
||
},
|
||
"rankdir": {
|
||
"String": "LR"
|
||
},
|
||
"model_stylesheet": {
|
||
"String": "\n * { model: claude-opus-4-7; }\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.245.0-nightly.1"
|
||
},
|
||
"client": {
|
||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"
|
||
},
|
||
"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": "6f9f13359bb1127e5e8f5207d578693a22272f8832d9a8a217a53f70b306984c",
|
||
"definition_blob": "f13f100f4f1f386c63f22b375c331077f1c1b3bd2df0dd17388b26786f23b9a6",
|
||
"git": {
|
||
"origin_url": "https://github.com/fabro-sh/fabro",
|
||
"branch": "main",
|
||
"sha": "70b9e9a1ab3efe10d723b9f2ab23891a28068425",
|
||
"dirty": "dirty",
|
||
"push_outcome": {
|
||
"type": "not_attempted"
|
||
}
|
||
}
|
||
},
|
||
"web_url": "http://127.0.0.1:32276/runs/01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"start": {
|
||
"start_time": "2026-05-27T03:13:05.519935Z",
|
||
"run_branch": "fabro/run/01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"base_sha": "c2da22a27c3cd73172aa85ba922d7e3cb491e962"
|
||
},
|
||
"status": {
|
||
"kind": "running"
|
||
},
|
||
"status_updated_at": "2026-05-27T03:13:05.519980Z",
|
||
"last_event_at": "2026-05-27T05:30:28.883458Z",
|
||
"pending_control": null,
|
||
"checkpoints": [
|
||
{
|
||
"seq": 20,
|
||
"checkpoint": {
|
||
"timestamp": "2026-05-27T03:13:07.732657Z",
|
||
"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.rankdir": "LR",
|
||
"failure_class": "",
|
||
"internal.work_dir": "/home/daytona/workspace/fabro",
|
||
"internal.thread_id": null,
|
||
"failure_signature": "",
|
||
"outcome": "succeeded",
|
||
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
||
"internal.node_visit_count": 1,
|
||
"internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"current_node": "start",
|
||
"internal.fidelity": "compact",
|
||
"internal.retry_count.start": 0
|
||
},
|
||
"node_outcomes": {
|
||
"start": {
|
||
"status": "succeeded",
|
||
"usage": null
|
||
}
|
||
},
|
||
"next_node_id": "toolchain",
|
||
"node_visits": {
|
||
"start": 1
|
||
}
|
||
},
|
||
"diff": {}
|
||
},
|
||
{
|
||
"seq": 28,
|
||
"checkpoint": {
|
||
"timestamp": "2026-05-27T03:13:13.813645Z",
|
||
"current_node": "toolchain",
|
||
"completed_nodes": [
|
||
"start",
|
||
"toolchain"
|
||
],
|
||
"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.rankdir": "LR",
|
||
"internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"thread.start.current_node": "toolchain",
|
||
"internal.retry_count.start": 0,
|
||
"internal.fidelity": "compact",
|
||
"internal.node_visit_count": 1,
|
||
"outcome": "succeeded",
|
||
"internal.retry_count.toolchain": 0,
|
||
"internal.thread_id": "start",
|
||
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
||
"failure_signature": "",
|
||
"internal.work_dir": "/home/daytona/workspace/fabro",
|
||
"failure_class": "",
|
||
"current_node": "toolchain",
|
||
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
||
},
|
||
"node_outcomes": {
|
||
"toolchain": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
||
},
|
||
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 1456,
|
||
"active_time_ms": 1456
|
||
}
|
||
},
|
||
"start": {
|
||
"status": "succeeded",
|
||
"usage": null
|
||
}
|
||
},
|
||
"next_node_id": "preflight_compile",
|
||
"git_commit_sha": "06894c1a58fda714a9c2a22e2972c709c9b9686e",
|
||
"node_visits": {
|
||
"start": 1,
|
||
"toolchain": 1
|
||
}
|
||
},
|
||
"diff": {
|
||
"summary": {
|
||
"files_changed": 0,
|
||
"additions": 0,
|
||
"deletions": 0
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"seq": 38,
|
||
"checkpoint": {
|
||
"timestamp": "2026-05-27T03:15:26.200355Z",
|
||
"current_node": "preflight_compile",
|
||
"completed_nodes": [
|
||
"start",
|
||
"toolchain",
|
||
"preflight_compile"
|
||
],
|
||
"node_retries": {},
|
||
"context_values": {
|
||
"internal.retry_count.preflight_compile": 0,
|
||
"internal.thread_id": "toolchain",
|
||
"internal.retry_count.start": 0,
|
||
"internal.retry_count.toolchain": 0,
|
||
"outcome": "succeeded",
|
||
"internal.fidelity": "compact",
|
||
"current_node": "preflight_compile",
|
||
"internal.node_visit_count": 1,
|
||
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
||
"graph.rankdir": "LR",
|
||
"failure_class": "",
|
||
"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",
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
||
"thread.start.current_node": "toolchain",
|
||
"failure_signature": "",
|
||
"internal.work_dir": "/home/daytona/workspace/fabro",
|
||
"internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A"
|
||
},
|
||
"node_outcomes": {
|
||
"toolchain": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
||
},
|
||
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 1456,
|
||
"active_time_ms": 1456
|
||
}
|
||
},
|
||
"preflight_compile": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 127963,
|
||
"active_time_ms": 127963
|
||
}
|
||
},
|
||
"start": {
|
||
"status": "succeeded",
|
||
"usage": null
|
||
}
|
||
},
|
||
"next_node_id": "preflight_lint",
|
||
"git_commit_sha": "829e3002d0dda5249687d3fefbb59e73d0908dce",
|
||
"node_visits": {
|
||
"toolchain": 1,
|
||
"preflight_compile": 1,
|
||
"start": 1
|
||
}
|
||
},
|
||
"diff": {
|
||
"summary": {
|
||
"files_changed": 0,
|
||
"additions": 0,
|
||
"deletions": 0
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"seq": 48,
|
||
"checkpoint": {
|
||
"timestamp": "2026-05-27T03:17:49.388758Z",
|
||
"current_node": "preflight_lint",
|
||
"completed_nodes": [
|
||
"start",
|
||
"toolchain",
|
||
"preflight_compile",
|
||
"preflight_lint"
|
||
],
|
||
"node_retries": {},
|
||
"context_values": {
|
||
"internal.retry_count.preflight_compile": 0,
|
||
"thread.start.current_node": "toolchain",
|
||
"internal.retry_count.preflight_lint": 0,
|
||
"internal.work_dir": "/home/daytona/workspace/fabro",
|
||
"graph.rankdir": "LR",
|
||
"internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"thread.preflight_compile.current_node": "preflight_lint",
|
||
"internal.node_visit_count": 1,
|
||
"internal.retry_count.start": 0,
|
||
"outcome": "succeeded",
|
||
"internal.thread_id": "preflight_compile",
|
||
"thread.toolchain.current_node": "preflight_compile",
|
||
"failure_signature": "",
|
||
"internal.retry_count.toolchain": 0,
|
||
"current_node": "preflight_lint",
|
||
"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",
|
||
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
||
"failure_class": ""
|
||
},
|
||
"node_outcomes": {
|
||
"preflight_compile": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 127963,
|
||
"active_time_ms": 127963
|
||
}
|
||
},
|
||
"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": 138598,
|
||
"active_time_ms": 138598
|
||
}
|
||
},
|
||
"toolchain": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
||
},
|
||
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 1456,
|
||
"active_time_ms": 1456
|
||
}
|
||
}
|
||
},
|
||
"next_node_id": "implement",
|
||
"git_commit_sha": "5e58ef714bc9f5fc7a922231c0310b834f97f1e6",
|
||
"node_visits": {
|
||
"toolchain": 1,
|
||
"preflight_compile": 1,
|
||
"start": 1,
|
||
"preflight_lint": 1
|
||
}
|
||
},
|
||
"diff": {
|
||
"summary": {
|
||
"files_changed": 0,
|
||
"additions": 0,
|
||
"deletions": 0
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"seq": 1735,
|
||
"checkpoint": {
|
||
"timestamp": "2026-05-27T05:06:57.697233Z",
|
||
"current_node": "implement",
|
||
"completed_nodes": [
|
||
"start",
|
||
"toolchain",
|
||
"preflight_compile",
|
||
"preflight_lint",
|
||
"implement"
|
||
],
|
||
"node_retries": {},
|
||
"context_values": {
|
||
"failure_class": "",
|
||
"internal.thread_id": "preflight_lint",
|
||
"thread.start.current_node": "toolchain",
|
||
"internal.retry_count.preflight_lint": 0,
|
||
"internal.fidelity": "compact",
|
||
"internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option<Principal>` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.",
|
||
"thread.toolchain.current_node": "preflight_compile",
|
||
"graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option<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.node_visit_count": 1,
|
||
"internal.retry_count.start": 0,
|
||
"outcome": "succeeded",
|
||
"thread.preflight_compile.current_node": "preflight_lint",
|
||
"internal.retry_count.preflight_compile": 0,
|
||
"internal.work_dir": "/home/daytona/workspace/fabro",
|
||
"graph.rankdir": "LR",
|
||
"internal.retry_count.toolchain": 0,
|
||
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
||
"thread.preflight_lint.current_node": "implement",
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
||
"last_response": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; mis",
|
||
"failure_signature": "",
|
||
"current_node": "implement",
|
||
"internal.retry_count.implement": 0,
|
||
"last_stage": "implement"
|
||
},
|
||
"node_outcomes": {
|
||
"toolchain": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
||
},
|
||
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 1456,
|
||
"active_time_ms": 1456
|
||
}
|
||
},
|
||
"preflight_compile": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 127963,
|
||
"active_time_ms": 127963
|
||
}
|
||
},
|
||
"start": {
|
||
"status": "succeeded",
|
||
"usage": null
|
||
},
|
||
"implement": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_stage": "implement",
|
||
"response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option<Principal>` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.",
|
||
"last_response": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; mis"
|
||
},
|
||
"notes": "Stage completed: implement",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "openai",
|
||
"model_id": "gpt-5.5"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 9898125,
|
||
"output_tokens": 51088,
|
||
"reasoning_tokens": 21958,
|
||
"cache_read_tokens": 42159616,
|
||
"cache_write_tokens": 0
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "openai"
|
||
}
|
||
},
|
||
"total_usd_micros": 72761813
|
||
},
|
||
"files_touched": [
|
||
"/home/daytona/workspace/fabro/apps/fabro-web/app/lib/test-principal.ts",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs"
|
||
],
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 4850345,
|
||
"tool_time_ms": 1392988,
|
||
"active_time_ms": 6243333
|
||
}
|
||
},
|
||
"preflight_lint": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 138598,
|
||
"active_time_ms": 138598
|
||
}
|
||
}
|
||
},
|
||
"next_node_id": "simplify_opus",
|
||
"git_commit_sha": "f12be47f707f3235a97e5bf1814f92084eeb4d45",
|
||
"node_visits": {
|
||
"preflight_lint": 1,
|
||
"preflight_compile": 1,
|
||
"toolchain": 1,
|
||
"start": 1,
|
||
"implement": 1
|
||
}
|
||
},
|
||
"diff": {
|
||
"patch": "diff --git a/apps/fabro-web/app/components/run-summary-panel.test.tsx b/apps/fabro-web/app/components/run-summary-panel.test.tsx\nindex f93a3fd8a..0a3ff3bbd 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.test.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.test.tsx\n@@ -6,6 +6,7 @@ import {\n RunSummaryPanelView,\n type RunSummaryPanelViewProps,\n } from \"./run-summary-panel\";\n+import { testPrincipal } from \"../lib/test-principal\";\n \n function instanceText(instance: TestRenderer.ReactTestInstance): string {\n const parts: string[] = [];\n@@ -53,7 +54,7 @@ function cellAfterLabel(\n function makeRun(overrides: Record<string, any> = {}) {\n return {\n id: \"run_1\",\n- created_by: null,\n+ created_by: testPrincipal(),\n diff: null,\n billing: null,\n ...overrides,\n@@ -71,9 +72,8 @@ describe(\"RunSummaryPanelView\", () => {\n }\n });\n \n- test(\"shows unavailable copy for missing run fields after load\", () => {\n+ test(\"shows unavailable copy for missing optional run fields after load\", () => {\n const tree = render({ run: makeRun() });\n- expect(instanceText(cellAfterLabel(tree, \"Created by\"))).toBe(EMPTY_VALUE);\n expect(instanceText(cellAfterLabel(tree, \"Changes\"))).toBe(EMPTY_VALUE);\n expect(instanceText(cellAfterLabel(tree, \"Cost\"))).toBe(EMPTY_VALUE);\n });\n@@ -174,7 +174,7 @@ describe(\"RunSummaryPanelView\", () => {\n kind: \"user\",\n identity: { issuer: \"github\", subject: \"1\" },\n login: \"brynary\",\n- auth_method: \"oauth\",\n+ auth_method: \"github\",\n },\n }),\n });\n@@ -188,7 +188,7 @@ describe(\"RunSummaryPanelView\", () => {\n kind: \"user\",\n identity: { issuer: \"github\", subject: \"1\" },\n login: \"brynary\",\n- auth_method: \"oauth\",\n+ auth_method: \"github\",\n avatar_url: \"https://example.com/brynary.png\",\n },\n }),\n@@ -200,7 +200,7 @@ describe(\"RunSummaryPanelView\", () => {\n });\n \n test(\"renders non-user actor with kind label\", () => {\n- for (const kind of [\"agent\", \"system\", \"slack\", \"webhook\", \"worker\", \"anonymous\"]) {\n+ for (const kind of [\"agent\", \"system\", \"slack\", \"webhook\", \"worker\"]) {\n const tree = render({ run: makeRun({ created_by: { kind } as any }) });\n expect(instanceText(cellAfterLabel(tree, \"Created by\"))).toContain(kind);\n }\ndiff --git a/apps/fabro-web/app/components/run-summary-panel.tsx b/apps/fabro-web/app/components/run-summary-panel.tsx\nindex 5d9c78ce4..8e3f6db65 100644\n--- a/apps/fabro-web/app/components/run-summary-panel.tsx\n+++ b/apps/fabro-web/app/components/run-summary-panel.tsx\n@@ -92,7 +92,7 @@ export function RunSummaryPanelView({\n artifactsCount,\n artifactsLoading,\n }: RunSummaryPanelViewProps) {\n- const created = run?.created_by ? principalDisplay(run.created_by) : null;\n+ const created = run ? principalDisplay(run.created_by) : null;\n const diff = run?.diff ?? null;\n const cost = formatUsdMicros(run?.billing?.total_usd_micros);\n \ndiff --git a/apps/fabro-web/app/components/runs-list/run-table-row.tsx b/apps/fabro-web/app/components/runs-list/run-table-row.tsx\nindex ca521fb92..6f38e2425 100644\n--- a/apps/fabro-web/app/components/runs-list/run-table-row.tsx\n+++ b/apps/fabro-web/app/components/runs-list/run-table-row.tsx\n@@ -35,6 +35,7 @@ export function RunTableRow({\n }) {\n const lifecycleLabel = listLifecycleStatusLabel(run);\n const statusDisplay = columnStatusDisplay[run.status];\n+ const createdBy = principalDisplay(run.createdBy);\n const show = (col: ToggleableColumn) => !hiddenColumns.has(col);\n \n return (\n@@ -54,14 +55,9 @@ export function RunTableRow({\n </td>\n {show(\"created_by\") && (\n <td className=\"relative z-10 w-8 whitespace-nowrap px-3 py-2.5\">\n- {run.createdBy && (() => {\n- const display = principalDisplay(run.createdBy);\n- return (\n- <Tooltip label={display.label}>\n- <span aria-label={`Created by ${display.label}`}>{display.glyph}</span>\n- </Tooltip>\n- );\n- })()}\n+ <Tooltip label={createdBy.label}>\n+ <span aria-label={`Created by ${createdBy.label}`}>{createdBy.glyph}</span>\n+ </Tooltip>\n </td>\n )}\n {show(\"repo\") && (\ndiff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts\nindex 98586b2c4..1894ffe27 100644\n--- a/apps/fabro-web/app/data/runs.test.ts\n+++ b/apps/fabro-web/app/data/runs.test.ts\n@@ -8,6 +8,7 @@ import {\n mapRunToRunItem,\n runStatusDisplay,\n } from \"./runs\";\n+import { testPrincipal } from \"../lib/test-principal\";\n \n function makeRun(overrides: Partial<Run> = {}): Run {\n return {\n@@ -17,7 +18,7 @@ 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: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts\nindex f820b24c1..e5595cf81 100644\n--- a/apps/fabro-web/app/data/runs.ts\n+++ b/apps/fabro-web/app/data/runs.ts\n@@ -40,7 +40,7 @@ export interface RunItem {\n sandboxWorkingDirectory?: string;\n sourceDirectory?: string;\n createdAt?: string;\n- createdBy?: Principal | null;\n+ createdBy: Principal;\n lastEventAt?: string;\n size?: RunSize;\n }\ndiff --git a/apps/fabro-web/app/lib/principal-display.tsx b/apps/fabro-web/app/lib/principal-display.tsx\nindex fa9d4ec16..666f0f64c 100644\n--- a/apps/fabro-web/app/lib/principal-display.tsx\n+++ b/apps/fabro-web/app/lib/principal-display.tsx\n@@ -4,7 +4,6 @@ import {\n ChatBubbleLeftEllipsisIcon,\n Cog6ToothIcon,\n CpuChipIcon,\n- QuestionMarkCircleIcon,\n ServerIcon,\n } from \"@heroicons/react/20/solid\";\n import type { Principal } from \"@qltysh/fabro-api-client\";\n@@ -57,10 +56,5 @@ export function principalDisplay(actor: Principal): PrincipalDisplay {\n return { glyph: principalIconGlyph(<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 8acde997f..2780ee2ed 100644\n--- a/apps/fabro-web/app/lib/run-actions.test.ts\n+++ b/apps/fabro-web/app/lib/run-actions.test.ts\n@@ -24,6 +24,7 @@ import {\n unarchiveRuns,\n } from \"./run-actions\";\n import { generatedAxios } from \"./api-client\";\n+import { testPrincipal } from \"./test-principal\";\n \n type StubResponseInit = {\n status: number;\n@@ -47,7 +48,7 @@ function makeRun(status: RunStatus, archived = false): Run {\n workflow: { slug: \"fix_build\", name: \"Fix Build\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: null,\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/lib/test-principal.ts b/apps/fabro-web/app/lib/test-principal.ts\nnew file mode 100644\nindex 000000000..7ecf7ad9e\n--- /dev/null\n+++ b/apps/fabro-web/app/lib/test-principal.ts\n@@ -0,0 +1,10 @@\n+import type { Principal } from \"@qltysh/fabro-api-client\";\n+\n+export function testPrincipal(login = \"test\"): Principal {\n+ return {\n+ kind: \"user\",\n+ identity: { issuer: \"fabro:test\", subject: `${login}-user` },\n+ login,\n+ auth_method: \"dev_token\",\n+ };\n+}\ndiff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts\nindex e30d38da8..08ecf0411 100644\n--- a/apps/fabro-web/app/routes/run-detail.test.ts\n+++ b/apps/fabro-web/app/routes/run-detail.test.ts\n@@ -178,6 +178,7 @@ import {\n } from \"./run-detail/lifecycle-toasts\";\n \n const { default: RunDetail } = await import(\"./run-detail\");\n+const { testPrincipal } = await import(\"../lib/test-principal\");\n mock.restore();\n type LifecycleToastState = import(\"./run-detail/lifecycle-toasts\").LifecycleToastState;\n type RunDetailActionResult = import(\"./run-detail/lifecycle-toasts\").RunDetailActionResult;\n@@ -209,7 +210,7 @@ function makeRunSummary(\n workflow: { slug: \"default\", name: \"Default\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/run-files.render.test.tsx b/apps/fabro-web/app/routes/run-files.render.test.tsx\nindex 5c9a24722..6a4678d60 100644\n--- a/apps/fabro-web/app/routes/run-files.render.test.tsx\n+++ b/apps/fabro-web/app/routes/run-files.render.test.tsx\n@@ -4,6 +4,7 @@ import TestRenderer, { act } from \"react-test-renderer\";\n import { MemoryRouter, Route, Routes } from \"react-router\";\n \n import { ToastProvider } from \"../components/toast\";\n+import { testPrincipal } from \"../lib/test-principal\";\n \n let currentFilesPayload: any = null;\n let currentCommitsPayload: any = null;\n@@ -50,7 +51,7 @@ mock.module(\"../lib/queries\", () => ({\n workflow: { slug: \"default\", name: \"Default\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"fabro\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/runs.preferences.test.tsx b/apps/fabro-web/app/routes/runs.preferences.test.tsx\nindex 648d8e35a..137a62a78 100644\n--- a/apps/fabro-web/app/routes/runs.preferences.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.preferences.test.tsx\n@@ -4,6 +4,7 @@ import { createMemoryRouter, RouterProvider } from \"react-router\";\n import type { PaginatedRunList, Run } from \"@qltysh/fabro-api-client\";\n \n import { ToastProvider } from \"../components/toast\";\n+import { testPrincipal } from \"../lib/test-principal\";\n import { setupReactTestEnv } from \"../lib/test-utils\";\n \n class MemoryStorage {\n@@ -34,7 +35,7 @@ function run(id: string, repo = \"qlty/fabro\", workflow = \"release\"): Run {\n workflow: { slug: workflow, name: workflow, graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: repo, origin_url: null, provider: \"github\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx\nindex 51483cff2..2e5f65bde 100644\n--- a/apps/fabro-web/app/routes/runs.test.tsx\n+++ b/apps/fabro-web/app/routes/runs.test.tsx\n@@ -11,6 +11,7 @@ import {\n shouldRefreshBoardForEvent,\n } from \"./runs\";\n import { summarizeBatchLifecycleAction } from \"../components/runs-list/batch-lifecycle\";\n+import { testPrincipal } from \"../lib/test-principal\";\n \n function boardRun(id: string, column: BoardColumn, questionText?: string): Run {\n const status =\n@@ -34,7 +35,7 @@ function boardRun(id: string, column: BoardColumn, questionText?: string): Run {\n workflow: { slug: \"test\", name: \"Test\", graph_name: null, node_count: 0, edge_count: 0 },\n automation: null,\n repository: { name: \"repo\", origin_url: null, provider: \"unknown\" },\n- created_by: null,\n+ created_by: testPrincipal(),\n origin: { kind: \"api\" },\n labels: {},\n lifecycle: {\ndiff --git a/docs/internal/logging-strategy.md b/docs/internal/logging-strategy.md\nindex 63f6f8f54..0a7c712e6 100644\n--- a/docs/internal/logging-strategy.md\n+++ b/docs/internal/logging-strategy.md\n@@ -118,11 +118,11 @@ Fields are key-value pairs that make events queryable. Include enough context th\n | `error` | Error value on failure |\n | `path` | File system path |\n | `duration_ms` | Elapsed time in milliseconds |\n-| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `anonymous`, etc.) |\n+| `principal_kind` | HTTP caller category (`user`, `worker`, `webhook`, `none`, etc.) |\n | `auth_status` | HTTP authentication result (`missing`, `invalid`, `expired`, `authenticated`) |\n | `idp_issuer`, `idp_subject` | Canonical user identity for authenticated user requests |\n \n-For HTTP request logs, use the request `Principal` projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`).\n+For HTTP request logs, use the request principal projection rather than hand-assembled auth strings. User identity fields are present only for `Principal::User`; worker and webhook requests use their variant-specific fields (`run_id`, `delivery_id`). Requests with no principal use `principal_kind=\"none\"`; keep `auth_status` as the field that distinguishes missing, invalid, expired, and authenticated auth state.\n \n Server auth intentionally exposes a mutable `RequestAuth` context slot for public auth routes and guard extractors such as `RequiredUser` / `RequireRunScoped` for protected routes. There is no loose `RequestPrincipal` extractor; route-facing extractors should enforce the route's auth contract while the slot supplies the final HTTP log fields.\n | `input_tokens` | Token count for LLM input |\ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex b39bd5f1a..8a49fb17a 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -7863,6 +7863,8 @@ components:\n \n RunProvenance:\n type: object\n+ required:\n+ - subject\n properties:\n server:\n oneOf:\n@@ -7873,9 +7875,7 @@ components:\n - $ref: \"#/components/schemas/RunClientProvenance\"\n - type: \"null\"\n subject:\n- oneOf:\n- - $ref: \"#/components/schemas/Principal\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/Principal\"\n \n Principal:\n oneOf:\n@@ -7885,7 +7885,6 @@ components:\n - $ref: \"#/components/schemas/PrincipalSlack\"\n - $ref: \"#/components/schemas/PrincipalAgent\"\n - $ref: \"#/components/schemas/PrincipalSystem\"\n- - $ref: \"#/components/schemas/PrincipalAnonymous\"\n discriminator:\n propertyName: kind\n mapping:\n@@ -7895,7 +7894,6 @@ components:\n slack: \"#/components/schemas/PrincipalSlack\"\n agent: \"#/components/schemas/PrincipalAgent\"\n system: \"#/components/schemas/PrincipalSystem\"\n- anonymous: \"#/components/schemas/PrincipalAnonymous\"\n \n PrincipalUser:\n type: object\n@@ -7985,15 +7983,6 @@ components:\n system_kind:\n $ref: \"#/components/schemas/SystemActorKind\"\n \n- PrincipalAnonymous:\n- type: object\n- required:\n- - kind\n- properties:\n- kind:\n- type: string\n- enum: [anonymous]\n-\n RunEvent:\n description: >\n Internal RunEvent-compatible JSON payload. The server validates this\n@@ -9121,6 +9110,7 @@ components:\n - run_id\n - settings\n - graph\n+ - provenance\n properties:\n run_id:\n type: string\n@@ -9140,9 +9130,7 @@ components:\n additionalProperties:\n type: string\n provenance:\n- oneOf:\n- - $ref: \"#/components/schemas/RunProvenance\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/RunProvenance\"\n manifest_blob:\n type: [\"string\", \"null\"]\n definition_blob:\n@@ -9454,9 +9442,7 @@ components:\n - $ref: \"#/components/schemas/RepositoryRef\"\n - type: \"null\"\n created_by:\n- oneOf:\n- - $ref: \"#/components/schemas/Principal\"\n- - type: \"null\"\n+ $ref: \"#/components/schemas/Principal\"\n origin:\n $ref: \"#/components/schemas/RunOrigin\"\n labels:\ndiff --git a/docs/public/changelog/2026-05-02.mdx b/docs/public/changelog/2026-05-02.mdx\nindex 48a501a59..2d55f8d05 100644\n--- a/docs/public/changelog/2026-05-02.mdx\n+++ b/docs/public/changelog/2026-05-02.mdx\n@@ -11,7 +11,7 @@ The dock listens to interview events and refreshes as questions arrive, so a par\n \n ## Principal attribution and auth routing\n \n-Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, agents, and anonymous actors. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from.\n+Run events and run creation now carry clearer principal information for users, workers, systems, Slack interactions, webhooks, and agents. API clients get explicit provenance objects instead of older actor-shaped fields that could lose where a run came from.\n \n This also closes attribution gaps across web, CLI, worker-token, Slack, and human-interview paths. Runs created or advanced through different surfaces now preserve who or what took the action more consistently.\n \n@@ -19,7 +19,7 @@ This also closes attribution gaps across web, CLI, worker-token, Slack, and huma\n \n <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 8b347f032..459e6bfda 100644\n--- a/lib/crates/fabro-api/Cargo.toml\n+++ b/lib/crates/fabro-api/Cargo.toml\n@@ -32,3 +32,6 @@ serde_json = \"1\"\n serde_yaml = \"0.9\"\n prettyplease = \"0.2\"\n syn = \"2\"\n+\n+[dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\ndiff --git a/lib/crates/fabro-api/tests/principal_round_trip.rs b/lib/crates/fabro-api/tests/principal_round_trip.rs\nindex ca1180e60..ac2b2c258 100644\n--- a/lib/crates/fabro-api/tests/principal_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/principal_round_trip.rs\n@@ -118,7 +118,6 @@ fn principal_round_trips_every_variant_through_api_type() {\n Principal::System {\n system_kind: SystemActorKind::Watchdog,\n },\n- Principal::Anonymous,\n ];\n \n for principal in variants {\n@@ -140,9 +139,9 @@ fn run_provenance_subject_round_trips_as_principal() {\n name: Some(\"fabro-cli\".to_string()),\n version: Some(\"0.1.0\".to_string()),\n }),\n- subject: Some(Principal::Worker {\n+ subject: Principal::Worker {\n run_id: fixtures::RUN_1,\n- }),\n+ },\n };\n let json = serde_json::to_value(&provenance).unwrap();\n \ndiff --git a/lib/crates/fabro-api/tests/run_event_round_trip.rs b/lib/crates/fabro-api/tests/run_event_round_trip.rs\nindex e39f3d8f4..8e1cf5d36 100644\n--- a/lib/crates/fabro-api/tests/run_event_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_event_round_trip.rs\n@@ -1,7 +1,7 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunEvent as ApiRunEvent;\n-use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures};\n+use fabro_types::{Graph, RunEvent, WorkflowSettings, fixtures, test_support};\n use serde_json::{Value, json};\n \n #[test]\n@@ -20,7 +20,8 @@ fn run_event_round_trips_run_created() {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/fabro/run-1\",\n- \"source_directory\": \"/tmp/fabro/run-1\"\n+ \"source_directory\": \"/tmp/fabro/run-1\",\n+ \"provenance\": test_support::test_run_provenance()\n }\n });\n \n@@ -39,6 +40,7 @@ fn run_event_round_trips_run_created_with_web_url() {\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/fabro/run-1\",\n \"source_directory\": \"/tmp/fabro/run-1\",\n+ \"provenance\": test_support::test_run_provenance(),\n \"web_url\": format!(\"http://localhost:3000/runs/{}\", fixtures::RUN_1)\n }\n });\ndiff --git a/lib/crates/fabro-api/tests/run_projection_round_trip.rs b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\nindex 64a00df91..30295d76a 100644\n--- a/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs\n@@ -1,7 +1,7 @@\n use std::any::{TypeId, type_name};\n \n use fabro_api::types::RunProjection as ApiRunProjection;\n-use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings};\n+use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, test_support};\n use serde_json::json;\n \n #[test]\n@@ -130,7 +130,7 @@ fn run_spec_json() -> serde_json::Value {\n workflow_slug: None,\n source_directory: None,\n labels: std::collections::HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-api/tests/run_summary_round_trip.rs b/lib/crates/fabro-api/tests/run_summary_round_trip.rs\nindex 21d237434..1e94c7b1b 100644\n--- a/lib/crates/fabro-api/tests/run_summary_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/run_summary_round_trip.rs\n@@ -12,7 +12,7 @@ use fabro_types::{\n AskFabro, AskFabroUnavailableReason, DiffSummary, PullRequestLink, RepositoryProvider,\n RepositoryRef, Run, RunApproval, RunApprovalState, RunBillingSummary, RunId, RunLifecycle,\n RunLinks, RunOrigin, RunRunnableSource, RunSize, RunTimestamps, RunTiming, WorkflowRef,\n- fixtures,\n+ fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -83,7 +83,7 @@ fn run_summary_json_matches_openapi_shape() {\n origin_url: None,\n provider: RepositoryProvider::Unknown,\n }),\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"team\".to_string(), \"core\".to_string())]),\n lifecycle: RunLifecycle {\n@@ -152,7 +152,15 @@ fn run_summary_json_matches_openapi_shape() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n- \"created_by\": null,\n+ \"created_by\": {\n+ \"kind\": \"user\",\n+ \"identity\": {\n+ \"issuer\": \"fabro:test\",\n+ \"subject\": \"test-user\"\n+ },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ },\n \"origin\": {\n \"kind\": \"api\"\n },\n@@ -244,6 +252,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": test_support::test_principal(),\n \"models\": [],\n \"timestamps\": {\n \"created_at\": \"2026-04-20T12:00:00Z\",\n@@ -276,6 +285,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {\n provider: RepositoryProvider::Unknown,\n })\n );\n+ assert_eq!(summary.created_by, test_support::test_principal());\n assert_eq!(summary.timestamps.started_at, None);\n assert_eq!(summary.timestamps.created_at, created_at);\n assert_eq!(summary.timestamps.last_event_at, None);\ndiff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml\nindex 4bff98f8d..9568ac22a 100644\n--- a/lib/crates/fabro-cli/Cargo.toml\n+++ b/lib/crates/fabro-cli/Cargo.toml\n@@ -129,3 +129,4 @@ fabro-test = { workspace = true }\n fabro-macros = { path = \"../fabro-macros\" }\n hkdf.workspace = true\n reqwest = { workspace = true, features = [\"cookies\"] }\n+fabro-types = { path = \"../fabro-types\", features = [\"clap\", \"test-support\"] }\ndiff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs\nindex c8ad2fff2..8dbb31b35 100644\n--- a/lib/crates/fabro-cli/src/commands/run/attach.rs\n+++ b/lib/crates/fabro-cli/src/commands/run/attach.rs\n@@ -822,6 +822,7 @@ mod tests {\n )]\n \n use fabro_interview::{Answer, AnswerValue};\n+ use fabro_types::test_support;\n use fabro_util::terminal::Styles;\n use httpmock::MockServer;\n \n@@ -840,7 +841,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\nindex ebcf1a2a0..f426c167b 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs\n@@ -221,7 +221,21 @@ fn inspect_resolves_selector_via_server_endpoint() {\n \"attrs\": {}\n },\n \"workflow_slug\": \"remote-workflow\",\n- \"source_directory\": \"/srv/repo\"\n+ \"source_directory\": \"/srv/repo\",\n+ \"provenance\": {\n+ \"server\": {\n+ \"version\": \"test\"\n+ },\n+ \"subject\": {\n+ \"kind\": \"user\",\n+ \"identity\": {\n+ \"issuer\": \"fabro:test\",\n+ \"subject\": \"test-user\"\n+ },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ }\n+ }\n },\n \"start_record\": null,\n \"conclusion\": null,\ndiff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs\nindex 9ac2e7673..d8572ac19 100644\n--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs\n+++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs\n@@ -177,6 +177,15 @@ pub(crate) fn remote_run_summary_json(\n \"origin_url\": null,\n \"provider\": \"unknown\"\n },\n+ \"created_by\": {\n+ \"kind\": \"user\",\n+ \"identity\": {\n+ \"issuer\": \"fabro:test\",\n+ \"subject\": \"test-user\"\n+ },\n+ \"login\": \"test\",\n+ \"auth_method\": \"dev_token\"\n+ },\n \"origin\": {\n \"kind\": \"api\"\n },\ndiff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs\nindex 7c7c59a22..41150f3f2 100644\n--- a/lib/crates/fabro-cli/tests/it/support/mod.rs\n+++ b/lib/crates/fabro-cli/tests/it/support/mod.rs\n@@ -9,7 +9,7 @@ pub(crate) use auth_harness::{\n pub(crate) use auth_tokens::{TEST_SESSION_SECRET, issue_test_github_jwt, issue_test_worker_jwt};\n use fabro_store::EventEnvelope;\n use fabro_test::{EnvVars, TestContext, preserve_coverage_env};\n-use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings};\n+use fabro_types::{Graph, RunId, RunSpec, WorkflowSettings, test_support};\n \n pub(crate) fn run_output_filters(context: &TestContext) -> Vec<(String, String)> {\n let mut filters = context.filters();\n@@ -48,7 +48,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s\n workflow_slug: Some(\"remote-workflow\".to_string()),\n source_directory: Some(\"/srv/repo\".to_string()),\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-dump/src/lib.rs b/lib/crates/fabro-dump/src/lib.rs\nindex d44e01215..ae3598666 100644\n--- a/lib/crates/fabro-dump/src/lib.rs\n+++ b/lib/crates/fabro-dump/src/lib.rs\n@@ -475,7 +475,7 @@ mod tests {\n use fabro_types::{\n Checkpoint, CheckpointRecord, Conclusion, RunDiff, RunSandbox, RunStatus,\n SandboxProviderKind, StageCompletion, StageModelUsage, StageOutcome, StartRecord,\n- SuccessReason, WorkflowSettings, first_event_seq, fixtures,\n+ SuccessReason, WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n use futures::executor;\n \n@@ -497,7 +497,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/crates/fabro-server/src/auth/cli_flow.rs\nindex 5e6a9f9cc..e45b933de 100644\n--- a/lib/crates/fabro-server/src/auth/cli_flow.rs\n+++ b/lib/crates/fabro-server/src/auth/cli_flow.rs\n@@ -1667,11 +1667,11 @@ client_id = \"github-client-id\"\n let [first, second, third] = <[RequestAuthContext; 3]>::try_from(contexts)\n .expect(\"expected three captured auth contexts\");\n assert_eq!(first.auth_status, AuthStatus::Authenticated);\n- assert_eq!(first.principal.display(), \"octocat\");\n+ assert_eq!(first.principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(second.auth_status, AuthStatus::Authenticated);\n- assert_eq!(second.principal.display(), \"octocat\");\n+ assert_eq!(second.principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(third.auth_status, AuthStatus::Authenticated);\n- assert_eq!(third.principal.display(), \"octocat\");\n+ assert_eq!(third.principal.as_ref().unwrap().display(), \"octocat\");\n }\n \n #[tokio::test]\n@@ -2076,7 +2076,7 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert_eq!(contexts[0].principal.display(), \"octocat\");\n+ assert_eq!(contexts[0].principal.as_ref().unwrap().display(), \"octocat\");\n assert_eq!(contexts[1].auth_status, AuthStatus::Invalid);\n assert_eq!(\n contexts[1].auth_error_code,\n@@ -2269,8 +2269,8 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert_eq!(contexts[0].principal.display(), \"octocat\");\n- let Principal::User(user) = &contexts[0].principal else {\n+ assert_eq!(contexts[0].principal.as_ref().unwrap().display(), \"octocat\");\n+ let Some(Principal::User(user)) = &contexts[0].principal else {\n panic!(\"expected user principal\");\n };\n assert_eq!(\ndiff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs\nindex 42e19a97c..b731cfd2e 100644\n--- a/lib/crates/fabro-server/src/demo/mod.rs\n+++ b/lib/crates/fabro-server/src/demo/mod.rs\n@@ -1080,7 +1080,7 @@ fn ts(s: &str) -> DateTime<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@@ -1091,13 +1091,22 @@ mod runs {\n };\n use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace};\n use fabro_types::{\n- PendingReason, RepositoryRef, RunBillingSummary, RunId, RunLifecycle, RunLinks, RunOrigin,\n- RunSize, RunTimestamps, StageId, WorkflowRef, WorkflowSettings,\n+ AuthMethod, IdpIdentity, PendingReason, Principal, RepositoryRef, RunBillingSummary, RunId,\n+ RunLifecycle, RunLinks, RunOrigin, RunSize, RunTimestamps, StageId, WorkflowRef,\n+ WorkflowSettings,\n };\n \n use super::ts;\n use crate::server::run_stage_from_stage_id;\n \n+ static DEMO_PRINCIPAL: LazyLock<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@@ -1170,7 +1179,7 @@ mod runs {\n repo_origin_url,\n source_directory.as_deref(),\n )),\n- created_by: None,\n+ created_by: DEMO_PRINCIPAL.clone(),\n origin: RunOrigin::default(),\n labels: labels(entries),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-server/src/principal_middleware.rs b/lib/crates/fabro-server/src/principal_middleware.rs\nindex acc9eb250..b1741e190 100644\n--- a/lib/crates/fabro-server/src/principal_middleware.rs\n+++ b/lib/crates/fabro-server/src/principal_middleware.rs\n@@ -19,7 +19,7 @@ use crate::worker_token::{self, WORKER_TOKEN_KID, WorkerScopeSet};\n \n #[derive(Clone, Debug)]\n pub(crate) struct RequestAuthContext {\n- pub principal: Principal,\n+ pub principal: Option<Principal>,\n pub auth_status: AuthStatus,\n pub auth_error_code: Option<AuthErrorCode>,\n pub user_profile: Option<UserProfile>,\n@@ -74,7 +74,7 @@ impl RequestAuthContext {\n #[must_use]\n pub(crate) fn initial() -> Self {\n Self {\n- principal: Principal::Anonymous,\n+ principal: None,\n auth_status: AuthStatus::Missing,\n auth_error_code: None,\n user_profile: None,\n@@ -85,7 +85,7 @@ impl RequestAuthContext {\n #[must_use]\n pub(crate) fn authenticated(principal: Principal, user_profile: Option<UserProfile>) -> Self {\n Self {\n- principal,\n+ principal: Some(principal),\n auth_status: AuthStatus::Authenticated,\n auth_error_code: None,\n user_profile,\n@@ -96,7 +96,7 @@ impl RequestAuthContext {\n #[must_use]\n pub(crate) fn authenticated_worker(run_id: RunId, scopes: WorkerScopeSet) -> Self {\n Self {\n- principal: Principal::Worker { run_id },\n+ principal: Some(Principal::Worker { run_id }),\n auth_status: AuthStatus::Authenticated,\n auth_error_code: None,\n user_profile: None,\n@@ -123,7 +123,7 @@ impl RequestAuthContext {\n #[must_use]\n pub(crate) fn rejected(status: AuthStatus, code: Option<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@@ -146,7 +146,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@@ -170,22 +170,23 @@ impl AuthContextSlot {\n pub(crate) fn log_snapshot(&self) -> RequestAuthLogContext {\n let context = self.0.lock().expect(\"auth context lock poisoned\");\n RequestAuthLogContext {\n- principal: principal_without_log_unused_fields(&context.principal),\n+ principal: principal_without_log_unused_fields(context.principal.as_ref()),\n auth_status: context.auth_status,\n auth_error_code: context.auth_error_code,\n }\n }\n }\n \n-fn principal_without_log_unused_fields(principal: &Principal) -> Principal {\n+fn principal_without_log_unused_fields(principal: Option<&Principal>) -> Option<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+ Some(principal) => Some(principal.clone()),\n+ None => None,\n }\n }\n \n@@ -370,7 +371,7 @@ fn auth_slot_from_parts(parts: &Parts) -> AuthContextSlot {\n pub(crate) fn require_user(slot: &AuthContextSlot) -> Result<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@@ -380,7 +381,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@@ -396,11 +397,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@@ -411,9 +412,9 @@ fn require_worker_or_user_for_run(\n ) -> Result<(), ApiError> {\n let context = slot.0.lock().expect(\"auth context lock poisoned\");\n match &context.principal {\n- Principal::User(_) => Ok(()),\n- Principal::Worker { run_id } if run_id == route_run_id => Ok(()),\n- Principal::Worker { .. } => Err(ApiError::forbidden()),\n+ Some(Principal::User(_)) => Ok(()),\n+ Some(Principal::Worker { run_id }) if run_id == route_run_id => Ok(()),\n+ Some(Principal::Worker { .. }) => Err(ApiError::forbidden()),\n _ => Err(auth_rejection(context.auth_status, context.auth_error_code)),\n }\n }\n@@ -424,13 +425,13 @@ fn require_run_management_target(\n ) -> Result<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@@ -646,7 +647,7 @@ mod tests {\n let context = classify_request(&request, state.as_ref());\n \n assert_eq!(context.auth_status, AuthStatus::Authenticated);\n- assert!(matches!(context.principal, Principal::User(_)));\n+ assert!(matches!(context.principal, Some(Principal::User(_))));\n assert!(context.user_profile.is_some());\n }\n \n@@ -686,7 +687,7 @@ mod tests {\n let context = classify_request(&request, state.as_ref());\n \n assert_eq!(context.auth_status, AuthStatus::Authenticated);\n- assert_eq!(context.principal, Principal::Worker { run_id });\n+ assert_eq!(context.principal, Some(Principal::Worker { run_id }));\n assert!(!context.worker_scopes.has_agent_run_tools());\n }\n \n@@ -705,7 +706,7 @@ mod tests {\n let context = classify_request(&request, state.as_ref());\n \n assert_eq!(context.auth_status, AuthStatus::Authenticated);\n- assert_eq!(context.principal, Principal::Worker { run_id });\n+ assert_eq!(context.principal, Some(Principal::Worker { run_id }));\n assert!(context.worker_scopes.has_agent_run_tools());\n }\n \n@@ -784,7 +785,7 @@ mod tests {\n \n assert_eq!(context.auth_status, AuthStatus::Missing);\n assert_eq!(context.auth_error_code, None);\n- assert_eq!(context.principal, Principal::Anonymous);\n+ assert_eq!(context.principal, None);\n }\n \n #[test]\ndiff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/crates/fabro-server/src/run_files.rs\nindex 53657282a..082f13025 100644\n--- a/lib/crates/fabro-server/src/run_files.rs\n+++ b/lib/crates/fabro-server/src/run_files.rs\n@@ -1715,7 +1715,7 @@ fn count_flags(data: &[FileDiff]) -> (u64, u64, u64, u64) {\n mod tests {\n use std::sync::atomic::{AtomicUsize, Ordering};\n \n- use fabro_types::{CommandTermination, RunId};\n+ use fabro_types::{CommandTermination, RunId, test_support};\n use tokio::time::{Duration, sleep};\n \n use super::*;\n@@ -2386,7 +2386,7 @@ index 1111111..2222222 160000\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs\nindex a004872cb..1760d76cf 100644\n--- a/lib/crates/fabro-server/src/run_manifest.rs\n+++ b/lib/crates/fabro-server/src/run_manifest.rs\n@@ -26,7 +26,9 @@ use fabro_static::EnvVars;\n use fabro_types::settings::cli::OutputVerbosity;\n use fabro_types::settings::interp::InterpString;\n use fabro_types::settings::run::{EnvironmentProvider, RunGoal, RunNamespace};\n-use fabro_types::{ManifestPath, RunId, SandboxProviderKind, ServerSettings, WorkflowSettings};\n+use fabro_types::{\n+ ManifestPath, RunId, RunProvenance, SandboxProviderKind, ServerSettings, WorkflowSettings,\n+};\n use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};\n use fabro_validate::Severity;\n use fabro_workflow::Error as WorkflowError;\n@@ -201,6 +203,7 @@ pub(crate) fn validate_prepared_manifest(\n pub(crate) fn create_run_input(\n prepared: PreparedManifest,\n configured_providers: Vec<ProviderId>,\n+ provenance: RunProvenance,\n web_url: Option<String>,\n ) -> CreateRunInput {\n CreateRunInput {\n@@ -216,7 +219,7 @@ pub(crate) fn create_run_input(\n git: prepared.git,\n fork_source_ref: None,\n parent_id: prepared.parent_id,\n- provenance: None,\n+ provenance,\n configured_providers,\n web_url,\n }\ndiff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs\nindex b0bb8432c..25316c66a 100644\n--- a/lib/crates/fabro-server/src/server.rs\n+++ b/lib/crates/fabro-server/src/server.rs\n@@ -1764,7 +1764,10 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n let status = response.status().as_u16();\n let latency_ms = start.elapsed().as_millis();\n let auth_context = auth_slot.log_snapshot();\n- let principal_kind = auth_context.principal.kind();\n+ let principal_kind = auth_context\n+ .principal\n+ .as_ref()\n+ .map_or(\"none\", Principal::kind);\n let auth_status = auth_context.auth_status.as_str();\n \n macro_rules! emit_http_log {\n@@ -1802,27 +1805,27 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n macro_rules! emit_principal_http_log {\n ($level:ident) => {{\n match &auth_context.principal {\n- Principal::User(user) => emit_http_log!(\n+ Some(Principal::User(user)) => emit_http_log!(\n $level,\n user_auth_method = user.auth_method.as_str(),\n idp_issuer = user.identity.issuer(),\n idp_subject = user.identity.subject(),\n login = user.login.as_str(),\n ),\n- Principal::Worker { run_id } => {\n+ Some(Principal::Worker { run_id }) => {\n emit_http_log!($level, run_id = run_id.to_string().as_str(),)\n }\n- Principal::Webhook { delivery_id } => {\n+ Some(Principal::Webhook { delivery_id }) => {\n emit_http_log!($level, delivery_id = delivery_id.as_str(),)\n }\n- Principal::Slack {\n+ Some(Principal::Slack {\n team_id, user_id, ..\n- } => emit_http_log!(\n+ }) => emit_http_log!(\n $level,\n team_id = team_id.as_str(),\n user_id = user_id.as_str(),\n ),\n- Principal::Agent { .. } | Principal::System { .. } | Principal::Anonymous => {\n+ None | Some(Principal::Agent { .. } | Principal::System { .. }) => {\n emit_http_log!($level)\n }\n }\ndiff --git a/lib/crates/fabro-server/src/server/handler/events.rs b/lib/crates/fabro-server/src/server/handler/events.rs\nindex 8e7806522..562e3d5fc 100644\n--- a/lib/crates/fabro-server/src/server/handler/events.rs\n+++ b/lib/crates/fabro-server/src/server/handler/events.rs\n@@ -535,7 +535,7 @@ mod stage_events_tests {\n use axum::body::{Body, to_bytes};\n use axum::http::{Request, StatusCode, header};\n use fabro_store::EventPayload;\n- use fabro_types::{Graph, RunId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, WorkflowSettings, test_support};\n use fabro_workflow::event as workflow_event;\n use http_body_util::BodyExt;\n use serde_json::json;\n@@ -569,7 +569,7 @@ mod stage_events_tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/server/handler/lifecycle.rs b/lib/crates/fabro-server/src/server/handler/lifecycle.rs\nindex 0c2697133..2d55e9b33 100644\n--- a/lib/crates/fabro-server/src/server/handler/lifecycle.rs\n+++ b/lib/crates/fabro-server/src/server/handler/lifecycle.rs\n@@ -864,7 +864,7 @@ async fn retry_run(\n let input = operations::RetryRunInput {\n source_run_id: id,\n new_run_id,\n- provenance: Some(run_provenance(&headers, &actor)),\n+ provenance: run_provenance(&headers, &actor),\n web_url: state.run_web_url(&new_run_id),\n };\n match Box::pin(operations::retry_run(&state.store, &input)).await {\ndiff --git a/lib/crates/fabro-server/src/server/handler/pair.rs b/lib/crates/fabro-server/src/server/handler/pair.rs\nindex e43f4e6f8..ea64e05ab 100644\n--- a/lib/crates/fabro-server/src/server/handler/pair.rs\n+++ b/lib/crates/fabro-server/src/server/handler/pair.rs\n@@ -850,7 +850,7 @@ mod tests {\n use fabro_types::run_event::AgentMessageProps;\n use fabro_types::{\n BilledTokenCounts, EventEnvelope, Graph, PairMessageId, RunEvent, StageId,\n- WorkflowSettings, fixtures,\n+ WorkflowSettings, fixtures, test_support,\n };\n use fabro_workflow::event as workflow_event;\n use tower::ServiceExt;\n@@ -1023,7 +1023,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs\nindex 090d1ba5d..e581c8048 100644\n--- a/lib/crates/fabro-server/src/server/handler/runs.rs\n+++ b/lib/crates/fabro-server/src/server/handler/runs.rs\n@@ -641,13 +641,14 @@ async fn create_run(\n .as_ref()\n .map(LlmClientResult::provider_ids)\n .unwrap_or_default();\n+ let provenance = run_provenance(&headers, &actor);\n let mut create_input = run_manifest::create_run_input(\n prepared.clone(),\n ready_provider_ids.clone(),\n+ provenance,\n web_url.clone(),\n );\n create_input.run_id = Some(run_id);\n- create_input.provenance = Some(run_provenance(&headers, &actor));\n create_input.submitted_manifest_bytes = Some(body.to_vec());\n \n let storage_root = match resolve_interp_string(&state.server_settings().server.storage.root) {\n@@ -817,7 +818,7 @@ pub(super) fn run_provenance(headers: &HeaderMap, subject: &Principal) -> RunPro\n version: FABRO_VERSION.to_string(),\n }),\n client: run_client_provenance(headers),\n- subject: Some(subject.clone()),\n+ subject: subject.clone(),\n }\n }\n \ndiff --git a/lib/crates/fabro-server/src/server/handler/sandbox.rs b/lib/crates/fabro-server/src/server/handler/sandbox.rs\nindex bfba9c242..be7d22023 100644\n--- a/lib/crates/fabro-server/src/server/handler/sandbox.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs\n@@ -1307,7 +1307,7 @@ FABRO_PROC_NET_TCP /proc/net/tcp6\n mod retrieve_sandbox_tests {\n use axum::body::{Body, to_bytes};\n use axum::http::{Request, StatusCode};\n- use fabro_types::{Graph, RunId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, WorkflowSettings, test_support};\n use serde_json::{Value, json};\n use tower::ServiceExt;\n \n@@ -1347,6 +1347,7 @@ mod retrieve_sandbox_tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": test_support::test_run_provenance(),\n },\n }),\n run_id,\ndiff --git a/lib/crates/fabro-server/src/server/handler/sessions.rs b/lib/crates/fabro-server/src/server/handler/sessions.rs\nindex 1791ee57b..5aa30cfed 100644\n--- a/lib/crates/fabro-server/src/server/handler/sessions.rs\n+++ b/lib/crates/fabro-server/src/server/handler/sessions.rs\n@@ -1508,6 +1508,7 @@ mod tests {\n use fabro_agent::config::ToolAccess;\n use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource};\n use fabro_llm::types::{ToolCall, ToolDefinition};\n+ use fabro_types::test_support;\n \n use super::*;\n \n@@ -1701,7 +1702,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs\nindex ba4520d87..62d4331c1 100644\n--- a/lib/crates/fabro-server/src/server/tests.rs\n+++ b/lib/crates/fabro-server/src/server/tests.rs\n@@ -24,7 +24,7 @@ use fabro_types::{\n SandboxProviderKind, StageContextWindowBreakdownItem, StageContextWindowCategory,\n StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness,\n StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind,\n- WorkflowSettings, fixtures,\n+ WorkflowSettings, fixtures, test_support,\n };\n use fabro_util::check_report::CheckStatus;\n use fabro_workflow::records::CheckpointExt;\n@@ -3367,7 +3367,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -3412,7 +3412,7 @@ async fn create_slack_notification_run(\n source_directory: None,\n workflow_slug: workflow_slug.map(str::to_string),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -4418,7 +4418,7 @@ async fn list_run_stages_distinguishes_visits() {\n source_directory: None,\n workflow_slug: Some(\"test\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -5470,7 +5470,7 @@ async fn create_completed_run_ready_for_pull_request(\n source_directory: Some(\"/tmp/project\".to_string()),\n git: git.clone(),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -9272,15 +9272,10 @@ async fn run_tool_worker_token_can_use_client_backend_routes_across_runs() {\n .unwrap()\n .expect(\"created run should be cached\");\n assert_eq!(\n- cached\n- .projection\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.as_ref()),\n- Some(&Principal::Worker {\n+ cached.projection.spec.provenance.subject,\n+ Principal::Worker {\n run_id: parent_run_id,\n- }),\n+ },\n );\n \n let response = app\n@@ -11604,7 +11599,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc<AppState>, run_id: RunId\n source_directory: Some(\"/tmp/fabro-run\".to_string()),\n workflow_slug: Some(\"test\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -12353,7 +12348,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {\n source_directory: Some(\"/tmp/fabro-run\".to_string()),\n workflow_slug: Some(\"test\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs\nindex 5f10aba10..9ecd7b68a 100644\n--- a/lib/crates/fabro-server/src/web_auth.rs\n+++ b/lib/crates/fabro-server/src/web_auth.rs\n@@ -1365,7 +1365,7 @@ client_id = \"github-client-id\"\n \n let contexts = captured.lock().expect(\"captured auth contexts\").clone();\n assert_eq!(contexts[0].auth_status, AuthStatus::Authenticated);\n- assert!(matches!(contexts[0].principal, Principal::User(_)));\n+ assert!(matches!(contexts[0].principal, Some(Principal::User(_))));\n assert_eq!(contexts[1].auth_status, AuthStatus::Invalid);\n assert_eq!(\n contexts[1].auth_error_code,\ndiff --git a/lib/crates/fabro-server/tests/it/api/run_files.rs b/lib/crates/fabro-server/tests/it/api/run_files.rs\nindex e820b00f9..5af21eb92 100644\n--- a/lib/crates/fabro-server/tests/it/api/run_files.rs\n+++ b/lib/crates/fabro-server/tests/it/api/run_files.rs\n@@ -14,7 +14,7 @@ use axum::body::Body;\n use axum::http::{Request, StatusCode};\n use fabro_server::test_support::test_app_state_with_store;\n use fabro_store::{ArtifactStore, Database};\n-use fabro_types::{Graph, RunId, WorkflowSettings};\n+use fabro_types::{Graph, RunId, WorkflowSettings, test_support};\n use fabro_workflow::event as workflow_event;\n use fabro_workflow::run_status::SuccessReason;\n use object_store::memory::InMemory as MemoryObjectStore;\n@@ -68,7 +68,7 @@ async fn append_completed_run_with_final_patch(\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-store/Cargo.toml b/lib/crates/fabro-store/Cargo.toml\nindex 016b55d3d..860ffbf7c 100644\n--- a/lib/crates/fabro-store/Cargo.toml\n+++ b/lib/crates/fabro-store/Cargo.toml\n@@ -36,3 +36,4 @@ tokio = { workspace = true, features = [\"test-util\", \"macros\"] }\n tempfile = \"3\"\n ulid.workspace = true\n insta = { workspace = true }\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\ndiff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs\nindex 552891d2e..619e12fb4 100644\n--- a/lib/crates/fabro-store/src/run_state.rs\n+++ b/lib/crates/fabro-store/src/run_state.rs\n@@ -902,11 +902,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run {\n })\n .map(|(_, record)| record.question.clone());\n let models = run_models(state);\n- let created_by = state\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.clone());\n+ let created_by = state.spec.provenance.subject.clone();\n let source_directory = state.spec.source_directory.clone();\n let repo_origin_url = state.spec.git.as_ref().map(|git| git.origin_url.clone());\n let start_time = state.start.as_ref().map(|start| start.start_time);\n@@ -1255,7 +1251,7 @@ mod tests {\n StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod,\n StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning,\n StageModelUsage, StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings,\n- first_event_seq, fixtures,\n+ first_event_seq, fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -1336,7 +1332,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1372,7 +1368,7 @@ mod tests {\n }\n \n #[test]\n- fn legacy_run_created_projects_retried_from_none() {\n+ fn run_created_projects_retried_from_none() {\n let event = test_raw_event(\n 1,\n \"run.created\",\n@@ -1380,7 +1376,8 @@ mod tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }),\n None,\n );\n@@ -1577,7 +1574,7 @@ mod tests {\n \"repo_origin_url\": null,\n \"base_branch\": null,\n \"labels\": {},\n- \"provenance\": null,\n+ \"provenance\": test_support::test_run_provenance(),\n \"manifest_blob\": null,\n \"definition_blob\": null,\n \"git\": null,\n@@ -2605,7 +2602,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2630,7 +2627,7 @@ mod tests {\n source_directory: Some(\"/tmp/repo\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -2669,7 +2666,8 @@ mod tests {\n \"attrs\": { \"goal\": { \"String\": \"Goal title\" } }\n },\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }),\n None,\n );\n@@ -2683,7 +2681,7 @@ mod tests {\n }\n \n #[test]\n- fn legacy_run_created_without_title_infers_projection_title() {\n+ fn run_created_without_title_infers_projection_title() {\n let event = test_raw_event(\n 1,\n \"run.created\",\n@@ -2696,7 +2694,8 @@ mod tests {\n \"attrs\": { \"goal\": { \"String\": \"## Plan: Legacy title\\n\\nDetails\" } }\n },\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }),\n None,\n );\n@@ -2725,7 +2724,8 @@ mod tests {\n \"attrs\": { \"goal\": { \"String\": \"Goal title\" } }\n },\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }),\n None,\n ),\n@@ -2769,6 +2769,7 @@ mod tests {\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance(),\n \"manifest_blob\": manifest_blob\n }\n }))\ndiff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs\nindex 784868ace..4f1432319 100644\n--- a/lib/crates/fabro-store/src/slate/mod.rs\n+++ b/lib/crates/fabro-store/src/slate/mod.rs\n@@ -472,7 +472,7 @@ mod tests {\n use chrono::{DateTime, Utc};\n use fabro_types::{\n AttrValue, FailureReason, Graph, RunControlAction, RunSpec, RunStatus, StageId,\n- SuccessReason, WorkflowSettings,\n+ SuccessReason, WorkflowSettings, test_support,\n };\n use futures::TryStreamExt;\n use object_store::memory::InMemory;\n@@ -541,7 +541,7 @@ mod tests {\n workflow_slug: Some(\"night-sky\".to_string()),\n source_directory: Some(format!(\"/tmp/{label}\")),\n labels: std::collections::HashMap::from([(\"team\".to_string(), \"infra\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\n@@ -600,6 +600,7 @@ mod tests {\n \"run_dir\": format!(\"/tmp/{label}\"),\n \"git\": run_spec.git,\n \"labels\": run_spec.labels,\n+ \"provenance\": run_spec.provenance,\n }),\n ))\n .await\n@@ -625,6 +626,7 @@ mod tests {\n \"run_dir\": format!(\"/tmp/{label}\"),\n \"git\": run_spec.git,\n \"labels\": run_spec.labels,\n+ \"provenance\": run_spec.provenance,\n \"parent_id\": parent_id,\n }),\n ))\n@@ -1299,6 +1301,7 @@ mod tests {\n \"run_dir\": \"/tmp/run-2\",\n \"git\": run_spec[\"git\"],\n \"labels\": run_spec[\"labels\"],\n+ \"provenance\": run_spec[\"provenance\"],\n },\n }))\n .unwrap(),\ndiff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs\nindex 1718cb662..94116fa94 100644\n--- a/lib/crates/fabro-store/src/slate/run_store.rs\n+++ b/lib/crates/fabro-store/src/slate/run_store.rs\n@@ -667,7 +667,7 @@ mod tests {\n use std::sync::Arc;\n use std::time::Duration;\n \n- use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings};\n+ use fabro_types::{Graph, RunId, SessionId, StageId, WorkflowSettings, test_support};\n use object_store::memory::InMemory;\n use serde_json::json;\n \n@@ -723,6 +723,7 @@ mod tests {\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"test\"),\n \"run_dir\": \"/tmp/test\",\n+ \"provenance\": test_support::test_run_provenance(),\n },\n }),\n run_id,\ndiff --git a/lib/crates/fabro-store/tests/serializable_projection.rs b/lib/crates/fabro-store/tests/serializable_projection.rs\nindex f6aa256fd..12ee8c932 100644\n--- a/lib/crates/fabro-store/tests/serializable_projection.rs\n+++ b/lib/crates/fabro-store/tests/serializable_projection.rs\n@@ -8,7 +8,7 @@ use fabro_types::{\n BilledModelUsage, BilledTokenCounts, Checkpoint, CheckpointRecord, InterviewQuestionRecord,\n QuestionType, RunDiff, RunSandbox, RunSandboxRuntime, RunStatus, SandboxProviderKind,\n StageCompletion, StageModelUsage, StageOutcome, StartRecord, WorkflowSettings, first_event_seq,\n- fixtures,\n+ fixtures, test_support,\n };\n use serde_json::json;\n \n@@ -21,7 +21,7 @@ fn sample_run_spec() -> RunSpec {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/tmp/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(fabro_types::GitContext {\ndiff --git a/lib/crates/fabro-tool/Cargo.toml b/lib/crates/fabro-tool/Cargo.toml\nindex 08b6df7cf..c50ea6a38 100644\n--- a/lib/crates/fabro-tool/Cargo.toml\n+++ b/lib/crates/fabro-tool/Cargo.toml\n@@ -29,4 +29,5 @@ tokio.workspace = true\n toml.workspace = true\n \n [dev-dependencies]\n+fabro-types = { path = \"../fabro-types\", features = [\"test-support\"] }\n tempfile = \"3\"\ndiff --git a/lib/crates/fabro-tool/src/common.rs b/lib/crates/fabro-tool/src/common.rs\nindex 9dd64599a..159ec426d 100644\n--- a/lib/crates/fabro-tool/src/common.rs\n+++ b/lib/crates/fabro-tool/src/common.rs\n@@ -307,7 +307,9 @@ fn format_tool_error(err: &anyhow::Error) -> String {\n #[cfg(test)]\n mod tests {\n use chrono::{TimeZone, Utc};\n- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n+ use fabro_types::{\n+ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support,\n+ };\n \n use super::*;\n \n@@ -413,7 +415,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/create.rs b/lib/crates/fabro-tool/src/create.rs\nindex 8488f8f0b..57e41acd5 100644\n--- a/lib/crates/fabro-tool/src/create.rs\n+++ b/lib/crates/fabro-tool/src/create.rs\n@@ -508,7 +508,7 @@ mod tests {\n use fabro_api::types;\n use fabro_types::{\n EventEnvelope, Run, RunLifecycle, RunLinks, RunOrigin, RunProjection, RunStatus,\n- RunTimestamps, WorkflowRef,\n+ RunTimestamps, WorkflowRef, test_support,\n };\n use schemars::SchemaGenerator;\n use serde_json::json;\n@@ -902,7 +902,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/interact.rs b/lib/crates/fabro-tool/src/interact.rs\nindex 34023120d..503147ece 100644\n--- a/lib/crates/fabro-tool/src/interact.rs\n+++ b/lib/crates/fabro-tool/src/interact.rs\n@@ -453,7 +453,7 @@ mod tests {\n use chrono::{TimeZone, Utc};\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin, RunProjection,\n- RunStatus, RunTimestamps, WorkflowRef,\n+ RunStatus, RunTimestamps, WorkflowRef, test_support,\n };\n use serde_json::json;\n \n@@ -690,7 +690,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-tool/src/search.rs b/lib/crates/fabro-tool/src/search.rs\nindex 0df7e391a..e3d0162c6 100644\n--- a/lib/crates/fabro-tool/src/search.rs\n+++ b/lib/crates/fabro-tool/src/search.rs\n@@ -293,7 +293,9 @@ mod tests {\n use std::collections::HashMap;\n \n use chrono::{TimeZone, Utc};\n- use fabro_types::{RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef};\n+ use fabro_types::{\n+ RunLifecycle, RunLinks, RunOrigin, RunStatus, RunTimestamps, WorkflowRef, test_support,\n+ };\n \n use super::*;\n \n@@ -444,7 +446,7 @@ mod tests {\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::from([(\"group\".to_string(), group.to_string())]),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs\nindex 318f982ad..339d1a0dc 100644\n--- a/lib/crates/fabro-types/src/lib.rs\n+++ b/lib/crates/fabro-types/src/lib.rs\n@@ -44,6 +44,8 @@ pub mod start;\n pub mod status;\n pub mod steering;\n pub mod system_integrations;\n+#[cfg(any(test, feature = \"test-support\"))]\n+pub mod test_support;\n pub mod timing;\n pub mod todo;\n pub mod transcript;\ndiff --git a/lib/crates/fabro-types/src/principal.rs b/lib/crates/fabro-types/src/principal.rs\nindex 2c0ff3807..4f1962ea2 100644\n--- a/lib/crates/fabro-types/src/principal.rs\n+++ b/lib/crates/fabro-types/src/principal.rs\n@@ -39,7 +39,6 @@ pub enum Principal {\n System {\n system_kind: SystemActorKind,\n },\n- Anonymous,\n }\n \n #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, IntoStaticStr)]\n@@ -97,7 +96,6 @@ impl Principal {\n Self::Slack { .. } => \"slack\",\n Self::Agent { .. } => \"agent\",\n Self::System { .. } => \"system\",\n- Self::Anonymous => \"anonymous\",\n }\n }\n \n@@ -123,7 +121,6 @@ impl Principal {\n } => session_id.clone(),\n Self::Agent { .. } => \"agent\".to_string(),\n Self::System { system_kind } => format!(\"system:{system_kind}\"),\n- Self::Anonymous => \"anonymous\".to_string(),\n }\n }\n }\n@@ -291,11 +288,6 @@ mod tests {\n });\n }\n \n- #[test]\n- fn round_trips_anonymous_variant() {\n- assert_round_trip(&Principal::Anonymous);\n- }\n-\n #[test]\n fn auth_method_as_str_matches_serde() {\n assert_eq!(AuthMethod::Github.as_str(), \"github\");\ndiff --git a/lib/crates/fabro-types/src/run.rs b/lib/crates/fabro-types/src/run.rs\nindex 269db43ee..afb2a1f24 100644\n--- a/lib/crates/fabro-types/src/run.rs\n+++ b/lib/crates/fabro-types/src/run.rs\n@@ -23,14 +23,13 @@ pub struct RunClientProvenance {\n pub version: Option<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@@ -90,8 +89,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 01ed04f4d..e57b5ed0a 100644\n--- a/lib/crates/fabro-types/src/run_event/mod.rs\n+++ b/lib/crates/fabro-types/src/run_event/mod.rs\n@@ -961,7 +961,7 @@ mod tests {\n use super::*;\n use crate::{\n AuthMethod, Edge, Graph, IdpIdentity, Node, PendingReason, RunBlobId, WorkflowSettings,\n- fixtures,\n+ fixtures, test_support,\n };\n \n fn user_principal(login: &str) -> Principal {\n@@ -1045,7 +1045,8 @@ mod tests {\n \"graph\": graph,\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n- \"source_directory\": \"/tmp/run\"\n+ \"source_directory\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance()\n }\n });\n \n@@ -1066,6 +1067,7 @@ mod tests {\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n \"source_directory\": \"/tmp/run\",\n+ \"provenance\": test_support::test_run_provenance(),\n \"manifest_blob\": RunBlobId::new(br#\"{\"version\":1}\"#).to_string()\n }\n });\ndiff --git a/lib/crates/fabro-types/src/run_event/run.rs b/lib/crates/fabro-types/src/run_event/run.rs\nindex fa189171f..be16946e2 100644\n--- a/lib/crates/fabro-types/src/run_event/run.rs\n+++ b/lib/crates/fabro-types/src/run_event/run.rs\n@@ -28,8 +28,7 @@ pub struct RunCreatedProps {\n pub workflow_slug: Option<String>,\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 3bba6d43e..b667f4fc8 100644\n--- a/lib/crates/fabro-types/src/run_projection.rs\n+++ b/lib/crates/fabro-types/src/run_projection.rs\n@@ -680,7 +680,7 @@ mod title_tests {\n \n use chrono::Utc;\n \n- use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings};\n+ use crate::{AttrValue, Graph, RunId, RunProjection, RunSpec, WorkflowSettings, test_support};\n \n fn projection_with_goal(goal: Option<&str>) -> RunProjection {\n let mut graph = Graph::new(\"test\");\n@@ -698,7 +698,7 @@ mod title_tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -750,7 +750,7 @@ mod iter_stages_tests {\n use serde_json::json;\n \n use super::RunProjection;\n- use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings};\n+ use crate::{Graph, RunId, RunSpec, StageProjection, WorkflowSettings, test_support};\n \n fn seq(n: u32) -> NonZeroU32 {\n NonZeroU32::new(n).unwrap()\n@@ -767,7 +767,7 @@ mod iter_stages_tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs\nindex 1cde31614..509303d3c 100644\n--- a/lib/crates/fabro-types/src/run_summary.rs\n+++ b/lib/crates/fabro-types/src/run_summary.rs\n@@ -52,8 +52,7 @@ pub struct Run {\n pub automation: Option<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..9db7eff91\n--- /dev/null\n+++ b/lib/crates/fabro-types/src/test_support.rs\n@@ -0,0 +1,21 @@\n+use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance, RunServerProvenance};\n+\n+#[must_use]\n+pub fn test_principal() -> Principal {\n+ Principal::user(\n+ IdpIdentity::new(\"fabro:test\", \"test-user\").expect(\"test identity should be valid\"),\n+ \"test\".to_string(),\n+ AuthMethod::DevToken,\n+ )\n+}\n+\n+#[must_use]\n+pub fn test_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: Some(RunServerProvenance {\n+ version: \"test\".to_string(),\n+ }),\n+ client: None,\n+ subject: test_principal(),\n+ }\n+}\ndiff --git a/lib/crates/fabro-types/tests/run_event_serde.rs b/lib/crates/fabro-types/tests/run_event_serde.rs\nindex 8f2df1972..533d21f15 100644\n--- a/lib/crates/fabro-types/tests/run_event_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_event_serde.rs\n@@ -8,6 +8,16 @@ use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n use fabro_types::{EventBody, TurnId, WorkflowSettings, fixtures};\n \n+fn test_run_provenance() -> fabro_types::RunProvenance {\n+ fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ }\n+}\n+\n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n settings.run.goal = Some(RunGoal::Inline(InterpString::parse(\"Ship {{ env.TASK }}\")));\n@@ -27,7 +37,7 @@ fn run_created_props_round_trip_templated_settings() {\n source_directory: Some(\"/Users/client/project\".to_string()),\n workflow_slug: Some(\"demo\".to_string()),\n db_prefix: Some(\"run_\".to_string()),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n git: Some(GitContext {\n origin_url: \"https://github.com/fabro-sh/fabro.git\".to_string(),\n@@ -89,7 +99,7 @@ fn run_created_props_omits_web_url_when_absent() {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -120,17 +130,17 @@ fn run_created_props_omits_web_url_when_absent() {\n }\n \n #[test]\n-fn run_created_props_defaults_retried_from_for_legacy_events() {\n+fn run_created_props_defaults_retried_from_when_absent() {\n let json = serde_json::json!({\n \"title\": null,\n \"settings\": WorkflowSettings::default(),\n \"graph\": Graph::new(\"ship\"),\n \"labels\": {},\n- \"run_dir\": \"/tmp/run\"\n+ \"run_dir\": \"/tmp/run\",\n+ \"provenance\": test_run_provenance()\n });\n \n- let props: RunCreatedProps =\n- serde_json::from_value(json).expect(\"legacy props should deserialize\");\n+ let props: RunCreatedProps = serde_json::from_value(json).expect(\"props should deserialize\");\n assert_eq!(props.retried_from, None);\n }\n \ndiff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs\nindex f5e8cf25f..ef29ce763 100644\n--- a/lib/crates/fabro-types/tests/run_spec_methods.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_methods.rs\n@@ -5,6 +5,16 @@ use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec};\n use fabro_types::settings::{ProjectNamespace, WorkflowNamespace};\n use fabro_types::{WorkflowSettings, fixtures};\n \n+fn test_run_provenance() -> fabro_types::RunProvenance {\n+ fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ }\n+}\n+\n fn sample_run_spec() -> RunSpec {\n let settings = WorkflowSettings {\n project: ProjectNamespace {\n@@ -26,7 +36,7 @@ fn sample_run_spec() -> RunSpec {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\ndiff --git a/lib/crates/fabro-types/tests/run_spec_serde.rs b/lib/crates/fabro-types/tests/run_spec_serde.rs\nindex f6278ff34..d45c56ac8 100644\n--- a/lib/crates/fabro-types/tests/run_spec_serde.rs\n+++ b/lib/crates/fabro-types/tests/run_spec_serde.rs\n@@ -6,6 +6,16 @@ use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunGoal;\n use fabro_types::{WorkflowSettings, fixtures};\n \n+fn test_run_provenance() -> fabro_types::RunProvenance {\n+ fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ }\n+}\n+\n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n settings.run.goal = Some(RunGoal::Inline(InterpString::parse(\"Ship {{ env.TASK }}\")));\n@@ -22,7 +32,7 @@ fn run_spec_round_trips_templated_settings() {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: None,\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\ndiff --git a/lib/crates/fabro-workflow/src/billing_rollup.rs b/lib/crates/fabro-workflow/src/billing_rollup.rs\nindex feceb0e12..e6fb39136 100644\n--- a/lib/crates/fabro-workflow/src/billing_rollup.rs\n+++ b/lib/crates/fabro-workflow/src/billing_rollup.rs\n@@ -165,7 +165,7 @@ mod tests {\n use fabro_model::{Catalog, ModelRef, ProviderId};\n use fabro_types::{\n AttrValue, BilledTokenCounts, Graph, Node, RunProjection, RunSpec, StageCompletion,\n- StageOutcome, WorkflowSettings, first_event_seq, fixtures,\n+ StageOutcome, WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n \n use super::billing_rollup_from_projection;\n@@ -352,7 +352,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs\nindex 684af5164..17eb5d157 100644\n--- a/lib/crates/fabro-workflow/src/event/convert.rs\n+++ b/lib/crates/fabro-workflow/src/event/convert.rs\n@@ -2403,28 +2403,28 @@ mod tests {\n let provenance = RunProvenance {\n server: None,\n client: None,\n- subject: Some(user_principal(\"alice\")),\n+ subject: user_principal(\"alice\"),\n };\n \n let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated {\n- run_id: fixtures::RUN_1,\n- title: None,\n- settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),\n- graph: serde_json::to_value(Graph::new(\"test\")).unwrap(),\n- workflow_source: None,\n- workflow_config: None,\n- labels: BTreeMap::default(),\n- run_dir: \"/tmp/run\".to_string(),\n+ run_id: fixtures::RUN_1,\n+ title: None,\n+ settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),\n+ graph: serde_json::to_value(Graph::new(\"test\")).unwrap(),\n+ workflow_source: None,\n+ workflow_config: None,\n+ labels: BTreeMap::default(),\n+ run_dir: \"/tmp/run\".to_string(),\n source_directory: Some(\"/tmp/run\".to_string()),\n- workflow_slug: None,\n- db_prefix: None,\n- provenance: Some(provenance),\n- manifest_blob: None,\n- git: None,\n- fork_source_ref: None,\n- retried_from: None,\n- parent_id: None,\n- web_url: None,\n+ workflow_slug: None,\n+ db_prefix: None,\n+ provenance,\n+ manifest_blob: None,\n+ git: None,\n+ fork_source_ref: None,\n+ retried_from: None,\n+ parent_id: None,\n+ web_url: None,\n });\n let actor = stored.actor.as_ref().expect(\"actor set\");\n assert_eq!(actor, &user_principal(\"alice\"));\ndiff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs\nindex cf458c121..5a70af52c 100644\n--- a/lib/crates/fabro-workflow/src/event/events.rs\n+++ b/lib/crates/fabro-workflow/src/event/events.rs\n@@ -39,8 +39,7 @@ pub enum Event {\n workflow_slug: Option<String>,\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 967f0570a..2c49372e4 100644\n--- a/lib/crates/fabro-workflow/src/event/sink.rs\n+++ b/lib/crates/fabro-workflow/src/event/sink.rs\n@@ -212,7 +212,7 @@ impl StoreProgressLogger {\n mod tests {\n use std::sync::Arc;\n \n- use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures};\n+ use ::fabro_types::{Graph, RunNoticeLevel, WorkflowSettings, fixtures, test_support};\n use tokio::sync::Mutex as AsyncMutex;\n \n use super::*;\n@@ -243,7 +243,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/event/stored_fields.rs b/lib/crates/fabro-workflow/src/event/stored_fields.rs\nindex 94fba25ad..4ada17b67 100644\n--- a/lib/crates/fabro-workflow/src/event/stored_fields.rs\n+++ b/lib/crates/fabro-workflow/src/event/stored_fields.rs\n@@ -57,7 +57,7 @@ pub(super) fn stored_event_fields(event: &Event, scope: Option<&StageScope>) ->\n fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields {\n match event {\n Event::RunCreated { provenance, .. } => StoredEventFields {\n- actor: provenance.as_ref().and_then(|p| p.subject.clone()),\n+ actor: Some(provenance.subject.clone()),\n ..StoredEventFields::default()\n },\n Event::RunCancelRequested { actor }\ndiff --git a/lib/crates/fabro-workflow/src/git.rs b/lib/crates/fabro-workflow/src/git.rs\nindex f48683dd0..177f09079 100644\n--- a/lib/crates/fabro-workflow/src/git.rs\n+++ b/lib/crates/fabro-workflow/src/git.rs\n@@ -343,7 +343,7 @@ mod tests {\n \n use fabro_dump::RunDump;\n use fabro_store::Database;\n- use fabro_types::{CommandTermination, StageModelUsage, fixtures};\n+ use fabro_types::{CommandTermination, StageModelUsage, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -468,7 +468,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs\nindex 648f00c4b..0f46cdfed 100644\n--- a/lib/crates/fabro-workflow/src/handler/agent.rs\n+++ b/lib/crates/fabro-workflow/src/handler/agent.rs\n@@ -429,7 +429,7 @@ mod tests {\n use fabro_graphviz::graph::AttrValue;\n use fabro_model::{ReasoningEffort, Speed};\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n use tempfile::TempDir;\n \n@@ -483,7 +483,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/command.rs b/lib/crates/fabro-workflow/src/handler/command.rs\nindex dde68dd5d..10ad51ce0 100644\n--- a/lib/crates/fabro-workflow/src/handler/command.rs\n+++ b/lib/crates/fabro-workflow/src/handler/command.rs\n@@ -228,7 +228,7 @@ mod tests {\n use bytes::Bytes;\n use fabro_graphviz::graph::AttrValue;\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures};\n+ use fabro_types::{Graph, RunProjection, RunSpec, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n use tokio::sync::Mutex;\n \n@@ -255,7 +255,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: std::collections::HashMap::default(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -355,7 +355,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs\nindex 91753f4c9..499cdd784 100644\n--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs\n+++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs\n@@ -1600,6 +1600,7 @@ mod tests {\n use fabro_types::{\n EventEnvelope, FailureReason, Run, RunId, RunLifecycle, RunLinks, RunOrigin,\n RunPairStatusResponse, RunProjection, RunStatus, RunTimestamps, SuccessReason, WorkflowRef,\n+ test_support,\n };\n use fabro_vault::{SecretType, Vault};\n use futures::stream;\n@@ -2133,7 +2134,7 @@ reasoning = false\n },\n automation: None,\n repository: None,\n- created_by: None,\n+ created_by: test_support::test_principal(),\n origin: RunOrigin::default(),\n labels: HashMap::new(),\n lifecycle: RunLifecycle {\ndiff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs\nindex 7c85042cc..e9b4d4fe3 100644\n--- a/lib/crates/fabro-workflow/src/handler/parallel.rs\n+++ b/lib/crates/fabro-workflow/src/handler/parallel.rs\n@@ -692,7 +692,7 @@ mod tests {\n \n use fabro_graphviz::graph::{AttrValue, Edge};\n use fabro_store::{Database, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -727,7 +727,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs\nindex 8d5b2fdf6..5c3ab7c11 100644\n--- a/lib/crates/fabro-workflow/src/handler/prompt.rs\n+++ b/lib/crates/fabro-workflow/src/handler/prompt.rs\n@@ -225,7 +225,7 @@ mod tests {\n use fabro_graphviz::graph::AttrValue;\n use fabro_model::{ReasoningEffort, Speed};\n use fabro_store::{Database, RunDatabase, StageId};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n use tempfile::TempDir;\n \n@@ -282,7 +282,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs\nindex fefaafa8c..e3cfeff89 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs\n@@ -598,7 +598,7 @@ mod tests {\n use fabro_model::Catalog;\n use fabro_store::{Database, EventEnvelope, RunDatabase, RunProjection};\n use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};\n- use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunBlobId, RunEvent, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -735,7 +735,7 @@ mod tests {\n source_directory: Some(\"/tmp/project\".to_string()),\n workflow_slug: Some(\"metadata\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/operations/archive.rs b/lib/crates/fabro-workflow/src/operations/archive.rs\nindex bb3693398..4393af607 100644\n--- a/lib/crates/fabro-workflow/src/operations/archive.rs\n+++ b/lib/crates/fabro-workflow/src/operations/archive.rs\n@@ -136,7 +136,9 @@ mod tests {\n use std::time::Duration;\n \n use fabro_store::Database;\n- use fabro_types::{FailureReason, RunId, SuccessReason, TerminalStatus, fixtures};\n+ use fabro_types::{\n+ FailureReason, RunId, SuccessReason, TerminalStatus, fixtures, test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -225,7 +227,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs\nindex 0460a1874..6b7233484 100644\n--- a/lib/crates/fabro-workflow/src/operations/create.rs\n+++ b/lib/crates/fabro-workflow/src/operations/create.rs\n@@ -44,7 +44,7 @@ pub struct CreateRunInput {\n pub git: Option<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@@ -70,7 +70,7 @@ struct PersistCreateOptions {\n source_directory: Option<String>,\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@@ -415,7 +415,7 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::settings::InterpString;\n use fabro_types::settings::run::RunMode;\n- use fabro_types::{WorkflowSettings, fixtures};\n+ use fabro_types::{WorkflowSettings, fixtures, test_support};\n use fabro_util::error::collect_chain;\n use fabro_validate::Severity;\n use object_store::local::LocalFileSystem;\n@@ -1099,7 +1099,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1166,7 +1166,7 @@ mod tests {\n }),\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1277,7 +1277,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1322,7 +1322,7 @@ mod tests {\n }),\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1389,7 +1389,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1435,7 +1435,7 @@ mod tests {\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: Some(fabro_types::RunProvenance {\n+ provenance: fabro_types::RunProvenance {\n server: Some(fabro_types::RunServerProvenance {\n version: \"0.9.0\".to_string(),\n }),\n@@ -1444,12 +1444,12 @@ mod tests {\n name: Some(\"fabro-cli\".to_string()),\n version: Some(\"0.9.0\".to_string()),\n }),\n- subject: Some(fabro_types::Principal::user(\n+ subject: fabro_types::Principal::user(\n fabro_types::IdpIdentity::new(\"https://github.com\", \"12345\").unwrap(),\n \"octocat\".to_string(),\n fabro_types::AuthMethod::Github,\n- )),\n- }),\n+ ),\n+ },\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1462,7 +1462,7 @@ mod tests {\n let run_store = store.open_run_reader(&created.run_id).await.unwrap();\n let state = run_store.state().await.unwrap();\n let run = state.spec;\n- let provenance = run.provenance.expect(\"provenance should be projected\");\n+ let provenance = run.provenance;\n \n assert_eq!(provenance.server.unwrap().version, \"0.9.0\");\n assert_eq!(\n@@ -1470,7 +1470,7 @@ mod tests {\n Some(\"fabro-cli\")\n );\n assert_eq!(\n- provenance.subject.unwrap(),\n+ provenance.subject,\n fabro_types::Principal::user(\n fabro_types::IdpIdentity::new(\"https://github.com\", \"12345\").unwrap(),\n \"octocat\".to_string(),\ndiff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs\nindex 513975d71..eb3a6720e 100644\n--- a/lib/crates/fabro-workflow/src/operations/fork.rs\n+++ b/lib/crates/fabro-workflow/src/operations/fork.rs\n@@ -283,7 +283,7 @@ mod tests {\n \n use fabro_graphviz::graph::Graph;\n use fabro_store::{Database, RunProjectionReducer};\n- use fabro_types::{StageId, WorkflowSettings, fixtures};\n+ use fabro_types::{StageId, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -381,7 +381,7 @@ mod tests {\n source_directory: Some(\"/client/source\".to_string()),\n workflow_slug: Some(\"fork-source\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: Some(fabro_types::GitContext {\n origin_url: \"https://github.com/example/repo.git\".to_string(),\ndiff --git a/lib/crates/fabro-workflow/src/operations/retry.rs b/lib/crates/fabro-workflow/src/operations/retry.rs\nindex 6551687c5..f71b90626 100644\n--- a/lib/crates/fabro-workflow/src/operations/retry.rs\n+++ b/lib/crates/fabro-workflow/src/operations/retry.rs\n@@ -12,7 +12,7 @@ use crate::event::{self, Event};\n pub struct RetryRunInput {\n pub source_run_id: RunId,\n pub new_run_id: RunId,\n- pub provenance: Option<RunProvenance>,\n+ pub provenance: RunProvenance,\n pub web_url: Option<String>,\n }\n \n@@ -149,7 +149,7 @@ mod tests {\n version: \"test\".to_string(),\n }),\n client: None,\n- subject: Some(actor(login)),\n+ subject: actor(login),\n }\n }\n \n@@ -187,7 +187,7 @@ mod tests {\n source_directory: Some(\"/workspace/source\".to_string()),\n workflow_slug: Some(\"retry-source\".to_string()),\n db_prefix: None,\n- provenance: Some(provenance(\"source-user\")),\n+ provenance: provenance(\"source-user\"),\n manifest_blob,\n git: Some(git_context()),\n fork_source_ref,\n@@ -345,7 +345,7 @@ mod tests {\n let outcome = retry_run(&store, &RetryRunInput {\n source_run_id,\n new_run_id: RunId::new(),\n- provenance: Some(provenance(\"retry-user\")),\n+ provenance: provenance(\"retry-user\"),\n web_url: Some(\"http://localhost:3000/runs/retry\".to_string()),\n })\n .await\n@@ -379,14 +379,7 @@ mod tests {\n assert_eq!(retry_state.spec.manifest_blob, manifest_blob);\n assert_eq!(retry_state.spec.definition_blob, definition_blob);\n assert_eq!(retry_state.spec.fork_source_ref, Some(fork_source_ref));\n- assert_eq!(\n- retry_state\n- .spec\n- .provenance\n- .as_ref()\n- .and_then(|provenance| provenance.subject.as_ref()),\n- Some(&actor(\"retry-user\"))\n- );\n+ assert_eq!(&retry_state.spec.provenance.subject, &actor(\"retry-user\"));\n assert_eq!(\n retry_state.web_url.as_deref(),\n Some(\"http://localhost:3000/runs/retry\")\n@@ -469,7 +462,7 @@ mod tests {\n let err = retry_run(&store, &RetryRunInput {\n source_run_id: run_id,\n new_run_id: RunId::new(),\n- provenance: None,\n+ provenance: provenance(\"retry-user\"),\n web_url: None,\n })\n .await\n@@ -487,7 +480,7 @@ mod tests {\n let err = retry_run(&store, &RetryRunInput {\n source_run_id: fixtures::RUN_1,\n new_run_id: RunId::new(),\n- provenance: None,\n+ provenance: provenance(\"retry-user\"),\n web_url: None,\n })\n .await\ndiff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs\nindex a962cd0e5..adf2741d7 100644\n--- a/lib/crates/fabro-workflow/src/operations/start.rs\n+++ b/lib/crates/fabro-workflow/src/operations/start.rs\n@@ -1112,7 +1112,9 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::settings::run::RunMode;\n use fabro_types::settings::{InterpString, ModelRef};\n- use fabro_types::{BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures};\n+ use fabro_types::{\n+ BilledModelUsage, ManifestPath, StageTiming, WorkflowSettings, fixtures, test_support,\n+ };\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -1416,7 +1418,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\n@@ -1837,7 +1839,7 @@ reasoning = false\n git: None,\n fork_source_ref: None,\n parent_id: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n configured_providers: Vec::new(),\n web_url: None,\n },\ndiff --git a/lib/crates/fabro-workflow/src/operations/timeline.rs b/lib/crates/fabro-workflow/src/operations/timeline.rs\nindex 2170dc28a..fdfdea119 100644\n--- a/lib/crates/fabro-workflow/src/operations/timeline.rs\n+++ b/lib/crates/fabro-workflow/src/operations/timeline.rs\n@@ -204,6 +204,7 @@ mod tests {\n use chrono::Utc;\n use fabro_types::{\n Checkpoint, CheckpointRecord, Graph, RunDiff, RunSpec, WorkflowSettings, fixtures,\n+ test_support,\n };\n \n use super::*;\n@@ -247,7 +248,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\nindex cef35cc39..39392007b 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs\n@@ -18,7 +18,9 @@ use fabro_interview::AutoApproveInterviewer;\n use fabro_sandbox::SandboxSpec;\n use fabro_store::Database;\n use fabro_types::settings::run::RunModelControls;\n-use fabro_types::{Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref};\n+use fabro_types::{\n+ Principal, RunId, SystemActorKind, WorkflowSettings, fixtures, format_blob_ref, test_support,\n+};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -164,7 +166,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -207,7 +209,7 @@ async fn seed_created_and_starting(\n source_directory: Some(std::env::current_dir().unwrap().display().to_string()),\n workflow_slug: run_options.workflow_slug.clone(),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: run_options.pre_run_git.clone(),\n fork_source_ref: run_options.fork_source_ref.clone(),\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\nindex 05d6db1d0..36b3b7452 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs\n@@ -651,7 +651,7 @@ mod tests {\n use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};\n use fabro_types::{\n BilledTokenCounts, EventBody, RunBlobId, RunEvent, RunId, RunSpec, StageCompletion,\n- WorkflowSettings, first_event_seq, fixtures,\n+ WorkflowSettings, first_event_seq, fixtures, test_support,\n };\n use object_store::memory::InMemory;\n \n@@ -738,7 +738,7 @@ mod tests {\n source_directory: Some(\"/tmp/project\".to_string()),\n workflow_slug: Some(\"metadata\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -854,7 +854,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\nindex 761a58e42..b8b8e5147 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs\n@@ -764,7 +764,7 @@ mod tests {\n use fabro_sandbox::SandboxSpec;\n use fabro_store::Database;\n use fabro_types::settings::run::RunModelControls;\n- use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunEvent, RunId, WorkflowSettings, fixtures, test_support};\n use fabro_vault::{SecretType, Vault};\n use object_store::memory::InMemory;\n use tokio::fs::{create_dir_all, write};\n@@ -888,7 +888,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs\nindex ee6150696..93da4da2c 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs\n@@ -59,7 +59,7 @@ mod tests {\n \n use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};\n use fabro_store::{Database, RunDatabase};\n- use fabro_types::fixtures;\n+ use fabro_types::{fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::*;\n@@ -147,7 +147,7 @@ mod tests {\n (\"env\".to_string(), \"test\".to_string()),\n (\"team\".to_string(), \"workflow\".to_string()),\n ]),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\nindex 92f4a0bc3..41e1f3434 100644\n--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n+++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs\n@@ -680,7 +680,7 @@ mod tests {\n use fabro_store::Database;\n use fabro_types::{\n BilledTokenCounts, RunProjection, RunSpec, SuccessReason, WorkflowSettings,\n- first_event_seq, fixtures,\n+ first_event_seq, fixtures, test_support,\n };\n use fabro_vault::{SecretType, Vault};\n use futures::stream;\n@@ -822,7 +822,7 @@ mod tests {\n workflow_slug: None,\n source_directory: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: None,\n@@ -1146,7 +1146,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1215,7 +1215,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1569,7 +1569,7 @@ mod tests {\n source_directory: Some(tmp.path().display().to_string()),\n git: None,\n labels: std::collections::HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1696,7 +1696,7 @@ mod tests {\n source_directory: Some(\"/tmp/project\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1713,7 +1713,7 @@ mod tests {\n source_directory: run_spec.source_directory.clone(),\n workflow_slug: run_spec.workflow_slug.clone(),\n db_prefix: None,\n- provenance: None,\n+ provenance: run_spec.provenance.clone(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -1865,7 +1865,7 @@ mod tests {\n source_directory: None,\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -1882,7 +1882,7 @@ mod tests {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: None,\n+ provenance: run_spec.provenance.clone(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs\nindex 5d8cdeb3b..750d2d5ea 100644\n--- a/lib/crates/fabro-workflow/src/run_lookup.rs\n+++ b/lib/crates/fabro-workflow/src/run_lookup.rs\n@@ -457,7 +457,7 @@ mod tests {\n \n use fabro_graphviz::graph::Graph;\n use fabro_store::Database;\n- use fabro_types::{RunStatus, WorkflowSettings, fixtures};\n+ use fabro_types::{RunStatus, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::scan_runs_combined;\n@@ -490,7 +490,7 @@ mod tests {\n push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/run_metadata.rs b/lib/crates/fabro-workflow/src/run_metadata.rs\nindex 9d679d0b4..f8606a307 100644\n--- a/lib/crates/fabro-workflow/src/run_metadata.rs\n+++ b/lib/crates/fabro-workflow/src/run_metadata.rs\n@@ -537,7 +537,9 @@ mod tests {\n use std::sync::Arc;\n \n use fabro_store::RunProjection;\n- use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings};\n+ use fabro_types::{\n+ DirtyStatus, GitContext, PreRunPushOutcome, RunSpec, WorkflowSettings, test_support,\n+ };\n use git2::{ErrorClass, ErrorCode};\n \n use super::*;\n@@ -638,7 +640,7 @@ mod tests {\n push_outcome: PreRunPushOutcome::NotAttempted,\n }),\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs\nindex 0f590c70f..e3b464abb 100644\n--- a/lib/crates/fabro-workflow/src/runtime_store.rs\n+++ b/lib/crates/fabro-workflow/src/runtime_store.rs\n@@ -120,7 +120,7 @@ mod tests {\n use fabro_graphviz::graph::Graph;\n use fabro_store::Database;\n use fabro_types::run_event::RunSubmittedProps;\n- use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures};\n+ use fabro_types::{EventBody, RunEvent, WorkflowSettings, fixtures, test_support};\n use object_store::memory::InMemory;\n \n use super::RunStoreHandle;\n@@ -147,7 +147,7 @@ mod tests {\n source_directory: Some(\"/tmp/test\".to_string()),\n git: None,\n labels: HashMap::new(),\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n fork_source_ref: None,\n@@ -168,7 +168,7 @@ mod tests {\n source_directory: Some(\"/tmp/test\".to_string()),\n workflow_slug: Some(\"test\".to_string()),\n db_prefix: None,\n- provenance: None,\n+ provenance: test_support::test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\ndiff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs\nindex c587e49fa..c54affbdc 100644\n--- a/lib/crates/fabro-workflow/src/test_support.rs\n+++ b/lib/crates/fabro-workflow/src/test_support.rs\n@@ -174,7 +174,13 @@ async fn initialized(\n source_directory: Some(sandbox.working_directory().to_string()),\n workflow_slug: run_options.workflow_slug.clone(),\n db_prefix: None,\n- provenance: None,\n+ provenance: fabro_types::RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: fabro_types::Principal::System {\n+ system_kind: fabro_types::SystemActorKind::Engine,\n+ },\n+ },\n manifest_blob: None,\n git: run_options.pre_run_git.clone(),\n fork_source_ref: run_options.fork_source_ref.clone(),\ndiff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex 8e4fa7d1d..03e4463f9 100644\n--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n+++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n@@ -256,7 +256,6 @@ models/preflight-workflow-summary.ts\n models/preview-url-request.ts\n models/preview-url-response.ts\n models/principal-agent.ts\n-models/principal-anonymous.ts\n models/principal-slack.ts\n models/principal-system.ts\n models/principal-user.ts\ndiff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts\nindex 4a208f5e3..2cca8d321 100644\n--- a/lib/packages/fabro-api-client/src/models/index.ts\n+++ b/lib/packages/fabro-api-client/src/models/index.ts\n@@ -232,7 +232,6 @@ export * from './preview-url-request';\n export * from './preview-url-response';\n export * from './principal';\n export * from './principal-agent';\n-export * from './principal-anonymous';\n export * from './principal-slack';\n export * from './principal-system';\n export * from './principal-user';\ndiff --git a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts b/lib/packages/fabro-api-client/src/models/principal-anonymous.ts\ndeleted file mode 100644\nindex daac61df0..000000000\n--- a/lib/packages/fabro-api-client/src/models/principal-anonymous.ts\n+++ /dev/null\n@@ -1,25 +0,0 @@\n-/* tslint:disable */\n-/* eslint-disable */\n-/**\n- * Fabro Run API\n- * HTTP API for managing Fabro workflow run executions.\n- *\n- * The version of the OpenAPI document: 0.1.0\n- *\n- *\n- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n- * https://openapi-generator.tech\n- * Do not edit the class manually.\n- */\n-\n-\n-\n-export interface PrincipalAnonymous {\n- 'kind': PrincipalAnonymousKindEnum;\n-}\n-\n-export const PrincipalAnonymousKindEnum = {\n- ANONYMOUS: 'anonymous'\n-} as const;\n-\n-export type PrincipalAnonymousKindEnum = typeof PrincipalAnonymousKindEnum[keyof typeof PrincipalAnonymousKindEnum];\ndiff --git a/lib/packages/fabro-api-client/src/models/principal.ts b/lib/packages/fabro-api-client/src/models/principal.ts\nindex e3597295d..08b5422df 100644\n--- a/lib/packages/fabro-api-client/src/models/principal.ts\n+++ b/lib/packages/fabro-api-client/src/models/principal.ts\n@@ -24,9 +24,6 @@ import type { IdpIdentity } from './idp-identity';\n import type { PrincipalAgent } from './principal-agent';\n // May contain unused imports in some cases\n // @ts-ignore\n-import type { PrincipalAnonymous } from './principal-anonymous';\n-// May contain unused imports in some cases\n-// @ts-ignore\n import type { PrincipalSlack } from './principal-slack';\n // May contain unused imports in some cases\n // @ts-ignore\n@@ -47,4 +44,4 @@ import type { SystemActorKind } from './system-actor-kind';\n /**\n * @type Principal\n */\n-export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'anonymous' } & PrincipalAnonymous | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker;\n+export type Principal = { kind: 'agent' } & PrincipalAgent | { kind: 'slack' } & PrincipalSlack | { kind: 'system' } & PrincipalSystem | { kind: 'user' } & PrincipalUser | { kind: 'webhook' } & PrincipalWebhook | { kind: 'worker' } & PrincipalWorker;\ndiff --git a/lib/packages/fabro-api-client/src/models/run-provenance.ts b/lib/packages/fabro-api-client/src/models/run-provenance.ts\nindex 7276857f8..59fa7a063 100644\n--- a/lib/packages/fabro-api-client/src/models/run-provenance.ts\n+++ b/lib/packages/fabro-api-client/src/models/run-provenance.ts\n@@ -26,5 +26,5 @@ import type { RunServerProvenance } from './run-server-provenance';\n export interface RunProvenance {\n 'server'?: RunServerProvenance | null;\n 'client'?: RunClientProvenance | null;\n- 'subject'?: Principal | null;\n+ 'subject': Principal;\n }\ndiff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts\nindex 2be7312b7..f86098a2d 100644\n--- a/lib/packages/fabro-api-client/src/models/run-spec.ts\n+++ b/lib/packages/fabro-api-client/src/models/run-spec.ts\n@@ -37,7 +37,7 @@ export interface RunSpec {\n 'workflow_slug'?: string | null;\n 'source_directory'?: string | null;\n 'labels'?: { [key: string]: string; };\n- 'provenance'?: RunProvenance | null;\n+ 'provenance': RunProvenance;\n 'manifest_blob'?: string | null;\n 'definition_blob'?: string | null;\n 'git'?: GitContext | null;\ndiff --git a/lib/packages/fabro-api-client/src/models/run.ts b/lib/packages/fabro-api-client/src/models/run.ts\nindex 1ed6c6c26..a4ade38a3 100644\n--- a/lib/packages/fabro-api-client/src/models/run.ts\n+++ b/lib/packages/fabro-api-client/src/models/run.ts\n@@ -83,7 +83,7 @@ export interface Run {\n 'workflow': WorkflowRef;\n 'automation': AutomationRef | null;\n 'repository': RepositoryRef | null;\n- 'created_by': Principal | null;\n+ 'created_by': Principal;\n 'origin': RunOrigin;\n 'labels': { [key: string]: string; };\n 'lifecycle': RunLifecycle;\ndiff --git a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\nindex 7b6bd4afd..568a21780 100644\n--- a/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n+++ b/lib/packages/fabro-api-client/tests/principal-exhaustive.ts\n@@ -12,8 +12,6 @@ export function principalKind(principal: Principal): string {\n switch (principal.kind) {\n case \"agent\":\n return \"agent\";\n- case \"anonymous\":\n- return \"anonymous\";\n case \"slack\":\n return \"slack\";\n case \"system\":\n",
|
||
"summary": {
|
||
"files_changed": 97,
|
||
"additions": 415,
|
||
"deletions": 340
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"seq": 2251,
|
||
"checkpoint": {
|
||
"timestamp": "2026-05-27T05:24:24.867896Z",
|
||
"current_node": "simplify_opus",
|
||
"completed_nodes": [
|
||
"start",
|
||
"toolchain",
|
||
"preflight_compile",
|
||
"preflight_lint",
|
||
"implement",
|
||
"simplify_opus"
|
||
],
|
||
"node_retries": {},
|
||
"context_values": {
|
||
"thread.preflight_compile.current_node": "preflight_lint",
|
||
"internal.fidelity": "compact",
|
||
"response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option<Principal>` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.",
|
||
"thread.implement.current_node": "simplify_opus",
|
||
"thread.start.current_node": "toolchain",
|
||
"internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"internal.retry_count.simplify_opus": 0,
|
||
"graph.rankdir": "LR",
|
||
"failure_class": "",
|
||
"thread.preflight_lint.current_node": "implement",
|
||
"thread.toolchain.current_node": "preflight_compile",
|
||
"internal.work_dir": "/home/daytona/workspace/fabro",
|
||
"internal.retry_count.start": 0,
|
||
"internal.retry_count.preflight_compile": 0,
|
||
"internal.retry_count.implement": 0,
|
||
"failure_signature": "",
|
||
"internal.node_visit_count": 1,
|
||
"graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option<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.retry_count.preflight_lint": 0,
|
||
"internal.thread_id": "implement",
|
||
"last_response": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_prove",
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
||
"internal.retry_count.toolchain": 0,
|
||
"last_stage": "simplify_opus",
|
||
"outcome": "succeeded",
|
||
"current_node": "simplify_opus",
|
||
"response.simplify_opus": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`.\n\n2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms.\n\n3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports.\n\n**Findings skipped** with rationale:\n- `Option<Principal>` → `RequestAuth` sum type: too invasive; the existing shape is readable.\n- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible.\n- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape.\n- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope.\n- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention.\n- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only.\n\n**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`)."
|
||
},
|
||
"node_outcomes": {
|
||
"simplify_opus": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_stage": "simplify_opus",
|
||
"last_response": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_prove",
|
||
"response.simplify_opus": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`.\n\n2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms.\n\n3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports.\n\n**Findings skipped** with rationale:\n- `Option<Principal>` → `RequestAuth` sum type: too invasive; the existing shape is readable.\n- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible.\n- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape.\n- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope.\n- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention.\n- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only.\n\n**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`)."
|
||
},
|
||
"notes": "Stage completed: simplify_opus",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "anthropic",
|
||
"model_id": "claude-opus-4-7"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 69536,
|
||
"output_tokens": 25178,
|
||
"reasoning_tokens": 0,
|
||
"cache_read_tokens": 3797227,
|
||
"cache_write_tokens": 362231
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "anthropic",
|
||
"cache_write_5m_tokens": 362231,
|
||
"cache_write_1h_tokens": 0
|
||
}
|
||
},
|
||
"total_usd_micros": 5139686
|
||
},
|
||
"files_touched": [
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/run_manifest.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/runs.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/Cargo.toml",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_event_serde.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_methods.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_serde.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/test_support.rs"
|
||
],
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 403076,
|
||
"tool_time_ms": 639117,
|
||
"active_time_ms": 1042193
|
||
}
|
||
},
|
||
"implement": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_stage": "implement",
|
||
"response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option<Principal>` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.",
|
||
"last_response": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; mis"
|
||
},
|
||
"notes": "Stage completed: implement",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "openai",
|
||
"model_id": "gpt-5.5"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 9898125,
|
||
"output_tokens": 51088,
|
||
"reasoning_tokens": 21958,
|
||
"cache_read_tokens": 42159616,
|
||
"cache_write_tokens": 0
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "openai"
|
||
}
|
||
},
|
||
"total_usd_micros": 72761813
|
||
},
|
||
"files_touched": [
|
||
"/home/daytona/workspace/fabro/apps/fabro-web/app/lib/test-principal.ts",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs"
|
||
],
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 4850345,
|
||
"tool_time_ms": 1392988,
|
||
"active_time_ms": 6243333
|
||
}
|
||
},
|
||
"preflight_compile": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 127963,
|
||
"active_time_ms": 127963
|
||
}
|
||
},
|
||
"start": {
|
||
"status": "succeeded",
|
||
"usage": null
|
||
},
|
||
"preflight_lint": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 138598,
|
||
"active_time_ms": 138598
|
||
}
|
||
},
|
||
"toolchain": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
||
},
|
||
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 1456,
|
||
"active_time_ms": 1456
|
||
}
|
||
}
|
||
},
|
||
"next_node_id": "simplify_gpt",
|
||
"git_commit_sha": "aa394ad762d73c4781cec54bd195377db78d909b",
|
||
"node_visits": {
|
||
"start": 1,
|
||
"preflight_lint": 1,
|
||
"simplify_opus": 1,
|
||
"implement": 1,
|
||
"preflight_compile": 1,
|
||
"toolchain": 1
|
||
}
|
||
},
|
||
"diff": {
|
||
"patch": "diff --git a/Cargo.lock b/Cargo.lock\nindex 7e8f48e4c..d73ff2ac9 100644\n--- a/Cargo.lock\n+++ b/Cargo.lock\n@@ -2508,6 +2508,7 @@ dependencies = [\n \"clap\",\n \"dirs\",\n \"fabro-model\",\n+ \"fabro-types\",\n \"fabro-util\",\n \"hex\",\n \"ipnet\",\ndiff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs\nindex 1760d76cf..71fc43b35 100644\n--- a/lib/crates/fabro-server/src/run_manifest.rs\n+++ b/lib/crates/fabro-server/src/run_manifest.rs\n@@ -26,13 +26,11 @@ 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::{\n- ManifestPath, RunId, RunProvenance, SandboxProviderKind, ServerSettings, WorkflowSettings,\n-};\n+use fabro_types::{ManifestPath, RunId, SandboxProviderKind, ServerSettings, WorkflowSettings};\n use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};\n use fabro_validate::Severity;\n use fabro_workflow::Error as WorkflowError;\n-use fabro_workflow::operations::{CreateRunInput, ValidateInput, WorkflowInput, validate};\n+use fabro_workflow::operations::{ValidateInput, WorkflowInput, validate};\n use fabro_workflow::pipeline::Validated;\n use fabro_workflow::run_materialization::materialize_run;\n use fabro_workflow::workflow_bundle::{BundledWorkflow, ParsedWorkflowConfig, WorkflowBundle};\n@@ -200,31 +198,6 @@ pub(crate) fn validate_prepared_manifest(\n })\n }\n \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- workflow: WorkflowInput::Bundled(prepared.workflow_input),\n- settings: prepared.settings,\n- cwd: prepared.cwd,\n- workflow_slug: None,\n- workflow_path: Some(prepared.target_path),\n- workflow_bundle: Some(prepared.workflow_bundle),\n- submitted_manifest_bytes: None,\n- run_id: prepared.run_id,\n- title: prepared.title,\n- git: prepared.git,\n- fork_source_ref: None,\n- parent_id: prepared.parent_id,\n- provenance,\n- configured_providers,\n- web_url,\n- }\n-}\n-\n pub(crate) async fn run_preflight(\n state: &AppState,\n prepared: &PreparedManifest,\ndiff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs\nindex 25316c66a..27fd06072 100644\n--- a/lib/crates/fabro-server/src/server.rs\n+++ b/lib/crates/fabro-server/src/server.rs\n@@ -1825,7 +1825,8 @@ async fn http_log_middleware(mut req: axum_extract::Request, next: Next) -> Resp\n team_id = team_id.as_str(),\n user_id = user_id.as_str(),\n ),\n- None | Some(Principal::Agent { .. } | Principal::System { .. }) => {\n+ None => emit_http_log!($level),\n+ Some(Principal::Agent { .. } | Principal::System { .. }) => {\n emit_http_log!($level)\n }\n }\ndiff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs\nindex e581c8048..5c6997d98 100644\n--- a/lib/crates/fabro-server/src/server/handler/runs.rs\n+++ b/lib/crates/fabro-server/src/server/handler/runs.rs\n@@ -642,14 +642,23 @@ async fn create_run(\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+ let create_input = operations::CreateRunInput {\n+ workflow: operations::WorkflowInput::Bundled(prepared.workflow_input.clone()),\n+ settings: prepared.settings.clone(),\n+ cwd: prepared.cwd.clone(),\n+ workflow_slug: None,\n+ workflow_path: Some(prepared.target_path.clone()),\n+ workflow_bundle: Some(prepared.workflow_bundle.clone()),\n+ submitted_manifest_bytes: Some(body.to_vec()),\n+ run_id: Some(run_id),\n+ title: prepared.title.clone(),\n+ git: prepared.git.clone(),\n+ fork_source_ref: None,\n+ parent_id: prepared.parent_id,\n provenance,\n- web_url.clone(),\n- );\n- create_input.run_id = Some(run_id);\n- create_input.submitted_manifest_bytes = Some(body.to_vec());\n+ configured_providers: ready_provider_ids.clone(),\n+ web_url: web_url.clone(),\n+ };\n \n let storage_root = match resolve_interp_string(&state.server_settings().server.storage.root) {\n Ok(path) => PathBuf::from(path),\ndiff --git a/lib/crates/fabro-types/Cargo.toml b/lib/crates/fabro-types/Cargo.toml\nindex c0a6dfc1b..86cf14331 100644\n--- a/lib/crates/fabro-types/Cargo.toml\n+++ b/lib/crates/fabro-types/Cargo.toml\n@@ -34,4 +34,5 @@ ulid.workspace = true\n url.workspace = true\n \n [dev-dependencies]\n+fabro-types = { path = \".\", features = [\"test-support\"] }\n tempfile = \"3\"\ndiff --git a/lib/crates/fabro-types/src/test_support.rs b/lib/crates/fabro-types/src/test_support.rs\nindex 9db7eff91..2f2a2e147 100644\n--- a/lib/crates/fabro-types/src/test_support.rs\n+++ b/lib/crates/fabro-types/src/test_support.rs\n@@ -1,4 +1,6 @@\n-use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance, RunServerProvenance};\n+use crate::{\n+ AuthMethod, IdpIdentity, Principal, RunProvenance, RunServerProvenance, SystemActorKind,\n+};\n \n #[must_use]\n pub fn test_principal() -> Principal {\n@@ -19,3 +21,16 @@ pub fn test_run_provenance() -> RunProvenance {\n subject: test_principal(),\n }\n }\n+\n+/// Provenance attributed to the engine itself, with no server/client metadata.\n+/// Used in tests that exercise system-initiated runs and serde round-trips.\n+#[must_use]\n+pub fn engine_run_provenance() -> RunProvenance {\n+ RunProvenance {\n+ server: None,\n+ client: None,\n+ subject: Principal::System {\n+ system_kind: SystemActorKind::Engine,\n+ },\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 533d21f15..be824fa8c 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,18 +6,9 @@ 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::test_support::engine_run_provenance;\n use fabro_types::{EventBody, TurnId, WorkflowSettings, fixtures};\n \n-fn test_run_provenance() -> fabro_types::RunProvenance {\n- fabro_types::RunProvenance {\n- server: None,\n- client: None,\n- subject: fabro_types::Principal::System {\n- system_kind: fabro_types::SystemActorKind::Engine,\n- },\n- }\n-}\n-\n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n settings.run.goal = Some(RunGoal::Inline(InterpString::parse(\"Ship {{ env.TASK }}\")));\n@@ -37,7 +28,7 @@ fn run_created_props_round_trip_templated_settings() {\n source_directory: Some(\"/Users/client/project\".to_string()),\n workflow_slug: Some(\"demo\".to_string()),\n db_prefix: Some(\"run_\".to_string()),\n- provenance: test_run_provenance(),\n+ provenance: engine_run_provenance(),\n manifest_blob: None,\n git: Some(GitContext {\n origin_url: \"https://github.com/fabro-sh/fabro.git\".to_string(),\n@@ -99,7 +90,7 @@ fn run_created_props_omits_web_url_when_absent() {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: test_run_provenance(),\n+ provenance: engine_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -137,7 +128,7 @@ fn run_created_props_defaults_retried_from_when_absent() {\n \"graph\": Graph::new(\"ship\"),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n- \"provenance\": test_run_provenance()\n+ \"provenance\": engine_run_provenance()\n });\n \n let props: RunCreatedProps = serde_json::from_value(json).expect(\"props should deserialize\");\ndiff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs\nindex ef29ce763..5004d87d7 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,18 +3,9 @@ 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::test_support::engine_run_provenance;\n use fabro_types::{WorkflowSettings, fixtures};\n \n-fn test_run_provenance() -> fabro_types::RunProvenance {\n- fabro_types::RunProvenance {\n- server: None,\n- client: None,\n- subject: fabro_types::Principal::System {\n- system_kind: fabro_types::SystemActorKind::Engine,\n- },\n- }\n-}\n-\n fn sample_run_spec() -> RunSpec {\n let settings = WorkflowSettings {\n project: ProjectNamespace {\n@@ -36,7 +27,7 @@ fn sample_run_spec() -> RunSpec {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: test_run_provenance(),\n+ provenance: engine_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 d45c56ac8..ebe556f3d 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,18 +4,9 @@ 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::test_support::engine_run_provenance;\n use fabro_types::{WorkflowSettings, fixtures};\n \n-fn test_run_provenance() -> fabro_types::RunProvenance {\n- fabro_types::RunProvenance {\n- server: None,\n- client: None,\n- subject: fabro_types::Principal::System {\n- system_kind: fabro_types::SystemActorKind::Engine,\n- },\n- }\n-}\n-\n fn templated_settings() -> WorkflowSettings {\n let mut settings = WorkflowSettings::default();\n settings.run.goal = Some(RunGoal::Inline(InterpString::parse(\"Ship {{ env.TASK }}\")));\n@@ -32,7 +23,7 @@ fn run_spec_round_trips_templated_settings() {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: test_run_provenance(),\n+ provenance: engine_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\n",
|
||
"summary": {
|
||
"files_changed": 99,
|
||
"additions": 418,
|
||
"deletions": 370
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"seq": 2538,
|
||
"checkpoint": {
|
||
"timestamp": "2026-05-27T05:30:28.879810Z",
|
||
"current_node": "simplify_gpt",
|
||
"completed_nodes": [
|
||
"start",
|
||
"toolchain",
|
||
"preflight_compile",
|
||
"preflight_lint",
|
||
"implement",
|
||
"simplify_opus",
|
||
"simplify_gpt"
|
||
],
|
||
"node_retries": {},
|
||
"context_values": {
|
||
"thread.simplify_opus.current_node": "simplify_gpt",
|
||
"graph.rankdir": "LR",
|
||
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
||
"internal.retry_count.start": 0,
|
||
"internal.work_dir": "/home/daytona/workspace/fabro",
|
||
"thread.preflight_lint.current_node": "implement",
|
||
"failure_signature": "",
|
||
"internal.retry_count.simplify_gpt": 0,
|
||
"thread.implement.current_node": "simplify_opus",
|
||
"internal.thread_id": "simplify_opus",
|
||
"current_node": "simplify_gpt",
|
||
"last_stage": "simplify_gpt",
|
||
"internal.retry_count.simplify_opus": 0,
|
||
"outcome": "succeeded",
|
||
"internal.retry_count.implement": 0,
|
||
"thread.start.current_node": "toolchain",
|
||
"response.simplify_gpt": "Reviewed the branch diff and applied one cleanup.\n\nFixed:\n- Removed the extra `engine_run_provenance()` test helper from `fabro-types::test_support`.\n- Updated affected `fabro-types` serde/method tests to use the canonical requested `test_run_provenance()` helper instead.\n- This keeps test-support minimal and aligned with the plan’s intended API: `test_principal()` + `test_run_provenance()`.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cargo check -q --workspace` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cargo nextest run -p fabro-types` ✅\n- `cargo nextest run -p fabro-server principal_middleware` ✅\n\nWorking tree now has only the cleanup edits in:\n- `lib/crates/fabro-types/src/test_support.rs`\n- `lib/crates/fabro-types/tests/run_event_serde.rs`\n- `lib/crates/fabro-types/tests/run_spec_methods.rs`\n- `lib/crates/fabro-types/tests/run_spec_serde.rs`",
|
||
"internal.retry_count.preflight_compile": 0,
|
||
"response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option<Principal>` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.",
|
||
"internal.fidelity": "compact",
|
||
"thread.preflight_compile.current_node": "preflight_lint",
|
||
"internal.retry_count.toolchain": 0,
|
||
"last_response": "Reviewed the branch diff and applied one cleanup.\n\nFixed:\n- Removed the extra `engine_run_provenance()` test helper from `fabro-types::test_support`.\n- Updated affected `fabro-types` serde/method test",
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
|
||
"internal.retry_count.preflight_lint": 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",
|
||
"failure_class": "",
|
||
"internal.node_visit_count": 1,
|
||
"thread.toolchain.current_node": "preflight_compile",
|
||
"internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"response.simplify_opus": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`.\n\n2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms.\n\n3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports.\n\n**Findings skipped** with rationale:\n- `Option<Principal>` → `RequestAuth` sum type: too invasive; the existing shape is readable.\n- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible.\n- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape.\n- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope.\n- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention.\n- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only.\n\n**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`)."
|
||
},
|
||
"node_outcomes": {
|
||
"implement": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_stage": "implement",
|
||
"response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option<Principal>` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.",
|
||
"last_response": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; mis"
|
||
},
|
||
"notes": "Stage completed: implement",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "openai",
|
||
"model_id": "gpt-5.5"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 9898125,
|
||
"output_tokens": 51088,
|
||
"reasoning_tokens": 21958,
|
||
"cache_read_tokens": 42159616,
|
||
"cache_write_tokens": 0
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "openai"
|
||
}
|
||
},
|
||
"total_usd_micros": 72761813
|
||
},
|
||
"files_touched": [
|
||
"/home/daytona/workspace/fabro/apps/fabro-web/app/lib/test-principal.ts",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs"
|
||
],
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 4850345,
|
||
"tool_time_ms": 1392988,
|
||
"active_time_ms": 6243333
|
||
}
|
||
},
|
||
"simplify_gpt": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_response": "Reviewed the branch diff and applied one cleanup.\n\nFixed:\n- Removed the extra `engine_run_provenance()` test helper from `fabro-types::test_support`.\n- Updated affected `fabro-types` serde/method test",
|
||
"response.simplify_gpt": "Reviewed the branch diff and applied one cleanup.\n\nFixed:\n- Removed the extra `engine_run_provenance()` test helper from `fabro-types::test_support`.\n- Updated affected `fabro-types` serde/method tests to use the canonical requested `test_run_provenance()` helper instead.\n- This keeps test-support minimal and aligned with the plan’s intended API: `test_principal()` + `test_run_provenance()`.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cargo check -q --workspace` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cargo nextest run -p fabro-types` ✅\n- `cargo nextest run -p fabro-server principal_middleware` ✅\n\nWorking tree now has only the cleanup edits in:\n- `lib/crates/fabro-types/src/test_support.rs`\n- `lib/crates/fabro-types/tests/run_event_serde.rs`\n- `lib/crates/fabro-types/tests/run_spec_methods.rs`\n- `lib/crates/fabro-types/tests/run_spec_serde.rs`",
|
||
"last_stage": "simplify_gpt"
|
||
},
|
||
"notes": "Stage completed: simplify_gpt",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "openai",
|
||
"model_id": "gpt-5.5"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 981972,
|
||
"output_tokens": 5026,
|
||
"reasoning_tokens": 1635,
|
||
"cache_read_tokens": 1567744,
|
||
"cache_write_tokens": 0
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "openai"
|
||
}
|
||
},
|
||
"total_usd_micros": 5893562
|
||
},
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 254586,
|
||
"tool_time_ms": 104329,
|
||
"active_time_ms": 358915
|
||
}
|
||
},
|
||
"preflight_lint": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 138598,
|
||
"active_time_ms": 138598
|
||
}
|
||
},
|
||
"start": {
|
||
"status": "succeeded",
|
||
"usage": null
|
||
},
|
||
"preflight_compile": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 127963,
|
||
"active_time_ms": 127963
|
||
}
|
||
},
|
||
"toolchain": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
||
},
|
||
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 1456,
|
||
"active_time_ms": 1456
|
||
}
|
||
},
|
||
"simplify_opus": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_stage": "simplify_opus",
|
||
"last_response": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_prove",
|
||
"response.simplify_opus": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`.\n\n2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms.\n\n3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports.\n\n**Findings skipped** with rationale:\n- `Option<Principal>` → `RequestAuth` sum type: too invasive; the existing shape is readable.\n- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible.\n- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape.\n- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope.\n- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention.\n- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only.\n\n**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`)."
|
||
},
|
||
"notes": "Stage completed: simplify_opus",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "anthropic",
|
||
"model_id": "claude-opus-4-7"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 69536,
|
||
"output_tokens": 25178,
|
||
"reasoning_tokens": 0,
|
||
"cache_read_tokens": 3797227,
|
||
"cache_write_tokens": 362231
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "anthropic",
|
||
"cache_write_5m_tokens": 362231,
|
||
"cache_write_1h_tokens": 0
|
||
}
|
||
},
|
||
"total_usd_micros": 5139686
|
||
},
|
||
"files_touched": [
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/run_manifest.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/runs.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/Cargo.toml",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_event_serde.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_methods.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_serde.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/test_support.rs"
|
||
],
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 403076,
|
||
"tool_time_ms": 639117,
|
||
"active_time_ms": 1042193
|
||
}
|
||
}
|
||
},
|
||
"next_node_id": "verify",
|
||
"git_commit_sha": "25356704aed10696ae897de552ba59fec710a821",
|
||
"node_visits": {
|
||
"simplify_opus": 1,
|
||
"preflight_compile": 1,
|
||
"simplify_gpt": 1,
|
||
"preflight_lint": 1,
|
||
"start": 1,
|
||
"toolchain": 1,
|
||
"implement": 1
|
||
}
|
||
},
|
||
"diff": {
|
||
"patch": "diff --git a/lib/crates/fabro-types/src/test_support.rs b/lib/crates/fabro-types/src/test_support.rs\nindex 2f2a2e147..9db7eff91 100644\n--- a/lib/crates/fabro-types/src/test_support.rs\n+++ b/lib/crates/fabro-types/src/test_support.rs\n@@ -1,6 +1,4 @@\n-use crate::{\n- AuthMethod, IdpIdentity, Principal, RunProvenance, RunServerProvenance, SystemActorKind,\n-};\n+use crate::{AuthMethod, IdpIdentity, Principal, RunProvenance, RunServerProvenance};\n \n #[must_use]\n pub fn test_principal() -> Principal {\n@@ -21,16 +19,3 @@ pub fn test_run_provenance() -> RunProvenance {\n subject: test_principal(),\n }\n }\n-\n-/// Provenance attributed to the engine itself, with no server/client metadata.\n-/// Used in tests that exercise system-initiated runs and serde round-trips.\n-#[must_use]\n-pub fn engine_run_provenance() -> RunProvenance {\n- RunProvenance {\n- server: None,\n- client: None,\n- subject: Principal::System {\n- system_kind: SystemActorKind::Engine,\n- },\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 be824fa8c..1639089d2 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::test_support::engine_run_provenance;\n+use fabro_types::test_support::test_run_provenance;\n use fabro_types::{EventBody, TurnId, WorkflowSettings, fixtures};\n \n fn templated_settings() -> WorkflowSettings {\n@@ -28,7 +28,7 @@ fn run_created_props_round_trip_templated_settings() {\n source_directory: Some(\"/Users/client/project\".to_string()),\n workflow_slug: Some(\"demo\".to_string()),\n db_prefix: Some(\"run_\".to_string()),\n- provenance: engine_run_provenance(),\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n git: Some(GitContext {\n origin_url: \"https://github.com/fabro-sh/fabro.git\".to_string(),\n@@ -90,7 +90,7 @@ fn run_created_props_omits_web_url_when_absent() {\n source_directory: None,\n workflow_slug: None,\n db_prefix: None,\n- provenance: engine_run_provenance(),\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n git: None,\n fork_source_ref: None,\n@@ -128,7 +128,7 @@ fn run_created_props_defaults_retried_from_when_absent() {\n \"graph\": Graph::new(\"ship\"),\n \"labels\": {},\n \"run_dir\": \"/tmp/run\",\n- \"provenance\": engine_run_provenance()\n+ \"provenance\": test_run_provenance()\n });\n \n let props: RunCreatedProps = serde_json::from_value(json).expect(\"props should deserialize\");\ndiff --git a/lib/crates/fabro-types/tests/run_spec_methods.rs b/lib/crates/fabro-types/tests/run_spec_methods.rs\nindex 5004d87d7..76a853561 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::test_support::engine_run_provenance;\n+use fabro_types::test_support::test_run_provenance;\n use fabro_types::{WorkflowSettings, fixtures};\n \n fn sample_run_spec() -> RunSpec {\n@@ -27,7 +27,7 @@ fn sample_run_spec() -> RunSpec {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: engine_run_provenance(),\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\ndiff --git a/lib/crates/fabro-types/tests/run_spec_serde.rs b/lib/crates/fabro-types/tests/run_spec_serde.rs\nindex ebe556f3d..8cd76c3df 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::test_support::engine_run_provenance;\n+use fabro_types::test_support::test_run_provenance;\n use fabro_types::{WorkflowSettings, fixtures};\n \n fn templated_settings() -> WorkflowSettings {\n@@ -23,7 +23,7 @@ fn run_spec_round_trips_templated_settings() {\n workflow_slug: Some(\"demo\".to_string()),\n source_directory: Some(\"/Users/client/project\".to_string()),\n labels: HashMap::from([(\"team\".to_string(), \"platform\".to_string())]),\n- provenance: engine_run_provenance(),\n+ provenance: test_run_provenance(),\n manifest_blob: None,\n definition_blob: None,\n git: Some(GitContext {\n",
|
||
"summary": {
|
||
"files_changed": 99,
|
||
"additions": 403,
|
||
"deletions": 370
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"seq": 0,
|
||
"checkpoint": {
|
||
"timestamp": "2026-05-27T05:35:56.076637Z",
|
||
"current_node": "verify",
|
||
"completed_nodes": [
|
||
"start",
|
||
"toolchain",
|
||
"preflight_compile",
|
||
"preflight_lint",
|
||
"implement",
|
||
"simplify_opus",
|
||
"simplify_gpt",
|
||
"verify"
|
||
],
|
||
"node_retries": {},
|
||
"context_values": {
|
||
"thread.toolchain.current_node": "preflight_compile",
|
||
"internal.work_dir": "/home/daytona/workspace/fabro",
|
||
"response.simplify_gpt": "Reviewed the branch diff and applied one cleanup.\n\nFixed:\n- Removed the extra `engine_run_provenance()` test helper from `fabro-types::test_support`.\n- Updated affected `fabro-types` serde/method tests to use the canonical requested `test_run_provenance()` helper instead.\n- This keeps test-support minimal and aligned with the plan’s intended API: `test_principal()` + `test_run_provenance()`.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cargo check -q --workspace` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cargo nextest run -p fabro-types` ✅\n- `cargo nextest run -p fabro-server principal_middleware` ✅\n\nWorking tree now has only the cleanup edits in:\n- `lib/crates/fabro-types/src/test_support.rs`\n- `lib/crates/fabro-types/tests/run_event_serde.rs`\n- `lib/crates/fabro-types/tests/run_spec_methods.rs`\n- `lib/crates/fabro-types/tests/run_spec_serde.rs`",
|
||
"internal.node_visit_count": 1,
|
||
"graph.goal": "# Plan: Make run actors and provenance total\n\n## Context\n\nThis is a greenfield app. Backward compatibility with old serialized runs, old API clients, old generated models, and old tests is not a constraint. Prefer the clean invariant and remove all traces of the placeholder shape.\n\n`Principal::Anonymous` currently represents \"no authenticated actor on this request\" inside auth middleware. That is auth state, not an actor. A `Principal` should only mean \"who acted.\"\n\nLikewise, a persisted run should always have a creator. `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` event provenance should all be total. No `Option<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.toolchain": 0,
|
||
"failure_signature": "verify|deterministic|script failed with exit code: <n> ## output tput rustc_wrapper = none cargo:rerun-if-env-changed=crate_cc_no_defaults crate_cc_no_defaults = none debug = some(false) cargo_cfg_target_feature = some(fxsr,sse,sse2) cargo:rerun-if-env-changed=",
|
||
"internal.retry_count.start": 0,
|
||
"internal.retry_count.preflight_compile": 0,
|
||
"thread.preflight_compile.current_node": "preflight_lint",
|
||
"command.output": "blob://sha256/a67da8587612aedf70e80489345fffa79a0ff72f6a20874d9d7fcc969a43cf05",
|
||
"internal.run_id": "01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option<Principal>` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.",
|
||
"last_response": "Reviewed the branch diff and applied one cleanup.\n\nFixed:\n- Removed the extra `engine_run_provenance()` test helper from `fabro-types::test_support`.\n- Updated affected `fabro-types` serde/method test",
|
||
"graph.rankdir": "LR",
|
||
"response.simplify_opus": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`.\n\n2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms.\n\n3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports.\n\n**Findings skipped** with rationale:\n- `Option<Principal>` → `RequestAuth` sum type: too invasive; the existing shape is readable.\n- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible.\n- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape.\n- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope.\n- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention.\n- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only.\n\n**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`).",
|
||
"internal.retry_count.simplify_opus": 0,
|
||
"thread.simplify_opus.current_node": "simplify_gpt",
|
||
"graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ",
|
||
"thread.preflight_lint.current_node": "implement",
|
||
"internal.thread_id": "simplify_gpt",
|
||
"thread.simplify_gpt.current_node": "verify",
|
||
"internal.retry_count.verify": 0,
|
||
"last_stage": "simplify_gpt",
|
||
"failure_class": "deterministic",
|
||
"thread.implement.current_node": "simplify_opus",
|
||
"internal.retry_count.simplify_gpt": 0,
|
||
"internal.retry_count.preflight_lint": 0,
|
||
"outcome": "failed",
|
||
"thread.start.current_node": "toolchain",
|
||
"internal.fidelity": "compact",
|
||
"current_node": "verify",
|
||
"internal.retry_count.implement": 0
|
||
},
|
||
"node_outcomes": {
|
||
"simplify_opus": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_stage": "simplify_opus",
|
||
"last_response": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_prove",
|
||
"response.simplify_opus": "All checks pass. Summary of cleanup:\n\n**Fixes applied** based on the three review agents' findings:\n\n1. **Consolidated triplicate `test_run_provenance()`** (Code Reuse review): Added `engine_run_provenance()` to `fabro-types::test_support`, dual-listed `fabro-types` in its own dev-deps with `test-support` feature, and deleted the three byte-identical local helpers in `fabro-types/tests/{run_event_serde,run_spec_methods,run_spec_serde}.rs`.\n\n2. **Split HTTP log match arm** (Code Quality review): In `server.rs`, separated `None => emit_http_log!($level)` from `Some(Principal::Agent { .. } | Principal::System { .. })` so the principal-absent case and stateless-principal case have distinct arms.\n\n3. **Inlined `create_run_input` helper** (Code Quality review): The helper in `run_manifest.rs` had a single caller that immediately patched two of its fields. Inlined the full `CreateRunInput` struct literal into the handler and removed the helper plus its now-unused imports.\n\n**Findings skipped** with rationale:\n- `Option<Principal>` → `RequestAuth` sum type: too invasive; the existing shape is readable.\n- `DEMO_PRINCIPAL` as `LazyLock`: reviewers disagreed; current form is fine since `Principal::user` is not const-constructible.\n- Hand-rolled JSON principals in insta snapshots: intentional — snapshots assert the literal wire shape.\n- Cross-crate `test_principal(login)` parameterization: would require feature-gating `fabro-workflow::test_support`, out of scope.\n- TS `test-principal.ts` location: consistent with `app/lib/test-utils.tsx` convention.\n- Per-event `Principal` clone in `build_summary` (Efficiency review): pre-existing pattern, flagged as follow-up only.\n\n**Verification**: `cargo build --workspace`, `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`, `cargo +nightly-2026-04-14 fmt --check --all`, `cargo nextest run --workspace` (6469 passed), and `bun run typecheck` all green. The 14 TS test failures are pre-existing on baseline (confirmed via `git stash`)."
|
||
},
|
||
"notes": "Stage completed: simplify_opus",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "anthropic",
|
||
"model_id": "claude-opus-4-7"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 69536,
|
||
"output_tokens": 25178,
|
||
"reasoning_tokens": 0,
|
||
"cache_read_tokens": 3797227,
|
||
"cache_write_tokens": 362231
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "anthropic",
|
||
"cache_write_5m_tokens": 362231,
|
||
"cache_write_1h_tokens": 0
|
||
}
|
||
},
|
||
"total_usd_micros": 5139686
|
||
},
|
||
"files_touched": [
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/run_manifest.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/handler/runs.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/Cargo.toml",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_event_serde.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_methods.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/tests/run_spec_serde.rs",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/test_support.rs"
|
||
],
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 403076,
|
||
"tool_time_ms": 639117,
|
||
"active_time_ms": 1042193
|
||
}
|
||
},
|
||
"start": {
|
||
"status": "succeeded",
|
||
"usage": null
|
||
},
|
||
"preflight_lint": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 138598,
|
||
"active_time_ms": 138598
|
||
}
|
||
},
|
||
"implement": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_stage": "implement",
|
||
"response.implement": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; missing auth is represented as `Option<Principal>` and HTTP logs use `principal_kind=\"none\"`.\n- `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `run.created` provenance are now total across Rust, OpenAPI, generated TS client, frontend, projections, create/retry flows, demo mode, and tests.\n- Added `fabro-types` test-support helpers and frontend `testPrincipal` fixture.\n- Regenerated/validated Rust and TypeScript API client outputs.\n- Working tree is clean.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo check -q --workspace`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cargo build --workspace`\n- `cargo nextest run --workspace`\n- `cargo nextest run -p fabro-server`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n- `cd apps/fabro-web && bun run test`\n- `cd apps/fabro-web && bun run build`\n- Final sweeps for anonymous principals and nullable creator/provenance patterns\n\nNote: direct `cd apps/fabro-web && bun test` without the package script failed due the suite’s non-isolated shared Axios adapter state; the repo’s configured test script (`bun run test`, which runs `bun test --isolate`) passes. Manual browser/server E2E checks were not run in this terminal session.",
|
||
"last_response": "Implemented the two-commit cleanup and committed both changes:\n\n- `d15d3d033 Remove anonymous principal variant`\n- `5d14b2ed0 Make run provenance total`\n\nKey outcomes:\n- `Principal` is actor-only; mis"
|
||
},
|
||
"notes": "Stage completed: implement",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "openai",
|
||
"model_id": "gpt-5.5"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 9898125,
|
||
"output_tokens": 51088,
|
||
"reasoning_tokens": 21958,
|
||
"cache_read_tokens": 42159616,
|
||
"cache_write_tokens": 0
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "openai"
|
||
}
|
||
},
|
||
"total_usd_micros": 72761813
|
||
},
|
||
"files_touched": [
|
||
"/home/daytona/workspace/fabro/apps/fabro-web/app/lib/test-principal.ts",
|
||
"/home/daytona/workspace/fabro/lib/crates/fabro-types/src/test_support.rs"
|
||
],
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 4850345,
|
||
"tool_time_ms": 1392988,
|
||
"active_time_ms": 6243333
|
||
}
|
||
},
|
||
"preflight_compile": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
|
||
},
|
||
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 127963,
|
||
"active_time_ms": 127963
|
||
}
|
||
},
|
||
"toolchain": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
|
||
},
|
||
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 1456,
|
||
"active_time_ms": 1456
|
||
}
|
||
},
|
||
"verify": {
|
||
"status": "failed",
|
||
"context_updates": {
|
||
"command.output": "blob://sha256/a67da8587612aedf70e80489345fffa79a0ff72f6a20874d9d7fcc969a43cf05"
|
||
},
|
||
"failure": {
|
||
"message": "Script failed with exit code: 1\n\n## output\nTPUT\n RUSTC_WRAPPER = None\n cargo:rerun-if-env-changed=CRATE_CC_NO_DEFAULTS\n CRATE_CC_NO_DEFAULTS = None\n DEBUG = Some(false)\n CARGO_CFG_TARGET_FEATURE = Some(fxsr,sse,sse2)\n cargo:rerun-if-env-changed=CFLAGS\n CFLAGS = None\n cargo:rerun-if-env-changed=HOST_CFLAGS\n HOST_CFLAGS = None\n cargo:rerun-if-env-changed=CFLAGS_x86_64_unknown_linux_gnu\n CFLAGS_x86_64_unknown_linux_gnu = None\n cargo:rerun-if-env-changed=CFLAGS_x86_64-unknown-linux-gnu\n CFLAGS_x86_64-unknown-linux-gnu = None\n cargo:rerun-if-env-changed=AR_x86_64-unknown-linux-gnu\n AR_x86_64-unknown-linux-gnu = None\n cargo:rerun-if-env-changed=AR_x86_64_unknown_linux_gnu\n AR_x86_64_unknown_linux_gnu = None\n cargo:rerun-if-env-changed=HOST_AR\n HOST_AR = None\n cargo:rerun-if-env-changed=AR\n AR = None\n cargo:rerun-if-env-changed=ARFLAGS\n ARFLAGS = None\n cargo:rerun-if-env-changed=HOST_ARFLAGS\n HOST_ARFLAGS = None\n cargo:rerun-if-env-changed=ARFLAGS_x86_64_unknown_linux_gnu\n ARFLAGS_x86_64_unknown_linux_gnu = None\n cargo:rerun-if-env-changed=ARFLAGS_x86_64-unknown-linux-gnu\n ARFLAGS_x86_64-unknown-linux-gnu = None\n cargo:rerun-if-env-changed=RANLIB_x86_64-unknown-linux-gnu\n RANLIB_x86_64-unknown-linux-gnu = None\n cargo:rerun-if-env-changed=RANLIB_x86_64_unknown_linux_gnu\n RANLIB_x86_64_unknown_linux_gnu = None\n cargo:rerun-if-env-changed=HOST_RANLIB\n HOST_RANLIB = None\n cargo:rerun-if-env-changed=RANLIB\n RANLIB = None\n cargo:rerun-if-env-changed=RANLIBFLAGS\n RANLIBFLAGS = None\n cargo:rerun-if-env-changed=HOST_RANLIBFLAGS\n HOST_RANLIBFLAGS = None\n cargo:rerun-if-env-changed=RANLIBFLAGS_x86_64_unknown_linux_gnu\n RANLIBFLAGS_x86_64_unknown_linux_gnu = None\n cargo:rerun-if-env-changed=RANLIBFLAGS_x86_64-unknown-linux-gnu\n RANLIBFLAGS_x86_64-unknown-linux-gnu = None\n running cd \"/home/daytona/repos/fabro-sh/fabro/target/release/build/openssl-sys-4b9c85ca063deafb/out/openssl-build/build/src\" && env -u CROSS_COMPILE AR=\"ar\" CC=\"cc\" RANLIB=\"ranlib\" \"perl\" \"./Configure\" \"--prefix=/home/daytona/repos/fabro-sh/fabro/target/release/build/openssl-sys-4b9c85ca063deafb/out/openssl-build/install\" \"--openssldir=/usr/local/ssl\" \"no-shared\" \"no-module\" \"no-tests\" \"no-comp\" \"no-zlib\" \"no-zlib-dynamic\" \"--libdir=lib\" \"no-ssl3\" \"no-md2\" \"no-rc5\" \"no-weak-ssl-ciphers\" \"no-camellia\" \"no-idea\" \"no-seed\" \"linux-x86_64\" \"-O2\" \"-ffunction-sections\" \"-fdata-sections\" \"-fPIC\" \"-m64\" \"-w\"\n Configuring OpenSSL version 3.6.2 for target linux-x86_64\n Using os-specific seed configuration\n Created configdata.pm\n Running configdata.pm\n Created Makefile.in\n Created Makefile\n Created include/openssl/configuration.h\n\n **********************************************************************\n *** ***\n *** OpenSSL has been successfully configured ***\n *** ***\n *** If you encounter a problem while building, please open an ***\n *** issue on GitHub <https://github.com/openssl/openssl/issues> ***\n *** and include the output from the following command: ***\n *** ***\n *** perl configdata.pm --dump ***\n *** ***\n *** (If you are new to OpenSSL, you might want to consult the ***\n *** 'Troubleshooting' section in the INSTALL.md file first) ***\n *** ***\n *****************************************************error: could not compile `fabro-llm` (lib) due to 1 previous error\nerror: could not compile `fabro-api` (lib) due to 1 previous error\nerror: failed to build archive at `/home/daytona/repos/fabro-sh/fabro/target/release/deps/libslatedb-7260f2218a7541d0.rlib`: No space left on device (os error 28)\n\nerror: could not compile `slatedb` (lib) due to 1 previous error\nfabro-dev failed\n caused by: command failed with exit status: 101: cargo build -p fabro-cli --release\n",
|
||
"category": "deterministic"
|
||
},
|
||
"usage": null,
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 327163,
|
||
"active_time_ms": 327163
|
||
}
|
||
},
|
||
"simplify_gpt": {
|
||
"status": "succeeded",
|
||
"context_updates": {
|
||
"last_response": "Reviewed the branch diff and applied one cleanup.\n\nFixed:\n- Removed the extra `engine_run_provenance()` test helper from `fabro-types::test_support`.\n- Updated affected `fabro-types` serde/method test",
|
||
"response.simplify_gpt": "Reviewed the branch diff and applied one cleanup.\n\nFixed:\n- Removed the extra `engine_run_provenance()` test helper from `fabro-types::test_support`.\n- Updated affected `fabro-types` serde/method tests to use the canonical requested `test_run_provenance()` helper instead.\n- This keeps test-support minimal and aligned with the plan’s intended API: `test_principal()` + `test_run_provenance()`.\n\nValidation run:\n- `cargo +nightly-2026-04-14 fmt --check --all` ✅\n- `cargo check -q --workspace` ✅\n- `cd apps/fabro-web && bun run typecheck` ✅\n- `cargo nextest run -p fabro-types` ✅\n- `cargo nextest run -p fabro-server principal_middleware` ✅\n\nWorking tree now has only the cleanup edits in:\n- `lib/crates/fabro-types/src/test_support.rs`\n- `lib/crates/fabro-types/tests/run_event_serde.rs`\n- `lib/crates/fabro-types/tests/run_spec_methods.rs`\n- `lib/crates/fabro-types/tests/run_spec_serde.rs`",
|
||
"last_stage": "simplify_gpt"
|
||
},
|
||
"notes": "Stage completed: simplify_gpt",
|
||
"usage": {
|
||
"input": {
|
||
"usage": {
|
||
"model": {
|
||
"provider": "openai",
|
||
"model_id": "gpt-5.5"
|
||
},
|
||
"tokens": {
|
||
"input_tokens": 981972,
|
||
"output_tokens": 5026,
|
||
"reasoning_tokens": 1635,
|
||
"cache_read_tokens": 1567744,
|
||
"cache_write_tokens": 0
|
||
}
|
||
},
|
||
"facts": {
|
||
"algorithm": "openai"
|
||
}
|
||
},
|
||
"total_usd_micros": 5893562
|
||
},
|
||
"timing": {
|
||
"wall_time_ms": 0,
|
||
"inference_time_ms": 254586,
|
||
"tool_time_ms": 104329,
|
||
"active_time_ms": 358915
|
||
}
|
||
}
|
||
},
|
||
"next_node_id": "fixup",
|
||
"node_visits": {
|
||
"preflight_compile": 1,
|
||
"implement": 1,
|
||
"simplify_opus": 1,
|
||
"simplify_gpt": 1,
|
||
"toolchain": 1,
|
||
"verify": 1,
|
||
"preflight_lint": 1,
|
||
"start": 1
|
||
}
|
||
},
|
||
"diff": {}
|
||
}
|
||
],
|
||
"conclusion": null,
|
||
"sandbox": {
|
||
"provider": "daytona",
|
||
"snapshot": "fabro-v12",
|
||
"runtime": {
|
||
"id": "fabro-01KSKPQ9CGRP5B4T9181B04S0A",
|
||
"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,
|
||
"retried_from": "01KSKJQ4H7KKENW3KC6HK09MDM",
|
||
"pending_interviews": {},
|
||
"stages": {
|
||
"toolchain@1": {
|
||
"first_event_seq": 21,
|
||
"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-05-27T03:13:09.202973Z"
|
||
},
|
||
"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": 1456,
|
||
"termination": "exited",
|
||
"output_bytes": 36,
|
||
"live_streaming": true
|
||
},
|
||
"parallel_results": null,
|
||
"output": null,
|
||
"output_bytes": 36,
|
||
"live_streaming": true,
|
||
"termination": "exited",
|
||
"started_at": "2026-05-27T03:13:07.732721Z",
|
||
"handler": "command",
|
||
"timing": {
|
||
"wall_time_ms": 1470,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 1456,
|
||
"active_time_ms": 1456
|
||
},
|
||
"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": 51,
|
||
"prompt": null,
|
||
"response": null,
|
||
"completion": {
|
||
"outcome": "succeeded",
|
||
"notes": "Stage completed: implement",
|
||
"failure_reason": null,
|
||
"timestamp": "2026-05-27T05:06:52.896823Z"
|
||
},
|
||
"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-05-27T03:17:49.390830Z",
|
||
"handler": "agent",
|
||
"timing": {
|
||
"wall_time_ms": 6543491,
|
||
"inference_time_ms": 4850345,
|
||
"tool_time_ms": 1392988,
|
||
"active_time_ms": 6243333
|
||
},
|
||
"usage": {
|
||
"input_tokens": 9898125,
|
||
"output_tokens": 51088,
|
||
"total_tokens": 52130787,
|
||
"reasoning_tokens": 21958,
|
||
"cache_read_tokens": 42159616,
|
||
"cache_write_tokens": 0,
|
||
"total_usd_micros": 72761813
|
||
},
|
||
"model": {
|
||
"provider": "openai",
|
||
"model_id": "gpt-5.5"
|
||
},
|
||
"todos": {
|
||
"kind": "openai_plan",
|
||
"list_id": "openai_plan:e55e1198-f6ba-41df-9033-71f6ad1cb2e6",
|
||
"items": [
|
||
{
|
||
"id": "e4b2e750b9046647",
|
||
"status": "completed",
|
||
"order": 0,
|
||
"subject": "Finish OpenAPI and regenerated client changes for total run provenance"
|
||
},
|
||
{
|
||
"id": "3e6912160c9c1a2e",
|
||
"status": "completed",
|
||
"order": 1,
|
||
"subject": "Update frontend fixtures and UI assumptions for non-null created_by"
|
||
},
|
||
{
|
||
"id": "34b8aa772b3a4fd1",
|
||
"status": "in_progress",
|
||
"order": 2,
|
||
"subject": "Sweep Rust/TS/docs for remaining optional/null actor-provenance traces"
|
||
},
|
||
{
|
||
"id": "b4887cb452f041ac",
|
||
"status": "pending",
|
||
"order": 3,
|
||
"subject": "Run formatting and Rust/frontend validation"
|
||
},
|
||
{
|
||
"id": "1fec46719c7b6b74",
|
||
"status": "pending",
|
||
"order": 4,
|
||
"subject": "Commit the second cleanup change"
|
||
}
|
||
]
|
||
},
|
||
"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": 138956,
|
||
"usage_percent": 51.08676470588235,
|
||
"count_method": "response_usage_scaled_breakdown",
|
||
"staleness": "live",
|
||
"generated_at": "2026-05-27T05:06:52.790625Z",
|
||
"event_seq": 1728,
|
||
"breakdown": [
|
||
{
|
||
"category": "system_prompt",
|
||
"tokens": 1060,
|
||
"usage_percent": 0.3897058823529412
|
||
},
|
||
{
|
||
"category": "tools",
|
||
"tokens": 1547,
|
||
"usage_percent": 0.56875
|
||
},
|
||
{
|
||
"category": "memory",
|
||
"tokens": 3703,
|
||
"usage_percent": 1.3613970588235293
|
||
},
|
||
{
|
||
"category": "conversation",
|
||
"tokens": 132639,
|
||
"usage_percent": 48.76433823529412
|
||
},
|
||
{
|
||
"category": "other",
|
||
"tokens": 7,
|
||
"usage_percent": 0.002573529411764706
|
||
}
|
||
],
|
||
"warnings": []
|
||
},
|
||
"state": "succeeded"
|
||
},
|
||
"preflight_lint@1": {
|
||
"first_event_seq": 41,
|
||
"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-05-27T03:17:44.810825Z"
|
||
},
|
||
"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": 138598,
|
||
"termination": "exited",
|
||
"output_bytes": 0,
|
||
"live_streaming": false
|
||
},
|
||
"parallel_results": null,
|
||
"output": null,
|
||
"output_bytes": 0,
|
||
"live_streaming": false,
|
||
"termination": "exited",
|
||
"started_at": "2026-05-27T03:15:26.202569Z",
|
||
"handler": "command",
|
||
"timing": {
|
||
"wall_time_ms": 138607,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 138598,
|
||
"active_time_ms": 138598
|
||
},
|
||
"usage": {
|
||
"input_tokens": 0,
|
||
"output_tokens": 0,
|
||
"total_tokens": 0,
|
||
"reasoning_tokens": 0,
|
||
"cache_read_tokens": 0,
|
||
"cache_write_tokens": 0
|
||
},
|
||
"state": "succeeded"
|
||
},
|
||
"simplify_gpt@1": {
|
||
"first_event_seq": 2254,
|
||
"prompt": null,
|
||
"response": null,
|
||
"completion": {
|
||
"outcome": "succeeded",
|
||
"notes": "Stage completed: simplify_gpt",
|
||
"failure_reason": null,
|
||
"timestamp": "2026-05-27T05:30:24.656115Z"
|
||
},
|
||
"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-05-27T05:24:24.870972Z",
|
||
"handler": "agent",
|
||
"timing": {
|
||
"wall_time_ms": 359784,
|
||
"inference_time_ms": 254586,
|
||
"tool_time_ms": 104329,
|
||
"active_time_ms": 358915
|
||
},
|
||
"usage": {
|
||
"input_tokens": 981972,
|
||
"output_tokens": 5026,
|
||
"total_tokens": 2556377,
|
||
"reasoning_tokens": 1635,
|
||
"cache_read_tokens": 1567744,
|
||
"cache_write_tokens": 0,
|
||
"total_usd_micros": 5893562
|
||
},
|
||
"model": {
|
||
"provider": "openai",
|
||
"model_id": "gpt-5.5"
|
||
},
|
||
"todos": {
|
||
"kind": "openai_plan",
|
||
"list_id": "openai_plan:9ff36552-aee4-4e52-b9d4-6d352c500089",
|
||
"items": [
|
||
{
|
||
"id": "bc2425a2210ec081",
|
||
"status": "completed",
|
||
"order": 0,
|
||
"subject": "Inspect current git diff and changed files"
|
||
},
|
||
{
|
||
"id": "192241d456667096",
|
||
"status": "completed",
|
||
"order": 1,
|
||
"subject": "Launch three parallel review agents with full diff"
|
||
},
|
||
{
|
||
"id": "f12b16a50b63f44b",
|
||
"status": "completed",
|
||
"order": 2,
|
||
"subject": "Aggregate findings and implement cleanup fixes"
|
||
},
|
||
{
|
||
"id": "851ab97aeca53b42",
|
||
"status": "completed",
|
||
"order": 3,
|
||
"subject": "Run targeted validation"
|
||
},
|
||
{
|
||
"id": "0a333c60fba97845",
|
||
"status": "in_progress",
|
||
"order": 4,
|
||
"subject": "Summarize outcome"
|
||
}
|
||
]
|
||
},
|
||
"subagents": [
|
||
{
|
||
"agent_id": "4f10120e",
|
||
"depth": 1,
|
||
"task": "Code Reuse Review. Read the full branch diff at /tmp/fabro-review.diff (diff from origin/main to HEAD) and review for reuse opportunities. For each change, search existing utilities/helpers using rg as needed. Flag duplicated newly written functions or inline logic that could use existing utilities. Focus on simplicity in this greenfield app. Return concise actionable findings with file/line references; if clean, say so.",
|
||
"status": {
|
||
"kind": "completed",
|
||
"success": true,
|
||
"turns_used": 9
|
||
}
|
||
},
|
||
{
|
||
"agent_id": "4ba62e67",
|
||
"depth": 1,
|
||
"task": "Code Quality Review. Read the full branch diff at /tmp/fabro-review.diff (diff from origin/main to HEAD) and review for hacky patterns: redundant state, parameter sprawl, copy-paste, leaky abstractions, stringly typed code. Be aggressive about quality but concise. Search files as needed. Return actionable findings with file/line references; if clean, say so.",
|
||
"status": {
|
||
"kind": "completed",
|
||
"success": true,
|
||
"turns_used": 9
|
||
}
|
||
},
|
||
{
|
||
"agent_id": "9793fac3",
|
||
"depth": 1,
|
||
"task": "Efficiency Review. Read the full branch diff at /tmp/fabro-review.diff (diff from origin/main to HEAD) and review for unnecessary work, missed concurrency, hot-path bloat, TOCTOU checks, memory/leaks, overly broad ops. Search files as needed. Return concise actionable findings with file/line references; if clean, say so.",
|
||
"status": {
|
||
"kind": "completed",
|
||
"success": true,
|
||
"turns_used": 9
|
||
}
|
||
}
|
||
],
|
||
"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": 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": 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": "openai",
|
||
"model": "gpt-5.5",
|
||
"context_window_tokens": 272000,
|
||
"input_tokens": 75346,
|
||
"usage_percent": 27.700735294117646,
|
||
"count_method": "response_usage_scaled_breakdown",
|
||
"staleness": "live",
|
||
"generated_at": "2026-05-27T05:30:24.496105Z",
|
||
"event_seq": 2531,
|
||
"breakdown": [
|
||
{
|
||
"category": "system_prompt",
|
||
"tokens": 1001,
|
||
"usage_percent": 0.36801470588235297
|
||
},
|
||
{
|
||
"category": "tools",
|
||
"tokens": 1447,
|
||
"usage_percent": 0.5319852941176471
|
||
},
|
||
{
|
||
"category": "memory",
|
||
"tokens": 3463,
|
||
"usage_percent": 1.2731617647058824
|
||
},
|
||
{
|
||
"category": "conversation",
|
||
"tokens": 69429,
|
||
"usage_percent": 25.52536764705882
|
||
},
|
||
{
|
||
"category": "other",
|
||
"tokens": 6,
|
||
"usage_percent": 0.0022058823529411764
|
||
}
|
||
],
|
||
"warnings": []
|
||
},
|
||
"state": "succeeded"
|
||
},
|
||
"start@1": {
|
||
"first_event_seq": 17,
|
||
"prompt": null,
|
||
"response": null,
|
||
"completion": {
|
||
"outcome": "succeeded",
|
||
"notes": null,
|
||
"failure_reason": null,
|
||
"timestamp": "2026-05-27T03:13:07.732562Z"
|
||
},
|
||
"provider_used": null,
|
||
"diff": null,
|
||
"script_invocation": null,
|
||
"script_timing": null,
|
||
"parallel_results": null,
|
||
"output": null,
|
||
"started_at": "2026-05-27T03:13:07.732481Z",
|
||
"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"
|
||
},
|
||
"simplify_opus@1": {
|
||
"first_event_seq": 1738,
|
||
"prompt": null,
|
||
"response": null,
|
||
"completion": {
|
||
"outcome": "succeeded",
|
||
"notes": "Stage completed: simplify_opus",
|
||
"failure_reason": null,
|
||
"timestamp": "2026-05-27T05:24:20.725108Z"
|
||
},
|
||
"provider_used": {
|
||
"mode": "agent",
|
||
"provider": "anthropic",
|
||
"model": "claude-opus-4-7"
|
||
},
|
||
"diff": null,
|
||
"script_invocation": null,
|
||
"script_timing": null,
|
||
"parallel_results": null,
|
||
"output": null,
|
||
"started_at": "2026-05-27T05:06:57.700061Z",
|
||
"handler": "agent",
|
||
"timing": {
|
||
"wall_time_ms": 1043023,
|
||
"inference_time_ms": 403076,
|
||
"tool_time_ms": 639117,
|
||
"active_time_ms": 1042193
|
||
},
|
||
"usage": {
|
||
"input_tokens": 69536,
|
||
"output_tokens": 25178,
|
||
"total_tokens": 4254172,
|
||
"reasoning_tokens": 0,
|
||
"cache_read_tokens": 3797227,
|
||
"cache_write_tokens": 362231,
|
||
"total_usd_micros": 5139686
|
||
},
|
||
"model": {
|
||
"provider": "anthropic",
|
||
"model_id": "claude-opus-4-7"
|
||
},
|
||
"todos": {
|
||
"kind": "anthropic_tasks",
|
||
"list_id": "anthropic_tasks:58be8615-4937-4eb3-ab2c-ef0327ef19ed",
|
||
"items": [
|
||
{
|
||
"id": "1",
|
||
"status": "completed",
|
||
"order": 0,
|
||
"subject": "Consolidate test_run_provenance Engine variant",
|
||
"description": "Add test_run_provenance_system() to fabro-types::test_support, dual-list fabro-types in its own dev-deps with test-support feature, delete 4 duplicates in fabro-types integration tests and fabro-workflow/src/test_support.rs.",
|
||
"active_form": "Consolidating test_run_provenance Engine duplicates"
|
||
},
|
||
{
|
||
"id": "2",
|
||
"status": "completed",
|
||
"order": 1,
|
||
"subject": "Split HTTP log match arm for principal absence vs stateless",
|
||
"description": "Split `None | Some(Principal::Agent { .. } | Principal::System { .. })` in server.rs HTTP log macro into two distinct arms so future fields on Agent/System aren't silently swallowed.",
|
||
"active_form": "Splitting HTTP log match arm"
|
||
},
|
||
{
|
||
"id": "3",
|
||
"status": "completed",
|
||
"order": 2,
|
||
"subject": "Inline create_run_input helper",
|
||
"description": "The create_run_input helper in run_manifest.rs has a single caller in handler/runs.rs that immediately post-mutates run_id and submitted_manifest_bytes. Inline it to make the data flow obvious and stop growing positional args.",
|
||
"active_form": "Inlining create_run_input helper"
|
||
}
|
||
]
|
||
},
|
||
"subagents": [
|
||
{
|
||
"agent_id": "aa31f620",
|
||
"depth": 1,
|
||
"task": "You are reviewing a code change for **code reuse opportunities**. The diff is at `/tmp/review.diff` (2945 lines). The repository root is `/home/daytona/workspace/fabro`.\n\nThe change does two things:\n1. Removes `Principal::Anonymous` variant\n2. Makes `Run.created_by`, `RunSpec.provenance`, `RunProvenance.subject`, and `Event::RunCreated.provenance` non-optional\n\nA new test_support module was added:\n- `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()`\n- `apps/fabro-web/app/lib/test-principal.ts` with a TS principal fixture\n\nCrates that consume `fabro-types` for test fixtures now dual-list `fabro-types` in dev-dependencies with `features = [\"test-support\"]` (fabro-api, fabro-cli, fabro-store, fabro-tool).\n\nYour task:\n1. Read `/tmp/review.diff` thoroughly.\n2. For each new function/helper added in the diff, search the codebase for existing utilities that already do the same thing. Use grep heavily.\n3. Specifically check:\n - Is the new `test_principal()` / `test_run_provenance()` duplicating an existing fixture or builder anywhere?\n - Is the TS `test-principal.ts` duplicating helpers in apps/fabro-web/app/test-utils or similar?\n - Are there inline `Principal::user(...)` constructions in tests/handlers that could just use `test_principal()` (especially in demo/mod.rs)?\n - Does `lib/crates/fabro-server/src/demo/mod.rs` `DEMO_PRINCIPAL` and the test fixture overlap suspiciously?\n - Any duplicate hand-rolled `RunProvenance { subject: ..., ..Default::default() }` literals that could use a helper?\n4. Flag inline logic that could use existing utilities.\n\nReport findings as a concrete list with file paths, line references, and the existing utility name to use. Skip nitpicks; only flag real duplications.\n\nNote: greenfield app — prefer simplicity, no backwards-compat needed.",
|
||
"status": {
|
||
"kind": "completed",
|
||
"success": true,
|
||
"turns_used": 52
|
||
}
|
||
},
|
||
{
|
||
"agent_id": "ec61ae37",
|
||
"depth": 1,
|
||
"task": "You are reviewing a code change for **code quality** (hacky patterns). The diff is at `/tmp/review.diff` (2945 lines). The repository root is `/home/daytona/workspace/fabro`.\n\nThe change does two things:\n1. Removes `Principal::Anonymous` variant — `Principal` is now actor-only; missing auth becomes `Option<Principal>` in middleware.\n2. Makes run creator/provenance non-optional: `Run.created_by: Principal`, `RunSpec.provenance: RunProvenance`, `RunProvenance.subject: Principal`, `Event::RunCreated.provenance: RunProvenance`.\n\nA new test_support module was added in `lib/crates/fabro-types/src/test_support.rs` with `test_principal()` and `test_run_provenance()`. `apps/fabro-web/app/lib/test-principal.ts` has the TS equivalent.\n\nRead `/tmp/review.diff` and flag hacky patterns:\n\n1. **Redundant state**: Are there fields whose values are always derived from another? E.g. is `Run.created_by` redundant with `RunSpec.provenance.subject` (they should be the same person — both stored)? If yes, flag.\n2. **Parameter sprawl**: New parameters added rather than restructuring. e.g. `create_run_input(provenance: RunProvenance, ...)` vs builder pattern.\n3. **Copy-paste with slight variation**: Are there near-duplicate `RunProvenance { subject: ..., ..Default::default() }` literals across handlers/tests that should share a helper? Look at `run_provenance(headers, subject)` and similar.\n4. **Leaky abstractions**: Look at `principal_middleware.rs` — does the `Option<Principal>` plumbing leak details that should be encapsulated by an enum like `AuthOutcome { Authenticated(Principal), Missing, Invalid }`? Be opinionated.\n5. **Stringly-typed code**: Any new string literals where a constant/enum already exists? Look especially for \"fabro:demo\", \"fabro:test\", \"demo\", \"test-user\" hardcoded in multiple places.\n6. **`Default` derive removal**: The plan says drop `Default` from `RunProvenance`. Are there places still using `..Default::default()` for `RunProvenance` that should now be explicit?\n7. **Demo principal**: Look at `lib/crates/fabro-server/src/demo/mod.rs` — is `DEMO_PRINCIPAL` using `LazyLock<Principal>` or just a function? Is it cloned everywhere when an `&Principal` would do?\n8. **Match arm collapsing**: In server.rs HTTP logging, look at the `None | Some(Agent { .. } | System { .. })` arm — is the pattern clean?\n9. **TS test fixture**: Is `apps/fabro-web/app/lib/test-principal.ts` putting test code in a non-test location? Check filename convention (should be `*.test-utils.ts` or in `__tests__`?).\n\nReport concrete findings with file paths and line refs. Be aggressive — this is greenfield. Skip nitpicks but flag real issues.",
|
||
"status": {
|
||
"kind": "completed",
|
||
"success": true,
|
||
"turns_used": 50
|
||
}
|
||
},
|
||
{
|
||
"agent_id": "1b8d560d",
|
||
"depth": 1,
|
||
"task": "You are reviewing a code change for **efficiency**. The diff is at `/tmp/review.diff` (2945 lines). The repository root is `/home/daytona/workspace/fabro`.\n\nThe change removes `Principal::Anonymous` and makes run creator/provenance fields non-optional.\n\nRead `/tmp/review.diff` and flag efficiency issues:\n\n1. **Unnecessary work / clones**: Was `Option<Principal>` cheap-to-pass while `Principal` is now always cloned in hot paths (HTTP logging, projections, event conversion)? Look for new `.clone()` calls on `Principal`/`RunProvenance` and ask whether a reference would do. Specifically check:\n - `lib/crates/fabro-server/src/server.rs` HTTP logging\n - `lib/crates/fabro-store/src/run_state.rs` `build_summary`/`projection_from_created`\n - `lib/crates/fabro-workflow/src/event/convert.rs`\n - `lib/crates/fabro-server/src/demo/mod.rs` — `DEMO_PRINCIPAL.clone()` calls\n2. **LazyLock overhead**: Is `static DEMO_PRINCIPAL: LazyLock<Principal>` justified, or could it be a const/function? `Principal::user(...)` calls `IdpIdentity::new(...).unwrap()` so it can't be const — confirm LazyLock is necessary.\n3. **N+1 / repeated computation**: Any place doing per-request work that should be cached?\n4. **Hot-path bloat**: Is anything new added to request lifecycle (middleware, per-event projection) that does extra work?\n5. **Overly broad operations**: E.g. cloning a full `Principal` (which has a `User` variant carrying avatar URL, login, email...) when only the kind is needed.\n\nLook closely at the server.rs HTTP logging change to ensure `principal_kind` computation isn't doing redundant work.\n\nReport concrete findings with file paths and line refs.",
|
||
"status": {
|
||
"kind": "completed",
|
||
"success": true,
|
||
"turns_used": 54
|
||
}
|
||
}
|
||
],
|
||
"permission_level": "full",
|
||
"agent_tools": [
|
||
{
|
||
"name": "AskUserQuestion",
|
||
"description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "other",
|
||
"invoked": false
|
||
},
|
||
{
|
||
"name": "TaskCreate",
|
||
"description": "Create pending tasks in the current session. Use concise subjects, descriptions, optional activeForm text, and metadata. Check TaskList first to avoid duplicate tasks.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "other",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "TaskGet",
|
||
"description": "Get one task by taskId, including subject, status, description, owner, blockedBy, and blocks.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "other",
|
||
"invoked": false
|
||
},
|
||
{
|
||
"name": "TaskList",
|
||
"description": "List tasks for the current session, including status, owner, and blocking dependencies. Use TaskGet with a taskId for full description and dependency details.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "other",
|
||
"invoked": false
|
||
},
|
||
{
|
||
"name": "TaskUpdate",
|
||
"description": "Update an existing task's status, text, owner, metadata, or dependencies. Valid statuses are pending, in_progress, completed, and deleted. After completing a task, call TaskList to find newly unblocked work.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "other",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "close_agent",
|
||
"description": "Close a running subagent that is no longer needed.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "subagent",
|
||
"invoked": false
|
||
},
|
||
{
|
||
"name": "edit_file",
|
||
"description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "write",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "glob",
|
||
"description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "read",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "grep",
|
||
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "read",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "read_file",
|
||
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "read",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "send_input",
|
||
"description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "subagent",
|
||
"invoked": false
|
||
},
|
||
{
|
||
"name": "shell",
|
||
"description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "shell",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "spawn_agent",
|
||
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "subagent",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "wait",
|
||
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "subagent",
|
||
"invoked": true
|
||
},
|
||
{
|
||
"name": "web_fetch",
|
||
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "other",
|
||
"invoked": false
|
||
},
|
||
{
|
||
"name": "web_search",
|
||
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "other",
|
||
"invoked": false
|
||
},
|
||
{
|
||
"name": "write_file",
|
||
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
|
||
"source": {
|
||
"kind": "native"
|
||
},
|
||
"category": "write",
|
||
"invoked": false
|
||
}
|
||
],
|
||
"context_window": {
|
||
"provider": "anthropic",
|
||
"model": "claude-opus-4-7",
|
||
"context_window_tokens": 1000000,
|
||
"input_tokens": 82014,
|
||
"usage_percent": 8.2014,
|
||
"count_method": "response_usage_scaled_breakdown",
|
||
"staleness": "live",
|
||
"generated_at": "2026-05-27T05:24:20.661649Z",
|
||
"event_seq": 2244,
|
||
"breakdown": [
|
||
{
|
||
"category": "system_prompt",
|
||
"tokens": 2411,
|
||
"usage_percent": 0.2411
|
||
},
|
||
{
|
||
"category": "tools",
|
||
"tokens": 2791,
|
||
"usage_percent": 0.2791
|
||
},
|
||
{
|
||
"category": "memory",
|
||
"tokens": 5965,
|
||
"usage_percent": 0.5965
|
||
},
|
||
{
|
||
"category": "conversation",
|
||
"tokens": 70839,
|
||
"usage_percent": 7.0839
|
||
},
|
||
{
|
||
"category": "other",
|
||
"tokens": 8,
|
||
"usage_percent": 0.0008
|
||
}
|
||
],
|
||
"warnings": []
|
||
},
|
||
"state": "succeeded"
|
||
},
|
||
"verify@1": {
|
||
"first_event_seq": 2541,
|
||
"prompt": null,
|
||
"response": null,
|
||
"completion": null,
|
||
"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": null,
|
||
"parallel_results": null,
|
||
"output": null,
|
||
"started_at": "2026-05-27T05:30:28.883055Z",
|
||
"handler": "command",
|
||
"usage": {
|
||
"input_tokens": 0,
|
||
"output_tokens": 0,
|
||
"total_tokens": 0,
|
||
"reasoning_tokens": 0,
|
||
"cache_read_tokens": 0,
|
||
"cache_write_tokens": 0
|
||
},
|
||
"state": "running"
|
||
},
|
||
"preflight_compile@1": {
|
||
"first_event_seq": 31,
|
||
"prompt": null,
|
||
"response": null,
|
||
"completion": {
|
||
"outcome": "succeeded",
|
||
"notes": "Script completed: cargo check -q --workspace 2>&1",
|
||
"failure_reason": null,
|
||
"timestamp": "2026-05-27T03:15:21.788423Z"
|
||
},
|
||
"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": 127963,
|
||
"termination": "exited",
|
||
"output_bytes": 0,
|
||
"live_streaming": false
|
||
},
|
||
"parallel_results": null,
|
||
"output": null,
|
||
"output_bytes": 0,
|
||
"live_streaming": false,
|
||
"termination": "exited",
|
||
"started_at": "2026-05-27T03:13:13.814856Z",
|
||
"handler": "command",
|
||
"timing": {
|
||
"wall_time_ms": 127972,
|
||
"inference_time_ms": 0,
|
||
"tool_time_ms": 127963,
|
||
"active_time_ms": 127963
|
||
},
|
||
"usage": {
|
||
"input_tokens": 0,
|
||
"output_tokens": 0,
|
||
"total_tokens": 0,
|
||
"reasoning_tokens": 0,
|
||
"cache_read_tokens": 0,
|
||
"cache_write_tokens": 0
|
||
},
|
||
"state": "succeeded"
|
||
}
|
||
}
|
||
} |