From c143b54b62d6325a1d4787116e692fedfc5716f2 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Tue, 7 Jul 2026 18:11:08 +0100 Subject: [PATCH 1/7] Add project setup visibility APIs --- .agent-loop/LOOP_STATE.md | 12 +- .../CHUNK_MAP.md | 353 ++++++++++ .../DECISIONS.md | 32 + .../DISCOVERY.md | 49 ++ .../PLAN.md | 62 ++ .../RISKS.md | 6 + .../STATUS.md | 19 +- ...12-project-setup-policy-visibility-apis.md | 149 ++++ ...sk-context-submission-requirements-apis.md | 136 ++++ ...-001-14-submission-finalize-no-db-drill.md | 150 ++++ .../versions/0013_project_setup_runs.py | 166 +++++ backend/app/db/models.py | 1 + backend/app/modules/projects/models.py | 64 ++ backend/app/modules/projects/repository.py | 72 ++ backend/app/modules/projects/router.py | 167 +++++ backend/app/modules/projects/schemas.py | 34 +- backend/app/modules/projects/service.py | 354 +++++++++- backend/app/modules/projects/setup_queue.py | 27 +- backend/app/workers/project_setup.py | 83 ++- backend/scripts/api_contract_e2e.py | 76 ++- backend/tests/test_projects.py | 641 ++++++++++++++++-- docs/architecture_data_model.md | 52 +- docs/architecture_system_architecture.md | 7 + docs/glossary.md | 9 + docs/operations_project_operating_manual.md | 16 + docs/roadmap_status.md | 8 +- 26 files changed, 2602 insertions(+), 143 deletions(-) create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-12-project-setup-policy-visibility-apis.md create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-13-task-context-submission-requirements-apis.md create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-14-submission-finalize-no-db-drill.md create mode 100644 backend/alembic/versions/0013_project_setup_runs.py diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index 87ec3c7..e42a5e6 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -4,15 +4,17 @@ - Active initiative: `WS-POL-001` - Submission Artifact Policy Foundation - Active planning chunk: none -- Active implementation chunk: none -- Branch: `main` +- Active implementation chunk: `WS-POL-001-12` +- Branch: `codex/ws-pol-001-12-project-setup-policy-visibility` - Status: `WS-POL-001-11` merged through PR #74 on 2026-07-07. The shared actor identity/profile registry is now on `main`; GitHub Agent - Gates, Backend, and CodeRabbit passed before merge. + Gates, Backend, and CodeRabbit passed before merge. The post-merge Terminal + Benchmark live API drill passed, but exposed missing visibility APIs that + forced direct DB inspection. - Last merged implementation SHA: `0729531` - Last merge commit: `5cec0e0` -- Current gate: rerun the Terminal Benchmark live API drill through real HTTP calls using `POST /api/v1/workers/me/profile` -- Next chunk: inactive until the post-merge Terminal Benchmark live API drill passes and findings are reviewed +- Current gate: implement, verify, and review `WS-POL-001-12` before opening PR. +- Next chunk: inactive until `WS-POL-001-12` receives human review and merge. ## Operating Rule diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/CHUNK_MAP.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/CHUNK_MAP.md index 6f36a9a..be94856 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/CHUNK_MAP.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/CHUNK_MAP.md @@ -895,3 +895,356 @@ Human review focus: The shared actor/profile model removes duplicated profile storage without turning persisted profiles into auth or permission authority. + +### WS-POL-001-12: Project Setup And Policy Visibility APIs + +Goal: + +Expose project setup and project policy state needed to continue a real setup +drill without database inspection. This chunk covers APIs 1-7 only. + +Risk: + +L1 + +Depends on: + +`WS-POL-001-11` and the post-merge Terminal Benchmark live API drill findings. + +Allowed files: + +```text +backend/alembic/versions/** +backend/app/db/models.py +backend/app/modules/projects/** +backend/app/workers/project_setup.py +backend/tests/test_projects.py +backend/scripts/api_contract_e2e.py +docs/architecture_data_model.md +docs/architecture_system_architecture.md +docs/glossary.md +docs/operations_project_operating_manual.md +docs/roadmap_status.md +.agent-loop/LOOP_STATE.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/** +``` + +Not allowed: + +```text +backend/app/modules/tasks/** +backend/app/modules/checkers/** +Workstream-owned login, signup, password reset, password storage, primary auth sessions, or API-key auth +Flow token verifier replacement +frontend/demo UI work +payment/reputation/blockchain settlement +agent prompt redesign or new project setup agent behavior +unrestricted generated checker code +review decision token changes +project owner-authored SubmissionArtifactPolicy schema +DB-only drill steps as accepted proof +``` + +API contract: + +```text +1. GET /api/v1/projects/{project_id}/guides/{guide_id}/setup-runs/latest +2. GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports +3. GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports/{report_id} +4. GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies +5. GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies/{policy_id} +6. GET /api/v1/projects/{project_id}/guides/{guide_id}/effective-submission-artifact-policy +7. GET /api/v1/projects/{project_id}/guides/{guide_id}/pre-submit-checker-policy +``` + +Authorization: + +- All endpoints require verified token auth. +- All endpoints require project setup operator access: `admin` or + `project_manager`. +- Worker, reviewer, finance, and auditor roles do not gain these project setup + endpoints in v0.1. + +Acceptance criteria: + +- `ProjectSetupRun` persistence exists for automatic project setup jobs. +- `ProjectSetupRun` is a non-authoritative orchestration ledger. It references + downstream truth by id/hash but does not replace sufficiency report, policy, + effective policy, or pre-submit checker policy records. +- Creating a guide/source snapshot with automatic setup enabled creates a setup + run before enqueue. +- Successful enqueue records the Celery task id. Enqueue failure records + `enqueue_failed` with a bounded error summary. +- The project setup worker updates setup-run status and current step as it runs + guide sufficiency and policy derivation. +- Setup-run statuses are explicit: `queued`, `enqueue_failed`, + `running_sufficiency_agent`, `sufficiency_blocked`, + `running_policy_derivation_agent`, `policy_draft_ready`, `setup_blocked`, + and `failed`. +- `GET .../setup-runs/latest` returns the latest setup run scoped to the + requested project and guide with source snapshot id/hash, Celery task id, + status, current step, output ids, bounded error code/summary, and timestamps. +- Setup-run errors must not expose signed URLs, credential-bearing refs, + token-bearing refs, local filesystem paths, private object keys, or raw stack + traces. +- Sufficiency report list/get endpoints return only reports for the requested + project and guide. +- Submission artifact policy list/get endpoints return only policies for the + requested project and guide. +- Effective policy GET returns the current approved effective project + submission artifact policy for the guide/source snapshot. +- Pre-submit checker policy GET returns the current compiled project + `PreSubmitCheckerPolicy` summary for the guide/effective policy. +- The pre-submit checker policy response can include checker names, compiler + version, compiled bundle hash, lifecycle status, source snapshot id/hash, and + effective policy id/hash. It must not expose mutable authority beyond the + persisted compiled bundle summary. + +Verification: + +- Postgres-backed project tests cover setup-run creation, enqueue task id, + enqueue failure, worker status updates, latest setup-run reads, + sufficiency report list/get, submission artifact policy list/get, effective + policy GET, pre-submit checker policy GET, scoping, and unauthorized roles. +- `backend/scripts/api_contract_e2e.py` may be updated only to consume the new + APIs through HTTP, not to add DB inspection as proof. +- Stale wording scan covers DB-inspection instructions in docs, scripts, and + examples. +- Markdown link check passes for changed docs. + +Required reviewers: + +senior engineering, QA/test, security/auth, product/ops, architecture, docs, +reuse/dedup, test delta. + +Human review focus: + +Setup-run status names, project setup operator access, redaction, and keeping +`ProjectSetupRun` as a ledger rather than a policy source of truth. + +### WS-POL-001-13: Task Context And Submission Requirement APIs + +Goal: + +Expose worker-safe task context, exact submission requirements, and +operator-only locked provenance. This chunk covers APIs 8-10 only. + +Risk: + +L1 + +Depends on: + +`WS-POL-001-12` + +Allowed files: + +```text +backend/app/modules/tasks/** +backend/app/modules/projects/schemas.py +backend/tests/test_tasks.py +backend/scripts/api_contract_e2e.py +docs/architecture_data_model.md +docs/architecture_system_architecture.md +docs/glossary.md +docs/operations_project_operating_manual.md +docs/roadmap_status.md +.agent-loop/LOOP_STATE.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/** +``` + +Not allowed: + +```text +backend/alembic/versions/** +backend/app/modules/checkers/** +backend/app/workers/project_setup.py +Workstream-owned login, signup, password reset, password storage, primary auth sessions, or API-key auth +Flow token verifier replacement +frontend/demo UI work +payment/reputation/blockchain settlement +agent prompt redesign or new project setup agent behavior +review decision token changes +DB-only drill steps as accepted proof +``` + +API contract: + +```text +8. GET /api/v1/tasks/{task_id}/work-context +9. GET /api/v1/tasks/{task_id}/submission-requirements +10. GET /api/v1/tasks/{task_id}/locked-context +``` + +Authorization: + +- `work-context` and `submission-requirements` require existing task visibility + for `admin`, `project_manager`, or the worker currently allowed to work the + task. +- `locked-context` is operator-only for `admin` or `project_manager`. +- Persisted actor profiles do not grant route authorization. Token-derived role + checks remain the route gate. + +Acceptance criteria: + +- `work-context` returns the task, project/guide summary, active locked guide + content, review/revision/payment summary, and worker-facing lifecycle state. +- `submission-requirements` returns exact required artifacts, required evidence + keys/labels/descriptions, forbidden artifact rules, allowed storage schemes, + packaging rules, hash algorithm, storage reference rules, and required + attestation concepts from the task's locked effective project policy. +- Worker-facing responses omit raw compiled checker bundles, checker configs, + internal route tokens, private source refs, full source snapshot hashes, + Celery task ids, and internal setup errors. +- `locked-context` returns full operator provenance: guide source snapshot + id/hash, effective policy id/hash, pre-submit checker policy id/hash, + post-submit checker policy id/hash/body summary, review policy version, + revision policy version, and payment policy version. +- All task context APIs read already-stamped task locked context. They must not + recompute policy from the current active guide or current project policy. +- If required locked context is missing or inconsistent, task context endpoints + fail closed with a structured setup/locked-context error. +- Tests prove guide v2 activation does not silently change work-context or + submission requirements for a task already locked to v1. + +Verification: + +- Postgres-backed task tests cover worker/operator visibility, redaction, + missing locked context, stale active-guide drift, and unauthorized roles. +- API-contract script updates use only HTTP responses for task context proof. +- Stale wording scan covers `task binding`, task-owned policy, and DB-only + proof wording in changed files. +- Markdown link check passes for changed docs. + +Required reviewers: + +senior engineering, QA/test, security/auth, product/ops, architecture, docs, +reuse/dedup, test delta. + +Human review focus: + +Whether worker-facing requirements are complete enough for a real submitter +without exposing internal compiler authority. + +### WS-POL-001-14: Submission Finalize And No-DB Drill Proof + +Goal: + +Replace the public submission `lock` route with `finalize`, clarify system +actor audit semantics, and prove the full 14-API Terminal Benchmark drill +through HTTP only. + +Risk: + +L1 + +Depends on: + +`WS-POL-001-13` + +Allowed files: + +```text +backend/app/modules/tasks/** +backend/app/modules/checkers/** +backend/tests/test_tasks.py +backend/tests/test_checkers.py +backend/scripts/api_contract_e2e.py +examples/terminal_benchmark/terminal_benchmark_api_e2e.py +docs/architecture_data_model.md +docs/architecture_system_architecture.md +docs/glossary.md +docs/operations_project_operating_manual.md +docs/roadmap_status.md +.agent-loop/LOOP_STATE.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/** +``` + +Not allowed: + +```text +backend/alembic/versions/** +backend/app/modules/projects/** +backend/app/workers/project_setup.py +Workstream-owned login, signup, password reset, password storage, primary auth sessions, or API-key auth +Flow token verifier replacement +frontend/demo UI work +payment/reputation/blockchain settlement +agent prompt redesign or new project setup agent behavior +review decision token changes +DB-only drill steps as accepted proof +``` + +API contract: + +```text +11. POST /api/v1/submissions/{submission_id}/finalize +12. GET /api/v1/submissions/{submission_id}/checker-runs +13. GET /api/v1/checker-runs/{checker_run_id} +14. GET /api/v1/tasks/{task_id}/audit-events +``` + +The existing `POST /api/v1/tasks/{task_id}/submission-precheck` remains part of +the no-DB drill proof. + +Authorization: + +- `finalize` requires verified `admin` or `project_manager`. +- The requester must be authorized against the submission's project/task. +- The internal pre-review gate system actor cannot authorize HTTP requests and + cannot be supplied by the client. +- Checker-run and audit reads retain their existing route permissions unless a + test proves a visibility mismatch. + +Acceptance criteria: + +- Public `POST /submissions/{submission_id}/finalize` replaces public + `POST /submissions/{submission_id}/lock` in code, tests, scripts, examples, + and docs. No v0.1 compatibility alias is kept. +- `finalize` is idempotent for the latest submitted version and returns the + existing finalized response on repeat calls. +- `finalize` fails for non-latest submission versions, unfinished submissions, + unauthorized roles, and submissions whose task locked context is missing or + inconsistent. +- Persistence may keep `locked_at` as the internal timestamp field. +- Public audit event wording uses `submission_finalized` for the requester + handoff. Old public `submission_locked` wording is removed from current docs, + scripts, and tests. +- Pre-review checker execution is audited under + `workstream-system:pre-review-gate` and includes requester actor id, issuer, + subject, and auth source in the event payload. +- Existing checker-run list/get endpoints remain stable and expose durable + post-submit checker results for the finalized submission. +- Existing task audit-event endpoint remains stable and exposes the finalization + and pre-review gate path. +- The Terminal Benchmark drill proceeds from guide creation through + `review_pending` using HTTP API responses only. Direct DB reads are allowed + only for test setup/cleanup and migration reset, not for proving lifecycle + state or finding ids. +- The drill explicitly proves both paths: pre-submit preflight failure returns + `200 PreSubmitCheckResponse` with `eligible_to_submit: false`, and blocked + submission creation returns `pre_submission_checker_failed` without creating + a submission. + +Verification: + +- Postgres-backed task/checker tests cover finalize success, idempotency, + authorization, non-latest rejection, system actor audit, checker-run reads, + and audit-event reads. +- `backend/scripts/api_contract_e2e.py` uses `/finalize`, not `/lock`. +- `examples/terminal_benchmark/terminal_benchmark_api_e2e.py` uses APIs 1-14 + plus pre-submit preflight and contains no DB inspection as proof. +- Stale wording scan covers `/submissions/.*/lock`, `submission_locked`, + `lock endpoint`, and DB-inspection proof wording in backend, docs, scripts, + examples, and `.agent-loop`. +- Markdown link check passes for changed docs. + +Required reviewers: + +senior engineering, QA/test, security/auth, product/ops, architecture, docs, +reuse/dedup, test delta. + +Human review focus: + +Public `finalize` wording, operator-only authorization, system actor audit +provenance, and whether the live drill is genuinely API-only. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/DECISIONS.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/DECISIONS.md index f2b029d..685801a 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/DECISIONS.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/DECISIONS.md @@ -48,6 +48,36 @@ pipeline runs guide sufficiency first, stops on blocking sufficiency, and creates only a draft `SubmissionArtifactPolicy` after sufficiency passes or passes with warnings. +- Project setup automation must be visible through Workstream APIs. Operators + should not inspect Postgres directly to learn whether sufficiency analysis, + policy derivation, policy approval, effective policy merge, or checker + compilation happened. +- `ProjectSetupRun` is a non-authoritative orchestration ledger. It can expose + queue status, Celery task id, current setup step, bounded errors, and output + ids, but it does not replace the actual guide sufficiency, submission + artifact policy, effective policy, or pre-submit checker policy records. +- Project setup and project policy visibility APIs require project setup + operator access. In v0.1, that means verified `admin` or `project_manager` + token roles. +- Worker-facing task context APIs must read already-stamped locked context from + the task. They must not recompute policy from the current active guide, and + they must omit compiler internals, raw compiled bundles, private source refs, + and internal policy authority fields. +- The Terminal Benchmark live API drill must be repeatable without direct DB + reads. Any state needed to continue the drill must be exposed through an + authorized API response. +- Submission finalization is the public operation that locks a submitted packet + and starts the pre-review gate. The external requester authorizes the action; + Workstream records system-owned pre-review gate execution through an internal + system actor so audit trails distinguish human/operator requests from + automated checker execution. +- In v0.1, submission finalization is an `admin` or `project_manager` + operation. The internal pre-review gate system actor cannot authorize HTTP + requests; it is only used for automated checker execution audit attribution. +- Public API wording should prefer `finalize` for submission handoff into the + pre-review gate. `lock` can remain an internal persistence concept such as + `locked_at`, but should not remain the public endpoint name once the finalize + API replaces it. ## Accepted Defaults @@ -70,3 +100,5 @@ generated project pre-submit checker compiled bundle hashes. - Confirm the next chunk contract before resuming the real Terminal Benchmark drill through the automatic Celery setup path. +- Confirm the visibility/finalize API contract before rerunning the Terminal + Benchmark drill without DB inspection. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/DISCOVERY.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/DISCOVERY.md index 989235b..daa50e9 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/DISCOVERY.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/DISCOVERY.md @@ -96,3 +96,52 @@ project owner does not approve Workstream's internal policy controls. - Review decision stored values only `accept`, `needs_revision`, `reject`. - Internal checker routes are not review decisions. - CodeRabbit and CI supplement, but do not replace, internal reviewer tracks. + +## Post-Actor-Registry Live API Drill Findings + +The 2026-07-07 Terminal Benchmark live API drill completed through real HTTP +calls and proved the core setup and submission path: + +```text +Flow-token auth +-> project create +-> guide/source snapshot create +-> Celery project setup +-> OpenAI Agents SDK sufficiency report +-> OpenAI Agents SDK submission artifact policy derivation +-> policy approval +-> effective project policy merge +-> deterministic project PreSubmitCheckerPolicy compilation +-> guide activation +-> task screen/release +-> worker profile activation +-> claim/start +-> pre-submit failure creates no submission +-> pre-submit pass creates submission +-> current submission lock route triggers pre-review gate +-> review_pending +``` + +The drill also exposed visibility gaps: + +| Gap | Current state | Needed API behavior | +|---|---|---| +| Project setup run status | Celery returns task state, but Workstream does not persist a setup-run record. | Persist and expose latest setup run with status, current step, Celery task id, timestamps, and output ids. | +| Sufficiency report discovery | Creation and acknowledgement endpoints exist, but list/get read endpoints are missing. | Operators can list and inspect reports by API. | +| Submission artifact policy discovery | Create/update/approve endpoints exist, but list/get read endpoints are missing. | Operators can inspect the generated draft before approval without DB access. | +| Effective policy discovery | Approval response shows the effective policy, but there is no stable current-policy GET. | Operators can fetch current effective policy by project/guide. | +| Pre-submit checker policy discovery | Active guide response includes a summary, but no focused checker-policy GET exists. | Operators can fetch checker names, configs, compiler version, and compiled bundle hash. | +| Worker submission requirements | Workers can run pre-submit, but cannot fetch the exact required artifacts/evidence/attestation concepts first. | Worker-facing requirements endpoint shows the locked intake contract without internal-only compiler details. | +| Operator locked context | Some task responses redact or omit internal lock fields depending on actor. | Operator endpoint exposes full task locked provenance for support and drill verification. | +| Public submission lock wording | Public endpoint is `/submissions/{id}/lock`; the worker cannot call it and the name sounds like a storage action. | Replace with `/submissions/{id}/finalize` as the public handoff into pre-review gate. | +| Gate audit actor | Current lock/finalize requester appears as the actor on pre-review gate execution. | Use an internal Workstream system actor for checker gate execution while retaining requester provenance. | + +Already-existing APIs from the desired drill surface: + +- `POST /api/v1/tasks/{task_id}/submission-precheck` +- `GET /api/v1/submissions/{submission_id}/checker-runs` +- `GET /api/v1/checker-runs/{checker_run_id}` +- `GET /api/v1/tasks/{task_id}/audit-events` + +These should stay covered by the new no-DB drill but do not need duplicate +routes. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/PLAN.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/PLAN.md index 1a7e1ae..522c7d0 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/PLAN.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/PLAN.md @@ -229,3 +229,65 @@ runtime rewiring until immutable guide-source snapshots, guide sufficiency reports, project policy objects, defaults, effective project submission artifact policy hash, generated project pre-submit checker bundle, task locked-context fields, and activation/ready guards are accepted. + +## Visibility API Extension + +The post-actor-registry Terminal Benchmark live API drill proved the lifecycle +but required direct database inspection to continue through project setup and +policy approval. The next implementation sequence closes that gap before the +drill is rerun. The work is split across narrow chunks because it crosses +project setup, task worker visibility, submission finalization, checker reads, +and audit proof. + +The target API surface is: + +| # | API | Status | +|---:|---|---| +| 1 | `GET /api/v1/projects/{project_id}/guides/{guide_id}/setup-runs/latest` | New | +| 2 | `GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports` | New | +| 3 | `GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports/{report_id}` | New | +| 4 | `GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies` | New | +| 5 | `GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies/{policy_id}` | New | +| 6 | `GET /api/v1/projects/{project_id}/guides/{guide_id}/effective-submission-artifact-policy` | New | +| 7 | `GET /api/v1/projects/{project_id}/guides/{guide_id}/pre-submit-checker-policy` | New | +| 8 | `GET /api/v1/tasks/{task_id}/work-context` | New | +| 9 | `GET /api/v1/tasks/{task_id}/submission-requirements` | New | +| 10 | `GET /api/v1/tasks/{task_id}/locked-context` | New | +| 11 | `POST /api/v1/submissions/{submission_id}/finalize` | New replacement for public `/lock` | +| 12 | `GET /api/v1/submissions/{submission_id}/checker-runs` | Existing; keep and cover | +| 13 | `GET /api/v1/checker-runs/{checker_run_id}` | Existing; keep and cover | +| 14 | `GET /api/v1/tasks/{task_id}/audit-events` | Existing; keep and cover | + +Implementation sequence: + +| Chunk | Scope | APIs | +|---|---|---| +| `WS-POL-001-12` | Project setup-run and project policy visibility | 1-7 | +| `WS-POL-001-13` | Task work context, submission requirements, and operator locked context | 8-10 | +| `WS-POL-001-14` | Submission finalize, system actor audit semantics, and no-DB drill coverage | 11-14 plus existing pre-submit preflight | + +`ProjectSetupRun` is a non-authoritative orchestration ledger. It records that +automatic setup was queued and how far the Celery job progressed, but policy +truth remains in `GuideSufficiencyReport`, `SubmissionArtifactPolicy`, +`EffectiveProjectSubmissionArtifactPolicy`, and `PreSubmitCheckerPolicy`. +The setup run records the Celery task id, lifecycle status, current step, +output report/policy ids, bounded errors, and timestamps. Enqueue writes +`queued`; enqueue failure writes `enqueue_failed`; the Celery worker updates +`running_sufficiency_agent`, `sufficiency_blocked`, +`running_policy_derivation_agent`, `policy_draft_ready`, `setup_blocked`, or +`failed`. + +Project setup and policy APIs require project setup operator access. For v0.1, +that means a verified token with `admin` or `project_manager`. + +Worker-facing visibility must be useful without leaking internal compiler +authority. Workers can see guide/work context and exact submission +requirements only for tasks they can work on. Operators can see full locked +provenance and compiled checker policy summaries. + +Submission finalization should be the public handoff into the pre-review gate. +For v0.1, finalization is an `admin` or `project_manager` operation and is +idempotent for the latest submitted version. The external actor authorizes the +request and receives the API response, but pre-review checker execution is +audited as `workstream-system:pre-review-gate` with requester provenance in +the event payload. The system actor never authorizes HTTP requests. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/RISKS.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/RISKS.md index 7433e2d..a8df855 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/RISKS.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/RISKS.md @@ -11,3 +11,9 @@ | Worker enqueue durability gap | Medium | Current Celery enqueue semantics are honest after commit; add a durable outbox later if guaranteed eventual enqueue is required. | | Agent scope creep | Medium | Project setup agent execution is now isolated behind Celery; future agent expansion must stay behind chunk contracts and must not run inline in request handlers. | | Insufficient real API proof | High | Require Postgres-backed API tests and real API drill before closing the initiative. | +| Hidden setup state forces DB inspection | High | Persist project setup runs and expose sufficiency, policy, effective policy, checker policy, task requirements, and locked context through authorized APIs before rerunning the live drill. | +| Overexposing policy/compiler internals to workers | Medium | Split worker-facing requirements/work-context endpoints from operator-only locked-context and checker-policy endpoints. | +| Ambiguous submission lock wording | Medium | Replace the public `/lock` endpoint with `/finalize` while keeping `locked_at` as internal persistence terminology. | +| Audit actor confusion for automated gates | Medium | Use a Workstream internal system actor for pre-review checker execution and keep requester provenance in audit payload. | +| Visibility API mega-PR becomes unreviewable | High | Split the 14 API surfaces into project setup visibility, task context visibility, and finalize/no-DB proof chunks. | +| Worker endpoints accidentally recompute from current guide | High | Task context endpoints must read stamped locked context only and fail closed when required lock fields are missing. | 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 d339e00..6b890ba 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 @@ -5,12 +5,16 @@ `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`, `WS-POL-001-08`, `WS-POL-001-09`, `WS-POL-001-10`, and `WS-POL-001-11` are merged to `main`. -The current gate is the post-actor-registry Terminal Benchmark live API drill -through real HTTP calls using the canonical worker profile endpoint. +The post-actor-registry Terminal Benchmark live API drill passed through real +HTTP calls, but it required DB inspection for project setup outputs and locked +context verification. The next work is split into three visibility/finalize +chunks before rerunning the drill without DB inspection. ## Active Chunk -No implementation chunk is active. +`WS-POL-001-12` is in implementation on branch +`codex/ws-pol-001-12-project-setup-policy-visibility`. `WS-POL-001-13` and +`WS-POL-001-14` remain inactive until `WS-POL-001-12` is reviewed and merged. ## Chunk Status @@ -27,12 +31,17 @@ No implementation chunk is active. | `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` | Merged | `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. | | `WS-POL-001-11` | Merged | `codex/ws-pol-001-11-actor-profile-registry-impl` | 74 | Implements local Workstream actor identity and actor profile registries for verified Flow actors before the next live API drill. | +| `WS-POL-001-12` | In implementation | `codex/ws-pol-001-12-project-setup-policy-visibility` | - | Add project setup-run and project policy visibility APIs for setup runs, sufficiency reports, submission artifact policies, effective policy, and compiled project pre-submit checker policy. | +| `WS-POL-001-13` | Proposed | - | - | Add task work-context, worker submission-requirements, and operator locked-context APIs. | +| `WS-POL-001-14` | Proposed | - | - | Replace public submission lock with finalize, define system actor audit semantics, and rerun the Terminal Benchmark proof without DB inspection. | ## Blockers | Blocker | Owner | Next action | |---|---|---| -| None | - | Rerun the Terminal Benchmark live API drill and keep the next chunk inactive until the drill result is reviewed. | +| Missing project setup visibility APIs | Workstream | Complete `WS-POL-001-12` implementation evidence and PR review. | +| Missing task context visibility APIs | Workstream | Implement `WS-POL-001-13`. | +| Public lock wording and no-DB drill proof | Workstream | Implement `WS-POL-001-14` before rerunning Terminal Benchmark as accepted proof. | ## Follow-Ups @@ -43,3 +52,5 @@ No implementation chunk is active. | Extract shared artifact path and forbidden-pattern helpers before further checker-policy expansion | Reuse/dedup review | Medium follow-up | | Add profile-level audit events if actor/profile changes become reputation-sensitive | Security review on PR #72 | Medium follow-up | | Rerun Terminal Benchmark live API drill with canonical worker profile setup | Post-merge gate after PR #74 | High | +| Rerun Terminal Benchmark live API drill without direct DB inspection | `WS-POL-001-14` | High | +| Add reviewer packet visibility scoped to eligible/assigned reviewers before full review lifecycle work | Product/ops review on visibility planning | High follow-up | diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-12-project-setup-policy-visibility-apis.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-12-project-setup-policy-visibility-apis.md new file mode 100644 index 0000000..d1cf8b0 --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-12-project-setup-policy-visibility-apis.md @@ -0,0 +1,149 @@ +# Chunk Contract: WS-POL-001-12 - Project Setup And Policy Visibility APIs + +## Parent Initiative + +`WS-POL-001` - Submission Artifact Policy Foundation + +## Goal + +Expose project setup and project policy state through authorized HTTP APIs so +operators no longer inspect Postgres to continue a real setup drill. + +This chunk covers APIs 1-7 only: + +```text +GET /api/v1/projects/{project_id}/guides/{guide_id}/setup-runs/latest +GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports +GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports/{report_id} +GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies +GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies/{policy_id} +GET /api/v1/projects/{project_id}/guides/{guide_id}/effective-submission-artifact-policy +GET /api/v1/projects/{project_id}/guides/{guide_id}/pre-submit-checker-policy +``` + +## Why This Chunk Exists + +The Terminal Benchmark live API drill proved that automatic setup works, but +the drill still needed direct DB reads to find setup outputs. That is not an +acceptable operator or demo path. + +## Risk Class + +L1 + +## SLA + +P1 + +## Work Type + +Backend API, project setup persistence, project policy visibility, tests, docs. + +## Allowed Files + +```text +backend/alembic/versions/** +backend/app/db/models.py +backend/app/modules/projects/** +backend/app/workers/project_setup.py +backend/tests/test_projects.py +backend/scripts/api_contract_e2e.py +docs/architecture_data_model.md +docs/architecture_system_architecture.md +docs/glossary.md +docs/operations_project_operating_manual.md +docs/roadmap_status.md +.agent-loop/LOOP_STATE.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/** +``` + +## Not Allowed + +```text +backend/app/modules/tasks/** +backend/app/modules/checkers/** +Workstream-owned login, signup, password reset, password storage, primary auth sessions, or API-key auth +Flow token verifier replacement +frontend/demo UI work +payment/reputation/blockchain settlement +agent prompt redesign or new project setup agent behavior +unrestricted generated checker code +review decision token changes +project owner-authored SubmissionArtifactPolicy schema +DB-only drill steps as accepted proof +``` + +## Authorization Contract + +- All endpoints require verified token auth. +- All endpoints require project setup operator access: `admin` or + `project_manager`. +- Worker, reviewer, finance, and auditor roles do not receive these project + setup endpoints in v0.1. + +## Acceptance Criteria + +- `ProjectSetupRun` persistence exists for automatic project setup jobs. +- `ProjectSetupRun` is a non-authoritative orchestration ledger. It references + downstream truth by id/hash but does not replace sufficiency report, policy, + effective policy, or pre-submit checker policy records. +- Creating a guide/source snapshot with automatic setup enabled creates a setup + run before enqueue. +- Successful enqueue records the Celery task id. +- Enqueue failure records `enqueue_failed` with a bounded error summary. +- The project setup worker updates setup-run status and current step as it runs + guide sufficiency and policy derivation. +- Setup-run statuses are explicit: `queued`, `enqueue_failed`, + `running_sufficiency_agent`, `sufficiency_blocked`, + `running_policy_derivation_agent`, `policy_draft_ready`, `setup_blocked`, + and `failed`. +- `GET .../setup-runs/latest` returns the latest setup run scoped to the + requested project and guide with source snapshot id/hash, Celery task id, + status, current step, output ids, bounded error code/summary, and timestamps. +- Setup-run errors do not expose signed URLs, credential-bearing refs, + token-bearing refs, local filesystem paths, private object keys, or raw stack + traces. +- Sufficiency report list/get endpoints return only reports for the requested + project and guide. +- Submission artifact policy list/get endpoints return only policies for the + requested project and guide. +- Effective policy GET returns the current approved effective project + submission artifact policy for the guide/source snapshot. +- Pre-submit checker policy GET returns the current compiled project + `PreSubmitCheckerPolicy` summary for the guide/effective policy. +- The pre-submit checker policy response can include checker names, compiler + version, compiled bundle hash, lifecycle status, source snapshot id/hash, and + effective policy id/hash. It must not expose mutable authority beyond the + persisted compiled bundle summary. + +## Verification Commands + +```bash +cd backend && .venv/bin/pytest tests/test_projects.py +cd backend && WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py +python scripts/check_internal_review_evidence.py +``` + +Also run: + +```bash +rg -n "DB inspection|direct DB|setup-runs/latest|ProjectSetupRun" docs backend examples .agent-loop +``` + +## Required Reviewers + +senior engineering, QA/test, security/auth, product/ops, architecture, docs, +reuse/dedup, test delta. + +## Human Review Focus + +Setup-run status names, project setup operator access, redaction, and keeping +`ProjectSetupRun` as a ledger rather than a policy source of truth. + +## Stop Conditions + +- Stop if durable setup-run persistence requires a broader queue/outbox + redesign. +- Stop if project policy visibility would require exposing raw compiled checker + authority to non-operator actors. +- Stop if setup-run errors cannot be safely redacted. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-13-task-context-submission-requirements-apis.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-13-task-context-submission-requirements-apis.md new file mode 100644 index 0000000..be65a14 --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-13-task-context-submission-requirements-apis.md @@ -0,0 +1,136 @@ +# Chunk Contract: WS-POL-001-13 - Task Context And Submission Requirement APIs + +## Parent Initiative + +`WS-POL-001` - Submission Artifact Policy Foundation + +## Goal + +Expose worker-safe task context, exact submission requirements, and +operator-only locked provenance through HTTP APIs. + +This chunk covers APIs 8-10 only: + +```text +GET /api/v1/tasks/{task_id}/work-context +GET /api/v1/tasks/{task_id}/submission-requirements +GET /api/v1/tasks/{task_id}/locked-context +``` + +## Why This Chunk Exists + +The live API drill proved that workers can eventually pass pre-submit checks, +but the worker had to infer the exact requirements from failures. A real worker +must see the locked requirements before submitting, and operators must see full +locked provenance without using SQL. + +## Risk Class + +L1 + +## SLA + +P1 + +## Work Type + +Backend API, task lifecycle visibility, authorization, tests, docs. + +## Depends On + +`WS-POL-001-12` + +## Allowed Files + +```text +backend/app/modules/tasks/** +backend/app/modules/projects/schemas.py +backend/tests/test_tasks.py +backend/scripts/api_contract_e2e.py +docs/architecture_data_model.md +docs/architecture_system_architecture.md +docs/glossary.md +docs/operations_project_operating_manual.md +docs/roadmap_status.md +.agent-loop/LOOP_STATE.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/** +``` + +## Not Allowed + +```text +backend/alembic/versions/** +backend/app/modules/checkers/** +backend/app/workers/project_setup.py +Workstream-owned login, signup, password reset, password storage, primary auth sessions, or API-key auth +Flow token verifier replacement +frontend/demo UI work +payment/reputation/blockchain settlement +agent prompt redesign or new project setup agent behavior +review decision token changes +DB-only drill steps as accepted proof +``` + +## Authorization Contract + +- `work-context` and `submission-requirements` require existing task visibility + for `admin`, `project_manager`, or the worker currently allowed to work the + task. +- `locked-context` is operator-only for `admin` or `project_manager`. +- Persisted actor profiles do not grant route authorization. Token-derived role + checks remain the route gate. + +## Acceptance Criteria + +- `work-context` returns the task, project/guide summary, active locked guide + content, review/revision/payment summary, and worker-facing lifecycle state. +- `submission-requirements` returns exact required artifacts, required evidence + keys/labels/descriptions, forbidden artifact rules, allowed storage schemes, + packaging rules, hash algorithm, storage reference rules, and required + attestation concepts from the task's locked effective project policy. +- Worker-facing responses omit raw compiled checker bundles, checker configs, + internal route tokens, private source refs, full source snapshot hashes, + Celery task ids, and internal setup errors. +- `locked-context` returns full operator provenance: guide source snapshot + id/hash, effective policy id/hash, pre-submit checker policy id/hash, + post-submit checker policy id/hash/body summary, review policy version, + revision policy version, and payment policy version. +- All task context APIs read already-stamped task locked context. They must not + recompute policy from the current active guide or current project policy. +- If required locked context is missing or inconsistent, task context endpoints + fail closed with a structured setup/locked-context error. +- Tests prove guide v2 activation does not silently change work-context or + submission requirements for a task already locked to v1. + +## Verification Commands + +```bash +cd backend && .venv/bin/pytest tests/test_tasks.py +cd backend && WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py +python scripts/check_internal_review_evidence.py +``` + +Also run: + +```bash +rg -n "task binding|task-owned policy|direct DB|DB inspection" docs backend examples .agent-loop +``` + +## Required Reviewers + +senior engineering, QA/test, security/auth, product/ops, architecture, docs, +reuse/dedup, test delta. + +## Human Review Focus + +Whether worker-facing requirements are complete enough for a real submitter +without exposing internal compiler authority. + +## Stop Conditions + +- Stop if worker-facing requirements require raw compiled checker bundles to be + useful. +- Stop if route authorization cannot distinguish worker-safe context from + operator-only locked provenance. +- Stop if task context would need to recompute from the current guide instead + of reading stamped locked context. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-14-submission-finalize-no-db-drill.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-14-submission-finalize-no-db-drill.md new file mode 100644 index 0000000..a70e118 --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-14-submission-finalize-no-db-drill.md @@ -0,0 +1,150 @@ +# Chunk Contract: WS-POL-001-14 - Submission Finalize And No-DB Drill Proof + +## Parent Initiative + +`WS-POL-001` - Submission Artifact Policy Foundation + +## Goal + +Replace the public submission `lock` route with `finalize`, clarify system +actor audit semantics, and prove the full Terminal Benchmark drill through +HTTP only. + +This chunk covers APIs 11-14: + +```text +POST /api/v1/submissions/{submission_id}/finalize +GET /api/v1/submissions/{submission_id}/checker-runs +GET /api/v1/checker-runs/{checker_run_id} +GET /api/v1/tasks/{task_id}/audit-events +``` + +The existing `POST /api/v1/tasks/{task_id}/submission-precheck` remains part of +the no-DB drill proof. + +## Why This Chunk Exists + +The live drill exposed that `/lock` is bad public wording and that the +pre-review gate needs clear system actor provenance. After APIs 1-10 exist, the +final proof must use all required state through HTTP rather than DB reads. + +## Risk Class + +L1 + +## SLA + +P1 + +## Work Type + +Backend API, submission lifecycle, checker/audit proof, tests, real API drill. + +## Depends On + +`WS-POL-001-13` + +## Allowed Files + +```text +backend/app/modules/tasks/** +backend/app/modules/checkers/** +backend/tests/test_tasks.py +backend/tests/test_checkers.py +backend/scripts/api_contract_e2e.py +examples/terminal_benchmark/terminal_benchmark_api_e2e.py +docs/architecture_data_model.md +docs/architecture_system_architecture.md +docs/glossary.md +docs/operations_project_operating_manual.md +docs/roadmap_status.md +.agent-loop/LOOP_STATE.md +.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/** +``` + +## Not Allowed + +```text +backend/alembic/versions/** +backend/app/modules/projects/** +backend/app/workers/project_setup.py +Workstream-owned login, signup, password reset, password storage, primary auth sessions, or API-key auth +Flow token verifier replacement +frontend/demo UI work +payment/reputation/blockchain settlement +agent prompt redesign or new project setup agent behavior +review decision token changes +DB-only drill steps as accepted proof +``` + +## Authorization Contract + +- `finalize` requires verified `admin` or `project_manager`. +- The requester must be authorized against the submission's project/task. +- The internal pre-review gate system actor cannot authorize HTTP requests and + cannot be supplied by the client. +- Checker-run and audit reads retain their existing route permissions unless a + test proves a visibility mismatch. + +## Acceptance Criteria + +- Public `POST /submissions/{submission_id}/finalize` replaces public + `POST /submissions/{submission_id}/lock` in code, tests, scripts, examples, + and docs. No v0.1 compatibility alias is kept. +- `finalize` is idempotent for the latest submitted version and returns the + existing finalized response on repeat calls. +- `finalize` fails for non-latest submission versions, unfinished submissions, + unauthorized roles, and submissions whose task locked context is missing or + inconsistent. +- Persistence may keep `locked_at` as the internal timestamp field. +- Public audit event wording uses `submission_finalized` for the requester + handoff. Old public `submission_locked` wording is removed from current docs, + scripts, and tests. +- Pre-review checker execution is audited under + `workstream-system:pre-review-gate` and includes requester actor id, issuer, + subject, and auth source in the event payload. +- Existing checker-run list/get endpoints remain stable and expose durable + post-submit checker results for the finalized submission. +- Existing task audit-event endpoint remains stable and exposes the finalization + and pre-review gate path. +- The Terminal Benchmark drill proceeds from guide creation through + `review_pending` using HTTP API responses only. Direct DB reads are allowed + only for test setup/cleanup and migration reset, not for proving lifecycle + state or finding ids. +- The drill explicitly proves both paths: pre-submit preflight failure returns + `200 PreSubmitCheckResponse` with `eligible_to_submit: false`, and blocked + submission creation returns `pre_submission_checker_failed` without creating + a submission. + +## Verification Commands + +```bash +cd backend && .venv/bin/pytest tests/test_tasks.py tests/test_checkers.py +cd backend && WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py +cd backend && WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python ../examples/terminal_benchmark/terminal_benchmark_api_e2e.py +python scripts/check_internal_review_evidence.py +``` + +Also run: + +```bash +rg -n "/submissions/.*/lock|submission_locked|lock endpoint|DB inspection|direct DB" docs backend examples .agent-loop +``` + +## Required Reviewers + +senior engineering, QA/test, security/auth, product/ops, architecture, docs, +reuse/dedup, test delta. + +## Human Review Focus + +Public `finalize` wording, operator-only authorization, system actor audit +provenance, and whether the live drill is genuinely API-only. + +## Stop Conditions + +- Stop if replacing public `/lock` breaks unrelated lifecycle contracts. +- Stop if system actor provenance cannot be separated from requester + authorization. +- Stop if the Terminal Benchmark drill still requires DB inspection to prove + lifecycle state. diff --git a/backend/alembic/versions/0013_project_setup_runs.py b/backend/alembic/versions/0013_project_setup_runs.py new file mode 100644 index 0000000..f7d4241 --- /dev/null +++ b/backend/alembic/versions/0013_project_setup_runs.py @@ -0,0 +1,166 @@ +"""project setup run ledger + +Revision ID: 0013_project_setup_runs +Revises: 0012_actor_identity_profiles +Create Date: 2026-07-07 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +revision = "0013_project_setup_runs" +down_revision = "0012_actor_identity_profiles" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Create the automatic project setup run ledger.""" + op.create_table( + "project_setup_runs", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("project_id", sa.String(length=36), nullable=False), + sa.Column("guide_id", sa.String(length=36), nullable=False), + sa.Column("guide_version", sa.String(length=50), nullable=False), + sa.Column("source_snapshot_id", sa.String(length=36), nullable=False), + sa.Column("source_snapshot_hash", sa.String(length=71), nullable=False), + sa.Column("celery_task_id", sa.String(length=155), nullable=True), + sa.Column("status", sa.String(length=50), nullable=False), + sa.Column("current_step", sa.String(length=100), nullable=False), + sa.Column("output_sufficiency_report_id", sa.String(length=36), nullable=True), + sa.Column("output_submission_artifact_policy_id", sa.String(length=36), nullable=True), + sa.Column("error_code", sa.String(length=100), nullable=True), + sa.Column("error_summary", sa.Text(), nullable=True), + sa.Column("created_by", sa.String(length=100), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True), + sa.CheckConstraint( + "status in (" + "'queued', " + "'enqueue_failed', " + "'running_sufficiency_agent', " + "'sufficiency_blocked', " + "'running_policy_derivation_agent', " + "'policy_draft_ready', " + "'setup_blocked', " + "'failed'" + ")", + name="ck_project_setup_runs_status", + ), + sa.ForeignKeyConstraint( + ["guide_id"], + ["project_guides.id"], + name=op.f("fk_project_setup_runs_guide_id_project_guides"), + ), + sa.ForeignKeyConstraint( + ["output_submission_artifact_policy_id"], + ["submission_artifact_policies.id"], + name="fk_project_setup_runs_submission_artifact_policy", + ), + sa.ForeignKeyConstraint( + ["output_sufficiency_report_id"], + ["guide_sufficiency_reports.id"], + name="fk_project_setup_runs_sufficiency_report", + ), + sa.ForeignKeyConstraint( + ["project_id"], + ["projects.id"], + name=op.f("fk_project_setup_runs_project_id_projects"), + ), + sa.ForeignKeyConstraint( + ["project_id", "guide_version"], + ["project_guides.project_id", "project_guides.version"], + name="fk_project_setup_runs_project_guide", + ), + sa.ForeignKeyConstraint( + ["source_snapshot_id"], + ["guide_source_snapshots.id"], + name=op.f("fk_project_setup_runs_source_snapshot_id_guide_source_snapshots"), + ), + sa.ForeignKeyConstraint( + ["source_snapshot_id", "source_snapshot_hash"], + ["guide_source_snapshots.id", "guide_source_snapshots.bundle_hash"], + name="fk_project_setup_runs_source_snapshot_hash", + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_project_setup_runs")), + ) + op.create_index( + op.f("ix_project_setup_runs_celery_task_id"), + "project_setup_runs", + ["celery_task_id"], + unique=False, + ) + op.create_index( + op.f("ix_project_setup_runs_guide_id"), + "project_setup_runs", + ["guide_id"], + unique=False, + ) + op.create_index( + op.f("ix_project_setup_runs_output_submission_artifact_policy_id"), + "project_setup_runs", + ["output_submission_artifact_policy_id"], + unique=False, + ) + op.create_index( + op.f("ix_project_setup_runs_output_sufficiency_report_id"), + "project_setup_runs", + ["output_sufficiency_report_id"], + unique=False, + ) + op.create_index( + op.f("ix_project_setup_runs_project_id"), + "project_setup_runs", + ["project_id"], + unique=False, + ) + op.create_index( + op.f("ix_project_setup_runs_source_snapshot_id"), + "project_setup_runs", + ["source_snapshot_id"], + unique=False, + ) + op.create_index( + op.f("ix_project_setup_runs_status"), + "project_setup_runs", + ["status"], + unique=False, + ) + + +def downgrade() -> None: + """Drop the automatic project setup run ledger.""" + op.drop_index(op.f("ix_project_setup_runs_status"), table_name="project_setup_runs") + op.drop_index( + op.f("ix_project_setup_runs_source_snapshot_id"), + table_name="project_setup_runs", + ) + op.drop_index(op.f("ix_project_setup_runs_project_id"), table_name="project_setup_runs") + op.drop_index( + op.f("ix_project_setup_runs_output_sufficiency_report_id"), + table_name="project_setup_runs", + ) + op.drop_index( + op.f("ix_project_setup_runs_output_submission_artifact_policy_id"), + table_name="project_setup_runs", + ) + op.drop_index(op.f("ix_project_setup_runs_guide_id"), table_name="project_setup_runs") + op.drop_index( + op.f("ix_project_setup_runs_celery_task_id"), + table_name="project_setup_runs", + ) + op.drop_table("project_setup_runs") diff --git a/backend/app/db/models.py b/backend/app/db/models.py index d674782..1f325f7 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -12,6 +12,7 @@ PreSubmitCheckerPolicy, Project, ProjectGuide, + ProjectSetupRun, RevisionPolicy, ReviewPolicy, SubmissionArtifactPolicy, diff --git a/backend/app/modules/projects/models.py b/backend/app/modules/projects/models.py index 4eda311..a2c9415 100644 --- a/backend/app/modules/projects/models.py +++ b/backend/app/modules/projects/models.py @@ -256,6 +256,70 @@ class GuideSourceSnapshotItem(Base): created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) +class ProjectSetupRun(Base): + """Non-authoritative ledger for automatic project setup execution.""" + + __tablename__ = "project_setup_runs" + __table_args__ = ( + CheckConstraint( + "status in (" + "'queued', " + "'enqueue_failed', " + "'running_sufficiency_agent', " + "'sufficiency_blocked', " + "'running_policy_derivation_agent', " + "'policy_draft_ready', " + "'setup_blocked', " + "'failed'" + ")", + name="ck_project_setup_runs_status", + ), + ForeignKeyConstraint( + ["project_id", "guide_version"], + ["project_guides.project_id", "project_guides.version"], + name="fk_project_setup_runs_project_guide", + ), + ForeignKeyConstraint( + ["source_snapshot_id", "source_snapshot_hash"], + ["guide_source_snapshots.id", "guide_source_snapshots.bundle_hash"], + name="fk_project_setup_runs_source_snapshot_hash", + ), + ) + + id: Mapped[str] = mapped_column(String(36), primary_key=True) + project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True) + guide_id: Mapped[str] = mapped_column(ForeignKey("project_guides.id"), nullable=False, index=True) + guide_version: Mapped[str] = mapped_column(String(50), nullable=False) + source_snapshot_id: Mapped[str] = mapped_column( + ForeignKey("guide_source_snapshots.id"), + nullable=False, + index=True, + ) + source_snapshot_hash: Mapped[str] = mapped_column(String(71), nullable=False) + celery_task_id: Mapped[str | None] = mapped_column(String(155), index=True) + status: Mapped[str] = mapped_column(String(50), nullable=False, index=True) + current_step: Mapped[str] = mapped_column(String(100), nullable=False) + output_sufficiency_report_id: Mapped[str | None] = mapped_column( + ForeignKey("guide_sufficiency_reports.id"), + index=True, + ) + output_submission_artifact_policy_id: Mapped[str | None] = mapped_column( + ForeignKey("submission_artifact_policies.id"), + index=True, + ) + error_code: Mapped[str | None] = mapped_column(String(100)) + error_summary: Mapped[str | None] = mapped_column(Text) + created_by: Mapped[str] = mapped_column(String(100), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + ) + started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + + class GuideSufficiencyReport(Base): """Workstream assessment of whether a guide snapshot is usable.""" diff --git a/backend/app/modules/projects/repository.py b/backend/app/modules/projects/repository.py index 769438c..e4a88f5 100644 --- a/backend/app/modules/projects/repository.py +++ b/backend/app/modules/projects/repository.py @@ -18,6 +18,7 @@ PreSubmitCheckerPolicy, Project, ProjectGuide, + ProjectSetupRun, RevisionPolicy, ReviewPolicy, SubmissionArtifactPolicy, @@ -267,6 +268,42 @@ async def list_guide_source_snapshot_items( ) return result.scalars().all() + async def add_project_setup_run( + self, + setup_run: ProjectSetupRun, + ) -> ProjectSetupRun: + """Persist a project setup run ledger row.""" + self._session.add(setup_run) + await self._session.flush() + await self._session.refresh(setup_run) + return setup_run + + async def get_project_setup_run(self, setup_run_id: str) -> ProjectSetupRun | None: + """Load one project setup run by primary key.""" + return await self._session.get(ProjectSetupRun, setup_run_id) + + async def get_latest_project_setup_run( + self, + project_id: str, + guide_id: str, + ) -> ProjectSetupRun | None: + """Load the latest setup run for one project guide.""" + result = await self._session.execute( + select(ProjectSetupRun) + .join(GuideSourceSnapshot, ProjectSetupRun.source_snapshot_id == GuideSourceSnapshot.id) + .where( + ProjectSetupRun.project_id == project_id, + ProjectSetupRun.guide_id == guide_id, + ) + .order_by( + GuideSourceSnapshot.captured_at.desc(), + ProjectSetupRun.created_at.desc(), + ProjectSetupRun.id.desc(), + ) + .limit(1) + ) + return result.scalar_one_or_none() + async def add_guide_sufficiency_report( self, report: GuideSufficiencyReport, @@ -291,6 +328,22 @@ async def get_guide_sufficiency_report( """Load one guide sufficiency report by primary key.""" return await self._session.get(GuideSufficiencyReport, report_id) + async def list_guide_sufficiency_reports( + self, + project_id: str, + guide_id: str, + ) -> Sequence[GuideSufficiencyReport]: + """List sufficiency reports for one project guide.""" + result = await self._session.execute( + select(GuideSufficiencyReport) + .where( + GuideSufficiencyReport.project_id == project_id, + GuideSufficiencyReport.guide_id == guide_id, + ) + .order_by(GuideSufficiencyReport.created_at.desc(), GuideSufficiencyReport.id.desc()) + ) + return result.scalars().all() + async def get_sufficiency_report_for_snapshot( self, snapshot_id: str, @@ -327,6 +380,25 @@ async def get_submission_artifact_policy( """Load one submission artifact policy by primary key.""" return await self._session.get(SubmissionArtifactPolicy, policy_id) + async def list_submission_artifact_policies( + self, + project_id: str, + guide_id: str, + ) -> Sequence[SubmissionArtifactPolicy]: + """List submission artifact policies for one project guide.""" + result = await self._session.execute( + select(SubmissionArtifactPolicy) + .where( + SubmissionArtifactPolicy.project_id == project_id, + SubmissionArtifactPolicy.guide_id == guide_id, + ) + .order_by( + SubmissionArtifactPolicy.created_at.desc(), + SubmissionArtifactPolicy.id.desc(), + ) + ) + return result.scalars().all() + async def get_agent_derived_submission_artifact_policy_for_snapshot( self, project_id: str, diff --git a/backend/app/modules/projects/router.py b/backend/app/modules/projects/router.py index e1150be..6232910 100644 --- a/backend/app/modules/projects/router.py +++ b/backend/app/modules/projects/router.py @@ -18,11 +18,13 @@ GuideSufficiencyAcknowledgement, GuideSufficiencyReportCreate, GuideSufficiencyReportResponse, + PreSubmitCheckerPolicySummaryResponse, ProjectCreate, ProjectGuideCreate, ProjectGuideResponse, ProjectGuideUpdate, ProjectResponse, + ProjectSetupRunResponse, SubmissionArtifactPolicyApprove, SubmissionArtifactPolicyCreate, SubmissionArtifactPolicyResponse, @@ -152,6 +154,77 @@ async def create_guide_source_snapshot( raise project_http_error(exc) from exc +@router.get( + "/{project_id}/guides/{guide_id}/setup-runs/latest", + response_model=ProjectSetupRunResponse, +) +async def get_latest_project_setup_run( + project_id: str, + guide_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> ProjectSetupRunResponse: + """Return the latest automatic setup run for one project guide.""" + try: + return await ProjectService(session).get_latest_project_setup_run( + actor, + project_id, + guide_id, + ) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except ProjectServiceError as exc: + raise project_http_error(exc) from exc + + +@router.get( + "/{project_id}/guides/{guide_id}/sufficiency-reports", + response_model=list[GuideSufficiencyReportResponse], +) +async def list_guide_sufficiency_reports( + project_id: str, + guide_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> list[GuideSufficiencyReportResponse]: + """List guide sufficiency reports for one project guide.""" + try: + return await ProjectService(session).list_guide_sufficiency_reports( + actor, + project_id, + guide_id, + ) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except ProjectServiceError as exc: + raise project_http_error(exc) from exc + + +@router.get( + "/{project_id}/guides/{guide_id}/sufficiency-reports/{report_id}", + response_model=GuideSufficiencyReportResponse, +) +async def get_guide_sufficiency_report( + project_id: str, + guide_id: str, + report_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> GuideSufficiencyReportResponse: + """Return one guide sufficiency report for one project guide.""" + try: + return await ProjectService(session).get_guide_sufficiency_report( + actor, + project_id, + guide_id, + report_id, + ) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except ProjectServiceError as exc: + raise project_http_error(exc) from exc + + @router.post( "/{project_id}/guides/{guide_id}/sufficiency-reports", response_model=GuideSufficiencyReportResponse, @@ -178,6 +251,54 @@ async def create_guide_sufficiency_report( raise project_http_error(exc) from exc +@router.get( + "/{project_id}/guides/{guide_id}/submission-artifact-policies", + response_model=list[SubmissionArtifactPolicyResponse], +) +async def list_submission_artifact_policies( + project_id: str, + guide_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> list[SubmissionArtifactPolicyResponse]: + """List submission artifact policies for one project guide.""" + try: + return await ProjectService(session).list_submission_artifact_policies( + actor, + project_id, + guide_id, + ) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except ProjectServiceError as exc: + raise project_http_error(exc) from exc + + +@router.get( + "/{project_id}/guides/{guide_id}/submission-artifact-policies/{policy_id}", + response_model=SubmissionArtifactPolicyResponse, +) +async def get_submission_artifact_policy( + project_id: str, + guide_id: str, + policy_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> SubmissionArtifactPolicyResponse: + """Return one submission artifact policy for one project guide.""" + try: + return await ProjectService(session).get_submission_artifact_policy( + actor, + project_id, + guide_id, + policy_id, + ) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except ProjectServiceError as exc: + raise project_http_error(exc) from exc + + @router.post( "/{project_id}/guides/{guide_id}/source-snapshots/{source_snapshot_id}/run-sufficiency-agent", response_model=GuideSufficiencyReportResponse, @@ -355,6 +476,52 @@ async def approve_submission_artifact_policy( raise project_http_error(exc) from exc +@router.get( + "/{project_id}/guides/{guide_id}/effective-submission-artifact-policy", + response_model=EffectiveProjectSubmissionArtifactPolicyResponse, +) +async def get_current_effective_submission_artifact_policy( + project_id: str, + guide_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> EffectiveProjectSubmissionArtifactPolicyResponse: + """Return the current effective submission artifact policy for a guide.""" + try: + return await ProjectService(session).get_current_effective_submission_artifact_policy( + actor, + project_id, + guide_id, + ) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except ProjectServiceError as exc: + raise project_http_error(exc) from exc + + +@router.get( + "/{project_id}/guides/{guide_id}/pre-submit-checker-policy", + response_model=PreSubmitCheckerPolicySummaryResponse, +) +async def get_current_pre_submit_checker_policy( + project_id: str, + guide_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> PreSubmitCheckerPolicySummaryResponse: + """Return the current project pre-submit checker policy summary.""" + try: + return await ProjectService(session).get_current_pre_submit_checker_policy( + actor, + project_id, + guide_id, + ) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except ProjectServiceError as exc: + raise project_http_error(exc) from exc + + @router.post("/{project_id}/guides/{guide_id}/activate", response_model=ActiveGuideResponse) async def activate_guide( project_id: str, diff --git a/backend/app/modules/projects/schemas.py b/backend/app/modules/projects/schemas.py index 669d421..7796f94 100644 --- a/backend/app/modules/projects/schemas.py +++ b/backend/app/modules/projects/schemas.py @@ -113,6 +113,31 @@ class GuideSourceSnapshotResponse(BaseModel): items: list[GuideSourceSnapshotItemResponse] = Field(default_factory=list) +class ProjectSetupRunResponse(BaseModel): + """Response schema for automatic project setup run ledger rows.""" + + model_config = ConfigDict(from_attributes=True) + + id: str + project_id: str + guide_id: str + guide_version: str + source_snapshot_id: str + source_snapshot_hash: str + celery_task_id: str | None + status: str + current_step: str + output_sufficiency_report_id: str | None + output_submission_artifact_policy_id: str | None + error_code: str | None + error_summary: str | None + created_by: str + created_at: datetime + updated_at: datetime + started_at: datetime | None + finished_at: datetime | None + + class GuideSufficiencyFindingInput(BaseModel): """Input schema for one guide sufficiency finding.""" @@ -371,13 +396,18 @@ class PreSubmitCheckerPolicySummaryResponse(BaseModel): 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 superseded_at: datetime | None +class ActiveGuidePreSubmitCheckerPolicyResponse(PreSubmitCheckerPolicySummaryResponse): + """Active-guide response schema for project pre-submit checker context.""" + + checker_configs: dict[str, Any] + + class ProjectCreate(BaseModel): """Request schema for creating a project shell.""" @@ -520,7 +550,7 @@ class ActiveGuideResponse(BaseModel): guide_sufficiency_report: GuideSufficiencyReportResponse submission_artifact_policy: SubmissionArtifactPolicyResponse effective_submission_artifact_policy: EffectiveProjectSubmissionArtifactPolicyResponse - pre_submit_checker_policy: PreSubmitCheckerPolicySummaryResponse + pre_submit_checker_policy: ActiveGuidePreSubmitCheckerPolicyResponse post_submit_checker_policy: PostSubmitCheckerPolicyResponse review_policy: ReviewPolicyResponse revision_policy: RevisionPolicyResponse diff --git a/backend/app/modules/projects/service.py b/backend/app/modules/projects/service.py index c9a2860..35cc2c5 100644 --- a/backend/app/modules/projects/service.py +++ b/backend/app/modules/projects/service.py @@ -43,6 +43,7 @@ PreSubmitCheckerPolicy, Project, ProjectGuide, + ProjectSetupRun, RevisionPolicy, ReviewPolicy, SubmissionArtifactPolicy, @@ -55,11 +56,11 @@ from app.modules.projects.repository import ProjectRepository, ProjectRepositoryIntegrityError from app.modules.projects.setup_queue import ( ProjectSetupQueueError, - ensure_pre_submit_setup_pipeline_queue_ready, enqueue_pre_submit_setup_pipeline, ) from app.modules.projects.schemas import ( ActiveGuideResponse, + ActiveGuidePreSubmitCheckerPolicyResponse, EffectiveProjectSubmissionArtifactPolicyResponse, GuideSourceSnapshotCreate, GuideSourceSnapshotItemResponse, @@ -77,6 +78,7 @@ ProjectGuideResponse, ProjectGuideUpdate, ProjectResponse, + ProjectSetupRunResponse, RevisionPolicyInput, RevisionPolicyResponse, ReviewPolicyInput, @@ -91,6 +93,7 @@ logger = logging.getLogger(__name__) +PROJECT_SETUP_PUBLIC_ERROR_SUMMARY = "project setup failed; inspect server logs with the setup run id" PROJECT_SETUP_ROLES = {"admin", "project_manager"} ALLOWED_REVIEW_DECISIONS = {"accept", "needs_revision", "reject"} ALLOWED_REVISION_RESUBMISSION_STATES = {"needs_revision"} @@ -118,6 +121,13 @@ r")($|[._/\-])", re.IGNORECASE, ) + + +def safe_project_setup_error_summary(summary: str | None) -> str: + """Return the only public setup-run error summary allowed by APIs/log results.""" + if summary is None or not " ".join(summary.split()): + return "project setup failed" + return PROJECT_SETUP_PUBLIC_ERROR_SUMMARY SECRET_ARTIFACT_TOKEN_SETS = [ {"access", "key"}, {"api", "key"}, @@ -158,6 +168,13 @@ report_status: agent_status for agent_status, report_status in AGENT_SUFFICIENCY_STATUS_TO_REPORT_STATUS.items() } +PROJECT_SETUP_TERMINAL_STATUSES = { + "enqueue_failed", + "sufficiency_blocked", + "policy_draft_ready", + "setup_blocked", + "failed", +} def agent_submission_artifact_policy_version(source_snapshot_hash: str) -> str: @@ -300,6 +317,12 @@ class SourceSnapshotNotFound(ProjectServiceError): status_code = 404 +class ProjectSetupRunNotFound(ProjectServiceError): + """Raised when a project setup run cannot be found for a guide.""" + + status_code = 404 + + class SourceSnapshotInvalid(ProjectServiceError): """Raised when guide-source snapshot input is unsafe or inconsistent.""" @@ -318,6 +341,18 @@ class SubmissionArtifactPolicyNotFound(ProjectServiceError): status_code = 404 +class EffectiveProjectSubmissionArtifactPolicyNotFound(ProjectServiceError): + """Raised when an effective project submission artifact policy cannot be found.""" + + status_code = 404 + + +class PreSubmitCheckerPolicyNotFound(ProjectServiceError): + """Raised when a pre-submit checker policy cannot be found.""" + + status_code = 404 + + class PolicySetupBlocked(ProjectServiceError): """Raised when project submission artifact policy setup is invalid.""" @@ -342,12 +377,6 @@ class AgentRuntimeUnavailable(ProjectServiceError): status_code = 503 -class ProjectSetupQueueUnavailable(ProjectServiceError): - """Raised when automatic project setup cannot be queued.""" - - status_code = 503 - - class ProjectService: """Coordinates project guide rules, persistence, and response shaping. @@ -454,7 +483,6 @@ async def create_guide( project = await self._repo.get_project(project_id) if project is None: raise ProjectNotFound("project not found") - await self._ensure_project_setup_queue_ready_if_needed() guide = ProjectGuide( id=str(uuid4()), project_id=project_id, @@ -466,6 +494,7 @@ async def create_guide( ) source_snapshot_payload = payload.source_snapshot source_snapshot: GuideSourceSnapshot | None = None + setup_run: ProjectSetupRun | None = None try: guide = await self._repo.add_guide(guide) except IntegrityError as exc: @@ -482,13 +511,20 @@ async def create_guide( guide, source_snapshot_payload or GuideSourceSnapshotCreate(), ) + if get_settings().project_setup_pipeline_autostart: + setup_run = await self._create_project_setup_run_model( + actor, + guide, + source_snapshot, + ) await self._session.commit() await self._session.refresh(guide) - if source_snapshot is not None and get_settings().project_setup_pipeline_autostart: + if source_snapshot is not None and setup_run is not None: await self._enqueue_pre_submit_setup_pipeline_after_commit( project_id=project_id, guide_id=guide.id, source_snapshot_id=source_snapshot.id, + setup_run_id=setup_run.id, ) return ProjectGuideResponse.model_validate(guide) @@ -573,14 +609,15 @@ async def create_guide_source_snapshot( guide = await self._lock_project_guide_for_setup(project_id, guide_id) if guide.status != "draft": raise GuideEditBlocked("only draft guides can receive source snapshots") - await self._ensure_project_setup_queue_ready_if_needed() - snapshot = await self._create_guide_source_snapshot_model( actor, project_id, guide, payload, ) + setup_run: ProjectSetupRun | None = None + if get_settings().project_setup_pipeline_autostart: + setup_run = await self._create_project_setup_run_model(actor, guide, snapshot) try: await self._session.commit() except IntegrityError as exc: @@ -588,14 +625,156 @@ async def create_guide_source_snapshot( raise PolicySetupConflict( "guide source snapshot conflicted with concurrent setup; retry" ) from exc - if get_settings().project_setup_pipeline_autostart: + if setup_run is not None: await self._enqueue_pre_submit_setup_pipeline_after_commit( project_id=project_id, guide_id=guide.id, source_snapshot_id=snapshot.id, + setup_run_id=setup_run.id, ) return await self._source_snapshot_response(snapshot) + async def get_latest_project_setup_run( + self, + actor: ActorContext, + project_id: str, + guide_id: str, + ) -> ProjectSetupRunResponse: + """Return the latest automatic setup run for one project guide.""" + require_any_role(actor, PROJECT_SETUP_ROLES) + guide = await self._get_project_guide(project_id, guide_id) + setup_run = await self._repo.get_latest_project_setup_run(project_id, guide.id) + if setup_run is None: + raise ProjectSetupRunNotFound("project setup run not found") + return ProjectSetupRunResponse.model_validate(setup_run) + + async def list_guide_sufficiency_reports( + self, + actor: ActorContext, + project_id: str, + guide_id: str, + ) -> list[GuideSufficiencyReportResponse]: + """List guide sufficiency reports for one project guide.""" + require_any_role(actor, PROJECT_SETUP_ROLES) + guide = await self._get_project_guide(project_id, guide_id) + reports = await self._repo.list_guide_sufficiency_reports(project_id, guide.id) + return [GuideSufficiencyReportResponse.model_validate(report) for report in reports] + + async def get_guide_sufficiency_report( + self, + actor: ActorContext, + project_id: str, + guide_id: str, + report_id: str, + ) -> GuideSufficiencyReportResponse: + """Return one guide sufficiency report scoped to a project guide.""" + require_any_role(actor, PROJECT_SETUP_ROLES) + guide = await self._get_project_guide(project_id, guide_id) + report = await self._repo.get_guide_sufficiency_report(report_id) + if report is None or report.project_id != project_id or report.guide_id != guide.id: + raise SufficiencyReportNotFound("guide sufficiency report not found") + return GuideSufficiencyReportResponse.model_validate(report) + + async def list_submission_artifact_policies( + self, + actor: ActorContext, + project_id: str, + guide_id: str, + ) -> list[SubmissionArtifactPolicyResponse]: + """List submission artifact policies for one project guide.""" + require_any_role(actor, PROJECT_SETUP_ROLES) + guide = await self._get_project_guide(project_id, guide_id) + policies = await self._repo.list_submission_artifact_policies(project_id, guide.id) + return [SubmissionArtifactPolicyResponse.model_validate(policy) for policy in policies] + + async def get_submission_artifact_policy( + self, + actor: ActorContext, + project_id: str, + guide_id: str, + policy_id: str, + ) -> SubmissionArtifactPolicyResponse: + """Return one submission artifact policy scoped to a project guide.""" + require_any_role(actor, PROJECT_SETUP_ROLES) + guide = await self._get_project_guide(project_id, guide_id) + policy = await self._repo.get_submission_artifact_policy(policy_id) + if policy is None or policy.project_id != project_id or policy.guide_id != guide.id: + raise SubmissionArtifactPolicyNotFound("submission artifact policy not found") + return SubmissionArtifactPolicyResponse.model_validate(policy) + + async def get_current_effective_submission_artifact_policy( + self, + actor: ActorContext, + project_id: str, + guide_id: str, + ) -> EffectiveProjectSubmissionArtifactPolicyResponse: + """Return the current effective submission artifact policy for a guide.""" + require_any_role(actor, PROJECT_SETUP_ROLES) + guide = await self._get_project_guide(project_id, guide_id) + try: + snapshot = await self._repo.get_latest_guide_source_snapshot( + project_id, + guide.id, + guide.version, + ) + if snapshot is None: + raise EffectiveProjectSubmissionArtifactPolicyNotFound( + "effective project submission artifact policy not found" + ) + policy = await self._repo.get_effective_submission_artifact_policy( + project_id, + guide.version, + snapshot.id, + ) + except ProjectRepositoryIntegrityError as exc: + raise PolicySetupConflict( + "effective project submission artifact policy chain is ambiguous" + ) from exc + if policy is None or policy.guide_id != guide.id: + raise EffectiveProjectSubmissionArtifactPolicyNotFound( + "effective project submission artifact policy not found" + ) + return EffectiveProjectSubmissionArtifactPolicyResponse.model_validate(policy) + + async def get_current_pre_submit_checker_policy( + self, + actor: ActorContext, + project_id: str, + guide_id: str, + ) -> PreSubmitCheckerPolicySummaryResponse: + """Return the current project pre-submit checker policy summary.""" + require_any_role(actor, PROJECT_SETUP_ROLES) + guide = await self._get_project_guide(project_id, guide_id) + try: + snapshot = await self._repo.get_latest_guide_source_snapshot( + project_id, + guide.id, + guide.version, + ) + if snapshot is None: + raise PreSubmitCheckerPolicyNotFound("pre-submit checker policy not found") + effective_policy = await self._repo.get_effective_submission_artifact_policy( + project_id, + guide.version, + snapshot.id, + ) + if effective_policy is None or effective_policy.guide_id != guide.id: + raise PreSubmitCheckerPolicyNotFound("pre-submit checker policy not found") + policy = await self._repo.get_pre_submit_checker_policy_for_effective_policy( + effective_policy.id + ) + except ProjectRepositoryIntegrityError as exc: + raise PolicySetupConflict("pre-submit checker policy chain is ambiguous") from exc + if ( + policy is None + or policy.guide_id != guide.id + or policy.source_snapshot_id != snapshot.id + or policy.lifecycle_status != "compiled" + or policy.compiled_bundle_hash is None + ): + raise PreSubmitCheckerPolicyNotFound("pre-submit checker policy not found") + return PreSubmitCheckerPolicySummaryResponse.model_validate(policy) + async def create_guide_sufficiency_report( self, actor: ActorContext, @@ -1549,12 +1728,33 @@ async def _create_guide_source_snapshot_model( ] return await self._repo.add_guide_source_snapshot(snapshot, item_models) + async def _create_project_setup_run_model( + self, + actor: ActorContext, + guide: ProjectGuide, + snapshot: GuideSourceSnapshot, + ) -> ProjectSetupRun: + """Create a queued setup-run ledger row without committing.""" + setup_run = ProjectSetupRun( + id=str(uuid4()), + project_id=guide.project_id, + guide_id=guide.id, + guide_version=guide.version, + source_snapshot_id=snapshot.id, + source_snapshot_hash=snapshot.bundle_hash, + status="queued", + current_step="queued", + created_by=actor.actor_id, + ) + return await self._repo.add_project_setup_run(setup_run) + async def _enqueue_pre_submit_setup_pipeline_after_commit( self, *, project_id: str, guide_id: str, source_snapshot_id: str, + setup_run_id: str, ) -> str | None: """Enqueue automatic pre-submit setup for an immutable source snapshot. @@ -1567,39 +1767,143 @@ async def _enqueue_pre_submit_setup_pipeline_after_commit( project_id: Project that owns the guide. guide_id: Guide whose snapshot should be processed. source_snapshot_id: Immutable source snapshot to process. + setup_run_id: Ledger row that should receive the Celery task id. Returns: Celery task id when enqueue succeeds; otherwise ``None``. """ try: - return await asyncio.to_thread( + task_id = await asyncio.to_thread( enqueue_pre_submit_setup_pipeline, project_id=project_id, guide_id=guide_id, source_snapshot_id=source_snapshot_id, + setup_run_id=setup_run_id, ) except ProjectSetupQueueError as exc: + safe_summary = self._safe_project_setup_error_summary(str(exc)) logger.warning( "project setup pipeline enqueue failed after commit", extra={ "project_id": project_id, "guide_id": guide_id, "source_snapshot_id": source_snapshot_id, + "setup_run_id": setup_run_id, + "error_code": exc.__class__.__name__, + "error_summary": safe_summary, }, - exc_info=exc, + ) + await self.update_project_setup_run_status( + setup_run_id, + status="enqueue_failed", + current_step="enqueue", + error_code=exc.__class__.__name__, + error_summary=safe_summary, ) return None + setup_run = await self._repo.get_project_setup_run(setup_run_id) + if setup_run is not None: + setup_run.celery_task_id = task_id + await self._session.commit() + return task_id - async def _ensure_project_setup_queue_ready_if_needed(self) -> None: - """Fail before mutation when automatic setup has no usable queue.""" - if not get_settings().project_setup_pipeline_autostart: - return - try: - await asyncio.to_thread(ensure_pre_submit_setup_pipeline_queue_ready) - except ProjectSetupQueueError as exc: - raise ProjectSetupQueueUnavailable( - "project setup pipeline queue is unavailable" - ) from exc + async def update_project_setup_run_status( + self, + setup_run_id: str, + *, + status: str, + current_step: str, + output_sufficiency_report_id: str | None = None, + output_submission_artifact_policy_id: str | None = None, + error_code: str | None = None, + error_summary: str | None = None, + ) -> ProjectSetupRunResponse: + """Update the setup-run ledger from the internal project setup worker.""" + setup_run = await self._repo.get_project_setup_run(setup_run_id) + if setup_run is None: + raise ProjectSetupRunNotFound("project setup run not found") + await self._validate_project_setup_run_outputs( + setup_run, + output_sufficiency_report_id=output_sufficiency_report_id, + output_submission_artifact_policy_id=output_submission_artifact_policy_id, + ) + now = datetime.now(UTC) + setup_run.status = status + setup_run.current_step = current_step + if setup_run.started_at is None and status != "queued": + setup_run.started_at = now + if status in PROJECT_SETUP_TERMINAL_STATUSES: + setup_run.finished_at = now + if output_sufficiency_report_id is not None: + setup_run.output_sufficiency_report_id = output_sufficiency_report_id + if output_submission_artifact_policy_id is not None: + setup_run.output_submission_artifact_policy_id = output_submission_artifact_policy_id + setup_run.error_code = error_code + setup_run.error_summary = ( + self._safe_project_setup_error_summary(error_summary) + if error_summary is not None + else None + ) + await self._session.commit() + await self._session.refresh(setup_run) + return ProjectSetupRunResponse.model_validate(setup_run) + + async def validate_project_setup_run_context( + self, + setup_run_id: str, + *, + project_id: str, + guide_id: str, + source_snapshot_id: str, + ) -> ProjectSetupRunResponse: + """Validate that a worker payload matches the setup-run ledger row.""" + setup_run = await self._repo.get_project_setup_run(setup_run_id) + if setup_run is None: + raise ProjectSetupRunNotFound("project setup run not found") + if ( + setup_run.project_id != project_id + or setup_run.guide_id != guide_id + or setup_run.source_snapshot_id != source_snapshot_id + ): + raise PolicySetupConflict("project setup run context mismatch") + return ProjectSetupRunResponse.model_validate(setup_run) + + async def _validate_project_setup_run_outputs( + self, + setup_run: ProjectSetupRun, + *, + output_sufficiency_report_id: str | None, + output_submission_artifact_policy_id: str | None, + ) -> None: + """Require setup-run output ids to belong to the same setup context.""" + if output_sufficiency_report_id is not None: + report = await self._repo.get_guide_sufficiency_report(output_sufficiency_report_id) + if report is None or not self._is_project_setup_run_output_match(setup_run, report): + raise PolicySetupConflict("project setup run sufficiency output mismatch") + if output_submission_artifact_policy_id is not None: + policy = await self._repo.get_submission_artifact_policy( + output_submission_artifact_policy_id + ) + if policy is None or not self._is_project_setup_run_output_match(setup_run, policy): + raise PolicySetupConflict("project setup run policy output mismatch") + + def _is_project_setup_run_output_match( + self, + setup_run: ProjectSetupRun, + output: GuideSufficiencyReport | SubmissionArtifactPolicy, + ) -> bool: + """Return whether an output row belongs to the setup-run context.""" + return ( + output.project_id == setup_run.project_id + and output.guide_id == setup_run.guide_id + and output.guide_version == setup_run.guide_version + and output.source_snapshot_id == setup_run.source_snapshot_id + and output.source_snapshot_hash == setup_run.source_snapshot_hash + ) + + def _safe_project_setup_error_summary(self, summary: str) -> str: + """Return a bounded setup error summary safe for API responses.""" + return safe_project_setup_error_summary(summary) async def _ensure_snapshot_is_latest( self, @@ -2825,7 +3129,7 @@ async def _active_response( effective_submission_artifact_policy=( EffectiveProjectSubmissionArtifactPolicyResponse.model_validate(effective_policy) ), - pre_submit_checker_policy=PreSubmitCheckerPolicySummaryResponse.model_validate( + pre_submit_checker_policy=ActiveGuidePreSubmitCheckerPolicyResponse.model_validate( pre_submit_checker_policy ), post_submit_checker_policy=PostSubmitCheckerPolicyResponse.model_validate( diff --git a/backend/app/modules/projects/setup_queue.py b/backend/app/modules/projects/setup_queue.py index 113d2b5..58b7f9d 100644 --- a/backend/app/modules/projects/setup_queue.py +++ b/backend/app/modules/projects/setup_queue.py @@ -13,34 +13,12 @@ class ProjectSetupQueueError(RuntimeError): """Raised when Workstream cannot enqueue project setup automation.""" -def ensure_pre_submit_setup_pipeline_queue_ready() -> None: - """Validate that the project setup queue can accept work. - - Raises: - ProjectSetupQueueError: If the queue is not configured or reachable. - """ - try: - from app.workers.project_setup import run_pre_submit_setup_pipeline - - _sync_task_settings() - settings = get_settings() - if settings.celery_task_always_eager: - return - if settings.celery_broker_url is None: - raise CeleryConfigurationError( - "WORKSTREAM_CELERY_BROKER_URL must be set for project setup queue" - ) - with run_pre_submit_setup_pipeline.app.connection_for_write() as connection: - connection.ensure_connection(max_retries=0) - except (CeleryConfigurationError, CeleryError, KombuError, OSError) as exc: - raise ProjectSetupQueueError("project setup pipeline queue is unavailable") from exc - - def enqueue_pre_submit_setup_pipeline( *, project_id: str, guide_id: str, source_snapshot_id: str, + setup_run_id: str, ) -> str: """Enqueue the Celery project setup pipeline. @@ -48,6 +26,7 @@ def enqueue_pre_submit_setup_pipeline( project_id: Project that owns the guide. guide_id: Guide whose source snapshot should be processed. source_snapshot_id: Immutable source snapshot to analyze. + setup_run_id: Project setup run ledger row to update from the worker. Returns: Celery task id. @@ -60,7 +39,7 @@ def enqueue_pre_submit_setup_pipeline( _sync_task_settings() result = run_pre_submit_setup_pipeline.apply_async( - args=(project_id, guide_id, source_snapshot_id) + args=(project_id, guide_id, source_snapshot_id, setup_run_id) ) except (CeleryConfigurationError, CeleryError, KombuError, OSError) as exc: raise ProjectSetupQueueError("project setup pipeline could not be enqueued") from exc diff --git a/backend/app/workers/project_setup.py b/backend/app/workers/project_setup.py index 7f63304..10af0e0 100644 --- a/backend/app/workers/project_setup.py +++ b/backend/app/workers/project_setup.py @@ -11,7 +11,11 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from app.db.session import get_database_url -from app.modules.projects.service import ProjectService, ProjectServiceError +from app.modules.projects.service import ( + ProjectService, + ProjectServiceError, + safe_project_setup_error_summary, +) from app.schemas.auth import ActorContext from app.workers.celery_app import celery_app @@ -42,6 +46,7 @@ def run_pre_submit_setup_pipeline( project_id: str, guide_id: str, source_snapshot_id: str, + setup_run_id: str, ) -> dict[str, Any]: """Run guide sufficiency and policy derivation for a source snapshot. @@ -49,12 +54,18 @@ def run_pre_submit_setup_pipeline( project_id: Project that owns the guide. guide_id: Guide whose latest source snapshot should be processed. source_snapshot_id: Immutable source snapshot to analyze. + setup_run_id: Project setup run ledger row to update. Returns: Machine-readable terminal pipeline state. """ return _run_async_task( - lambda: _run_pre_submit_setup_pipeline(project_id, guide_id, source_snapshot_id) + lambda: _run_pre_submit_setup_pipeline( + project_id, + guide_id, + source_snapshot_id, + setup_run_id, + ) ) @@ -78,6 +89,7 @@ async def _run_pre_submit_setup_pipeline( project_id: str, guide_id: str, source_snapshot_id: str, + setup_run_id: str, ) -> dict[str, Any]: """Execute the project setup pipeline using async service contracts.""" actor = project_setup_pipeline_actor() @@ -87,6 +99,17 @@ async def _run_pre_submit_setup_pipeline( async with session_factory() as session: service = ProjectService(session) try: + await service.validate_project_setup_run_context( + setup_run_id, + project_id=project_id, + guide_id=guide_id, + source_snapshot_id=source_snapshot_id, + ) + await service.update_project_setup_run_status( + setup_run_id, + status="running_sufficiency_agent", + current_step="guide_sufficiency", + ) sufficiency_report, _ = await service.run_guide_sufficiency_agent( actor, project_id, @@ -94,35 +117,87 @@ async def _run_pre_submit_setup_pipeline( source_snapshot_id, ) if sufficiency_report.status == "blocked": + await service.update_project_setup_run_status( + setup_run_id, + status="sufficiency_blocked", + current_step="guide_sufficiency", + output_sufficiency_report_id=sufficiency_report.id, + ) return { "status": "sufficiency_blocked", "guide_sufficiency_report_id": sufficiency_report.id, "submission_artifact_policy_id": None, } + await service.update_project_setup_run_status( + setup_run_id, + status="running_policy_derivation_agent", + current_step="submission_artifact_policy_derivation", + output_sufficiency_report_id=sufficiency_report.id, + ) policy, _ = await service.run_submission_artifact_policy_derivation_agent( actor, project_id, guide_id, source_snapshot_id, ) + await service.update_project_setup_run_status( + setup_run_id, + status="policy_draft_ready", + current_step="submission_artifact_policy_derivation", + output_sufficiency_report_id=sufficiency_report.id, + output_submission_artifact_policy_id=policy.id, + ) return { "status": "policy_draft_ready", "guide_sufficiency_report_id": sufficiency_report.id, "submission_artifact_policy_id": policy.id, } except ProjectServiceError as exc: + public_error = safe_project_setup_error_summary(str(exc)) logger.warning( "project setup pipeline stopped", extra={ "project_id": project_id, "guide_id": guide_id, "source_snapshot_id": source_snapshot_id, - "error": str(exc), + "setup_run_id": setup_run_id, + "error_code": exc.__class__.__name__, + "error_summary": public_error, }, ) + await service.update_project_setup_run_status( + setup_run_id, + status="setup_blocked", + current_step="project_setup", + error_code=exc.__class__.__name__, + error_summary=public_error, + ) return { "status": "setup_blocked", - "error": str(exc), + "error": public_error, + "guide_sufficiency_report_id": None, + "submission_artifact_policy_id": None, + } + except Exception as exc: + logger.exception( + "project setup pipeline failed", + extra={ + "project_id": project_id, + "guide_id": guide_id, + "source_snapshot_id": source_snapshot_id, + "setup_run_id": setup_run_id, + }, + ) + await service.update_project_setup_run_status( + setup_run_id, + status="failed", + current_step="project_setup", + error_code=exc.__class__.__name__, + error_summary="unexpected project setup pipeline failure", + ) + return { + "status": "failed", + "error": "unexpected project setup pipeline failure", "guide_sufficiency_report_id": None, "submission_artifact_policy_id": None, } diff --git a/backend/scripts/api_contract_e2e.py b/backend/scripts/api_contract_e2e.py index be5bb00..d543a33 100644 --- a/backend/scripts/api_contract_e2e.py +++ b/backend/scripts/api_contract_e2e.py @@ -500,25 +500,6 @@ def sha256_token(seed: str) -> str: return f"sha256:{hashlib.sha256(seed.encode('utf-8')).hexdigest()}" -async def load_pre_submit_checker_policy(effective_policy: dict) -> dict: - """Load the project pre-submit checker policy compiled during approval.""" - async with db_session.get_session_factory()() as session: - pre_submit_checker_policy = await session.scalar( - select(PreSubmitCheckerPolicy).where( - PreSubmitCheckerPolicy.effective_policy_id == effective_policy["id"] - ) - ) - ensure(pre_submit_checker_policy is not None, "pre-submit checker policy missing") - ensure( - pre_submit_checker_policy.lifecycle_status == "compiled", - "pre-submit checker policy was not compiled during approval", - ) - return { - "compiled_bundle": pre_submit_checker_policy.compiled_bundle, - "compiled_bundle_hash": pre_submit_checker_policy.compiled_bundle_hash, - } - - def submission_artifact_policy_body() -> dict: """Build the project submission artifact policy used by the API contract drill. @@ -593,7 +574,7 @@ async def create_policy_bundle_for_guide( }, 201, ) - await request_json( + report = await request_json( client, "POST", f"/api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports", @@ -606,6 +587,21 @@ async def create_policy_bundle_for_guide( }, 201, ) + reports = await request_json( + client, + "GET", + f"/api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports", + manager_token, + ) + ensure(isinstance(reports, list), "sufficiency report list did not return a list") + ensure(len(reports) == 1, f"expected one sufficiency report, got {len(reports)}") + ensure(reports[0]["id"] == report["id"], "sufficiency report list returned wrong report") + await request_json( + client, + "GET", + f"/api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports/{report['id']}", + manager_token, + ) policy = await request_json( client, "POST", @@ -618,6 +614,21 @@ async def create_policy_bundle_for_guide( }, 201, ) + policies = await request_json( + client, + "GET", + f"/api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies", + manager_token, + ) + ensure(isinstance(policies, list), "submission artifact policy list did not return a list") + ensure(len(policies) == 1, f"expected one submission artifact policy, got {len(policies)}") + ensure(policies[0]["id"] == policy["id"], "submission policy list returned wrong policy") + await request_json( + client, + "GET", + f"/api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies/{policy['id']}", + manager_token, + ) effective_policy = await request_json( client, "POST", @@ -626,7 +637,30 @@ async def create_policy_bundle_for_guide( manager_token, {"approval_note": "Approved for API contract real API drill."}, ) - await load_pre_submit_checker_policy(effective_policy) + visible_effective_policy = await request_json( + client, + "GET", + f"/api/v1/projects/{project_id}/guides/{guide_id}/effective-submission-artifact-policy", + manager_token, + ) + ensure( + visible_effective_policy["id"] == effective_policy["id"], + "effective policy visibility endpoint returned the wrong policy", + ) + pre_submit_checker_policy = await request_json( + client, + "GET", + f"/api/v1/projects/{project_id}/guides/{guide_id}/pre-submit-checker-policy", + manager_token, + ) + ensure( + pre_submit_checker_policy["lifecycle_status"] == "compiled", + "pre-submit checker policy was not compiled during approval", + ) + ensure( + pre_submit_checker_policy["effective_policy_id"] == effective_policy["id"], + "pre-submit checker visibility endpoint returned the wrong effective policy", + ) return effective_policy diff --git a/backend/tests/test_projects.py b/backend/tests/test_projects.py index d135d91..9501808 100644 --- a/backend/tests/test_projects.py +++ b/backend/tests/test_projects.py @@ -45,6 +45,7 @@ PostSubmitCheckerPolicy, PreSubmitCheckerPolicy, ProjectGuide, + ProjectSetupRun, RevisionPolicy, ReviewPolicy, SubmissionArtifactPolicy, @@ -518,13 +519,20 @@ async def derive_submission_artifact_policy( enqueued: list[dict[str, str]] = [] - def capture_enqueue(*, project_id: str, guide_id: str, source_snapshot_id: str) -> str: + def capture_enqueue( + *, + project_id: str, + guide_id: str, + source_snapshot_id: str, + setup_run_id: str, + ) -> str: """Capture queue arguments without running Celery.""" enqueued.append( { "project_id": project_id, "guide_id": guide_id, "source_snapshot_id": source_snapshot_id, + "setup_run_id": setup_run_id, } ) return "captured-task-id" @@ -549,6 +557,7 @@ def capture_enqueue(*, project_id: str, guide_id: str, source_snapshot_id: str) assert len(enqueued) == 1 assert enqueued[0]["project_id"] == project["id"] assert enqueued[0]["guide_id"] == guide["id"] + assert enqueued[0]["setup_run_id"] async with db_session.get_session_factory()() as session: snapshots = ( await session.scalars( @@ -572,41 +581,6 @@ def capture_enqueue(*, project_id: str, guide_id: str, source_snapshot_id: str) assert policies == [] -async def test_create_guide_autostart_requires_queue_before_persisting( - project_client: AsyncClient, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Prove queue failure does not leave guide setup half-created.""" - project = await create_project(project_client) - monkeypatch.setenv("WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART", "true") - monkeypatch.setenv("WORKSTREAM_CELERY_TASK_ALWAYS_EAGER", "false") - monkeypatch.delenv("WORKSTREAM_CELERY_BROKER_URL", raising=False) - get_settings.cache_clear() - - response = await project_client.post( - f"/api/v1/projects/{project['id']}/guides", - headers=auth_headers(), - json=complete_guide_payload(), - ) - - assert response.status_code == 503, response.text - assert response.json()["detail"] == "project setup pipeline queue is unavailable" - async with db_session.get_session_factory()() as session: - guides = ( - await session.scalars( - select(ProjectGuide).where(ProjectGuide.project_id == project["id"]) - ) - ).all() - snapshots = ( - await session.scalars( - select(GuideSourceSnapshot).where(GuideSourceSnapshot.project_id == project["id"]) - ) - ).all() - - assert guides == [] - assert snapshots == [] - - async def test_get_project_does_not_require_project_setup_queue( project_client: AsyncClient, monkeypatch: pytest.MonkeyPatch, @@ -634,21 +608,19 @@ async def test_create_guide_returns_created_when_post_commit_enqueue_fails( """A late broker failure cannot turn a durable guide create into a false 503.""" project = await create_project(project_client) - def queue_ready() -> None: - """Pretend the pre-mutation readiness check succeeded.""" - - def enqueue_failure(*, project_id: str, guide_id: str, source_snapshot_id: str) -> str: + def enqueue_failure( + *, + project_id: str, + guide_id: str, + source_snapshot_id: str, + setup_run_id: str, + ) -> str: """Simulate a broker outage after the guide transaction commits.""" raise ProjectSetupQueueError("queue failed after commit") monkeypatch.setenv("WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART", "true") monkeypatch.setenv("WORKSTREAM_CELERY_TASK_ALWAYS_EAGER", "false") get_settings.cache_clear() - monkeypatch.setattr( - project_service_module, - "ensure_pre_submit_setup_pipeline_queue_ready", - queue_ready, - ) monkeypatch.setattr( project_service_module, "enqueue_pre_submit_setup_pipeline", @@ -754,13 +726,20 @@ async def test_create_source_snapshot_autostart_enqueues_latest_snapshot( ) -> None: enqueued: list[dict[str, str]] = [] - def capture_enqueue(*, project_id: str, guide_id: str, source_snapshot_id: str) -> str: + def capture_enqueue( + *, + project_id: str, + guide_id: str, + source_snapshot_id: str, + setup_run_id: str, + ) -> str: """Capture queue arguments without running Celery.""" enqueued.append( { "project_id": project_id, "guide_id": guide_id, "source_snapshot_id": source_snapshot_id, + "setup_run_id": setup_run_id, } ) return "captured-task-id" @@ -783,6 +762,7 @@ def capture_enqueue(*, project_id: str, guide_id: str, source_snapshot_id: str) "project_id": project["id"], "guide_id": guide["id"], "source_snapshot_id": snapshot["id"], + "setup_run_id": enqueued[0]["setup_run_id"], } ] @@ -795,20 +775,18 @@ async def test_create_source_snapshot_returns_created_when_post_commit_enqueue_f project = await create_project(project_client) guide = await create_guide(project_client, project["id"], complete_guide_payload()) - def queue_ready() -> None: - """Pretend the pre-mutation readiness check succeeded.""" - - def enqueue_failure(*, project_id: str, guide_id: str, source_snapshot_id: str) -> str: + def enqueue_failure( + *, + project_id: str, + guide_id: str, + source_snapshot_id: str, + setup_run_id: str, + ) -> str: """Simulate a broker outage after the snapshot transaction commits.""" raise ProjectSetupQueueError("queue failed after commit") monkeypatch.setenv("WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART", "true") get_settings.cache_clear() - monkeypatch.setattr( - project_service_module, - "ensure_pre_submit_setup_pipeline_queue_ready", - queue_ready, - ) monkeypatch.setattr( project_service_module, "enqueue_pre_submit_setup_pipeline", @@ -1070,6 +1048,559 @@ async def create_approved_policy_bundle( } +def test_project_setup_run_status_constraint_metadata() -> None: + status_constraint = next( + constraint + for constraint in ProjectSetupRun.__table__.constraints + if constraint.name is not None + and constraint.name.endswith("ck_project_setup_runs_status") + ) + + constraint_sql = str(status_constraint.sqltext) + + for status in ( + "queued", + "enqueue_failed", + "running_sufficiency_agent", + "sufficiency_blocked", + "running_policy_derivation_agent", + "policy_draft_ready", + "setup_blocked", + "failed", + ): + assert status in constraint_sql + + +def test_project_setup_error_summary_redacts_sensitive_diagnostics() -> None: + service = ProjectService.__new__(ProjectService) + + unsafe_summaries = [ + "broker rejected https://storage.flow.test/signed?token=secret", + "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.signature", + "Basic d29ya3N0cmVhbTpzZWNyZXQ=", + "aws access key AKIAIOSFODNN7EXAMPLE failed", + "failed reading projects/acme/snapshots/source.md", + "path=/home/abiorh/workstream/private.py failed", + 'Traceback most recent call last File "/srv/app/project_setup.py", line 10', + r"worker failed at C:\Users\alice\secret\guide.md", + r"worker failed at \\server\share\guide.md", + "object key s3://private-bucket/customer/path failed", + ] + + for summary in unsafe_summaries: + assert service._safe_project_setup_error_summary(summary) == ( + "project setup failed; inspect server logs with the setup run id" + ) + + assert service._safe_project_setup_error_summary("broker temporarily unavailable") == ( + "project setup failed; inspect server logs with the setup run id" + ) + assert service._safe_project_setup_error_summary(" ") == "project setup failed" + + +async def test_project_setup_visibility_apis_show_automatic_setup_outputs( + project_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, + deterministic_project_agent_runtime: None, +) -> None: + monkeypatch.setenv("WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART", "true") + monkeypatch.setenv("WORKSTREAM_CELERY_TASK_ALWAYS_EAGER", "true") + get_settings.cache_clear() + project = await create_project(project_client) + guide = await create_guide( + project_client, + project["id"], + { + **complete_guide_payload(), + "source_snapshot": source_snapshot_payload(), + }, + ) + + setup_run_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/setup-runs/latest", + headers=auth_headers(), + ) + + assert setup_run_response.status_code == 200, setup_run_response.text + setup_run = setup_run_response.json() + assert setup_run["status"] == "policy_draft_ready" + assert setup_run["current_step"] == "submission_artifact_policy_derivation" + assert setup_run["source_snapshot_hash"].startswith("sha256:") + assert setup_run["celery_task_id"] + assert setup_run["output_sufficiency_report_id"] + assert setup_run["output_submission_artifact_policy_id"] + assert setup_run["error_code"] is None + assert setup_run["error_summary"] is None + + reports_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/sufficiency-reports", + headers=auth_headers(), + ) + assert reports_response.status_code == 200, reports_response.text + reports = reports_response.json() + assert [report["id"] for report in reports] == [setup_run["output_sufficiency_report_id"]] + + report_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/sufficiency-reports/" + f"{reports[0]['id']}", + headers=auth_headers(), + ) + assert report_response.status_code == 200, report_response.text + assert report_response.json()["source_snapshot_id"] == setup_run["source_snapshot_id"] + + policies_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/submission-artifact-policies", + headers=auth_headers(), + ) + assert policies_response.status_code == 200, policies_response.text + policies = policies_response.json() + assert [policy["id"] for policy in policies] == [ + setup_run["output_submission_artifact_policy_id"] + ] + + policy_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/submission-artifact-policies/" + f"{policies[0]['id']}", + headers=auth_headers(), + ) + assert policy_response.status_code == 200, policy_response.text + assert policy_response.json()["source_snapshot_hash"] == setup_run["source_snapshot_hash"] + + missing_effective = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/" + "effective-submission-artifact-policy", + headers=auth_headers(), + ) + missing_pre_submit = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/pre-submit-checker-policy", + headers=auth_headers(), + ) + assert missing_effective.status_code == 404 + assert missing_pre_submit.status_code == 404 + + effective = await approve_submission_artifact_policy( + project_client, + project["id"], + guide["id"], + policies[0]["id"], + ) + effective_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/" + "effective-submission-artifact-policy", + headers=auth_headers(), + ) + assert effective_response.status_code == 200, effective_response.text + assert effective_response.json()["id"] == effective["id"] + + checker_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/pre-submit-checker-policy", + headers=auth_headers(), + ) + assert checker_response.status_code == 200, checker_response.text + checker_policy = checker_response.json() + assert checker_policy["effective_policy_id"] == effective["id"] + assert checker_policy["compiled_bundle_hash"].startswith("sha256:") + assert "compiled_bundle" not in checker_policy + assert "checker_configs" not in checker_policy + + second_project_response = await project_client.post( + "/api/v1/projects", + headers=auth_headers(), + json={ + "name": "STEM Eval Visibility Two", + "slug": "stem-eval-visibility-two", + "description": "Second project for visibility scoping checks", + }, + ) + assert second_project_response.status_code == 201, second_project_response.text + second_project = second_project_response.json() + second_guide = await create_guide( + project_client, + second_project["id"], + { + **complete_guide_payload(), + "source_snapshot": source_snapshot_payload( + durable_ref="https://docs.flow.test/stem/second-guide.md" + ), + }, + ) + second_setup_response = await project_client.get( + f"/api/v1/projects/{second_project['id']}/guides/{second_guide['id']}/" + "setup-runs/latest", + headers=auth_headers(), + ) + assert second_setup_response.status_code == 200, second_setup_response.text + second_setup_run = second_setup_response.json() + second_policies_response = await project_client.get( + f"/api/v1/projects/{second_project['id']}/guides/{second_guide['id']}/" + "submission-artifact-policies", + headers=auth_headers(), + ) + assert second_policies_response.status_code == 200, second_policies_response.text + second_policy = second_policies_response.json()[0] + second_effective = await approve_submission_artifact_policy( + project_client, + second_project["id"], + second_guide["id"], + second_policy["id"], + ) + + first_setup_again_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/setup-runs/latest", + headers=auth_headers(), + ) + assert first_setup_again_response.status_code == 200, first_setup_again_response.text + assert first_setup_again_response.json()["id"] == setup_run["id"] + first_reports_again_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/sufficiency-reports", + headers=auth_headers(), + ) + assert first_reports_again_response.status_code == 200, first_reports_again_response.text + assert [report["id"] for report in first_reports_again_response.json()] == [ + setup_run["output_sufficiency_report_id"] + ] + first_policies_again_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/submission-artifact-policies", + headers=auth_headers(), + ) + assert first_policies_again_response.status_code == 200, first_policies_again_response.text + assert [policy["id"] for policy in first_policies_again_response.json()] == [ + setup_run["output_submission_artifact_policy_id"] + ] + wrong_report_context_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/sufficiency-reports/" + f"{second_setup_run['output_sufficiency_report_id']}", + headers=auth_headers(), + ) + wrong_policy_context_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/submission-artifact-policies/" + f"{second_setup_run['output_submission_artifact_policy_id']}", + headers=auth_headers(), + ) + assert wrong_report_context_response.status_code == 404 + assert wrong_policy_context_response.status_code == 404 + second_effective_response = await project_client.get( + f"/api/v1/projects/{second_project['id']}/guides/{second_guide['id']}/" + "effective-submission-artifact-policy", + headers=auth_headers(), + ) + assert second_effective_response.status_code == 200, second_effective_response.text + assert second_effective_response.json()["id"] == second_effective["id"] + second_checker_response = await project_client.get( + f"/api/v1/projects/{second_project['id']}/guides/{second_guide['id']}/" + "pre-submit-checker-policy", + headers=auth_headers(), + ) + assert second_checker_response.status_code == 200, second_checker_response.text + assert second_checker_response.json()["effective_policy_id"] == second_effective["id"] + + same_project_other_guide = await create_guide( + project_client, + project["id"], + { + **complete_guide_payload(version="v2"), + "source_snapshot": source_snapshot_payload( + durable_ref="https://docs.flow.test/stem/same-project-other-guide.md" + ), + }, + ) + same_project_other_setup_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{same_project_other_guide['id']}/" + "setup-runs/latest", + headers=auth_headers(), + ) + assert ( + same_project_other_setup_response.status_code == 200 + ), same_project_other_setup_response.text + same_project_other_setup_run = same_project_other_setup_response.json() + same_project_other_policies_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{same_project_other_guide['id']}/" + "submission-artifact-policies", + headers=auth_headers(), + ) + assert ( + same_project_other_policies_response.status_code == 200 + ), same_project_other_policies_response.text + same_project_other_policy = same_project_other_policies_response.json()[0] + same_project_other_effective = await approve_submission_artifact_policy( + project_client, + project["id"], + same_project_other_guide["id"], + same_project_other_policy["id"], + ) + first_setup_after_same_project_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/setup-runs/latest", + headers=auth_headers(), + ) + assert ( + first_setup_after_same_project_response.status_code == 200 + ), first_setup_after_same_project_response.text + assert first_setup_after_same_project_response.json()["id"] == setup_run["id"] + wrong_same_project_report_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/sufficiency-reports/" + f"{same_project_other_setup_run['output_sufficiency_report_id']}", + headers=auth_headers(), + ) + wrong_same_project_policy_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/submission-artifact-policies/" + f"{same_project_other_setup_run['output_submission_artifact_policy_id']}", + headers=auth_headers(), + ) + assert wrong_same_project_report_response.status_code == 404 + assert wrong_same_project_policy_response.status_code == 404 + same_project_other_effective_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{same_project_other_guide['id']}/" + "effective-submission-artifact-policy", + headers=auth_headers(), + ) + assert ( + same_project_other_effective_response.status_code == 200 + ), same_project_other_effective_response.text + assert same_project_other_effective_response.json()["id"] == same_project_other_effective["id"] + same_project_other_checker_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{same_project_other_guide['id']}/" + "pre-submit-checker-policy", + headers=auth_headers(), + ) + assert ( + same_project_other_checker_response.status_code == 200 + ), same_project_other_checker_response.text + assert ( + same_project_other_checker_response.json()["effective_policy_id"] + == same_project_other_effective["id"] + ) + + newer_snapshot_response = await project_client.post( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/source-snapshots", + headers=auth_headers(), + json=source_snapshot_payload(durable_ref="https://docs.flow.test/stem/new-guide.md"), + ) + assert newer_snapshot_response.status_code == 201, newer_snapshot_response.text + + stale_effective_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/" + "effective-submission-artifact-policy", + headers=auth_headers(), + ) + stale_checker_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/pre-submit-checker-policy", + headers=auth_headers(), + ) + + assert stale_effective_response.status_code == 404 + assert stale_checker_response.status_code == 404 + + +async def test_pre_submit_visibility_requires_compiled_policy( + project_client: AsyncClient, +) -> None: + project = await create_project(project_client) + guide = await create_guide(project_client, project["id"], complete_guide_payload()) + await create_approved_policy_bundle( + project_client, + project["id"], + guide["id"], + compile_pre_submit_checker=False, + ) + + response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/pre-submit-checker-policy", + headers=auth_headers(), + ) + + assert response.status_code == 404 + + +async def test_project_setup_run_records_enqueue_failure_without_leaking_error( + project_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART", "true") + get_settings.cache_clear() + + def fail_enqueue(**_: object) -> str: + raise ProjectSetupQueueError( + "broker rejected https://storage.flow.test/signed?token=secret" + ) + + monkeypatch.setattr(project_service_module, "enqueue_pre_submit_setup_pipeline", fail_enqueue) + project = await create_project(project_client) + guide = await create_guide( + project_client, + project["id"], + { + **complete_guide_payload(), + "source_snapshot": source_snapshot_payload(), + }, + ) + + response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/setup-runs/latest", + headers=auth_headers(), + ) + + assert response.status_code == 200, response.text + body = response.json() + assert body["status"] == "enqueue_failed" + assert body["current_step"] == "enqueue" + assert body["celery_task_id"] is None + assert body["error_code"] == "ProjectSetupQueueError" + assert body["error_summary"] == ( + "project setup failed; inspect server logs with the setup run id" + ) + assert "token" not in body["error_summary"] + assert "https://" not in body["error_summary"] + + +async def test_project_setup_run_rejects_cross_context_worker_updates( + project_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, + deterministic_project_agent_runtime: None, +) -> None: + monkeypatch.setenv("WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART", "true") + monkeypatch.setenv("WORKSTREAM_CELERY_TASK_ALWAYS_EAGER", "true") + get_settings.cache_clear() + + first_project = await create_project(project_client) + first_guide = await create_guide( + project_client, + first_project["id"], + { + **complete_guide_payload(), + "source_snapshot": source_snapshot_payload(), + }, + ) + first_setup_response = await project_client.get( + f"/api/v1/projects/{first_project['id']}/guides/{first_guide['id']}/setup-runs/latest", + headers=auth_headers(), + ) + assert first_setup_response.status_code == 200, first_setup_response.text + first_setup_run = first_setup_response.json() + + second_project_response = await project_client.post( + "/api/v1/projects", + headers=auth_headers(), + json={ + "name": "STEM Eval Two", + "slug": "stem-eval-two", + "description": "Second internal STEM evaluation project", + }, + ) + assert second_project_response.status_code == 201, second_project_response.text + second_project = second_project_response.json() + second_guide = await create_guide( + project_client, + second_project["id"], + { + **complete_guide_payload(version="v1"), + "source_snapshot": { + **source_snapshot_payload(), + "items": [ + { + **source_snapshot_payload()["items"][0], + "durable_ref": "inline:/guides/second/v1", + "content_hash": "sha256:" + + hashlib.sha256(b"second-guide").hexdigest(), + } + ], + }, + }, + ) + second_setup_response = await project_client.get( + f"/api/v1/projects/{second_project['id']}/guides/{second_guide['id']}/setup-runs/latest", + headers=auth_headers(), + ) + assert second_setup_response.status_code == 200, second_setup_response.text + second_setup_run = second_setup_response.json() + + async with db_session.get_session_factory()() as session: + service = ProjectService(session) + with pytest.raises(project_service_module.PolicySetupConflict): + await service.validate_project_setup_run_context( + first_setup_run["id"], + project_id=second_project["id"], + guide_id=second_guide["id"], + source_snapshot_id=second_setup_run["source_snapshot_id"], + ) + with pytest.raises(project_service_module.PolicySetupConflict): + await service.update_project_setup_run_status( + first_setup_run["id"], + status="policy_draft_ready", + current_step="submission_artifact_policy_derivation", + output_sufficiency_report_id=second_setup_run["output_sufficiency_report_id"], + ) + with pytest.raises(project_service_module.PolicySetupConflict): + await service.update_project_setup_run_status( + first_setup_run["id"], + status="policy_draft_ready", + current_step="submission_artifact_policy_derivation", + output_submission_artifact_policy_id=second_setup_run[ + "output_submission_artifact_policy_id" + ], + ) + + +async def test_project_setup_visibility_apis_require_project_setup_role( + project_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, + deterministic_project_agent_runtime: None, +) -> None: + monkeypatch.setenv("WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART", "true") + monkeypatch.setenv("WORKSTREAM_CELERY_TASK_ALWAYS_EAGER", "true") + get_settings.cache_clear() + project = await create_project(project_client) + guide = await create_guide( + project_client, + project["id"], + { + **complete_guide_payload(), + "source_snapshot": source_snapshot_payload(), + }, + ) + setup_run_response = await project_client.get( + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/setup-runs/latest", + headers=auth_headers(), + ) + assert setup_run_response.status_code == 200, setup_run_response.text + setup_run = setup_run_response.json() + + endpoints = [ + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/setup-runs/latest", + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/sufficiency-reports", + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/sufficiency-reports/" + f"{setup_run['output_sufficiency_report_id']}", + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/submission-artifact-policies", + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/submission-artifact-policies/" + f"{setup_run['output_submission_artifact_policy_id']}", + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/" + "effective-submission-artifact-policy", + f"/api/v1/projects/{project['id']}/guides/{guide['id']}/pre-submit-checker-policy", + ] + monkeypatch.setenv("WORKSTREAM_DEV_AUTH_ROLES", "admin") + get_settings.cache_clear() + admin_responses = [ + await project_client.get(endpoint, headers=auth_headers()) for endpoint in endpoints + ] + assert [response.status_code for response in admin_responses] == [ + 200, + 200, + 200, + 200, + 200, + 404, + 404, + ] + + for role in ("worker", "reviewer", "finance", "auditor"): + monkeypatch.setenv("WORKSTREAM_DEV_AUTH_ROLES", role) + get_settings.cache_clear() + responses = [ + await project_client.get(endpoint, headers=auth_headers()) for endpoint in endpoints + ] + + assert [response.status_code for response in responses] == [403] * len(endpoints) + + async def test_project_can_be_created(project_client: AsyncClient) -> None: project = await create_project(project_client) diff --git a/docs/architecture_data_model.md b/docs/architecture_data_model.md index bee34ff..ac4351e 100644 --- a/docs/architecture_data_model.md +++ b/docs/architecture_data_model.md @@ -14,6 +14,7 @@ Project ProjectGuide GuideSourceSnapshot GuideSourceSnapshotItem + ProjectSetupRun GuideSufficiencyReport SubmissionArtifactPolicy EffectiveProjectSubmissionArtifactPolicy @@ -217,8 +218,8 @@ Every task records the guide version active at creation or screening time before When a task is claimed or moved to `IN_PROGRESS`, its locked guide and policy context does not change silently. A newer upstream guide version can only affect unclaimed work or a controlled revision path when policy allows it and the audit log records the reason. -Material changes require a new guide version or policy version. Material changes -include guide source material, submission artifact policy, pre-submit checker +Material changes require a new guide version or policy version. They include +guide source material, submission artifact policy, pre-submit checker generation rules, post-submit checker policy, review policy, revision policy, and payment policy. @@ -308,6 +309,53 @@ A new guide-source snapshot invalidates prior setup records for new activation and unlocked tasks only. Tasks already locked to an earlier snapshot retain that policy context unless an explicit audited rebase occurs. +## ProjectSetupRun + +Fields: + +- `id` +- `project_id` +- `guide_id` +- `guide_version` +- `source_snapshot_id` +- `source_snapshot_hash` +- `celery_task_id` +- `status` +- `current_step` +- `output_sufficiency_report_id` +- `output_submission_artifact_policy_id` +- `error_code` +- `error_summary` +- `created_by` +- `created_at` +- `updated_at` +- `started_at` +- `finished_at` + +`ProjectSetupRun` is a non-authoritative orchestration ledger for automatic +project setup. It records that Workstream queued or attempted the guide +sufficiency and submission artifact policy derivation pipeline for one guide +source snapshot. It does not replace the source snapshot, sufficiency report, +submission artifact policy, effective project policy, or pre-submit checker +policy rows. + +Statuses: + +- `queued` +- `enqueue_failed` +- `running_sufficiency_agent` +- `sufficiency_blocked` +- `running_policy_derivation_agent` +- `policy_draft_ready` +- `setup_blocked` +- `failed` + +The run references downstream truth by id/hash. Operators can read the latest +run through the project setup API to understand whether setup is still queued, +blocked by guide sufficiency, waiting on policy approval, or failed at the +queue/worker layer. Error summaries are bounded and redacted; server logs remain +the source for sensitive diagnostics. + ## GuideSufficiencyReport Fields: diff --git a/docs/architecture_system_architecture.md b/docs/architecture_system_architecture.md index d7bc26e..35653a2 100644 --- a/docs/architecture_system_architecture.md +++ b/docs/architecture_system_architecture.md @@ -117,6 +117,7 @@ Owns: - project metadata - guide +- project setup run ledger - submission artifact policy - generated project pre-submit checker policy - post-submit checker policy @@ -125,6 +126,12 @@ Owns: - payment policy - skill taxonomy +Project setup visibility APIs expose the latest setup run, sufficiency reports, +submission artifact policies, the current effective project policy, and the +compiled project pre-submit checker policy summary to `admin` and +`project_manager` actors. These reads replace operator DB inspection for setup +drills, but they do not make `ProjectSetupRun` a policy source of truth. + ### Source/Import Service Owns: diff --git a/docs/glossary.md b/docs/glossary.md index 069562f..1578d24 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -75,6 +75,15 @@ whether the guide passed, is blocked by gaps, or passed with warnings that an clear only the manual policy path; agent derivation requires an agent-created sufficiency report for the same snapshot. +## Project Setup Run + +A non-authoritative orchestration ledger for automatic project setup. It records +queue status, current setup step, Celery task id, bounded errors, and output +record ids for guide sufficiency and submission artifact policy derivation. The +actual policy truth remains in the source snapshot, sufficiency report, +submission artifact policy, effective project policy, and pre-submit checker +policy rows. + ## Submission Artifact Policy The Workstream-derived, admin-or-project-manager-approved machine-readable diff --git a/docs/operations_project_operating_manual.md b/docs/operations_project_operating_manual.md index 96a1be7..e817644 100644 --- a/docs/operations_project_operating_manual.md +++ b/docs/operations_project_operating_manual.md @@ -29,6 +29,7 @@ Before releasing tasks: - project guide imported - guide source snapshot captured - project owner setup material captured +- latest project setup run visible to `admin` or `project_manager` - payment policy base amount configured - payment policy currency configured - allowed task types listed @@ -52,6 +53,21 @@ still draft, an `admin` or `project_manager` may attach or update checker, review, revision, and payment policy records after snapshot capture because those records are activated as separate policy context. +Project setup operators inspect setup state through API reads, not direct +database queries. These endpoints require `admin` or `project_manager` access: + +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/setup-runs/latest` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports/{report_id}` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies/{policy_id}` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/effective-submission-artifact-policy` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/pre-submit-checker-policy` + +`ProjectSetupRun` is only a setup ledger. Policy truth remains in the guide +source snapshot, sufficiency report, submission artifact policy, effective +project policy, and compiled project pre-submit checker policy rows. + ## v0.1 Quality Gates ### Project Activation Gate diff --git a/docs/roadmap_status.md b/docs/roadmap_status.md index 8ad1601..62783e0 100644 --- a/docs/roadmap_status.md +++ b/docs/roadmap_status.md @@ -43,6 +43,7 @@ Current phase: Week 3 review and revision preparation. - Chunk 9 automatic pre-review gate with pre-submit intake blocking, internal `task_setup_blocked`, trusted checker retry, and worker redaction. - Chunk 10 checker trial with the expanded real API sample matrix, failure catalog, false-positive notes, missing-checker notes, and internal verifier evidence. - Week 2 real HTTP API drill through Flow-token auth, project/guide/task/submission lifecycle, pre-submit checks, automatic checker runs, `pre_submission_checker_failed` intake failures, worker redaction, internal `task_setup_blocked`, and trusted checker retry. +- Chunk 11 actor identity/profile registry for verified Flow actors. ## Review Tracks Closed @@ -62,6 +63,11 @@ Current phase: Week 3 review and revision preparation. - Week 3 must keep review decisions canonical: `accept`, `needs_revision`, and `reject`. - `needs_revision` from human review must carry `outcome_source = human_review` and a review decision id; checker-caused `needs_revision` keeps `outcome_source = auto_checker`. - Review findings, revision replay, and reviewer-quality metrics are the next backend contracts to lock. +- Chunk 12 project setup-run and project policy visibility APIs are in implementation. +- Before the next Terminal Benchmark drill, operators need HTTP visibility for + setup runs, sufficiency reports, submission artifact policies, effective + policy, and compiled project pre-submit checker policy instead of direct DB + inspection. ## Pending Before Pilot @@ -78,7 +84,7 @@ Run from the backend directory against local Postgres: WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py ``` -The script runs migrations forward and exercises: +The script runs migrations forward and exercises project setup visibility plus: `Project -> Guide -> Task -> Screening -> Ready -> Claim -> Start -> Submit -> Lock submission` From 85b08a1cd7a07a53dc6c4d1d402e06ad26dfc5e0 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Tue, 7 Jul 2026 18:12:26 +0100 Subject: [PATCH 2/7] Add WS-POL-001-12 review evidence --- .../WS-POL-001-12-internal-review-evidence.md | 89 +++++++++++++++++++ .../reviews/WS-POL-001-12-pr-trust-bundle.md | 83 +++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md new file mode 100644 index 0000000..5439531 --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md @@ -0,0 +1,89 @@ +# Internal Review Evidence: WS-POL-001-12 + +## Chunk + +WS-POL-001-12 + +open sub-agent sessions: none + +valid findings addressed: yes + +## Reviewed Revision + +Reviewed code SHA: c143b54b62d6325a1d4787116e692fedfc5716f2 + +Reviewed at: 2026-07-07T17:11:38Z + +Reviewer run IDs: senior-engineering-review-019f3d21-2c35-7923-ad77-4e3aeefc653e, senior-engineering-rerun-019f3d2f-7f7d-7ec1-96c9-72ce50010243, qa-test-review-019f3d21-36bb-7881-8b01-74405a83a9af, security-auth-review-019f3d21-3c05-7fe3-8c98-a88e15ee82ac, security-auth-rerun-019f3d2f-6ae2-7601-bf97-3e43070480c3, product-ops-review-019f3d21-46f2-7330-ab33-2e31a174a8c2, product-ops-rerun-019f3d2f-8d32-7232-ba81-d7b721bce0a5, architecture-review-019f3d21-502c-7331-adc0-8c50c43fad2c, architecture-rerun-019f3d2f-78ac-72d1-ba9b-146c4dc04ec6, docs-review-019f3d21-5ad7-7f20-9f77-0cc747ded03d, reuse-dedup-review-019f3d2f-a189-70f2-ae58-836448d74835, test-delta-review-019f3d2f-b8f5-7a00-88c8-ac5e54158b13, test-delta-rerun-019f3d3a-da3a-7372-9082-b41fa77b5236, test-delta-final-019f3d69-1863-7923-809d-dbb327c19952 + +After the reviewed SHA, only evidence and status files changed. + +## Reviewed Change + +Scope: + +- Adds `ProjectSetupRun` as a non-authoritative ledger for automatic project setup execution. +- Persists setup runs before enqueue, records Celery task ids, and records `enqueue_failed` when enqueue fails. +- Updates the project setup worker to validate setup-run context, advance setup-run status, and validate output ids before recording them. +- Adds seven operator-only project setup and policy visibility APIs for setup run, sufficiency report, submission artifact policy, effective project submission artifact policy, and compiled project pre-submit checker policy state. +- Keeps policy truth in guide source snapshot, sufficiency report, submission artifact policy, effective policy, and pre-submit checker policy records; setup-run output ids are only pointers. +- Hides raw compiled checker bundle and checker configs from the new pre-submit checker visibility endpoint while preserving active-guide response behavior. +- Fails closed for public setup-run error summaries so operator APIs and worker task results do not expose tokens, private object keys, local paths, signed URLs, or raw stack traces. +- Updates docs and initiative planning for APIs 1-7 and records WS-POL-001-13/14 as later chunks. + +## Reviewer Results + +| Reviewer | Result | Blocking findings | Notes | +|---|---:|---|---| +| senior engineering | PASS WITH LOW RISKS | None | Fixed fail-closed redaction, output scope validation, active-guide schema boundary, and removed dead queue preflight. | +| QA/test | PASS WITH LOW RISKS | None | Added latest-run ordering by source snapshot and denied-role coverage beyond workers. | +| security/auth | PASS | None | Fixed public setup-run error summaries and worker logs/results to avoid raw exception leakage. | +| product/ops | PASS WITH LOW RISKS | None | Accepted that full setup-run proof in real API drill belongs to WS-POL-001-14 while integration tests cover this chunk. | +| architecture | PASS | None | Split active-guide checker schema, removed unscoped effective-policy helper, and wrapped latest snapshot ambiguity. | +| docs | PASS | None | Confirmed exact seven API paths are documented and ProjectSetupRun is consistently described as a ledger. | +| reuse/dedup | PASS WITH LOW RISKS | None | Addressed schema duplication by subclassing the active-guide checker schema and removed dead redaction regexes. | +| test delta | PASS WITH LOW RISKS | None | Added cross-project, same-project/different-guide, admin allow, and denied-role visibility coverage. | + +## Valid Findings Addressed + +- Changed setup-run public error handling to fail closed for every non-empty summary. +- Updated enqueue failure and worker error handling to store/log/return sanitized public summaries only. +- Added worker setup-run context validation before running guide sufficiency or policy derivation. +- Added setup-run output validation so sufficiency report and submission artifact policy ids must match project, guide, guide version, source snapshot id, and source snapshot hash. +- Split the active-guide checker policy response from the new redacted visibility response so APIs 1-7 do not silently alter the existing active-guide contract. +- Removed the unused queue readiness probe after enqueue failure became ledger state rather than a pre-persist blocker. +- Removed the unscoped current effective policy repository helper. +- Ordered latest setup-run lookup by source snapshot capture time before setup-run creation time. +- Broadened authorization tests to prove `admin` and `project_manager` can read these endpoints while `worker`, `reviewer`, `finance`, and `auditor` cannot. +- Added cross-project and same-project/different-guide scoping checks for setup-run latest, list endpoints, item GET endpoints, effective policy, and pre-submit checker policy reads. +- Preserved active-guide `checker_configs` while ensuring the new pre-submit visibility summary omits raw checker authority. + +## Commands Run + +```bash +cd backend && .venv/bin/python -m ruff check app tests scripts +python3 scripts/check_stale_workstream_wording.py +python3 scripts/check_markdown_links.py +rg -n "DB inspection|direct DB|setup-runs/latest|ProjectSetupRun" docs backend examples .agent-loop +cd backend && .venv/bin/pytest tests/test_projects.py::test_project_setup_error_summary_redacts_sensitive_diagnostics tests/test_projects.py::test_project_setup_visibility_apis_show_automatic_setup_outputs tests/test_projects.py::test_project_setup_run_rejects_cross_context_worker_updates tests/test_projects.py::test_project_setup_run_records_enqueue_failure_without_leaking_error tests/test_projects.py::test_project_setup_visibility_apis_require_project_setup_role tests/test_projects.py::test_guide_activation_and_active_guide_retrieval tests/test_projects.py::test_pre_submit_visibility_requires_compiled_policy -vv +cd backend && .venv/bin/pytest tests/test_projects.py -q +cd backend && .venv/bin/pytest tests/test_projects.py::test_project_setup_visibility_apis_show_automatic_setup_outputs tests/test_projects.py::test_project_setup_visibility_apis_require_project_setup_role -q +cd backend && WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py +``` + +Results: + +- Ruff: passed. +- Stale wording scan: passed. +- Markdown link check: passed for 15 changed Markdown files. +- Scope/stale proof scan: only expected setup-run docs and future DB-inspection-removal chunk references remained. +- Targeted setup/project tests: 7 passed in 186.41s on final targeted run. +- Full project test suite: 206 passed in 2269.22s before final same-project/different-guide list-scope hardening; the post-hardening full-suite process exited cleanly but its interrupted terminal summary was not recoverable. +- Final post-hardening targeted visibility tests: 2 passed in 38.57s. +- API contract real API E2E: passed after final implementation changes. + +## Remaining Risks + +- `api_contract_e2e.py` still uses manual setup records for deterministic local execution and does not call `setup-runs/latest`; full no-DB Terminal Benchmark drill proof is assigned to WS-POL-001-14. +- Same-project/different-guide list scoping was hardened in targeted tests after test-delta review. Full project suite was interrupted during output capture, so final proof combines prior full-suite pass plus final targeted visibility pass. +- Setup-run statuses are repeated across migration/model/service/docs. A future cleanup can centralize runtime constants while keeping Alembic literals frozen. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md new file mode 100644 index 0000000..f8ddeef --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md @@ -0,0 +1,83 @@ +# PR Trust Bundle: WS-POL-001-12 + +## Intent + +Expose project setup and project policy state through authorized HTTP APIs so a real operator drill does not need to inspect Postgres to find setup outputs. + +## Scope + +Implemented APIs 1-7 only: + +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/setup-runs/latest` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/sufficiency-reports/{report_id}` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/submission-artifact-policies/{policy_id}` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/effective-submission-artifact-policy` +- `GET /api/v1/projects/{project_id}/guides/{guide_id}/pre-submit-checker-policy` + +## Design + +`ProjectSetupRun` is a non-authoritative orchestration ledger. It records setup execution, queue status, worker status, Celery task id, safe error summary, and output ids. It does not replace guide source snapshots, sufficiency reports, submission artifact policies, effective project policies, or compiled pre-submit checker policies as truth. + +The worker validates that its task payload matches the setup-run row and validates output ids before writing them to the ledger. + +The pre-submit checker visibility API returns only summary metadata and the compiled bundle hash. It does not expose raw `compiled_bundle` or `checker_configs`. Active-guide response behavior is preserved through a separate schema. + +## Authorization + +All seven endpoints require verified token auth and project setup operator access: + +- allowed: `admin`, `project_manager` +- denied in v0.1: `worker`, `reviewer`, `finance`, `auditor` + +## Evidence + +Commands passed: + +```bash +cd backend && .venv/bin/python -m ruff check app tests scripts +python3 scripts/check_stale_workstream_wording.py +python3 scripts/check_markdown_links.py +cd backend && .venv/bin/pytest tests/test_projects.py::test_project_setup_error_summary_redacts_sensitive_diagnostics tests/test_projects.py::test_project_setup_visibility_apis_show_automatic_setup_outputs tests/test_projects.py::test_project_setup_run_rejects_cross_context_worker_updates tests/test_projects.py::test_project_setup_run_records_enqueue_failure_without_leaking_error tests/test_projects.py::test_project_setup_visibility_apis_require_project_setup_role tests/test_projects.py::test_guide_activation_and_active_guide_retrieval tests/test_projects.py::test_pre_submit_visibility_requires_compiled_policy -vv +cd backend && .venv/bin/pytest tests/test_projects.py -q +cd backend && .venv/bin/pytest tests/test_projects.py::test_project_setup_visibility_apis_show_automatic_setup_outputs tests/test_projects.py::test_project_setup_visibility_apis_require_project_setup_role -q +cd backend && WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py +``` + +Key results: + +- Targeted setup/project tests: 7 passed on final targeted run. +- Full project suite: 206 passed before the last optional test hardening. +- Final post-hardening visibility tests: 2 passed. +- API contract real API E2E: passed. + +## Internal Review + +- Senior engineering: PASS WITH LOW RISKS +- QA/test: PASS WITH LOW RISKS +- Security/auth: PASS +- Product/ops: PASS WITH LOW RISKS +- Architecture: PASS +- Docs: PASS +- Reuse/dedup: PASS WITH LOW RISKS +- Test delta: PASS WITH LOW RISKS + +All sub-agent sessions were closed. + +## External Review + +External review not yet run for this PR state. + +## Human Review Focus + +- Confirm setup-run status names and terminal states are understandable. +- Confirm `ProjectSetupRun` remains a ledger and not a policy source of truth. +- Confirm operator access is limited to `admin` and `project_manager`. +- Confirm pre-submit checker visibility should hide `checker_configs` while active-guide keeps its existing policy context shape. + +## Remaining Risks + +- Full no-DB Terminal Benchmark drill proof remains assigned to WS-POL-001-14. +- `api_contract_e2e.py` uses manual setup records because there is no app-level deterministic project-agent runtime outside pytest fixtures. +- Setup-run status constants can be centralized later; Alembic migration literals should remain frozen. From 3feb43729e3240a5792fe9b6f15dd4da37ed63ff Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Tue, 7 Jul 2026 19:15:46 +0100 Subject: [PATCH 3/7] Address setup visibility review fixes --- .agent-loop/LOOP_STATE.md | 13 +-- .../STATUS.md | 17 +-- backend/app/workers/project_setup.py | 9 +- backend/tests/test_projects.py | 102 ++++++++++++++++++ docs/roadmap_status.md | 4 +- 5 files changed, 127 insertions(+), 18 deletions(-) diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index e42a5e6..3be91ac 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -6,14 +6,15 @@ - Active planning chunk: none - Active implementation chunk: `WS-POL-001-12` - Branch: `codex/ws-pol-001-12-project-setup-policy-visibility` -- Status: `WS-POL-001-11` merged through PR #74 on 2026-07-07. - The shared actor identity/profile registry is now on `main`; GitHub Agent - Gates, Backend, and CodeRabbit passed before merge. The post-merge Terminal - Benchmark live API drill passed, but exposed missing visibility APIs that - forced direct DB inspection. +- Status: `WS-POL-001-12` is implemented in PR #76 on branch + `codex/ws-pol-001-12-project-setup-policy-visibility`. Internal review is + complete, CodeRabbit feedback is being addressed in a follow-up commit, and + the chunk is waiting for human merge review after external-review fixes are + verified. - Last merged implementation SHA: `0729531` - Last merge commit: `5cec0e0` -- Current gate: implement, verify, and review `WS-POL-001-12` before opening PR. +- Current gate: verify CodeRabbit fixes on PR #76, update trust evidence, then + wait for explicit human merge approval. - Next chunk: inactive until `WS-POL-001-12` receives human review and merge. ## Operating Rule 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 6b890ba..2488a36 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 @@ -7,14 +7,17 @@ `WS-POL-001-09`, `WS-POL-001-10`, and `WS-POL-001-11` are merged to `main`. The post-actor-registry Terminal Benchmark live API drill passed through real HTTP calls, but it required DB inspection for project setup outputs and locked -context verification. The next work is split into three visibility/finalize -chunks before rerunning the drill without DB inspection. +context verification. `WS-POL-001-12` is implemented in PR #76 to expose the +first project setup and project policy visibility APIs. `WS-POL-001-13` and +`WS-POL-001-14` remain the next visibility/finalize chunks before rerunning the +drill without DB inspection. ## Active Chunk -`WS-POL-001-12` is in implementation on branch -`codex/ws-pol-001-12-project-setup-policy-visibility`. `WS-POL-001-13` and -`WS-POL-001-14` remain inactive until `WS-POL-001-12` is reviewed and merged. +`WS-POL-001-12` is implemented on branch +`codex/ws-pol-001-12-project-setup-policy-visibility` in PR #76. Internal +review is complete, CodeRabbit feedback is being addressed, and the chunk is +not merged until the user explicitly approves PR #76. ## Chunk Status @@ -31,7 +34,7 @@ chunks before rerunning the drill without DB inspection. | `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` | Merged | `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. | | `WS-POL-001-11` | Merged | `codex/ws-pol-001-11-actor-profile-registry-impl` | 74 | Implements local Workstream actor identity and actor profile registries for verified Flow actors before the next live API drill. | -| `WS-POL-001-12` | In implementation | `codex/ws-pol-001-12-project-setup-policy-visibility` | - | Add project setup-run and project policy visibility APIs for setup runs, sufficiency reports, submission artifact policies, effective policy, and compiled project pre-submit checker policy. | +| `WS-POL-001-12` | PR open | `codex/ws-pol-001-12-project-setup-policy-visibility` | 76 | Adds project setup-run and project policy visibility APIs for setup runs, sufficiency reports, submission artifact policies, effective policy, and compiled project pre-submit checker policy; awaiting human merge review after external feedback fixes. | | `WS-POL-001-13` | Proposed | - | - | Add task work-context, worker submission-requirements, and operator locked-context APIs. | | `WS-POL-001-14` | Proposed | - | - | Replace public submission lock with finalize, define system actor audit semantics, and rerun the Terminal Benchmark proof without DB inspection. | @@ -39,7 +42,7 @@ chunks before rerunning the drill without DB inspection. | Blocker | Owner | Next action | |---|---|---| -| Missing project setup visibility APIs | Workstream | Complete `WS-POL-001-12` implementation evidence and PR review. | +| PR #76 human merge review | Human reviewer | Review and explicitly approve or request changes for `WS-POL-001-12`. | | Missing task context visibility APIs | Workstream | Implement `WS-POL-001-13`. | | Public lock wording and no-DB drill proof | Workstream | Implement `WS-POL-001-14` before rerunning Terminal Benchmark as accepted proof. | diff --git a/backend/app/workers/project_setup.py b/backend/app/workers/project_setup.py index 10af0e0..d8ad76e 100644 --- a/backend/app/workers/project_setup.py +++ b/backend/app/workers/project_setup.py @@ -179,13 +179,16 @@ async def _run_pre_submit_setup_pipeline( "submission_artifact_policy_id": None, } except Exception as exc: - logger.exception( + public_error = "unexpected project setup pipeline failure" + logger.error( "project setup pipeline failed", extra={ "project_id": project_id, "guide_id": guide_id, "source_snapshot_id": source_snapshot_id, "setup_run_id": setup_run_id, + "error_code": exc.__class__.__name__, + "error_summary": public_error, }, ) await service.update_project_setup_run_status( @@ -193,11 +196,11 @@ async def _run_pre_submit_setup_pipeline( status="failed", current_step="project_setup", error_code=exc.__class__.__name__, - error_summary="unexpected project setup pipeline failure", + error_summary=public_error, ) return { "status": "failed", - "error": "unexpected project setup pipeline failure", + "error": public_error, "guide_sufficiency_report_id": None, "submission_artifact_policy_id": None, } diff --git a/backend/tests/test_projects.py b/backend/tests/test_projects.py index 9501808..df17acc 100644 --- a/backend/tests/test_projects.py +++ b/backend/tests/test_projects.py @@ -4,6 +4,7 @@ import hashlib import inspect import json +import logging import sys import types from collections.abc import AsyncIterator, Iterator @@ -574,9 +575,20 @@ def capture_enqueue( select(SubmissionArtifactPolicy).where(SubmissionArtifactPolicy.guide_id == guide["id"]) ) ).all() + setup_runs = ( + await session.scalars( + select(ProjectSetupRun).where( + ProjectSetupRun.guide_id == guide["id"], + ProjectSetupRun.source_snapshot_id == snapshots[0].id, + ) + ) + ).all() assert len(snapshots) == 1 assert enqueued[0]["source_snapshot_id"] == snapshots[0].id + assert len(setup_runs) == 1 + assert enqueued[0]["setup_run_id"] == setup_runs[0].id + assert setup_runs[0].celery_task_id == "captured-task-id" assert reports == [] assert policies == [] @@ -757,6 +769,8 @@ def capture_enqueue( snapshot = await create_source_snapshot(project_client, project["id"], guide["id"]) + assert len(enqueued) == 1 + assert enqueued[0]["setup_run_id"] assert enqueued == [ { "project_id": project["id"], @@ -765,6 +779,19 @@ def capture_enqueue( "setup_run_id": enqueued[0]["setup_run_id"], } ] + async with db_session.get_session_factory()() as session: + setup_runs = ( + await session.scalars( + select(ProjectSetupRun).where( + ProjectSetupRun.guide_id == guide["id"], + ProjectSetupRun.source_snapshot_id == snapshot["id"], + ) + ) + ).all() + + assert len(setup_runs) == 1 + assert enqueued[0]["setup_run_id"] == setup_runs[0].id + assert setup_runs[0].celery_task_id == "captured-task-id" async def test_create_source_snapshot_returns_created_when_post_commit_enqueue_fails( @@ -1452,6 +1479,81 @@ def fail_enqueue(**_: object) -> str: assert "https://" not in body["error_summary"] +async def test_project_setup_worker_unexpected_error_does_not_leak_raw_exception( + project_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Unexpected worker failures keep secrets out of logs, results, and setup runs.""" + from app.workers import project_setup as project_setup_worker_module + + project = await create_project(project_client) + guide = await create_guide( + project_client, + project["id"], + { + **complete_guide_payload(), + "source_snapshot": source_snapshot_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 + setup_run = ProjectSetupRun( + id=str(uuid4()), + project_id=project["id"], + guide_id=guide["id"], + guide_version=guide["version"], + source_snapshot_id=snapshot.id, + source_snapshot_hash=snapshot.bundle_hash, + status="queued", + current_step="queued", + created_by="test-project-manager", + ) + session.add(setup_run) + await session.commit() + setup_run_id = setup_run.id + snapshot_id = snapshot.id + + async def raise_raw_secret_error(*_: object, **__: object) -> object: + raise RuntimeError("raw-token=secret at /srv/private/guide.md") + + monkeypatch.setattr( + project_setup_worker_module.ProjectService, + "run_guide_sufficiency_agent", + raise_raw_secret_error, + ) + caplog.set_level(logging.ERROR, logger=project_setup_worker_module.logger.name) + + result = await project_setup_worker_module._run_pre_submit_setup_pipeline( + project["id"], + guide["id"], + snapshot_id, + setup_run_id, + ) + + async with db_session.get_session_factory()() as session: + persisted = await session.get(ProjectSetupRun, setup_run_id) + + assert result == { + "status": "failed", + "error": "unexpected project setup pipeline failure", + "guide_sufficiency_report_id": None, + "submission_artifact_policy_id": None, + } + assert persisted is not None + assert persisted.status == "failed" + assert persisted.error_code == "RuntimeError" + assert persisted.error_summary == ( + "project setup failed; inspect server logs with the setup run id" + ) + assert "raw-token" not in caplog.text + assert "secret" not in caplog.text + assert "/srv/private" not in caplog.text + + async def test_project_setup_run_rejects_cross_context_worker_updates( project_client: AsyncClient, monkeypatch: pytest.MonkeyPatch, diff --git a/docs/roadmap_status.md b/docs/roadmap_status.md index 62783e0..67ddff2 100644 --- a/docs/roadmap_status.md +++ b/docs/roadmap_status.md @@ -63,7 +63,7 @@ Current phase: Week 3 review and revision preparation. - Week 3 must keep review decisions canonical: `accept`, `needs_revision`, and `reject`. - `needs_revision` from human review must carry `outcome_source = human_review` and a review decision id; checker-caused `needs_revision` keeps `outcome_source = auto_checker`. - Review findings, revision replay, and reviewer-quality metrics are the next backend contracts to lock. -- Chunk 12 project setup-run and project policy visibility APIs are in implementation. +- Chunk 12 project setup-run and project policy visibility APIs are implemented in open PR #76 and under human review. - Before the next Terminal Benchmark drill, operators need HTTP visibility for setup runs, sufficiency reports, submission artifact policies, effective policy, and compiled project pre-submit checker policy instead of direct DB @@ -84,7 +84,7 @@ Run from the backend directory against local Postgres: WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py ``` -The script runs migrations forward and exercises project setup visibility plus: +The script runs migrations forward and exercises project policy visibility plus the following flow: `Project -> Guide -> Task -> Screening -> Ready -> Claim -> Start -> Submit -> Lock submission` From 965cbddd607e9ac7ef9b070e85bdeccd9cbefa48 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Tue, 7 Jul 2026 19:25:40 +0100 Subject: [PATCH 4/7] Strengthen setup worker redaction regression --- backend/tests/test_projects.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/tests/test_projects.py b/backend/tests/test_projects.py index df17acc..5f2fe4b 100644 --- a/backend/tests/test_projects.py +++ b/backend/tests/test_projects.py @@ -1549,6 +1549,15 @@ async def raise_raw_secret_error(*_: object, **__: object) -> object: assert persisted.error_summary == ( "project setup failed; inspect server logs with the setup run id" ) + error_records = [ + record + for record in caplog.records + if record.levelno == logging.ERROR and record.message == "project setup pipeline failed" + ] + assert len(error_records) == 1 + assert error_records[0].setup_run_id == setup_run_id + assert error_records[0].error_code == "RuntimeError" + assert error_records[0].error_summary == "unexpected project setup pipeline failure" assert "raw-token" not in caplog.text assert "secret" not in caplog.text assert "/srv/private" not in caplog.text From 038223fbc2d9999c24d2b95cfd58a236e289d7e1 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Tue, 7 Jul 2026 19:28:55 +0100 Subject: [PATCH 5/7] Update setup visibility review evidence --- .../WS-POL-001-12-external-review-response.md | 54 +++++++++++++++++++ .../WS-POL-001-12-internal-review-evidence.md | 32 +++++++---- .../reviews/WS-POL-001-12-pr-trust-bundle.md | 15 +++++- 3 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-external-review-response.md diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-external-review-response.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-external-review-response.md new file mode 100644 index 0000000..356804b --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-external-review-response.md @@ -0,0 +1,54 @@ +# External Review Response: WS-POL-001-12 + +## Comments Addressed + +- CodeRabbit: `backend/tests/test_projects.py` had a tautological + `setup_run_id` assertion. Added explicit length/truthiness assertions and + DB-backed checks that captured enqueue payloads match the persisted + `ProjectSetupRun.id` and recorded Celery task id. +- CodeRabbit: `docs/roadmap_status.md` still described Chunk 12 as in + implementation. Updated it to say the chunk is implemented in open PR #76 + and under human review. +- CodeRabbit: `docs/roadmap_status.md` used awkward wording around the API + contract drill. Reworded the sentence for clarity. +- Internal security review: the generic setup worker exception path used + `logger.exception`, which could leak raw exception text and tracebacks. + Replaced it with sanitized structured logging and added regression coverage + for task result, persisted setup-run state, and captured logs. +- Internal docs/product review: trust evidence and status artifacts were stale + after external-review fixes. Rebound internal evidence to reviewed code SHA + `965cbddd607e9ac7ef9b070e85bdeccd9cbefa48` and updated this response plus + the PR trust bundle. + +## Comments Deferred + +None. + +## Human Decisions Needed + +None for these comments. Human merge review is still required for PR #76. + +## Commands Rerun + +- `cd backend && .venv/bin/pytest tests/test_projects.py::test_create_guide_autostart_enqueues_without_inline_agent_execution tests/test_projects.py::test_create_source_snapshot_autostart_enqueues_latest_snapshot tests/test_projects.py::test_project_setup_worker_unexpected_error_does_not_leak_raw_exception -q` + - Result: 3 passed in 29.13s on the final local run. +- `cd backend && .venv/bin/python -m ruff check app/workers/project_setup.py tests/test_projects.py` + - Result: passed. +- `python3 scripts/check_stale_workstream_wording.py` + - Result: passed. +- `python3 scripts/check_markdown_links.py` + - Result: passed for 18 changed Markdown files. +- Internal reviewer reruns after external-review fixes: + - senior engineering: PASS WITH LOW RISKS + - QA/test: PASS WITH LOW RISKS + - security/auth: PASS + - product/ops: PASS WITH LOW RISKS after evidence artifacts are committed + - architecture: PASS + - docs: PASS after evidence artifacts are committed + - reuse/dedup: PASS WITH LOW RISKS + - test delta: PASS WITH LOW RISKS + +## Remaining Risks + +- `WS-POL-001-13` and `WS-POL-001-14` still own the remaining visibility and + finalize APIs before the Terminal Benchmark no-DB drill can be repeated. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md index 5439531..29dee3a 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md @@ -10,13 +10,13 @@ valid findings addressed: yes ## Reviewed Revision -Reviewed code SHA: c143b54b62d6325a1d4787116e692fedfc5716f2 +Reviewed code SHA: 965cbddd607e9ac7ef9b070e85bdeccd9cbefa48 -Reviewed at: 2026-07-07T17:11:38Z +Reviewed at: 2026-07-07T18:26:47Z -Reviewer run IDs: senior-engineering-review-019f3d21-2c35-7923-ad77-4e3aeefc653e, senior-engineering-rerun-019f3d2f-7f7d-7ec1-96c9-72ce50010243, qa-test-review-019f3d21-36bb-7881-8b01-74405a83a9af, security-auth-review-019f3d21-3c05-7fe3-8c98-a88e15ee82ac, security-auth-rerun-019f3d2f-6ae2-7601-bf97-3e43070480c3, product-ops-review-019f3d21-46f2-7330-ab33-2e31a174a8c2, product-ops-rerun-019f3d2f-8d32-7232-ba81-d7b721bce0a5, architecture-review-019f3d21-502c-7331-adc0-8c50c43fad2c, architecture-rerun-019f3d2f-78ac-72d1-ba9b-146c4dc04ec6, docs-review-019f3d21-5ad7-7f20-9f77-0cc747ded03d, reuse-dedup-review-019f3d2f-a189-70f2-ae58-836448d74835, test-delta-review-019f3d2f-b8f5-7a00-88c8-ac5e54158b13, test-delta-rerun-019f3d3a-da3a-7372-9082-b41fa77b5236, test-delta-final-019f3d69-1863-7923-809d-dbb327c19952 +Reviewer run IDs: senior-engineering-review-019f3d21-2c35-7923-ad77-4e3aeefc653e, senior-engineering-rerun-019f3d2f-7f7d-7ec1-96c9-72ce50010243, senior-engineering-final-019f3dcf-c4a5-7e00-95bc-fd65855864d4, qa-test-review-019f3d21-36bb-7881-8b01-74405a83a9af, qa-test-final-019f3dcb-a346-7d52-ac31-e00053b0c81d, security-auth-review-019f3d21-3c05-7fe3-8c98-a88e15ee82ac, security-auth-rerun-019f3d2f-6ae2-7601-bf97-3e43070480c3, security-auth-final-019f3dcb-d1a9-7ed0-be64-b449f854ae9e, product-ops-review-019f3d21-46f2-7330-ab33-2e31a174a8c2, product-ops-rerun-019f3d2f-8d32-7232-ba81-d7b721bce0a5, product-ops-final-019f3dcc-0208-7c72-8896-8e0526be1904, architecture-review-019f3d21-502c-7331-adc0-8c50c43fad2c, architecture-rerun-019f3d2f-78ac-72d1-ba9b-146c4dc04ec6, architecture-final-019f3dcf-ee6a-7e60-aa73-b62d38ffab1d, docs-review-019f3d21-5ad7-7f20-9f77-0cc747ded03d, docs-final-019f3dcc-3de1-70e1-8320-91769dc877ac, reuse-dedup-review-019f3d2f-a189-70f2-ae58-836448d74835, reuse-dedup-final-019f3dd0-156e-7c93-9947-8631f68d5292, test-delta-review-019f3d2f-b8f5-7a00-88c8-ac5e54158b13, test-delta-rerun-019f3d3a-da3a-7372-9082-b41fa77b5236, test-delta-final-019f3dd0-3ee6-78a3-9a42-518bb77cf5ed -After the reviewed SHA, only evidence and status files changed. +After the reviewed SHA, only review evidence files changed. ## Reviewed Change @@ -36,13 +36,13 @@ Scope: | Reviewer | Result | Blocking findings | Notes | |---|---:|---|---| | senior engineering | PASS WITH LOW RISKS | None | Fixed fail-closed redaction, output scope validation, active-guide schema boundary, and removed dead queue preflight. | -| QA/test | PASS WITH LOW RISKS | None | Added latest-run ordering by source snapshot and denied-role coverage beyond workers. | -| security/auth | PASS | None | Fixed public setup-run error summaries and worker logs/results to avoid raw exception leakage. | -| product/ops | PASS WITH LOW RISKS | None | Accepted that full setup-run proof in real API drill belongs to WS-POL-001-14 while integration tests cover this chunk. | +| QA/test | PASS WITH LOW RISKS | None | Verified setup-run id correlation now compares queue payloads to persisted `ProjectSetupRun.id` and Celery task id. | +| security/auth | PASS | None | Verified unexpected worker failures no longer log raw exception text or tracebacks and visibility APIs remain scoped. | +| product/ops | PASS WITH LOW RISKS | None | Confirmed PR #76 is represented as open and under human review, not merged. | | architecture | PASS | None | Split active-guide checker schema, removed unscoped effective-policy helper, and wrapped latest snapshot ambiguity. | -| docs | PASS | None | Confirmed exact seven API paths are documented and ProjectSetupRun is consistently described as a ledger. | -| reuse/dedup | PASS WITH LOW RISKS | None | Addressed schema duplication by subclassing the active-guide checker schema and removed dead redaction regexes. | -| test delta | PASS WITH LOW RISKS | None | Added cross-project, same-project/different-guide, admin allow, and denied-role visibility coverage. | +| docs | PASS | None | Confirmed exact seven API paths are documented and ProjectSetupRun is consistently described as a ledger; evidence artifacts updated after external review. | +| reuse/dedup | PASS WITH LOW RISKS | None | Accepted low-risk test helper duplication for the focused worker regression. | +| test delta | PASS WITH LOW RISKS | None | Verified setup-run correlation tests and unexpected worker error regression strengthen coverage. | ## Valid Findings Addressed @@ -57,6 +57,10 @@ Scope: - Broadened authorization tests to prove `admin` and `project_manager` can read these endpoints while `worker`, `reviewer`, `finance`, and `auditor` cannot. - Added cross-project and same-project/different-guide scoping checks for setup-run latest, list endpoints, item GET endpoints, effective policy, and pre-submit checker policy reads. - Preserved active-guide `checker_configs` while ensuring the new pre-submit visibility summary omits raw checker authority. +- Addressed CodeRabbit's tautological `setup_run_id` assertion by comparing captured enqueue payloads to persisted `ProjectSetupRun.id` and recorded Celery task id. +- Updated roadmap and loop status wording so PR #76 is represented as open and under human review, not merged. +- Replaced raw unexpected worker exception logging with sanitized structured logging and added regression coverage for result, persisted setup-run error, and log output. +- Added positive log-record assertions for the unexpected worker failure path. ## Commands Run @@ -69,6 +73,8 @@ cd backend && .venv/bin/pytest tests/test_projects.py::test_project_setup_error_ cd backend && .venv/bin/pytest tests/test_projects.py -q cd backend && .venv/bin/pytest tests/test_projects.py::test_project_setup_visibility_apis_show_automatic_setup_outputs tests/test_projects.py::test_project_setup_visibility_apis_require_project_setup_role -q cd backend && WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py +cd backend && .venv/bin/pytest tests/test_projects.py::test_create_guide_autostart_enqueues_without_inline_agent_execution tests/test_projects.py::test_create_source_snapshot_autostart_enqueues_latest_snapshot tests/test_projects.py::test_project_setup_worker_unexpected_error_does_not_leak_raw_exception -q +cd backend && .venv/bin/python -m ruff check app/workers/project_setup.py tests/test_projects.py ``` Results: @@ -81,9 +87,13 @@ Results: - Full project test suite: 206 passed in 2269.22s before final same-project/different-guide list-scope hardening; the post-hardening full-suite process exited cleanly but its interrupted terminal summary was not recoverable. - Final post-hardening targeted visibility tests: 2 passed in 38.57s. - API contract real API E2E: passed after final implementation changes. +- CodeRabbit/security targeted regression tests: 3 passed in 29.13s on final local run. +- Focused ruff check for worker/test changes: passed. +- Final stale wording scan: passed. +- Final Markdown link check: passed for 18 changed Markdown files. ## Remaining Risks - `api_contract_e2e.py` still uses manual setup records for deterministic local execution and does not call `setup-runs/latest`; full no-DB Terminal Benchmark drill proof is assigned to WS-POL-001-14. -- Same-project/different-guide list scoping was hardened in targeted tests after test-delta review. Full project suite was interrupted during output capture, so final proof combines prior full-suite pass plus final targeted visibility pass. +- Same-project/different-guide list scoping was hardened in targeted tests after test-delta review. Full project suite was interrupted during output capture, so final proof combines prior full-suite pass plus final targeted visibility and external-review regression passes. - Setup-run statuses are repeated across migration/model/service/docs. A future cleanup can centralize runtime constants while keeping Alembic literals frozen. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md index f8ddeef..6369c94 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md @@ -43,6 +43,8 @@ cd backend && .venv/bin/pytest tests/test_projects.py::test_project_setup_error_ cd backend && .venv/bin/pytest tests/test_projects.py -q cd backend && .venv/bin/pytest tests/test_projects.py::test_project_setup_visibility_apis_show_automatic_setup_outputs tests/test_projects.py::test_project_setup_visibility_apis_require_project_setup_role -q cd backend && WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream_test .venv/bin/python scripts/api_contract_e2e.py +cd backend && .venv/bin/pytest tests/test_projects.py::test_create_guide_autostart_enqueues_without_inline_agent_execution tests/test_projects.py::test_create_source_snapshot_autostart_enqueues_latest_snapshot tests/test_projects.py::test_project_setup_worker_unexpected_error_does_not_leak_raw_exception -q +cd backend && .venv/bin/python -m ruff check app/workers/project_setup.py tests/test_projects.py ``` Key results: @@ -51,6 +53,10 @@ Key results: - Full project suite: 206 passed before the last optional test hardening. - Final post-hardening visibility tests: 2 passed. - API contract real API E2E: passed. +- External-review regression tests: 3 passed in 29.13s on final local run. +- Focused ruff check for worker/test changes: passed. +- Final stale wording scan: passed. +- Final Markdown link check: passed for 18 changed Markdown files. ## Internal Review @@ -67,7 +73,14 @@ All sub-agent sessions were closed. ## External Review -External review not yet run for this PR state. +CodeRabbit completed review on PR #76. Valid comments were addressed in +`WS-POL-001-12-external-review-response.md`: + +- strengthened the captured `setup_run_id` tests with persisted + `ProjectSetupRun.id` and Celery task id checks +- updated Chunk 12 roadmap/status wording to open PR #76 under human review +- clarified the API contract drill wording +- fixed the internal security finding on raw unexpected worker exception logs ## Human Review Focus From 1465ddb2c8c40eb3b7cc8f1e2befd3971cea53a5 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Tue, 7 Jul 2026 19:38:00 +0100 Subject: [PATCH 6/7] Make setup worker log test deterministic --- backend/tests/test_projects.py | 36 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/backend/tests/test_projects.py b/backend/tests/test_projects.py index 5f2fe4b..c587190 100644 --- a/backend/tests/test_projects.py +++ b/backend/tests/test_projects.py @@ -4,7 +4,6 @@ import hashlib import inspect import json -import logging import sys import types from collections.abc import AsyncIterator, Iterator @@ -1482,7 +1481,6 @@ def fail_enqueue(**_: object) -> str: async def test_project_setup_worker_unexpected_error_does_not_leak_raw_exception( project_client: AsyncClient, monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, ) -> None: """Unexpected worker failures keep secrets out of logs, results, and setup runs.""" from app.workers import project_setup as project_setup_worker_module @@ -1525,7 +1523,12 @@ async def raise_raw_secret_error(*_: object, **__: object) -> object: "run_guide_sufficiency_agent", raise_raw_secret_error, ) - caplog.set_level(logging.ERROR, logger=project_setup_worker_module.logger.name) + error_logs: list[dict[str, object]] = [] + + def capture_error(message: str, *, extra: dict[str, object]) -> None: + error_logs.append({"message": message, "extra": extra}) + + monkeypatch.setattr(project_setup_worker_module.logger, "error", capture_error) result = await project_setup_worker_module._run_pre_submit_setup_pipeline( project["id"], @@ -1549,18 +1552,23 @@ async def raise_raw_secret_error(*_: object, **__: object) -> object: assert persisted.error_summary == ( "project setup failed; inspect server logs with the setup run id" ) - error_records = [ - record - for record in caplog.records - if record.levelno == logging.ERROR and record.message == "project setup pipeline failed" + assert error_logs == [ + { + "message": "project setup pipeline failed", + "extra": { + "project_id": project["id"], + "guide_id": guide["id"], + "source_snapshot_id": snapshot_id, + "setup_run_id": setup_run_id, + "error_code": "RuntimeError", + "error_summary": "unexpected project setup pipeline failure", + }, + } ] - assert len(error_records) == 1 - assert error_records[0].setup_run_id == setup_run_id - assert error_records[0].error_code == "RuntimeError" - assert error_records[0].error_summary == "unexpected project setup pipeline failure" - assert "raw-token" not in caplog.text - assert "secret" not in caplog.text - assert "/srv/private" not in caplog.text + logged_payload = json.dumps(error_logs, sort_keys=True) + assert "raw-token" not in logged_payload + assert "secret" not in logged_payload + assert "/srv/private" not in logged_payload async def test_project_setup_run_rejects_cross_context_worker_updates( From ffdbe4aefb7498ca3bd003aa0ad6fbba1cf4e4ba Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Tue, 7 Jul 2026 19:41:55 +0100 Subject: [PATCH 7/7] Rebind setup visibility review evidence after CI fix --- .../reviews/WS-POL-001-12-external-review-response.md | 9 +++++++-- .../reviews/WS-POL-001-12-internal-review-evidence.md | 9 +++++---- .../reviews/WS-POL-001-12-pr-trust-bundle.md | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-external-review-response.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-external-review-response.md index 356804b..6862250 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-external-review-response.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-external-review-response.md @@ -15,9 +15,13 @@ `logger.exception`, which could leak raw exception text and tracebacks. Replaced it with sanitized structured logging and added regression coverage for task result, persisted setup-run state, and captured logs. +- Internal test-delta review: the first positive log assertion relied on + `caplog` propagation and failed in GitHub Actions. Replaced it with a + deterministic monkeypatched `logger.error` capture that asserts the exact + structured payload and secret-free log data. - Internal docs/product review: trust evidence and status artifacts were stale after external-review fixes. Rebound internal evidence to reviewed code SHA - `965cbddd607e9ac7ef9b070e85bdeccd9cbefa48` and updated this response plus + `1465ddb2c8c40eb3b7cc8f1e2befd3971cea53a5` and updated this response plus the PR trust bundle. ## Comments Deferred @@ -31,7 +35,7 @@ None for these comments. Human merge review is still required for PR #76. ## Commands Rerun - `cd backend && .venv/bin/pytest tests/test_projects.py::test_create_guide_autostart_enqueues_without_inline_agent_execution tests/test_projects.py::test_create_source_snapshot_autostart_enqueues_latest_snapshot tests/test_projects.py::test_project_setup_worker_unexpected_error_does_not_leak_raw_exception -q` - - Result: 3 passed in 29.13s on the final local run. + - Result: 3 passed in 50.76s on the final local run after the CI-specific test fix. - `cd backend && .venv/bin/python -m ruff check app/workers/project_setup.py tests/test_projects.py` - Result: passed. - `python3 scripts/check_stale_workstream_wording.py` @@ -47,6 +51,7 @@ None for these comments. Human merge review is still required for PR #76. - docs: PASS after evidence artifacts are committed - reuse/dedup: PASS WITH LOW RISKS - test delta: PASS WITH LOW RISKS + - CI-specific test-delta rerun: PASS ## Remaining Risks diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md index 29dee3a..d983918 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-internal-review-evidence.md @@ -10,11 +10,11 @@ valid findings addressed: yes ## Reviewed Revision -Reviewed code SHA: 965cbddd607e9ac7ef9b070e85bdeccd9cbefa48 +Reviewed code SHA: 1465ddb2c8c40eb3b7cc8f1e2befd3971cea53a5 -Reviewed at: 2026-07-07T18:26:47Z +Reviewed at: 2026-07-07T18:40:28Z -Reviewer run IDs: senior-engineering-review-019f3d21-2c35-7923-ad77-4e3aeefc653e, senior-engineering-rerun-019f3d2f-7f7d-7ec1-96c9-72ce50010243, senior-engineering-final-019f3dcf-c4a5-7e00-95bc-fd65855864d4, qa-test-review-019f3d21-36bb-7881-8b01-74405a83a9af, qa-test-final-019f3dcb-a346-7d52-ac31-e00053b0c81d, security-auth-review-019f3d21-3c05-7fe3-8c98-a88e15ee82ac, security-auth-rerun-019f3d2f-6ae2-7601-bf97-3e43070480c3, security-auth-final-019f3dcb-d1a9-7ed0-be64-b449f854ae9e, product-ops-review-019f3d21-46f2-7330-ab33-2e31a174a8c2, product-ops-rerun-019f3d2f-8d32-7232-ba81-d7b721bce0a5, product-ops-final-019f3dcc-0208-7c72-8896-8e0526be1904, architecture-review-019f3d21-502c-7331-adc0-8c50c43fad2c, architecture-rerun-019f3d2f-78ac-72d1-ba9b-146c4dc04ec6, architecture-final-019f3dcf-ee6a-7e60-aa73-b62d38ffab1d, docs-review-019f3d21-5ad7-7f20-9f77-0cc747ded03d, docs-final-019f3dcc-3de1-70e1-8320-91769dc877ac, reuse-dedup-review-019f3d2f-a189-70f2-ae58-836448d74835, reuse-dedup-final-019f3dd0-156e-7c93-9947-8631f68d5292, test-delta-review-019f3d2f-b8f5-7a00-88c8-ac5e54158b13, test-delta-rerun-019f3d3a-da3a-7372-9082-b41fa77b5236, test-delta-final-019f3dd0-3ee6-78a3-9a42-518bb77cf5ed +Reviewer run IDs: senior-engineering-review-019f3d21-2c35-7923-ad77-4e3aeefc653e, senior-engineering-rerun-019f3d2f-7f7d-7ec1-96c9-72ce50010243, senior-engineering-final-019f3dcf-c4a5-7e00-95bc-fd65855864d4, qa-test-review-019f3d21-36bb-7881-8b01-74405a83a9af, qa-test-final-019f3dcb-a346-7d52-ac31-e00053b0c81d, security-auth-review-019f3d21-3c05-7fe3-8c98-a88e15ee82ac, security-auth-rerun-019f3d2f-6ae2-7601-bf97-3e43070480c3, security-auth-final-019f3dcb-d1a9-7ed0-be64-b449f854ae9e, product-ops-review-019f3d21-46f2-7330-ab33-2e31a174a8c2, product-ops-rerun-019f3d2f-8d32-7232-ba81-d7b721bce0a5, product-ops-final-019f3dcc-0208-7c72-8896-8e0526be1904, architecture-review-019f3d21-502c-7331-adc0-8c50c43fad2c, architecture-rerun-019f3d2f-78ac-72d1-ba9b-146c4dc04ec6, architecture-final-019f3dcf-ee6a-7e60-aa73-b62d38ffab1d, docs-review-019f3d21-5ad7-7f20-9f77-0cc747ded03d, docs-final-019f3dcc-3de1-70e1-8320-91769dc877ac, reuse-dedup-review-019f3d2f-a189-70f2-ae58-836448d74835, reuse-dedup-final-019f3dd0-156e-7c93-9947-8631f68d5292, test-delta-review-019f3d2f-b8f5-7a00-88c8-ac5e54158b13, test-delta-rerun-019f3d3a-da3a-7372-9082-b41fa77b5236, test-delta-final-019f3dd0-3ee6-78a3-9a42-518bb77cf5ed, test-delta-ci-fix-019f3ddf-a0e1-7563-8224-7fba49e5eb82 After the reviewed SHA, only review evidence files changed. @@ -60,7 +60,7 @@ Scope: - Addressed CodeRabbit's tautological `setup_run_id` assertion by comparing captured enqueue payloads to persisted `ProjectSetupRun.id` and recorded Celery task id. - Updated roadmap and loop status wording so PR #76 is represented as open and under human review, not merged. - Replaced raw unexpected worker exception logging with sanitized structured logging and added regression coverage for result, persisted setup-run error, and log output. -- Added positive log-record assertions for the unexpected worker failure path. +- Added deterministic positive logger assertions for the unexpected worker failure path. ## Commands Run @@ -88,6 +88,7 @@ Results: - Final post-hardening targeted visibility tests: 2 passed in 38.57s. - API contract real API E2E: passed after final implementation changes. - CodeRabbit/security targeted regression tests: 3 passed in 29.13s on final local run. +- CI-specific deterministic logger regression rerun: 3 passed in 50.76s locally. - Focused ruff check for worker/test changes: passed. - Final stale wording scan: passed. - Final Markdown link check: passed for 18 changed Markdown files. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md index 6369c94..9393961 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-12-pr-trust-bundle.md @@ -53,7 +53,7 @@ Key results: - Full project suite: 206 passed before the last optional test hardening. - Final post-hardening visibility tests: 2 passed. - API contract real API E2E: passed. -- External-review regression tests: 3 passed in 29.13s on final local run. +- External-review regression tests: 3 passed in 50.76s on final local run after the CI-specific test fix. - Focused ruff check for worker/test changes: passed. - Final stale wording scan: passed. - Final Markdown link check: passed for 18 changed Markdown files. @@ -81,6 +81,7 @@ CodeRabbit completed review on PR #76. Valid comments were addressed in - updated Chunk 12 roadmap/status wording to open PR #76 under human review - clarified the API contract drill wording - fixed the internal security finding on raw unexpected worker exception logs +- fixed the CI failure by making the positive logger assertion deterministic ## Human Review Focus