diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index 62d2b1a..3cd9686 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -4,18 +4,16 @@ - Active initiative: `WS-POL-001` - Submission Artifact Policy Foundation - Active planning chunk: none -- Active implementation chunk: `WS-POL-001-09` - OpenAI Agents SDK Only Project - Setup Runtime -- Branch: `codex/ws-pol-001-09-openai-agent-sdk-only` -- Status: `WS-POL-001-09` is in implementation and internal review. This chunk - removes the production `local_fixture` project setup runtime and the old - runtime selector so automatic project setup only uses the OpenAI Agents SDK - runtime behind the Workstream project-agent port. -- Last merged implementation SHA: `0c32c97` -- Last merge commit: `aea7024` -- Current gate: deterministic verification and internal reviewer fanout for - `WS-POL-001-09` -- Next chunk: inactive until `WS-POL-001-09` reaches a human checkpoint +- Active implementation chunk: `WS-POL-001-10` - Pre-Submit Live Drill Hardening +- Branch: `codex/ws-pol-001-10-pre-submit-hardening` +- Status: `WS-POL-001-10` is open as PR #72. Implementation and internal + review are complete; CodeRabbit follow-up fixes are addressed locally and the + branch is awaiting pushed CI reruns and the human merge checkpoint. +- Last merged implementation SHA: `8a524de` +- Last merge commit: `8a524de` +- Current gate: PR #72 external checks after evidence rebind, then human merge + decision +- Next chunk: inactive until `WS-POL-001-10` reaches a human checkpoint ## Operating Rule @@ -38,10 +36,17 @@ through Celery. It did not redesign post-submit policy, review, revision, payment, reputation, blockchain integrations, frontend behavior, or task submission runtime. -The active `WS-POL-001-09` chunk is a corrective hardening chunk for the project -setup runtime boundary. It removes the production fixture adapter and does not -change task, checker, post-submit, review, revision, payment, reputation, -blockchain, frontend, or object-storage behavior. +The merged `WS-POL-001-09` chunk was a corrective hardening chunk for the +project setup runtime boundary. It removed the production fixture adapter and +did not change task, checker, post-submit, review, revision, payment, +reputation, blockchain, frontend, or object-storage behavior. + +The active `WS-POL-001-10` chunk is a corrective hardening chunk for the +pre-submit live API drill. It fixes guide-version conflict mapping, guide-create +source snapshot capture, active-guide checker summary visibility, worker +self-profile onboarding, and failed pre-submit audit evidence. It does not +change post-submit policy, review, revision, payment, reputation, blockchain, +frontend, or agent-runtime behavior. ## Last Review State @@ -94,3 +99,13 @@ blockchain, frontend, or object-storage behavior. - `WS-POL-001-08` external review response is tracked at `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-08-external-review-response.md`. - `WS-POL-001-08` PR trust bundle is tracked at `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-08-pr-trust-bundle.md`. - PR #69 merged into `main` as `aea7024`. +- `WS-POL-001-09` started on branch `codex/ws-pol-001-09-openai-agent-sdk-only` + after the user's explicit correction to remove the production fixture runtime. +- `WS-POL-001-09` internal review evidence is tracked at `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-09-internal-review-evidence.md`. +- `WS-POL-001-09` external review response is tracked at `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-09-external-review-response.md`. +- `WS-POL-001-09` PR trust bundle is tracked at `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-09-pr-trust-bundle.md`. +- PR #71 merged into `main` as `8a524de`. +- `WS-POL-001-10` started after the user's explicit start signal for the first + five pre-submit hardening fixes from the live API drill. +- PR #72 is open from `codex/ws-pol-001-10-pre-submit-hardening`; CodeRabbit + follow-up fixes are applied locally and require CI rerun after push. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/STATUS.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/STATUS.md index 5b5168f..42af727 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/STATUS.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/STATUS.md @@ -3,15 +3,15 @@ ## Current Status `WS-POL-001-01`, `WS-POL-001-02`, `WS-POL-001-03`, `WS-POL-001-04`, -`WS-POL-001-05`, `WS-POL-001-06`, `WS-POL-001-07`, and `WS-POL-001-08` are -merged to `main`. `WS-POL-001-09` is active on -`codex/ws-pol-001-09-openai-agent-sdk-only` after the user's explicit correction -to remove the production fixture runtime and keep project setup on the OpenAI -Agents SDK runtime. +`WS-POL-001-05`, `WS-POL-001-06`, `WS-POL-001-07`, `WS-POL-001-08`, and +`WS-POL-001-09` are merged to `main`. `WS-POL-001-10` is open as PR #72 from +`codex/ws-pol-001-10-pre-submit-hardening` after the user's explicit start +signal for the first five pre-submit hardening fixes from the real Terminal +Benchmark API drill. ## Active Chunk -`WS-POL-001-09` - OpenAI Agents SDK Only Project Setup Runtime. +`WS-POL-001-10` - Pre-Submit Live Drill Hardening. ## Chunk Status @@ -25,13 +25,14 @@ Agents SDK runtime. | `WS-POL-001-06` | Merged | `codex/ws-pol-001-06-terminal-benchmark-drill` | 67 | Hardens the Terminal Benchmark proof harness and removes stale project guide/payment contracts before continuing pre-submit checker work. | | `WS-POL-001-07` | Merged | `codex/ws-pol-001-07-task-contract-cleanup` | 68 | Removes task-owned `required_files`/`required_evidence` from request/response/model/migration and keeps artifact requirements project-policy driven. | | `WS-POL-001-08` | Merged | `codex/ws-pol-001-08-celery-project-setup` | 69 | Makes guide/source capture enqueue Celery pre-submit setup automatically: sufficiency first, blocked stops, draft submission artifact policy next; removes remaining construction-state compatibility surfaces. | -| `WS-POL-001-09` | In review | `codex/ws-pol-001-09-openai-agent-sdk-only` | - | Removes the production `local_fixture` project setup runtime and old runtime selector; keeps deterministic test behavior in explicit test-local fakes only. | +| `WS-POL-001-09` | Merged | `codex/ws-pol-001-09-openai-agent-sdk-only` | 71 | Removes the production `local_fixture` project setup runtime and old runtime selector; keeps deterministic test behavior in explicit test-local fakes only. | +| `WS-POL-001-10` | PR open; CodeRabbit fixes applied locally | `codex/ws-pol-001-10-pre-submit-hardening` | 72 | Hardens duplicate guide-version conflicts, guide-create source snapshots, active-guide checker summaries, worker self-profile onboarding, and failed pre-submit audit evidence. | ## Blockers | Blocker | Owner | Next action | |---|---|---| -| None | - | Complete deterministic verification and internal review for `WS-POL-001-09`. | +| None | - | Push evidence-rebound follow-up, rerun PR #72 CI, then human checkpoint for `WS-POL-001-10`. | ## Follow-Ups diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-10-pre-submit-live-drill-hardening.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-10-pre-submit-live-drill-hardening.md new file mode 100644 index 0000000..5b46b07 --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-10-pre-submit-live-drill-hardening.md @@ -0,0 +1,150 @@ +# Chunk Contract: WS-POL-001-10 - Pre-Submit Live Drill Hardening + +## Parent initiative + +WS-POL-001 - Submission Artifact Policy Foundation + +## Goal + +Fix the concrete pre-submit setup and intake gaps found during the real +Terminal Benchmark API drill, without expanding into post-submit review, +revision, payment, or agent-runtime redesign. + +## Why this chunk exists + +The live drill proved the project guide -> agent setup -> policy approval -> +task lock -> pre-submit path, but it also exposed several places where the API +still behaved like an internal test harness instead of a real operator/worker +workflow. This chunk hardens those seams before we continue post-submit work. + +## Approved plan reference + +- INTENT: + `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/INTENT.md` +- PLAN: + `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/PLAN.md` +- CHUNK_MAP: + `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/CHUNK_MAP.md` + +## Risk class + +L1 + +## SLA + +P1 + +## Allowed files + +```text +backend/app/modules/projects/schemas.py +backend/app/modules/projects/service.py +backend/app/modules/tasks/router.py +backend/app/modules/tasks/schemas.py +backend/app/modules/tasks/service.py +backend/tests/test_projects.py +backend/tests/test_tasks.py +docs/architecture_data_model.md +docs/architecture_lockdown.md +docs/decision_0011_submission_artifact_policy_drives_pre_submit.md +docs/spec_chunk_4_task_queue_assignment.md +.agent-loop/LOOP_STATE.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/STATUS.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-10-pre-submit-live-drill-hardening.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-* +``` + +## Not allowed + +```text +backend/alembic/** +backend/app/adapters/project_agents/** +backend/app/interfaces/project_agents.py +backend/app/modules/checkers/** +backend/app/modules/projects/models.py +backend/app/modules/projects/repository.py +backend/app/modules/tasks/models.py +backend/app/modules/tasks/repository.py +docs/** except the explicitly allowed files above +.github/workflows/** +demos/** +frontend/** +review lifecycle implementation +revision lifecycle implementation +payment/reputation/blockchain code +post-submit checker policy redesign +agent runtime provider replacement +new scripts or fake local fixtures +``` + +## Acceptance criteria + +- [ ] Duplicate guide versions return a mapped conflict response instead of an + unhandled database error. +- [ ] Project guide creation can include source-snapshot items, including + representative task examples, so the automatic sufficiency and derivation + agents receive the same material captured at guide setup time. +- [ ] The active-guide response exposes project pre-submit checker names and + checker configs, while still excluding the compiled checker bundle body. +- [ ] Workers can create or refresh their own active worker profile through a + public authenticated worker API before claiming a task; workers without a + profile remain blocked from claiming. +- [ ] A failed pre-submit submission attempt creates no submission version but + writes a durable task audit event containing the structured checker + result for project operators. +- [ ] Request bodies remain fail-closed through Pydantic `extra="forbid"`; no + legacy project guide artifact fields are reintroduced. + +## Verification commands + +```bash +cd backend && .venv/bin/python -m ruff check app/modules/projects/schemas.py app/modules/projects/service.py app/modules/tasks/router.py app/modules/tasks/schemas.py app/modules/tasks/service.py tests/test_projects.py tests/test_tasks.py +cd backend && .venv/bin/python -m pytest tests/test_projects.py -k 'duplicate_guide_version_returns_conflict or guide_creation_accepts_source_snapshot_items_for_agent_material or guide_activation_and_active_guide_retrieval' -q +cd backend && .venv/bin/python -m pytest tests/test_tasks.py -k 'worker_can_create_profile_before_claiming_task or worker_without_profile_cannot_claim_ready_task or pre_submit_failure_writes_audit_event_without_submission' -q +cd backend && .venv/bin/docstr-coverage app/modules/projects app/modules/tasks --config .docstr.yaml +python3 scripts/check_stale_workstream_wording.py +python3 scripts/check_markdown_links.py +git diff --check +``` + +## Required reviewers + +Every listed reviewer must end with one exact result value: + +- `PASS` +- `PASS AFTER FIXES` +- `PASS WITH LOW RISKS` +- `N/A - with approved reason` + +Required: + +- [ ] senior engineering +- [ ] QA/test +- [ ] security/auth +- [ ] product/ops +- [ ] architecture +- [ ] reuse/dedup +- [ ] test delta + +## Human review focus + +- The worker profile endpoint is authenticated and worker-owned, not a hidden + seed path. +- Guide creation source items are captured as immutable snapshot material and + do not revive legacy guide-owned artifact fields. +- Failed pre-submit attempts remain user-facing as + `pre_submission_checker_failed`, create no submission row, and leave operator + audit evidence. +- Active-guide output gives enough checker visibility for operators without + leaking the compiled bundle body. + +## Stop conditions + +Stop and escalate if: + +- fixing any item requires database migrations +- the project-agent runtime contract needs to change +- post-submit/review/revision/payment scope is required +- tests or CI must be weakened to pass +- same blocker remains after 2 repair attempts +- secrets or production data are needed diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-external-review-response.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-external-review-response.md new file mode 100644 index 0000000..dcda4ee --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-external-review-response.md @@ -0,0 +1,41 @@ +# External Review Response: WS-POL-001-10 + +## Scope + +External review feedback for PR #72: `Harden pre-submit live drill flows`. + +Internal sub-agent evidence is tracked separately in: + +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-internal-review-evidence.md` + +## CodeRabbit Review + +CodeRabbit reviewed PR #72 on 2026-07-06 and posted three actionable comments. + +| Source | Finding | Severity | Status | Response | +|---|---|---:|---|---| +| CodeRabbit | Internal review evidence was not in the parser-required format; the `test delta` row and reviewed revision metadata could not pass `scripts/check_internal_review_evidence.py`. | Major | Fixed | Rebound evidence to reviewed code SHA `1b7d2e889fe1fd0460349b77c37e643a4e0c4cb0`, normalized reviewer rows to known tracks, used parseable reviewer run IDs, and limited post-review changes to evidence/status files. | +| CodeRabbit | `POST /api/v1/workers/me/profile` used `response_model_exclude_none=True`, dropping required nullable `display_name` and `email` fields. | Minor | Fixed | Removed `response_model_exclude_none=True` from the route and added `test_worker_profile_response_includes_nullable_identity_fields`. | +| CodeRabbit | `POST /api/v1/workers/me/profile` was documented in Security/Auth but missing from the chunk 4 endpoint inventory. | Minor | Fixed | Added the endpoint to `docs/spec_chunk_4_task_queue_assignment.md` under API Impact. | + +## Verification + +```bash +cd backend && .venv/bin/python -m ruff check tests/test_tasks.py app/modules/tasks/router.py +cd backend && .venv/bin/python -m pytest tests/test_tasks.py -k 'worker_can_create_profile_before_claiming_task or worker_profile_response_includes_nullable_identity_fields or worker_profile_request_is_fail_closed_and_validated or worker_profile_requires_worker_role or worker_without_profile_cannot_claim_ready_task or pre_submit_failure_writes_audit_event_without_submission' -q +python3 scripts/check_markdown_links.py docs/spec_chunk_4_task_queue_assignment.md +python3 scripts/check_stale_workstream_wording.py +git diff --check +``` + +Results: + +- ruff: passed. +- Focused worker/pre-submit task tests: 6 passed, 59 deselected. +- Markdown link check: passed for 10 changed Markdown files. +- Stale wording scan: passed. +- Diff whitespace check: passed. + +## Remaining External State + +GitHub Actions and CodeRabbit must rerun after this response and fix are pushed. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-internal-review-evidence.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-internal-review-evidence.md new file mode 100644 index 0000000..4900e0c --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-internal-review-evidence.md @@ -0,0 +1,111 @@ +# Internal Review Evidence: WS-POL-001-10 + +## Chunk + +WS-POL-001-10 + +open sub-agent sessions: none + +valid findings addressed: yes + +## Reviewed Revision + +Reviewed code SHA: 1b7d2e889fe1fd0460349b77c37e643a4e0c4cb0 + +Reviewed working diff digest before evidence files: +67e2b7561d3688f6e588e5abe09317104da3bbd66ec97231f9f49b886801903b + +Reviewed at: 2026-07-06T10:38:01Z + +Reviewer run IDs: senior-engineering-review-019f36f1-72aa-7bd2-921e-2e667b03b7d3, qa-test-review-019f36f1-74e2-7d73-b9f5-7a5415f49133, security-auth-review-019f36f1-793d-7b53-b7c6-d12e71063ac3, product-ops-review-019f36f1-7de2-7fe1-a9ea-a3c46fd34605, docs-review-019f36f1-8264-7c33-8448-c7c470678a9b, test-delta-review-019f36f1-86b1-7e93-947b-2d36ef32c931, qa-rereview-019f36fd-6300-7081-84ae-f4bb6072a142, architecture-review-019f36fd-691c-7181-93c7-c0691bcc7078, reuse-dedup-review-019f36fd-7241-77a1-9da3-6aa31e10e7ee, ci-integrity-review-019f36fd-89df-7a41-bb1c-53d86e08f122, test-delta-rereview-019f36fd-aa2e-7ff0-a997-45d4b3824f48 + +After the reviewed SHA, only evidence and status files changed. + +## Reviewed Change + +Scope: + +- Maps duplicate guide versions to a 409 conflict instead of an unhandled DB + error. +- Allows guide creation to include immutable source-snapshot items, including + representative task examples, so the existing automatic setup pipeline has + full setup material. +- Exposes project pre-submit checker names and configs in the active-guide + summary while still excluding the compiled checker bundle body. +- Adds `POST /api/v1/workers/me/profile` so workers can create or refresh their + own active profile from verified Flow identity before claim. +- Writes durable `pre_submission_check_failed` task audit evidence when + submission creation is blocked by pre-submit checks, while creating no + submission version. +- Updates standing docs to distinguish no submission-created audit event from + the new failed-attempt task audit evidence. + +## Reviewer Results + +| Reviewer | Result | Blocking findings | Notes | +|---|---:|---|---| +| senior engineering | PASS WITH LOW RISKS | None | Confirmed the CodeRabbit follow-up route and docs changes are minimal; evidence rebind required before merge. | +| QA/test | PASS | None | Initial review requested nullable worker profile response coverage; rereview confirmed `display_name` and `email` stay serialized as null. | +| security/auth | PASS WITH LOW RISKS | None | Verified worker profile identity is derived from `ActorContext`, role-gated, bounded in audit exposure, and nullable self-profile fields are not a new exposure. | +| product/ops | PASS | None | Confirmed worker-owned profile setup, pre-submit failure audit evidence, and naming stay aligned with Workstream workflow. | +| architecture | PASS WITH LOW RISKS | None | Confirmed route/docs/test changes stay inside WS-POL-001-10 and do not change pre-submit architecture boundaries. | +| reuse/dedup | PASS WITH LOW RISKS | None | Implementation reuses `TaskRepository.upsert_worker_profile`; evidence gate issue addressed in this evidence-only update. | +| docs | PASS WITH LOW RISKS | None | Confirmed worker profile endpoint inventory and task assignment docs; noted loop metadata cleanup, addressed here. | +| ci integrity | PASS AFTER FIXES | None | Initial review required evidence rebinding to reviewed code SHA `1b7d2e889fe1fd0460349b77c37e643a4e0c4cb0`; this evidence-only update applies it. | +| test delta | PASS | None | Reviewed code sha `1b7d2e889fe1fd0460349b77c37e643a4e0c4cb0`; reviewer run ids `019f36f1-86b1-7e93-947b-2d36ef32c931`, `019f36fd-aa2e-7ff0-a997-45d4b3824f48`; confirmed the nullable identity-field test is meaningful and no assertions were weakened. | + +## Valid Findings Addressed + +- Narrowed `create_guide` duplicate-version handling so `GuideVersionConflict` + maps only the guide insert/flush integrity failure, not later optional policy + or source-snapshot persistence. +- Added worker profile skill-tag normalization: strip, lowercase, dedupe, reject + blank tags, and cap each tag at 64 characters. +- Added worker profile request negative tests for unknown client identity fields, + blank tags, overlong tags, non-worker role access, and public refresh behavior. +- Added worker profile response contract coverage proving nullable + `display_name` and `email` fields remain serialized as explicit null values. +- Updated docs to state that failed pre-submit submission-create attempts write + `pre_submission_check_failed` task audit evidence while creating no + `Submission`, submission version, task transition to `submitted`, or + submission-created audit event. +- Updated task assignment docs to document the worker-owned profile create or + refresh endpoint and its Flow-token-derived identity boundary. +- Updated the chunk contract to allow only the specific standing docs needed by + the docs reviewer findings. +- Rebound internal review evidence to implementation commit + `1b7d2e889fe1fd0460349b77c37e643a4e0c4cb0`; only evidence and status files + changed after that reviewed SHA. + +## Commands Run + +```bash +cd backend && .venv/bin/python -m ruff check app/modules/projects/schemas.py app/modules/projects/service.py app/modules/tasks/router.py app/modules/tasks/schemas.py app/modules/tasks/service.py tests/test_projects.py tests/test_tasks.py +cd backend && .venv/bin/python -m pytest tests/test_projects.py -k 'duplicate_guide_version_returns_conflict or guide_creation_accepts_source_snapshot_items_for_agent_material or guide_activation_and_active_guide_retrieval' -q +cd backend && .venv/bin/python -m pytest tests/test_tasks.py -k 'worker_can_create_profile_before_claiming_task or worker_profile_response_includes_nullable_identity_fields or worker_profile_request_is_fail_closed_and_validated or worker_profile_requires_worker_role or worker_without_profile_cannot_claim_ready_task or pre_submit_failure_writes_audit_event_without_submission' -q +cd backend && .venv/bin/docstr-coverage app/modules/projects app/modules/tasks --config .docstr.yaml +python3 scripts/check_markdown_links.py docs/spec_chunk_4_task_queue_assignment.md +python3 scripts/check_stale_workstream_wording.py +git diff --check +cd backend && .venv/bin/python -m pytest tests/test_projects.py tests/test_tasks.py tests/test_checkers.py -q +``` + +Results: + +- ruff: passed. +- Focused project tests: 3 passed, 196 deselected. +- Focused task tests: 6 passed, 59 deselected. +- Docstring coverage: 100.0%. +- Markdown link check: passed for 10 changed Markdown files. +- Stale wording scan: passed. +- Diff whitespace check: passed. +- Full project/task/checker suite before the final nullable-response test + addition: 310 passed in 41:18; final focused worker/pre-submit rerun passed. + +## Remaining Risks + +- The worker and reviewer profile construction paths still have small duplicate + actor-claim mapping logic. This is intentionally left as-is until another + profile path appears; adding an abstraction now would not simplify the chunk. +- This chunk does not implement post-submit policy derivation or review + lifecycle behavior. That remains out of scope. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-pr-trust-bundle.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-pr-trust-bundle.md new file mode 100644 index 0000000..1463055 --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-pr-trust-bundle.md @@ -0,0 +1,194 @@ +# PR Trust Bundle: WS-POL-001-10 + +## Chunk + +`WS-POL-001-10` - Pre-Submit Live Drill Hardening + +## Goal + +Fix the concrete pre-submit setup and intake gaps found during the real +Terminal Benchmark API drill before continuing post-submit work. + +## Human-Approved Intent + +The user explicitly asked to fix the first five pre-submit issues before moving +forward: + +1. Duplicate guide versions must not bubble as 500s. +2. Worker profile setup must be possible through a real authenticated API, not + direct DB seeding. +3. Project guide creation must support captured source material and + representative task examples so agents are not forced to reason from guide + markdown alone. +4. Active guide reads must show enough project pre-submit checker context for + operators. +5. Failed pre-submit submission attempts must create no submission but still + leave durable operator audit evidence. + +Chunk contract: + +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-10-pre-submit-live-drill-hardening.md` + +## What Changed + +- Added `GuideVersionConflict` and mapped duplicate guide insert conflicts to + HTTP 409. +- Added optional `source_snapshot` to guide creation, reusing the existing + source snapshot capture and automatic setup enqueue path. +- Added `checker_names` and `checker_configs` to the active-guide + `PreSubmitCheckerPolicySummaryResponse`; `compiled_bundle` remains excluded. +- Added `WorkerProfileUpsertRequest`, `WorkerProfileResponse`, and + `POST /api/v1/workers/me/profile`. +- Added worker skill-tag normalization and fail-closed validation. +- Added durable `pre_submission_check_failed` task audit events for blocked + submission-create attempts. +- Updated tests for all five acceptance criteria and reviewer-requested edge + cases. +- Updated standing docs for worker self-profile setup and failed pre-submit + audit semantics. +- Updated loop state/status and added this chunk contract. + +## Why It Changed + +The live Terminal Benchmark drill showed the lifecycle was mostly wired, but +parts still behaved like tests rather than a real Workstream workflow. This +chunk removes those rough edges while keeping the architecture project-scoped: +one active guide/source snapshot/effective project policy/project +`PreSubmitCheckerPolicy`, with tasks locking references to that context. + +## Design Chosen + +- Keep guide source material capture in the project module and reuse + `_create_guide_source_snapshot_model`. +- Keep worker profile creation in the task module because claiming and worker + assignment already depend on `WorkerProfile`. +- Derive worker identity only from verified Flow actor context. +- Keep failed pre-submit outcomes as `pre_submission_checker_failed` for + submission-create responses and `pre_submission_check_failed` for internal + task audit evidence. +- Expose checker names/configs to operators but not compiled checker bundle + bodies. + +## Alternatives Rejected + +- Direct DB seeding for workers in real flows: rejected because it does not + represent a human/API workflow. +- Reintroducing guide-owned artifact fields: rejected because + `SubmissionArtifactPolicy` remains the artifact intake contract. +- Creating a submission row for failed pre-submit attempts: rejected because a + failed pre-submit check means no valid submission exists yet. +- Broadly documenting all APIs in this chunk: rejected; only docs affected by + changed public/operator behavior were updated. + +## Scope Control + +No migrations, checker internals, project-agent runtime changes, post-submit +policy redesign, review lifecycle, revision lifecycle, payment, reputation, +blockchain, frontend, or demo behavior was changed. + +## Product Behavior + +- Workers can create or refresh their own active profile before claiming a task. +- Workers without an active profile still cannot claim. +- Project operators can see checker names/configs on active guide reads. +- Blocked pre-submit submission-create attempts return + `pre_submission_checker_failed`, create no submission, and write + `pre_submission_check_failed` audit evidence for project operators. +- Product review decisions remain only `accept`, `needs_revision`, and `reject`. + +## Acceptance Criteria Proof + +- Duplicate guide version conflict: tested by + `test_duplicate_guide_version_returns_conflict`. +- Guide-create source snapshot material: tested by + `test_guide_creation_accepts_source_snapshot_items_for_agent_material`. +- Active-guide checker summary: tested by + `test_guide_activation_and_active_guide_retrieval`. +- Worker self-profile create/refresh and claim: tested by + `test_worker_can_create_profile_before_claiming_task`. +- Worker profile fail-closed validation and role gate: tested by + `test_worker_profile_request_is_fail_closed_and_validated` and + `test_worker_profile_requires_worker_role`. +- Nullable worker identity response contract: tested by + `test_worker_profile_response_includes_nullable_identity_fields`. +- Failed pre-submit audit without submission: tested by + `test_pre_submit_failure_writes_audit_event_without_submission`. + +## Tests/Checks Run + +```bash +cd backend && .venv/bin/python -m ruff check app/modules/projects/schemas.py app/modules/projects/service.py app/modules/tasks/router.py app/modules/tasks/schemas.py app/modules/tasks/service.py tests/test_projects.py tests/test_tasks.py +cd backend && .venv/bin/python -m pytest tests/test_projects.py -k 'duplicate_guide_version_returns_conflict or guide_creation_accepts_source_snapshot_items_for_agent_material or guide_activation_and_active_guide_retrieval' -q +cd backend && .venv/bin/python -m pytest tests/test_tasks.py -k 'worker_can_create_profile_before_claiming_task or worker_profile_response_includes_nullable_identity_fields or worker_profile_request_is_fail_closed_and_validated or worker_profile_requires_worker_role or worker_without_profile_cannot_claim_ready_task or pre_submit_failure_writes_audit_event_without_submission' -q +cd backend && .venv/bin/docstr-coverage app/modules/projects app/modules/tasks --config .docstr.yaml +python3 scripts/check_markdown_links.py +python3 scripts/check_stale_workstream_wording.py +git diff --check +cd backend && .venv/bin/python -m pytest tests/test_projects.py tests/test_tasks.py tests/test_checkers.py -q +``` + +Result summary: + +- ruff: passed. +- Focused project tests: 3 passed. +- Focused task tests: 6 passed. +- Docstring coverage: 100.0%. +- Markdown link check: passed for 10 changed Markdown files. +- Stale wording scan: passed. +- Diff whitespace check: passed. +- Full project/task/checker suite before the final nullable-response test + addition: 310 passed; final focused worker/pre-submit rerun passed. + +## Reviewer Results + +Internal review evidence: + +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-internal-review-evidence.md` + +Reviewed code SHA: `1b7d2e889fe1fd0460349b77c37e643a4e0c4cb0` + +Reviewed diff digest before evidence files: +`67e2b7561d3688f6e588e5abe09317104da3bbd66ec97231f9f49b886801903b` + +| Reviewer | Result | Blocking findings | Notes | +|---|---:|---|---| +| senior engineering | PASS WITH LOW RISKS | None | Findings on conflict mapping and skill tags addressed. | +| QA/test | PASS | None | Requested nullable worker identity response coverage; addressed and passed rereview. | +| security/auth | PASS WITH LOW RISKS | None | Verified Flow-derived identity, role gate, bounded audit exposure, and nullable self-profile fields. | +| product/ops | PASS | None | Confirmed worker-owned profile setup, pre-submit failure audit evidence, and naming. | +| architecture | PASS WITH LOW RISKS | None | Broad `IntegrityError` concern addressed. | +| reuse/dedup | PASS WITH LOW RISKS | None | Minor profile construction duplication accepted. | +| docs | PASS WITH LOW RISKS | None | Endpoint inventory and loop metadata cleanup confirmed. | +| ci integrity | PASS AFTER FIXES | None | Evidence rebound to reviewed code SHA; only evidence/status files changed afterward. | +| test delta | PASS | None | Confirmed nullable identity-field regression test is meaningful and no assertions were weakened. | + +## External Review + +External review response: + +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-10-external-review-response.md` + +CodeRabbit found three valid follow-up issues: evidence metadata, worker profile +response serialization, and endpoint inventory documentation. All are addressed. +GitHub Actions must rerun after the follow-up push. + +## Remaining Risks + +- Small duplication remains between worker/reviewer profile construction. It is + not worth abstracting until another profile path appears. +- Post-submit policy derivation is still future work and intentionally not part + of this chunk. + +## Human Review Focus + +- Confirm the worker profile endpoint is the right product boundary for + authenticated workers before claim. +- Confirm failed pre-submit audit evidence is useful without confusing it with a + product review decision. +- Confirm active-guide checker summary exposes enough operator context while + keeping compiled bundles private. + +## Human Merge Ownership + +Only the user can approve and merge this PR. Codex must not merge it without +explicit user approval for that specific PR. diff --git a/backend/app/modules/projects/schemas.py b/backend/app/modules/projects/schemas.py index 4b78f08..669d421 100644 --- a/backend/app/modules/projects/schemas.py +++ b/backend/app/modules/projects/schemas.py @@ -370,6 +370,8 @@ class PreSubmitCheckerPolicySummaryResponse(BaseModel): lifecycle_status: str compiler_version: str | None compiled_bundle_hash: str | None + checker_names: list[str] + checker_configs: dict[str, Any] created_by: str created_at: datetime supersedes_pre_submit_checker_policy_id: str | None @@ -408,6 +410,7 @@ class ProjectGuideCreate(BaseModel): version: str content_markdown: str change_summary: str | None = None + source_snapshot: GuideSourceSnapshotCreate | None = None post_submit_checker_policy: PostSubmitCheckerPolicyInput | None = None review_policy: ReviewPolicyInput | None = None revision_policy: RevisionPolicyInput | None = None diff --git a/backend/app/modules/projects/service.py b/backend/app/modules/projects/service.py index 0d79f88..4748bad 100644 --- a/backend/app/modules/projects/service.py +++ b/backend/app/modules/projects/service.py @@ -283,6 +283,12 @@ class GuideEditBlocked(ProjectServiceError): status_code = 409 +class GuideVersionConflict(ProjectServiceError): + """Raised when a guide version already exists for a project.""" + + status_code = 409 + + class GuideActivationConflict(ProjectServiceError): """Raised when another transaction wins the guide activation race.""" @@ -443,6 +449,7 @@ async def create_guide( Raises: PermissionDenied: If the actor cannot manage project setup. ProjectNotFound: If the parent project is unknown. + GuideVersionConflict: If the project already has the requested guide version. """ require_any_role(actor, PROJECT_SETUP_ROLES) project = await self._repo.get_project(project_id) @@ -458,19 +465,27 @@ async def create_guide( change_summary=payload.change_summary, created_by=actor.actor_id, ) - guide = await self._repo.add_guide(guide) - await self._upsert_optional_policies(project_id, payload.version, payload) + source_snapshot_payload = payload.source_snapshot source_snapshot: GuideSourceSnapshot | None = None - if get_settings().project_setup_pipeline_autostart: + try: + guide = await self._repo.add_guide(guide) + except IntegrityError as exc: + await self._session.rollback() + raise GuideVersionConflict("guide version already exists for project") from exc + await self._upsert_optional_policies(project_id, payload.version, payload) + if ( + source_snapshot_payload is not None + or get_settings().project_setup_pipeline_autostart + ): source_snapshot = await self._create_guide_source_snapshot_model( actor, project_id, guide, - GuideSourceSnapshotCreate(), + source_snapshot_payload or GuideSourceSnapshotCreate(), ) await self._session.commit() await self._session.refresh(guide) - if source_snapshot is not None: + if source_snapshot is not None and get_settings().project_setup_pipeline_autostart: await self._enqueue_pre_submit_setup_pipeline_after_commit( project_id=project_id, guide_id=guide.id, diff --git a/backend/app/modules/tasks/router.py b/backend/app/modules/tasks/router.py index be6bde3..4808abd 100644 --- a/backend/app/modules/tasks/router.py +++ b/backend/app/modules/tasks/router.py @@ -19,6 +19,8 @@ TaskResponse, TaskTransitionRequest, TaskWithAssignmentResponse, + WorkerProfileResponse, + WorkerProfileUpsertRequest, ) from app.modules.tasks.service import TaskService, TaskServiceError from app.schemas.auth import ActorContext @@ -79,6 +81,24 @@ def permission_http_error(exc: PermissionDenied) -> HTTPException: return HTTPException(status_code=403, detail=str(exc)) +@router.post( + "/workers/me/profile", + response_model=WorkerProfileResponse, +) +async def ensure_worker_profile( + payload: WorkerProfileUpsertRequest, + actor: Annotated[ActorContext, Depends(get_current_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> WorkerProfileResponse: + """Create or refresh the current worker profile from trusted Flow identity.""" + try: + return await TaskService(session).ensure_worker_profile(actor, payload) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except TaskServiceError as exc: + raise task_http_error(exc) from exc + + @router.post( "/projects/{project_id}/tasks", response_model=TaskResponse, diff --git a/backend/app/modules/tasks/schemas.py b/backend/app/modules/tasks/schemas.py index e08f1c0..c45ecf7 100644 --- a/backend/app/modules/tasks/schemas.py +++ b/backend/app/modules/tasks/schemas.py @@ -96,6 +96,48 @@ class TaskTransitionRequest(BaseModel): reason: str | None = None +class WorkerProfileUpsertRequest(BaseModel): + """Request schema for creating or refreshing the current worker profile.""" + + model_config = ConfigDict(extra="forbid") + + skill_tags: list[str] = Field(default_factory=list, max_length=100) + + @field_validator("skill_tags") + @classmethod + def normalize_skill_tags(cls, value: list[str]) -> list[str]: + """Normalize worker skill tags before they enter profile metadata.""" + normalized_tags: list[str] = [] + seen_tags: set[str] = set() + for raw_tag in value: + tag = raw_tag.strip().lower() + if not tag: + raise ValueError("skill_tags cannot include empty values") + if len(tag) > 64: + raise ValueError("skill_tags values must be 64 characters or fewer") + if tag not in seen_tags: + normalized_tags.append(tag) + seen_tags.add(tag) + return normalized_tags + + +class WorkerProfileResponse(BaseModel): + """Response schema for a worker profile derived from Flow identity.""" + + model_config = ConfigDict(from_attributes=True) + + id: str + actor_id: str + external_subject: str + external_issuer: str + display_name: str | None + email: str | None + skill_tags: list[str] + status: str + created_at: datetime + updated_at: datetime + + class EvidenceItemCreate(BaseModel): """Request schema for one evidence item in a submission packet.""" diff --git a/backend/app/modules/tasks/service.py b/backend/app/modules/tasks/service.py index 7d596f8..2d81ead 100644 --- a/backend/app/modules/tasks/service.py +++ b/backend/app/modules/tasks/service.py @@ -52,6 +52,8 @@ TaskCreate, TaskResponse, TaskWithAssignmentResponse, + WorkerProfileResponse, + WorkerProfileUpsertRequest, ) from app.schemas.auth import ActorContext @@ -497,9 +499,18 @@ async def create_submission( except CheckerServiceError as exc: raise SubmissionCheckerGateError(str(exc), exc.status_code) from exc if not pre_submit_response.eligible_to_submit: - raise PreSubmissionCheckerFailed( - pre_submit_response.model_dump(mode="json"), + pre_submit_details = pre_submit_response.model_dump(mode="json") + await self._write_task_audit( + actor, + task, + event_type="pre_submission_check_failed", + from_status=task.status, + to_status=task.status, + reason=None, + event_payload={"pre_submit_check": pre_submit_details}, ) + await self._session.commit() + raise PreSubmissionCheckerFailed(pre_submit_details) latest_submission = await self._repo.get_latest_submission_for_task(task.id) next_version = 1 if latest_submission is None else latest_submission.version + 1 @@ -1057,6 +1068,37 @@ async def _require_active_worker_profile( raise WorkerProfileRequired("active worker profile is required to claim task") return profile + async def ensure_worker_profile( + self, + actor: ActorContext, + payload: WorkerProfileUpsertRequest, + ) -> WorkerProfileResponse: + """Create or refresh a worker profile for the current actor. + + Args: + actor: Verified Flow actor context. + payload: Worker-controlled skill tags for task matching. + + Returns: + Persisted active worker profile response. + """ + require_any_role(actor, {"worker"}) + profile = await self._repo.upsert_worker_profile( + WorkerProfile( + id=str(uuid4()), + actor_id=actor.actor_id, + external_subject=actor.external_subject, + external_issuer=actor.external_issuer, + display_name=actor.display_name, + email=actor.email, + skill_tags=payload.skill_tags, + status="active", + ) + ) + await self._session.commit() + await self._session.refresh(profile) + return WorkerProfileResponse.model_validate(profile) + async def ensure_reviewer_profile( self, actor: ActorContext, diff --git a/backend/tests/test_projects.py b/backend/tests/test_projects.py index f73df3c..e7eb5d0 100644 --- a/backend/tests/test_projects.py +++ b/backend/tests/test_projects.py @@ -1088,6 +1088,94 @@ async def test_draft_guide_can_be_created(project_client: AsyncClient) -> None: ) +async def test_duplicate_guide_version_returns_conflict(project_client: AsyncClient) -> None: + project = await create_project(project_client) + await create_guide(project_client, project["id"], complete_guide_payload("v1")) + + response = await project_client.post( + f"/api/v1/projects/{project['id']}/guides", + headers=auth_headers(), + json=complete_guide_payload("v1"), + ) + + assert response.status_code == 409 + assert response.json()["detail"] == "guide version already exists for project" + + +async def test_guide_creation_accepts_source_snapshot_items_for_agent_material( + project_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, GuideSourceMaterial] = {} + + class CapturingRuntime: + """Runtime that records material supplied to the sufficiency agent.""" + + async def analyze_guide_sufficiency( + self, + material: GuideSourceMaterial, + ) -> GuideSufficiencyAgentResult: + """Capture material and return a passing guide report.""" + captured["material"] = material + return GuideSufficiencyAgentResult( + status="guide_sufficient", + findings=[], + summary="Captured guide creation source material.", + agent_version="capture-v0", + ) + + async def derive_submission_artifact_policy( + self, + _: GuideSourceMaterial, + __: GuideSufficiencyAgentResult, + ) -> SubmissionArtifactPolicyDerivationResult: + """Unused derivation implementation required by the runtime protocol.""" + raise AssertionError("derivation is not part of this test") + + monkeypatch.setattr( + project_service_module, + "get_project_guide_agent_runtime", + lambda: CapturingRuntime(), + ) + project = await create_project(project_client) + payload = complete_guide_payload() + payload["source_snapshot"] = source_snapshot_payload() + payload["source_snapshot"]["items"].append( + { + "source_kind": "representative_task", + "durable_ref": "inline:/examples/tasks/stem/sample-1", + "ingestion_adapter": "manual_import", + "content_hash": sha256_hash("guide-create-representative-task"), + "media_type": "application/json", + "content_excerpt": "Representative task: solve a STEM prompt and submit evidence.", + } + ) + guide = await create_guide(project_client, project["id"], payload) + async with db_session.get_session_factory()() as session: + snapshot = await session.scalar( + select(GuideSourceSnapshot).where(GuideSourceSnapshot.guide_id == guide["id"]) + ) + assert snapshot is not None + snapshot_id = snapshot.id + + response = await project_client.post( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/source-snapshots/" + f"{snapshot_id}/run-sufficiency-agent", + headers=auth_headers(), + ) + + assert response.status_code == 201, response.text + material = captured["material"] + assert material.source_snapshot_id == snapshot_id + assert len(material.representative_task_material.items) == 1 + representative_task = material.representative_task_material.items[0] + assert representative_task.source_kind == "representative_task" + assert representative_task.durable_ref == "inline:/examples/tasks/stem/sample-1" + assert representative_task.content_excerpt == ( + "Representative task: solve a STEM prompt and submit evidence." + ) + + async def test_project_guide_rejects_unknown_non_contract_fields( project_client: AsyncClient, ) -> None: @@ -4501,8 +4589,12 @@ async def test_guide_activation_and_active_guide_retrieval(project_client: Async bundle["pre_submit_checker_policy"]["compiled_bundle_hash"] ) assert "compiled_bundle" not in active.json()["pre_submit_checker_policy"] - assert "checker_configs" not in active.json()["pre_submit_checker_policy"] - assert "checker_names" not in active.json()["pre_submit_checker_policy"] + assert active.json()["pre_submit_checker_policy"]["checker_names"] == ( + bundle["pre_submit_checker_policy"]["checker_names"] + ) + assert active.json()["pre_submit_checker_policy"]["checker_configs"] == ( + bundle["pre_submit_checker_policy"]["checker_configs"] + ) assert active.json()["revision_policy"]["max_revision_rounds"] == 7 assert active.json()["revision_policy"]["auto_reject_after_limit"] is True assert active.json()["payment_policy"]["base_amount"] == "25.00" diff --git a/backend/tests/test_tasks.py b/backend/tests/test_tasks.py index 04ff890..a01f8ef 100644 --- a/backend/tests/test_tasks.py +++ b/backend/tests/test_tasks.py @@ -85,6 +85,9 @@ def auth_headers(token: str = "task-token") -> dict[str, str]: return {"Authorization": f"Bearer {token}"} +_DEFAULT_DEV_ACTOR_FIELD = object() + + def set_dev_actor( monkeypatch: pytest.MonkeyPatch, *, @@ -92,14 +95,26 @@ def set_dev_actor( subject: str, token: str = "task-token", issuer: str = "flow-test", + email: str | None | object = _DEFAULT_DEV_ACTOR_FIELD, + display_name: str | None | object = _DEFAULT_DEV_ACTOR_FIELD, ) -> None: monkeypatch.setenv("WORKSTREAM_AUTH_PROVIDER", "dev") monkeypatch.setenv("WORKSTREAM_ENVIRONMENT", "test") monkeypatch.setenv("WORKSTREAM_DEV_AUTH_TOKEN", token) monkeypatch.setenv("WORKSTREAM_DEV_AUTH_SUBJECT", subject) monkeypatch.setenv("WORKSTREAM_DEV_AUTH_ISSUER", issuer) - monkeypatch.setenv("WORKSTREAM_DEV_AUTH_EMAIL", f"{subject}@example.test") - monkeypatch.setenv("WORKSTREAM_DEV_AUTH_DISPLAY_NAME", subject.replace("-", " ").title()) + if email is _DEFAULT_DEV_ACTOR_FIELD: + monkeypatch.setenv("WORKSTREAM_DEV_AUTH_EMAIL", f"{subject}@example.test") + elif email is None: + monkeypatch.delenv("WORKSTREAM_DEV_AUTH_EMAIL", raising=False) + else: + monkeypatch.setenv("WORKSTREAM_DEV_AUTH_EMAIL", email) + if display_name is _DEFAULT_DEV_ACTOR_FIELD: + monkeypatch.setenv("WORKSTREAM_DEV_AUTH_DISPLAY_NAME", subject.replace("-", " ").title()) + elif display_name is None: + monkeypatch.delenv("WORKSTREAM_DEV_AUTH_DISPLAY_NAME", raising=False) + else: + monkeypatch.setenv("WORKSTREAM_DEV_AUTH_DISPLAY_NAME", display_name) monkeypatch.setenv("WORKSTREAM_DEV_AUTH_ROLES", roles) monkeypatch.setenv("WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART", "false") get_settings.cache_clear() @@ -1256,6 +1271,124 @@ async def test_worker_without_profile_cannot_claim_ready_task( assert "active worker profile" in response.json()["detail"] +async def test_worker_can_create_profile_before_claiming_task( + task_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + project = await create_active_project(task_client) + ready_task = await create_ready_task(task_client, project["id"]) + set_dev_actor(monkeypatch, roles="worker", subject="worker-self-profile") + + profile = await task_client.post( + "/api/v1/workers/me/profile", + headers=auth_headers(), + json={"skill_tags": [" Terminal_Benchmark ", "GO", "go"]}, + ) + assert profile.status_code == 200, profile.text + profile_body = profile.json() + assert profile_body["actor_id"] == actor_id("worker-self-profile") + assert profile_body["external_subject"] == "worker-self-profile" + assert profile_body["skill_tags"] == ["terminal_benchmark", "go"] + assert profile_body["status"] == "active" + + refreshed_profile = await task_client.post( + "/api/v1/workers/me/profile", + headers=auth_headers(), + json={"skill_tags": ["stem"]}, + ) + assert refreshed_profile.status_code == 200, refreshed_profile.text + assert refreshed_profile.json()["id"] == profile_body["id"] + assert refreshed_profile.json()["skill_tags"] == ["stem"] + + claim = await task_client.post( + f"/api/v1/tasks/{ready_task['id']}/claim", + headers=auth_headers(), + json={"reason": "claim after self profile"}, + ) + + assert claim.status_code == 200, claim.text + assert claim.json()["task"]["status"] == "claimed" + assert claim.json()["assignment"]["worker_id"] == actor_id("worker-self-profile") + + +async def test_worker_profile_response_includes_nullable_identity_fields( + task_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + set_dev_actor( + monkeypatch, + roles="worker", + subject="worker-null-identity", + email=None, + display_name=None, + ) + + response = await task_client.post( + "/api/v1/workers/me/profile", + headers=auth_headers(), + json={"skill_tags": []}, + ) + + assert response.status_code == 200, response.text + body = response.json() + assert body["actor_id"] == actor_id("worker-null-identity") + assert body["external_subject"] == "worker-null-identity" + assert body["external_issuer"] == "flow-test" + assert body["display_name"] is None + assert body["email"] is None + assert body["skill_tags"] == [] + assert body["status"] == "active" + + +async def test_worker_profile_request_is_fail_closed_and_validated( + task_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + set_dev_actor(monkeypatch, roles="worker", subject="worker-profile-validation") + + unknown_field = await task_client.post( + "/api/v1/workers/me/profile", + headers=auth_headers(), + json={ + "skill_tags": ["stem"], + "actor_id": actor_id("malicious"), + }, + ) + blank_tag = await task_client.post( + "/api/v1/workers/me/profile", + headers=auth_headers(), + json={"skill_tags": [" "]}, + ) + long_tag = await task_client.post( + "/api/v1/workers/me/profile", + headers=auth_headers(), + json={"skill_tags": ["x" * 65]}, + ) + + assert unknown_field.status_code == 422 + assert "actor_id" in unknown_field.text + assert blank_tag.status_code == 422 + assert "skill_tags cannot include empty values" in blank_tag.text + assert long_tag.status_code == 422 + assert "skill_tags values must be 64 characters or fewer" in long_tag.text + + +async def test_worker_profile_requires_worker_role( + task_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + set_dev_actor(monkeypatch, roles="project_manager", subject="project-manager-subject") + + response = await task_client.post( + "/api/v1/workers/me/profile", + headers=auth_headers(), + json={"skill_tags": ["stem"]}, + ) + + assert response.status_code == 403 + assert "actor lacks required role" in response.json()["detail"] + + async def test_second_claim_is_rejected(task_client: AsyncClient, monkeypatch: pytest.MonkeyPatch) -> None: project = await create_active_project(task_client) ready_task = await create_ready_task(task_client, project["id"]) @@ -1526,7 +1659,7 @@ async def test_submission_requires_assigned_worker_and_in_progress_task( assert other_worker_response.status_code == 404 -async def test_submission_pre_submit_failure_returns_structured_domain_error( +async def test_pre_submit_failure_writes_audit_event_without_submission( task_client: AsyncClient, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -1580,11 +1713,33 @@ async def test_submission_pre_submit_failure_returns_structured_domain_error( checker_runs = (await session.execute(select(db_models.CheckerRun))).scalars().all() task = await session.get(WorkstreamTask, started_task["id"]) assert submissions == [] - assert {event.id for event in audit_events} == audit_ids_before + new_audit_events = [event for event in audit_events if event.id not in audit_ids_before] + assert len(new_audit_events) == 1 + assert new_audit_events[0].event_type == "pre_submission_check_failed" + assert new_audit_events[0].from_status == "in_progress" + assert new_audit_events[0].to_status == "in_progress" + assert new_audit_events[0].event_payload["pre_submit_check"]["status"] == "failed" + assert ( + new_audit_events[0].event_payload["pre_submit_check"]["eligible_to_submit"] + is False + ) assert checker_runs == [] assert task is not None assert task.status == "in_progress" + set_dev_actor(monkeypatch, roles="project_manager", subject="project-manager-subject") + audit_response = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/audit-events", + headers=auth_headers(), + ) + assert audit_response.status_code == 200, audit_response.text + audit_event = next( + event + for event in audit_response.json() + if event["event_type"] == "pre_submission_check_failed" + ) + assert audit_event["event_payload"]["pre_submit_check"]["status"] == "failed" + async def test_submission_pre_submit_requires_specific_evidence_key( task_client: AsyncClient, diff --git a/docs/architecture_data_model.md b/docs/architecture_data_model.md index 60aed71..b1bc430 100644 --- a/docs/architecture_data_model.md +++ b/docs/architecture_data_model.md @@ -561,7 +561,9 @@ POST /tasks/{id}/submissions Blocking pre-submit failures prevent submission creation, create no submission row, no submission version, no task transition to `submitted`, and no -submission-created audit event. They do not return review decision values. +submission-created audit event. Workstream still writes a task audit event named +`pre_submission_check_failed` with the structured checker result for project +operators. They do not return review decision values. ## PostSubmitCheckerPolicy diff --git a/docs/architecture_lockdown.md b/docs/architecture_lockdown.md index 20a8063..7b9b1a3 100644 --- a/docs/architecture_lockdown.md +++ b/docs/architecture_lockdown.md @@ -94,7 +94,10 @@ return `PreSubmitCheckResponse(status="failed", eligible_to_submit=false, results=[...])`. Blocked submission-create attempts return `pre_submission_checker_failed` with structured pass/fail/warning details and create no submission row, no submission version, no task transition to -`submitted`, and no submission-created audit event. +`submitted`, and no submission-created audit event. Workstream still writes a +task audit event named `pre_submission_check_failed` with the structured checker +result for project operators; this is audit evidence, not a product review +decision. Tasks lock to the active guide version at creation or screening time before entering `READY`. Material guide changes require a new guide version. diff --git a/docs/decision_0011_submission_artifact_policy_drives_pre_submit.md b/docs/decision_0011_submission_artifact_policy_drives_pre_submit.md index 4708ef7..e269dfd 100644 --- a/docs/decision_0011_submission_artifact_policy_drives_pre_submit.md +++ b/docs/decision_0011_submission_artifact_policy_drives_pre_submit.md @@ -200,6 +200,8 @@ Blocking pre-submit failures prevent submission creation. When blocking pre-subm - no submission version is assigned - no task transition to `submitted` occurs - no submission-created audit event is written +- a task audit event named `pre_submission_check_failed` is written with the + structured checker result for project operators - the response does not use review decision values: `accept`, `needs_revision`, or `reject` Pre-submit has two API contracts: diff --git a/docs/spec_chunk_4_task_queue_assignment.md b/docs/spec_chunk_4_task_queue_assignment.md index b1ec606..cf80f79 100644 --- a/docs/spec_chunk_4_task_queue_assignment.md +++ b/docs/spec_chunk_4_task_queue_assignment.md @@ -89,6 +89,7 @@ New endpoints: - `POST /api/v1/tasks/{task_id}/claim` - `POST /api/v1/tasks/{task_id}/start` - `GET /api/v1/tasks/{task_id}/audit-events` +- `POST /api/v1/workers/me/profile` Routers stay thin. Services own authorization, lifecycle checks, locked context stamping, assignment rules, and audit writes. @@ -119,6 +120,9 @@ Role expectations: - admin and project manager can create, screen, release, and inspect tasks - workers can claim ready tasks only when an active worker profile already exists +- workers create or refresh their own active profile through + `POST /api/v1/workers/me/profile` before claim; identity fields come from the + verified Flow token and the request may only supply normalized skill tags - worker claim does not self-create or overwrite worker eligibility skill state - workers can read ready tasks and their own assigned tasks - admins and project managers can start a claimed task for operational testing, but do not create worker assignments for themselves through the claim path