Weftra — Operator QA Guide
Audience: the operator (you) running Weftra (FA) and validating it end-to-end before trusting a use case in production. Scope: how to set up the accounts, engines, and integrations FA can drive, and a repeatable test matrix for each capability — with the exact command/endpoint and the expected result for each check.
Ground rules for QA
- Never QA against a live production project. Enroll a throwaway test project (or a fork). The trader (AiAutonomousTrader) is live — do not point FA runs at it.
- FA is project-agnostic by law. Nothing in FA knows what your command does. Every project-specific behavior is declared config (
runtime_image,data_dirs,test_command, …). If a QA step seems to need FA to "know about" a project, that's a bug, not a test gap.- A safety claim is not evidence. When a test's expected result is "the secret is not present", assert it by scanning the actual output — don't trust a label.
- Verified state, honestly reported. If a step is skipped (no vendor key, Docker unavailable), say so in your QA notes — a skipped check is not a pass.
Companion docs: docs/SETUP.md (install/config), docs/OPERATIONS.md (run/operate), docs/USER_GUIDE.md (feature lifecycle), docs/ENGINES.md (Engine HowTos) and docs/design/engine-runtime.md (engine internals).
0. Prerequisites — the QA baseline
Before any use-case test, get a clean baseline green.
| Check | Command | Expected |
|---|---|---|
| Build | npm run build | exit 0, no tsc errors |
| Full test suite | npx vitest run | all green |
| FA diagnose | npm run diagnose | Claude CLI reachable |
| Server up | curl -s localhost:3100/health (or your PORT) | 200 / ok |
| Runtime mode | echo $RUNTIME (default docker) | docker for a real QA of the sandbox; local only for fast unit-style checks |
| Runtime image (docker) | docker image inspect $FA_RUNTIME_IMAGE (default fa-runtime:latest) | image exists |
Auth-mode parity (NON-NEGOTIABLE). Anything touching an agent run must be validated under bothapi and oauth Claude auth (they differ in HOME/mounts/env and have diverged before). Where a test below says "run the agent", run it once with FA_AGENT_AUTH=api (needs ANTHROPIC_API_KEY) and once with FA_AGENT_AUTH=oauth (needs a valid ~/.claude cred dir). See docs/design/engine-runtime.md §7.
1. Engines — the mental model (read this before wiring vendors)
FA does not bundle Gemini/OpenAI/etc. It is the client; each non-Claude engine is a host binary you supply that speaks one of two transports. You declare engines in the FA_ENGINE_PROFILES env var (a JSON array) and FA spawns them inside the sandbox runtime, never directly on the host.
Profile shape (src/config.ts:5-12):
{
"id": "gemini", // required, unique; may NOT be "claude-code" or "acp" (reserved)
"cmd": "gemini-acp", // required: the host executable FA runs
"args": ["--flag"], // optional
"authEnv": { // optional: vendor creds, injected as -e KEY=value into the sandbox
"GOOGLE_API_KEY": "AI..."
},
"transport": "acp" // optional: "acp" (default) | "headless"
}Built-in engine ids (no profile needed): claude-code (the default) and acp (a generic ACP engine driven by FA_ACP_AGENT_CMD/FA_ACP_AGENT_ARGS).
Two transports:
acp(default) — vendor-neutral Agent Client Protocol, JSON-RPC 2.0 over the subprocess's stdio, protocol version1.0. Supports a mid-run per-tool permission gate. Yourcmdmust be an ACP-speaking agent.headless— single-shot: FA writes the prompt to stdin, captures stdout as the result. No per-tool gate (run-level governance only). Use for a vendor CLI that has a--print/pipe mode but doesn't speak ACP.
The two hard security facts to verify (they encode the #173 trust boundary):
- A non-Claude engine gets no FA host secrets — the engine subprocess env is built from nothing: only a minimal OS base allowlist (
PATH,HOME, locale) plus the declaredauthEnvis added (spec 119, 2026-07-18).ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,GITHUB_TOKEN,ADMIN_API_KEY, andSESSION_SECRETare absent on both LocalRuntime and DockerRuntime because they are never copied in. Under DockerRuntime,authEnvvalues additionally arrive via the spawn env (name-only-e KEYdocker flags — values not in argv). Smoke-test caveat:npm run diagnose:enginealways uses LocalRuntime regardless ofRUNTIME=docker— it does not exercise the DockerRuntime container env seam. It also reportsanthropicStripped:truebased onENV:KEY=absentlines the engine emits; a real vendor CLI (codex, gemini) prints none, so the flag is true by absence of evidence, not confirmed measurement. The mock-based CI tests are the verifiable check (the mocks explicitly echo and assert env keys). - Vendor creds declared in the profile's
authEnvare delivered to the engine subprocess. The env-from-nothing model (fact 1) means onlyauthEnvkeys (plus the base allowlist) are present — no FA host vars leak alongside them. DockerRuntime path gap for reference shims: theadapters/codex-acp/andadapters/gemini-acp/shims ship as host files — thefa-runtimecontainer image does not include them. Thenode /abs/path/adapters/…recipe therefore requires baking the adapter into the image or bind-mounting it before using DockerRuntime. Seedocs/ENGINES.md §2.2for options.RUNTIME=localalso works (env-from-nothing isolation still holds) but runs the vendor CLI unsandboxed on the FA host.
2. Engine cookbook — run a loop with each engine
Authoritative drive paths live in
docs/ENGINES.md(Engine HowTos) (per-vendorcmd/args/auth + the full live-run playbook). This section is the fast copy-paste version. If the two ever disagree,ENGINES.mdwins — update this to match.
The honest state (read once): FA's client side — ACP handshake, credential isolation, routing, smoke test — is built and stable. The vendor adapter (the cmd) is what varies, and most vendor --acp flags are experimental or community-maintained (see each caveat). When a vendor CLI has no ACP server flag yet, you wrap it in a thin ACP shim (stdin JSON-RPC → the vendor's REST/CLI). FA does not ship vendor adapters — it's the neutral client.
2.0 The loop, once — reuse for every engine below
Every engine follows the same three steps; only the profile changes. Set the profile, point a test project at the engine id, submit a feature, watch it run:
# 1) Declare the engine (paste ONE recipe's JSON from §2.2+ here), then restart FA.
export FA_ENGINE_PROFILES='[ <profile-from-below> ]'
# …restart FA… then confirm it registered:
curl -s localhost:3100/api/engines # → engines[] must include your id
# 1a) SMOKE-TEST before trusting it (handshake + Anthropic-cred isolation, one command):
npm run diagnose:engine -- <engine-id> # expect PASS; anthropicStripped:true for non-Claude
# 2) A throwaway project routed to that engine:
curl -sX POST localhost:3100/api/projects -H 'Content-Type: application/json' -d '{
"name":"vendor-validation","repo_url":"https://github.com/you/test-repo.git",
"autonomy_mode":"full_auto","engine":"<engine-id>" }' # save api_key → PROJECT_KEY
# 3) A minimal feature = the loop:
curl -sX POST localhost:3100/api/features -H "Authorization: Bearer $PROJECT_KEY" \
-H 'Content-Type: application/json' -d '{
"title":"vendor-validation: hello",
"description":"Add hello.txt containing the string VENDOR_VALIDATED." }' # save id → FEATURE_ID
# watch: http://localhost:3100/logs.html?id=<FEATURE_ID>
# expect: pending → queued → in_progress → implemented → draft PR with hello.txtTo route only one role (e.g. author=vendor, reviewer=Claude) instead of the whole run, use engine_routing_policy (§3) rather than the project engine field.
2.1 Claude — built-in claude-code (default, no profile)
api:ANTHROPIC_API_KEY(console.anthropic.com → API Keys),FA_AGENT_AUTH=api.oauth: sign in the Claude CLI on the host so~/.claudeholds creds (your Max/Pro pool),FA_AGENT_AUTH=oauth,FA_AGENT_OAUTH_DIR=$HOME/.claude. LeaveANTHROPIC_API_KEYunset — an empty value poisons oauth.- Smoke:
npm run diagnose:engine -- claude-code→PASS,PONG,anthropicStripped: n/a(Claude is meant to hold Anthropic creds).
2.2 Gemini (Google) — reference ACP shim (adapters/gemini-acp/)
Turnkey path (Shape B, spec 117 inc-1). The Gemini CLI does not natively speak FA's ACP protocol; the reference shim at adapters/gemini-acp/index.mjs wraps gemini -p and is CI-verified.
Install: npm install -g @google/gemini-cli (tested ≥ 1.5) → confirms gemini on PATH.
Key: Google AI Studio (aistudio.google.com) → Get API key → GOOGLE_API_KEY.
{"id":"gemini","cmd":"node","args":["/abs/path/to/adapters/gemini-acp/index.mjs"],
"authEnv":{"GOOGLE_API_KEY":"AIza..."}}Vertex (service account) instead of API key: "authEnv":{"GOOGLE_APPLICATION_CREDENTIALS":"/run/secrets/gcp-sa.json","GOOGLE_CLOUD_PROJECT":"…","GOOGLE_CLOUD_LOCATION":"us-central1"}.
Smoke → anthropicStripped:true, authEnv keys: [GOOGLE_API_KEY].
See adapters/gemini-acp/README.md for full setup and live-run validation steps.
2.3 OpenAI Codex — reference ACP shim (adapters/codex-acp/)
Turnkey path (Shape B, spec 117 inc-2). The Codex CLI does not natively speak FA's ACP protocol; the reference shim at adapters/codex-acp/index.mjs wraps codex exec and is CI-verified.
Install: npm install -g @openai/codex (tested ≥ 0.1) → confirms codex on PATH.
Key: platform.openai.com → API keys → Create new secret key → OPENAI_API_KEY.
{"id":"codex","cmd":"node","args":["/abs/path/to/adapters/codex-acp/index.mjs"],
"authEnv":{"OPENAI_API_KEY":"sk-proj-..."}}Org-scoped: add "OPENAI_ORG":"org-..." to authEnv.
Smoke → anthropicStripped:true, authEnv keys: [OPENAI_API_KEY] (see smoke-test caveats in §1 — for real Codex CLI the flag is inferred, not measured; CI mocks provide the verifiable check).
DockerRuntime gap: under the default RUNTIME=docker the shim path is not in the container image — bake it in or bind-mount it before using DockerRuntime. RUNTIME=local also works (env-from-nothing isolation holds) but runs the vendor CLI unsandboxed on the FA host. See docs/ENGINES.md §2.2 for options and adapters/codex-acp/README.md for full setup.
2.4 GitHub Copilot — gh copilot --acp
Drives the local gh-copilot extension (gh extension install github/gh-copilot), not the cloud agent. Token needs the copilot scope.
{"id":"copilot","cmd":"gh","args":["copilot","--acp"],"authEnv":{"GH_TOKEN":"gho_..."}}Caveat (important): gh copilot --acp is a proposed flag — not in the stable gh-copilot release as of 2026-07. Watch github/gh-copilot release notes; until it ships, this recipe won't run without a shim. The cloud Copilot agent is unsuitable for FA (no per-run isolation, no headless mode).
2.5 Local model — Ollama / vLLM (self-hosted, no cloud key)
Both expose an OpenAI-compatible HTTP API locally, so drive them with an OpenAI-compatible adapter/ shim pointed at the local endpoint. Network matters: the sandbox must reach the host server — set FA_RUNTIME_NETWORK to a network that resolves your Ollama/vLLM host (default bridge; none would block it; use host/a named network as your setup requires).
{"id":"local-llama","cmd":"acp-openai-shim","args":["--model","llama3"],
"authEnv":{"OPENAI_BASE_URL":"http://host.docker.internal:11434/v1","OPENAI_API_KEY":"ollama"}}(vLLM: point OPENAI_BASE_URL at http://<vllm-host>:8000/v1.) OPENAI_API_KEY is a throwaway placeholder these servers ignore. Smoke → PASS, anthropicStripped:true. If the smoke test can't reach the server, it's almost always FA_RUNTIME_NETWORK.
2.6 OpenHands — run the OpenHands agent as a backend
OpenHands (Agent Canvas) enables ACP by default and hosts an agent loop; FA drives it as any other ACP engine. Point cmd at your OpenHands ACP server entrypoint (exact binary/args depend on your OpenHands install/version — see OpenHands docs; the roadmap tracks pinning it like openhands-sdk).
{"id":"openhands","cmd":"openhands-acp","transport":"acp","authEnv":{"LLM_API_KEY":"...","LLM_MODEL":"anthropic/claude-…"}}OpenHands runs its own model underneath — its creds go in authEnv (whatever your OpenHands config expects, e.g. LLM_API_KEY/LLM_MODEL), and FA still strips its own ANTHROPIC_* from the subprocess. Caveat: the cmd/authEnv here are illustrative — confirm your OpenHands version's ACP entrypoint and env before relying on it. Smoke → PASS, anthropicStripped:true.
The generic built-in
acpengine (no profile): instead of a profile you can setFA_ACP_AGENT_CMD(+FA_ACP_AGENT_ARGS) and use engine idacp. Handy for a single ad-hoc ACP agent; profiles are better when you run several.
2.7 Engine discovery & the "typo routes to default" trap
- List registered engines:
GET /api/engines→{ "engines": ["claude-code","acp","gemini",…], "default": "claude-code" }. Confirm your new id appears here after restart — if it doesn't, the profile didn't parse. - Trap: if you route to an engine id that isn't registered (a typo), FA silently falls back to the default and only appends a note to the run's routing
source. So a wrong id looks like it worked. Always smoke-test the exact id you'll route to, and after a run, check the routing source names the engine you intended — notclaude-code (fallback).
Not built yet (spec 115): the admin-gated, secret-redacting
GET /api/admin/engines(returnsid/transport/builtIn/isDefault, nevercmd/authEnv) and the dashboard engine picker. Until it lands, useGET /api/engines(ids only) + the smoke test.
3. Engine routing QA (Tier 0 declarative vs Tier 1 classifier)
Route which engine plays each role (author, reviewer, fixer) — set engine_routing_policy (JSON) on a project (default for its features) or per feature (override).
Feature categories (Tier-1 classifier output): small-patch, large-integration, bugfix, refactor, other.
Tier 0 — declarative (a role = a fixed engine id):
{"author":"gemini","reviewer":"claude-code","fixer":"codex"}QA: submit a feature, confirm the author run used gemini, the review used claude-code. Tier 0 always beats Tier 1.
Tier 1 — classifier (route by category):
{"author":{"classify":true,"category":{"large-integration":"claude-code","small-patch":"gemini"}}}QA: submit one small-patch-ish feature and one large-integration feature; confirm each was routed to the mapped engine. The classifier runs one short headless call and fails closed to other on any error — so an unmapped/failed classification lands on the role's default, not a crash.
Resolver precedence to keep in mind (highest first): feature static engine field → feature Tier-0 → project Tier-0 → project static field → Tier-1 classifier map → global default (claude-code).
4. @weftra PR-comment mentions (spec 113, phase 1)
What phase 1 does: detect a mention of the bot handle in a PR comment, authorize the commenter, and notify — no execution (mention → /revise is phase 2, not built). QA is about detection + authorization, because the whole point is that an untrusted comment body cannot make FA act.
The handle is configurable: FA_BOT_HANDLE (default weftra). Detection is @<handle>, case-insensitive.
Authorization is by the comment author's VCS identity — never the comment text. For a mention to be authorized, the commenter must be a registered FA approver on that project, matched by VCS handle:
- Create/curate an FA user whose
vcs_loginequals the commenter's GitHub/GitLab login (dashboard user form → VCS login field). - Add that user as an approver on the test project. (
getProjectApproverByVcsHandlejoinsusers.vcs_login+project_approvers.)
Operator prerequisites: a VCS token so FA can read comments — GITHUB_TOKEN (and/or GITLAB_TOKEN+GITLAB_HOST, BITBUCKET_TOKEN+BITBUCKET_USERNAME). For push delivery, FA_INBOUND_TRIGGERS_ENABLED (default on) + a GitHub webhook to POST /…/triggers/github for issue_comment events. Otherwise FA picks up mentions on the polling path during PR-review processing.
Provider coverage: the inbound webhook
issue_commentinterceptor is GitHub-only today. GitLab/Bitbucket mentions surface via the polling path only.
Test matrix:
| Case | Setup | Action | Expected |
|---|---|---|---|
| Authorized mention | Commenter is an approver on the project (vcs_login mapped) | Comment @weftra please look on the feature's PR | mention_authorized run_event; pending-mention count ++; notification fires (role po); no /revise |
| Unauthorized mention | Commenter is NOT an approver | Same comment from a random account | mention_unauthorized run_event; no notification; no action |
| Bot's own comment | — | FA's own bot account comments | Ignored (isBot) — no event |
| Duplicate delivery | — | Same comment id delivered twice (webhook + poll) | Processed at most once (dedup by comment id) |
| Wrong handle | — | Comment mentions @someoneelse | No mention detected |
Security assertion: put an instruction in the comment body ("delete the branch", "run env"). Expect FA to record/notify only — the body is never executed. That's the phase-1 contract.
5. Core platform use-case matrix
The lifecycle checks every install should pass. Use a disposable test project.
5.1 Enrollment & autonomy modes
| Mode | Submit a feature | Expected transition |
|---|---|---|
full_auto | POST /api/features | pending → queued immediately → in_progress → implemented |
auto_safe | same | pending → analyzing → (clarification if needed) → queued → … |
po_approval | same | pending → awaiting_approval (waits for approver) → analyzing → … |
5.2 Clarifications
Submit a deliberately under-specified feature in auto_safe. Expect status clarification_needed with questions; answer via the dashboard; expect it to resume to queued.
5.3 Standard implementation → PR
Expect: clone → branch feature/<slug>-<id> → agent run → test_command → commit → push → draft PR. Verify the PR exists, is draft, targets the right base (respect a per-feature base_branch override).
5.4 Spec-kit pipeline
On a spec-kit-enabled project, submit with use_spec_kit:true. Expect /speckit.specify → clarify → plan → tasks → analyze → implement, pausing at clarification gates.
5.5 Verbatim spec handoff
Submit with spec_content + spec_path. Expect the branch file at spec_path to be byte-identical to what you sent (no regeneration; use_spec_kit ignored). Diff to confirm.
5.6 PR revision (/revise)
On an implemented feature with a PR, add review comments, POST /api/features/:id/revise. Expect status revising, the agent applies feedback, pushes the same branch (no new PR), returns to implemented. "No comments found" reverts to implemented.
5.7 Merge polling & manual PR
Merge the PR on GitHub; within PR_CHECK_INTERVAL_MS expect auto-transition to merged. For a feature whose auto-PR failed (branch pushed, no pr_url), POST /api/features/:id/create-pr opens the draft PR.
5.8 Cancel / retry
Cancel an in-flight feature → cancelled, workspace wiped, re-runnable via retry (no retry_count bump). Cancelling a revising feature reverts to implemented (PR left intact).
5.9 Data provisioning & workspace confinement
Declare data_dirs with a missing source → expect the run to fail up front with an actionable error (not silently reach outside the workspace). With a valid source, expect a read-only symlink in the workspace, added to .git/info/exclude, and setup_command run.
5.10 Notifications & webhooks
Configure a channel (Telegram/Discord/Slack/Email) with notify rules; drive a status change; confirm delivery to the right audience (po/submitter/all). Set callback_url (feature overrides project); confirm the POST is HMAC-SHA256 signed with the project key even for a per-feature URL.
5.11 Login methods (see also the login-setup flow)
- API key — always on.
- Local email+password — needs
SESSION_SECRET; password is set via the reset-email flow, which needs SMTP (being made portal-configurable — spec 116). - Google —
GOOGLE_CLIENT_ID/SECRET. OIDC/SSO —OIDC_ISSUER/CLIENT_ID/SECRET. CheckGET /auth/statusreflects which are enabled. Accounts are invite-only (admin pre-provisions).
5.12 Fleet fan-out & campaigns
Fan one spec across N test projects; confirm N features/PRs and a campaign rollup. (See the Fleet view and the campaigns CTA in the dashboard header.)
6. Security / trust-boundary QA (do these every release)
These directly exercise the #173 lessons. Each expects a negative result you must assert against real output, not a label.
| Check | How | Expected |
|---|---|---|
| Non-Claude engine has no Anthropic creds | npm run diagnose:engine -- <vendor-id> | anthropicStripped: true; FA secrets absent (from-nothing env): YES ✓ |
| Subprocess env built from nothing (spec 119) | npm run diagnose:engine -- <vendor-id> | Child env contains ONLY base allowlist + authEnv; GITHUB_TOKEN/ADMIN_API_KEY/SESSION_SECRET absent on every runtime |
| Vendor secret values never in docker run argv | Review engineAuthArgs output or unit tests | -e KEY name-only flags; no KEY=VALUE form in /proc |
| Vendor secret not leaked to responses/logs | Inspect the smoke-test result + run logs | authEnv keys may appear; values never do |
| Execution only in the sandbox | Confirm RUNTIME=docker; grep the run | agent/engine spawn goes through the runtime, not host spawn |
| Project key can't execute host actions | Try a project-key call to any admin/execution route | 401/403 — authz matches blast radius |
| Mention body never executed | §4 security assertion | detect/notify only |
Engine profile secrets not in GET /api/engines | curl localhost:3100/api/engines | ids + default only; no cmd/args/authEnv |
7. Environment variable reference (QA-relevant)
Engines / transport / routing
FA_ENGINE_PROFILES— JSON array of engine profiles (default[]).FA_ACP_AGENT_CMD/FA_ACP_AGENT_ARGS— command/args for the built-inacpengine.RUNTIME—docker(default) |local.FA_RUNTIME_IMAGE(defaultfa-runtime:latest).FA_RUNTIME_NETWORK—bridge(default) |none| network name.
Claude / agent auth
FA_AGENT_AUTH—api(default) |oauth.FA_AGENT_OAUTH_DIR(default$HOME/.claude).ANTHROPIC_API_KEY(api mode; leave unset for oauth).FA_AGENT_MAX_RUN_MS(oauth preflight, 4h).
Permission gate (ACP transport only)
FA_PERMISSION_POLICY—allow_all(default) |deny_all|ask|governed.FA_PERMISSION_TIMEOUT_MS— default 30 min.
Mentions / VCS
FA_BOT_HANDLE— mention handle (defaultweftra).FA_INBOUND_TRIGGERS_ENABLED(default on).GITHUB_TOKEN;GITLAB_TOKEN+GITLAB_HOST;BITBUCKET_TOKEN+BITBUCKET_USERNAME.PR_CHECK_INTERVAL_MS— merge/review poll cadence (default 10 min).
Budgets (all engines)
FA_MAX_BUDGET_TOKENS,FA_MAX_BUDGET_SECONDS(uncapped by default),FA_BUDGET_WARN_PCT(80).
8. A minimal end-to-end QA run (copy/paste checklist)
npm run build && npx vitest run→ green.- Bring up FA (
RUNTIME=docker),GET /health→ ok. npm run diagnose:engine -- claude-codeunder bothFA_AGENT_AUTH=apiandoauth→ PASS.- Wire one vendor engine (§2.2/2.3/2.4), restart, confirm it's in
GET /api/engines,npm run diagnose:engine -- <id>→ PASS +anthropicStripped:true. - Enroll a disposable project; run one feature per autonomy mode (§5.1) to
implemented+ draft PR. - Route the author role to the vendor engine (§3 Tier 0); confirm the run used it (not a fallback).
- Merge the PR → confirm auto
merged. - Mentions (§4): authorized comment → notify; unauthorized → silent; instruction-in-body → not executed.
- Security sweep (§6) — all negatives asserted against real output.
- Record results, noting any skipped checks (missing vendor key, Docker off) as skipped, not passed.
Keep this guide honest about BUILT vs SPEC-ONLY. As of writing: engine profiles, ACP + headless transports, Tier-0/Tier-1 routing, per-engine auth isolation, GET /api/engines, the engine smoke test, and spec-113 phase-1 mentions are built. The admin engine list + dashboard picker (spec 115) and mention execution (phase 2) are not — don't QA them until they land.