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
@@ -0,0 +1,122 @@
# WS-ART-001-03B3B1R1: Linux Architecture Portability

## Intent

Provide one copy-pasteable backend setup for contributors on macOS, Windows,
Linux ARM, and Linux x86_64 without weakening Workstream's Linux-only image
extractor isolation boundary.

## Scope

Extend the existing hash-bound Pillow approval from Linux glibc x86_64 to the
corresponding Linux glibc aarch64 wheels for CPython 3.11 and 3.12. Add a
native-architecture Linux development container, connect it to the
repository-managed services, and make Docker versus native setup explicit in
contributor-facing documentation.

## Allowed Files

- `README.md`
- `CONTRIBUTING.md`
- `.dockerignore`
- `backend/.env.example`
- `backend/app/modules/artifacts/guide_extraction.py`
- `backend/config/guide_extractor_dependencies.json`
- `backend/pyproject.toml`
- `backend/scripts/check_guide_extractor_dependencies.py`
- `backend/tests/test_guide_extractor_dependencies.py`
- `backend/tests/test_guide_extraction.py`
- `backend/uv.lock`
- `docker/backend/Dockerfile.dev`
- `docker-compose.yml`
- `docs/operations_backend_testing.md`
- `docs/spec_artifact_storage_service.md`
- this chunk contract

## Not Allowed

- macOS, Windows, musl, or 32-bit native parser support
- source distributions, unpinned packages, relaxed hashes, or index fallback
- a Pillow version change or unrelated dependency update
- parser output semantics, migrations, production images, or deployment
configuration; only the fixed secret-free extraction child environment may change
- CI workflow, test-routing, or coverage changes
- automatic destructive database or volume resets

## Acceptance Criteria

- Pillow has exact URL and SHA-256 approvals for CPython 3.11 and 3.12 on both
manylinux x86_64 and manylinux aarch64, with no sdist or fallback path.
- PEP 508 markers are mutually exclusive by Python version and machine and
select only Linux artifacts.
- The dependency gate accepts only CPython 3.11/3.12 on Linux glibc x86_64 or
aarch64 and continues to reject macOS, Windows, musl, unsupported Python, and
other architectures.
- The Docker workflow uses the Docker host's native x86_64 or aarch64 Linux
architecture, applies migrations, and serves `GET /api/v1/health` on host
loopback.
- A real isolated image extraction succeeds inside the aarch64 development
container with Workstream's inner seccomp filter active.
- The extraction child's fixed environment disables ARM OpenSSL acceleration
without inheriting arbitrary parent variables, and real PDF extraction also
succeeds inside the aarch64 development container.
- Native host setup is documented only for supported Linux glibc architectures;
macOS and Windows users are directed to Docker.
- Tracked environment examples contain local-only values or placeholders, no
deployable credentials, and `.env` remains ignored.
- Existing Postgres, Redis, and MinIO service workflows remain available.
- Setup, verification, shutdown, and explicitly destructive reset commands are
clear and copy-pasteable.
- Any pull request changing the approval manifest requires fresh independent
approval on its exact final head from a repository owner, member, or
collaborator before merge.

## Risk

L1 supply-chain and native-runtime change. The implementation expands one
approved Linux architecture while preserving the Linux/glibc isolation model,
exact artifact hashes, and fail-closed platform checks.

## Verification

Supported native Linux checkout, after installing the locked development environment
(hosted CI runs the equivalent checks with its managed interpreter):

- `cd backend && uv lock --check`
- `cd backend && .venv/bin/python scripts/check_guide_extractor_dependencies.py`
- `cd backend && PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/python -m pytest -q -p pytest_cov.plugin tests/test_guide_extractor_dependencies.py --cov=scripts.check_guide_extractor_dependencies --cov-branch --cov-report=term-missing --cov-fail-under=90`
- `cd backend && .venv/bin/python -m scripts.authorization_boundary validate --ledger ../.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md`
- `cd backend && .venv/bin/python -m scripts.test_structure_boundary validate --policy ../.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_POLICY.md --ledger ../.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json`
- `cd backend && .venv/bin/python -m scripts.behavior_ownership validate`
- `cd backend && install -d -m 700 .ci/test-lanes`
- `cd backend && PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/python scripts/run_test_lanes.py --collect-only --metadata-dir .ci/test-lanes/collect --summary-json .ci/test-lanes/collect-summary.json`
- `cd backend && .venv/bin/python scripts/validate_test_lane_evidence.py --metadata-dir .ci/test-lanes/collect --summary-json .ci/test-lanes/collect-summary.json`

Native-architecture Docker:

- `docker compose run --rm --no-deps backend python -m pytest -q tests/test_guide_extraction.py -k real_isolated_image_runner`
- `docker compose run --rm --no-deps backend python -m pytest -q tests/test_guide_pdf.py -k isolated_runner`
- `docker compose config --quiet`
- `docker compose build backend`
- `docker compose up --wait backend`
- `curl --fail http://127.0.0.1:8000/api/v1/health`
- `docker compose run --rm --no-deps backend ruff check app tests scripts`

Repository-root checks:

- `python3 scripts/check_markdown_links.py`
- `python3 scripts/check_stale_workstream_wording.py`
- `python3 scripts/check_stale_authorization_docs.py`
- `python3 scripts/check_stale_artifact_contracts.py`
- `python3 -m unittest -v scripts.test_lightweight_agent_gates`
- `git diff --check`

## Reviewers

- security
- architecture
- QA/test
- CI integrity
- documentation
- reuse/deduplication
- senior engineering
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*
!backend/
!backend/**
!docker/
!docker/backend/
!docker/backend/Dockerfile.dev

backend/.ci/
backend/.coverage*
backend/.env
backend/.pytest_cache/
backend/.ruff_cache/
backend/.venv/
backend/**/__pycache__/
backend/**/*.pyc
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ in [README.md](README.md) before changing product terminology or architecture.
Intent -> Plan -> Bounded Change -> Tests -> Review -> Pull Request -> Human Merge
```

## Set Up The Development Environment

Use the [Developer Quickstart](README.md#developer-quickstart) before running
repository checks. Docker is the supported cross-host path for macOS and
Windows and selects native Linux x86_64 or aarch64 inside the Docker VM. Native
host setup is supported only on the Linux/glibc/Python matrix documented there.
Do not replace the approved Pillow artifacts to make an unsupported host install
pass.

For a small change, record the intent and scope in the pull request. For larger
or higher-risk work, add a short initiative plan and chunk contract under
`.agent-loop/initiatives/`. Existing planning artifacts are useful context, not
Expand Down
115 changes: 113 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ capability ledger and explicit remaining work.

## Start Here

- [Developer Quickstart](#developer-quickstart)
- [Contribution Guide](CONTRIBUTING.md)
- [Current v0.1 Status](docs/roadmap_status.md)
- [Product Principles](docs/product_principles.md)
Expand Down Expand Up @@ -278,17 +279,127 @@ the repository is changed; it does not define runtime task or review records.
Independent initiatives and branches may proceed concurrently. Start with
[CONTRIBUTING.md](CONTRIBUTING.md) before proposing repository work.

## Local Backend Database
## Developer Quickstart

Workstream's image-extraction boundary is intentionally Linux-only. The
supported runtime is CPython 3.11 or 3.12 on Linux glibc 2.27 or newer, using
either x86_64 or aarch64. macOS and Windows contributors should run the backend
through Docker; do not install a different Pillow build to bypass the approved
artifact boundary.

### Docker Workflow (Recommended)

Prerequisites are Git, Docker Engine, and Docker Compose v2. From the repository
root, build the native-architecture Linux image and start the API with healthy
Postgres and Redis dependencies:

```bash
docker compose up --build --wait backend
```

Then verify the API with the command for your shell:

```bash
# macOS, Linux, or Git Bash
curl --fail http://127.0.0.1:8000/api/v1/health
```

```powershell
# PowerShell
Invoke-RestMethod http://127.0.0.1:8000/api/v1/health
```

The expected response is `{"status":"ok"}`. The backend service applies
Alembic migrations before serving, binds the API only to host loopback, and
uses explicit local-only development auth and key material. Artifact storage is
disabled in this first-run profile; integration tests configure MinIO when they
exercise the S3-compatible path.

The image uses Linux glibc on the Docker host's native x86_64 or aarch64
architecture. On Docker Desktop, this is the Docker VM's native architecture.
Do not force `--platform linux/amd64` on an ARM host: CPU emulation does not
provide equivalent evidence for Workstream's inner seccomp isolation filter. If
your shell sets `DOCKER_DEFAULT_PLATFORM`, clear it before building; the
Dockerfile rejects a foreign target architecture.

Run focused checks in the same containerized environment:

```bash
docker compose run --rm --no-deps backend python scripts/check_guide_extractor_dependencies.py
docker compose run --rm --no-deps backend python -m pytest -q tests/test_app.py tests/test_guide_extractor_dependencies.py
docker compose run --rm --no-deps backend ruff check app tests scripts
```

Dependency changes require a rebuild:

```bash
docker compose build backend
```

### Native Linux Workflow

Use this path only with CPython 3.11 or 3.12 on Linux glibc 2.27 or newer and
an x86_64 or aarch64 machine. Docker is still used for backing services.
Confirm that `python3 --version` reports Python 3.11 or 3.12 before creating
the environment. Native extraction also requires `libseccomp.so.2` and a normal
Linux `/proc`; install `libseccomp2` on Debian/Ubuntu or the equivalent
`libseccomp` package for your distribution. Install uv 0.12.3 and use the
committed lockfile; an unconstrained pip install is not a supported setup path.

```bash
docker compose up -d --wait postgres redis
cd backend
cp .env.example .env
python3 --version
uv --version
uv sync --locked --extra dev --python python3
.venv/bin/python -m alembic upgrade head
.venv/bin/python -m uvicorn app.main:app --reload
```

Verify the API from another terminal with:

```bash
curl --fail http://127.0.0.1:8000/api/v1/health
```

`backend/.env` is ignored. Its checked-in example contains only public,
local-development values; replace those values when specifically testing key
rotation, and never reuse them in a shared or hosted environment.

### Logs, Shutdown, And Reset

```bash
docker compose logs -f backend
docker compose down
```

For the native workflow, stop Uvicorn with `Ctrl+C` before running
`docker compose down` for the backing services.

To deliberately delete the local Postgres and MinIO volumes as well, run the
following destructive reset command:

```bash
docker compose down --volumes
```

### Backing Services And Artifact Storage

Workstream uses Postgres locally and in CI. It uses Celery with Redis for
durable local project setup jobs and automatic pre-review checker gates. MinIO
provides the S3-compatible artifact protocol in local development and CI. Start
the local services with:

```bash
docker compose up -d postgres redis minio
docker compose up -d --wait postgres redis minio
```

If either default host port is already in use, set
`WORKSTREAM_POSTGRES_HOST_PORT` or `WORKSTREAM_REDIS_HOST_PORT` before running
Compose. Native-backend users must put the same selected ports in
`backend/.env`; the containerized backend uses the internal service ports.

MinIO uses the compose-only static credentials and the private
`workstream-artifacts` bucket. The integration tests create that bucket
automatically. For local runtime use, create the private bucket with an S3
Expand Down
35 changes: 35 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Native local-development defaults.
#
# Copy this file to backend/.env only when using the supported native runtime:
# CPython 3.11 or 3.12 on Linux glibc x86_64 or aarch64, with libseccomp.so.2
# and /proc available. The Docker workflow supplies equivalent
# container-network values and does not require this file.

WORKSTREAM_ENVIRONMENT=local
WORKSTREAM_DEBUG=true
WORKSTREAM_DATABASE_URL=postgresql+asyncpg://workstream:workstream@localhost:5433/workstream

# Development auth is local-only. These values grant identity, not Workstream
# product authority; bootstrap grants separately when an endpoint requires it.
WORKSTREAM_AUTH_PROVIDER=dev
WORKSTREAM_DEV_AUTH_TOKEN=workstream-local-dev-token
WORKSTREAM_DEV_AUTH_SUBJECT=local-developer
WORKSTREAM_DEV_AUTH_ISSUER=https://workstream.local/development

# Deterministic local-only 32-byte base64 keys. Replace them when testing key
# rotation. Never reuse these public example values outside local development.
WORKSTREAM_API_RATE_LIMIT_KEY_SECRET=ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
WORKSTREAM_PAGINATION_CURSOR_HMAC_SECRET=AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=

WORKSTREAM_CELERY_BROKER_URL=redis://localhost:6379/0
WORKSTREAM_PROJECT_SETUP_PIPELINE_AUTOSTART=false

# The quickstart keeps artifact storage disabled. Integration tests configure
# repository-managed MinIO explicitly when they need the S3-compatible path.
WORKSTREAM_ARTIFACT_STORE_BACKEND=disabled

# Optional automatic project-setup agent settings:
# WORKSTREAM_PROJECT_AGENT_OPENAI_AGENT_SDK_MODEL=<approved-model>
# WORKSTREAM_PROJECT_AGENT_RUN_TIMEOUT_SECONDS=1800
# WORKSTREAM_PROJECT_AGENT_MAX_PROMPT_BYTES=2000000
# OPENAI_API_KEY=<runtime-secret>
7 changes: 6 additions & 1 deletion backend/app/modules/artifacts/guide_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ def extract(
payload = reader.read(MAXIMUM_INPUT_BYTES + 1)
if len(payload) > MAXIMUM_INPUT_BYTES:
return self._result(detected_format, "limit_exceeded", "input_limit", None)
environment = {"LANG": "C.UTF-8", "LC_ALL": "C.UTF-8", "PATH": "/usr/bin:/bin"}
environment = {
"LANG": "C.UTF-8",
"LC_ALL": "C.UTF-8",
"OPENSSL_armcap": "0",
"PATH": "/usr/bin:/bin",
}
process = None
try:
process = subprocess.Popen(
Expand Down
Loading
Loading