SPB Git

spb/autoscholar

Public
8.1 KB

# Architecture decisions — AutoScholar.ai

Each decision records what was verified vs assumed. Deviations from the master build instructions are explicit.

# D-01 Monorepo and language

pnpm workspace, TypeScript 5 (strict), Node ≥ 22 (nodes run 25/26), Biome for lint/format, Vitest for unit/integration, Playwright for e2e. Rationale: single toolchain on macOS Apple Silicon nodes that already have Node + pnpm (mld runtime detection).

# D-02 Web and API in one Next.js app (deviation from apps/api)

apps/web (Next.js 16, React 19, Tailwind 4) serves the public library, the admin panel and all HTTP APIs as route handlers (/api/public/*, /api/admin/*, /api/internal/workers/*). The suggested separate apps/api service is folded into apps/web to keep one public port behind the tunnel. Business logic lives in packages (db, auth, schemas, storage, research-contract, maclustr-adapter) so the API surface stays thin and testable. Deviation documented here; subsystem responsibilities are unchanged.

# D-03 Durable queue = PostgreSQL (FOR UPDATE SKIP LOCKED)

No Redis dependency. Runs/attempts/leases live in Postgres with lease expiry, fencing tokens (monotonic per run), idempotency keys (unique constraints) and an append-only event ledger. Redis exists on M3U96b but adds nothing at this scale.

# D-04 Control plane host = M3U96b:8470

Verified: 96 GB RAM, Postgres 17 + pm2 present, wg1 peer, free ports. Two pm2 processes: autoscholar (Next.js) and autoscholar-scheduler (orchestration loop). Deployed through mld with tunnel.domain = autoscholar.ai.

# D-05 Worker host = M4M64c, dedicated user asworker

Verified: empty node, 64 GB, macOS 26.4, Seatbelt + pf + launchd available. The worker daemon runs as a non-admin user via a LaunchDaemon (io.autoscholar.worker), not under the operator session; workspaces in /var/autoscholar/runs/<run-id>; env file /etc/autoscholar/worker.env (root:asworker 0640) holds the Anthropic key and the worker token. The operator installs it once with services/worker/install/install-macos.sh (sudo). The platform never uses SSH or sudo at runtime.

# D-06 Worker transport = outbound LAN HTTP with scoped token

Workers poll http://<control-plane LAN IP>:8470/api/internal/workers/* (lease, heartbeat, events, checkpoints, artifact upload). Public tunnel traffic to /api/internal/* is rejected. Enrollment: one-time enrollment secret → long-lived per-worker token (hashed in DB, revocable, rotated by POST …/rotate). mTLS not used (no PKI in cluster); the LAN is private and tokens are scoped. Documented limitation.

# D-07 Artifact store = versioned filesystem store with SHA-256 (deviation from S3)

No S3-compatible service exists in the cluster and no new paid product is introduced. packages/storage implements an immutable content-addressed store (<root>/runs/<run-id>/<stage>/<path> + .sha256 sidecars, write-once, verified on read) with a StorageBackend interface so S3 can be added later. Public artifacts are promoted (copied, re-hashed) into <root>/public/<paper-id>/v<n>/ and served by the web app with immutable cache headers. Backups: nightly pg_dump + rsync of the store to M3U96a (docs/OPERATIONS.md).

# D-08 Research engine = Claude Agent SDK (TypeScript) with API key

query() with settingSources: ['project'] so the frozen CLAUDE.md in the run workspace is the project instruction file (verified by integration test in Phase 5), permissionMode: 'dontAsk', explicit allowedTools, disallowedTools scoped rules, PreToolUse hook policy (platform-side), SDK sandbox (Seatbelt + hostname-allowlisted egress proxy, failIfUnavailable: true, allowUnsandboxedCommands: false), maxBudgetUsd/maxTurns/wall-clock abort from the manifest, persistSession: false, isolated CLAUDE_CONFIG_DIR and HOME per run. Auth: ANTHROPIC_API_KEY only (subscription login is not allowed for third-party products per official docs).

# D-09 Stage execution split

Stages 0–8 run on the worker as independently checkpointed SDK sessions (one session per stage, fresh context; the reviewer stage uses a separate session that cannot edit the manuscript directory — enforced by hook + filesystem denyWrite). Stage 9 (quality decision) runs on the control plane (services/qa) over the uploaded artifacts, never from text returned by the model. Stage 10 (publication) runs on the control plane (services/publisher) with credentials the worker does not have.

# D-10 Password hashing

Argon2id via @node-rs/argon2 (prebuilt darwin-arm64). Bootstrap password admin123 is read from ADMIN_BOOTSTRAP_PASSWORD only when the admin_users table is empty, hashed immediately, flagged must_change_password, and the env value is never logged. After rotation the old hash is kept in password_changes for audit and rejected on login.

# D-11 Search = PostgreSQL full-text (tsvector + GIN) + trigram for titles

No search engine. Re-evaluate only with measurements.

# D-12 Paper identifiers

AS-WP-<year>-<4-digit> from a Postgres sequence per year inside the publication transaction; slugs derived from the title + ID; DOIs not issued (no agency membership) — UI shows "No DOI".

# D-13 LaTeX toolchain = tectonic

Installed on the worker via Homebrew (no LaTeX exists on any node). Proprietary class research/templates/latex/autoscholar.cls + autoscholar-plots.mplstyle. PDF checks: pdfinfo-equivalent via pdf-lib/pdfjs in Node (page count, fonts embedded, text extractable) plus rendered page previews with pdftoppm if available (poppler installed with tectonic step) else mutool.

# D-14 Local development without MacLustr

pnpm dev runs web + scheduler against local Postgres (laptop has Postgres 17) with WORKER_MODE=simulated for tests only; simulated results are marked synthetic: true and can never be published (hard gate).

# D-15 Deviation: no separate apps/api, services/qa and services/publisher are libraries invoked by the scheduler process

They keep their own directories and tests but execute inside autoscholar-scheduler to avoid three extra pm2 processes with identical DB access. Responsibilities unchanged.

# D-16 Greenfield development exception to remote-first

Until the first deployment the repository was authored on the operator laptop (fast iteration, local Postgres, Playwright). First deployment: mld stage → mld deploy --node M3U96b → mld adopt (node directory = git clone, origin spbgit autoscholar.git, commit e709d44). From then on changes are committed and pushed through spbgit and pulled on the node (git pull && pnpm install && build && pm2 restart); the worker is a separate bundle (services/worker/install/bundle.sh) rsynced to /var/autoscholar/bundle on M4M64c. After launch the standard remote-first rule applies (edit on the node).

# D-17 Internal API source check

next start sets X-Forwarded-For to the socket address for direct connections; the gateway sets it to the public client address. The internal worker API therefore trusts the first XFF hop: it must be inside INTERNAL_API_ALLOWED_CIDRS (LAN/loopback), which excludes any request that came through the tunnel.

# D-18 Paper numbers reserved at launch

AS-WP-<year>-NNNN is drawn from the per-year sequence when a run is launched and stored in the immutable manifest (paperNumber), so the manuscript carries its final number. Numbers of runs that never publish remain unused (gaps are acceptable, as in most working-paper series); the publisher consumes the reserved number and only allocates a fresh one when the manifest has none (legacy runs).

# D-19 Retry after quality_failed = QA-driven revision pass

When a run that already completed every worker stage is leased again (after an admin retry), the worker downloads the server's review/qa-report.json, filters out platform-side gates (checksums, contract, attempt, worker), and runs one bounded revision session (reproduction-stage rules, 60 min, ≤ USD 20) listing the remaining hard failures, then rebuilds and re-finalizes (processes killed → artifacts synced → checksums.sha256 derived from uploaded digests). Platform-only failures re-finalize without a model session.

Files · main
Menu

Theme