diff --git a/.github/workflows/clean-build-check.yml b/.github/workflows/clean-build-check.yml index f80eff95..1e8c63a8 100644 --- a/.github/workflows/clean-build-check.yml +++ b/.github/workflows/clean-build-check.yml @@ -27,13 +27,13 @@ jobs: run: npm ci - name: Build without REPL_ID - run: env -u REPL_ID NODE_ENV=production npx tsx script/build.ts + run: env -u REPL_ID NODE_ENV=production npm run build - name: Check for @replit references in client bundle run: | MATCHES=$( (grep -r "@replit" dist/public/assets/ || true) ) if [ -n "$MATCHES" ]; then - echo "::error::@replit packages leaked into the client bundle. Run 'env -u REPL_ID NODE_ENV=production npx tsx script/build.ts' locally and check vite.config.ts for static @replit imports." + echo "::error::@replit packages leaked into the client bundle. Run 'env -u REPL_ID NODE_ENV=production npm run build' locally and check vite.config.ts for static @replit imports." echo "$MATCHES" exit 1 fi diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8ad4e27f..a5eecd88 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,7 +41,7 @@ jobs: enable-cache: true - name: Install Python dependencies - run: uv sync --frozen + run: uv sync --locked - name: Run provider contracts run: | @@ -97,7 +97,7 @@ jobs: run: npm ci - name: Install Python dependencies - run: uv sync --frozen + run: uv sync --locked - name: Apply database schema run: npm run db:push diff --git a/CLAUDE.md b/CLAUDE.md index f9425148..6b5254e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,14 +32,14 @@ scripts/start-dev.sh npm run dev # = NODE_ENV=development tsx server/index.ts # Production build (Vite → dist/public/, esbuild → dist/index.cjs) -npm run build # = tsx script/build.ts +npm run build # = node --import tsx script/build.ts -# Full prod bootstrap: npm install + pip install -e . + npm run build +# Locked prod bootstrap: npm ci + uv sync --locked --no-dev + npm run build scripts/build-prod.sh # Production start (Express :5000 + uvicorn :8001) -npm start # Express only (dist/index.cjs) -scripts/start-prod.sh # both processes +npm start # canonical scripts/start-prod.sh launcher +scripts/start-prod.sh # Express owns the sole uvicorn child; uses locked .venv # TypeScript type checking npm run check # = tsc @@ -59,6 +59,10 @@ python scripts/annotate.py # single-file mode (preserves C:D / I:O) uv run pytest tests/ -v uv run pytest tests/ -v --ignore=tests/test_live_server.py # skip live-server smoke +# Credential-free offline checks, with database/live exclusions printed explicitly +# Requires npm ci and uv sync --locked (including Python dev dependencies) +python scripts/subm_chec_offl_v0.0.0alpha.py + # Contract runner — walks python/ for "# === CONTRACTS ===" blocks and runs them python -m python.tests.contract_runner diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cad10ce0..7007a1e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,10 +18,10 @@ Useful contributions include: Use these versions for local development unless a task-specific issue says otherwise: -- **Node.js 20+** — `README.md` lists Node.js 20+ for local development, and `package.json` pins `@types/node` to the 20.x line. +- **Node.js 20.19+ or 22.12+** — matches the locked Vite version; CI and the container use Node 22. See `package.json` engines. - **Python 3.12+** — `README.md` lists Python 3.12+, and `pyproject.toml` declares `requires-python = ">=3.12"`. - **npm** — JavaScript and TypeScript commands are exposed through `package.json` scripts such as `npm run build`, `npm run check`, and `npm run db:push`. -- **pip or an equivalent PEP 517 installer** — the README uses `pip install -e .`; Python package metadata lives in `pyproject.toml`. +- **uv** — use `uv sync --locked` for development and the committed `uv.lock`. The production build uses `uv sync --locked --no-dev`; package metadata still uses standard PEP 508 direct Git references. - **PostgreSQL** — required for the full local stack and contract suite paths described in `README.md`. Grounding files: `README.md`, `package.json`, `pyproject.toml`. diff --git a/Dockerfile b/Dockerfile index cdafdd1c..31233fa7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,21 @@ +# === CONTRACTS === +# id: container_recipe_reproducibility +# given: The production container recipe installs application dependencies. +# then: Both stages use the CI Node major, committed locks are checked, and build-only Node tools are excluded from the runtime install. +# class: reproducibility +# === END CONTRACTS === FROM ghcr.io/astral-sh/uv:0.11.18 AS uv -FROM node:20-bookworm-slim AS builder +FROM node:22-bookworm-slim AS builder WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci +RUN npm ci --include=dev COPY . . -RUN npm run build +RUN NODE_ENV=production npm run build -FROM node:20-bookworm-slim AS runner +FROM node:22-bookworm-slim AS runner WORKDIR /app RUN apt-get update \ @@ -28,7 +34,7 @@ COPY --from=builder /app/dist ./dist ENV UV_LINK_MODE=copy ENV UV_PYTHON_INSTALL_DIR=/opt/uv/python RUN uv python install 3.12 \ - && uv sync --frozen --no-dev + && uv sync --locked --no-dev RUN mkdir -p uploads diff --git a/README.md b/README.md index c65b310f..046ef577 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,22 @@ The only productized service is the **EDCMbone transcript explainer** — a one- ## Local Development -**Prerequisites:** Node.js 20+, Python 3.12+, PostgreSQL +**Prerequisites:** Node.js 20.19+ or 22.12+, Python 3.12+, uv, and PostgreSQL for the full application. CI and the container use Node 22. ```bash -npm install -pip install -e . +npm ci +uv sync --locked + +# No server, production database, provider key, or paid inference is needed. +# Prints the database/live suites that are not covered by this command. +python scripts/subm_chec_offl_v0.0.0alpha.py +``` + +The complete application is separate from these offline checks. Configure an isolated PostgreSQL database, apply the schema, and supply the required environment variables before starting it. The legacy development launcher terminates processes on its fixed ports; normal startup also changes database schema/data and starts background services. Do not run it against an existing database or on a shared workstation without reviewing those effects. + +```bash +# Only after the full-application prerequisites and side effects are reviewed: +export PATH="$PWD/.venv/bin:$PATH" # Start all three processes (Express :5000, Vite :5001, Python :8001) bash scripts/start-dev.sh @@ -86,6 +97,10 @@ bash scripts/start-dev.sh In development `scripts/start-dev.sh` generates a shared `INTERNAL_API_SECRET` automatically. +For a locked production build without starting the application, run `bash scripts/build-prod.sh`. It installs runtime Python dependencies only; run `uv sync --locked` again before running the offline development checks. + +Hackathon preparation, source-evidence limits, and the still-outstanding working-demo gates are recorded in the [candidate runbook](docs/hackathon/demo_runb_v0.0.0alpha.md). Neither a successful build nor a source archive establishes Replit parity or a qualifying Amazon-track demo. + ### Standalone a0 / Termux with DeepSeek The standalone `run.sh` runtime reads the same `python/config/providers.json` diff --git a/devpost-submission.md b/devpost-submission.md new file mode 100644 index 00000000..57015cf3 --- /dev/null +++ b/devpost-submission.md @@ -0,0 +1,72 @@ +# a0: continuous work across model providers + +**Draft only. Not sent to Devpost.** This describes the repository, not a verified Amazon-track entry. Resolve the track and demo gates in [submission evidence](docs/hackathon/subm_evid_v0.0.0alpha.md) before using it as final submission copy. + +This root filename is the Devpost preparation-tool boundary; supporting documents follow the repository's versioned filename convention. + +## One-line summary + +A research instrument for keeping one conversation and its work history continuous while language-model providers remain replaceable execution resources. + +## Problem and intended user + +People doing extended work with AI need to preserve the task, recent context, and a reviewable history when a model's context fills or a provider fails. a0 explores that problem for researchers and technically engaged users. Its value proposition is continuity and inspectability, not a claim that every task can be completed autonomously. + +## What a0 does + +a0 separates the persistent conversation from the model executing a turn. Its continuous-work harness retains the initial task and recent messages, constructs a bounded checkpoint for older turns, and keeps the stored transcript authoritative. An explicitly selected provider stays pinned. In automatic mode, the harness can try an eligible alternative after a supported failure, but stops automatic replay once a registry tool has executed. + +The repository combines a React console, an Express authentication and request boundary, and a Python/FastAPI orchestration service backed by PostgreSQL. Console structure comes from route-owned metadata. Provider adapters and model configuration are separated, and public access has explicit entitlement and cost limits. + +These are source-backed implementation descriptions. The current run results and limits belong in the [demo instructions](docs/hackathon/demo_runb_v0.0.0alpha.md), not in a blanket claim that the full platform is verified. + +## What changed during the hackathon + +This is an **existing project**. During the event window, the repository added the continuous-work harness and persisted continuity state, typed provider failures and guarded failover, a generic OpenAI-compatible transport, and public-access controls. An Android Capacitor shell was also added; it has not been established as a Fire TV application. + +The strongest before/after story is the new continuous-work harness: long conversations retain their original task identity, older context is bounded, and provider failures are handled without automatically repeating an already executed tool. Exact commits, source files, and proof limits are in [the change evidence](docs/hackathon/subm_evid_v0.0.0alpha.md#existing-project-evidence). + +## How AI is used + +The runtime delegates model inference to configured providers through its adapters. This draft does not claim that every provider in the registry has been exercised in the submitted build. Development assistance for this preparation included repository inspection and documentation drafting with an AI coding assistant. The submitter should confirm the actual development tools used for the final required answer. + +## Links and testing + +- Repository: https://github.com/The-Interdependency/a0 +- Exact candidate revision: **UNRESOLVED. Record the final tested commit before recording the video.** +- Run instructions and recording plan: [docs/hackathon/demo_runb_v0.0.0alpha.md](docs/hackathon/demo_runb_v0.0.0alpha.md). +- Public YouTube/Vimeo video: **REQUIRED, NOT PROVIDED.** +- Hosted testing URL: optional; no public hosting claim is made. +- License: [AGPL-3.0-or-later](LICENSE); preserve the notices for vendored dependencies, including skill-lib. + +## Track positioning, not yet selected + +No implemented Amazon-track runtime or simulated Alexa+ experience has been verified. Do not label the existing chat UI an Alexa+ simulation merely to fill the form. The owner must choose the intended experience, and the repository and video must then demonstrate it. See the practical alternatives in [submission evidence](docs/hackathon/subm_evid_v0.0.0alpha.md#track-decision). + +## Known limitations + +- Bounded continuity is intentionally lossy. It is not lossless memory or a substitute for the stored transcript. +- Tests using fake providers establish local behavior only, not live provider reliability, useful end-to-end task execution, or track qualification. +- The previously exercised Replit revision and the canonical GitHub source have not been proved identical. +- The repo's public-release checklist remains separate. A local judge demo does not establish safe public deployment, production privacy, scientific efficacy. + +## Required form answers still owned by the submitter + +Live form checked 2026-10-22. IDs identify fields; these are preparation notes, not authorization to submit. + +| Field IDs | Answer or remaining decision | +| --- | --- | +| 28285–28288 | Individual/team/organization, organization name or N/A, country, and Canadian province or N/A. Do not infer. | +| 28289 | Primary track selection, pending working track evidence. | +| 28290 | Final GitHub judging URL and exact tested revision. | +| 28291–28292 | Existing, but significantly updated. Use the sourced change description above, with the demonstrated feature updates. | +| 28293–28294 | AWS mini-challenge choice. No qualifying AWS or Kiro use has been verified; do not claim it. The service-description field is marked required even when the choice is No. | +| 28295–28299 | Open-source mini-challenge choice. If Yes, supply a distinct qualifying contribution, repository URL, personal GitHub username, and explanation. A public license alone does not answer these fields. | +| 28303–28307 | Five product-feedback answers: [draft and evidence limits](docs/hackathon/tool_feed_v0.0.0alpha.md). | +| 28308–28310 | Age, eligible jurisdiction, and absence of disqualifying employment/representation. Personal attestations, unanswered. | +| 29056 | AI tools actually used for development, to be confirmed. | +| 29057–29058 | Learning level and career AI value. Personal judgments, unanswered. | + +Optional testing instructions belong in field 29377. Never place real API keys, account passwords, or production credentials in this file or the public form. The form does not ask for a Codex session ID. + +Official sources: [submission requirements and rules](https://amazonappdev2026.devpost.com/rules), [FAQs](https://amazonappdev2026.devpost.com/details/faqs). Refresh the live form before final entry. diff --git a/docs/hackathon/demo_runb_v0.0.0alpha.md b/docs/hackathon/demo_runb_v0.0.0alpha.md new file mode 100644 index 00000000..48872afc --- /dev/null +++ b/docs/hackathon/demo_runb_v0.0.0alpha.md @@ -0,0 +1,94 @@ +# Candidate runbook and video plan + +**Preparation, not a completed demonstration.** Build/test commands below must be tied to the final candidate's run record. Offline contracts do not demonstrate a functioning Amazon-track experience. + +## Establish the source identity + +Use a fresh checkout of the exact candidate, not an unidentified Replit workspace or a moving branch. Before running: + +```bash +git rev-parse HEAD +git status --short +node --version +python3 --version +uv --version +``` + +Record those outputs with the run. The inspected baseline is `b5be72523b687c2fbf5fb45893d1d5643c086700`. It is not the final tested submission revision. Prerequisites are Node.js 20.19+ or 22.12+ (or a compatible later release), Python 3.12+, and uv. PostgreSQL is needed for the full application, not the offline checks. + +## Locked build and offline checks + +From the repository root: + +```bash +npm ci --include=dev +uv sync --locked +python3 scripts/subm_chec_offl_v0.0.0alpha.py +``` + +The runner uses installed dependencies and does not start the application. It runs the Python suite with `tests/test_artifacts.py` and `tests/test_live_server.py` excluded, two selected provider-contract checks, public request-boundary unit checks, TypeScript checking, and the production bundle. It excludes PostgreSQL-backed rate-enforcement tests, browser tests, and full application acceptance. The contract graph is the legacy CONTRACTS/CHECKS subset, not complete native-metadata verification. Repeated contract-wrapper cases must not be counted as additional unique coverage. + +The runner removes ambient service credentials, uses temporary home/log/state paths, and supplies a fail-closed loopback database fixture at port 1. This is process-environment isolation, **not an operating-system network sandbox**. Dependency installation needs network access. No real database or inference key is required for these checks. + +The standalone locked production-build entry point is: + +```bash +bash scripts/build-prod.sh +``` + +It resolves the repository root from the script location, installs `package-lock.json` with development build tools, validates `uv.lock` against the manifest, installs Python runtime dependencies, and builds in production mode. It does not launch the application. This production build omits Python development dependencies; run `uv sync --locked` again before subsequent offline tests. + +Implementation-checkout evidence reported 2026-10-22: + +- Production build passed when invoked from `/tmp`; 2,872 client modules and the server bundle were built without changing dependency locks. +- Host versions: Node 24.19.0, Python 3.12.14, uv 0.13.4. The runtime dependency installation resolved 98 packages. +- The Dockerfile's pinned uv 0.11.18 lock validation passed separately. A Docker image was **not** built because no Docker executable was available. +- Final integrated offline runner passed: 322 Python tests (4.64 s), the selected provider wrapper's 34 cases (1.07 s, repeated coverage), public request-boundary `--unit` checks, TypeScript checking, and the full production build. +- The suite includes 11 new checks for locked builds, source identity/refusals, credential-free validation, and the production launcher. Launcher tests use inert executables, including missing-runtime refusal and process-ownership checks; they do not start the app. +- `npm start` and `scripts/start-prod.sh` use one canonical launcher, which resolves the repository root and requires its locked Python environment before Express starts its sole Python child. + +These observations concern the implementation checkout, not a live deployment. Record the final committed candidate and its receipt with fresh run output; neither the passing checks nor the archive establishes a working track demo. + +## Export the exact committed source + +After source and documentation changes are committed: + +```bash +python3 scripts/subm_src_pack_v0.0.0alpha.py --output-dir ../a0-source-receipt +``` + +Choose a fresh output directory outside the checkout. The command requires a clean tracked HEAD and exports committed Git blobs only, with a deterministic compressed archive and JSON receipt containing the commit, tree, and archive SHA-256. It refuses submodules, LFS pointers, and archive-attribute transformations rather than silently omitting or rewriting source. Ignored/untracked files, installed dependencies, and Git history are excluded. + +Tracked secrets would be included like any other committed file; review the source before sharing. This verifies source bytes only. It does not scan history, prove runtime functionality, establish Replit parity, publish anything, or decide which repository is frozen. Keep receipts and test logs separate from the source so recording evidence does not change the commit it identifies. + +## Full application demonstration: outstanding + +The source routes the browser through Express `:5000`, then to FastAPI `:8001`; Vite `:5001` is development-only. The application needs an isolated PostgreSQL database and appropriate local-only account/configuration. Real model inference needs a configured provider key and may incur provider charges. Never publish a key or real account password for judges. + +Do not casually start the legacy `scripts/start-dev.sh` on a shared workstation: it terminates processes occupying its fixed ports. Python application startup also performs database setup/cleanup and starts background work. Until a candidate's isolated launch path is tested, this runbook does not present ordinary production startup as a safe judge sandbox. Do not point any test at a production database. + +The UI flow visible in source is `/register` or `/login`, authenticated chat at `/`, and the metadata-driven console at `/console`. Once isolated startup is verified, rehearse a fresh local account, a new conversation, one real response, reopening that same conversation, and the selected track-specific action. Use synthetic, non-sensitive example data. These UI steps are a rehearsal plan, not a report that they passed. + +To close this gate, record the exact launch command, isolated database creation/schema step, local URL, provider configuration names (not values), sample input, observed output, process shutdown, and test-data cleanup. A compiled frontend or health endpoint alone does not prove this flow. + +## Video storyboard: target 2:50 + +Do not record a final submission video until the track-specific segment works. Show actual screen footage. Clearly label a controlled failure or fake-provider test; never edit it to look like a successful live provider call. + +| Time | Screen evidence | Suggested narration and readiness | +| --- | --- | --- | +| 0:00–0:15 | Project name and the chosen user's task | “a0 explores how one task can stay continuous while the model doing the work changes.” Opening is source-backed. Add the concrete target user and task once chosen. | +| 0:15–1:15 | **Missing:** selected Amazon-track workflow completing an actual user action | No narration is finalized for this segment. Show the real input, track-specific runtime or explicitly labeled Alexa+ simulation, and observable result. A generic chat screenshot cannot fill this slot. | +| 1:15–1:55 | Same conversation retained; evidence of the new continuity/failure boundary | If demonstrated live: explain what remained continuous. If showing the contract suite: say, “This controlled test checks the harness boundary; it is not a live provider outage.” Do not claim a UI-only example proves long-context compaction. | +| 1:55–2:25 | August 31 baseline and September harness change, then the working candidate | “This was an existing project. During the event window, we added bounded conversation continuity and provider-failure handling that avoids automatic replay after tool execution.” Only keep features actually shown or clearly identified as test evidence. | +| 2:25–2:40 | Limitation caption and real result | “The checkpoint is intentionally lossy. The stored transcript remains the authoritative history.” Add the selected demo's concrete limit, not generic assurances. | +| 2:40–2:50 | Exact GitHub revision, runbook, and license | “The repository includes the source, setup instructions, and focused tests for this candidate.” Use the final tested link, not an untested branch tip. | + +Useful screenshots are the task input, the actual track interaction/result, and the unchanged conversation identity after continuation. Do not fabricate screenshots or include unrelated private conversations, credentials, or unlicensed media. + +## Before sharing the video + +1. Confirm the video, repository and description refer to the same tested revision. +2. Confirm the video visibly demonstrates the claimed platform/track, not just tests or slides. +3. Upload to public YouTube/Vimeo, check playback without the owner's login, and add that verified URL to the draft. +4. Resolve the [remaining evidence gates](subm_evid_v0.0.0alpha.md). A recorded video is not a completed Devpost entry. diff --git a/docs/hackathon/subm_evid_v0.0.0alpha.md b/docs/hackathon/subm_evid_v0.0.0alpha.md new file mode 100644 index 00000000..105adc54 --- /dev/null +++ b/docs/hackathon/subm_evid_v0.0.0alpha.md @@ -0,0 +1,64 @@ +# Submission evidence and remaining gates + +Checked 2026-10-22 against canonical source `b5be72523b687c2fbf5fb45893d1d5643c086700`, live Devpost requirements, and the official links below. Later implementation changes need their own exact-head run record. + +## Requirements/evidence matrix + +| Gate | Evidence in hand | Remaining proof or decision | +| --- | --- | --- | +| Primary-track functionality | Existing web-agent runtime; no verified Amazon-track implementation | Choose a track and demonstrate its actual required experience. | +| GitHub source and license | Public a0 repository; root AGPL license and consistent package declarations | Pin the tested candidate. Check all dependencies/assets are retrievable. Preserve third-party notices. | +| Reproducible run | Locked production build and scoped offline aggregate passed in the implementation checkout | Record final committed-head results and source receipt; full app/track runtime remains untested. | +| Existing-project update | Pre-start baseline and substantive post-start commits identified below | Video must show the chosen update working, not just a diff. | +| Description | [Repository-grounded draft](../../devpost-submission.md) | Remove unsupported claims; finish track-specific positioning. | +| Demo video | [Recording plan](demo_runb_v0.0.0alpha.md) | Record actual functionality, under three minutes; publish on YouTube/Vimeo and check public playback. | +| Tool feedback | [Evidence-based draft](tool_feed_v0.0.0alpha.md) | Confirm firsthand tool experience and fill unanswered opinions. | +| Entrant/team fields | Live form inventory in the draft | Submitter confirms details, eligibility, and representation. | +| Final entry | Local materials only | Enter required fields and obtain explicit authority for final submission. | + +Deadline: **2026-10-23 19:00 UTC / noon PDT**. A locally runnable repo and video can suffice; a public website and website-maintainer integration are not prerequisites. [Official rules](https://amazonappdev2026.devpost.com/rules), [FAQ](https://amazonappdev2026.devpost.com/details/faqs). + +## Track decision + +The material decision is: **Which a0 capability should the Amazon-track demonstration actually exercise?** + +| Option | Fit with a0 | New evidence needed | +| --- | --- | --- | +| Alexa+ through MCP/Agent Skill | Expose a bounded existing a0 capability rather than replace the application | A real working integration. The MCP route needs version 2025-11-25 or later and Streamable HTTP, with demonstrated calls. | +| Alexa+ simulated experience | Reuse the conversational frontend and agent runtime | An actual, explicitly simulated Alexa+ experience in source and video; generic chatbot screenshots are not proof. No particular SDK is required for this alternative. | +| Fire TV | Android source is present | Run and demonstrate the app on the qualifying Fire OS/Vega device or simulator; an Android shell alone does not establish that. | +| Bee or Ring | No connection found in the inspected implementation | Real Bee-device/Watch data use, or a working documented Ring API/SDK/simulator/device integration, respectively. | + +No option is selected by this document. The first two are the closest architectural fits, an engineering judgment rather than a ruling of eligibility. [Track rules](https://amazonappdev2026.devpost.com/rules). Gated Alexa+ partner tools are not needed; [the FAQ](https://amazonappdev2026.devpost.com/details/faqs) describes participant-accessible paths. + +## Existing-project evidence + +The event opened at `2026-08-31T17:15:00Z`. The last first-parent commit before that boundary is [`6dc1ac0f5cf9e7f1a232c086274ce1af405735ec`](https://github.com/The-Interdependency/a0/commit/6dc1ac0f5cf9e7f1a232c086274ce1af405735ec), timestamped `2026-08-31T13:04:25Z`. This establishes that a0 predates the event. + +| Change | Source evidence after the boundary | What it does not prove | +| --- | --- | --- | +| Android shell and bounded native bridge | [cab1aa4c](https://github.com/The-Interdependency/a0/commit/cab1aa4c1d48a7b0c8b9d33f59eb91c689777e77), September 7; `capacitor.config.ts`, `android/` | Fire TV/Vega compatibility or a tested APK. | +| Registry-driven compatible-provider adapter | [PR #106 merge 60a6b87a](https://github.com/The-Interdependency/a0/commit/60a6b87a6c39fd2293bbb944bf2b4aea27cbb708), September 10 UTC; `python/services/providers/open_comp_prov_v0.0.0alpha.py` | Successful calls to every configured provider. | +| Public-access boundary | [f4faf5c5](https://github.com/The-Interdependency/a0/commit/f4faf5c50dd5bc55e377279a0e403e088bcdced8), September 19; `python/services/publ_acce_poli_v0.0.0alpha.py`, `PUBLIC_RELEASE.md` | Completed public deployment, historical cleanup, or production privacy. | +| Continuous work and guarded failover | [PR #110 merge ad958a4e](https://github.com/The-Interdependency/a0/commit/ad958a4e5f4cdb17ba815539d2e6a545034db161), September 23; `python/services/work_harness.py`, `provider_failure.py`, `python/routes/chat.py`, `shared/schema.ts` | A live user task, actual provider availability, or safe replay outside the tested boundary. | + +Commit dates and source differences establish repository history, not independent authorship or eligibility certification. They support a specific significant-update description; the selected updates must still be shown functioning. + +Reproduce the history comparison from a full checkout: + +```bash +git log --first-parent --reverse --since='2026-08-31T17:15:00Z' \ + --format='%H %cI %s' b5be72523b687c2fbf5fb45893d1d5643c086700 +git diff --stat 6dc1ac0f5cf9e7f1a232c086274ce1af405735ec \ + b5be72523b687c2fbf5fb45893d1d5643c086700 +git show ad958a4e5f4cdb17ba815539d2e6a545034db161 -- \ + python/services/work_harness.py tests/test_work_harness_v0.0.0alpha.py +``` + +## Candidate identity and post-deadline preservation + +Before recording, capture the exact commit, dependency locks, run commands, outputs, and any deliberately mocked boundary. Use the same candidate in the repository link, description, and video. The earlier Replit revision `ca1aa77` is not established as equivalent to canonical `b5be7252`; old test totals must not be relabeled as current-candidate results. + +The [October 19 organizer update](https://amazonappdev2026.devpost.com/updates/46539-final-stretch) instructs entrants not to alter submitted code, video, or materials after the deadline until after the December 3 winner announcement. Decide which repository/materials will be frozen before entry. A pinned commit is useful identity evidence but does not, by itself, override an instruction not to edit the submitted repository. No separate repository, freeze mechanism, permission change, or publication is authorized by this document. + +Keep [PUBLIC_RELEASE.md](../../PUBLIC_RELEASE.md) intact. Local judging and public deployment have different acceptance gates. diff --git a/docs/hackathon/tool_feed_v0.0.0alpha.md b/docs/hackathon/tool_feed_v0.0.0alpha.md new file mode 100644 index 00000000..9c08eb24 --- /dev/null +++ b/docs/hackathon/tool_feed_v0.0.0alpha.md @@ -0,0 +1,43 @@ +# Product-feedback preparation + +Draft for fields 28303–28307, not a statement that the submitter used every dependency or agrees with these opinions. Confirm each tool actually used in the chosen demo. Do not manufacture Amazon-tool experience from a README mention or package name. + +## 1. Tools and purpose + +Source-backed starting copy: + +> a0 uses React and Vite for its web interface, Express for its authentication and request boundary, FastAPI for the Python orchestration service, and PostgreSQL for persisted application state. The continuous-work harness sits above provider adapters, including a registry-driven OpenAI-compatible transport. Pytest provides focused contract tests; the repository also includes TypeScript checking and Playwright browser tests. + +Before final use, name the exact provider and any Amazon-track tool exercised in the recorded candidate, along with its role. Dependency presence is not proof of use. No qualifying AWS runtime or Kiro development use is verified in this preparation. Do not list AWS as used merely because an S3-related package exists. + +## 2. What worked well + +Evidence-based observation, not yet a personal endorsement: + +> Separating the work harness from provider execution makes it possible to test continuity, explicit provider pinning, and safe fallback with controlled inputs. The contract tests can distinguish failures before a tool executes from failures after execution, rather than treating every error as safely repeatable. + +The locked production build was exercised successfully in the implementation checkout. Add final-candidate results and the submitter's firsthand setup, documentation, performance, or reliability experience for each tool. Do not state that onboarding was easy or that live services were reliable without such evidence. + +## 3. What needs work + +Source-grounded project limitation: + +> The full application has a multi-process setup with PostgreSQL and provider credentials. The legacy development launcher also stops processes on fixed ports, which is unsuitable for a casual trial on a shared workstation. Bounded continuity deliberately loses older detail; the full stored transcript remains necessary for authoritative inspection. + +These are a0's integration/setup limitations, not claims that Amazon's tools are faulty. Describe any candidate-run failure with its exact tool, command, expected result, actual result, workaround, and severity. If a new isolated launcher resolves the port issue, describe it as a resolved integration lesson rather than a current defect. + +## 4. Onboarding + +**Owner answer required.** Record the actual path from installation to first working example for each tool: versions, elapsed effort if known, documentation used, blockers, and eventual result. A successful installation log can support a factual sequence; it cannot establish how the experience felt to the submitter. + +No firsthand Amazon onboarding account has been supplied. Leave that gap visible until the chosen integration is exercised. + +## 5. Would you use it again? + +**Owner answer required: Yes/No, with a reason for each relevant device/service.** Do not infer an endorsement from current dependencies, previous project participation, or the decision to enter this event. + +## Development AI and personal attestations + +Field 29056 asks which AI development tools were used. Confirm their actual names and roles. Fields 29057 and 29058 ask the submitter's learning level and career value; neither can be completed from code inspection. This preparation does not answer those fields on the owner's behalf. + +If supplying optional friction-log material, include only observed facts from this build. Do not reuse a different project's experience or put account identifiers, secrets, private conversations, or unrelated user data in a public log. diff --git a/package-lock.json b/package-lock.json index d5f4b0f3..4a997cb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -136,6 +136,9 @@ }, "optionalDependencies": { "bufferutil": "^4.0.8" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@alloc/quick-lru": { diff --git a/package.json b/package.json index cd549f58..dbd04d4a 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "license": "AGPL-3.0-or-later", "scripts": { "dev": "NODE_ENV=development tsx server/index.ts", - "build": "tsx script/build.ts", - "start": "NODE_ENV=production node dist/index.cjs", + "build": "node --import tsx script/build.ts", + "start": "bash scripts/start-prod.sh", "check": "tsc", "db:push": "drizzle-kit push" }, @@ -145,5 +145,8 @@ }, "optionalDependencies": { "bufferutil": "^4.0.8" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" } } diff --git a/python/tests/test_subm_tool_v0.0.0alpha.py b/python/tests/test_subm_tool_v0.0.0alpha.py new file mode 100644 index 00000000..096002cb --- /dev/null +++ b/python/tests/test_subm_tool_v0.0.0alpha.py @@ -0,0 +1,186 @@ +# 129:33 0:0 0:0 +"""Self-contained source/check harness witnesses; no database or provider use. + +All subprocess Git operations use disposable repositories only. +""" +from __future__ import annotations + +# === CHECKS === +# id: check_submission_environment_isolated +# proves: submission_checks_remove_ambient_credentials +# call: self::test_submission_environment_isolated +# requires: python3 +# timeout: 10 +# mutates: environment +# cleanup: patch_dict_restore +# id: check_submission_stages_fail_closed +# proves: submission_checks_fail_closed, submission_checks_disclose_scope +# call: self::test_submission_stages_fail_closed +# requires: python3 +# timeout: 20 +# mutates: temporary_files +# cleanup: tempdir_teardown +# id: check_submission_source_exact +# proves: submission_source_commit_identity, submission_source_excludes_local_files +# call: self::test_submission_source_exact +# requires: python3, git +# timeout: 20 +# mutates: temporary_files +# cleanup: tempdir_teardown +# id: check_submission_source_refusals +# proves: submission_source_fail_closed, submission_source_commit_identity +# call: self::test_submission_source_refusals +# requires: python3, git +# timeout: 20 +# mutates: temporary_files +# cleanup: tempdir_teardown +# === END CHECKS === + +from contextlib import redirect_stdout, redirect_stderr +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import tarfile +import tempfile +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[2] + + +def _load(name: str): + path = ROOT / "scripts" / f"{name}_v0.0.0alpha.py" + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +checks = _load("subm_chec_offl") +source = _load("subm_src_pack") + + +def _git(root: Path, *arguments: str) -> bytes: + return subprocess.run(["git", "-C", str(root), *arguments], check=True, + capture_output=True, timeout=10).stdout + + +def _repo(base: Path) -> Path: + root = base / "repo" + root.mkdir() + _git(root, "init", "-q") + _git(root, "config", "user.email", "fixture@example.invalid") + _git(root, "config", "user.name", "Source fixture") + (root / "LICENSE").write_text("fixture license\n") + (root / "app.py").write_text("print('fixture')\n") + (root / ".gitignore").write_text(".env\n") + _git(root, "add", "LICENSE", "app.py", ".gitignore") + _git(root, "commit", "-qm", "fixture") + return root + + +def _raises(kind, function, *arguments): + try: + function(*arguments) + except kind: + return + raise AssertionError(f"expected {kind.__name__}") + + +def test_submission_environment_isolated(): + secrets = {"OPENAI_API_KEY": "private", "DATABASE_URL": "production", + "ADMIN_PASSWORD": "private", "GITHUB_TOKEN": "private", + "NODE_OPTIONS": "--require=unsafe", "PYTHONPATH": "/unsafe", + "PGSERVICE": "production", "AWS_PROFILE": "private"} + with patch.dict(os.environ, secrets): + environment = checks.check_environment(Path("/repo"), Path("/scratch")) + for name in secrets: + assert name not in environment or environment[name] != secrets[name] + assert environment["DATABASE_URL"].startswith("postgresql://a0_offline@127.0.0.1:1/") + assert environment["HOME"] == "/scratch" + commands = checks.check_commands(Path("/repo"), "/usr/bin/node") + flattened = [argument for _, command in commands for argument in command] + assert "--ignore=tests/test_artifacts.py" in flattened + assert "--ignore=tests/test_live_server.py" in flattened + rate_command = next(command for label, command in commands if label == "Public request boundary unit checks") + assert rate_command[-2:] == ["scripts/chec_publ_rate_v0.0.0alpha.ts", "--unit"] + assert not {"npx", "npm", "uv", "uvicorn", "db:push"}.intersection(flattened) + + +def test_submission_stages_fail_closed(): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name in (".venv/bin/python", "node_modules/typescript/bin/tsc", "node_modules/tsx/package.json"): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("fixture") + for outcome, expected in [(subprocess.CompletedProcess([], 7), 1), + (FileNotFoundError(), 2)]: + output = io.StringIO() + with patch.object(checks.shutil, "which", return_value="/usr/bin/node"), \ + patch.object(checks.subprocess, "run") as run, redirect_stdout(output), redirect_stderr(output): + if isinstance(outcome, Exception): + run.side_effect = outcome + else: + run.return_value = outcome + assert checks.run_checks(root) == expected + assert run.call_count == 1 + assert "Excluded:" in output.getvalue() + assert "PASS: all listed" not in output.getvalue() + with patch.object(checks.shutil, "which", return_value=None), \ + patch.object(checks.subprocess, "run") as run, redirect_stderr(io.StringIO()): + assert checks.run_checks(root) == 2 + run.assert_not_called() + + +def test_submission_source_exact(): + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + root = _repo(base) + (root / ".env").write_text("SECRET=never-export") + (root / "private-untracked.txt").write_text("never-export") + with patch.object(source, "git", wraps=source.git) as calls, \ + patch.dict(os.environ, {"GIT_DIR": "/not-the-source-repository"}): + first = source.export_source(root, base / "first") + assert any(call.args[1:] == ("rev-parse", f"{first['commit']}^{{tree}}") for call in calls.call_args_list) + second = source.export_source(root, base / "second") + assert first == second + archive = base / "first" / first["archive"] + assert hashlib.sha256(archive.read_bytes()).hexdigest() == first["archive_sha256"] + assert first["commit"] == _git(root, "rev-parse", "HEAD").decode().strip() + assert first["tree"] == _git(root, "rev-parse", "HEAD^{tree}").decode().strip() + assert first["tracked_file_count"] == 3 + with tarfile.open(archive, "r:gz") as bundle: + names = bundle.getnames() + assert not any(name.endswith(("/.env", "/private-untracked.txt")) for name in names) + assert bundle.extractfile(f"a0-{first['commit']}/app.py").read() == (root / "app.py").read_bytes() + receipt = base / "first" / f"a0-{first['commit']}-source.json" + assert json.loads(receipt.read_text()) == first + _raises(FileExistsError, source.export_source, root, base / "first") + _raises(ValueError, source.export_source, root, root / "output") + + +def test_submission_source_refusals(): + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + root = _repo(base) + (root / "app.py").write_text("changed\n") + _raises(ValueError, source.export_source, root, base / "dirty") + _git(root, "checkout", "--", "app.py") + (root / ".gitattributes").write_text("app.py export-ignore\n") + _git(root, "add", ".gitattributes") + _git(root, "commit", "-qm", "export exclusion") + _raises(ValueError, source.export_source, root, base / "omitted") + (root / ".gitattributes").write_text("") + (root / "app.py").write_text("version https://git-lfs.github.com/spec/v1\noid sha256:fixture\nsize 5\n") + _git(root, "add", ".gitattributes", "app.py") + _git(root, "commit", "-qm", "external object") + _raises(ValueError, source.export_source, root, base / "lfs") + commit = _git(root, "rev-parse", "HEAD").decode().strip() + _git(root, "update-index", "--add", "--cacheinfo", f"160000,{commit},external") + _git(root, "commit", "-qm", "submodule") + _raises(ValueError, source.export_source, root, base / "submodule") +# 129:33 0:0 0:0 diff --git a/scripts/build-prod.sh b/scripts/build-prod.sh index 08f594b8..a19cf837 100755 --- a/scripts/build-prod.sh +++ b/scripts/build-prod.sh @@ -1,5 +1,42 @@ #!/bin/bash -set -e -npm install -pip install -e . --quiet -npm run build +# === MODULE_BUILD === +# id: production_build_reproducibility +# module_name: production_build +# module_kind: hmmm +# summary: Install committed dependency locks and compile the existing production application. +# owner: a0 maintainers +# public_surface: scripts/build-prod.sh +# internal_surface: npm ci, uv sync, npm run build +# auth_boundary: none +# storage_boundary: write +# network_boundary: external +# user_data_boundary: none +# admin_only: false +# tests: tests/test_buil_prod_v0.0.0alpha.py +# rollout: existing production build entrypoint +# rollback: revert scripts/build-prod.sh and Dockerfile together +# === END MODULE_BUILD === +# === CONTRACTS === +# id: production_build_locked_dependencies +# given: A production build is invoked from any working directory. +# then: Install the repository's committed Node and Python locks without updating them, including Node build tools and excluding Python dev dependencies. +# class: reproducibility +# id: production_build_fail_closed +# given: A dependency installation or compilation command fails. +# then: Return its nonzero status and do not run later build stages. +# class: safety +# id: production_build_environment +# given: A caller has development or Replit environment variables set. +# then: Compile in production mode so development-only plugins remain disabled. +# class: reproducibility +# === END CONTRACTS === +set -euo pipefail + +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." + +# ci refuses stale/missing package-lock.json; --include keeps build tools available +# even when NODE_ENV=production or npm's omit setting otherwise excludes them. +npm ci --include=dev +# --locked checks manifest freshness as well as refusing lockfile changes. +uv sync --locked --no-dev +NODE_ENV=production npm run build diff --git a/scripts/start-prod.sh b/scripts/start-prod.sh index bffadc4a..71ebb3d6 100755 --- a/scripts/start-prod.sh +++ b/scripts/start-prod.sh @@ -1,6 +1,24 @@ #!/usr/bin/env bash +# === CONTRACTS === +# id: production_launcher_environment +# given: The production launcher is called from outside the repository with an unactivated Python environment. +# then: Express starts in the repository root with executable local .venv/bin/uvicorn ahead of global tools; an unavailable local runtime is rejected. +# class: reproducibility +# id: production_launcher_single_owner +# given: A production deployment starts through the launcher. +# then: Replace the launcher with Express and preserve its exit status without starting a second Python process. +# class: safety +# === END CONTRACTS === set -Eeuo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." +# uv sync installs the production Python dependencies here. Express inherits +# this PATH when it starts its sole Uvicorn child. +if [[ ! -x "$PWD/.venv/bin/uvicorn" ]]; then + printf '%s\n' '[start-prod] Missing executable .venv/bin/uvicorn. Run scripts/build-prod.sh to install the locked production dependencies.' >&2 + exit 1 +fi +export PATH="$PWD/.venv/bin:$PATH" export NODE_ENV=production # Production process ownership is singular: diff --git a/scripts/subm_chec_offl_v0.0.0alpha.py b/scripts/subm_chec_offl_v0.0.0alpha.py new file mode 100644 index 00000000..962556b2 --- /dev/null +++ b/scripts/subm_chec_offl_v0.0.0alpha.py @@ -0,0 +1,121 @@ +# 65:41 0:0 0:0 +"""Run the credential-free repository checks, not the live application. + +Usage: python scripts/subm_chec_offl_v0.0.0alpha.py +Install locked development dependencies first: npm ci; uv sync --locked. +The two excluded suites require a real PostgreSQL database or live servers. +Passing this command is not owner-use, deployment, or hackathon-track evidence. +""" +from __future__ import annotations + +# === MODULE_BUILD === +# id: a0_submission_offline_checks +# module_name: submission_offline_checks +# module_kind: instrument +# summary: Runs the existing repository checks with a bounded, credential-free environment and discloses live-test exclusions. +# owner: Erin Spencer +# public_surface: command line exit status and stage output +# internal_surface: check_environment, check_commands, run_checks +# auth_boundary: none +# storage_boundary: write +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: python/tests/test_subm_tool_v0.0.0alpha.py +# rollout: explicit local command only +# rollback: remove this command and its documentation; runtime is unchanged +# unresolved: this is process-environment isolation, not an operating-system network sandbox +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: submission_checks_remove_ambient_credentials +# given: the caller environment contains provider, database, administrator, or cloud credentials +# then: check processes receive only explicitly permitted executable paths and disposable test configuration +# class: safety +# id: submission_checks_fail_closed +# given: a required executable is missing, a stage fails, or a process cannot start +# then: the command exits nonzero and does not report complete verification +# class: correctness +# id: submission_checks_disclose_scope +# given: the offline repository verification command runs +# then: output identifies omitted database and live-server tests and does not start the application or invoke dependency installers +# class: evidence +# === END CONTRACTS === + +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[1] +EXCLUSIONS = ("tests/test_artifacts.py", "tests/test_live_server.py") + + +def check_environment(root: Path, scratch: Path) -> dict[str, str]: + """Build an allowlisted environment; never inherit service configuration.""" + return { + "PATH": str(root / ".venv" / "bin") + os.pathsep + os.defpath, + "HOME": str(scratch), + "TMPDIR": str(scratch), + "LANG": "C.UTF-8", + "PYTHONPATH": str(root), + "PYTHONNOUSERSITE": "1", + "DATABASE_URL": "postgresql://a0_offline@127.0.0.1:1/a0_offline", + "INTERNAL_API_SECRET": "offline-fixture-not-a-runtime-secret", + "SESSION_SECRET": "offline-fixture-not-a-runtime-secret", + "A0_STATE_PATH": str(scratch / "state.json"), + "A0_LOG_DIR": str(scratch / "logs"), + "NODE_ENV": "production", + } + + +def check_commands(root: Path, node: str) -> list[tuple[str, list[str]]]: + """Use installed executables directly: no npx downloads or uv sync.""" + python = str(root / ".venv" / "bin" / "python") + return [ + ("Python offline tests", [python, "-m", "pytest", "-q", "tests", "python/tests", + *(f"--ignore={path}" for path in EXCLUSIONS)]), + ("Provider contract graph", [python, "-m", "python.tests.contract_runner", + "--only", "check_openai_compatible_registry_wiring", + "--only", "check_openai_compatible_repair_regressions"]), + ("TypeScript", [node, "node_modules/typescript/bin/tsc"]), + ("Public request boundary unit checks", [node, "--import", "tsx", "scripts/chec_publ_rate_v0.0.0alpha.ts", "--unit"]), + ("Production bundle", [node, "--import", "tsx", "script/build.ts"]), + ] + + +def run_checks(root: Path = ROOT) -> int: + node = shutil.which("node") + required = (root / ".venv/bin/python", root / "node_modules/typescript/bin/tsc", + root / "node_modules/tsx/package.json") + if not node or any(not path.is_file() for path in required): + print("Missing locked development dependencies. Run npm ci and uv sync --locked first.", file=sys.stderr) + return 2 + print("Scope: credential-free repository checks; no application startup.", flush=True) + print("Excluded: tests/test_artifacts.py (PostgreSQL); tests/test_live_server.py (live servers).", flush=True) + print("The PostgreSQL public-rate check, browser tests, and full application acceptance are also excluded.", flush=True) + print("Contract graph scope is legacy CONTRACTS/CHECKS blocks, not complete native-metadata coverage.", flush=True) + print("This is not a live owner-task test or a Replit-source parity check.", flush=True) + with tempfile.TemporaryDirectory(prefix="a0-offline-") as directory: + environment = check_environment(root, Path(directory)) + # Preserve only the resolved Node executable's directory, not ambient PATH. + environment["PATH"] += os.pathsep + str(Path(node).parent) + for label, command in check_commands(root, node): + print(f"\n=== {label} ===", flush=True) + try: + result = subprocess.run(command, cwd=root, env=environment, check=False) + except OSError as exc: + print(f"ERROR: {label}: {type(exc).__name__}", file=sys.stderr) + return 2 + if result.returncode: + print(f"FAIL: {label} (exit {result.returncode})", file=sys.stderr) + return 1 + print("\nPASS: all listed offline stages. Database, browser, live provider and owner-task acceptance remain separate.") + return 0 + + +if __name__ == "__main__": + sys.exit(run_checks()) +# 65:41 0:0 0:0 diff --git a/scripts/subm_src_pack_v0.0.0alpha.py b/scripts/subm_src_pack_v0.0.0alpha.py new file mode 100644 index 00000000..8bbb579f --- /dev/null +++ b/scripts/subm_src_pack_v0.0.0alpha.py @@ -0,0 +1,161 @@ +# 106:40 0:0 0:0 +"""Export an exact Git commit as a deterministic source archive plus receipt. + +Usage: python scripts/subm_src_pack_v0.0.0alpha.py --output-dir ../a0-source +This never uploads, tags, freezes, publishes, or claims runtime verification. +Only committed Git blobs are exported. Ignored/untracked files are not read. +""" +from __future__ import annotations + +# === MODULE_BUILD === +# id: a0_submission_source_export +# module_name: submission_source_export +# module_kind: instrument +# summary: Exports an explicitly identified Git commit and binds the archive to its commit and tree without copying ignored or untracked local files. +# owner: Erin Spencer +# public_surface: command line archive and JSON receipt +# internal_surface: export_source +# auth_boundary: none +# storage_boundary: write +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: python/tests/test_subm_tool_v0.0.0alpha.py +# rollout: explicit local command only +# rollback: remove the command; existing source history is unchanged +# unresolved: Git object identity is not producer authentication or runtime proof; external LFS and submodule content is unsupported +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: submission_source_commit_identity +# given: a clean checkout with an exact committed source tree +# then: the archive contains that tree only and its receipt records the exact commit, tree, and archive SHA-256 +# class: evidence +# id: submission_source_fail_closed +# given: uncommitted tracked changes, submodules, LFS pointers, or a preexisting output artifact +# then: export refuses rather than omitting source dependencies, overwriting evidence, or claiming mixed-state provenance +# class: correctness +# id: submission_source_excludes_local_files +# given: ignored or untracked local credentials and runtime data exist beside the repository +# then: they are never read or included in the exported archive +# class: safety +# === END CONTRACTS === + +import argparse +import gzip +import hashlib +import io +import json +import os +from pathlib import Path +import subprocess +import sys +import tarfile + +ROOT = Path(__file__).resolve().parents[1] + + +def git(root: Path, *arguments: str) -> bytes: + # Ambient Git overrides must not redirect the requested repository or replace + # its objects. No credential/network operations are needed for a local export. + environment = {name: value for name, value in os.environ.items() if not name.startswith("GIT_")} + result = subprocess.run(["git", "--no-replace-objects", "-C", str(root), *arguments], + capture_output=True, check=True, env=environment) + return result.stdout + + +def export_source(root: Path, output: Path) -> dict: + root, output = root.resolve(), output.resolve() + if output == root or root in output.parents: + raise ValueError("output directory must be outside the source checkout") + if git(root, "status", "--porcelain", "--untracked-files=no").strip(): + raise ValueError("tracked changes must be committed before exporting source") + commit = git(root, "rev-parse", "HEAD^{commit}").decode().strip() + tree = git(root, "rev-parse", f"{commit}^{{tree}}").decode().strip() + entries = git(root, "ls-tree", "-r", "-z", commit).split(b"\0") + if any(entry.startswith(b"160000 ") for entry in entries): + raise ValueError("submodule content is not included by git archive; resolve source packaging first") + expected = {} + for entry in filter(None, entries): + metadata, path = entry.split(b"\t", 1) + mode, kind, oid = metadata.decode("ascii").split() + if kind != "blob": + raise ValueError("unsupported Git source entry") + expected[path.decode("utf-8")] = (mode, oid) + object_format = git(root, "rev-parse", "--show-object-format").decode().strip() + if object_format not in {"sha1", "sha256"}: + raise ValueError("unsupported Git object format") + archive = git(root, "archive", "--format=tar", f"--prefix=a0-{commit}/", commit) + seen = set() + with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as source: + for member in source.getmembers(): + if member.isdir(): + continue + path = member.name.removeprefix(f"a0-{commit}/") + if path not in expected or path in seen: + raise ValueError("archive contains an unexpected or duplicate source path") + mode, oid = expected[path] + if member.issym() and mode == "120000": + data = member.linkname.encode("utf-8") + elif member.isfile() and mode in {"100644", "100755"}: + stream = source.extractfile(member) + if stream is None: + raise ValueError("archive file cannot be read") + data = stream.read() + if data.startswith(b"version https://git-lfs.github.com/spec/v1\n"): + raise ValueError("LFS objects are not included by git archive; resolve source packaging first") + if bool(member.mode & 0o111) != (mode == "100755"): + raise ValueError("archive executable mode differs from Git tree") + else: + raise ValueError("archive type differs from Git tree") + identity = hashlib.new(object_format, f"blob {len(data)}\0".encode() + data).hexdigest() + if identity != oid: + raise ValueError("archive bytes differ from Git tree (for example export-subst)") + seen.add(path) + if seen != set(expected): + raise ValueError("archive omits committed files (for example export-ignore)") + compressed = io.BytesIO() + with gzip.GzipFile(fileobj=compressed, mode="wb", filename="", mtime=0) as stream: + stream.write(archive) + packed = compressed.getvalue() + name = f"a0-{commit}-source.tar.gz" + receipt = { + "schema": 1, + "source_kind": "committed_git_tree", + "commit": commit, + "tree": tree, + "archive": name, + "archive_sha256": hashlib.sha256(packed).hexdigest(), + "tracked_file_count": len(seen), + "excludes": ["ignored files", "untracked files", "Git history", "installed dependencies"], + "verification": "source bytes only; not build, runtime, Replit parity, or eligibility proof", + } + output.mkdir(parents=True, exist_ok=True) + archive_path = output / name + receipt_path = output / f"a0-{commit}-source.json" + if archive_path.exists() or receipt_path.exists(): + raise FileExistsError("output artifact already exists; choose a fresh output directory") + # Exclusive creation preserves earlier receipts; never overwrite an artifact. + with archive_path.open("xb") as stream: + stream.write(packed) + with receipt_path.open("x", encoding="utf-8") as stream: + json.dump(receipt, stream, indent=2, sort_keys=True) + stream.write("\n") + return receipt + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output-dir", type=Path, required=True) + args = parser.parse_args() + try: + print(json.dumps(export_source(ROOT, args.output_dir), indent=2, sort_keys=True)) + except (OSError, ValueError, subprocess.SubprocessError) as exc: + print(f"Source export failed: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) +# 106:40 0:0 0:0 diff --git a/tests/test_buil_prod_v0.0.0alpha.py b/tests/test_buil_prod_v0.0.0alpha.py new file mode 100644 index 00000000..d01d385a --- /dev/null +++ b/tests/test_buil_prod_v0.0.0alpha.py @@ -0,0 +1,238 @@ +# 168:44 0:0 0:0 +"""Isolated build-entrypoint checks; no package installs or app startup occur.""" + +# === CHECKS === +# id: check_production_build_locked_dependencies +# proves: production_build_locked_dependencies +# call: self::test_production_build_locked_dependencies +# mutates: filesystem +# cleanup: tempdir_teardown +# id: check_production_build_fail_closed +# proves: production_build_fail_closed +# call: self::test_production_build_fail_closed +# mutates: filesystem +# cleanup: tempdir_teardown +# id: check_production_build_environment +# proves: production_build_environment +# call: self::test_production_build_environment +# mutates: filesystem +# cleanup: tempdir_teardown +# id: check_container_recipe_reproducibility +# proves: container_recipe_reproducibility +# call: self::test_container_recipe_reproducibility +# mutates: none +# cleanup: none +# id: check_production_launcher_environment +# proves: production_launcher_environment +# call: self::test_production_launcher_environment +# mutates: filesystem +# cleanup: tempdir_teardown +# id: check_production_launcher_single_owner +# proves: production_launcher_single_owner +# call: self::test_production_launcher_single_owner +# mutates: filesystem +# cleanup: tempdir_teardown +# id: check_production_launcher_requires_local_runtime +# proves: production_launcher_environment +# call: self::test_production_launcher_requires_local_runtime +# mutates: filesystem +# cleanup: tempdir_teardown +# === END CHECKS === + +import json +import os +import shutil +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def _run_build(fail_stage: str = "") -> tuple[subprocess.CompletedProcess, list]: + """Run a copied entrypoint with inert npm/uv stubs from outside its repo.""" + with tempfile.TemporaryDirectory(prefix="a0-build-test-") as temporary: + base = Path(temporary) + repo = base / "repo with spaces" + scripts = repo / "scripts" + scripts.mkdir(parents=True) + script = scripts / "build-prod.sh" + shutil.copyfile(ROOT / "scripts/build-prod.sh", script) + # Sentinels detect an accidental wrapper-level lockfile rewrite. + for lock in ("package-lock.json", "uv.lock"): + (repo / lock).write_text("lock sentinel\n") + bin_dir = base / "bin" + bin_dir.mkdir() + log = base / "calls.log" + for command in ("npm", "uv"): + stub = bin_dir / command + stub.write_text( + "#!/bin/bash\n" + f"printf '%s|%s|%s|%s\\n' '{command}' \"$PWD\" \"$*\" " + "\"${NODE_ENV-}\" >> \"$BUILD_TEST_LOG\"\n" + f'if [[ "{command} $*" == "$FAIL_STAGE" ]]; then exit 42; fi\n' + ) + stub.chmod(0o755) + result = subprocess.run( + ["/bin/bash", str(script)], + cwd=base, + env={ + **os.environ, + "PATH": f"{bin_dir}:{os.environ['PATH']}", + "BUILD_TEST_LOG": str(log), + "FAIL_STAGE": fail_stage, + "NODE_ENV": "development", + "REPL_ID": "test-replit-context", + "NPM_CONFIG_OMIT": "dev", + }, + text=True, + capture_output=True, + timeout=20, + ) + calls = [line.split("|") for line in log.read_text().splitlines()] + assert all(Path(call[1]) == repo for call in calls) + for lock in ("package-lock.json", "uv.lock"): + assert (repo / lock).read_text() == "lock sentinel\n" + return result, calls + + +def test_production_build_locked_dependencies(): + result, calls = _run_build() + assert result.returncode == 0, result.stderr + assert [(call[0], call[2]) for call in calls] == [ + ("npm", "ci --include=dev"), + ("uv", "sync --locked --no-dev"), + ("npm", "run build"), + ] + + +def test_production_build_fail_closed(): + stages = ["npm ci --include=dev", "uv sync --locked --no-dev", "npm run build"] + for position, stage in enumerate(stages): + result, calls = _run_build(stage) + assert result.returncode == 42, (stage, result.stderr) + assert len(calls) == position + 1, (stage, calls) + + +def test_production_build_environment(): + result, calls = _run_build() + assert result.returncode == 0, result.stderr + assert calls[-1][3] == "production" + package = json.loads((ROOT / "package.json").read_text()) + assert package["scripts"]["build"] == "node --import tsx script/build.ts" + + +def test_container_recipe_reproducibility(): + """Check declared recipe policy, not an unexecuted Docker image's behavior.""" + dockerfile = (ROOT / "Dockerfile").read_text() + stages = [line for line in dockerfile.splitlines() if line.startswith("FROM node:")] + assert stages == [ + "FROM node:22-bookworm-slim AS builder", + "FROM node:22-bookworm-slim AS runner", + ] + workflow = (ROOT / ".github/workflows/clean-build-check.yml").read_text() + assert 'node-version: "22"' in workflow + builder, runner = dockerfile.split("FROM node:22-bookworm-slim AS runner", 1) + assert "RUN npm ci --include=dev" in builder + assert "RUN NODE_ENV=production npm run build" in builder + assert "RUN npm ci --omit=dev" in runner + assert "uv sync --locked --no-dev" in runner + assert "--frozen" not in runner + assert 'CMD ["node", "dist/index.cjs"]' in runner + # The lock's package tree remains the owner of exact dependency versions. + package = json.loads((ROOT / "package.json").read_text()) + lock_root = json.loads((ROOT / "package-lock.json").read_text())["packages"][""] + for field in ("dependencies", "devDependencies", "optionalDependencies", "engines"): + assert lock_root[field] == package[field] + + + +def _run_launcher( + exit_status: int = 0, venv_state: str = "ready" +) -> tuple[subprocess.CompletedProcess, list, int]: + """Observe inherited cwd/PATH and exec identity without launching the app.""" + with tempfile.TemporaryDirectory(prefix="a0-launcher-test-") as temporary: + base = Path(temporary) + repo = base / "repo with spaces" + scripts = repo / "scripts" + scripts.mkdir(parents=True) + launcher = scripts / "start-prod.sh" + shutil.copyfile(ROOT / "scripts/start-prod.sh", launcher) + venv_bin = repo / ".venv" / "bin" + venv_bin.mkdir(parents=True) + global_bin = base / "global-bin" + global_bin.mkdir() + log = base / "calls.log" + # Either Python stub records an unexpected launcher-owned process. + for binary_dir in (venv_bin, global_bin): + if binary_dir == venv_bin and venv_state == "missing": + continue + uvicorn = binary_dir / "uvicorn" + uvicorn.write_text( + '#!/bin/bash\nprintf "unexpected-python\\n" >> "$LAUNCH_TEST_LOG"\n' + ) + mode = 0o644 if binary_dir == venv_bin and venv_state == "nonexecutable" else 0o755 + uvicorn.chmod(mode) + node = global_bin / "node" + node.write_text( + '#!/bin/bash\n' + 'printf "%s|%s|%s|%s|%s|%s\\n" node "$PWD" "$*" ' + '"$NODE_ENV" "$(command -v uvicorn)" "$$" >> "$LAUNCH_TEST_LOG"\n' + 'exit "$LAUNCH_EXIT_STATUS"\n' + ) + node.chmod(0o755) + process = subprocess.Popen( + ["/bin/bash", str(launcher)], + cwd=base, + env={ + **os.environ, + "PATH": f"{global_bin}:{os.environ['PATH']}", + "NODE_ENV": "development", + "LAUNCH_TEST_LOG": str(log), + "LAUNCH_EXIT_STATUS": str(exit_status), + }, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + try: + stdout, stderr = process.communicate(timeout=20) + except subprocess.TimeoutExpired: + process.kill() + process.communicate() + raise + result = subprocess.CompletedProcess(process.args, process.returncode, stdout, stderr) + calls = [line.split("|") for line in log.read_text().splitlines()] if log.exists() else [] + if venv_state == "ready": + assert len(calls) == 1, calls + assert calls[0][1] == str(repo) + assert calls[0][4] == str(venv_bin / "uvicorn") + return result, calls, process.pid + + +def test_production_launcher_environment(): + result, calls, _ = _run_launcher() + assert result.returncode == 0, result.stderr + assert calls[0][0] == "node" + assert calls[0][2] == "dist/index.cjs" + assert calls[0][3] == "production" + package = json.loads((ROOT / "package.json").read_text()) + assert package["scripts"]["start"] == "bash scripts/start-prod.sh" + + +def test_production_launcher_single_owner(): + result, calls, launcher_pid = _run_launcher(exit_status=37) + assert result.returncode == 37, result.stderr + assert int(calls[0][5]) == launcher_pid + + + +def test_production_launcher_requires_local_runtime(): + for state in ("missing", "nonexecutable"): + result, calls, _ = _run_launcher(venv_state=state) + assert result.returncode == 1, result.stderr + assert "Missing executable .venv/bin/uvicorn" in result.stderr + assert "scripts/build-prod.sh" in result.stderr + assert calls == [], (state, calls) + +# 168:44 0:0 0:0