Minimum security and privacy expectations for ProofPilot AI, plus its product-specific trust boundaries.
- Never commit secrets, credentials, tokens, private keys, production datasets, or sensitive local configuration.
- Use least privilege for users, services, agents, CI jobs, and deployment credentials.
- Treat all external input as untrusted.
- Preserve authorization checks at every trust boundary.
- Prefer secure defaults and explicit failure over silent fallback that weakens protection.
- Log security-relevant events without leaking secrets or unnecessary personal data.
- Do not disable security controls merely to unblock development.
Document the categories the product handles:
| Data type | Collected? | Sensitivity | Storage | Retention |
|---|---|---|---|---|
| Anonymous telemetry | TBD | Low/Medium | TBD | TBD |
| Account identifiers | TBD | Medium | TBD | TBD |
| Personal data | TBD | High | TBD | TBD |
| Payment data | TBD | High | TBD | TBD |
| Secrets/credentials | TBD | Critical | Never in source | Minimal |
- Local secrets belong in ignored environment/config files.
- CI/CD secrets belong in approved secret stores.
- Production secrets must not be copied into documentation, tickets, prompts, or logs.
- Provide sanitized
.env.exampleor equivalent when configuration guidance is needed. - Rotate exposed credentials immediately and treat repository history as compromised if a secret was committed.
- ProofPilot provider keys (
MODEL_API_KEY,BASETEN_API_KEY) are read only from the server-side environment (.env.local, gitignored). They are never logged, printed, written to evidence, or passed to the browser/oracle process (scrubEnv).
Document:
- identity provider or authentication mechanism;
- session/token model;
- authorization roles/permissions;
- tenant isolation rules;
- privileged/admin operations;
- recovery flows.
Security-sensitive authorization must be enforced server-side or at the authoritative trust boundary, not only in the UI.
- Pin/lock dependencies using the stack's normal mechanism.
- Review new dependencies for necessity and maintenance risk.
- Run dependency/security scanning appropriate to the stack.
- Do not accept generated packages, binaries, or build artifacts from unknown sources without verification.
| Asset | Trust | Control |
|---|---|---|
| Provider API keys | Secret | Env only; sent only to the allowlisted HTTPS host; redirect: "error"; errors redacted and truncated; scrubbed from child processes |
Trusted oracle (verifier/), verdict logic (packages/core/), patch policy |
Trust root | Denied by config/patch-policy.json; enforced in tests/security/trust-root.test.ts |
| Fixture code, page text, recordings, model output | Untrusted data | Never executed as instructions; verdicts come only from Playwright results via deterministic code |
Run evidence (.proofpilot/) |
Local, gitignored | SHA-256 hashed per run; contains no credentials |
| Billable inference | Owner-gated | Off unless PROOFPILOT_ALLOW_BILLABLE_CALLS=1 |
| User recording (C2A) | Untrusted input; leaves the machine only with consent | Validated locally first (real signature, size, duration, codec, full decode); consent bound to run + provider + SHA-256; bytes re-verified before upload; second opt-in PROOFPILOT_ALLOW_VIDEO_UPLOAD=1; expiring remote file deleted in finally; no retry of upload; local copy under gitignored .proofpilot/ |
Media tools (ffprobe/ffmpeg) |
Run on untrusted bytes | Only on a private 0700 temp copy of the validated bytes; -protocol_whitelist file; credential-scrubbed env; timeouts; no shell; temp dir always removed |
| Model output (Symptom) | Untrusted hypothesis | Strict local schema; unknown keys rejected; provenance set by code; can never become a verdict or oracle evidence; text rendered as data |
| Provider-returned file ids | Untrusted | Must match ^file-[A-Za-z0-9_-]{1,64}$ before any request path is built |
Data classification: the MVP handles only synthetic fixtures and owner-made recordings. Model inputs are sent to Meta (or Baseten if selected); this is not an offline-privacy product.
Known gaps:
- Live Meta behaviour. Two single-use live video-analysis smokes ran in C2B; run 002 completed (agent-reported, not independently replayed or provider-confirmed; see
docs/STATUS.md). The text/patch-proposal path has never been run live. generateTextis not safe for proposals. It uses the configured retry count (PROOFPILOT_INFERENCE_MAX_RETRIESmay exceed 0) and does not check Responsesstatus,error,incomplete_detailsor refusal items before extracting text. The video path is stricter. C3 must use a separate fail-closed adapter (tasks/PP-META-I2-C3.md);generateTextmust not be used for patch proposals.- The
text.formatand response shapes are OpenAI-compatible assumptions. H.264-only acceptance is a deliberate fail-safe pending live confirmation of other codecs. - No verified sandbox. Container validation is NOT VERIFIED (the agent environment lacks Docker access). In force today: no AI-generated code or candidate patch executes until an independently verified OS/container sandbox exists. This remains the rule for HTML, JavaScript, free-form CSS and multi-file patches.
- The policy lint is a pattern check, not a full secret scanner.
The owner approved decision SEC-C3-01 on 2026-10-28 (record: tasks/PP-META-I2-C3.md approval register, PR #6 comment 6460953906). The approval covers only the narrow design below for fixture F01. It does not certify that the unimplemented parts work, and it does not widen the "no AI-generated code executes" rule above for anything else:
- In force today: only hand-authored control inputs (PV-1/PC-1 and PV-2/NC-1) may be rendered and run, through the C3-0 staging harness (
scripts/c3-stage-spike.ts). No model-generated candidate may execute until the C3-A validator and its tests exist, the staging proof has passed independent exact-head audit, and any further required approval is recorded. - Still prohibited, pending independently verified OS/container isolation and a separate owner decision: model-authored HTML, JavaScript, free-form CSS, file paths, multi-file patches and any change to trust roots. Docker/sandbox remains NOT VERIFIED.
- Not approved: LIVE-C3-01 (12 Meta + 4 Baseten calls are proposed maximums only; the effective billable quota is 0, and any live inference needs separate explicit owner authorization) and UI-C3-01 (report layout).
The approved design:
- The model returns structured selector/property/value adjustments, never markup, scripts, CSS text or file paths. Trusted code parses strictly, scans the parsed keys and strings for forbidden content, validates against a closed grammar, renders CSS from host constants and applies it to
styles.cssin a disposable per-candidate staging workspace (pipeline:tasks/PP-META-I2-C3.md).product.html,app.js, the server and the whole trusted verifier are the approved-SHA bytes, hash-checked before and after the run. - The unmodified oracle runs in that staging workspace at its fixed root and fixed port 4173, strictly serial, under an exclusive lock. Nothing derived from a proposal is executed by Node.
- A candidate is reported as passing only if the oracle process exits 0, both trusted tests pass with the expected observations, the Playwright trace shows the staged candidate was served, integrity checks pass after the run, and the hashed evidence record was persisted and read back. Any abnormal exit or incomplete evidence is
INCONCLUSIVE.ORACLE_PASSis a single run and is notVERIFIED. - This removes model-authored executable content instead of containing it. It is not a sandbox and must not be described as one. Dependency integrity in the stage is detected, not enforced; browser binaries are outside the manifests.
- Playwright request interception is defense in depth only, tested in a separate harness that cannot award a verdict. It is not process or OS isolation, and candidate oracle runs are not instrumented because that would require changing trust-root verifier files.
- C3-A implementation (mock-tested; no live call exists):
packages/core/src/patch-proposal.ts(pipeline V0 to V7),packages/inference/src/patch-proposal.ts(pure pre-flight, response processing and artifact pieces) andreservations.ts(single-use run ids and quota slots). The production entry pointproposeCandidateis hard-disabled: it rejects before reading its argument or touching any provider, reservation or file, whatever transport, credentials, consent or quota the caller supplies (a caller-asserted transport label is not a security boundary and was removed).APPROVED_LIVE_QUOTA = {meta: 0, baseten: 0}is a frozen code constant. The full flow runs only in the test-only harnesstests/support/proposal-harness.ts, which production code never imports. Proposals require an explicitstatus: "completed". Orphan quota slots are never recycled and are listed read-only for operator reconciliation.createProposalConsentonly builds a record; real human approval for live use must be captured and audited separately. Proposal artifacts are labelledUNTRUSTED PROPOSAL — NOT VERIFIED. Nothing in C3-A executes a proposal. - C3-B1 (offline, hand-authored controls only): the coordinator (
candidate-coordinator.ts) imports no provider or proposal code;VERIFIEDis returned in exactly one pure function and is recomputed from persisted evidence byverifyScoreboard, which also requires the exact control inventory, recomputes NC-5, replay counts and metadata, binds the baseline and every replay (record and manifest) to the approved SHA and the baseline's trusted-file manifest, requires exactly oneF01-MOBILEand oneF01-DESKTOPsource proof per run, and rebuilds every published replay field (including mobile, desktop, failure message and evidence problems) from the persisted record and evidence, pinning the baseline toBASELINE/replay-1and each record to its control label; DID-1 (containment-demo.ts) is a separate Playwright-routing demonstration that is not a candidate verdict and not proof of candidate sandboxing.docs/BASETEN-COMPATIBILITY-PLAN.mdanddocs/MODAL-FEASIBILITY-PROPOSAL.mdare plans only: no Baseten or Modal request exists, a hosted backend is prohibited by the C3 contract (HOST-C3-01 not requested), and Modal is not a verified boundary. - C3-LIVE-01B0 (proposal orchestration; mock-only, enables nothing):
runSyntheticProposalAttempt(proposal-orchestrator.ts) runs preflight, authorize/consume/claim on a SYNTHETIC context, one scripted response, strict lifecycle/model/grammar validation, a sanitizedUNTRUSTED PROPOSAL — NOT VERIFIEDartifact, and settlement, then stops. It refuses production contexts and any transport that is not a data-onlyscriptedTransport(no fetch, env or credential), its post-claim tail is module-private, it is imported only by the synthetic rehearsal and not exported from the index, and a static test fails if any module combinesprepareProductionAttemptwith a model call (there is no production runner until a separately approved C3-LIVE-01B). Nested provider metadata is never copied:processProposalResponserebuildsusage(five counters, non-negative safe integers or null) andlifecycle(known status or null, short non-secret machine codes elseINVALID_CODE, literal booleans) from explicit allow-lists, so unknown nested keys, raw text and secret-shaped values cannot reach the persisted or returned artifact, and malformed values fail closed. Raw provider text and exception messages are never persisted; a persistence failure drops the adjustments; a settlement failure leaves the claim spent; no outcome retries or releases anything.proposeCandidate()still rejects before reading its argument whatever the environment or arguments say. The exact later activation diff is intasks/PP-META-I2-C3-LIVE-01B0.mdand is NOT applied. - C3-LIVE-01B-PREP (dormant production runner; enables nothing):
runProductionProposalAttempt(proposal-orchestrator.ts) takes only a run id and the bounded request. It runs a credential-free pre-flight,authorizeProductionAttempt(read-only), then opens the Meta provider (meta-proposal-transport.ts, zero-argument, retries 0, pinned model, allow-listed host; the only production call site ofstructuredResponse), thenprepareProductionAttempt(consume the approval, claim the reservation), then the same module-private tail as the synthetic runner (one request, strict validation, sanitized artifact in the fixed private.proofpilot/evidence/, settle, stop). It never usesprepareAttemptCoreor a synthetic context; the caller cannot supply a provider, transport, key, root, store, Git probe, clock, evidence directory or consent. Today it stops at authorization (empty register and pin, quota 0) before any credential is read. It is imported by nothing, not exported from the index, and there is no CLI. A missing credential fails after read-only authorization and before the approval is consumed. Tests run the unmodified production code from a throwaway copy with its own fixtures and a mockfetch. - C3-LIVE-01B (owner-approved activation implementation; the request is still gated): LIVE-C3-01 is recorded in
config/live-authorization.json(exactly one non-synthetic entry from the owner's comment) andAPPROVED_LIVE_QUOTAis{ meta: 1, baseten: 0 }, a lifetime total. Neither is sufficient: authorization also needs the owner-pinned activation revision (config/live-activation.json: while it isactivation: nullevery request is refused withACTIVATION_NOT_PINNED; once a separate pin-only commit records it, a checkout that is not the pinned tree or not a linear pin-only descendant is refused withACTIVATION_MISMATCH/REVISION_NOT_APPROVED, and a valid one is refused withAPPROVAL_MISSINGuntil an operator approval exists, in every case before any credential read, approval consumption, reservation or network), a clean tree at that pin-only-descended revision, a single-use operator approval bound to the exact run, provider, model, input hash, HEAD and activation revision, and the credential (read only after read-only authorization and before consumption, so a missing key spends nothing).proposeCandidate({ runId, request })delegates only torunProductionProposalAttemptand reads only those two fields; no caller-supplied root, provider, transport, Git probe, store, credential, quota, clock or evidence directory is consulted. The owner commandscripts/c3-live-proposal.tsrequires a real TTY, builds the input only from re-verified certified evidence (live-input.ts: certified F01 baseline for exactly HEAD and the fixture tree with every artifact re-hashed and the verdict re-derived; a live C2B analysis bound by SHA-256 to a certified recording), displays the exact input hash, evaluates the gate read-only, requires a typed phrase, makes one call and prints only sanitized fields; it has no loop, retry, fallback, provider, model, limit or credential option. The Git probe now runs with an allow-listed environment (no inherited credential, noGIT_DIR/GIT_WORK_TREEredirection). Failed, timed-out, interrupted and uncertain attempts stay permanently spent. An accepted proposal remainsUNTRUSTED PROPOSAL — NOT VERIFIEDand is not staged, executed or verified (C3-B2B is NOT APPROVED). Baseten and Modal stay unauthorized. - C3-LIVE-01A remediation (audit 2, revision identity): the owner pin authenticates the revision RELATIONSHIP, not only tree contents: besides the pin-excluded tree digest,
checkActivation(shared by the CLI and the validator) requires the pinned activation commit to be an ancestor of HEAD with every commit inA..HEADlinear and changing onlyconfig/live-activation.json(1 to 50 commits). An unauthorized clean same-tree descendant (--allow-empty, revert/recommit, empty commit before the pin, merge, same-tree sibling) is refused withREVISION_NOT_APPROVED. Re-pinning is a reviewed pin-only commit (or a reviewed new activation commit, then a pin-only commit). Boundary: git cannot prove a commit's SHA from its tree and the repository cannot authenticate commit authors; structure is enforced and the pin edit's authenticity is the owner's review of that commit. - C3-LIVE-01A remediation (audit 1): the only production authorization API is
authorizeProductionAttempt/prepareProductionAttempt(live-gate.ts); it derives the install root, register, owner-pinned activation revision (config/live-activation.json, a tree digest excluding the pin file), real Git state, directories and quota internally and ignores every other argument property, so a fake repository root, forged Git probe, synthetic approval or simulated quota cannot authorize a production attempt. The injectable core and CLI-with-context are not exported from the package index; a static test limits importers. HEAD must equal the owner-pinned activation revision (elseREVISION_NOT_APPROVED) for both the CLI and the validator; approvals are bound to the pinned digest. Existing.proofpilotand its parent must be real, self-owned, non-group/other-writable directories (refused, never chmod-ed). Limits: a same-user attacker with full write access can still forge local files; ownership checks for another user are not testable here. - C3-LIVE-01A (approval gate; enables nothing):
config/live-authorization.jsonis the only place a live decision can exist and it is EMPTY, soauthorizeProductionAttempt(the only production authorization API) fails withLIVE_NOT_APPROVED. Operator approvals (.proofpilot/approvals/<runId>.json, gitignored, directory 0700, files 0600) are created only by an interactive TTY command (scripts/c3-approve-live-run.ts) that reads no environment variable, requires the exact phraseAPPROVE <runId> <input-hash-prefix>, and is itself refused unless the repository register holds an approved decision. A record is self-declared and NOT authenticated identity; its digest detects accidents and edits, not an attacker who can write the operator's files, so authority comes from the register, the code quota and the audit trail. Records are published with temp file +link()(exclusive, atomic), read withO_NOFOLLOWand verified on the opened file (regular, one link, 0600, own uid), bound to run id, providermeta, modelmuse-spark-1.3, input hash, repository SHA and register digest, expire in 10 minutes (max 15), and are spent by exclusive creation of<runId>.consumedbefore the reservation is claimed, with expiry/HEAD/tree/lock re-checked in between; nothing is ever released. The synthetic rehearsal is zero-network, uses a disposable directory and a synthetic decision that the production wrapper rejects, and never producesVERIFIED. Model-derived adjustments remain disconnected from the staging runner and coordinator (static import checks). - C3-B2A (readiness only):
runProposalPreflight(scripts/c3-proposal-preflight.ts) is a deterministic offline check over mock literals: it has no provider construction, no fetch, no environment or file read of secrets (instrumented-environment tests prove zero access toMODEL_API_KEY/BASETEN_API_KEYby default); credential presence is a boolean and only on request; it reportsNOT_AUTHORIZED_TECHNICALLY_READYorTECHNICAL_BLOCKERSand can never authorize.runOfflineVerificationrefuses any control that is not one of the in-code hand-authored control OBJECTS (assertHandAuthoredControls): provenance is object identity (deep-frozen definitions registered in a module-private set), not field equality, so a model-derived clone with identical CSS and metadata, or a control carrying a foreigncheckcallback, is refused before any lock, stage, runner or browser; there is no production injection path. The default preflight never reads a provider API-key field (loadProviderConfig(..., {readApiKey:false})); only--check-credentialsreads the selected provider's key, to report presence. The Baseten discovery tool (scripts/baseten-model-discovery.ts) is owner-run only: dry run by default,--executeplusPROOFPILOT_ALLOW_PROVIDER_DISCOVERY=1for exactly one non-billableGET /modelswith retries forced to 0, no secret printed, public alias never substituted; nothing else imports it.docs/LIVE-C3-01-DECISION-RECORD.mdis a proposal and is NOT APPROVED; a credit balance is not permission to spend. Modal documentation lists vendor-documented egress controls but Modal is not a verified boundary and HOST-C3-01 is not approved. - Live proposal calls require a separate fail-closed adapter: completed lifecycle only,
retries: 0regardless of configuration, exact model id match on request and response, bounded input and output, one-time exclusive reservations that are burned on every outcome, and a pre-authorized per-provider slot count that defaults to 0.
AI agents may:
- edit code and docs inside assigned scope;
- run approved tests and analysis;
- prepare pull requests.
AI agents must never:
- reveal, print, exfiltrate, or intentionally expose secrets or credentials;
- falsify validation results or claim an unrun check passed;
- bypass, disable, or conceal a required review/security/quality gate;
- silently weaken authentication, authorization, tenant isolation, encryption, or other security boundaries.
AI agents may perform the following only when the current task includes explicit human authorization and an approved mechanism:
- rotate or change production credentials without exposing their values;
- alter production data;
- execute a production deployment or release;
- merge a reviewed change after the human approval gate has been satisfied.
Authorization for a privileged action does not authorize bypassing required validation, review, auditability, or security controls.
Logs should:
- support debugging and security investigation;
- avoid secrets;
- minimize personal/sensitive content;
- include correlation or request identifiers where useful;
- have documented retention for production systems.
For a suspected vulnerability:
- avoid public disclosure until impact is understood;
- preserve evidence and relevant logs;
- identify affected versions/environments;
- contain exposure;
- fix through reviewed changes;
- rotate compromised credentials where applicable;
- document durable lessons in project memory when useful.
Before production release, confirm:
- required security tests/checks passed;
- no unresolved critical/high-severity issue blocks the release;
- configuration and secrets are supplied through approved mechanisms;
- migration and rollback implications are understood;
- security/privacy-impacting changes received human review.