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

LayerWhatLocal?
1 — GatesApp-only SCM connect, PAT refusal, server-bound cloneUrl, clone-credential mint, LLM cloud-agent project gateYes
2 — Agent loopModels / tools via CLI pointed at the gatewayYes (not a hosted sandbox)
3 — Hosted runnerPer-session local process + on-disk workspace (PVC stand-in); hibernate stops process, cold-start reuses workspace + DB historyYes (CLOUD_AGENT_RUNNER_KIND=local, default)
3b — Real loopLocal runner /turn runs cfactory run with CFACTORY_FEATURE=cloud-agent + verified projectOpt-in (CLOUD_AGENT_REAL_LOOP=1)
Full K8s (Pod + PVC)Same runner protocol on clusterNot yet (k8s reserved)

Do not run bun test from the repo root (blocked). Run package tests from each package directory.


Prerequisites

  • Docker + Bun
  • docker/.env filled (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) — not drizzle-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:

  1. Register a GitHub App–style installation + repo
  2. List installations / repos
  3. Mint clone credentials
  4. POST /api/cloud-agents/sessions returns { sessionId, cloneUrl } from the server record
  5. Message → force hibernate → message (cold-start) with history length ≥ 4
  6. A PAT-labeled install is refused with pat_not_allowed
  7. 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
  1. Start deps + host gateway (Steps 1–2 above).
  2. Enable Cloud Agents for the org (Staff Console feature flag, or Admin → Settings as staff).
  3. bun run dev:adminCloud Agents appears in the sidebar only when enabled.
  4. Pick an eligible Integration repo → Start session → send messages.
  5. 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 / state must pass isAllowedReturnTo (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.