Testing Cloud Agents Locally
Use this page when validating Cloud Agent work in this monorepo. Hosted sandbox containers are not implemented here yet — you test gates, Integrations, and session create, then optionally run the local CLI as an agent stand-in.
Related: SCM Integrations Architecture.
What you can and cannot test
| Layer | What | Local? |
|---|---|---|
| 1 — Gates | App-only SCM connect, PAT refusal, server-bound cloneUrl, clone-credential mint, LLM cloud-agent project gate | Yes |
| 2 — Agent loop | Models / tools via CLI pointed at the gateway | Yes (not a hosted sandbox) |
| 3 — Hosted runner | Per-session local process + on-disk workspace (PVC stand-in); hibernate stops process, cold-start reuses workspace + DB history | Yes (CLOUD_AGENT_RUNNER_KIND=local, default) |
| 3b — Real loop | Local runner /turn runs cfactory run with CFACTORY_FEATURE=cloud-agent + verified project | Opt-in (CLOUD_AGENT_REAL_LOOP=1) |
| Full K8s (Pod + PVC) | Same runner protocol on cluster | Not yet (k8s reserved) |
Do not run bun test from the repo root (blocked). Run package tests from each package directory.
Prerequisites
- Docker + Bun
docker/.envfilled (at least Postgres/Keycloak; provider keys for real LLM chats)- Prefer host gateway so local SCM code is loaded (compose gateway image lags until rebuild)
- Apply DB migrations with
bun run db:migrate:apply(or gateway boot) — notdrizzle-kit migrate, which may warn about snapshot versions and do nothing
Step 1 — Start the stack
Terminal A — dependencies
# from repo root — local docker Postgres (not the cloud k8s cluster) docker compose --env-file docker/.env up postgres redis keycloak bifrost
Apply migrations (once Postgres is healthy):
cd packages/cfactory-gateway-server export DATABASE_URL=postgresql://cfactory:cfactory@localhost:5432/cfactory bun run db:migrate:apply
Terminal B — gateway (working tree)
export DATABASE_URL=postgresql://cfactory:cfactory@localhost:5432/cfactory export REDIS_URL=redis://localhost:6379 export GATEWAY_PUBLIC_URL=http://localhost:3000 export OIDC_ISSUER=http://localhost:8080/realms/cfactory export OIDC_ISSUER_INTERNAL=http://localhost:8080/realms/cfactory export BIFROST_BASE_URL=http://localhost:4000 bun run dev:gateway
Check
curl -s http://localhost:3000/ready # expect postgres/redis ok
Alternative: bun run dev:stack (includes gateway container). Rebuild after SCM changes:docker compose --env-file docker/.env up --build gateway.
Step 2 — Admin UI (provider App / OAuth)
bun run dev:admin # http://127.0.0.1:3020 → login → Keycloak: dev / dev # Governance → Integrations → Connect GitHub | GitLab | Azure DevOps
Configure provider secrets in the gateway env (see docker/.env.example):
- GitHub App:
GITHUB_APP_ID,GITHUB_APP_PRIVATE_KEY,GITHUB_APP_SLUG
Setup URL:http://localhost:3000/api/integrations/github/callback - GitLab:
GITLAB_OAUTH_CLIENT_ID,GITLAB_OAUTH_CLIENT_SECRET - Azure DevOps:
AZURE_DEVOPS_CLIENT_ID,AZURE_DEVOPS_CLIENT_SECRET
Manual (dev) remains available for smoke without real apps. Prefer Connect for live tokens.
After connect, use Sync to refresh the authorized repo list from the provider API.
Step 3 — Layer 1 automated smoke
With the gateway up:
cd packages/cfactory-gateway-server bun run smoke:scm # or: BEARER_TOKEN=cflocal_... bun run smoke:scm
Without BEARER_TOKEN, the script starts device-auth; open the printed URL and approve with Keycloak dev / dev.
The smoke script asserts:
- Register a GitHub App–style installation + repo
- List installations / repos
- Mint clone credentials
POST /api/cloud-agents/sessionsreturns{ sessionId, cloneUrl }from the server record- Message → force hibernate → message (cold-start) with history length ≥ 4
- A PAT-labeled install is refused with
pat_not_allowed - Cleanup
With the default local runner, step 5 also proves workspace persistence under .data/cloud-agent-workspaces/<sessionId>/ (stub clone unless SCM_REQUIRE_LIVE_TOKENS=1).
Cloud Agents are off by default per tenant (cloud_agents_enabled). Staff must enable them (Staff Console → tenant → Feature flags, or Admin Settings when signed in as staff). The smoke script enables the flag for the active org.
Unit tests (no stack)
cd packages/cfactory-gateway && bun test test/scm.test.ts cd packages/cfactory-gateway-server && bun test ./test/scm-gate.test.ts ./test/cloud-agents*.test.ts
Use CLOUD_AGENT_RUNNER_KIND=fake for pure in-process unit tests that import fake-runner (default gateway is local).
Step 4 — Layer 3 local runner (Admin UI)
Default gateway env:
export CLOUD_AGENT_RUNNER_KIND=local # default export CLOUD_AGENT_WORKSPACE_ROOT="$PWD/.data/cloud-agent-workspaces" # Stub workspace (no real git clone) while live tokens are off: # CLOUD_AGENT_SKIP_CLONE defaults to 1 when SCM_REQUIRE_LIVE_TOKENS≠1
- Start deps + host gateway (Steps 1–2 above).
- Enable Cloud Agents for the org (Staff Console feature flag, or Admin → Settings as staff).
bun run dev:admin→ Cloud Agents appears in the sidebar only when enabled.- Pick an eligible Integration repo → Start session → send messages.
- In another terminal,
POST /api/cloud-agents/sessions/:id/hibernate(or wait 2h idle) then send again — status wakes and transcript continues; workspace dir remains.
Runner process: packages/cfactory-gateway-server/src/cloud-agents/runner-server.ts (HTTP /health, /provision, /turn, /shutdown). Gateway spawns one process per warm session.
By default /turn echoes (safe for smoke/unit tests). For a real agent reply:
# on the host gateway process export CLOUD_AGENT_REAL_LOOP=1 # optional: export CLOUD_AGENT_MODEL=openai/gpt-4.1-mini # needs OPENAI_API_KEY (or other provider) + Bifrost up
When enabled, cold-start mints a short-lived gateway session token, ensures an org project that allowlists the session remote (ca-<slug>), and passes CFACTORY_API_KEY / CFACTORY_PROJECT_ID / CFACTORY_FEATURE=cloud-agent into the runner. If cfactory run fails, the runner falls back to echo with mode: echo_fallback.
Step 5 — Manual API checks (optional)
export TOKEN='cflocal_...' # from Admin localStorage / CLI auth.json / smoke login
export API=http://localhost:3000
curl -s -X POST "$API/api/integrations/installations" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"provider":"github","authKind":"github_app","owner":"acme",
"repos":[{"name":"widget","owner":"acme","cloneUrl":"https://github.com/acme/widget.git"}]
}'
curl -s "$API/api/integrations/installations" -H "Authorization: Bearer $TOKEN"
# then list repos, mint credentials, create session as in smoke-scm.ts
LLM defense: call completions with header X-CFACTORY-FEATURE: cloud-agent against a verified org project that lists the remote in repos[]. Unverified / empty repos → reject.
Step 6 — Layer 2 agent stand-in (CLI)
export CFACTORY_API_URL=http://localhost:3000 export CFACTORY_MODELS_URL=http://localhost:3000 bun run --cwd packages/cfactory-cli --conditions=browser src/index.ts auth login # work inside a checkout of an Integration-listed repo bun run --cwd packages/cfactory-cli --conditions=browser src/index.ts
CLI helpers:
bun run --cwd packages/cfactory-cli --conditions=browser src/index.ts scm connect github bun run --cwd packages/cfactory-cli --conditions=browser src/index.ts scm list
This validates gateway auth, models, and org policy. It is not a hosted Cloud Agent container.
Step 7 — Prod-shaped stack (optional)
For Traefik + admin + api hosts (not required for gates):
cp docker/.env.prod.example docker/.env.prod # fill secrets docker compose -f docker-compose.prod.yml --env-file docker/.env.prod pull docker compose -f docker-compose.prod.yml --env-file docker/.env.prod up -d # http://api.localhost http://admin.localhost http://auth.localhost
GHCR images may lag your branch — build locally or use dev:gateway for SCM work.
Minikube/Helm: viable for K8s chart fidelity only after a local values profile (no ESO/cloud DNS). Skip for Cloud Agent gate testing.
Security notes (local)
- Clone-credentials and session create require installation ownership / org membership.
- Org-scoped installs require admin to register or delete.
- SCM tokens are sealed at rest (
credentials.ciphertext). - OAuth
returnTo/statemust passisAllowedReturnTo(no open redirects). SCM_REQUIRE_LIVE_TOKENS=0(default) mints placeholder passwords so gates work without real GitHub/GitLab/Entra apps.
What’s next
CLOUD_AGENT_RUNNER_KIND=k8s: same runner protocol on Pod + PVC (Helm).- Triggers / schedules / apex domain cutover remain separate.