From 449b68093fb62d54a64adab32517cca33f17cb59 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Tue, 7 Jul 2026 23:14:24 +0100 Subject: [PATCH 1/5] Expose task locked context APIs --- .agent-loop/LOOP_STATE.md | 26 +- .../STATUS.md | 29 +- backend/app/modules/tasks/router.py | 117 ++- backend/app/modules/tasks/schemas.py | 182 ++++- backend/app/modules/tasks/service.py | 770 +++++++++++++++++- backend/scripts/api_contract_e2e.py | 94 +++ backend/tests/test_tasks.py | 457 ++++++++++- docs/architecture_data_model.md | 8 + docs/architecture_system_architecture.md | 3 + docs/glossary.md | 22 + docs/operations_project_operating_manual.md | 8 + docs/roadmap_status.md | 13 +- 12 files changed, 1670 insertions(+), 59 deletions(-) diff --git a/.agent-loop/LOOP_STATE.md b/.agent-loop/LOOP_STATE.md index 3be91ac..593dff7 100644 --- a/.agent-loop/LOOP_STATE.md +++ b/.agent-loop/LOOP_STATE.md @@ -4,18 +4,16 @@ - Active initiative: `WS-POL-001` - Submission Artifact Policy Foundation - Active planning chunk: none -- Active implementation chunk: `WS-POL-001-12` -- Branch: `codex/ws-pol-001-12-project-setup-policy-visibility` -- 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: 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. +- Active implementation chunk: `WS-POL-001-13` +- Branch: `codex/ws-pol-001-13-task-context-apis` +- Status: `WS-POL-001-13` is implementing worker-safe task work context, + exact submission requirements, and operator-only locked task provenance APIs + after PR #76 merged to `main`. +- Last merged implementation SHA: `46e74de` +- Last merge commit: `46e74de` +- Current gate: implement Chunk 13, run deterministic proof, run required + internal reviewer tracks, then open a PR for human review. +- Next chunk: inactive until `WS-POL-001-13` receives human review and merge. ## Operating Rule @@ -115,3 +113,7 @@ blockchain, frontend, or agent-runtime behavior. actors before the next Terminal Benchmark live API drill. - `WS-POL-001-11` internal review evidence is tracked at `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-11-internal-review-evidence.md`. - `WS-POL-001-11` PR trust bundle is tracked at `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-11-pr-trust-bundle.md`. +- PR #76 merged into `main` as `46e74de`; it implemented `WS-POL-001-12` + project setup-run and policy visibility APIs. +- `WS-POL-001-13` started on branch `codex/ws-pol-001-13-task-context-apis` + after the user's explicit start signal. 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 2488a36..8cee0af 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 @@ -4,20 +4,22 @@ `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`. +`WS-POL-001-09`, `WS-POL-001-10`, `WS-POL-001-11`, and `WS-POL-001-12` 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. `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. +HTTP calls, but it still needs task context visibility, submission finalize +wording, and system-actor audit semantics before being accepted as a no-DB +proof. `WS-POL-001-13` is active to expose worker-safe task context, exact +submission requirements, and operator-only locked provenance. `WS-POL-001-14` +remains the finalize/no-DB proof chunk before rerunning the Terminal Benchmark +drill as accepted evidence. ## Active Chunk -`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. +`WS-POL-001-13` is active on branch +`codex/ws-pol-001-13-task-context-apis`. It adds task work-context, +submission-requirements, and operator locked-context APIs before the next +Terminal Benchmark drill. ## Chunk Status @@ -34,16 +36,15 @@ not merged until the user explicitly approves PR #76. | `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` | 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-12` | Merged | `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. | +| `WS-POL-001-13` | Active | `codex/ws-pol-001-13-task-context-apis` | - | 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 | |---|---|---| -| 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`. | +| Missing task context visibility APIs | Workstream | Implement and review `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 diff --git a/backend/app/modules/tasks/router.py b/backend/app/modules/tasks/router.py index 70a5be5..4b080f4 100644 --- a/backend/app/modules/tasks/router.py +++ b/backend/app/modules/tasks/router.py @@ -16,10 +16,13 @@ from app.modules.tasks.schemas import ( AuditEventResponse, SubmissionCreate, + SubmissionRequirementsResponse, SubmissionResponse, TaskCreate, + TaskLockedContextResponse, TaskResponse, TaskTransitionRequest, + TaskWorkContextResponse, TaskWithAssignmentResponse, ) from app.modules.tasks.service import TaskService, TaskServiceError @@ -27,7 +30,7 @@ router = APIRouter(tags=["tasks"]) -DOMAIN_ERROR_RESPONSE_SCHEMA = { +PRE_SUBMIT_DOMAIN_ERROR_RESPONSE_SCHEMA = { "oneOf": [ { "type": "object", @@ -44,6 +47,23 @@ {"$ref": "#/components/schemas/HTTPValidationError"}, ] } +TASK_LOCKED_CONTEXT_DOMAIN_ERROR_RESPONSE_SCHEMA = { + "oneOf": [ + { + "type": "object", + "required": ["code", "details"], + "properties": { + "code": { + "type": "string", + "enum": ["task_locked_context_invalid"], + }, + "details": {"type": "object"}, + }, + "additionalProperties": False, + }, + {"$ref": "#/components/schemas/HTTPValidationError"}, + ] +} def task_http_error(exc: TaskServiceError) -> HTTPException: @@ -144,6 +164,99 @@ async def get_task( raise task_http_error(exc) from exc +@router.get( + "/tasks/{task_id}/work-context", + response_model=TaskWorkContextResponse, + response_model_exclude_none=True, + responses={ + 422: { + "description": "Locked task context is missing or inconsistent.", + "content": { + "application/json": { + "schema": TASK_LOCKED_CONTEXT_DOMAIN_ERROR_RESPONSE_SCHEMA + } + }, + } + }, +) +async def get_task_work_context( + task_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> TaskWorkContextResponse | JSONResponse: + """Return worker-safe locked guide, policy, and lifecycle context.""" + try: + return await TaskService(session).get_task_work_context(actor, task_id) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except TaskServiceError as exc: + if getattr(exc, "code", None) is not None: + return task_domain_error_response(exc) + raise task_http_error(exc) from exc + + +@router.get( + "/tasks/{task_id}/submission-requirements", + response_model=SubmissionRequirementsResponse, + response_model_exclude_none=True, + responses={ + 422: { + "description": "Locked task context is missing or inconsistent.", + "content": { + "application/json": { + "schema": TASK_LOCKED_CONTEXT_DOMAIN_ERROR_RESPONSE_SCHEMA + } + }, + } + }, +) +async def get_task_submission_requirements( + task_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> SubmissionRequirementsResponse | JSONResponse: + """Return exact worker submission requirements from locked policy context.""" + try: + return await TaskService(session).get_task_submission_requirements(actor, task_id) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except TaskServiceError as exc: + if getattr(exc, "code", None) is not None: + return task_domain_error_response(exc) + raise task_http_error(exc) from exc + + +@router.get( + "/tasks/{task_id}/locked-context", + response_model=TaskLockedContextResponse, + response_model_exclude_none=True, + responses={ + 422: { + "description": "Locked task context is missing or inconsistent.", + "content": { + "application/json": { + "schema": TASK_LOCKED_CONTEXT_DOMAIN_ERROR_RESPONSE_SCHEMA + } + }, + } + }, +) +async def get_task_locked_context( + task_id: str, + actor: Annotated[ActorContext, Depends(get_registered_actor)], + session: Annotated[AsyncSession, Depends(get_db_session)], +) -> TaskLockedContextResponse | JSONResponse: + """Return operator-only locked task provenance.""" + try: + return await TaskService(session).get_task_locked_context(actor, task_id) + except PermissionDenied as exc: + raise permission_http_error(exc) from exc + except TaskServiceError as exc: + if getattr(exc, "code", None) is not None: + return task_domain_error_response(exc) + raise task_http_error(exc) from exc + + @router.post( "/tasks/{task_id}/screen", response_model=TaskResponse, @@ -250,7 +363,7 @@ async def start_task( "description": "Pre-submit domain failure or request validation error.", "content": { "application/json": { - "schema": DOMAIN_ERROR_RESPONSE_SCHEMA, + "schema": PRE_SUBMIT_DOMAIN_ERROR_RESPONSE_SCHEMA, } }, } diff --git a/backend/app/modules/tasks/schemas.py b/backend/app/modules/tasks/schemas.py index e08f1c0..eb3f3f0 100644 --- a/backend/app/modules/tasks/schemas.py +++ b/backend/app/modules/tasks/schemas.py @@ -180,12 +180,192 @@ class TaskResponse(BaseModel): acceptance_criteria: str | None rejection_criteria: str | None deadline_at: datetime | None - created_by: str + created_by: str | None assigned_to: str | None created_at: datetime updated_at: datetime +class TaskProjectContext(BaseModel): + """Worker-safe project summary for a task context response.""" + + id: str + name: str + slug: str + description: str | None + + +class TaskWorkerTaskContext(BaseModel): + """Worker-safe task summary for work-context responses.""" + + id: str + project_id: str + locked_guide_version: str + title: str + description: str + task_type: str | None + difficulty: str | None + skill_tags: list[str] + estimated_time_minutes: int | None + base_amount: Decimal | None + currency: str | None + payout_type: str | None + status: str + acceptance_criteria: str | None + rejection_criteria: str | None + deadline_at: datetime | None + created_at: datetime + updated_at: datetime + + +class TaskGuideContext(BaseModel): + """Worker-safe guide material locked to a task.""" + + id: str + version: str + content_markdown: str + change_summary: str | None + effective_at: datetime | None + + +class TaskReviewPolicyContext(BaseModel): + """Worker-safe review policy summary for the locked guide version.""" + + guide_version: str + + +class TaskRevisionPolicyContext(BaseModel): + """Worker-safe revision policy summary for the locked guide version.""" + + guide_version: str + + +class TaskPaymentPolicyContext(BaseModel): + """Worker-safe payment terms stamped onto the task at screening.""" + + guide_version: str + base_amount: Decimal | None + currency: str | None + payout_type: str | None + + +class TaskWorkerLifecycleContext(BaseModel): + """Worker-facing lifecycle state for a task.""" + + status: str + assigned_to_current_actor: bool + can_run_pre_submit_check: bool + can_submit: bool + next_actions: list[str] + + +class TaskWorkContextResponse(BaseModel): + """Worker-safe context needed before doing task work.""" + + task: TaskWorkerTaskContext + project: TaskProjectContext + guide: TaskGuideContext + review_policy: TaskReviewPolicyContext + revision_policy: TaskRevisionPolicyContext + payment_policy: TaskPaymentPolicyContext + lifecycle: TaskWorkerLifecycleContext + + +class RequiredArtifactRequirement(BaseModel): + """Worker-facing required artifact rule from the locked effective policy.""" + + key: str + path: str + hash_required: bool + required: bool + description: str | None = None + + +class RequiredEvidenceRequirement(BaseModel): + """Worker-facing required evidence rule from the locked effective policy.""" + + key: str + label: str + hash_required: bool + required: bool + description: str | None = None + + +class ForbiddenArtifactRequirement(BaseModel): + """Worker-facing forbidden artifact rule from the locked effective policy.""" + + pattern: str + reason: str | None = None + worker_facing_fix: str | None = None + severity: str | None = None + + +class StorageReferenceRules(BaseModel): + """Worker-facing storage-reference constraints for staged artifacts.""" + + allowed_storage_schemes: list[str] + allowed_uri_prefixes: list[str] + credentials_allowed: bool + query_strings_allowed: bool + fragments_allowed: bool + path_traversal_allowed: bool + + +class SubmissionRequirementsResponse(BaseModel): + """Worker-safe exact submission requirements for a locked task.""" + + task_id: str + project_id: str + guide_version: str + policy_schema_version: str | None + merge_algorithm_version: str | None + required_packet_fields: list[str] + required_artifacts: list[RequiredArtifactRequirement] + required_evidence: list[RequiredEvidenceRequirement] + forbidden_artifacts: list[ForbiddenArtifactRequirement] + attestation_terms: list[str] + manifest_required: bool + artifact_hash_required: bool + artifact_hash_algorithm: Literal["sha256"] + allowed_storage_schemes: list[str] + storage_reference_rules: StorageReferenceRules + maximum_file_size_bytes: int | None + maximum_package_size_bytes: int | None + packaging: dict[str, Any] + + +class PostSubmitPolicyBodySummary(BaseModel): + """Operator-facing summary of the locked post-submit checker policy body.""" + + schema_version: str | None + default_checkers: list[str] + required_checkers: list[str] + warning_checkers: list[str] + execution_checkers: list[str] + blocking_severities: list[str] + + +class TaskLockedContextResponse(BaseModel): + """Operator-only locked provenance for a task.""" + + task_id: str + project_id: str + locked_guide_version: str + locked_guide_source_snapshot_id: str + locked_guide_source_snapshot_hash: str + locked_effective_project_submission_artifact_policy_id: str + locked_effective_project_submission_artifact_policy_hash: str + locked_pre_submit_checker_policy_id: str + locked_pre_submit_checker_bundle_hash: str + locked_post_submit_checker_policy_id: str + locked_post_submit_checker_policy_version: str + locked_post_submit_checker_policy_hash: str + locked_post_submit_checker_policy_body_summary: PostSubmitPolicyBodySummary + locked_review_policy_version: str + locked_revision_policy_version: str + locked_payment_policy_version: str + + class AssignmentResponse(BaseModel): """Response schema for task assignments.""" diff --git a/backend/app/modules/tasks/service.py b/backend/app/modules/tasks/service.py index c048406..277c8ba 100644 --- a/backend/app/modules/tasks/service.py +++ b/backend/app/modules/tasks/service.py @@ -2,13 +2,20 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import UTC, datetime +from typing import Any from uuid import uuid4 from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession +from app.core.hashing import canonical_json_hash from app.core.permissions import require_any_role +from app.modules.checkers.compiler import ( + PreSubmitCheckerCompilerError, + validate_compiled_pre_submit_checker_bundle, +) from app.modules.actors.models import ActorProfile from app.modules.actors.service import ActorService from app.modules.projects.models import ( @@ -17,6 +24,7 @@ PaymentPolicy, PostSubmitCheckerPolicy, PreSubmitCheckerPolicy, + Project, ProjectGuide, RevisionPolicy, ReviewPolicy, @@ -47,10 +55,25 @@ from app.modules.tasks.schemas import ( AssignmentResponse, AuditEventResponse, + ForbiddenArtifactRequirement, + PostSubmitPolicyBodySummary, + RequiredArtifactRequirement, + RequiredEvidenceRequirement, + StorageReferenceRules, SubmissionCreate, + SubmissionRequirementsResponse, SubmissionResponse, TaskCreate, + TaskGuideContext, + TaskLockedContextResponse, + TaskPaymentPolicyContext, + TaskProjectContext, TaskResponse, + TaskReviewPolicyContext, + TaskRevisionPolicyContext, + TaskWorkerLifecycleContext, + TaskWorkerTaskContext, + TaskWorkContextResponse, TaskWithAssignmentResponse, ) from app.schemas.auth import ActorContext @@ -80,6 +103,28 @@ "pre_review_gate_needs_revision", "pre_review_gate_blocked", } +SUBMISSION_CREATE_REQUIRED_PACKET_FIELDS = ( + "summary", + "package_hash", + "artifact_hash_manifest", + "worker_attestation", +) +LOCKED_CONTEXT_REQUIRED_FIELDS = ( + "locked_guide_version", + "locked_post_submit_checker_policy_id", + "locked_post_submit_checker_policy_version", + "locked_post_submit_checker_policy_hash", + "locked_post_submit_checker_policy_body", + "locked_review_policy_version", + "locked_revision_policy_version", + "locked_payment_policy_version", + "locked_guide_source_snapshot_id", + "locked_guide_source_snapshot_hash", + "locked_effective_project_submission_artifact_policy_id", + "locked_effective_project_submission_artifact_policy_hash", + "locked_pre_submit_checker_policy_id", + "locked_pre_submit_checker_bundle_hash", +) class TaskServiceError(Exception): @@ -162,6 +207,39 @@ def __init__(self, details: dict) -> None: self.details = details +class TaskLockedContextInvalid(TaskServiceError): + """Raised when a task's stamped policy provenance is missing or inconsistent.""" + + status_code = 422 + code = "task_locked_context_invalid" + + def __init__(self, message: str, details: dict[str, Any] | None = None) -> None: + """Create a locked-context error with machine-readable details. + + Args: + message: Human-readable error summary safe for API responses. + details: Optional structured error details. + """ + super().__init__(message) + self.details = details or {"message": message} + + +@dataclass(frozen=True) +class LockedTaskContext: + """Validated records bound to one task's stamped locked context.""" + + project: Project + guide: ProjectGuide + source_snapshot: GuideSourceSnapshot + effective_policy: EffectiveProjectSubmissionArtifactPolicy + pre_submit_checker_policy: PreSubmitCheckerPolicy + post_submit_checker_policy: PostSubmitCheckerPolicy + locked_post_submit_policy_body: PostSubmitPolicyBodySummary + review_policy: ReviewPolicy + revision_policy: RevisionPolicy + payment_policy: PaymentPolicy + + class TaskService: """Coordinates task lifecycle rules, assignment, and audit writes.""" @@ -254,6 +332,80 @@ async def get_task(self, actor: ActorContext, task_id: str) -> TaskResponse: await self._ensure_task_visible(actor, task) return self._task_response(actor, task) + async def get_task_work_context( + self, + actor: ActorContext, + task_id: str, + ) -> TaskWorkContextResponse: + """Return worker-safe work context from the task's locked provenance. + + Args: + actor: Verified Flow actor context for the current request. + task_id: Task whose context should be returned. + + Returns: + Worker-facing task, guide, policy, and lifecycle context. + + Raises: + PermissionDenied: If the actor cannot view tasks. + TaskNotFound: If the task is unknown or hidden. + TaskLockedContextInvalid: If locked context is incomplete or stale. + """ + require_any_role(actor, TASK_VIEW_ROLES) + task = await self._get_task(task_id) + await self._ensure_task_visible(actor, task) + context = await self._load_locked_task_context(task) + return self._work_context_response(actor, task, context) + + async def get_task_submission_requirements( + self, + actor: ActorContext, + task_id: str, + ) -> SubmissionRequirementsResponse: + """Return exact worker submission requirements for a locked task. + + Args: + actor: Verified Flow actor context for the current request. + task_id: Task whose locked requirements should be returned. + + Returns: + Worker-facing submission artifact requirements. + + Raises: + PermissionDenied: If the actor cannot view tasks. + TaskNotFound: If the task is unknown or hidden. + TaskLockedContextInvalid: If locked context is incomplete or stale. + """ + require_any_role(actor, TASK_VIEW_ROLES) + task = await self._get_task(task_id) + await self._ensure_task_visible(actor, task) + context = await self._load_locked_task_context(task) + return self._submission_requirements_response(task, context) + + async def get_task_locked_context( + self, + actor: ActorContext, + task_id: str, + ) -> TaskLockedContextResponse: + """Return operator-only locked provenance for a task. + + Args: + actor: Verified Flow actor context for the current request. + task_id: Task whose locked provenance should be returned. + + Returns: + Full locked guide and policy provenance for support/debugging. + + Raises: + PermissionDenied: If the actor is not an operator. + TaskNotFound: If the task is unknown. + TaskLockedContextInvalid: If locked context is incomplete or stale. + """ + require_any_role(actor, PROJECT_OPERATOR_ROLES) + task = await self._get_task(task_id) + context = await self._load_locked_task_context(task) + return self._locked_context_response(task, context) + async def move_to_screening( self, actor: ActorContext, @@ -895,6 +1047,597 @@ async def _load_active_policy_context( pre_submit_checker_policy, ) + async def _load_locked_task_context(self, task: WorkstreamTask) -> LockedTaskContext: + """Load and validate every row stamped into a task's locked context. + + Args: + task: Task whose locked context is authoritative. + + Returns: + Validated locked task context rows. + + Raises: + TaskLockedContextInvalid: If any stamped row is missing, stale, or + inconsistent with the task's locked ids and hashes. + """ + missing = self._missing_locked_context_fields(task) + if missing: + raise TaskLockedContextInvalid( + "task locked context is incomplete", + {"missing_fields": missing}, + ) + + project = await self._project_repo.get_project(task.project_id) + guide = await self._project_repo.get_guide_by_version( + task.project_id, + task.locked_guide_version or "", + ) + if project is None or guide is None or guide.project_id != task.project_id: + raise TaskLockedContextInvalid( + "task locked guide context is invalid", + {"field": "locked_guide_version"}, + ) + + source_snapshot = await self._project_repo.get_guide_source_snapshot( + task.locked_guide_source_snapshot_id or "", + ) + if ( + source_snapshot is None + or source_snapshot.project_id != task.project_id + or source_snapshot.guide_version != task.locked_guide_version + or source_snapshot.bundle_hash != task.locked_guide_source_snapshot_hash + or canonical_json_hash(source_snapshot.manifest_json) != source_snapshot.bundle_hash + ): + raise TaskLockedContextInvalid( + "task locked guide source snapshot is invalid", + {"field": "locked_guide_source_snapshot_hash"}, + ) + + effective_policy = ( + await self._project_repo.get_effective_submission_artifact_policy_by_id( + task.locked_effective_project_submission_artifact_policy_id or "", + ) + ) + if ( + effective_policy is None + or effective_policy.project_id != task.project_id + or effective_policy.guide_version != task.locked_guide_version + or effective_policy.source_snapshot_id != source_snapshot.id + or effective_policy.source_snapshot_hash != source_snapshot.bundle_hash + or effective_policy.effective_policy_hash + != task.locked_effective_project_submission_artifact_policy_hash + or effective_policy.lifecycle_status not in {"approved", "superseded"} + or canonical_json_hash(effective_policy.effective_policy) + != task.locked_effective_project_submission_artifact_policy_hash + ): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": "locked_effective_project_submission_artifact_policy_hash"}, + ) + + pre_submit_checker_policy = await self._project_repo.get_pre_submit_checker_policy( + task.locked_pre_submit_checker_policy_id or "", + ) + compiled_bundle = ( + pre_submit_checker_policy.compiled_bundle + if pre_submit_checker_policy is not None + else None + ) + if ( + pre_submit_checker_policy is None + or pre_submit_checker_policy.project_id != task.project_id + or pre_submit_checker_policy.guide_version != task.locked_guide_version + or pre_submit_checker_policy.source_snapshot_id != source_snapshot.id + or pre_submit_checker_policy.source_snapshot_hash != source_snapshot.bundle_hash + or pre_submit_checker_policy.effective_policy_id != effective_policy.id + or pre_submit_checker_policy.effective_policy_hash + != effective_policy.effective_policy_hash + or pre_submit_checker_policy.lifecycle_status not in {"compiled", "superseded"} + or pre_submit_checker_policy.compiled_bundle_hash + != task.locked_pre_submit_checker_bundle_hash + or not isinstance(compiled_bundle, dict) + or canonical_json_hash(compiled_bundle) != task.locked_pre_submit_checker_bundle_hash + ): + raise TaskLockedContextInvalid( + "task locked project pre-submit checker policy is invalid", + {"field": "locked_pre_submit_checker_bundle_hash"}, + ) + try: + compiled_checker_names = validate_compiled_pre_submit_checker_bundle( + effective_policy.effective_policy, + effective_policy.effective_policy_hash, + compiled_bundle, + compiler_version=pre_submit_checker_policy.compiler_version, + ) + except PreSubmitCheckerCompilerError as exc: + raise TaskLockedContextInvalid( + "task locked project pre-submit checker policy is invalid", + {"field": "locked_pre_submit_checker_bundle_hash"}, + ) from exc + if list(pre_submit_checker_policy.checker_names or []) != compiled_checker_names: + raise TaskLockedContextInvalid( + "task locked project pre-submit checker projection is invalid", + {"field": "locked_pre_submit_checker_policy_id"}, + ) + + post_submit_checker_policy = ( + await self._project_repo.get_post_submit_checker_policy_by_id( + task.locked_post_submit_checker_policy_id or "", + ) + ) + if ( + post_submit_checker_policy is None + or post_submit_checker_policy.project_id != task.project_id + or post_submit_checker_policy.guide_version + != task.locked_post_submit_checker_policy_version + or post_submit_checker_policy.guide_version != task.locked_guide_version + or post_submit_checker_policy.policy_hash + != task.locked_post_submit_checker_policy_hash + ): + raise TaskLockedContextInvalid( + "task locked post-submit checker policy is invalid", + {"field": "locked_post_submit_checker_policy_hash"}, + ) + try: + parsed_post_submit_body = parse_locked_post_submit_checker_policy_body( + task.locked_post_submit_checker_policy_body, + project_id=task.project_id, + guide_version=task.locked_post_submit_checker_policy_version or "", + policy_hash=task.locked_post_submit_checker_policy_hash or "", + ) + except ValueError as exc: + raise TaskLockedContextInvalid( + "task locked post-submit checker policy body is invalid", + {"field": "locked_post_submit_checker_policy_body"}, + ) from exc + post_submit_summary = PostSubmitPolicyBodySummary( + schema_version=(task.locked_post_submit_checker_policy_body or {}).get( + "schema_version" + ), + default_checkers=parsed_post_submit_body.default_checkers, + required_checkers=parsed_post_submit_body.required_checkers, + warning_checkers=parsed_post_submit_body.warning_checkers, + execution_checkers=parsed_post_submit_body.execution_checkers, + blocking_severities=parsed_post_submit_body.blocking_severities, + ) + + review_policy = await self._project_repo.get_review_policy( + task.project_id, + task.locked_review_policy_version or "", + ) + revision_policy = await self._project_repo.get_revision_policy( + task.project_id, + task.locked_revision_policy_version or "", + ) + payment_policy = await self._project_repo.get_payment_policy( + task.project_id, + task.locked_payment_policy_version or "", + ) + if ( + review_policy is None + or review_policy.guide_version != task.locked_guide_version + or revision_policy is None + or revision_policy.guide_version != task.locked_guide_version + or payment_policy is None + or payment_policy.guide_version != task.locked_guide_version + ): + raise TaskLockedContextInvalid( + "task locked review, revision, or payment policy is invalid", + {"field": "locked_policy_versions"}, + ) + + return LockedTaskContext( + project=project, + guide=guide, + source_snapshot=source_snapshot, + effective_policy=effective_policy, + pre_submit_checker_policy=pre_submit_checker_policy, + post_submit_checker_policy=post_submit_checker_policy, + locked_post_submit_policy_body=post_submit_summary, + review_policy=review_policy, + revision_policy=revision_policy, + payment_policy=payment_policy, + ) + + def _missing_locked_context_fields(self, task: WorkstreamTask) -> list[str]: + """Return missing locked-context fields for a task.""" + return [ + field + for field in LOCKED_CONTEXT_REQUIRED_FIELDS + if not getattr(task, field) + ] + + def _work_context_response( + self, + actor: ActorContext, + task: WorkstreamTask, + context: LockedTaskContext, + ) -> TaskWorkContextResponse: + """Build the worker-safe work-context response.""" + return TaskWorkContextResponse( + task=self._worker_safe_task_response(task), + project=TaskProjectContext( + id=context.project.id, + name=context.project.name, + slug=context.project.slug, + description=context.project.description, + ), + guide=TaskGuideContext( + id=context.guide.id, + version=context.guide.version, + content_markdown=context.guide.content_markdown, + change_summary=context.guide.change_summary, + effective_at=context.guide.effective_at, + ), + review_policy=TaskReviewPolicyContext( + guide_version=task.locked_review_policy_version or "", + ), + revision_policy=TaskRevisionPolicyContext( + guide_version=task.locked_revision_policy_version or "", + ), + payment_policy=TaskPaymentPolicyContext( + guide_version=task.locked_payment_policy_version or "", + base_amount=task.base_amount, + currency=task.currency, + payout_type=task.payout_type, + ), + lifecycle=self._worker_lifecycle_context(actor, task), + ) + + def _submission_requirements_response( + self, + task: WorkstreamTask, + context: LockedTaskContext, + ) -> SubmissionRequirementsResponse: + """Build worker-facing requirements from the locked effective policy.""" + policy = context.effective_policy.effective_policy + if not isinstance(policy, dict): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": "effective_policy"}, + ) + allowed_storage_schemes = self._policy_string_list( + policy, + "allowed_storage_schemes", + ) + required_artifacts = self._policy_list(policy, "required_artifacts") + required_evidence = self._policy_list(policy, "required_evidence") + forbidden_artifacts = self._policy_list(policy, "forbidden_artifacts") + return SubmissionRequirementsResponse( + task_id=task.id, + project_id=task.project_id, + guide_version=task.locked_guide_version or "", + policy_schema_version=self._optional_policy_text(policy, "schema_version"), + merge_algorithm_version=self._optional_policy_text( + policy, + "merge_algorithm_version", + ), + required_packet_fields=self._required_packet_fields(policy), + required_artifacts=[ + RequiredArtifactRequirement( + key=self._policy_rule_text(rule, "key", "required_artifacts"), + path=self._policy_rule_text(rule, "path", "required_artifacts"), + hash_required=self._policy_rule_bool( + rule, + "hash_required", + "required_artifacts", + ), + required=self._policy_rule_bool( + rule, + "required", + "required_artifacts", + ), + description=self._optional_policy_rule_text( + rule, + "description", + "required_artifacts", + ), + ) + for rule in required_artifacts + ], + required_evidence=[ + RequiredEvidenceRequirement( + key=self._policy_rule_text(rule, "key", "required_evidence"), + label=self._policy_rule_text(rule, "label", "required_evidence"), + hash_required=self._policy_rule_bool( + rule, + "hash_required", + "required_evidence", + ), + required=self._policy_rule_bool( + rule, + "required", + "required_evidence", + ), + description=self._optional_policy_rule_text( + rule, + "description", + "required_evidence", + ), + ) + for rule in required_evidence + ], + forbidden_artifacts=[ + ForbiddenArtifactRequirement( + pattern=self._policy_rule_text(rule, "pattern", "forbidden_artifacts"), + reason=self._optional_policy_rule_text( + rule, + "reason", + "forbidden_artifacts", + ), + worker_facing_fix=self._optional_policy_rule_text( + rule, + "worker_facing_fix", + "forbidden_artifacts", + ), + severity=self._optional_policy_rule_text( + rule, + "severity", + "forbidden_artifacts", + ), + ) + for rule in forbidden_artifacts + ], + attestation_terms=self._policy_string_list(policy, "attestation_terms"), + manifest_required=self._policy_bool(policy, "manifest_required"), + artifact_hash_required=self._policy_bool(policy, "artifact_hash_required"), + artifact_hash_algorithm="sha256", + allowed_storage_schemes=allowed_storage_schemes, + storage_reference_rules=StorageReferenceRules( + allowed_storage_schemes=allowed_storage_schemes, + allowed_uri_prefixes=[f"{scheme}://" for scheme in allowed_storage_schemes], + credentials_allowed=False, + query_strings_allowed=False, + fragments_allowed=False, + path_traversal_allowed=False, + ), + maximum_file_size_bytes=self._optional_policy_non_negative_int( + policy, + "maximum_file_size_bytes", + ), + maximum_package_size_bytes=self._optional_policy_non_negative_int( + policy, + "maximum_package_size_bytes", + ), + packaging=self._policy_object(policy, "packaging"), + ) + + def _policy_list(self, policy: dict[str, Any], field: str) -> list[Any]: + """Return a list field from the locked policy or fail closed.""" + value = policy.get(field, []) + if not isinstance(value, list): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{field}"}, + ) + return value + + def _policy_string_list(self, policy: dict[str, Any], field: str) -> list[str]: + """Return a string-list field from the locked policy or fail closed.""" + values = self._policy_list(policy, field) + normalized: list[str] = [] + for value in values: + if not isinstance(value, str) or not value.strip(): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{field}"}, + ) + normalized.append(value.strip()) + return normalized + + def _policy_bool(self, policy: dict[str, Any], field: str) -> bool: + """Return a boolean field from the locked policy or fail closed.""" + value = policy.get(field, True) + if not isinstance(value, bool): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{field}"}, + ) + return value + + def _policy_object(self, policy: dict[str, Any], field: str) -> dict[str, Any]: + """Return an object field from the locked policy or fail closed.""" + value = policy.get(field, {}) + if not isinstance(value, dict): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{field}"}, + ) + return dict(value) + + def _optional_policy_text(self, policy: dict[str, Any], field: str) -> str | None: + """Return an optional text field from the locked policy or fail closed.""" + value = policy.get(field) + if value is None: + return None + if not isinstance(value, str) or not value.strip(): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{field}"}, + ) + return value + + def _optional_policy_non_negative_int( + self, + policy: dict[str, Any], + field: str, + ) -> int | None: + """Return an optional non-negative integer from the locked policy.""" + value = policy.get(field) + if value is None: + return None + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{field}"}, + ) + return value + + def _required_packet_fields(self, policy: dict[str, Any]) -> list[str]: + """Return exact submission packet fields required by API and policy.""" + required_fields: list[str] = [] + policy_fields = policy.get("required_packet_fields", []) + if not isinstance(policy_fields, list): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": "effective_policy.required_packet_fields"}, + ) + for field in (*SUBMISSION_CREATE_REQUIRED_PACKET_FIELDS, *policy_fields): + if not isinstance(field, str) or not field.strip(): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": "effective_policy.required_packet_fields"}, + ) + normalized = field.strip() + if normalized not in required_fields: + required_fields.append(normalized) + return required_fields + + def _policy_rule_text(self, rule: object, key: str, collection: str) -> str: + """Return a required text field from a locked policy rule or fail closed.""" + if not isinstance(rule, dict): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{collection}"}, + ) + value = rule.get(key) + if not isinstance(value, str) or not value.strip(): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{collection}.{key}"}, + ) + return value + + def _optional_policy_rule_text( + self, + rule: object, + key: str, + collection: str, + ) -> str | None: + """Return an optional text field from a locked policy rule.""" + if not isinstance(rule, dict): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{collection}"}, + ) + value = rule.get(key) + if value is None: + return None + if not isinstance(value, str) or not value.strip(): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{collection}.{key}"}, + ) + return value + + def _policy_rule_bool( + self, + rule: object, + key: str, + collection: str, + ) -> bool: + """Return a boolean field from a locked policy rule or fail closed.""" + if not isinstance(rule, dict): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{collection}"}, + ) + value = rule.get(key, True) + if not isinstance(value, bool): + raise TaskLockedContextInvalid( + "task locked effective project submission artifact policy is invalid", + {"field": f"effective_policy.{collection}.{key}"}, + ) + return value + + def _locked_context_response( + self, + task: WorkstreamTask, + context: LockedTaskContext, + ) -> TaskLockedContextResponse: + """Build the operator-only locked-context response.""" + return TaskLockedContextResponse( + task_id=task.id, + project_id=task.project_id, + locked_guide_version=task.locked_guide_version or "", + locked_guide_source_snapshot_id=task.locked_guide_source_snapshot_id or "", + locked_guide_source_snapshot_hash=task.locked_guide_source_snapshot_hash or "", + locked_effective_project_submission_artifact_policy_id=( + task.locked_effective_project_submission_artifact_policy_id or "" + ), + locked_effective_project_submission_artifact_policy_hash=( + task.locked_effective_project_submission_artifact_policy_hash or "" + ), + locked_pre_submit_checker_policy_id=task.locked_pre_submit_checker_policy_id or "", + locked_pre_submit_checker_bundle_hash=( + task.locked_pre_submit_checker_bundle_hash or "" + ), + locked_post_submit_checker_policy_id=( + task.locked_post_submit_checker_policy_id or "" + ), + locked_post_submit_checker_policy_version=( + task.locked_post_submit_checker_policy_version or "" + ), + locked_post_submit_checker_policy_hash=( + task.locked_post_submit_checker_policy_hash or "" + ), + locked_post_submit_checker_policy_body_summary=( + context.locked_post_submit_policy_body + ), + locked_review_policy_version=task.locked_review_policy_version or "", + locked_revision_policy_version=task.locked_revision_policy_version or "", + locked_payment_policy_version=task.locked_payment_policy_version or "", + ) + + def _worker_safe_task_response(self, task: WorkstreamTask) -> TaskWorkerTaskContext: + """Build a task summary without private source/import provenance.""" + return TaskWorkerTaskContext( + id=task.id, + project_id=task.project_id, + locked_guide_version=task.locked_guide_version or "", + title=task.title, + description=task.description, + task_type=task.task_type, + difficulty=task.difficulty, + skill_tags=list(task.skill_tags), + estimated_time_minutes=task.estimated_time_minutes, + base_amount=task.base_amount, + currency=task.currency, + payout_type=task.payout_type, + status=task.status, + acceptance_criteria=task.acceptance_criteria, + rejection_criteria=task.rejection_criteria, + deadline_at=task.deadline_at, + created_at=task.created_at, + updated_at=task.updated_at, + ) + + def _worker_lifecycle_context( + self, + actor: ActorContext, + task: WorkstreamTask, + ) -> TaskWorkerLifecycleContext: + """Build worker-facing lifecycle booleans and next actions.""" + assigned_to_current_actor = task.assigned_to == actor.actor_id + can_submit = assigned_to_current_actor and task.status in { + TASK_STATUS_IN_PROGRESS, + TASK_STATUS_NEEDS_REVISION, + } + next_actions: list[str] = [] + if task.status == TASK_STATUS_READY and task.assigned_to is None: + next_actions.append("claim") + elif task.status == TASK_STATUS_CLAIMED and assigned_to_current_actor: + next_actions.append("start") + elif can_submit: + next_actions.extend(["run_pre_submit_check", "submit"]) + return TaskWorkerLifecycleContext( + status=task.status, + assigned_to_current_actor=assigned_to_current_actor, + can_run_pre_submit_check=can_submit, + can_submit=can_submit, + next_actions=next_actions, + ) + def _validate_task_contract_fields(self, task: WorkstreamTask) -> None: """Validate task source and reviewability fields before screening. @@ -992,26 +1735,7 @@ def _ensure_locked_context(self, task: WorkstreamTask) -> None: Raises: TaskTransitionBlocked: If any context field is missing. """ - missing = [ - field - for field in ( - "locked_guide_version", - "locked_post_submit_checker_policy_id", - "locked_post_submit_checker_policy_version", - "locked_post_submit_checker_policy_hash", - "locked_post_submit_checker_policy_body", - "locked_review_policy_version", - "locked_revision_policy_version", - "locked_payment_policy_version", - "locked_guide_source_snapshot_id", - "locked_guide_source_snapshot_hash", - "locked_effective_project_submission_artifact_policy_id", - "locked_effective_project_submission_artifact_policy_hash", - "locked_pre_submit_checker_policy_id", - "locked_pre_submit_checker_bundle_hash", - ) - if not getattr(task, field) - ] + missing = self._missing_locked_context_fields(task) if missing: raise TaskTransitionBlocked(f"task missing locked context: {', '.join(missing)}") @@ -1195,6 +1919,12 @@ def _task_response(self, actor: ActorContext, task: WorkstreamTask) -> TaskRespo """ response = TaskResponse.model_validate(task) if not set(actor.roles).intersection(PROJECT_OPERATOR_ROLES): + response.source_ref = None + response.source_payload_hash = None + response.import_batch_id = None + response.external_task_id = None + response.created_by = None + response.assigned_to = None response.locked_guide_source_snapshot_id = None response.locked_guide_source_snapshot_hash = None response.locked_effective_project_submission_artifact_policy_id = None diff --git a/backend/scripts/api_contract_e2e.py b/backend/scripts/api_contract_e2e.py index d543a33..ba4c591 100644 --- a/backend/scripts/api_contract_e2e.py +++ b/backend/scripts/api_contract_e2e.py @@ -884,6 +884,90 @@ async def exercise_api_contract(base_url: str, env: dict[str, str]) -> None: assert worker_profile["status"] == "active" assert set(worker_profile["skill_tags"]) == {"stem", "proofs"} await request_json(client, "GET", f"/api/v1/tasks/{task['id']}", worker_token) + ready_work_context = await request_json( + client, + "GET", + f"/api/v1/tasks/{task['id']}/work-context", + worker_token, + ) + ensure( + ready_work_context["guide"]["version"] == "v1", + "worker work context did not use locked guide v1", + ) + ensure( + ready_work_context["lifecycle"]["next_actions"] == ["claim"], + "ready worker context did not expose claim as next action", + ) + ensure( + "locked_guide_source_snapshot_hash" + not in json.dumps(ready_work_context, sort_keys=True), + "worker work context leaked source snapshot hash", + ) + for private_field in ( + "source_ref", + "source_payload_hash", + "import_batch_id", + "external_task_id", + "created_by", + "assigned_to", + ): + ensure( + private_field not in ready_work_context["task"], + f"worker work context leaked {private_field}", + ) + submission_requirements = await request_json( + client, + "GET", + f"/api/v1/tasks/{task['id']}/submission-requirements", + worker_token, + ) + ensure( + submission_requirements["required_artifacts"][0]["path"] == "answer.md", + "submission requirements did not expose the locked artifact path", + ) + ensure( + submission_requirements["required_evidence"][0]["key"] == "checker_log", + "submission requirements did not expose the locked evidence key", + ) + ensure( + submission_requirements["artifact_hash_algorithm"] == "sha256", + "submission requirements did not expose platform hash algorithm", + ) + ensure( + submission_requirements["required_packet_fields"] + == [ + "summary", + "package_hash", + "artifact_hash_manifest", + "worker_attestation", + ], + "submission requirements did not expose exact submission request fields", + ) + ensure( + "compiled_bundle" not in json.dumps(submission_requirements, sort_keys=True), + "submission requirements leaked compiled checker bundle", + ) + await request_json( + client, + "GET", + f"/api/v1/tasks/{task['id']}/locked-context", + worker_token, + expected_status=403, + ) + locked_context = await request_json( + client, + "GET", + f"/api/v1/tasks/{task['id']}/locked-context", + manager_token, + ) + ensure( + locked_context["locked_guide_source_snapshot_hash"].startswith("sha256:"), + "operator locked context omitted source snapshot hash", + ) + ensure( + locked_context["locked_pre_submit_checker_bundle_hash"].startswith("sha256:"), + "operator locked context omitted pre-submit checker hash", + ) await request_json( client, "GET", @@ -912,6 +996,16 @@ async def exercise_api_contract(base_url: str, env: dict[str, str]) -> None: worker_token, {"reason": "real worker start"}, ) + active_work_context = await request_json( + client, + "GET", + f"/api/v1/tasks/{task['id']}/work-context", + worker_token, + ) + ensure( + active_work_context["lifecycle"]["can_submit"] is True, + "in-progress worker context did not expose submit readiness", + ) submission = await request_json( client, "POST", diff --git a/backend/tests/test_tasks.py b/backend/tests/test_tasks.py index 10e55cc..93bb610 100644 --- a/backend/tests/test_tasks.py +++ b/backend/tests/test_tasks.py @@ -2,6 +2,7 @@ import asyncio import hashlib +import json from collections.abc import AsyncIterator, Iterator from datetime import UTC, datetime from decimal import Decimal @@ -29,8 +30,12 @@ from app.modules.actors.service import ActorService from app.modules.projects.models import ( EffectiveProjectSubmissionArtifactPolicy, + GuideSourceSnapshot, + PaymentPolicy, PostSubmitCheckerPolicy, PreSubmitCheckerPolicy, + ReviewPolicy, + RevisionPolicy, SubmissionArtifactPolicy, ) from app.modules.tasks.lifecycle import InvalidTaskTransition, ensure_allowed_transition @@ -255,6 +260,7 @@ async def create_policy_bundle_for_guide( client: AsyncClient, project_id: str, guide_id: str, + policy_body: dict | None = None, ) -> dict: snapshot_response = await client.post( f"/api/v1/projects/{project_id}/guides/{guide_id}/source-snapshots", @@ -292,7 +298,7 @@ async def create_policy_bundle_for_guide( json={ "source_snapshot_id": snapshot["id"], "policy_version": "v1", - "policy_body": policy_body_for_task_tests(), + "policy_body": policy_body or policy_body_for_task_tests(), }, ) assert policy_response.status_code == 201, policy_response.text @@ -614,6 +620,19 @@ def test_submission_create_openapi_documents_domain_error() -> None: assert "details" in domain_schema["properties"] +def test_task_context_openapi_documents_locked_context_domain_error() -> None: + schema = create_app().openapi() + responses = schema["paths"]["/api/v1/tasks/{task_id}/work-context"]["get"][ + "responses" + ] + response_422 = responses["422"]["content"]["application/json"]["schema"] + + assert {"$ref": "#/components/schemas/HTTPValidationError"} in response_422["oneOf"] + domain_schema = next(option for option in response_422["oneOf"] if "properties" in option) + assert domain_schema["properties"]["code"]["enum"] == ["task_locked_context_invalid"] + assert "details" in domain_schema["properties"] + + def test_task_locked_context_constraints_bind_task_submission_and_hashes() -> None: expected_task_constraints = { "fk_workstream_tasks_locked_source_snapshot_hash": [ @@ -1080,7 +1099,10 @@ async def test_worker_task_response_redacts_locked_policy_hashes( monkeypatch: pytest.MonkeyPatch, ) -> None: project = await create_active_project(task_client) - ready_task = await create_ready_task(task_client, project["id"]) + payload = complete_task_payload() + payload["import_batch_id"] = "private-import-batch" + payload["external_task_id"] = "private-external-task" + ready_task = await create_ready_task(task_client, project["id"], payload=payload) operator_response = await task_client.get( f"/api/v1/tasks/{ready_task['id']}", headers=auth_headers(), @@ -1110,10 +1132,429 @@ async def test_worker_task_response_redacts_locked_policy_hashes( "locked_post_submit_checker_policy_version", "locked_post_submit_checker_policy_hash", "locked_post_submit_checker_policy_body", + "source_ref", + "source_payload_hash", + "import_batch_id", + "external_task_id", + "created_by", + "assigned_to", ): assert internal_field not in body +async def test_task_context_apis_return_worker_requirements_and_operator_provenance( + task_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + project = await create_active_project(task_client) + payload = complete_task_payload() + payload["import_batch_id"] = "private-import-batch" + payload["external_task_id"] = "private-external-task" + started_task = await create_started_task(task_client, project["id"], monkeypatch, payload=payload) + + work_context = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/work-context", + headers=auth_headers(), + ) + assert work_context.status_code == 200, work_context.text + work_body = work_context.json() + assert work_body["task"]["locked_guide_version"] == "v1" + assert work_body["guide"]["version"] == "v1" + assert "# Task Guide v1" in work_body["guide"]["content_markdown"] + assert work_body["payment_policy"]["base_amount"] == "25.00" + assert work_body["lifecycle"]["can_submit"] is True + worker_context_json = json.dumps(work_body, sort_keys=True) + for internal_field in ( + "locked_guide_source_snapshot_hash", + "locked_effective_project_submission_artifact_policy_hash", + "locked_pre_submit_checker_bundle_hash", + "compiled_bundle", + "checker_configs", + ): + assert internal_field not in worker_context_json + for private_field in ( + "source_ref", + "source_payload_hash", + "import_batch_id", + "external_task_id", + "created_by", + "assigned_to", + ): + assert private_field not in work_body["task"] + + requirements = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/submission-requirements", + headers=auth_headers(), + ) + assert requirements.status_code == 200, requirements.text + requirements_body = requirements.json() + assert requirements_body["guide_version"] == "v1" + assert requirements_body["required_packet_fields"] == [ + "summary", + "package_hash", + "artifact_hash_manifest", + "worker_attestation", + ] + assert requirements_body["required_artifacts"] == [ + { + "key": "answer", + "path": "answer.md", + "hash_required": True, + "required": True, + "description": "Main task answer.", + } + ] + assert requirements_body["required_evidence"] == [ + { + "key": "checker_log", + "label": "checker log", + "hash_required": True, + "required": True, + "description": "Evidence used by the reviewer.", + } + ] + assert requirements_body["artifact_hash_algorithm"] == "sha256" + assert set(requirements_body["allowed_storage_schemes"]) == {"local", "s3", "r2"} + assert requirements_body["storage_reference_rules"]["credentials_allowed"] is False + assert requirements_body["storage_reference_rules"]["query_strings_allowed"] is False + requirements_json = json.dumps(requirements_body, sort_keys=True) + for internal_field in ( + "source_snapshot_hash", + "compiled_bundle", + "checker_configs", + "celery", + ): + assert internal_field not in requirements_json + assert "source" not in requirements_body["forbidden_artifacts"][0] + + worker_locked_context = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/locked-context", + headers=auth_headers(), + ) + assert worker_locked_context.status_code == 403 + + set_dev_actor(monkeypatch, roles="project_manager", subject="project-manager-subject") + locked_context = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/locked-context", + headers=auth_headers(), + ) + assert locked_context.status_code == 200, locked_context.text + locked_body = locked_context.json() + assert locked_body["locked_guide_version"] == "v1" + assert locked_body["locked_guide_source_snapshot_hash"].startswith("sha256:") + assert locked_body[ + "locked_effective_project_submission_artifact_policy_hash" + ].startswith("sha256:") + assert locked_body["locked_pre_submit_checker_bundle_hash"].startswith("sha256:") + assert locked_body["locked_post_submit_checker_policy_hash"].startswith("sha256:") + assert locked_body["locked_post_submit_checker_policy_body_summary"][ + "required_checkers" + ] == ["check_policy_context_present"] + + +async def test_ready_worker_work_context_omits_private_task_source_fields( + task_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + project = await create_active_project(task_client) + payload = complete_task_payload() + payload["import_batch_id"] = "ready-private-import" + payload["external_task_id"] = "ready-private-external" + ready_task = await create_ready_task(task_client, project["id"], payload) + await seed_worker_profile("worker-one") + set_dev_actor(monkeypatch, roles="worker", subject="worker-one") + + response = await task_client.get( + f"/api/v1/tasks/{ready_task['id']}/work-context", + headers=auth_headers(), + ) + + assert response.status_code == 200, response.text + body = response.json() + assert body["lifecycle"]["next_actions"] == ["claim"] + for private_field in ( + "source_ref", + "source_payload_hash", + "import_batch_id", + "external_task_id", + "created_by", + "assigned_to", + ): + assert private_field not in body["task"] + + +async def test_work_context_uses_stamped_policy_values_after_same_version_policy_mutation( + task_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + project = await create_active_project(task_client) + started_task = await create_started_task(task_client, project["id"], monkeypatch) + before_response = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/work-context", + headers=auth_headers(), + ) + assert before_response.status_code == 200, before_response.text + before = before_response.json() + + async with db_session.get_session_factory()() as session: + review_policy = await session.scalar( + select(ReviewPolicy).where( + ReviewPolicy.project_id == project["id"], + ReviewPolicy.guide_version == "v1", + ) + ) + revision_policy = await session.scalar( + select(RevisionPolicy).where( + RevisionPolicy.project_id == project["id"], + RevisionPolicy.guide_version == "v1", + ) + ) + payment_policy = await session.scalar( + select(PaymentPolicy).where( + PaymentPolicy.project_id == project["id"], + PaymentPolicy.guide_version == "v1", + ) + ) + assert review_policy is not None + assert revision_policy is not None + assert payment_policy is not None + review_policy.requires_second_review = True + review_policy.allowed_decisions = ["reject"] + revision_policy.max_revision_rounds = 1 + revision_policy.revision_deadline_hours = 1 + payment_policy.base_amount = Decimal("999.00") + payment_policy.currency = "EUR" + payment_policy.payout_type = "manual" + await session.commit() + + after_response = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/work-context", + headers=auth_headers(), + ) + + assert after_response.status_code == 200, after_response.text + after = after_response.json() + assert after["review_policy"] == before["review_policy"] + assert after["revision_policy"] == before["revision_policy"] + assert after["payment_policy"] == before["payment_policy"] + assert after["payment_policy"]["base_amount"] == "25.00" + assert after["payment_policy"]["currency"] == "USD" + assert after["payment_policy"]["payout_type"] == "fixed" + + +async def test_task_context_apis_fail_closed_when_locked_context_is_missing( + task_client: AsyncClient, +) -> None: + project = await create_active_project(task_client) + task = await create_draft_task(task_client, project["id"]) + + response = await task_client.get( + f"/api/v1/tasks/{task['id']}/submission-requirements", + headers=auth_headers(), + ) + + assert response.status_code == 422 + assert response.json()["code"] == "task_locked_context_invalid" + assert "locked_guide_version" in response.json()["details"]["missing_fields"] + + +@pytest.mark.parametrize( + "mutation", + [ + "source_snapshot_manifest", + "effective_policy_body", + "pre_submit_bundle", + "post_submit_body", + ], +) +async def test_task_context_apis_fail_closed_on_stale_locked_context_rows( + task_client: AsyncClient, + mutation: str, +) -> None: + project = await create_active_project(task_client) + ready_task = await create_ready_task(task_client, project["id"]) + + async with db_session.get_session_factory()() as session: + persisted_task = await session.get(WorkstreamTask, ready_task["id"]) + assert persisted_task is not None + if mutation == "source_snapshot_manifest": + snapshot = await session.get( + GuideSourceSnapshot, + persisted_task.locked_guide_source_snapshot_id, + ) + assert snapshot is not None + snapshot.manifest_json = {**snapshot.manifest_json, "tampered": True} + elif mutation == "effective_policy_body": + effective_policy = await session.get( + EffectiveProjectSubmissionArtifactPolicy, + persisted_task.locked_effective_project_submission_artifact_policy_id, + ) + assert effective_policy is not None + effective_policy.effective_policy = { + **effective_policy.effective_policy, + "required_artifacts": [], + } + elif mutation == "pre_submit_bundle": + pre_submit_policy = await session.get( + PreSubmitCheckerPolicy, + persisted_task.locked_pre_submit_checker_policy_id, + ) + assert pre_submit_policy is not None + pre_submit_policy.compiled_bundle = { + **pre_submit_policy.compiled_bundle, + "rules": [], + } + else: + persisted_task.locked_post_submit_checker_policy_body = { + **persisted_task.locked_post_submit_checker_policy_body, + "blocking_severities": [], + } + await session.commit() + + response = await task_client.get( + f"/api/v1/tasks/{ready_task['id']}/work-context", + headers=auth_headers(), + ) + + assert response.status_code == 422 + assert response.json()["code"] == "task_locked_context_invalid" + + +async def test_submission_requirements_fail_closed_on_hash_consistent_malformed_policy_shape( + task_client: AsyncClient, +) -> None: + project = await create_active_project(task_client) + async with db_session.get_session_factory()() as session: + effective_policy = await session.scalar( + select(EffectiveProjectSubmissionArtifactPolicy).where( + EffectiveProjectSubmissionArtifactPolicy.project_id == project["id"], + EffectiveProjectSubmissionArtifactPolicy.guide_version == "v1", + EffectiveProjectSubmissionArtifactPolicy.lifecycle_status == "approved", + ) + ) + assert effective_policy is not None + pre_submit_policy = await session.scalar( + select(PreSubmitCheckerPolicy).where( + PreSubmitCheckerPolicy.effective_policy_id == effective_policy.id, + PreSubmitCheckerPolicy.lifecycle_status == "compiled", + ) + ) + assert pre_submit_policy is not None + replacement_checker_names = list(pre_submit_policy.checker_names) + replacement_checker_configs = dict(pre_submit_policy.checker_configs) + compiler_version = pre_submit_policy.compiler_version + await session.delete(pre_submit_policy) + await session.flush() + + malformed_policy = { + **effective_policy.effective_policy, + "schema_version": ["not", "a", "string"], + } + malformed_policy_hash = canonical_json_hash(malformed_policy) + malformed_bundle = { + **pre_submit_policy.compiled_bundle, + "effective_policy_hash": malformed_policy_hash, + } + malformed_bundle_hash = canonical_json_hash(malformed_bundle) + + effective_policy.effective_policy = malformed_policy + effective_policy.effective_policy_hash = malformed_policy_hash + session.add( + PreSubmitCheckerPolicy( + id=str(uuid4()), + project_id=effective_policy.project_id, + guide_id=effective_policy.guide_id, + guide_version=effective_policy.guide_version, + source_snapshot_id=effective_policy.source_snapshot_id, + source_snapshot_hash=effective_policy.source_snapshot_hash, + effective_policy_id=effective_policy.id, + effective_policy_hash=malformed_policy_hash, + lifecycle_status="compiled", + compiler_version=compiler_version, + compiled_bundle=malformed_bundle, + compiled_bundle_hash=malformed_bundle_hash, + checker_names=replacement_checker_names, + checker_configs=replacement_checker_configs, + created_by="test", + ) + ) + await session.commit() + + ready_task = await create_ready_task(task_client, project["id"]) + response = await task_client.get( + f"/api/v1/tasks/{ready_task['id']}/submission-requirements", + headers=auth_headers(), + ) + + assert response.status_code == 422 + body = response.json() + assert body["code"] == "task_locked_context_invalid" + assert body["details"]["field"] == "effective_policy.schema_version" + + +async def test_task_context_apis_use_v1_locked_requirements_after_v2_activation( + task_client: AsyncClient, + monkeypatch: pytest.MonkeyPatch, +) -> None: + project = await create_active_project(task_client) + started_task = await create_started_task(task_client, project["id"], monkeypatch) + + v1_requirements = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/submission-requirements", + headers=auth_headers(), + ) + assert v1_requirements.status_code == 200, v1_requirements.text + + set_dev_actor(monkeypatch, roles="project_manager", subject="project-manager-subject") + guide_v2 = await task_client.post( + f"/api/v1/projects/{project['id']}/guides", + headers=auth_headers(), + json=complete_guide_payload("v2"), + ) + assert guide_v2.status_code == 201, guide_v2.text + policy_v2 = policy_body_for_task_tests() + policy_v2["required_artifacts"] = [ + { + "key": "v2_answer", + "path": "v2-answer.md", + "hash_required": True, + "required": True, + "description": "New v2 artifact.", + } + ] + await create_policy_bundle_for_guide( + task_client, + project["id"], + guide_v2.json()["id"], + policy_v2, + ) + activate_v2 = await task_client.post( + f"/api/v1/projects/{project['id']}/guides/{guide_v2.json()['id']}/activate", + headers=auth_headers(), + ) + assert activate_v2.status_code == 200, activate_v2.text + assert activate_v2.json()["guide"]["version"] == "v2" + + set_dev_actor(monkeypatch, roles="worker", subject="worker-one") + work_context = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/work-context", + headers=auth_headers(), + ) + requirements = await task_client.get( + f"/api/v1/tasks/{started_task['id']}/submission-requirements", + headers=auth_headers(), + ) + + assert work_context.status_code == 200, work_context.text + assert requirements.status_code == 200, requirements.text + assert work_context.json()["guide"]["version"] == "v1" + assert requirements.json()["guide_version"] == "v1" + assert requirements.json()["required_artifacts"] == v1_requirements.json()[ + "required_artifacts" + ] + assert requirements.json()["required_artifacts"][0]["path"] == "answer.md" + + async def test_tasks_under_same_active_guide_share_project_pre_submit_checker( task_client: AsyncClient, ) -> None: @@ -1770,7 +2211,17 @@ async def test_assigned_worker_submits_v1_and_task_moves_to_submitted( task = await task_client.get(f"/api/v1/tasks/{started_task['id']}", headers=auth_headers()) assert task.status_code == 200, task.text - assert task.json()["status"] == "submitted" + task_body = task.json() + assert task_body["status"] == "submitted" + for internal_field in ( + "source_ref", + "source_payload_hash", + "import_batch_id", + "external_task_id", + "created_by", + "assigned_to", + ): + assert internal_field not in task_body audit = await task_client.get( f"/api/v1/tasks/{started_task['id']}/audit-events", diff --git a/docs/architecture_data_model.md b/docs/architecture_data_model.md index ac4351e..5941dab 100644 --- a/docs/architecture_data_model.md +++ b/docs/architecture_data_model.md @@ -613,6 +613,14 @@ the work is split into another project/guide. The task stores `PreSubmitCheckerPolicy.compiled_bundle_hash`; it does not own a newly derived policy or newly compiled checker. +Task context APIs read this already-stamped context. `work-context` and +`submission-requirements` return task-visible worker-safe guide and requirement +projections from the locked rows. `locked-context` is available only to +token-authenticated `admin` and `project_manager` actors and exposes the full +locked source snapshot, effective policy, pre-submit checker, post-submit +checker, review, revision, and payment provenance. None of these reads +recompute from the current active guide. + Approval creates a project-scoped `PreSubmitCheckerPolicy` row with lifecycle status `compiled`. The trusted compiler writes the immutable `compiled_bundle` JSON and `compiled_bundle_hash` in the same approval path. The compiled bundle diff --git a/docs/architecture_system_architecture.md b/docs/architecture_system_architecture.md index 35653a2..291ba74 100644 --- a/docs/architecture_system_architecture.md +++ b/docs/architecture_system_architecture.md @@ -155,6 +155,9 @@ Owns: - task requirements - task acceptance criteria - queue views +- worker-safe task work context +- worker-safe submission requirements derived from locked effective project policy +- locked task provenance for `admin` and `project_manager` actors ### Submission Service diff --git a/docs/glossary.md b/docs/glossary.md index 1578d24..64ce3ef 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -116,6 +116,28 @@ The worker-facing domain error code returned when a submission-create attempt is A unit of work inside a project. +## Task Work Context + +The worker-safe API projection of a task's locked guide, project summary, +review policy, revision policy, payment policy, and lifecycle state. It is read +from the task's stamped locked context and does not expose source snapshot +hashes, private source/import refs, compiled checker bundles, checker configs, +Celery ids, or setup errors. + +## Task Submission Requirements + +The worker-safe API projection of the task's locked effective project +submission artifact policy. It tells the worker the exact required artifacts, +evidence keys, forbidden artifact rules, storage reference rules, packaging +rules, hash algorithm, size limits, and attestation terms before submission. + +## Task Locked Context + +The `admin` and `project_manager` API projection of a task's locked guide and +policy provenance, including guide source snapshot id/hash, effective policy +id/hash, pre-submit checker policy id/hash, post-submit checker policy +id/hash/body summary, and review, revision, and payment policy versions. + ## Task Contract The normalized task fields required for Workstream to screen, assign, check, review, pay, and audit work. diff --git a/docs/operations_project_operating_manual.md b/docs/operations_project_operating_manual.md index e817644..f610e43 100644 --- a/docs/operations_project_operating_manual.md +++ b/docs/operations_project_operating_manual.md @@ -84,6 +84,14 @@ submission artifact policy hash, and project pre-submit checker bundle hash. A task cannot move to `READY` until the task contract is complete, the guide version is locked, submission artifact requirements are clear, checker/review/revision/payment policy versions are locked, and a release decision is recorded. +After screening and release, workers use +`GET /api/v1/tasks/{task_id}/work-context` for the locked guide and lifecycle +context and `GET /api/v1/tasks/{task_id}/submission-requirements` for the exact +artifact, evidence, storage, packaging, hash, and attestation requirements. +Actors with the `admin` or `project_manager` role use +`GET /api/v1/tasks/{task_id}/locked-context` when support or a live API drill +needs full locked provenance without database inspection. + ### Submission Quality Gate A submission cannot move to human review until required checkers run against the exact submission version and artifact hashes. High-severity failures return to the worker when submission-caused; platform infrastructure failures remain in checker retry handling or audited admin/project manager intervention. diff --git a/docs/roadmap_status.md b/docs/roadmap_status.md index 67ddff2..426b2cd 100644 --- a/docs/roadmap_status.md +++ b/docs/roadmap_status.md @@ -44,6 +44,7 @@ Current phase: Week 3 review and revision preparation. - 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. +- Chunk 12 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. ## Review Tracks Closed @@ -63,11 +64,9 @@ 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 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 - inspection. +- Chunk 13 task context APIs are in progress to expose worker-safe work + context, exact submission requirements, and operator-only locked provenance + before rerunning the Terminal Benchmark drill. ## Pending Before Pilot @@ -84,9 +83,9 @@ 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 policy visibility plus the following flow: +The script runs migrations forward and exercises project policy visibility plus task context APIs across the following flow: -`Project -> Guide -> Task -> Screening -> Ready -> Claim -> Start -> Submit -> Lock submission` +`Project -> Guide -> Task -> Screening -> Ready -> Work context -> Submission requirements -> Locked context -> Claim -> Start -> Submit -> Lock submission` ## Week 2 Real API Drill From 071f8484053c97ca56dd6fe6dbf18dca3f781cb8 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 8 Jul 2026 00:06:31 +0100 Subject: [PATCH 2/5] Add WS-POL-001-13 review evidence --- .../WS-POL-001-13-internal-review-evidence.md | 90 ++++++++++++++++ .../reviews/WS-POL-001-13-pr-trust-bundle.md | 101 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md new file mode 100644 index 0000000..cf12566 --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md @@ -0,0 +1,90 @@ +# Internal Review Evidence: WS-POL-001-13 + +## Chunk + +WS-POL-001-13 + +open sub-agent sessions: none + +valid findings addressed: yes + +## Reviewed Revision + +Reviewed code SHA: 449b68093fb62d54a64adab32517cca33f17cb59 + +Reviewed at: 2026-07-07T23:04:55Z + +Reviewer run IDs: senior-engineering-final-019f3ecb-6123-7581-9b08-a1812b789d57, qa-test-final-019f3ecb-a018-7e02-81a7-3a7fd102a4ba, security-auth-final-019f3ecb-d5a9-7800-94b3-e4358974f0cd, product-ops-final-019f3ecc-0e2d-7e90-873a-af311a10fd59, architecture-final-019f3ecc-5034-7da0-a53e-1cb66880535b, docs-final-019f3ecc-87cf-75a3-b806-d694cf0f8114, reuse-dedup-final-019f3ed0-bdf1-7ae3-99ae-a33db36dea1c, test-delta-final-019f3ed0-f9d9-7590-a829-de92b9296105 + +After the reviewed SHA, only evidence files changed. + +## Reviewed Change + +Scope: + +- Adds `GET /api/v1/tasks/{task_id}/work-context` for worker-safe task, project, guide, payment, review/revision version, and lifecycle context. +- Adds `GET /api/v1/tasks/{task_id}/submission-requirements` for exact locked submission packet fields, artifacts, evidence, storage rules, packaging, hash algorithm, and attestation terms. +- Adds `GET /api/v1/tasks/{task_id}/locked-context` for `admin` and `project_manager` locked provenance. +- Validates task-stamped guide source snapshot, effective project policy, project pre-submit checker, post-submit checker, review, revision, and payment context before response projection. +- Keeps worker-facing responses free of private source/import refs, assignment provenance, raw compiled bundles, checker configs, setup internals, source snapshot hashes, and policy provenance hashes. +- Redacts existing worker-visible `GET /tasks/{task_id}` responses so workers cannot bypass `work-context` redaction. +- Fails closed with structured `task_locked_context_invalid` when locked context is missing, stale, hash-mismatched, or hash-consistent but malformed. +- Extends the real API contract drill to call the three task-context APIs through HTTP. + +## Reviewer Results + +| Reviewer | Result | Blocking findings | Notes | +|---|---:|---|---| +| senior engineering | PASS | None | Confirmed router/service boundaries, route-specific domain schemas, fail-closed malformed policy behavior, and worker redaction. | +| QA/test | PASS | None | Confirmed acceptance criteria, exact requirements including `package_hash`, v1 lock after v2 activation, and live API drill coverage. | +| security/auth | PASS WITH LOW RISKS | None | Confirmed no private values/hashes/configs leak. Low residual risk: worker-facing structured errors include internal field names without values. | +| product/ops | PASS | None | Confirmed worker and operator workflows, existing task read redaction, and no payment/reputation behavior expansion. | +| architecture | PASS | None | Confirmed stamped-context reads, no current-guide recompute, operator-only locked context, and allowed-file scope. | +| docs | PASS | None | Confirmed glossary, architecture, operations, and roadmap docs align with the new API surfaces. | +| reuse/dedup | PASS WITH LOW RISKS | None | Low residual risk: required packet field constants mirror `SubmissionCreate`; future shared extraction can avoid drift. | +| test delta | PASS WITH LOW RISKS | None | Confirmed strengthened tests and no skips/removals. Low residual risk: locked-context response shape is sampled rather than fully enumerated. | + +## Valid Findings Addressed + +- Replaced broad worker `TaskResponse` usage in `work-context` with `TaskWorkerTaskContext`. +- Removed private source/import/creator/assignment fields from worker `work-context` task data. +- Stopped exposing mutable same-version review/revision/payment row details in worker work context; response now uses stamped guide/payment values and guide-version references. +- Added route-specific domain error schemas so submission creation documents only `pre_submission_checker_failed` and task-context endpoints document only `task_locked_context_invalid`. +- Added `package_hash` to the exact worker-facing required packet field list. +- Removed `forbidden_artifacts.source` from worker-facing submission requirements. +- Redacted private source/import/creator/assignment fields from existing worker-visible `GET /tasks/{task_id}` responses. +- Added explicit locked effective-policy projection validation so malformed hash-consistent policy shapes return structured `task_locked_context_invalid`. +- Centralized locked-context required fields and reused the shared missing-field helper. +- Added tests for worker-safe redaction, operator-only locked context, exact submission requirements, malformed policy fail-closed behavior, same-version policy mutation, and v1 locked context after v2 guide activation. +- Updated the real API contract drill to call `work-context`, `submission-requirements`, and `locked-context`. + +## 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 +git diff --check +rg -n "task binding|task-owned policy|direct DB|DB inspection" docs backend examples .agent-loop +cd backend && .venv/bin/pytest tests/test_tasks.py::test_worker_task_response_redacts_locked_policy_hashes tests/test_tasks.py::test_assigned_worker_submits_v1_and_task_moves_to_submitted tests/test_tasks.py::test_task_context_apis_return_worker_requirements_and_operator_provenance tests/test_tasks.py::test_submission_requirements_fail_closed_on_hash_consistent_malformed_policy_shape -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_tasks.py +``` + +Results: + +- Ruff: passed. +- Stale wording scan: passed. +- Markdown link check: passed for 7 changed Markdown files. +- Diff whitespace check: passed. +- Stale model/no-DB scan: expected references only in chunk contracts, status/decision docs, and the documented future no-DB proof path; no active per-task policy wording was found. +- Focused task context and worker redaction regressions: 4 passed in 43.18s. +- API contract real API E2E: passed and called `work-context`, `submission-requirements`, and `locked-context` through HTTP. +- Full task suite: 81 passed in 743.67s. + +## Remaining Risks + +- Worker-facing locked-context errors include internal field names such as locked context field identifiers. They do not expose values, hashes, private refs, compiled bundles, checker configs, or source material. A future UX/security hardening pass can map worker errors to public categories while preserving exact operator diagnostics. +- `SUBMISSION_CREATE_REQUIRED_PACKET_FIELDS` mirrors `SubmissionCreate` required fields. It is centralized for this projection, but a future cleanup can derive it from schema metadata or move the contract beside the schema to reduce drift. +- The task context loader repeats some locked pre-submit validation concepts also used by checker execution. The current direction is acceptable because this API needs task-specific domain errors and richer context; if another reader appears, extract a shared locked-context validator. +- The next Terminal Benchmark no-DB drill is still assigned to `WS-POL-001-14`. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md new file mode 100644 index 0000000..aff1d05 --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md @@ -0,0 +1,101 @@ +# PR Trust Bundle: WS-POL-001-13 + +## Intent + +Expose task context and submission requirements through authorized HTTP APIs so workers and operators no longer need to infer locked requirements from failures or inspect Postgres during the next live drill. + +## Scope + +Implemented APIs 8-10 only: + +- `GET /api/v1/tasks/{task_id}/work-context` +- `GET /api/v1/tasks/{task_id}/submission-requirements` +- `GET /api/v1/tasks/{task_id}/locked-context` + +## Design + +Task context APIs read the task's already-stamped locked context. They do not recompute from the current active guide or current project policy. + +Worker-facing APIs return: + +- safe task summary +- project and locked guide summary +- stamped payment terms +- review/revision guide-version references +- lifecycle next actions +- exact submission packet fields, including `package_hash` +- artifact, evidence, storage, packaging, hash, and attestation requirements + +Operator-only `locked-context` returns full locked provenance for `admin` and `project_manager`. + +The implementation also redacts existing worker-visible task reads so `GET /tasks/{task_id}` cannot bypass the worker-safe projection. + +## Authorization + +- `work-context`: existing task visibility for `admin`, `project_manager`, or eligible/assigned worker. +- `submission-requirements`: existing task visibility for `admin`, `project_manager`, or eligible/assigned worker. +- `locked-context`: `admin` or `project_manager` only. +- Persisted actor profiles do not grant route authorization; token-derived roles remain the route gate. + +## 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 +git diff --check +cd backend && .venv/bin/pytest tests/test_tasks.py::test_worker_task_response_redacts_locked_policy_hashes tests/test_tasks.py::test_assigned_worker_submits_v1_and_task_moves_to_submitted tests/test_tasks.py::test_task_context_apis_return_worker_requirements_and_operator_provenance tests/test_tasks.py::test_submission_requirements_fail_closed_on_hash_consistent_malformed_policy_shape -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_tasks.py +``` + +Key results: + +- Focused task-context and worker-redaction regressions: 4 passed. +- API contract real API E2E: passed with the three new task-context endpoints. +- Full task suite: 81 passed in 743.67s. +- Stale wording and Markdown links: passed. + +## Internal Review + +Internal review evidence: + +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md` + +Reviewed code SHA: `449b68093fb62d54a64adab32517cca33f17cb59` + +| Reviewer | Result | Blocking findings | +|---|---:|---| +| senior engineering | PASS | None | +| QA/test | PASS | None | +| security/auth | PASS WITH LOW RISKS | None | +| product/ops | PASS | None | +| architecture | PASS | None | +| docs | PASS | None | +| reuse/dedup | PASS WITH LOW RISKS | None | +| test delta | PASS WITH LOW RISKS | None | + +All sub-agent sessions were closed. + +## External Review + +External review has not run yet. CodeRabbit and GitHub checks should review this PR after it is opened. + +## Human Review Focus + +- Confirm worker-facing requirements are complete enough to submit without exposing internal checker authority. +- Confirm existing `GET /tasks/{task_id}` redaction is acceptable for workers. +- Confirm `locked-context` exposes the right operator provenance and remains restricted to `admin` and `project_manager`. +- Confirm `task_locked_context_invalid` is the right public error code for missing/stale/malformed task locked context. + +## Remaining Risks + +- Worker-facing locked-context errors include internal field names, but no values, hashes, source refs, bundles, or configs. +- Required packet field constants mirror `SubmissionCreate` and should be kept in sync until a shared schema-derived helper is extracted. +- Full no-DB Terminal Benchmark proof remains assigned to `WS-POL-001-14`. + +## Human Merge Ownership + +Only the user can approve and merge this PR. Codex must not merge it without explicit user approval for that specific PR. From e5c33ef70773c48971b292a26a533dbb284461a7 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 8 Jul 2026 03:06:11 +0100 Subject: [PATCH 3/5] Address task context review feedback --- .agent-loop/REVIEW_LOG.md | 32 +++ .../WS-POL-001-13-external-review-response.md | 59 ++++ .../reviews/WS-POL-001-13-pr-trust-bundle.md | 254 ++++++++++++++---- backend/tests/test_tasks.py | 3 +- 4 files changed, 302 insertions(+), 46 deletions(-) create mode 100644 .agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-external-review-response.md diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index a6eac9d..c9b89ef 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -337,3 +337,35 @@ no-backward-compatibility chunk decision. Next gate: rerun the Terminal Benchmark live API drill through real HTTP calls using `POST /api/v1/workers/me/profile`, then review findings before starting the next product chunk. + +## WS-POL-001-13 + +Status: PR #77 open for human review. + +Branch: `codex/ws-pol-001-13-task-context-apis` + +Reviewed implementation SHA: `449b68093fb62d54a64adab32517cca33f17cb59` + +Required reviewer tracks: + +- senior engineering +- QA/test +- security/auth +- product/ops +- architecture +- docs +- reuse/dedup +- test delta + +Result: internal review passed; CodeRabbit external review produced one valid +test-maintainability nitpick and one PR-description warning, both addressed. + +Scope: worker-safe task work context, exact submission requirements, and +operator-only locked task provenance APIs. + +Evidence: `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md` + +External review response: `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-external-review-response.md` + +Next gate: wait for CodeRabbit and GitHub checks to rerun on the external +review response commit, then wait for the user's merge decision. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-external-review-response.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-external-review-response.md new file mode 100644 index 0000000..b4e3c4d --- /dev/null +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-external-review-response.md @@ -0,0 +1,59 @@ +# External Review Response: WS-POL-001-13 + +## PR + +PR #77: https://github.com/Flow-Research/workstream/pull/77 + +## Chunk + +`WS-POL-001-13` + +## Source + +CodeRabbit + +## Summary + +CodeRabbit reported one valid test-maintainability nitpick and one PR +description warning. No product, architecture, security, authorization, or API +contract changes were requested. + +## External Findings + +| Source | Finding | Severity | Status | Response | +|---|---|---:|---:|---| +| CodeRabbit | The malformed locked-context regression test read `pre_submit_policy.compiled_bundle` after `session.delete()` and `flush()`. | Trivial | Addressed | Captured `original_compiled_bundle` before deleting the ORM row and used the saved bundle when constructing the malformed checker bundle. | +| CodeRabbit | The PR description omitted explicit template sections for chunk, goal, changed behavior, acceptance proof, and test delta. | Warning | Addressed | Reshaped the PR trust bundle and PR body around the required review sections. | + +## Fix plan + +- Update `backend/tests/test_tasks.py` so the malformed locked-context test does + not dereference a deleted ORM instance. +- Update the PR trust bundle with explicit review-template headings. +- Record this external review response separately from internal review + evidence. +- Update the GitHub PR body from the corrected trust bundle. + +## Out-of-scope items to defer + +None. + +## Evidence after fixes + +```bash +cd backend && .venv/bin/pytest tests/test_tasks.py::test_submission_requirements_fail_closed_on_hash_consistent_malformed_policy_shape -q +cd backend && .venv/bin/python -m ruff check tests/test_tasks.py +python3 scripts/check_stale_workstream_wording.py +python3 scripts/check_markdown_links.py +git diff --check +``` + +Result summary: + +```text +Focused pytest: 1 passed +Ruff: All checks passed +Stale wording: passed +Markdown links: passed for changed Markdown files +git diff --check: passed +``` diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md index aff1d05..bc5ada3 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md @@ -1,10 +1,26 @@ # PR Trust Bundle: WS-POL-001-13 -## Intent +## Chunk -Expose task context and submission requirements through authorized HTTP APIs so workers and operators no longer need to infer locked requirements from failures or inspect Postgres during the next live drill. +`WS-POL-001-13` - Task context and submission requirement APIs. -## Scope +## Goal + +Expose task context and submission requirements through authorized HTTP APIs so +workers and operators no longer need to infer locked requirements from failures +or inspect Postgres during the next live drill. + +## Human-approved intent + +Chunk contract: + +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/chunks/WS-POL-001-13-task-context-submission-requirements-apis.md` + +The user approved exposing worker-safe task context, exact submission +requirements, and operator-only locked provenance APIs before rerunning the +Terminal Benchmark drill. + +## What changed Implemented APIs 8-10 only: @@ -12,9 +28,21 @@ Implemented APIs 8-10 only: - `GET /api/v1/tasks/{task_id}/submission-requirements` - `GET /api/v1/tasks/{task_id}/locked-context` -## Design +The implementation also redacts existing worker-visible task reads so +`GET /tasks/{task_id}` cannot bypass the worker-safe projection. + +## Why it changed + +The Terminal Benchmark drill showed that setup and task policy state was still +too dependent on direct database inspection. This chunk exposes the +already-locked task context through authorized APIs while preserving the rule +that runtime uses stamped policy references, not recomputed current project +state. + +## Design chosen -Task context APIs read the task's already-stamped locked context. They do not recompute from the current active guide or current project policy. +Task context APIs read the task's already-stamped locked context. They do not +recompute from the current active guide or current project policy. Worker-facing APIs return: @@ -26,20 +54,82 @@ Worker-facing APIs return: - exact submission packet fields, including `package_hash` - artifact, evidence, storage, packaging, hash, and attestation requirements -Operator-only `locked-context` returns full locked provenance for `admin` and `project_manager`. +Operator-only `locked-context` returns full locked provenance for `admin` and +`project_manager`. -The implementation also redacts existing worker-visible task reads so `GET /tasks/{task_id}` cannot bypass the worker-safe projection. +Authorization: -## Authorization - -- `work-context`: existing task visibility for `admin`, `project_manager`, or eligible/assigned worker. -- `submission-requirements`: existing task visibility for `admin`, `project_manager`, or eligible/assigned worker. +- `work-context`: existing task visibility for `admin`, `project_manager`, or + eligible/assigned worker. +- `submission-requirements`: existing task visibility for `admin`, + `project_manager`, or eligible/assigned worker. - `locked-context`: `admin` or `project_manager` only. -- Persisted actor profiles do not grant route authorization; token-derived roles remain the route gate. +- Persisted actor profiles do not grant route authorization; token-derived + roles remain the route gate. + +## Alternatives rejected + +- Recomputing task requirements from the current active guide at read time: + rejected because tasks must expose the stamped context they already locked. +- Exposing compiled bundles, checker configs, source refs, or private policy + internals to workers: rejected because worker-facing APIs should show what + must be submitted, not checker authority. +- Continuing to inspect Postgres during the live drill: rejected because the + next drill must prove the public/operator API surface. + +## Scope control + +### Allowed files changed + +- `backend/app/modules/tasks/router.py` +- `backend/app/modules/tasks/schemas.py` +- `backend/app/modules/tasks/service.py` +- `backend/scripts/api_contract_e2e.py` +- `backend/tests/test_tasks.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/STATUS.md` +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md` +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-external-review-response.md` +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md` + +### Files outside scope + +- `.agent-loop/REVIEW_LOG.md`: governance tracking update added after external + review so the repository-level review log points to the separate CodeRabbit + response artifact. It does not change product behavior, tests, CI, or chunk + implementation scope. + +## Product Behavior + +- [ ] No Workstream product behavior changed. +- [x] Product behavior changed and is explained here: + +Workers and operators can now inspect task context and submission requirements +through authorized APIs. Worker responses are intentionally redacted. Operators +with `admin` or `project_manager` can inspect full locked provenance. + +## Acceptance criteria proof -## Evidence +- [x] Worker-safe task context API exists and reads locked context. + Evidence: `test_task_context_apis_return_worker_requirements_and_operator_provenance`. +- [x] Submission requirements API returns exact packet, artifact, evidence, + storage, packaging, hash, and attestation requirements. + Evidence: `test_task_context_apis_return_worker_requirements_and_operator_provenance` + and `scripts/api_contract_e2e.py`. +- [x] Operator locked-context API is restricted to `admin` and + `project_manager`. + Evidence: `test_task_context_apis_return_worker_requirements_and_operator_provenance`. +- [x] Missing, stale, or malformed locked context fails closed. + Evidence: `test_submission_requirements_fail_closed_on_hash_consistent_malformed_policy_shape`. +- [x] Worker-facing task reads remain redacted. + Evidence: `test_worker_task_response_redacts_locked_policy_hashes`. -Commands passed: +## Tests/checks run ```bash cd backend && .venv/bin/python -m ruff check app tests scripts @@ -49,53 +139,127 @@ git diff --check cd backend && .venv/bin/pytest tests/test_tasks.py::test_worker_task_response_redacts_locked_policy_hashes tests/test_tasks.py::test_assigned_worker_submits_v1_and_task_moves_to_submitted tests/test_tasks.py::test_task_context_apis_return_worker_requirements_and_operator_provenance tests/test_tasks.py::test_submission_requirements_fail_closed_on_hash_consistent_malformed_policy_shape -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_tasks.py +cd backend && .venv/bin/pytest tests/test_tasks.py::test_submission_requirements_fail_closed_on_hash_consistent_malformed_policy_shape -q +cd backend && .venv/bin/python -m ruff check tests/test_tasks.py ``` -Key results: +Result summary: + +```text +Focused task-context and worker-redaction regressions: 4 passed +API contract real API E2E: passed with the three new task-context endpoints +Full task suite: 81 passed in 743.67s +CodeRabbit regression pytest: 1 passed +Ruff: passed +Stale wording: passed +Markdown links: passed +git diff --check: passed +``` -- Focused task-context and worker-redaction regressions: 4 passed. -- API contract real API E2E: passed with the three new task-context endpoints. -- Full task suite: 81 passed in 743.67s. -- Stale wording and Markdown links: passed. +## Test delta -## Internal Review +### Tests added -Internal review evidence: +- Added task API coverage for worker-facing work context and submission + requirements. +- Added operator-only locked-context API coverage. +- Added fail-closed locked-context validation coverage. +- Added regression coverage for worker redaction on existing task reads. +- Extended the real API contract drill to call the three new endpoints. -- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md` +### Tests modified + +- Updated `test_submission_requirements_fail_closed_on_hash_consistent_malformed_policy_shape` + after CodeRabbit review to capture the compiled bundle before deleting the + ORM row. + +### Tests removed/skipped + +None. + +## CI integrity + +- [x] Coverage threshold unchanged +- [x] Lint unchanged +- [x] Typecheck unchanged +- [x] No workflow weakening +- [x] No package script weakening +- [x] No unpinned new GitHub Action +- [x] Checkout credential persistence unchanged + +## External review -Reviewed code SHA: `449b68093fb62d54a64adab32517cca33f17cb59` +External review response file: -| Reviewer | Result | Blocking findings | +- `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-external-review-response.md` + +| Source | Status | Notes | |---|---:|---| -| senior engineering | PASS | None | -| QA/test | PASS | None | -| security/auth | PASS WITH LOW RISKS | None | -| product/ops | PASS | None | -| architecture | PASS | None | -| docs | PASS | None | -| reuse/dedup | PASS WITH LOW RISKS | None | -| test delta | PASS WITH LOW RISKS | None | +| CodeRabbit | Addressed locally | One valid test-maintainability nitpick and one PR-description warning were fixed. | +| GitHub checks | Pending rerun | Existing checks passed before this external-review response commit; they must rerun after push. | -All sub-agent sessions were closed. +## Reviewer results -## External Review +Reviewed implementation SHA: `449b68093fb62d54a64adab32517cca33f17cb59` -External review has not run yet. CodeRabbit and GitHub checks should review this PR after it is opened. +Reviewed at: 2026-07-07 through 2026-07-08 -## Human Review Focus +Reviewer run IDs: -- Confirm worker-facing requirements are complete enough to submit without exposing internal checker authority. -- Confirm existing `GET /tasks/{task_id}` redaction is acceptable for workers. -- Confirm `locked-context` exposes the right operator provenance and remains restricted to `admin` and `project_manager`. -- Confirm `task_locked_context_invalid` is the right public error code for missing/stale/malformed task locked context. +- senior engineering: `019f3f6d-441f-7340-9bcc-ef92155d956d` +- QA/test: `019f3f6d-4986-7612-a7a7-9457a2182d0b` +- security/auth: `019f3f6d-4fea-7890-a372-a261d2482b91` +- product/ops: `019f3f6d-5758-79d3-a7d4-3d58933064da` +- docs: `019f3f6d-6062-7590-8358-3bc0766e935d` +- reuse/dedup: original internal review evidence +- test delta: `019f3f6d-685a-7300-82ab-966c3c034976` + +| Reviewer | Result | Blocking findings | Notes | +|---|---:|---|---| +| senior engineering | PASS | None | Confirmed the CodeRabbit test fix is minimal. | +| QA/test | PASS WITH LOW RISKS | None | Confirmed coverage is not weakened. | +| security/auth | PASS | None | Confirmed no auth or data-leakage risk in the delta. | +| product/ops | PASS | None | Confirmed external review stays separate from internal evidence and human merge ownership remains intact. | +| architecture | PASS | None | Original internal review result. | +| CI integrity | N/A - with approved reason | None | No CI/workflow/package-script files changed in the external-review response delta. | +| docs | PASS AFTER FIXES | None | Initial template-format finding was fixed. | +| reuse/dedup | PASS WITH LOW RISKS | None | Original internal review result. | +| test delta | PASS | None | Confirmed the test still proves fail-closed malformed policy behavior. | + +All sub-agent sessions were closed before final reporting. -## Remaining Risks +## Remaining risks -- Worker-facing locked-context errors include internal field names, but no values, hashes, source refs, bundles, or configs. -- Required packet field constants mirror `SubmissionCreate` and should be kept in sync until a shared schema-derived helper is extracted. +- Worker-facing locked-context errors include internal field names, but no + values, hashes, source refs, bundles, or configs. +- Required packet field constants mirror `SubmissionCreate` and should be kept + in sync until a shared schema-derived helper is extracted. - Full no-DB Terminal Benchmark proof remains assigned to `WS-POL-001-14`. -## Human Merge Ownership +## Follow-up work -Only the user can approve and merge this PR. Codex must not merge it without explicit user approval for that specific PR. +- `WS-POL-001-14`: replace public submission lock wording with finalize, + define system actor audit semantics, and rerun the Terminal Benchmark proof + without DB inspection. +- Later cleanup: extract a shared schema-derived helper for required packet + fields if another caller needs the same constants. + +## Human review focus + +Please inspect: + +- Confirm worker-facing requirements are complete enough to submit without + exposing internal checker authority. +- Confirm existing `GET /tasks/{task_id}` redaction is acceptable for workers. +- Confirm `locked-context` exposes the right operator provenance and remains + restricted to `admin` and `project_manager`. +- Confirm `task_locked_context_invalid` is the right public error code for + missing/stale/malformed task locked context. + +## Human ownership + +- [ ] I can explain what changed. +- [ ] I can explain why it changed. +- [ ] I know what could break. +- [ ] I accept the remaining risks. +- [ ] The user explicitly approved this specific PR for merge. diff --git a/backend/tests/test_tasks.py b/backend/tests/test_tasks.py index 93bb610..5b6a31b 100644 --- a/backend/tests/test_tasks.py +++ b/backend/tests/test_tasks.py @@ -1443,6 +1443,7 @@ async def test_submission_requirements_fail_closed_on_hash_consistent_malformed_ replacement_checker_names = list(pre_submit_policy.checker_names) replacement_checker_configs = dict(pre_submit_policy.checker_configs) compiler_version = pre_submit_policy.compiler_version + original_compiled_bundle = dict(pre_submit_policy.compiled_bundle) await session.delete(pre_submit_policy) await session.flush() @@ -1452,7 +1453,7 @@ async def test_submission_requirements_fail_closed_on_hash_consistent_malformed_ } malformed_policy_hash = canonical_json_hash(malformed_policy) malformed_bundle = { - **pre_submit_policy.compiled_bundle, + **original_compiled_bundle, "effective_policy_hash": malformed_policy_hash, } malformed_bundle_hash = canonical_json_hash(malformed_bundle) From f533f1a572a38d4e8ecd34ff6316885c6c6b1016 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 8 Jul 2026 03:09:38 +0100 Subject: [PATCH 4/5] Repair task context review evidence binding --- .agent-loop/REVIEW_LOG.md | 32 ------------------- .../WS-POL-001-13-internal-review-evidence.md | 9 ++++-- .../reviews/WS-POL-001-13-pr-trust-bundle.md | 9 ++---- 3 files changed, 9 insertions(+), 41 deletions(-) diff --git a/.agent-loop/REVIEW_LOG.md b/.agent-loop/REVIEW_LOG.md index c9b89ef..a6eac9d 100644 --- a/.agent-loop/REVIEW_LOG.md +++ b/.agent-loop/REVIEW_LOG.md @@ -337,35 +337,3 @@ no-backward-compatibility chunk decision. Next gate: rerun the Terminal Benchmark live API drill through real HTTP calls using `POST /api/v1/workers/me/profile`, then review findings before starting the next product chunk. - -## WS-POL-001-13 - -Status: PR #77 open for human review. - -Branch: `codex/ws-pol-001-13-task-context-apis` - -Reviewed implementation SHA: `449b68093fb62d54a64adab32517cca33f17cb59` - -Required reviewer tracks: - -- senior engineering -- QA/test -- security/auth -- product/ops -- architecture -- docs -- reuse/dedup -- test delta - -Result: internal review passed; CodeRabbit external review produced one valid -test-maintainability nitpick and one PR-description warning, both addressed. - -Scope: worker-safe task work context, exact submission requirements, and -operator-only locked task provenance APIs. - -Evidence: `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md` - -External review response: `.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-external-review-response.md` - -Next gate: wait for CodeRabbit and GitHub checks to rerun on the external -review response commit, then wait for the user's merge decision. diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md index cf12566..1d5dda7 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md @@ -10,11 +10,11 @@ valid findings addressed: yes ## Reviewed Revision -Reviewed code SHA: 449b68093fb62d54a64adab32517cca33f17cb59 +Reviewed code SHA: 7f0c0ecb2107cb706150f82c71f32255fb99e14e -Reviewed at: 2026-07-07T23:04:55Z +Reviewed at: 2026-07-08T02:09:55Z -Reviewer run IDs: senior-engineering-final-019f3ecb-6123-7581-9b08-a1812b789d57, qa-test-final-019f3ecb-a018-7e02-81a7-3a7fd102a4ba, security-auth-final-019f3ecb-d5a9-7800-94b3-e4358974f0cd, product-ops-final-019f3ecc-0e2d-7e90-873a-af311a10fd59, architecture-final-019f3ecc-5034-7da0-a53e-1cb66880535b, docs-final-019f3ecc-87cf-75a3-b806-d694cf0f8114, reuse-dedup-final-019f3ed0-bdf1-7ae3-99ae-a33db36dea1c, test-delta-final-019f3ed0-f9d9-7590-a829-de92b9296105 +Reviewer run IDs: senior-engineering-final-019f3ecb-6123-7581-9b08-a1812b789d57, qa-test-final-019f3ecb-a018-7e02-81a7-3a7fd102a4ba, security-auth-final-019f3ecb-d5a9-7800-94b3-e4358974f0cd, product-ops-final-019f3ecc-0e2d-7e90-873a-af311a10fd59, architecture-final-019f3ecc-5034-7da0-a53e-1cb66880535b, docs-final-019f3ecc-87cf-75a3-b806-d694cf0f8114, reuse-dedup-final-019f3ed0-bdf1-7ae3-99ae-a33db36dea1c, test-delta-final-019f3ed0-f9d9-7590-a829-de92b9296105, senior-engineering-external-019f3f6d-441f-7340-9bcc-ef92155d956d, qa-test-external-019f3f6d-4986-7612-a7a7-9457a2182d0b, security-auth-external-019f3f6d-4fea-7890-a372-a261d2482b91, product-ops-external-019f3f6d-5758-79d3-a7d4-3d58933064da, docs-external-final-019f3f76-dd39-74f3-8cf3-d551908fa058, test-delta-external-019f3f6d-685a-7300-82ab-966c3c034976 After the reviewed SHA, only evidence files changed. @@ -57,6 +57,9 @@ Scope: - Centralized locked-context required fields and reused the shared missing-field helper. - Added tests for worker-safe redaction, operator-only locked context, exact submission requirements, malformed policy fail-closed behavior, same-version policy mutation, and v1 locked context after v2 guide activation. - Updated the real API contract drill to call `work-context`, `submission-requirements`, and `locked-context`. +- Addressed CodeRabbit's test-maintainability nitpick by capturing the compiled + bundle before deleting the ORM row in the malformed locked-context + regression. ## Commands Run diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md index bc5ada3..fcf0c9e 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md @@ -99,10 +99,7 @@ Authorization: ### Files outside scope -- `.agent-loop/REVIEW_LOG.md`: governance tracking update added after external - review so the repository-level review log points to the separate CodeRabbit - response artifact. It does not change product behavior, tests, CI, or chunk - implementation scope. +None. ## Product Behavior @@ -200,9 +197,9 @@ External review response file: ## Reviewer results -Reviewed implementation SHA: `449b68093fb62d54a64adab32517cca33f17cb59` +Reviewed implementation SHA: `7f0c0ecb2107cb706150f82c71f32255fb99e14e` -Reviewed at: 2026-07-07 through 2026-07-08 +Reviewed at: 2026-07-08T02:09:55Z Reviewer run IDs: From b1e5b238931ef9fb03245675070a69376e47f717 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 8 Jul 2026 03:12:38 +0100 Subject: [PATCH 5/5] Bind task context evidence to pushed revision --- .../reviews/WS-POL-001-13-internal-review-evidence.md | 2 +- .../reviews/WS-POL-001-13-pr-trust-bundle.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md index 1d5dda7..5c2300c 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-internal-review-evidence.md @@ -10,7 +10,7 @@ valid findings addressed: yes ## Reviewed Revision -Reviewed code SHA: 7f0c0ecb2107cb706150f82c71f32255fb99e14e +Reviewed code SHA: f533f1a572a38d4e8ecd34ff6316885c6c6b1016 Reviewed at: 2026-07-08T02:09:55Z diff --git a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md index fcf0c9e..47f779f 100644 --- a/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-POL-001-submission-artifact-policy-foundation/reviews/WS-POL-001-13-pr-trust-bundle.md @@ -197,7 +197,7 @@ External review response file: ## Reviewer results -Reviewed implementation SHA: `7f0c0ecb2107cb706150f82c71f32255fb99e14e` +Reviewed implementation SHA: `f533f1a572a38d4e8ecd34ff6316885c6c6b1016` Reviewed at: 2026-07-08T02:09:55Z