Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,26 @@
All are repaired with regression proof. Its review-event checkout concern is
rejected because hosted review-event run `30600808957` passed exact-head
approval and executed the PR's lane inventory before its later failure.

## WS-ART-001-03B3B2

- Installed only the approved hash-bound `pypdf==6.14.2` wheel and added the
bounded passive-PDF adapter to the existing isolated extraction child.
- Initial architecture review found eager pre-limit parser loading and obsolete
v1 `unsupported` replay. Repairs made loading PDF-only after resource limits,
retained seccomp before untrusted-byte parsing, introduced PDF policy v2,
and reset only an exact-lineage obsolete retry budget.
- Initial QA/test-delta review found that generic action dictionaries and
several JavaScript/submit/import/launch variants lacked proof. Repairs reject
`/A` generically and cover real annotation actions and named active features.
- The new policy regression now proves v1 `unsupported` evidence remains
historical while fresh v2 extraction succeeds and subsequent claims replay
only the v2 success. The isolated PostgreSQL test passes.
- Focused PDF/extraction/architecture tests pass, PDF coverage remains above 90
percent, the dependency/lock gate and canonical lane inventory pass, and no
CI or coverage policy is weakened.
- Final architecture, security, QA, product/ops, senior engineering, CI
integrity, docs, reuse/dedup, and test-delta reviews pass. Low future reuse
advice is recorded but is not a blocker for this bounded format adapter.
- CodeRabbit's valid external findings add bounded `KeyError`/`IndexError`
malformed-PDF mapping and document the 100,000-object inspection limit.
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,16 @@ approval, 03B3B2 PDF, 03B3B3A shared OOXML security, separate 03B3B3B DOCX,
03B3B3C PPTX, and 03B3B3D XLSX adapters, and 03B3B4 image metadata. No dependency is
installed until the human owner approves 03B3B1's exact pinned allowlist.

03B3B1 implementation is active as a dependency-decision and CI-only chunk.
It proposes exact hashed wheels for `pypdf`, `defusedxml`, and `Pillow`, with
no package, lock, runtime import, or parser behavior change. Its approval gate
03B3B1 is merged through PR #230 as the dependency-decision and CI-only chunk.
It approved exact hashed wheels for `pypdf`, `defusedxml`, and `Pillow`, with no
package, lock, runtime import, or parser behavior change. Its approval gate
requires independent protected GitHub review of the exact final PR head before
merge; repository-authored evidence alone is not authority.

03B3B2 is the active successor. It installs only the approved `pypdf` wheel and
adds bounded passive-PDF text extraction inside the existing isolated child.
It does not activate AUTH, invoke sufficiency, or begin OOXML/image work.

AUTH `WS-XINT-002-04B` follows the complete hidden split-03B series and
activates only fixed-service binding and guide read. ART-03C then removes the
legacy identity/excerpt path and makes the verified pipeline authoritative.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ backend/config/guide_extractor_dependencies.json
backend/app/modules/artifacts/guide_pdf.py
backend/app/modules/artifacts/guide_extraction.py
backend/app/modules/artifacts/guide_extraction_worker.py
backend/app/modules/artifacts/guide_extraction_service.py
backend/scripts/run_test_lanes.py
backend/tests/test_guide_pdf.py
backend/tests/test_guide_bindings.py
backend/tests/test_guide_extraction.py
backend/tests/fixtures/guide_pdf/**
backend/tests/test_artifact_architecture.py
docs/spec_artifact_storage_service.md
Expand All @@ -48,6 +52,10 @@ fetches, provider/AUTH/Celery/submission changes.
## Acceptance criteria

- Exact 03B2 PDF classification is required.
- PDF uses a new policy identity so pre-support `unsupported` attempts cannot
replay; an exact-lineage retry budget safely resets for that policy change.
- The new focused PDF test module is assigned to an existing canonical hosted
semantic lane without changing lane or coverage policy.
- Encrypted, malformed, attachments, embedded files, forms, XFA, launch/open
actions, active/external, and over-500-page PDFs fail with bounded outcomes.
- Parser imports and execution exist only in the isolated extraction child,
Expand All @@ -58,9 +66,11 @@ fetches, provider/AUTH/Celery/submission changes.
## Verification commands

```bash
(cd backend && python scripts/check_guide_extractor_dependencies.py)
(cd backend && uv run python scripts/check_guide_extractor_dependencies.py)
(cd backend && uv run ruff check app tests scripts)
(cd backend && uv run pytest -q tests/test_guide_pdf.py tests/test_guide_extraction.py tests/test_artifact_architecture.py --cov=app.modules.artifacts --cov-report=term-missing --cov-fail-under=90)
(cd backend && uv run pytest -q tests/test_guide_pdf.py tests/test_guide_extraction.py tests/test_artifact_architecture.py)
(cd backend && uv run pytest -q tests/test_guide_pdf.py --cov=app.modules.artifacts.guide_pdf --cov-report=term-missing --cov-fail-under=90)
(metadata_dir="$(mktemp -d)" && trap 'rm -rf "$metadata_dir"' EXIT && (cd backend && WORKSTREAM_TEST_ADMIN_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/postgres .venv/bin/python scripts/run_isolated_tests.py --metadata-json "$metadata_dir/result.json" --timeout-seconds 600 -- .venv/bin/python -m pytest -q tests/test_guide_bindings.py::test_pdf_support_replaces_the_obsolete_policy_budget_without_replay))
(metadata_dir="$(mktemp -d)" && trap 'rm -rf "$metadata_dir"' EXIT && (cd backend && WORKSTREAM_TEST_ADMIN_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/postgres .venv/bin/python scripts/run_isolated_tests.py --metadata-json "$metadata_dir/result.json" --timeout-seconds 12600 -- .venv/bin/python -m pytest -q --ignore=tests/test_isolated_database_runner.py --cov=app --cov-report=term-missing --cov-fail-under=78))
python3 scripts/check_stale_artifact_contracts.py
python3 scripts/check_markdown_links.py
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# WS-ART-001-03B3B2 External Review Response

## Comments addressed

- CodeRabbit Major: valid. `pypdf` may raise `KeyError` or `IndexError` while
reading malformed internal structures. Both now map to terminal
`malformed/invalid_pdf` rather than retryable `parser_failure`, with direct
regression proof.
- CodeRabbit documentation nitpick: valid. The artifact specification now
records the enforced 100,000-object inspection limit and bounded
`pdf_object_limit` outcome.

## Comments deferred

None.

## Human decisions needed

None beyond normal PR review and merge ownership.

## Commands rerun

- Focused PDF regression tests.
- Ruff on the changed PDF code/tests.
- Dependency gate, stale-contract scan, Markdown links, and `git diff --check`.
- Hosted Backend and Agent Gates rerun after the repair commit.

## Remaining risks

The parser remains an untrusted-format dependency; existing child resource
limits, seccomp-before-byte-read ordering, bounded errors, and complete process
termination remain required controls.
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# WS-ART-001-03B3B2 PR Trust Bundle

## Chunk

`WS-ART-001-03B3B2` — PDF Extractor (L1).

## Goal and human-approved intent

Install only the dependency approved in merged 03B3B1 and add bounded PDF text
extraction inside the existing hidden, isolated guide-extraction framework.
Guide uploads remain independent from contributor ZIP submissions. This chunk
does not activate AUTH, invoke sufficiency, or begin OOXML/image work.

## What changed and why

- Added the exact hash-bound `pypdf==6.14.2` wheel to runtime dependencies and
the lockfile.
- Added a PDF-only adapter that rejects encrypted, malformed, interactive,
attached, embedded, external, and active PDFs; enforces 500 pages; and emits
deterministic compact JSON with explicit page boundaries.
- Loads the trusted adapter only for PDF after process resource limits, then
installs seccomp before reading or parsing untrusted bytes.
- Added PDF policy v2 so pre-support v1 `unsupported` attempts cannot replay.
Exact-lineage retry custody resets once to the new policy and preserves the
old immutable evidence.
- Assigned the new focused test to the existing `shared_foundations` lane and
updated the artifact specification and initiative status.

## Design chosen

The existing `GuideExtractionRunner` and child protocol remain authoritative.
The parent sees only the server-derived `pdf` classification and bounded result.
There is no second parser runtime, direct provider access, raw agent input, or
new authorization path. Canonical output is `{"pages":[...]}`.

Rejected alternatives were parsing during upload, loading `pypdf` for every
format, retaining PDF as policy-v1 unsupported, accepting partial extraction,
or allowing arbitrary PDF actions.

## Scope control and product behavior

All changed files are in the corrected chunk contract. The only contract
additions are the existing persistence service, its policy replay test, and the
canonical lane inventory required to make the original acceptance criteria
executable. No provider, AUTH, Celery, submission, review, compensation,
reputation, OCR, OOXML, image, route, or agent-assembly behavior changes.

PDF failures remain internal bounded extraction outcomes and never become guide
insufficiency decisions. Only successful current-policy canonical content can
later enter sufficiency.

## Acceptance criteria proof

- Exact 500/501 page boundary is tested.
- Malformed and encrypted documents are rejected.
- Forms, XFA, attachments, embedded/file-spec objects, open/additional actions,
JavaScript, named/launch/submit/import/external actions, URI links, widgets,
and rich media are rejected with bounded codes.
- Import graph and execution-order tests confine the parser to the PDF child:
limits, trusted import, seccomp, then untrusted parsing.
- Workspace cleanup, child failure, timeout, cancellation, input/output limits,
and protocol validation reuse and exercise the existing framework proofs.
- The isolated PostgreSQL regression proves v1 unsupported evidence is not
replayed after PDF policy v2 and that v2 success becomes the replay target.

## Tests and checks

- `uv run python scripts/check_guide_extractor_dependencies.py` — pass.
- `uv lock --check` — pass; wheel URL/hash matches the approved manifest.
- `uv run ruff check app tests scripts` — pass.
- Focused PDF/extraction/architecture suite — 77 passed before the final added
order/action cases; targeted final repair suite — 31 passed.
- New PDF module coverage — 94.81 percent (above 90 percent).
- Isolated PostgreSQL policy replay test — 1 passed.
- CI lane inventory tests — pass; `test_guide_pdf.py` is in
`shared_foundations`.
- Stale-contract scan, Markdown links, and `git diff --check` — pass.
- Repository-wide 78 percent coverage and hosted semantic lanes remain for the
GitHub Backend gate; no local full-suite run was used.

## Test delta and CI integrity

No test or assertion was removed, skipped, or weakened. New adversarial cases
cover actual action annotations rather than only synthetic root keys. No
workflow, lane count, coverage threshold, or failure behavior changed. Hosted
installation remains protected by the exact direct wheel URL/hash and the
dependency checker; the lockfile records the same bytes.

## Reviewer results

- Architecture: initial High findings repaired; re-review pass with one Low
import-style proof hardening item, also repaired.
- Security/auth: pass.
- QA: initial action-dictionary High repaired; re-review pass.
- Product/ops: pass.
- Senior engineering: initial verification/DB seed/scope findings repaired;
final re-review pass.
- CI integrity: pass with informational hosted-install notes.
- Docs: pass.
- Reuse/dedup: pass with Low future shared-normalizer advice.
- Test delta: initial missing-case and branch-proof findings repaired; final
re-review pass.

## External review

CodeRabbit found one valid malformed-PDF exception mapping issue and one valid
documentation omission. `KeyError`/`IndexError` now map to bounded terminal
malformed outcomes with regression proof, and the 100,000-object limit is
documented. Hosted Backend and Agent Gates passed before the repair and rerun on
the final head.

## Remaining risks and follow-up

`pypdf` is an untrusted-format parser, so OS termination remains an essential
control. The trusted dependency import intentionally precedes descriptor-only
seccomp because Python import needs filesystem reads; resource limits are
already installed and no untrusted bytes are read until after seccomp.

03B3B3A (OOXML security) and 03B3B4 (image metadata) remain separate chunks.
03B4 sufficiency continuation cannot start until all required format chunks
merge.

## Human review focus and merge ownership

Review the exact wheel/hash, active-content deny semantics, 500-page boundary,
trusted-import/seccomp ordering, PDF policy-v2 replay reset, and deterministic
page JSON. A human owns merge approval; the agent will not merge this PR.
11 changes: 10 additions & 1 deletion backend/app/modules/artifacts/guide_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@


EXTRACTION_POLICY_VERSION = "guide-extraction-v1"
PDF_EXTRACTION_POLICY_VERSION = "guide-extraction-v2"
EXTRACTOR_VERSION = "1"
MAXIMUM_INPUT_BYTES = 32 * 1024 * 1024
MAXIMUM_OUTPUT_BYTES = 4 * 1024 * 1024
MAXIMUM_PROTOCOL_BYTES = (MAXIMUM_OUTPUT_BYTES * 6) + 1024
WALL_TIMEOUT_SECONDS = 60
_SUPPORTED = frozenset({"plain_text", "markdown", "json", "csv"})
_SUPPORTED = frozenset({"plain_text", "markdown", "json", "csv", "pdf"})


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -160,4 +161,12 @@ def _result(
canonical_output=output,
output_sha256=digest,
extractor_name=f"workstream.{detected_format}",
policy_version=extraction_policy_version(detected_format),
)


def extraction_policy_version(detected_format: str) -> str:
"""Return the policy identity that prevents obsolete format replay."""
if detected_format == "pdf":
return PDF_EXTRACTION_POLICY_VERSION
return EXTRACTION_POLICY_VERSION
24 changes: 15 additions & 9 deletions backend/app/modules/artifacts/guide_extraction_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from app.core.cancellation import await_cancellation_resistant

from app.modules.artifacts.guide_extraction import (
EXTRACTION_POLICY_VERSION,
GuideExtractionRegistry,
extraction_policy_version,
)
from app.modules.artifacts.guide_formats import DETECTOR_NAME, DETECTOR_VERSION
from app.modules.artifacts.models import (
Expand Down Expand Up @@ -143,7 +143,7 @@ async def record_cancellation() -> None:
after = await self._load_facts(session, request)
if after != before:
raise GuideExtractionError("guide extraction is unavailable")
if extracted.policy_version != EXTRACTION_POLICY_VERSION:
if extracted.policy_version != extraction_policy_version(before.detected_format):
raise GuideExtractionError("guide extraction result conflicts")
attempt_number = 1 + int(
await session.scalar(
Expand Down Expand Up @@ -264,6 +264,7 @@ async def claim_materialization_slot(
facts = await self._load_facts(session, request)
if facts is None:
raise GuideExtractionError("guide extraction is unavailable")
policy_version = extraction_policy_version(facts.detected_format)
successful = (
await session.execute(
select(
Expand All @@ -283,8 +284,8 @@ async def claim_materialization_slot(
)
.where(
GuideSourceExtractionUsage.binding_id == facts.binding_id,
GuideSourceExtractionAttempt.policy_version == EXTRACTION_POLICY_VERSION,
GuideSourceExtractedContent.policy_version == EXTRACTION_POLICY_VERSION,
GuideSourceExtractionAttempt.policy_version == policy_version,
GuideSourceExtractedContent.policy_version == policy_version,
)
.order_by(
GuideSourceExtractionAttempt.attempt_number.asc(),
Expand All @@ -304,7 +305,7 @@ async def claim_materialization_slot(
GuideSourceExtractionAttempt.content_id == facts.content_id,
GuideSourceExtractionAttempt.classification_id == facts.classification_id,
GuideSourceExtractionAttempt.setup_generation == facts.setup_generation,
GuideSourceExtractionAttempt.policy_version == EXTRACTION_POLICY_VERSION,
GuideSourceExtractionAttempt.policy_version == policy_version,
)
.order_by(GuideSourceExtractionAttempt.attempt_number.desc())
.limit(1)
Expand All @@ -326,7 +327,7 @@ async def claim_materialization_slot(
content_id=facts.content_id,
classification_id=facts.classification_id,
setup_generation=facts.setup_generation,
policy_version=EXTRACTION_POLICY_VERSION,
policy_version=policy_version,
claimed_slots=1,
)
)
Expand All @@ -336,9 +337,13 @@ async def claim_materialization_slot(
budget.content_id != facts.content_id
or budget.classification_id != facts.classification_id
or budget.setup_generation != facts.setup_generation
or budget.policy_version != EXTRACTION_POLICY_VERSION
):
raise GuideExtractionError("guide extraction is unavailable")
if budget.policy_version != policy_version:
budget.policy_version = policy_version
budget.claimed_slots = 1
await session.flush()
return None
if latest_attempt is None:
raise GuideExtractionError("guide extraction is unavailable")
if budget.claimed_slots < 2:
Expand Down Expand Up @@ -448,13 +453,14 @@ async def _persist_failure(
status: str,
error_code: str,
) -> GuideExtractionPersistenceResult:
policy_version = extraction_policy_version(facts.detected_format)
attempt_number = 1 + int(
await session.scalar(
select(
func.coalesce(func.max(GuideSourceExtractionAttempt.attempt_number), 0)
).where(
GuideSourceExtractionAttempt.binding_id == facts.binding_id,
GuideSourceExtractionAttempt.policy_version == EXTRACTION_POLICY_VERSION,
GuideSourceExtractionAttempt.policy_version == policy_version,
)
)
or 0
Expand All @@ -468,7 +474,7 @@ async def _persist_failure(
detected_format=facts.detected_format,
extractor_name=f"workstream.{facts.detected_format}",
extractor_version="1",
policy_version=EXTRACTION_POLICY_VERSION,
policy_version=policy_version,
attempt_number=attempt_number,
status=status,
error_code=error_code,
Expand Down
Loading
Loading