The Weftra Trust Page
GENERATED from committed evidence files — do not hand-edit; regenerate with npm run trust:export. Every number below carries the source path it was read from (spec 404 invariant I2); anything this generator could not find in a clean, no-network checkout renders as "not generated" with the command to produce it (AC1).
The queue, the approval, the ledger and the policy stay on your infrastructure, engine-neutral — the agent runs where you declare (a host sandbox, a vendor’s self-hosted environment, a rented harness), and FA records which. This page is the evidence behind that sentence, not a marketing restatement of it.
1. Authorization surface
Source: docs/security/authz-surface.json (machine-checked against every route by tests/authz-surface.test.ts — completeness, no stale rows, guard match, floor rules).
310 routes total, by guard tier:
| tier | routes |
|---|---|
admin | 98 |
admin-or-user | 69 |
explicit-admin | 15 |
project | 36 |
project-or-admin | 38 |
project-or-admin-or-approver | 8 |
public | 26 |
run-token | 5 |
session | 2 |
user | 13 |
By blast radius:
| blast_radius | routes |
|---|---|
credentials | 28 |
execution | 16 |
none | 9 |
read | 126 |
secrets | 9 |
write | 122 |
The tenant-reachable set (108 routes)
A project API key (fa_…) is FA’s lowest-privilege credential (CLAUDE.md’s Trust Boundary law). These are every route a tenant key can reach, either unauthenticated (public) or via a guard that accepts a project key alongside admin/approver (project, project-or-admin, project-or-admin-or-approver) — listed explicitly, not summarized, because this is the question CLAUDE.md’s Trust Boundary law asks first.
| Method | Path | Guard | Tier | Blast radius |
|---|---|---|---|---|
| GET | /.well-known/oauth-authorization-server | none | public | read |
| GET | /.well-known/oauth-protected-resource | none | public | read |
| GET | /api/audit/export | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/audit/manifest | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/audit/records | requireProjectOrAdminAuth | project-or-admin | read |
| POST | /api/builder/agent/interview | requireProjectAuth | project | write |
| POST | /api/builder/agent/interview/:id/draft | requireProjectAuth | project | write |
| POST | /api/builder/agent/interview/:id/refine | requireProjectAuth | project | write |
| GET | /api/builder/branches | requireProjectAuth | project | read |
| POST | /api/builder/clarify | requireProjectAuth | project | write |
| POST | /api/builder/draft | requireProjectAuth | project | write |
| POST | /api/builder/estimate | requireProjectAuth | project | read |
| POST | /api/builder/product/:id/artifacts | requireProjectOrAdminAuth | project-or-admin | write |
| POST | /api/builder/product/:id/artifacts/:lineageId/versions | requireProjectOrAdminAuth | project-or-admin | write |
| POST | /api/builder/quickpick | requireProjectAuth | project | write |
| POST | /api/builder/refine | requireProjectAuth | project | write |
| POST | /api/builder/submit | requireProjectAuth | project | write |
| GET | /api/engines | resolveSessionAuth+requireCsrf | public | read |
| GET | /api/features/ | requireProjectAuth | project | read |
| POST | /api/features/ | requireProjectAuth | project | write |
| GET | /api/features/:id | requireProjectAuth | project | read |
| PATCH | /api/features/:id | requireProjectAuth | project | write |
| GET | /api/features/:id/artifacts | requireArtifactAuth | project-or-admin | read |
| POST | /api/features/:id/artifacts | requireArtifactAuthOrSessionAdmin | project-or-admin | write |
| DELETE | /api/features/:id/artifacts/:artifactId | requireArtifactAuth | project-or-admin | write |
| GET | /api/features/:id/artifacts/:artifactId | requireArtifactAuthOrSessionAdmin | project-or-admin | read |
| POST | /api/features/:id/cancel | requireProjectAuth | project | write |
| GET | /api/features/:id/checkpoints | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/features/:id/checkpoints/:a/compare/:b | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/features/:id/checkpoints/:checkpointId | requireProjectOrAdminAuth | project-or-admin | read |
| POST | /api/features/:id/clarifications/:clarificationId/answer | requireProjectAuth | project | write |
| POST | /api/features/:id/create-pr | requireProjectAuth | project | write |
| GET | /api/features/:id/escalations | requireProjectOrAdminOrUserAuth | project-or-admin-or-approver | read |
| POST | /api/features/:id/escalations | requireProjectOrAdminOrUserAuth | project-or-admin-or-approver | write |
| GET | /api/features/:id/events | requireProjectAuth | project | read |
| GET | /api/features/:id/evidence-pack | requireProjectOrAdminOrUserAuth | project-or-admin-or-approver | read |
| POST | /api/features/:id/evidence-pack/verify | requireProjectOrAdminOrUserAuth | project-or-admin-or-approver | write |
| POST | /api/features/:id/notify | requireProjectAuth | project | write |
| POST | /api/features/:id/permissions/:permissionId/resolve | requireProjectAuth | project | write |
| GET | /api/features/:id/provenance | requireProjectAuth | project | read |
| GET | /api/features/:id/relationships | requireProjectOrAdminOrUserAuth | project-or-admin-or-approver | read |
| POST | /api/features/:id/relationships | requireProjectOrAdminOrUserAuth | project-or-admin-or-approver | write |
| DELETE | /api/features/:id/relationships/:relationshipId | requireProjectOrAdminOrUserAuth | project-or-admin-or-approver | write |
| GET | /api/features/:id/replay | requireProjectAuth | project | read |
| POST | /api/features/:id/rerun | requireProjectAuth | project | write |
| POST | /api/features/:id/retry | requireProjectAuth | project | write |
| POST | /api/features/:id/revise | requireProjectAuth | project | write |
| POST | /api/features/:id/wont-merge | requireProjectAuth | project | write |
| GET | /api/features/cost-summary | requireProjectAuth | project | read |
| GET | /api/features/permissions/pending | requireProjectAuth | project | read |
| GET | /api/features/report | requireProjectAuth | project | read |
| GET | /api/features/report/timeseries | requireProjectAuth | project | read |
| GET | /api/instance/verification-key | none | public | read |
| DELETE | /api/mcp/ | requireProjectAuthOrMcpOAuthToken | project | none |
| GET | /api/mcp/ | requireProjectAuthOrMcpOAuthToken | project | none |
| POST | /api/mcp/ | requireProjectAuthOrMcpOAuthToken | project | write |
| GET | /api/product/openapi.json | none | public | read |
| GET | /api/project/ | requireProjectAuth | project | read |
| PATCH | /api/project/ | requireProjectAuth | project | write |
| GET | /api/project/usage | requireProjectAuth | project | read |
| GET | /api/projects/:id/discovery | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/discovery/:runId | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/env/check | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/env/check/logs | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/env/scaffold | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/env/scaffold/logs | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/knowledge | requireProjectOrAdminAuth | project-or-admin | read |
| POST | /api/projects/:id/knowledge | requireProjectOrAdminAuth | project-or-admin | write |
| PATCH | /api/projects/:id/knowledge/:kid | requireProjectOrAdminAuth | project-or-admin | write |
| GET | /api/projects/:id/product/artifacts | requireProjectOrAdminAuth | project-or-admin | read |
| POST | /api/projects/:id/product/artifacts | requireProjectOrAdminAuth | project-or-admin | write |
| GET | /api/projects/:id/product/artifacts/:lineageId | requireProjectOrAdminAuth | project-or-admin | read |
| POST | /api/projects/:id/product/artifacts/:lineageId/propose-ready | requireProjectOrAdminOrUserAuth | project-or-admin-or-approver | write |
| POST | /api/projects/:id/product/artifacts/:lineageId/review | requireProjectOrAdminAuth | project-or-admin | write |
| POST | /api/projects/:id/product/artifacts/:lineageId/versions | requireProjectOrAdminAuth | project-or-admin | write |
| GET | /api/projects/:id/product/connectors/:connectorId/ingests | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/product/connectors/:connectorId/ingests/:runId | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/product/connectors/:connectorId/sources | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/product/export | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/product/sources/:lineageId | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/skills | requireProjectOrAdminAuth | project-or-admin | read |
| POST | /api/projects/:id/skills | requireProjectOrAdminAuth | project-or-admin | write |
| GET | /api/projects/:id/skills/:skillId | requireProjectOrAdminAuth | project-or-admin | read |
| GET | /api/projects/:id/skills/:skillId/effectiveness | requireProjectOrAdminAuth | project-or-admin | read |
| POST | /api/projects/:id/skills/:skillId/revise | requireProjectOrAdminAuth | project-or-admin | write |
| GET | /api/projects/:projectId/triggers | requireProjectOrAdminAuth | project-or-admin | read |
| POST | /api/projects/:projectId/triggers/:provider | none | public | write |
| POST | /api/projects/:projectId/triggers/rotate-secret | requireProjectOrAdminAuth+closeDevModeOpenPassForTriggerSecretRotation | project-or-admin | credentials |
| GET | /api/status | resolveSessionAuth+requireCsrf | public | read |
| GET | /api/verify/ | none | public | read |
| GET | /auth/github | none | public | none |
| GET | /auth/github/callback | none | public | credentials |
| GET | /auth/gitlab | none | public | none |
| GET | /auth/gitlab/callback | none | public | credentials |
| GET | /auth/google | none | public | none |
| GET | /auth/google/callback | none | public | credentials |
| POST | /auth/local/login | none | public | credentials |
| POST | /auth/local/reset | none | public | credentials |
| POST | /auth/local/reset-request | none | public | credentials |
| POST | /auth/login/apikey | none | public | credentials |
| POST | /auth/logout | none | public | credentials |
| GET | /auth/oidc/callback | none | public | credentials |
| GET | /auth/oidc/login | none | public | none |
| GET | /auth/status | none | public | none |
| GET | /health | resolveSessionAuth+requireCsrf | public | none |
| POST | /oauth/mcp/register | none | public | write |
| POST | /oauth/mcp/revoke | none | public | credentials |
| POST | /oauth/mcp/token | none | public | credentials |
2. Execution boundary
CLAUDE.md’s Trust Boundary law: "Execution happens in DockerRuntime. Never on the host." Source: src/services/runtime/docker-runtime.ts (the argv builders below are pure, imported functions — not transcribed) and a static scan of src/ for every child_process import outside src/services/runtime/.
17 spawn sites outside src/services/runtime/, every one allowlisted with a reason (tests/child-process-tripwire.test.ts fails the build on a new, unlisted one):
| File | Why it is not a boundary breach |
|---|---|
src/cli/lib/git.ts | src/cli is the standalone operator CLI (not the FA server) — execFileSync runs git with hardened flags for the operator's own local repository operations. No HTTP route, no tenant input reaches this file. |
src/cli/lib/keychain.ts | src/cli operator tool — execFileSync shells out to the OS keychain (macOS security / freedesktop secret-tool) to store the operator's OWN CLI credential locally, argv never carries the secret value. No HTTP route, no tenant input reaches this file. |
src/services/agent/claude-runner.ts | Outer host spawn() invokes ONLY the command Runtime.wrap() resolves — docker exec under RUNTIME=docker, a bash -lc passthrough under the local dev opt-out — through the fixed HOST_SPAWN_ENV_ALLOWLIST env. The project's own commands run inside DockerRuntime's container, never as a second execution path on this host process. |
src/services/agent/exec.ts | FA's own git plumbing (clone/branch/commit/push) with fixed, FA-authored argv (buildGitHardeningFlags, execGitHardened) and an env built from nothing plus explicit credentials — manages the checkout itself, never runs a project-declared command. setup_command/test_command/verify_command are dispatched through Runtime.wrap() (src/services/agent/gates.ts), not this file. |
src/services/agent/gates.ts | Same outer-wrapper pattern as claude-runner.ts: spawn() runs only runtime.wrap('bash', ['-lc', testCommand])'s resolved {cmd, args} — under RUNTIME=docker that resolved command is a docker exec into the sandbox container, where the project's test/verify command actually executes. This call is the invocation mechanism, never a second execution path. |
src/services/agent/mcp-gateway-upstream.ts | Same outer-wrapper pattern — the file's own header comment: 'the gateway spawns runtime.wrap(entry.command, entry.args, ...)'s result ... this NEVER falls back to a bare host spawn' (fail-closed if wrap() is unavailable). |
src/services/agent/workspace.ts | Read-only docker image inspect <image> existence probe (stdio ignore) run before pulling/building a runtime image — queries the Docker CLI about a local image, executes nothing project-declared. |
src/services/engine/acp-engine.ts | Same outer-wrapper pattern as claude-runner.ts — the file's own header comment: 'Always spawn THROUGH the runtime — never directly against the host.' Spawns only runtime.wrap()'s resolved {cmd, args}. |
src/services/engine/headless-engine.ts | Same outer-wrapper pattern as claude-runner.ts/acp-engine.ts — spawns only task.runtime.wrap()'s resolved {cmd, args}; env from buildSubprocessEnv's allowlist, never a raw tenant command. |
src/services/release-notes.ts | Read-only git introspection (rev-parse/log/diff, spawnSync with hardened flags) over FA's OWN repository at doc-generation time — a build-time tool, not a route any tenant reaches. |
src/tools/capability-docs-gate.ts | CI/build gate (the capability-docs role-check script) — execFileSync runs git against a checkout with hardened flags. Not part of the running server; no tenant input reaches it. |
src/tools/depcruise-runner.ts | CI/lint tool (npm run arch:check) — spawnSync runs the dependency-cruiser binary with fixed args against FA's own src/ tree. Not part of the running server. |
src/tools/deps-audit.ts | CI/build tool (npm run deps:audit) — execSync runs npm audit --json over FA's own lockfile. Not part of the running server; no HTTP route reaches it. |
src/tools/diagnose.ts | Operator-run diagnostic CLI (npm run diagnose) — spawns the Claude CLI directly on the host to test the OPERATOR's own local setup. Not an HTTP route; no tenant input reaches it. |
src/tools/doc-drift-gate.ts | CI/build gate (the doc-drift check script) — imports execFileSync alongside the same family of git-backed doc-drift tooling as capability-docs-gate.ts (currently unused in this file's body). Not part of the running server; no tenant input reaches it. |
src/tools/merge-preflight.ts | CI/CLI tool (npm run merge-preflight) — execFileSync runs git with a fixed argv array (no shell). Not part of the running server. |
src/tools/sandbox-exec.ts | THE canonical sandboxed-execution entry point CLAUDE.md names explicitly ("every agent run is sandboxed — src/tools/sandbox-exec.ts"). Its host spawn() invokes only runtime.wrap()'s resolved wrapper command, never a raw tenant command directly. |
The sandbox argv the runtime builds
From sandboxResourceLimitArgs() (the one resource ceiling every FA-started sandbox container carries):
--cpus 2 --memory 4g --pids-limit 512From sandboxHardeningArgs() under the default strict posture (shown with the repo-relative seccomp profile path in place of this checkout’s absolute path, for a reproducible render — the flag itself is identical):
--cap-drop ALL --security-opt no-new-privileges --security-opt seccomp=containers/seccomp/fa-default.json --read-only --tmpfs /tmp:rw,exec,nosuid,nodev,mode=1777 --ulimit nofile=65536:65536From authArgs() — what credential material (if any) reaches the container, by auth_mode. A secret VALUE never appears in argv; api mode passes only the variable name:
api: -e ANTHROPIC_API_KEY -e HOME=/tmp
oauth: -e HOME=/tmp
none: -e HOME=/tmpNetwork and egress defaults (src/config.ts)
| Setting | Default | Meaning |
|---|---|---|
RUNTIME | docker | Isolated per-run container. local is a development-only host opt-out — see docs/SETUP.md and docs/OPERATIONS.md §"Docker socket / runtime sandbox posture". |
FA_RUNTIME_NETWORK | bridge | The agent job container has outbound network access by default (needed to reach the model API); filesystem isolation is unaffected. |
FA_BOOTSTRAP_GIT_EGRESS | enforced | The bootstrap clone container is attached to an FA-owned --internal network behind a CONNECT forwarder permitting exactly the current run’s repo host on port 443. Fail-closed: no automatic fallback to open. |
FA_RUNTIME_EGRESS | open | Opt-in allowlist mode for the agent job container itself (allowlist) is available but not the default; open is byte-identical to bridge with no egress restriction. |
FA_SANDBOX_HARDENING | strict | --cap-drop ALL, no-new-privileges, a seccomp profile, and a read-only rootfs. compat is an explicit, recorded operator opt-out to the pre-hardening argv — never a silent fallback. |
Containment vocabulary. Every run today executes under exactly one shape: FA’s own DockerRuntime (full containment) or, under the RUNTIME=local development opt-out, the host process directly (no containment — development only, never the documented deployment shape). A formal multi-tier containment taxonomy (fa-hosted-sandbox / remote-engine, for a future engine that FA does not itself execute) is proposed in docs/design/fa-roadmap.md spec 161 and is explicitly design-only, not built — this page does not claim it exists yet.
3. Derived state
Source: docs/security/derived-state-inventory.json (machine-checked by tests/derived-state-inventory.test.ts, which parses and type-checks every src/**/*.ts module-level Map/Set declaration at test time).
153 module-level derived-state declarations, every one keyed or bounded by the scope dimension(s) that make its reads correct for the tenant, project, run, or process it belongs to (no live under-scoped cache on this baseline), by kind:
| kind | count |
|---|---|
cache | 12 |
constant | 96 |
dedup | 17 |
memo | 2 |
registry | 26 |
4. Tests and pins
Test/file counts: not generated in this render — run npx vitest run --reporter=json --outputFile=vitest-report.json and re-run npm run trust:export.
Coverage: not generated in this render — run npm test -- --coverage and re-run npm run trust:export.
Pinned invariants CLAUDE.md names (a source-tripwire test fails the build if the invariant moves without the pin being updated in the same change):
| Present | Invariant | Test file |
|---|---|---|
| ✅ | A project field aliased into the feature join (PROJECT_RENAMED_FIELDS) | tests/project-join-constant.test.ts |
| ✅ | Dashboard attribute-escaping (esc() site counts, no innerHTML templates) | tests/spec-259-dashboard-attribute-escaping.test.ts |
| ✅ | Route guard roles + a matching USER_GUIDE Role: line | tests/route-roles.test.ts |
| ✅ | Operator-identity withholding from a tenant-key ledger read | tests/spec-334-operator-identity-ledger-withhold.test.ts |
| ✅ | docs/features.yml → capabilities.md byte-match | tests/capabilities-doc.test.ts |
| ✅ | docs/features.yml → build-tracker.md byte-match | tests/build-tracker.test.ts |
| ✅ | A tenant route refuses reviewer-arming fields before the first write | tests/spec-256-reviewer-arming-guard.test.ts |
| ✅ | A feature-selection query carries the unresolved-prior-decision guard | tests/prior-decision-relaunch-guard.test.ts |
| ✅ | The generated-path merge resolver names no specific generator/tool (§VII) | tests/spec-385-generated-path-merge-resolution.test.ts |
| ✅ | Authorization-surface inventory completeness (this page’s own source) | tests/authz-surface.test.ts |
| ✅ | Derived-state inventory completeness (this page’s own source) | tests/derived-state-inventory.test.ts |
| ✅ | Every child_process import outside src/services/runtime/ is allowlisted with a reason | tests/child-process-tripwire.test.ts |
| ✅ | This page byte-matches a fresh regeneration | tests/trust-page.test.ts |
| ✅ | This page and README/OPERATIONS/SETUP never claim an unheld certification or an unshipped integration | tests/trust-page-forbidden-phrases.test.ts |
5. Supply chain
SBOM: not generated in this render — run npm run sbom and re-run npm run trust:export.
deps:audit (npm run deps:audit, CI-gated — see .github/workflows/ci.yml) is not re-run by this generator (it needs network access to the advisory database, and AC1 requires this generator to run with none). Declared waivers on file: 1 (from deps-audit-waivers.json, each carrying a justification, scope, and expiry — run npm run deps:audit for the live pass/fail result).
Node.js: 18+ (CLAUDE.md Tech Stack — not pinned in package.json engines).
6. Ledger and audit
150 distinct run-event types found by a static scan of recordRunEvent(s) call sites in src/ (a lower bound — a computed/templated type string is not literal and is not counted). The ledger itself (run_events) is append-only; no pruning code was found in src/ for it, so retention is unbounded unless an operator prunes the database file directly.
Hash-chain export routes (from docs/security/authz-surface.json):
| Method | Path | Guard |
|---|---|---|
| GET | /api/audit/export | requireProjectOrAdminAuth |
| GET | /api/audit/manifest | requireProjectOrAdminAuth |
| GET | /api/audit/records | requireProjectOrAdminAuth |
Export format: hash-chained JSONL/CSV. Push integration into a SIEM platform is not shipped — it is roadmap RM-096.
7. Adversarial review coverage
Source: docs/security/adversarial-review/COVERAGE.md.
1 of 65 in-scope surfaces examined (2%). Coverage counts surfaces looked at, not surfaces proven safe — see the source file’s own "What this number does NOT say" section.
8. Incident record
PR #173 (2026-07-17) shipped a tenant-reachable deploy route with no approval that ran a request-body command on the FA host with the full process environment — a tenant’s project key could read every FA secret. It was rated auto-merge eligible by the loop’s own risk classifier and approved by the Reviewer; CI was green. Caught by the operator asking, not by any automated gate. Closed, not merged.
Full account and the fixes that followed: CLAUDE.md — "Core Law: The Trust Boundary"; this page’s own Execution boundary and Authorization surface sections above are the mechanical checks that incident produced. docs/OPERATIONS.md §"4a. Risk tiering — what the loop will and won’t auto-merge" (subsection "Why the content signal exists (2026-07-17)").
9. What is NOT claimed
- No third-party certification is held. No SOC 2 attestation, no ISO 27001 attestation, no other compliance certification of any kind.
RUNTIME=localis a development-only opt-out, never the documented or recommended deployment shape — see docs/SETUP.md and docs/OPERATIONS.md.- Single-tenant. FA is a self-hosted, single-operator instance; Postgres-backed multi-tenant hardening is a roadmap chapter, not shipped.
- Audit-log export is hash-chained JSONL/CSV, not a SIEM platform push. Push integration is roadmap RM-096.
- A formal multi-tier containment taxonomy is design-only. See §2 above.
- Accepted residuals recorded on the roadmap, including the bootstrap-clone egress residual noted in
docs/design/fa-roadmap.md— recorded, not hidden, not eliminated. - Coverage numbers above measure attention, not proof. A
no-findingfrom a review pass is triage, never a clean bill of health (see §7).
10. Verification
The instance’s public verification key is published at GET /api/instance/verification-key (no auth — public by construction, rate-limited). Fetch a feature’s evidence pack via GET /api/features/:id/evidence-pack and verify its signature against this key, or run npx weftra verify <pr_url|statement.json>.
Generated 2026-09-14T03:38:58.815Z (content hash 3a2c3d4a41dc over docs/security/authz-surface.json + docs/security/derived-state-inventory.json).