Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ FOREMAN_REQUEST_TIMEOUT_MS=120000
UHP_BASE_URL=http://127.0.0.1:8787
UHP_HARNESS_ID=
UHP_MODEL=
# Bearer credential for the bridge or UHP server at UHP_BASE_URL. If you started the bridge
# with LOCAL_CLI_UHP_TOKEN set, use the same value here and in FOREMAN_WORKSPACE_BRIDGE_TOKEN.
# Leave both empty for a bridge started without a token (it then accepts any local process).
UHP_TOKEN=
HINDSIGHT_BASE_URL=http://127.0.0.1:8888
HINDSIGHT_TOKEN=
# For a verified live Worker run, set this to the disposable Git repository
# configured as LOCAL_CLI_UHP_SOURCE_REPO in the bridge process.
FOREMAN_WORKSPACE_SOURCE_REPO=
FOREMAN_WORKSPACE_BRIDGE_URL=
# Optional bearer token for the bridge above (its LOCAL_CLI_UHP_TOKEN). Requires the URL.
# Foreman's own per-project bridges need no configuration: it generates their tokens.
FOREMAN_WORKSPACE_BRIDGE_TOKEN=
FOREMAN_WORKSPACE_ALLOWED_SCOPE=
# JSON argv list; commands are run by Foreman in a disposable validation
# workspace. A workspace workflow requires a source repo, bridge URL, allowed
Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Open `http://127.0.0.1:4399` and choose **Open repository**. Pick a local Git re

The selected repository must have a committed HEAD. Uncommitted source changes are visible but runs start from the pinned commit. The folder browser shows folders on the server machine; if Foreman runs remotely, the paths are on that machine.

No `.env` file or separate bridge command is needed for the standard local flow. Foreman starts its own per-project bridge process.
No `.env` file or separate bridge command is needed for the standard local flow. Foreman starts its own per-project bridge process, protects it with a random token, logs it, and restarts it if it crashes; see [Local bridge](#local-bridge).

For the manual bridge workflow (custom CLIs, external UHP servers, or disposable-repo testing), see [the host CLI workflow guide](docs/three-harness-workflow.md).

Expand Down Expand Up @@ -135,14 +135,15 @@ The standard local flow requires no environment variables. The following setting
| `FOREMAN_DATA_DIR` | `.foreman-data` | Durable state directory. |
| `UHP_BASE_URL` | — | External UHP server base URL. Without this, Foreman uses its own per-project bridge. |
| `UHP_HARNESS_ID`, `UHP_MODEL` | — | Optional explicit initial harness/model for the external UHP server. Must be set together. |
| `UHP_TOKEN` | — | Optional bearer credential for the external UHP server. |
| `UHP_TOKEN` | — | Optional bearer credential for the external UHP server, sent on every request to `UHP_BASE_URL` including discovery. For a manual bridge started with `LOCAL_CLI_UHP_TOKEN`, use the same value. |
| `HINDSIGHT_BASE_URL` | — | Hindsight API base URL. Outages show degraded memory status and do not stop workflow. |
| `HINDSIGHT_TOKEN` | — | Optional credential for Hindsight. |
| `FOREMAN_REQUEST_TIMEOUT_MS` | `120000` | Initial HTTP connection timeout (max 120,000 ms). Does not limit turn duration. |
| `FOREMAN_TASK_TIMEOUT_MS` | `180000` | Turn timeout for Reviewer and other non-Worker, non-Planner/Orchestrator roles (max 900,000 ms). |
| `FOREMAN_WORKER_TIMEOUT_MS` | `600000` | Turn timeout for Worker roles (max 900,000 ms). |
| `FOREMAN_WORKSPACE_SOURCE_REPO` | — | Local Git repository for snapshot verification. Required with the manual bridge workflow. |
| `FOREMAN_WORKSPACE_BRIDGE_URL` | — | Loopback URL for an external workspace bridge. |
| `FOREMAN_WORKSPACE_BRIDGE_TOKEN` | — | Optional bearer token for that bridge, for a bridge started with `LOCAL_CLI_UHP_TOKEN` (use the same value). Requires `FOREMAN_WORKSPACE_BRIDGE_URL`; printable ASCII without spaces. It is only ever sent to that loopback URL and is never logged or returned by the API. |
| `FOREMAN_WORKSPACE_ALLOWED_SCOPE` | — | Comma-separated exact paths or directory prefixes ending in `/` allowed in Worker results. |
| `FOREMAN_VALIDATION_COMMANDS` | — | JSON array of `{"name","command","args","cwd?","network?"}` entries run in the disposable validation workspace. `network` is `true` or `false`; validation runs offline unless it is `true`, and a package-manager install without the field defaults to `true`. See [Validation sandbox](#validation-sandbox). |
| `FOREMAN_VALIDATION_TIMEOUT_MS` | `120000` | Per-command time limit (max 600,000 ms). |
Expand All @@ -152,9 +153,20 @@ The standard local flow requires no environment variables. The following setting

Planner and Orchestrator turn timeouts are fixed at 300 seconds and are not configurable via environment variable. The optional configuration shape is recorded in `config.schema.json`.

## Local bridge

In the standard flow Foreman runs one bridge per project, `investigations/local-cli-uhp/server.mjs`, on a random `127.0.0.1` port. The bridge drives your signed-in Claude Code, Codex and Antigravity CLIs and holds full copies of the repository, so Foreman protects and supervises it:

- **Authentication.** Each start gets a fresh random 32-byte bearer token, passed to the bridge in its environment and sent by Foreman on every call: UHP requests, workspace seed, overlay and snapshot, and usage. The bridge answers `401` to any request without it, before doing any work (`/v1/uhp` discovery included), and `403` to any request whose `Host` is not `127.0.0.1`, `localhost` or `[::1]` on its port, so neither another local process nor a web page using DNS rebinding can read the repository or use your subscriptions. The token is never logged and never appears in events or `/api/*` responses.
- **Log.** The bridge's stdout and stderr go to `<data dir>/local-bridges/<key>/bridge.log` (mode 0600). At every (re)start, a log over 5 MB is moved to `bridge.log.1`, replacing any older one, so at most one old file is kept.
- **Restart.** If the bridge exits unexpectedly, Foreman restarts it on the same port with the same token, so URLs and credentials already handed out stay valid, after 1 s, then 2 s, 4 s and so on up to 30 s. Anything the bridge was running is marked failed by the bridge, and Foreman's normal reconciliation shows it as failed; nothing is replayed. After 5 consecutive runs that each end within 60 s of starting, including restarts that cannot bind the port, Foreman gives up. A stopped project, such as a deleted one, is never restarted.
- **Status.** `GET /api/projects/:id/workspace-setup` reports `bridgeStatus` (`ready`, `restarting` or `unavailable`) and a `bridgeHealth` object with the last exit code or signal, the restart count and the log path. `GET /api/projects/:id/usage` reports the same `bridgeStatus` while the bridge is not ready. Foreman also prints each change to its own stderr. Reopening the repository starts a fresh bridge for a project that was given up on.

For a bridge you start yourself (`UHP_BASE_URL`), see [the host CLI workflow guide](docs/three-harness-workflow.md#bridge-authentication): set `LOCAL_CLI_UHP_TOKEN` on the bridge and the same value as `UHP_TOKEN` and `FOREMAN_WORKSPACE_BRIDGE_TOKEN` for Foreman. Without a token that bridge accepts any local process and warns at startup, and Foreman does not supervise it.

## Security

Foreman has no login; it relies on binding to `127.0.0.1` and on request checks that stop other web pages from driving it through your browser. Every request must carry a `Host` of `localhost`, `127.0.0.1` or `[::1]` (or the `FOREMAN_HOST` value) on `FOREMAN_PORT`, which blocks DNS rebinding. Every request other than `GET` and `HEAD` must also carry an `Origin` that matches `Host`, and a `Sec-Fetch-Site`, if sent, must be `same-origin`; otherwise it gets a 403. Scripts that call the API directly (for example with `curl`) must therefore send `-H 'Origin: http://127.0.0.1:4399'` on writes. `pnpm dev:ui` rewrites `Origin` on proxied requests to `http://127.0.0.1:4399`. GitHub write actions additionally require an explicit confirmation token. Binding to a non-loopback address with `FOREMAN_HOST` exposes an unauthenticated control plane to that network; don't.
Foreman has no login; it relies on binding to `127.0.0.1` and on request checks that stop other web pages from driving it through your browser. Every request must carry a `Host` of `localhost`, `127.0.0.1` or `[::1]` (or the `FOREMAN_HOST` value) on `FOREMAN_PORT`, which blocks DNS rebinding. Every request other than `GET` and `HEAD` must also carry an `Origin` that matches `Host`, and a `Sec-Fetch-Site`, if sent, must be `same-origin`; otherwise it gets a 403. Scripts that call the API directly (for example with `curl`) must therefore send `-H 'Origin: http://127.0.0.1:4399'` on writes. `pnpm dev:ui` rewrites `Origin` on proxied requests to `http://127.0.0.1:4399`. GitHub write actions additionally require an explicit confirmation token. Binding to a non-loopback address with `FOREMAN_HOST` exposes an unauthenticated control plane to that network; don't. The per-project bridge behind it has its own token and `Host` checks; see [Local bridge](#local-bridge).

## Data and cleanup

Expand Down
2 changes: 2 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"FOREMAN_PORT": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 4399 },
"FOREMAN_DATA_DIR": { "type": "string", "default": ".foreman-data" },
"UHP_BASE_URL": { "type": "string", "format": "uri", "description": "Configured UHP server base URL; absent means disconnected" },
"UHP_TOKEN": { "type": "string", "writeOnly": true, "minLength": 1, "description": "Optional bearer credential for the UHP server, sent on every request including discovery; use the bridge's LOCAL_CLI_UHP_TOKEN for a manually started bridge" },
"UHP_HARNESS_ID": { "type": "string", "description": "Optional explicit initial harness choice" },
"UHP_MODEL": { "type": "string", "description": "Optional explicit initial model choice; pair with UHP_HARNESS_ID" },
"HINDSIGHT_BASE_URL": { "type": "string", "format": "uri", "description": "Configured Hindsight server base URL; absent means degraded memory" },
Expand All @@ -16,6 +17,7 @@
"FOREMAN_WORKER_TIMEOUT_MS": { "type": "integer", "minimum": 1000, "maximum": 900000, "default": 600000 },
"FOREMAN_WORKSPACE_SOURCE_REPO": { "type": "string", "description": "Local Git source repository used for pinned Worker snapshot verification" },
"FOREMAN_WORKSPACE_BRIDGE_URL": { "type": "string", "format": "uri", "description": "Loopback URL for the external workspace bridge" },
"FOREMAN_WORKSPACE_BRIDGE_TOKEN": { "type": "string", "writeOnly": true, "minLength": 1, "maxLength": 512, "pattern": "^[\\x21-\\x7e]+$", "description": "Optional bearer token for the external workspace bridge (its LOCAL_CLI_UHP_TOKEN); printable ASCII without spaces; requires FOREMAN_WORKSPACE_BRIDGE_URL" },
"FOREMAN_WORKSPACE_ALLOWED_SCOPE": { "type": "string", "description": "Comma-separated exact paths or directory prefixes ending in / allowed for Worker changes" },
"FOREMAN_VALIDATION_COMMANDS": { "type": "string", "description": "JSON array of validation command objects {name, command, args, cwd?, network?} with explicit command and args. network is true or false (no other value is accepted): validation commands run without network access unless it is true. When omitted, a pnpm, npm or yarn install (first argument install, i, ci or add, or bare yarn) defaults to true and every other command to false" },
"FOREMAN_VALIDATION_TIMEOUT_MS": { "type": "integer", "minimum": 1, "maximum": 600000, "default": 120000 },
Expand Down
47 changes: 44 additions & 3 deletions docs/three-harness-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,14 @@ configuration. Validation commands run in a disposable validation workspace
with bounded time and output. Only configured commands run. Do not use a
valuable checkout as the task repository.

Start the bridge in one terminal from the repository root:
Choose a bridge token first (see [Bridge authentication](#bridge-authentication)):

```sh
export LOCAL_CLI_UHP_TOKEN="$(node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))")"
echo "$LOCAL_CLI_UHP_TOKEN" # you will paste this into Foreman's .env below
```

Start the bridge in that terminal from the repository root:

```sh
cd investigations/local-cli-uhp
Expand All @@ -84,7 +91,36 @@ listed on the proof host and completed the live Worker turn. If it is absent on
another host, configure an explicitly listed Flash model instead. Any
discovered AGY Flash model can be selected per role and per run in Foreman's
UI. Do not let the bridge silently substitute a model. The bridge listens on
loopback and has no authentication; keep it local.
loopback only. If it reports `WARNING: LOCAL_CLI_UHP_TOKEN is not set` it is
running unauthenticated; see the next section.

### Bridge authentication

The bridge drives your signed-in CLIs and holds full copies of the repository,
so it can be protected with a bearer token. It has two modes:

- **Token set (recommended).** With `LOCAL_CLI_UHP_TOKEN` set (1-512 printable
ASCII characters, no spaces; 32 random bytes as hex is a good choice), every
request must carry `Authorization: Bearer <token>`, including `/v1/uhp`
discovery and the workspace extension routes. The token is compared in
constant time. A missing or wrong token gets `401` before any other work is
done. Foreman itself uses this mode for the per-project bridges it starts,
with a fresh random token per start.
- **Token unset (legacy manual mode).** The bridge accepts requests from any
local process, and prints one `WARNING: LOCAL_CLI_UHP_TOKEN is not set` line at
startup. The `smoke.mjs`, `workspace-smoke.mjs`, `codex-worker-smoke.mjs` and
`reviewer-smoke.mjs` scripts under `investigations/local-cli-uhp/` send no
token, so they need a bridge in this mode.

In both modes the bridge checks the `Host` header of every request: only
`127.0.0.1:<port>`, `localhost:<port>` and `[::1]:<port>` (case-insensitive, one
trailing dot tolerated) are accepted, anything else gets `403`. This blocks DNS
rebinding from web pages, and it does not depend on the token.

When the bridge has a token, give Foreman the same value (see below) in both
`UHP_TOKEN` (UHP calls) and `FOREMAN_WORKSPACE_BRIDGE_TOKEN` (workspace
seed, overlay and snapshot calls). Foreman never logs either value or returns
them from its API.

In another terminal, configure the same repo and validation policy for Foreman,
then build and start the UI/API service:
Expand All @@ -98,8 +134,10 @@ Set these entries in `.env` (retain the loopback URL and adjust paths/checks):

```dotenv
UHP_BASE_URL=http://127.0.0.1:8787
UHP_TOKEN=<the value of LOCAL_CLI_UHP_TOKEN>
FOREMAN_WORKSPACE_SOURCE_REPO=/absolute/path/to/disposable-repo
FOREMAN_WORKSPACE_BRIDGE_URL=http://127.0.0.1:8787
FOREMAN_WORKSPACE_BRIDGE_TOKEN=<the value of LOCAL_CLI_UHP_TOKEN>
FOREMAN_WORKSPACE_ALLOWED_SCOPE=README.md
FOREMAN_VALIDATION_COMMANDS=[{"name":"build","command":"pnpm","args":["build"]}]
```
Expand All @@ -114,7 +152,10 @@ pnpm start

Open `http://127.0.0.1:4399`. The bridge and Foreman have separate processes
and configuration; `.env` configures Foreman, while the shell variables above
configure the bridge. Hindsight is optional and advisory.
configure the bridge. If you started the bridge without a token, leave
`UHP_TOKEN` and `FOREMAN_WORKSPACE_BRIDGE_TOKEN` out. Foreman does not restart
a bridge you started yourself; if it stops, start it again. Hindsight is
optional and advisory.

If AGY reports that it needs sign-in, use the interactive sign-in flow offered
by the installed `agy` CLI in a terminal as the same host user, then restart
Expand Down
17 changes: 16 additions & 1 deletion investigations/local-cli-uhp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,22 @@ provider-selection overrides are excluded. Do not set provider API keys for
this experiment. State is stored at
`LOCAL_CLI_UHP_STATE` (default `/tmp/local-cli-uhp-state.json`, mode 0600) and
work directories under `LOCAL_CLI_UHP_WORK` (default `/tmp/local-cli-uhp-work`).
The HTTP listener binds loopback only and has no authentication; keep it local.
The HTTP listener binds loopback only.

Authentication: set `LOCAL_CLI_UHP_TOKEN` (1-512 printable ASCII characters, no
spaces) and every request, including `/v1/uhp` discovery and the workspace
extension routes, must carry `Authorization: Bearer <token>`; a missing or wrong
token gets `401` before any other work, compared in constant time. Without it
the bridge accepts requests from any local process and prints a startup
warning. Foreman starts its own bridges with a fresh random token per start. The
token is removed from the bridge's environment, so the CLIs it spawns never see
it. Independently of the token, the `Host` header must be `127.0.0.1:<port>`,
`localhost:<port>` or `[::1]:<port>` (case-insensitive, one trailing dot
tolerated) or the request gets `403`, which stops DNS-rebinding attacks. The
smoke scripts in this directory send no token and need a bridge without one.
If the port cannot be bound the bridge exits with status 1 and a message on
stderr; Foreman's supervisor logs that output to `bridge.log` and treats it as a
failed restart.

### Claude subscription usage

Expand Down
Loading
Loading