diff --git a/cmd/harnesscli/service.go b/cmd/harnesscli/service.go index 591e963e8..609bb09f7 100644 --- a/cmd/harnesscli/service.go +++ b/cmd/harnesscli/service.go @@ -407,7 +407,7 @@ func runServiceInstall(args []string) int { fs := flag.NewFlagSet("service install", flag.ContinueOnError) fs.SetOutput(stderr) binary := fs.String("binary", "", "path to the harnessd binary (default: look up harnessd on PATH)") - addr := fs.String("addr", "", "listen address for harnessd (default: resolve like harnessd — HARNESS_ADDR env or :8080)") + addr := fs.String("addr", "", "listen address for harnessd (default: resolve like harnessd — HARNESS_ADDR env or 127.0.0.1:8080)") logDir := fs.String("log-dir", "", "directory for service logs (default ~/.harness/logs)") dryRun := fs.Bool("dry-run", false, "print the rendered unit file and target path without writing anything") if err := fs.Parse(args); err != nil { diff --git a/docs/logs/engineering-log.md b/docs/logs/engineering-log.md index 0e9fb86bf..47cdb1df4 100644 --- a/docs/logs/engineering-log.md +++ b/docs/logs/engineering-log.md @@ -63,6 +63,90 @@ - Verification: `go test ./cmd/harnessd ./cmd/harnesscli/... ./internal/config -race` and `go vet` on the same packages, all green. +## 2026-09-05 — Issue #1380 website/docs staleness correction + +- Scope: `website/docs/**` reference/concept/tutorial/server pages plus the one-line + `harnesscli service install --addr` help-text default at `cmd/harnesscli/service.go:410`. + A prior read-only audit (attached to epic #1369) flagged 19 website pages with + verified-wrong claims; every claim was re-verified against the current tree with `rg` + before being corrected (the audit was treated as a lead, not gospel). +- Bind default: `HARNESS_ADDR` changed to `127.0.0.1:8080` in issue #1328 + (`internal/config/config.go:254`) with a non-loopback-bind refusal + (`cmd/harnessd/bind_guard.go:29-42`); ~15 stray `:8080` claims across + reference/getting-started/concepts/server/tutorials pages were corrected to match, + and the refusal behavior is now documented next to each default. +- Resume story: `harnesscli continue` / `POST /v1/runs/{id}/continue` requires the + source run's status to be `completed` and returns 409 `run_not_completed` + otherwise (`internal/harness/runner.go:2217`, `internal/server/http_runs.go:817`); + a cancelled run cannot be resumed. Corrected in `reference/exit-codes.md`, + `cli/harnesscli.md`, `cli/go-code-wrapper.md`, `server/http-api-guide.md`, and + `concepts/runs-and-conversations.md`, which previously told operators to + `harnesscli continue` a blocked or cancelled run. +- Route inventory (`reference/http-routes.md`) was rebuilt from the actual + `mux.Handle`/`mux.HandleFunc` registrations in `internal/server/http*.go`: added + `/v1/tools`, `/v1/hooks`, `/v1/config/reload`, `/v1/model-settings*`, `/v1/tasks`, + `/v1/jobs/{id}/kill|output`, `/v1/callbacks/{id}/cancel`, `/v1/cron/runs`, + `/v1/cron/jobs/{id}/executions`, `/v1/conversations/{id}/events|rewind-points|rewind|undo`, + `/v1/runs/{id}/replay`, `/v1/providers/{name}/import-subscription`, the 6 + `/v1/relay/*` control-plane routes, and `/viz`. The `RunRequest` schema block gained + `attachments`, `plan_mode`, `plan_file`, `extra_dirs`, `denied_tools`, `rules`, and + `workspace_path` (documented as the post-#1372 state — the field does not exist in + `RunRequest` yet on this branch). +- Events catalog (`reference/events-catalog.md`): `AllEventTypes()` returns 79 of the + 86 event constants actually declared and emitted; the 7 gap events + (`callback.dispatching|failed|retry_wait|started`, `plan.approval_required|granted|denied`) + are genuinely emitted via a separate string-keyed bridge + (`internal/harness/tools/delayed_callback.go`, `internal/harness/plan_mode.go:75-107`) and + are now documented alongside a corrected 86-event total. Also added `todos.updated` and + `job.completed` (`EventBackgroundJobCompleted`, delivered to the conversation stream, not + the originating run), and fixed a stale `"tool": "ask_user_question"` payload example to + the real `"AskUserQuestion"` value. +- Tools catalog (`reference/tools-catalog.md`): added `list_models`, `deploy`, `goals`, + `cron_update`, `cron_history`, `message_subagent`, `notify_parent`, and `agent_swarm` — + all verified as real `Definition{Name: ...}` registrations in + `internal/harness/tools_default.go`. Rejected the audit's `compact_summary` entry after + verification: it is an internal message-tagging label used by the `compact_history` tool, + not a callable tool. +- Providers: `catalog/models.json` has 15 providers, not 10 — added `cerebras`, + `codex-subscription`, `kimi-subscription`, `lmstudio`, `ollama` to + `reference/providers-and-models-reference.md`, `reference/environment-variables.md`, + `concepts/providers-and-models.md`, and `getting-started/what-is-go-code.md`. Live model + discovery is provider-agnostic (OpenRouter, OpenAI, Anthropic, DeepSeek all refresh on a + 5-minute TTL via `internal/provider/openai/discovery.go:24` and + `internal/provider/anthropic/discovery.go:24`), not OpenRouter-only as previously stated. +- `max_steps`: `0` means unlimited with no daemon-level fallback to a non-zero cap + (documented as the post-#1376 state — `cmd/harnessd/config_reload.go:44-47` on this + branch still resets a resolved `0` to `8`). +- `server/expose-as-mcp-server.md` needed a substantial rewrite beyond the audit's + findings: `/mcp` is mounted via `harnessmcp.NewHTTPHandler` + (`cmd/harnessd/runtime_container.go:348-352`), not `internal/mcpserver.NewServer` (which + has no production caller). `/mcp` and the `harness-mcp` stdio proxy share the same + 25-tool, REST-backed dispatcher (`internal/harnessmcp`) — the page previously described + them as two different tool sets (10 vs. 5) and claimed `/mcp` supports `GET` SSE + notifications via `subscribe_run`; the handler is POST-only and 405s on GET + (`internal/harnessmcp/httptransport.go:29-31`). Same "five tools" and unmounted-SSE + staleness also existed in `tutorials/claude-desktop-mcp.md`, corrected there too. + `list_mcp_resources`/`read_mcp_resource` are implemented (`cmd/harnessd/mcp_setup.go:68-90`, + `internal/mcp/mcp.go:231,246`), contradicting the "not yet implemented" claim in + `integrations/mcp-consume.md`. +- Added missing `harnesscli` subcommand documentation (dispatch table in + `cmd/harnesscli/auth.go`): `steer`, `viz`, `acp`, `plugin`, `mcp`, `hooks`, `service`, + `auth kimi`, `auth codex`, and `input` (documented as the post-#1374 CLI addition; the + route `POST /v1/runs/{id}/input` it calls already exists today). +- One-line code fix: `cmd/harnesscli/service.go:410`'s `--addr` flag help text said the + resolved default was `:8080`; corrected to `127.0.0.1:8080` to match + `internal/config/config.go:254`. No test asserted the old string; + `go test ./cmd/harnesscli -run Service -race` passes unchanged. +- Verification: `rg` re-check of every corrected route/flag/env/tool/event against the + current tree; `npm run build` in `website/` (Docusaurus) completed with no broken + internal links or MDX errors. +- New finding, not fixed here (outside `website/docs` + the one help string): the VM + workspace cloud-init template (`internal/workspace/bootstrap.go:36`) sets + `HARNESS_ADDR=:8080` (a non-loopback bind) with no `HARNESS_AUTH_DISABLED` and no + configured auth, so `bind_guard.go`'s #1328 protection likely now rejects `harnessd` + startup on provisioned VM workspaces; `systemctl start harnessd || true` would swallow + the failure silently. Filed as a follow-up rather than fixed in this docs-only PR. + ## 2026-08-08 — Issue #1285 attached lifecycle PTY (in implementation) - Planned boundary: ptyrunner will accept only the typed identity returned by diff --git a/website/docs/cli/go-code-wrapper.md b/website/docs/cli/go-code-wrapper.md index 56d5d3d02..1039f2f7b 100644 --- a/website/docs/cli/go-code-wrapper.md +++ b/website/docs/cli/go-code-wrapper.md @@ -134,8 +134,8 @@ go-code "summarize the diff" case $? in 0) echo "run completed" ;; 2) echo "run failed" ;; - 3) echo "run blocked on input — resume interactively" ;; - 6) echo "run cancelled — resumable via go-code continue ..." ;; + 3) echo "run blocked on input — answer with harnesscli input \"=\", or resume interactively with go-code --resume " ;; + 6) echo "run cancelled — not resumable; harnesscli continue requires status=completed. Start a new run instead." ;; esac ``` @@ -154,6 +154,7 @@ The full code table (`0` completed, `1` client error, `2` failed, `3` blocked, ` | `go-code` | `--tui` | Launches the interactive BubbleTea TUI | | `go-code "prompt"` | `-prompt "..."` | Runs a single prompt, streams events, exits | | `go-code --server` | (server lifecycle only) | Starts `harnessd` in background and exits | +| `go-code --resume ` | `--tui -resume ` | Launches the TUI resuming an existing conversation | | `go-code runs` | `list` | Lists known runs | | `go-code list` | `list` | Alias for `runs` | | `go-code show ` | `status` | Shows one run | @@ -170,7 +171,7 @@ The full code table (`0` completed, `1` client error, `2` failed, `3` blocked, ` ### Server address: `HARNESS_ADDR` -The `HARNESS_ADDR` environment variable controls the listen address. The default is `:8080`. The wrapper extracts the port from this value and constructs the base URL as `http://127.0.0.1:`. +The `HARNESS_ADDR` environment variable controls the listen address. The default is `127.0.0.1:8080`. The wrapper extracts the port from this value and constructs the base URL as `http://127.0.0.1:`. ```bash # Run on a different port diff --git a/website/docs/cli/harnesscli.md b/website/docs/cli/harnesscli.md index 71a9496bd..edc9ed808 100644 --- a/website/docs/cli/harnesscli.md +++ b/website/docs/cli/harnesscli.md @@ -52,7 +52,7 @@ The process exit code reports the run's outcome, so scripts and CI can branch on | `1` | Client-side error: bad flags, missing prompt, connection/HTTP/stream failure | | `2` | `run.failed` — a turn failed server-side | | `3` | Blocked — the run needs input it will never get headlessly (`run.waiting_for_user`, `tool.approval_required`, or `plan.approval_required` observed while stdin is non-interactive) | -| `6` | `run.cancelled` — interrupted but resumable via `harnesscli continue ` | +| `6` | `run.cancelled` — interrupted; **not** resumable via `harnesscli continue` (that command requires the source run's status to be `completed` and returns HTTP 409 otherwise). Start a new run to continue the work. | | `130` | SIGINT/SIGTERM while streaming | The `run_id=` / `terminal_event=` stdout lines are unchanged by this mapping. See [Exit Codes](/docs/reference/exit-codes) for the full contract — blocked-signal details, goal-status reservations, and per-command coverage. @@ -69,7 +69,9 @@ The `run_id=` / `terminal_event=` stdout lines are unchanged by this mapping. Se | `-task-context` | `""` | Task context injected into the startup prompt | | `-prompt-profile` | `""` | Prompt profile override for model routing | | `-prompt-custom` | `""` | Custom prompt extension text | -| `-workspace` | cwd | Workspace directory for this run | +| `-workspace` | cwd | Workspace directory for this run (sent as `workspace_path`; see the callout below) | +| `-plan-mode` | `false` | Start the run in enforced read-only plan mode (`plan_mode` in the request); see [Enforced Plan Mode](/docs/concepts/configuration) | +| `-resume` | `""` | Resume an existing conversation by ID in the TUI; implies `-tui` | | `-tui` | `false` | Launch the interactive BubbleTea TUI (requires a real terminal) | | `-list-profiles` | `false` | List available profiles and exit | | `-prompt-behavior` | (empty) | Behavior extension IDs — repeatable or comma-separated | @@ -102,7 +104,7 @@ terminal_event=run.completed ``` -`-workspace` defaults to the current working directory via `os.Getwd()`. The value is serialized as `workspace_path` in the run creation request, but the server's `POST /v1/runs` handler decodes into `harness.RunRequest`, which has no `workspace_path` field — the value is currently silently ignored server-side. Workspace selection is controlled by `workspace_type` and profile-level runner configuration, not by this flag. +`-workspace` defaults to the current working directory via `os.Getwd()` and is sent as `workspace_path` in the run creation request. The server honors `workspace_path` when it is an absolute path to an existing directory: tools for the run are rooted there instead of the server's own working directory. `workspace_type` and profile-level runner configuration control workspace *provisioning* (local directory, git worktree, container, VM); `workspace_path` only selects which existing directory a non-provisioned (local-process) run is rooted in. @@ -177,6 +179,10 @@ Output includes: ID, Status, Model, Created, Updated, Prompt (truncated at 80 ch Send a follow-up prompt to an existing run, creating a new run in the same conversation. + +`continue` only works when the source run's status is `completed`. It returns HTTP 409 `run_not_completed` for any other status (`waiting_for_user`, `waiting_for_approval`, `running`, `queued`, `failed`, or `cancelled`) — a cancelled run cannot be resumed at all, and a run blocked on a question or approval must be unblocked first (see `harnesscli input` below, or `POST /v1/runs/{id}/approve` / `/deny`). + + ```bash # Stream the continuation (default): harnesscli continue Now explain it to a 5-year-old @@ -198,6 +204,21 @@ When `-no-stream` is false (the default), the new run's events are streamed and --- +### input + +Answer a run that is blocked on `run.waiting_for_user` (the run invoked the `AskUserQuestion` tool). + +```bash +harnesscli input "question-key=the answer" +harnesscli input "q1=yes" "q2=no" +``` + +**API:** `POST /v1/runs/{id}/input` with body `{"answers": {"": ""}}` + +Each positional argument after the run ID is split on the first `=`; the part before `=` is the question key (as returned by `GET /v1/runs/{id}/input`) and the part after is the answer. The run resumes automatically once all pending questions are answered. + +--- + ### replay Replay a recorded rollout. The rollout can be provided as a run ID (the server locates the JSONL file) or as a direct rollout file path. @@ -250,6 +271,109 @@ The query is all positional args joined with spaces. Matching is case-insensitiv --- +### steer + +Inject a steering message into an active run without stopping it. + +```bash +harnesscli steer "focus on the auth module instead" +``` + +**API:** `POST /v1/runs/{id}/steer` with body `{"prompt": "..."}` + +The server queues the message; the harness delivers it to the agent as a user message at the next step boundary, and the run keeps going. Empty or whitespace-only prompts are rejected client-side before any request is sent. + +--- + +### viz + +Print the URL for the `/viz` static visualization UI served by `harnessd`, optionally opening it in the default browser. + +```bash +harnesscli viz +harnesscli viz --open +``` + +--- + +### acp + +Serve the Agent Client Protocol (newline-delimited JSON-RPC 2.0) over stdin/stdout so ACP-compatible editors (Zed, JetBrains via ACP) can drive go-code as a subprocess. This is the same protocol the standalone `harness-acp` binary exposes; `harnesscli acp` is an equivalent entrypoint reached through the main CLI. See the [ACP runbook](https://github.com/dennisonbertram/go-code/blob/main/docs/runbooks/acp.md) for the manual Zed verification checklist. + +```bash +harnesscli acp +harnesscli acp -server http://my-harness:9090 +``` + +stdout is a pure protocol channel — all diagnostics go to stderr. + +--- + +### plugin + +Manage installable plugin bundles (`plugin.json` bundles under `~/.go-harness/plugins`). + +```bash +harnesscli plugin install +harnesscli plugin list +harnesscli plugin uninstall +harnesscli plugin update +harnesscli plugin trust +harnesscli plugin untrust +harnesscli plugin marketplace +``` + +Trusted bundles alone reach profiles, MCP validation, and hooks; enabled visibility is independent from executable trust. See `docs/design/plugins.md` for the bundle schema. + +--- + +### mcp + +Manage saved credentials for remote MCP servers configured for this CLI. + +```bash +harnesscli mcp login +harnesscli mcp status +harnesscli mcp logout +``` + +--- + +### hooks + +Manage trust for config-driven lifecycle hook files (shell/HTTP hooks, epic #737). + +```bash +harnesscli hooks trust +harnesscli hooks revoke +harnesscli hooks list +``` + +Hook loading itself is read-only and startup-computed; use `GET /v1/hooks` to see what a running `harnessd` actually loaded. See `docs/design/plugins.md` → "Config-driven hooks" for the hook-file schema. + +--- + +### service + +Install, manage, and check the status of `harnessd` as an OS-level background service (launchd on macOS, systemd on Linux). + +```bash +harnesscli service install --binary /path/to/harnessd --addr 127.0.0.1:8080 +harnesscli service start +harnesscli service stop +harnesscli service status +harnesscli service uninstall +``` + +| Flag (install) | Default | Description | +|---|---|---| +| `--binary` | look up `harnessd` on `PATH` | Path to the `harnessd` binary | +| `--addr` | resolve like `harnessd` — `HARNESS_ADDR` env or `127.0.0.1:8080` | Listen address for harnessd | +| `--log-dir` | `~/.harness/logs` | Directory for service logs | +| `--dry-run` | `false` | Print the rendered unit file and target path without writing anything | + +--- + ## auth login and config files ### auth login @@ -277,6 +401,32 @@ On success, `auth login`: The generated key carries three scopes: `store.ScopeRunsRead`, `store.ScopeRunsWrite`, and `store.ScopeAdmin`. +### auth kimi + +Manage Kimi Code subscription auth (epic #848). Reuses a `kimi-code`-authenticated vendor session through a harness-owned credential copy at `~/.harness/subscription-auth/kimi.json`; it never writes under `~/.kimi-code/`. + +```bash +kimi-code login # vendor CLI login, done once outside harnesscli +harnesscli auth kimi login +harnesscli auth kimi status +harnesscli auth kimi logout +``` + +`logout` removes only `~/.harness/subscription-auth/kimi.json`. + +### auth codex + +Manage Codex subscription auth (epic #847). Reuses a ChatGPT-authenticated vendor Codex session through a harness-owned credential copy at `~/.harness/subscription-auth/codex.json`; it never writes under `~/.codex/` and only reads from it. + +```bash +codex login # vendor CLI login, done once outside harnesscli +harnesscli auth codex login +harnesscli auth codex status +harnesscli auth codex logout +``` + +`logout` removes only `~/.harness/subscription-auth/codex.json`. The `openai` provider (`OPENAI_API_KEY`) remains the primary, unaffected path. + ### Config file locations `harnesscli` uses two separate config files for different purposes: @@ -390,12 +540,11 @@ For reference, here are all the server routes that `harnesscli` calls: | `POST` | `/v1/runs/{id}/continue` | continue | | `POST` | `/v1/runs/replay` | replay | | `GET` | `/v1/profiles` | -list-profiles | -| `GET` | `/v1/runs/{id}/input` | ask-user (non-TUI, see note) | -| `POST` | `/v1/runs/{id}/input` | ask-user (non-TUI, see note) | - - -`handleAskUserQuestion` — the function that calls `/v1/runs/{id}/input` to handle interactive `run.waiting_for_user` events — is defined in `cmd/harnesscli/askuser.go` and tested independently, but is **not wired** into the non-TUI streaming loop in `main.go`. Interactive question-answering in streaming mode is not yet available outside the TUI. - +| `GET` | `/v1/runs/{id}/input` | `input` (reads pending questions) | +| `POST` | `/v1/runs/{id}/input` | `input` (posts answers) | +| `POST` | `/v1/runs/{id}/steer` | steer | +| `POST` | `/v1/runs/{id}/approve` | approve (TUI) | +| `POST` | `/v1/runs/{id}/deny` | deny (TUI) | --- diff --git a/website/docs/concepts/architecture.md b/website/docs/concepts/architecture.md index e431f1c3a..6d758dd36 100644 --- a/website/docs/concepts/architecture.md +++ b/website/docs/concepts/architecture.md @@ -20,7 +20,7 @@ This page maps every named component and shows how a prompt travels from your te The `go-code` shell script (`scripts/go-code.sh`) is the single user-facing entry point. When you run it, it: -1. Detects whether a healthy `harnessd` is already listening on `HARNESS_ADDR` (default `:8080`). +1. Detects whether a healthy `harnessd` is already listening on `HARNESS_ADDR` (default `127.0.0.1:8080`). 2. If not, starts one in the background. 3. Forwards your command to `harnesscli`. 4. On exit, stops the server only if it started it — a pre-existing server is always left running. diff --git a/website/docs/concepts/configuration.md b/website/docs/concepts/configuration.md index b755a10f7..b60e0b999 100644 --- a/website/docs/concepts/configuration.md +++ b/website/docs/concepts/configuration.md @@ -41,7 +41,7 @@ Both config files use **TOML**. The full schema is divided into a top-level core # ── Core ────────────────────────────────────────────────── model = "gpt-4.1-mini" # LLM model identifier max_steps = 0 # 0 = unlimited (the harnessd runtime default, no implicit cap) -addr = ":8080" # HTTP listen address (socket form, not a URL) +addr = "127.0.0.1:8080" # HTTP listen address (socket form, not a URL) # ── Per-run cost ceiling ────────────────────────────────── [cost] @@ -162,7 +162,7 @@ Invalid `HARNESS_*` values fail silently. Always verify your environment after a | Variable | TOML key | Default | Description | |----------|----------|---------|-------------| | `HARNESS_MODEL` | `model` | `gpt-4.1-mini` | LLM model identifier | -| `HARNESS_ADDR` | `addr` | `:8080` | HTTP listen address (socket form) | +| `HARNESS_ADDR` | `addr` | `127.0.0.1:8080` | HTTP listen address (socket form) | | `HARNESS_MAX_STEPS` | `max_steps` | `0` (unlimited) | Max tool-call steps per run | | `HARNESS_MAX_COST_PER_RUN_USD` | `cost.max_per_run_usd` | `0.0` (unlimited) | Per-run cost ceiling in USD | | `HARNESS_WORKSPACE` | — | `.` | Workspace root; controls project config and DB paths | @@ -313,7 +313,7 @@ A profile may also declare `extends = ""` to inherit fields f **`HARNESS_ADDR` is a socket address, not a URL.** -The default is `:8080` — a bare port suitable for `net.Listen`. It is not `http://localhost:8080`. Clients connect to `http://localhost:8080`; the server *listens* on `:8080`. +The default is `127.0.0.1:8080` — a host:port pair suitable for `net.Listen`, not `http://localhost:8080`. Clients connect to `http://localhost:8080`; the server *listens* on `127.0.0.1:8080`. A non-loopback bind (for example a bare `:PORT`) is refused at startup unless authentication is configured or `HARNESS_AUTH_DISABLED=true` is set deliberately (`cmd/harnessd/bind_guard.go:29-42`, issue #1328). ## Next steps diff --git a/website/docs/concepts/providers-and-models.md b/website/docs/concepts/providers-and-models.md index f67393513..bf730571b 100644 --- a/website/docs/concepts/providers-and-models.md +++ b/website/docs/concepts/providers-and-models.md @@ -63,7 +63,7 @@ The `api` field controls which HTTP endpoint is used: `"responses"` routes the c ## Supported providers -go-code ships with ten providers pre-wired in the catalog. The table shows the provider key you use in API calls, the required API key environment variable, and the wire protocol. +go-code ships with 15 providers pre-wired in the catalog. The table shows the provider key you use in API calls, the required API key environment variable, and the wire protocol. The `anthropic` provider uses a native Anthropic client (`internal/provider/anthropic`). All other providers use an OpenAI-compatible client (`internal/provider/openai`), even when the underlying API is not from OpenAI. @@ -75,12 +75,17 @@ The `anthropic` provider uses a native Anthropic client (`internal/provider/anth | `anthropic` | Anthropic | `ANTHROPIC_API_KEY` | `anthropic` | | `deepseek` | DeepSeek | `DEEPSEEK_API_KEY` | `openai_compat` | | `groq` | Groq | `GROQ_API_KEY` | `openai_compat` | +| `cerebras` | Cerebras | `CEREBRAS_API_KEY` | `openai_compat` | | `xai` | xAI (Grok) | `XAI_API_KEY` | `openai_compat` | | `kimi` | Kimi (Moonshot) | `MOONSHOT_API_KEY` | `openai_compat` | +| `kimi-subscription` | Kimi Code Subscription | (vendor-CLI session import) | `openai_compat` | | `qwen` | Qwen (DashScope) | `DASHSCOPE_API_KEY` | `openai_compat` | | `together` | Together AI | `TOGETHER_API_KEY` | `openai_compat` | +| `codex-subscription` | Codex (ChatGPT subscription) | (vendor-CLI session import) | `openai_compat` | | `openrouter` | OpenRouter | `OPENROUTER_API_KEY` | `openai_compat` | | `gemini` | Google Gemini | `GOOGLE_API_KEY` | `openai` | +| `ollama` | Ollama | (none — local server) | `openai_compat` | +| `lmstudio` | LM Studio | (none — local server) | `openai_compat` | A provider is considered "configured" when its API key environment variable is set. You can check which providers are configured at runtime: @@ -267,9 +272,9 @@ Each `POST /v1/runs` call resolves a provider independently, letting different r Aliases let you use short names like `"codex"` instead of `"gpt-5.1-codex-mini"`. Each provider in the catalog can define an `aliases` map. The resolver follows chains up to 8 hops to prevent cycles. -### OpenRouter dynamic discovery +### Live model discovery -OpenRouter can serve thousands of models not listed in the static catalog. When `OPENROUTER_API_KEY` is set, the harness fetches `https://openrouter.ai/api/v1/models` with a 5-minute TTL cache. Live results are merged additively with the static catalog — static metadata wins on conflicts. +Live discovery is provider-agnostic, not OpenRouter-only: OpenRouter, OpenAI, Anthropic, and DeepSeek entries all refresh from the provider's own models endpoint on a 5-minute TTL (`internal/provider/openai/discovery.go:24`, `internal/provider/anthropic/discovery.go:24`). When the matching API key is set, the harness fetches the live list (for OpenRouter: `https://openrouter.ai/api/v1/models`) and merges it additively with the static catalog — static metadata wins on ID conflicts. A failed refresh never removes static models; the last successful result is served stale. As a convenience, any model ID containing `/` is automatically routed to the `openrouter` provider if the key is configured. This means you can pass `"openai/gpt-4.1"` directly in `model` without setting `provider_name`, and go-code will route it to OpenRouter. diff --git a/website/docs/concepts/runs-and-conversations.md b/website/docs/concepts/runs-and-conversations.md index 724f1e516..fa33503b5 100644 --- a/website/docs/concepts/runs-and-conversations.md +++ b/website/docs/concepts/runs-and-conversations.md @@ -49,7 +49,7 @@ A run moves through a well-defined set of statuses over its lifetime: | `completed` | Finished successfully. `run.completed` is emitted and the stream closes. | | `failed` | Finished with an error. `run.failed` is emitted and the stream closes. | | `cancelled` | Stopped by a `POST /v1/runs/{id}/cancel` call. `run.cancelled` is emitted and the stream closes. | -| `waiting_for_user` | Paused on an interactive question (`AskUserQuestion` tool). Resumes when answers are submitted. | +| `waiting_for_user` | Paused on an interactive question (`AskUserQuestion` tool). Resumes when answers are submitted via `POST /v1/runs/{id}/input` (or `harnesscli input "="`). | | `waiting_for_approval` | Paused on a tool call that requires explicit operator approval. Resumes when approved or denied. | The three **terminal events** — `run.completed`, `run.failed`, and `run.cancelled` — signal the definitive end of the run. When your SSE client receives any of these, it should close the connection. diff --git a/website/docs/getting-started/installation.md b/website/docs/getting-started/installation.md index 4b6e0d6da..54da524f5 100644 --- a/website/docs/getting-started/installation.md +++ b/website/docs/getting-started/installation.md @@ -157,7 +157,7 @@ Regardless of which method you use, a complete install puts these three things i |------|------------| | `go-code` | Shell wrapper (`scripts/go-code.sh`). Auto-starts `harnessd` when no server is running, then launches the TUI or streams a prompt. This is the command you will use day-to-day. | | `harnesscli` | Terminal client and BubbleTea TUI (`cmd/harnesscli`). `go-code` delegates to it under the hood. | -| `harnessd` | Local HTTP daemon and runtime bootstrap (`cmd/harnessd`). Listens on `:8080` by default. Handles runs, events, tools, providers, workflows, and more. | +| `harnessd` | Local HTTP daemon and runtime bootstrap (`cmd/harnessd`). Listens on `127.0.0.1:8080` by default. Handles runs, events, tools, providers, workflows, and more. | | `prompts/` + `catalog/` | Runtime assets — bundled prompt templates and the model/provider catalog. `harnessd` reads these at startup. | --- diff --git a/website/docs/getting-started/quickstart.md b/website/docs/getting-started/quickstart.md index 11a93aa21..620c17a59 100644 --- a/website/docs/getting-started/quickstart.md +++ b/website/docs/getting-started/quickstart.md @@ -159,7 +159,7 @@ go-code "Summarize the repository" ### What happens under the hood 1. `go-code` traverses parent directories looking for `.git/` or `.harness/config.toml`. If neither is found it falls back to `$PWD`. The resolved directory becomes the workspace root. -2. If no healthy server is already running on the configured port (default `:8080`), `go-code` starts `harnessd` in the background. **It only stops the server on exit if it started it** — a pre-existing server is always left alone. +2. If no healthy server is already running on the configured port (default `127.0.0.1:8080`), `go-code` starts `harnessd` in the background. **It only stops the server on exit if it started it** — a pre-existing server is always left alone. 3. The `go-code` wrapper invokes `harnesscli`, which POSTs the run to harnessd via `POST /v1/runs`; harnessd's runner invokes the LLM, and `harnesscli` streams events over SSE from `GET /v1/runs/{id}/events`. diff --git a/website/docs/getting-started/what-is-go-code.md b/website/docs/getting-started/what-is-go-code.md index f721aa0e4..ded6f23b7 100644 --- a/website/docs/getting-started/what-is-go-code.md +++ b/website/docs/getting-started/what-is-go-code.md @@ -30,7 +30,7 @@ Key properties: - **Written in Go.** One static binary, no language runtime to install, and goroutine-based concurrency underneath the orchestration. It cross-compiles and drops into a tiny container image. - **Built for parallelism.** A workflow engine fans agents out with `ctx.Parallel()` and `ctx.Pipeline()` (bounded by a concurrency semaphore); isolated git worktrees let many agents work one repository at once with no checkout conflicts; warm workspace pools, containers, and VMs extend that across machines, with a relay control plane for multi-location routing in progress. - **Local-first.** `harnessd` runs on your laptop, pointed at your working directory. No repository upload, no remote execution required. Cloud and relay features exist but are optional additions on top of this local core. -- **Provider-aware routing.** A JSON catalog (`catalog/models.json`) describes 10 providers — `openai`, `anthropic`, `gemini`, `deepseek`, `groq`, `xai`, `kimi`, `qwen`, `together`, `openrouter` — with per-provider API keys, pricing, and capability flags. go-code routes to whichever provider and model you configure, with an optional per-run cost ceiling (`HARNESS_MAX_COST_PER_RUN_USD`). +- **Provider-aware routing.** A JSON catalog (`catalog/models.json`) describes 15 providers — `openai`, `anthropic`, `gemini`, `deepseek`, `groq`, `cerebras`, `xai`, `kimi`, `kimi-subscription`, `qwen`, `together`, `codex-subscription`, `openrouter`, `ollama`, `lmstudio` — with per-provider API keys, pricing, and capability flags. go-code routes to whichever provider and model you configure, with an optional per-run cost ceiling (`HARNESS_MAX_COST_PER_RUN_USD`). - **Streamed everything.** Every event — LLM token deltas, tool calls, cost accounting, workspace provisioning — is emitted on an SSE stream at `GET /v1/runs/{id}/events`. The TUI and CLI consume the same stream that your own scripts can consume. - **Key-free smoke path.** Set `HARNESS_PROVIDER=fake` to run the full stack without any API key. This is how CI tests and new contributor smoke checks work. @@ -168,7 +168,7 @@ Registered workflows are exposed at `POST /v1/script-workflows/{name}/runs` and go-code uses a 6-layer configuration cascade (lowest to highest priority): -1. Built-in defaults (`harnessd` default model: `gpt-4.1-mini`, listen address: `:8080`) +1. Built-in defaults (`harnessd` default model: `gpt-4.1-mini`, listen address: `127.0.0.1:8080`) 2. User global config: `~/.harness/config.toml` 3. Project config: `.harness/config.toml` in the workspace root 4. Named profile: `~/.harness/profiles/.toml` (via `harnessd --profile `) @@ -179,9 +179,9 @@ The most useful environment variables to know up front: | Variable | Default | Purpose | |---|---|---| -| `HARNESS_ADDR` | `:8080` | Server listen address | +| `HARNESS_ADDR` | `127.0.0.1:8080` | Server listen address | | `HARNESS_MODEL` | `gpt-4.1-mini` | Default LLM model | -| `HARNESS_MAX_STEPS` | `8` | Max tool-call steps per run | +| `HARNESS_MAX_STEPS` | `0` (unlimited) | Max tool-call steps per run | | `HARNESS_MAX_COST_PER_RUN_USD` | `0` (unlimited) | Per-run cost ceiling in USD | | `HARNESS_PROVIDER` | (catalog) | Set to `fake` for key-free smoke testing | | `HARNESS_WORKSPACE` | `.` | Workspace root for the agent | diff --git a/website/docs/integrations/mcp-consume.md b/website/docs/integrations/mcp-consume.md index af92eab72..07d9bdd04 100644 --- a/website/docs/integrations/mcp-consume.md +++ b/website/docs/integrations/mcp-consume.md @@ -197,8 +197,8 @@ Two additional deferred tools let the agent work with MCP _resources_ (data obje | `list_mcp_resources` | `mcp_name` | List all resources exposed by the named server | | `read_mcp_resource` | `mcp_name`, `uri` | Read a resource by its URI | - -In the current production implementation, `list_mcp_resources` returns an empty list and `read_mcp_resource` returns an error. MCP resource support is defined in the interface but not yet implemented in the production `clientManagerRegistry`. Do not rely on these tools returning meaningful data in the current release. + +`list_mcp_resources` and `read_mcp_resource` are implemented: they call through `tools.MCPRegistry` (`internal/harness/tools/deferred/mcp.go:51,89`) to the production `clientManagerRegistry` (`cmd/harnessd/mcp_setup.go:68-90`), which delegates to `internal/mcp.ClientManager.ListResources`/`ReadResource` (`internal/mcp/mcp.go:231,246`) against the real connected server. ### `connect_mcp` — connecting mid-session diff --git a/website/docs/reference/cli-flags.md b/website/docs/reference/cli-flags.md index 2bfe28f49..700413590 100644 --- a/website/docs/reference/cli-flags.md +++ b/website/docs/reference/cli-flags.md @@ -28,7 +28,7 @@ Go's `flag` package accepts both single-dash and double-dash forms for every fla | `--mcp` | bool | `false` | Start as an MCP stdio server instead of an HTTP server. Reads stdin and writes stdout; does not bind a TCP port. | | `--mcp-workspace` | string | `""` (resolves to `$PWD`) | Workspace root used when `--mcp` is active. Falls back to `HARNESS_WORKSPACE`, then `.`. | -Source: `cmd/harnessd/main.go:170-178` +Source: `cmd/harnessd/main.go:257-265` ### Key environment variables @@ -36,9 +36,9 @@ Source: `cmd/harnessd/main.go:170-178` | Variable | Default | Description | |---|---|---| -| `HARNESS_ADDR` | `:8080` | HTTP listen address (e.g. `:9000` or `127.0.0.1:8080`). | +| `HARNESS_ADDR` | `127.0.0.1:8080` | HTTP listen address (e.g. `:9000` or `127.0.0.1:8080`). A non-loopback bind is refused at startup unless authentication is configured or `HARNESS_AUTH_DISABLED=true` is set deliberately (`cmd/harnessd/bind_guard.go:29-42`, issue #1328). | | `HARNESS_MODEL` | `"gpt-4.1-mini"` | Default LLM model identifier. | -| `HARNESS_MAX_STEPS` | `8` | Maximum tool-call steps per run. Set to `0` in config for unlimited; the daemon resets a `0` config default back to `8` for backward compatibility. | +| `HARNESS_MAX_STEPS` | `0` (unlimited) | Maximum tool-call steps per run. `0` means unlimited — there is no default step cap. | | `HARNESS_WORKSPACE` | `.` | Workspace root path. | | `HARNESS_PROVIDER` | (catalog) | Set to `"fake"` for key-free deterministic smoke testing. | | `HARNESS_FAKE_TURNS` | `""` | Path to the JSON turns file when `HARNESS_PROVIDER=fake`. Required when the fake provider is active. | @@ -77,7 +77,9 @@ Source: `cmd/harnesscli/main.go:123` | `-task-context` | string | `""` | Task context injected into the startup prompt. | | `-prompt-profile` | string | `""` | Prompt profile override for model routing. | | `-prompt-custom` | string | `""` | Custom prompt extension text appended to the prompt. | -| `-workspace` | string | `""` (resolves to cwd) | Workspace directory for this run. Resolved via `os.Getwd()` when empty. | +| `-workspace` | string | `""` (resolves to cwd) | Workspace directory for this run. Resolved via `os.Getwd()` when empty, sent as `workspace_path`, and honored by the server when it is an absolute path to an existing directory (tools are rooted there). | +| `-plan-mode` | bool | `false` | Start the run in enforced read-only plan mode (sent as `plan_mode`). | +| `-resume` | string | `""` | Resume an existing conversation by ID in the TUI; implies `-tui`. | | `-tui` | bool | `false` | Launch the interactive BubbleTea TUI. Requires a real terminal — fails with an error if stdout is a pipe. | | `-list-profiles` | bool | `false` | Fetch and print available profiles, then exit. | | `-prompt-behavior` | csvListFlag | (empty) | Behavior extension IDs. Accepts comma-separated values or repeated flags. | @@ -165,6 +167,26 @@ Output is pretty-printed JSON to stdout. Source: `cmd/harnesscli/runctl.go:334-340` +### `input` subcommand + +Answers a run blocked on `run.waiting_for_user`. Takes a run ID as the first positional argument and one or more `key=value` answer pairs as the remaining arguments. Calls `POST /v1/runs/{id}/input` with body `{"answers": {...}}`. + +| Flag | Default | Description | +|---|---|---| +| `-base-url` | `http://localhost:8080` | Harness API base URL. | + +Source: `internal/server/http_runs.go:355,863-883` + +### `steer` subcommand + +Injects a steering message into an active run without stopping it. Takes a run ID as the first positional argument and the message as the remaining arguments (joined with spaces). Calls `POST /v1/runs/{id}/steer`. + +| Flag | Default | Description | +|---|---|---| +| `-base-url` | `http://localhost:8080` | Harness API base URL. | + +Source: `cmd/harnesscli/runctl.go:175-215` + ### `search` subcommand Takes one or more positional arguments as the query (joined with spaces). Fetches all runs from `GET /v1/runs` and filters client-side — there is no dedicated server search endpoint. @@ -219,6 +241,21 @@ Source: `cmd/harnesscli/auth.go:32-37` `auth login` does not contact the server. The key is generated locally and stored at `~/.harness/config.json` (permissions: directory `0700`, file `0600`). The server URL you pass is stored as metadata only — no validation occurs. +### `auth kimi` / `auth codex` subcommands + +Manage subscription auth for Kimi Code (epic #848) and Codex (epic #847). Both reuse a vendor-CLI-authenticated session through a harness-owned credential copy; neither writes under the vendor's own config directory. + +| Subcommand | Description | +|---|---| +| `auth kimi login` | Import the `kimi-code`-authenticated session into `~/.harness/subscription-auth/kimi.json`. | +| `auth kimi status` | Print the stored credential's status. | +| `auth kimi logout` | Remove `~/.harness/subscription-auth/kimi.json`. | +| `auth codex login` | Import the ChatGPT-authenticated Codex session into `~/.harness/subscription-auth/codex.json`. | +| `auth codex status` | Print the stored credential's status. | +| `auth codex logout` | Remove `~/.harness/subscription-auth/codex.json`. | + +Source: `cmd/harnesscli/auth.go:89-183` + --- ## `go-code` — the user-facing wrapper @@ -239,14 +276,15 @@ Source: `cmd/harnesscli/auth.go:32-37` | `go-code replay ` | `replay` | Replays a recorded run. | | `go-code search ` | `search` | Searches run metadata. | | `go-code improve [--target seam]` | `improve` | Runs or plans the self-improvement test loop. | +| `go-code --resume ` | `tui` | Launches the TUI resuming an existing conversation (passes `-resume ` to `harnesscli`). | -Source: `scripts/go-code.sh:31-56`, `scripts/go-code.sh:210-282` +Source: `scripts/go-code.sh:31-56`, `scripts/go-code.sh:210-282`, `scripts/go-code.sh:10` ### Relevant environment variables | Variable | Default | Description | |---|---|---| -| `HARNESS_ADDR` | `:8080` | Server listen address. The port is extracted and used to construct the base URL passed to `harnesscli`. | +| `HARNESS_ADDR` | `127.0.0.1:8080` | Server listen address. The port is extracted and used to construct the base URL passed to `harnesscli`. | | `GO_CODE_DATA_DIR` | (auto-detected) | Override the runtime asset root (prompts, catalog). | | `HARNESS_MODEL_CATALOG_PATH` | (auto-detected) | Path to `catalog/models.json`. | @@ -308,8 +346,14 @@ Cron expressions are 5-field UTC only (Minute Hour DOM Month DOW). The `robfig/c | `CRONSD_ADDR` | `:9090` | Listen address. | | `CRONSD_DB_PATH` | `~/.go-harness/cronsd.db` | SQLite database file path. | | `CRONSD_MAX_CONCURRENT` | `5` | Maximum simultaneous job executions. | - -Source: `cmd/cronsd/main.go:65-69` +| `CRONSD_INGRESS_API_KEY` | `""` | API key required on inbound requests to `cronsd` itself. | +| `CRONSD_INGRESS_TENANT_ID` | `""` | Tenant ID associated with `CRONSD_INGRESS_API_KEY`. | +| `CRONSD_HARNESS_URL` | `""` | Base URL of the `harnessd` instance `cronsd` dispatches jobs to. | +| `CRONSD_HARNESS_API_KEY` | `""` | API key `cronsd` uses when calling `harnessd`. Must differ from `CRONSD_INGRESS_API_KEY`. | +| `CRONSD_HARNESS_CONNECT_TIMEOUT` | `5s` | Connect timeout for `cronsd` → `harnessd` requests. | +| `CRONSD_HARNESS_REQUEST_TIMEOUT` | `15s` | Overall request timeout for `cronsd` → `harnessd` requests. | + +Source: `cmd/cronsd/main.go:173-204` `cronsd` has no CLI flags beyond the implicit help output. All configuration is via environment variables. diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index d268381e6..510be03e1 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -25,8 +25,8 @@ These variables cover the fundamental harness runtime: address, model, cost limi | Variable | TOML equivalent | Default | Description | |---|---|---|---| | `HARNESS_MODEL` | `model` | `"gpt-4.1-mini"` | Default LLM model identifier for every run. Any model ID or alias from the catalog is valid. | -| `HARNESS_ADDR` | `addr` | `":8080"` | TCP listen address for the HTTP server (socket form, e.g. `":9090"`, not a URL). | -| `HARNESS_MAX_STEPS` | `max_steps` | `0` (unlimited) | Max tool-calling steps per run. `0` means no limit; set an explicit positive value to cap it. | +| `HARNESS_ADDR` | `addr` | `"127.0.0.1:8080"` | TCP listen address for the HTTP server (socket form, e.g. `":9090"`, not a URL). A non-loopback bind is refused at startup unless authentication is configured or `HARNESS_AUTH_DISABLED=true` is set deliberately. | +| `HARNESS_MAX_STEPS` | `max_steps` | `0` (unlimited) | Max tool-calling steps per run. `0` means unlimited — there is no default step cap. | | `HARNESS_MAX_COST_PER_RUN_USD` | `cost.max_per_run_usd` | `0.0` (unlimited) | Per-run cost ceiling in USD. `0` means no limit. | | `HARNESS_WORKSPACE` | — | `"."` | Workspace root directory. Determines where project TOML config and database files are found. | | `HARNESS_SYSTEM_PROMPT` | — | Built-in coding assistant prompt | Override the default system prompt text for all runs. | @@ -37,8 +37,8 @@ These variables cover the fundamental harness runtime: address, model, cost limi | `HARNESS_SSE_KEEPALIVE_SECONDS` | — | `15` | Interval between SSE keepalive pings on event streams. | | `HARNESS_AUTH_DISABLED` | — | — | Set to `"true"` to disable Bearer token authentication entirely. Implied when `HARNESS_RUN_DB` is not set (no key store exists). | - -`HARNESS_MAX_STEPS=0` in a TOML file means "unlimited." However, if the env var is absent and the resolved config value is 0, `harnessd` resets it to 8 as a backward-compatible default. To truly remove the step limit at runtime, set `HARNESS_MAX_STEPS=0` explicitly in the environment. + +`HARNESS_MAX_STEPS=0` (the default) means "unlimited" — there is no daemon-level fallback that raises it to a non-zero cap. Set an explicit positive value to cap tool-calling steps per run. ### Fake provider (key-free testing) @@ -60,14 +60,19 @@ Each provider in the model catalog declares an `api_key_env` field. The table be | `anthropic` | `ANTHROPIC_API_KEY` | `https://api.anthropic.com/v1` | | `deepseek` | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` | | `groq` | `GROQ_API_KEY` | `https://api.groq.com/openai/v1` | +| `cerebras` | `CEREBRAS_API_KEY` | `https://api.cerebras.ai/v1` | | `xai` | `XAI_API_KEY` | `https://api.x.ai/v1` | | `kimi` | `MOONSHOT_API_KEY` | `https://api.moonshot.ai/v1` | +| `kimi-subscription` | (none — vendor-CLI session import) | `https://api.kimi.com/coding/v1` | | `qwen` | `DASHSCOPE_API_KEY` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | | `together` | `TOGETHER_API_KEY` | `https://api.together.xyz/v1` | +| `codex-subscription` | (none — vendor-CLI session import) | `https://chatgpt.com/backend-api/codex` | | `openrouter` | `OPENROUTER_API_KEY` | `https://openrouter.ai/api/v1` | | `gemini` | `GOOGLE_API_KEY` | `https://generativelanguage.googleapis.com/v1beta/openai` | +| `ollama` | (none — local server) | `http://localhost:11434/v1` | +| `lmstudio` | (none — local server) | `http://localhost:1234/v1` | -Only one key is required at startup: whichever matches the default model. Additional keys are loaded on demand when a run requests a different provider. +That is 15 providers total (`catalog/models.json`). Only one key is required at startup: whichever matches the default model. Additional keys are loaded on demand when a run requests a different provider. `kimi-subscription` and `codex-subscription` authenticate via `harnesscli auth kimi login` / `auth codex login` (imported vendor-CLI credentials) instead of an API key env var — see [harnesscli Reference](/docs/cli/harnesscli#auth-kimi). `OPENAI_API_KEY` also serves as the legacy bootstrap path: if no catalog-matched provider is configured, `harnessd` falls back to a bare OpenAI client if this variable is set. @@ -84,6 +89,21 @@ Only one key is required at startup: whichever matches the default model. Additi | `HARNESS_OPENROUTER_REFERER` | `"https://github.com/dennisonbertram/go-agent-harness"` | Value sent as the HTTP `Referer` header on OpenRouter requests. | | `HARNESS_OPENROUTER_TITLE` | `"go-agent-harness"` | Value sent as the `X-Title` header on OpenRouter requests. | +### Retry, process lifecycle, and misc + +| Variable | Default | Description | +|---|---|---| +| `HARNESS_RETRY_MAX_ATTEMPTS` | `3` | Maximum provider-call retry attempts (`internal/provider/retry.go:41-44`). Useful against providers with aggressive rate limiting, e.g. free tiers. | +| `HARNESS_RETRY_MAX_TOTAL_SEC` | `60` | Maximum total time (seconds) spent retrying a single provider call (`internal/provider/retry.go:46-49`). | +| `HARNESS_LISTEN_FD` | — | File descriptor number to adopt as the HTTP listener instead of binding a new socket. Used for zero-downtime restart handoff (`cmd/harnessd/main.go:1116`). | +| `HARNESS_EXIT_WITH_PARENT` | `false` | When `true`, `harnessd` watches its parent process and exits when the parent exits (`cmd/harnessd/parent_watchdog.go:76-85`). | +| `HARNESS_MODEL_STORE_PATH` | `modelstore.DefaultPath()` | Override the file path for the model-settings store (`cmd/harnessd/model_settings.go:104-107`). | +| `HARNESS_PROVIDER_CATALOG_DIR` | Auto-detected | Override the directory containing per-provider catalog JSON files (`cmd/harnessd/bootstrap_helpers.go:733`). | +| `HARNESS_COLOR_PROFILE` | `"auto"` | TUI color profile override: `truecolor`, `256`, `ansi`, or `none` (`cmd/harnesscli/main.go:526-528`). | +| `HARNESS_CRON_API_KEY` | — | API key `harnessd`'s embedded cron dispatch uses when calling back into itself (`cmd/harnessd/main.go:550`). | +| `HARNESS_SOURCE_ROOT` | Auto-detected | Override the repository root the Go workflow engine resolves source files against (`internal/workflow/source.go:862`). | +| `HARNESS_BENCHMARK_CMD` | `./scripts/test-regression.sh` | Override the regression command run by the training/scoring loop (`internal/training/regression.go:145`). | + --- ## Memory, cron, and persistence @@ -272,6 +292,12 @@ The conclusion watcher detects when the agent jumps to a conclusion prematurely. | `CRONSD_ADDR` | `":9090"` | TCP listen address for `cronsd`. | | `CRONSD_DB_PATH` | `~/.go-harness/cronsd.db` | SQLite database file path. | | `CRONSD_MAX_CONCURRENT` | `5` | Maximum number of job executions that may run simultaneously. | +| `CRONSD_INGRESS_API_KEY` | — | API key required on inbound requests to `cronsd` itself. | +| `CRONSD_INGRESS_TENANT_ID` | — | Tenant ID associated with `CRONSD_INGRESS_API_KEY`. | +| `CRONSD_HARNESS_URL` | — | Base URL of the `harnessd` instance `cronsd` dispatches jobs to. | +| `CRONSD_HARNESS_API_KEY` | — | API key `cronsd` uses when calling `harnessd`. Must differ from `CRONSD_INGRESS_API_KEY`. | +| `CRONSD_HARNESS_CONNECT_TIMEOUT` | `5s` | Connect timeout for `cronsd` → `harnessd` requests. | +| `CRONSD_HARNESS_REQUEST_TIMEOUT` | `15s` | Overall request timeout for `cronsd` → `harnessd` requests. | `cronctl` (the CLI client for `cronsd`) reads one variable: diff --git a/website/docs/reference/events-catalog.md b/website/docs/reference/events-catalog.md index 1312e5530..b81656598 100644 --- a/website/docs/reference/events-catalog.md +++ b/website/docs/reference/events-catalog.md @@ -64,7 +64,7 @@ Three events signal the end of the stream. Clients MUST stop reading after recei | `EventRunFailed` | `run.failed` | Run ended with an error | | `EventRunCancelled` | `run.cancelled` | Run cancelled via `POST /v1/runs/{id}/cancel` | -`IsTerminalEvent(et EventType) bool` returns `true` for exactly these three types (source: `internal/harness/events.go:466–468`). +`IsTerminalEvent(et EventType) bool` returns `true` for exactly these three types (source: `internal/harness/events.go:477–479`). In headless mode (`harnesscli -prompt ...` or streaming `harnesscli continue`) the terminal event also determines the process exit code — see [Exit Codes](/docs/reference/exit-codes) for the mapping. @@ -84,7 +84,7 @@ Source: `internal/harness/events.go:18–41` | `run.queued` | Run accepted but the worker pool is at capacity (bounded pool mode only) | | `run.step.started` | Each step loop iteration begins | | `run.step.completed` | Step loop iteration finishes | -| `run.waiting_for_user` | `ask_user_question` tool invoked; run paused | +| `run.waiting_for_user` | `AskUserQuestion` tool invoked; run paused | | `run.resumed` | User answered; run continuing | | `run.cost_limit_reached` | Cumulative cost hit `max_cost_usd`; immediately followed by `run.step.completed` then `run.completed` (always ends with `run.completed`, never `run.failed`) | | `run.completed` | Run finished successfully **(terminal)** | @@ -172,7 +172,7 @@ When the run hit `max_steps`, the payload also includes `"reason": "max_steps_re ```json { "call_id": "string", - "tool": "ask_user_question", + "tool": "AskUserQuestion", "questions": ["string", "..."], "deadline_at": "2026-01-01T00:00:00Z" } @@ -301,6 +301,12 @@ Source: `internal/harness/events.go:55–81` | `tool.approval_granted` | Operator approved the pending tool call | | `tool.approval_denied` | Operator denied; run continues with a `permission_denied` result | | `tool.call.blocked` | A skill constraint blocked the tool call before execution | +| `todos.updated` | The run's todo list changed (the `todos` tool ran) | +| `plan.approval_required` | Plan mode requires operator approval before the run can act; run status moves to `waiting_for_approval` | +| `plan.approval_granted` | Operator approved the pending plan | +| `plan.approval_denied` | Operator denied the pending plan | + +`plan.approval_required` / `plan.approval_granted` / `plan.approval_denied` share the same approval mechanism as `tool.approval_*` (`internal/harness/plan_mode.go:75-107`), but gate the plan-mode read-only-to-mutating transition rather than a single tool call. `plan.approval_denied` payload: `{"plan": ""}`. `tool.activated` is defined in the event catalog (constant `EventToolActivated`, string `"tool.activated"`) but no production emission site was found in the codebase. It appears to be reserved for future use when a deferred tool is activated via `find_tool`. Do not rely on it being emitted today. @@ -525,6 +531,10 @@ Source: `internal/harness/events.go:142–146` | Event | When emitted | |---|---| | `callback.scheduled` | `set_delayed_callback` tool ran | +| `callback.dispatching` | The callback manager begins admitting the callback as a new run (`internal/harness/tools/delayed_callback.go:672`) | +| `callback.started` | The dispatched callback's run has started (`:878`) | +| `callback.retry_wait` | Admission failed with a retryable error; the callback is scheduled to try again (`:645,817,850`) | +| `callback.failed` | Admission failed with a non-retryable error, or retries were exhausted (`:798,864`) | | `callback.fired` | Timer fires | | `callback.canceled` | Callback was cancelled | @@ -781,6 +791,26 @@ All events in this section require a flag set on `RunnerConfig`. They are never ## Other system events +### Background job completion + +Source: `internal/harness/job_bridge.go:16` + +`job.completed` (constant `EventBackgroundJobCompleted`) — emitted when a `bash` tool call started with `run_in_background: true` finishes. Delivered to the **conversation** stream (`GET /v1/conversations/{id}/events`), not the originating run, because a background job routinely outlives the run that started it: + +```json +{ + "shell_id": "string", + "command": "string", + "exit_code": 0, + "timed_out": false, + "output": "string (truncated to ~2000 bytes)", + "truncated": false, + "working_dir": "string" +} +``` + +The same completion is also queued and replayed to the model as a notice on its next turn (`JobEventBridge.TakeNotices`), independent of whether any client is subscribed to the event stream. + ### Empty-response retry Source: `internal/harness/events.go:288–295` @@ -857,12 +887,12 @@ These events appear reserved for multi-agent and skill-fork features not yet plu |---|---|---| | Run lifecycle | 10 | No | | LLM turn | 6 | No | -| Tool execution | 8 | No (+ 1 unconfirmed) | +| Tool execution | 12 | No (+ 1 unconfirmed) | | Accounting / cost | 2 | `cost.anomaly` only | | Workspace | 3 | When `workspace_type` is set | | Memory | 4 | When memory is enabled | | Hooks (message + tool) | 6 | No | -| Callbacks | 3 | No | +| Callbacks | 7 | No | | Skill constraints | 3 | No | | Steering / conversation / prompt / provider | 5 | No | | Context management | 4 | No | @@ -874,11 +904,11 @@ These events appear reserved for multi-agent and skill-fork features not yet plu | Audit trail | 1 | `AuditTrailEnabled` | | Causal graph | 1 | `CausalGraphEnabled` | | Error chain | 1 | `ErrorChainEnabled` | -| Other system events | 4 | No | +| Other system events | 5 | No | | Reserved / unconfirmed | 7 | — | -| **Total** | **77** | | +| **Total** | **86** | | -> **Note:** The Count column sums to more than 77 because a few events (`tool.call.blocked`, `spawn_agent.started`, `spawn_agent.completed`, `task.completed`) are cross-listed in multiple categories. The true distinct total is 77, as returned by `AllEventTypes()`. +> **Note:** The Count column sums to more than 86 because a few events (`tool.call.blocked`, `spawn_agent.started`, `spawn_agent.completed`, `task.completed`) are cross-listed in multiple categories. The true distinct total is 86: 79 returned by `AllEventTypes()` (`internal/harness/events.go:392`) plus 7 declared event constants that exist but are not included in that function's return list — `EventCallbackDispatching`, `EventCallbackFailed`, `EventCallbackRetryWait`, `EventCallbackStarted`, `EventPlanApprovalRequired`, `EventPlanApprovalGranted`, and `EventPlanApprovalDenied` — all of which are genuinely emitted in production (see the Callback and Tool execution sections above). --- diff --git a/website/docs/reference/exit-codes.md b/website/docs/reference/exit-codes.md index 075173c63..4ef8b87da 100644 --- a/website/docs/reference/exit-codes.md +++ b/website/docs/reference/exit-codes.md @@ -26,13 +26,13 @@ Applies to: `harnesscli -prompt ...` (default streaming run mode) and `harnesscl | `1` | Client-side error | Bad flags, missing prompt, connection/HTTP failure, stream transport error. Also the defensive default for an unknown or empty terminal event type, so a scripting caller never mistakes an unrecognized outcome for success. | | `2` | Run failed | Terminal event `run.failed` — a turn failed server-side (satisfies kimi's "non-zero on turn failure"). | | `3` | Blocked | The run cannot proceed without input it will never get headlessly: `run.waiting_for_user`, `tool.approval_required`, or `plan.approval_required` observed while stdin is **not** a terminal. See [blocked runs](#blocked-runs-exit-3). | -| `6` | Paused / cancelled | Terminal event `run.cancelled`. Work is interrupted but resumable via `harnesscli continue `. | +| `6` | Paused / cancelled | Terminal event `run.cancelled`. Work is interrupted; a cancelled run cannot be resumed — `harnesscli continue` requires the source run's status to be `completed` and returns HTTP 409 `run_not_completed` otherwise (`internal/harness/runner.go:2217`, `internal/server/http_runs.go:817`). Start a new run to continue the work. | | `130` | Interrupted | SIGINT/SIGTERM while streaming (`128 + SIGINT`, the conventional shell code). The CLI best-effort cancels the still-executing server-side run before exiting. | ### kimi-code alignment rationale - `0` for a completed run/goal is identical in both CLIs. -- `3` (blocked) and `6` (paused) reuse kimi's exact codes for the same semantics: a headless caller can distinguish "needs a human" (`3`) from "stopped but resumable" (`6`) without reading any output. +- `3` (blocked) and `6` (paused) reuse kimi's exact codes for the same semantics: a headless caller can distinguish "needs a human" (`3`) from "cancelled" (`6`) without reading any output. Unlike kimi's pause semantics, a go-code run that exits `6` (`run.cancelled`) cannot itself be resumed — see the note in the contract table above. - `2` for `run.failed` satisfies kimi's "non-zero on turn failure" guarantee while staying distinct from `1` (the failure is server-side, not a client usage or transport problem), so `if [ $? -eq 1 ]` retry-the-invocation logic keeps its current meaning. - `1` and `130` are go-code's current behavior and are unchanged; `130` is the standard shell convention both CLIs follow. @@ -40,7 +40,7 @@ Applies to: `harnesscli -prompt ...` (default streaming run mode) and `harnesscl ## Run terminal events -The terminal event set is exactly three event types — `run.completed`, `run.failed`, `run.cancelled` — as defined by `IsTerminalEvent` (`internal/harness/events.go:472`). After a terminal event the SSE stream ends; the exit code is derived from which terminal event arrived: +The terminal event set is exactly three event types — `run.completed`, `run.failed`, `run.cancelled` — as defined by `IsTerminalEvent` (`internal/harness/events.go:477`). After a terminal event the SSE stream ends; the exit code is derived from which terminal event arrived: | Terminal event | Source constant | Exit code | |---|---|---| @@ -66,15 +66,15 @@ A run is **blocked** when it cannot make progress without input a headless calle | Signal | Source constant | Run status while blocked | Kind | |---|---|---|---| -| `run.waiting_for_user` | `EventRunWaitingForUser` (`internal/harness/events.go:22`) | `waiting_for_user` (`RunStatusWaitingForUser`, `internal/harness/types.go:337`) | Question-blocked: the run invoked `ask_user_question`. | -| `tool.approval_required` | `EventToolApprovalRequired` (`internal/harness/events.go:69`) | `waiting_for_approval` (`RunStatusWaitingForApproval`, `internal/harness/types.go:338`) | Approval-blocked: a tool call needs operator approval. | -| `plan.approval_required` | `EventPlanApprovalRequired` (`internal/harness/events.go:83`) | `waiting_for_approval` (`internal/harness/types.go:338`) | Approval-blocked: a plan needs operator approval. | +| `run.waiting_for_user` | `EventRunWaitingForUser` (`internal/harness/events.go:22`) | `waiting_for_user` (`RunStatusWaitingForUser`, `internal/harness/types.go:401`) | Question-blocked: the run invoked the `AskUserQuestion` tool (`internal/harness/tools/ask_user_question.go:12`). | +| `tool.approval_required` | `EventToolApprovalRequired` (`internal/harness/events.go:69`) | `waiting_for_approval` (`RunStatusWaitingForApproval`, `internal/harness/types.go:402`) | Approval-blocked: a tool call needs operator approval. | +| `plan.approval_required` | `EventPlanApprovalRequired` (`internal/harness/events.go:83`) | `waiting_for_approval` (`internal/harness/types.go:402`) | Approval-blocked: a plan needs operator approval. | There is no dedicated "waiting-for-approval" run event — the approval-required events are the signal, and the run status transitions to `waiting_for_approval`. Behavior when a blocked signal is observed in one-shot or streaming `continue` mode (implemented in epic #823 slice 3): -- **stdin is not a terminal** (piped/redirected, the CI case): the CLI prints the blocked reason and run ID to **stderr**, stops streaming, and exits `3`. The server-side run is left intact — no auto-cancel — so an operator can resume it later with `harnesscli continue ` (the resume command is named in the stderr message) or by answering via `POST /v1/runs/{id}/input` (questions) or `/approve` / `/deny` (approvals). +- **stdin is not a terminal** (piped/redirected, the CI case): the CLI prints the blocked reason and run ID to **stderr**, stops streaming, and exits `3`. The server-side run is left intact — no auto-cancel — so an operator can unblock it: a question-blocked run (`waiting_for_user`) is answered with `harnesscli input "="` (`POST /v1/runs/{id}/input`); an approval-blocked run (`waiting_for_approval`) is resolved with `POST /v1/runs/{id}/approve` or `/deny`. `harnesscli continue` does **not** apply here — it only works on runs whose status is already `completed` (see the `run.cancelled` row above), and a blocked run is neither completed nor cancelled. - **stdin is a terminal**: behavior is unchanged — the stream stays open and no exit-3 shortcut is taken. Interactive answer wiring in the streaming loop is a separate epic's scope; that epic must preserve exit `3` for non-interactive stdin. The terminal check uses the package's shared injectable `term.IsTerminal`-based stdin double (`stdinIsTerminal`, `cmd/harnesscli/plugins.go:107`), the same style of terminal detection the `--tui` path uses for stdout. @@ -86,7 +86,8 @@ The terminal check uses the package's shared injectable `term.IsTerminal`-based | Command | Covered by this contract? | Exit codes | |---|---|---| | `harnesscli -prompt ...` (streaming run mode) | **Yes** | `0`, `1`, `2`, `3`, `6`, `130` per the table above. | -| `harnesscli continue ` (streaming, the default) | **Yes** | Same mapping as the one-shot path. | +| `harnesscli continue ` (streaming, the default) | **Yes** | Same mapping as the one-shot path. Only applies to a run whose status is `completed`; otherwise the server returns HTTP 409 `run_not_completed` and the CLI exits `1`. | +| `harnesscli input "="` (answers a `waiting_for_user` run) | No (non-streaming) | `0` on success, `1` on error. | | `harnesscli continue -no-stream ...` | No (never observes events) | Prints `run_id=` and exits `0`; `1` on client error. | | `list`, `status` / `show`, `cancel`, `replay`, `search` | No (non-streaming) | Unchanged: `0` on success, `1` on error. This contract documents but does not change them. | | `--tui` | No | Interactive TUI exit behavior is out of scope; the contract covers headless/streaming mode only. | @@ -143,7 +144,7 @@ Every code in the contract traces to an existing event constant, run status, or | `0` | `EventRunCompleted` (`internal/harness/events.go:20`); current `run()` return at `cmd/harnesscli/main.go:220` | | `1` | Current usage/transport error returns (`cmd/harnesscli/main.go:164`, `:176`, `:183`, `:211`, `:236`; `cmd/harnesscli/runctl.go`) | | `2` | `EventRunFailed` (`internal/harness/events.go:21`) | -| `3` | `EventRunWaitingForUser` (`internal/harness/events.go:22`), `EventToolApprovalRequired` (`internal/harness/events.go:69`), `EventPlanApprovalRequired` (`internal/harness/events.go:83`); statuses `RunStatusWaitingForUser` / `RunStatusWaitingForApproval` (`internal/harness/types.go:337-338`) | +| `3` | `EventRunWaitingForUser` (`internal/harness/events.go:22`), `EventToolApprovalRequired` (`internal/harness/events.go:69`), `EventPlanApprovalRequired` (`internal/harness/events.go:83`); statuses `RunStatusWaitingForUser` / `RunStatusWaitingForApproval` (`internal/harness/types.go:401-402`) | | `6` | `EventRunCancelled` (`internal/harness/events.go:34`) | | `130` | Current `handleStreamError` interrupt path (`cmd/harnesscli/main.go:233`) | diff --git a/website/docs/reference/http-routes.md b/website/docs/reference/http-routes.md index 2c40f33d4..bfba16565 100644 --- a/website/docs/reference/http-routes.md +++ b/website/docs/reference/http-routes.md @@ -6,7 +6,7 @@ sidebar_position: 2 import { Callout, Tabs, TabsList, TabsTrigger, TabsContent, Card, CardHeader, CardTitle, CardContent } from '@site/src/components/ui'; -`harnessd` exposes a REST + Server-Sent Events (SSE) API over a single TCP port (default `:8080`). Every agent run, subagent, scheduled job, script workflow, and relay worker is reachable through this surface. This page covers the primary public route inventory — method, scope, request/response shape, and notes on when a route requires an optional server component. Note: the definition-based workflow routes (`/v1/workflows*`, `/v1/workflow-runs/*`) are registered but not yet fully documented here. +`harnessd` exposes a REST + Server-Sent Events (SSE) API over a single TCP port (default `127.0.0.1:8080`). Every agent run, subagent, scheduled job, script workflow, and relay worker is reachable through this surface. This page covers the primary public route inventory — method, scope, request/response shape, and notes on when a route requires an optional server component. Note: the definition-based workflow routes (`/v1/workflows*`, `/v1/workflow-runs/*`) are registered but not yet fully documented here. **Key terms used throughout this page:** @@ -46,6 +46,16 @@ All routes (except `/healthz` and the webhook routes) pass through `authMiddlewa --- +## Operational + +| Method | Path | Scope | Notes | +|--------|------|-------|-------| +| `GET` | `/v1/hooks` | `runs:read` | Startup-computed listing of loaded config-driven lifecycle hooks (name, event, kind, source, matcher) and skipped hook files with the skip reason. Read-only — trust is managed offline with `harnesscli hooks trust\|revoke\|list`. | +| `POST` | `/v1/config/reload` | `admin` | Reload daemon config from disk. 501 when config reload is not enabled on this server. | +| `GET` | `/viz`, `/viz/` | `runs:read` | Embedded read-only session visualizer shell (static assets). `/viz` redirects to `/viz/` only after auth and scope checks pass. | + +--- + ## Runs and Conversations Runs are the core unit of execution in `harnessd`. A run accepts a prompt, invokes an LLM agent with a set of tools, streams back events, and reaches a terminal state. @@ -69,16 +79,17 @@ Runs are the core unit of execution in `harnessd`. A run accepts a prompt, invok | `GET` | `/v1/runs/{id}/events` | `runs:read` | **SSE stream.** Supports `Last-Event-ID` reconnection. Terminal events close the stream. | | `GET` | `/v1/runs/{id}/summary` | `runs:read` | Post-run telemetry: steps, tokens, cost, tool calls, cache hit rate. | | `GET` | `/v1/runs/{id}/context` | `runs:read` | Context window status for an active run. | -| `GET` | `/v1/runs/{id}/input` | `runs:read` | Get a pending `ask_user_question` request. | +| `GET` | `/v1/runs/{id}/input` | `runs:read` | Get a pending `AskUserQuestion` request. | | `POST` | `/v1/runs/{id}/input` | `runs:write` | Submit answers. Body: `{"answers": {"q_id": "answer"}}`. Returns HTTP 202. | | `GET` | `/v1/runs/{id}/todos` | `runs:read` | Get the todo list for the run. | | `PUT` | `/v1/runs/{id}/todos` | `runs:write` | Replace the todo list. Body: `{"todos": [...]}`. | -| `POST` | `/v1/runs/{id}/continue` | `runs:write` | Start a new run in the same conversation. Body: `{"prompt":"…","allowed_tools":[],"permissions":{}}`. Returns HTTP 202. | +| `POST` | `/v1/runs/{id}/continue` | `runs:write` | Start a new run in the same conversation. Body: `{"prompt":"…","allowed_tools":[],"permissions":{}}`. Returns HTTP 202. **Requires the source run's status to be `completed`; returns HTTP 409 `run_not_completed` otherwise** (`internal/harness/runner.go:2217`, `internal/server/http_runs.go:817`). A cancelled run cannot be continued. | | `POST` | `/v1/runs/{id}/steer` | `runs:write` | Inject a steering message into an active run. Body: `{"prompt":"…"}`. Returns HTTP 202 `{"status":"accepted"}`. | | `POST` | `/v1/runs/{id}/compact` | `runs:write` | Trigger in-memory context compaction. Body: `{"mode":…,"keep_last":N}`. Returns `{"ok":true,"messages_removed":N}`. | | `POST` | `/v1/runs/{id}/cancel` | `runs:write` | Request cooperative cancellation. Returns `{"status":"cancelling"}`. | | `POST` | `/v1/runs/{id}/approve` | `runs:write` | Approve a pending tool call (requires `ApprovalBroker`). Returns `{"status":"approved"}`. | | `POST` | `/v1/runs/{id}/deny` | `runs:write` | Deny a pending tool call. Returns `{"status":"denied"}`. | +| `POST` | `/v1/runs/{id}/replay` | `runs:write` | Re-execute a completed durable run in its original conversation. Distinct from `POST /v1/runs/replay` below — this takes a durable run ID, not a filesystem path. | | `POST` | `/v1/runs/replay` | `runs:write` | Replay a recorded rollout. Body fields: `rollout_path` (required), `mode` (`"simulate"` or `"fork"`, required), `fork_step` (required for fork mode), `detect_drift` (bool, simulate only). | ### Conversation routes @@ -90,6 +101,10 @@ Runs are the core unit of execution in `harnessd`. A run accepts a prompt, invok | `GET` | `/v1/conversations/{id}/messages` | `runs:read` | In-memory messages for the conversation. | | `GET` | `/v1/conversations/{id}/runs` | `runs:read` | All runs for a conversation. | | `GET` | `/v1/conversations/{id}/export` | `runs:read` | JSONL (ndjson) export of all messages. | +| `GET` | `/v1/conversations/{id}/events` | `runs:read` | **SSE stream** of events from every run on the conversation, including a run that has not started yet — unlike `/v1/runs/{id}/events`, which is scoped to one run. | +| `GET` | `/v1/conversations/{id}/rewind-points` | `runs:read` | List file-snapshot rewind points recorded during the conversation. | +| `POST` | `/v1/conversations/{id}/rewind` | `runs:write` | **Destructive.** Restore a `point_id` (writes files, truncates later conversation history). Accepts `force` to override the default refusal when files were modified outside the snapshot. | +| `POST` | `/v1/conversations/{id}/undo` | `runs:write` | Drop recent prompts from the active context (issue #805). | | `POST` | `/v1/conversations/{id}/compact` | `runs:write` | Replace early messages with a summary. Body: `{"keep_from_step":N,"summary":"…","role":"system"}`. Auto-generates summary via LLM when `summary` is omitted. | | `POST` | `/v1/conversations/{id}/fork` | `runs:write` | Duplicate the conversation — full message history included — under a server-minted ID. No body. Returns `{"conversation_id":"…","forked_from":"…","message_count":N}`. The fork inherits the source's workspace and tenant (cross-tenant requests are rejected with 404); pinned flag and token/cost counters start at zero. Works for persisted conversations and ones held only in server memory (mid-run), capturing the latest in-memory view. 404 unknown source; 405 for non-POST; 501 when conversation persistence is not configured. Afterwards the two conversations diverge independently. | | `POST` | `/v1/conversations/cleanup` | `runs:write` | Bulk-delete old conversations. Body: `{"max_age_days":30}`. Returns `{"deleted":N}`. | @@ -121,7 +136,15 @@ data: {"id":"…","run_id":"…","type":"…","timestamp":"…","payload":{…}} | `GET` | `/v1/models` | `runs:read` | Returns `{"models":[{id,provider,aliases,input_cost_per_mtok,output_cost_per_mtok}]}`. | | `GET` | `/v1/providers` | `runs:read` | Returns `{"providers":[{name,configured,api_key_env,base_url,model_count}]}`. | | `PUT` | `/v1/providers/{name}/key` | `admin` | Set a provider API key at runtime. Body: `{"key":"…"}`. Returns HTTP 204. | +| `POST` | `/v1/providers/{name}/import-subscription` | `admin` | Import a vendor-CLI-authenticated session for `codex-subscription` or `kimi-subscription` from files already present on the harnessd host. Any other provider name 404s. Returns HTTP 204. | | `POST` | `/v1/summarize` | `runs:write` | LLM-generated summary of a message list. Body: `{"messages":[…],"system":"…"}`. Returns `{"summary":"…"}`. | +| `GET` | `/v1/tools` | `runs:read` | Enumerate the registered LLM tool catalog (core and deferred), with tier, tags, owner, and enabling condition per tool. | +| `GET` | `/v1/model-settings` | `runs:read` | Snapshot of per-provider model-settings state. Requires the model-settings store to be configured. | +| `POST` | `/v1/model-settings/providers` | `admin` | Add or update a provider entry in the model-settings store. | +| `DELETE` | `/v1/model-settings/providers/{name}` | `admin` | Remove one provider's model-settings entry. | +| `POST` | `/v1/model-settings/providers/{name}/fetch` | `admin` | Fetch and count that provider's live model list. | +| `POST` | `/v1/model-settings/providers/{name}/expose` | `admin` | Toggle whether the provider's models are exposed. Body: `{"exposed": bool}`. | +| `POST` | `/v1/model-settings/providers/{name}/cost` | `admin` | Set a per-model cost override. Body: `{"model":"…","input":N,"output":N}`. | --- @@ -170,6 +193,15 @@ All subagent routes return 501 when `ServerOptions.SubagentManager` is nil. | `PUT` | `/v1/profiles/{name}` | `runs:write` | Update a user-tier profile. Returns 403 for built-in names. | | `DELETE` | `/v1/profiles/{name}` | `runs:write` | Delete a user-tier profile. Returns 403 for built-ins. | +### Tasks, background jobs, and callbacks + +| Method | Path | Scope | Notes | +|--------|------|-------|-------| +| `GET` | `/v1/tasks` | `runs:read` | Unified view of subagent tasks (and other background work) across the server. | +| `POST` | `/v1/jobs/{id}/kill` | `runs:write` | Kill a background shell job started with `run_in_background: true`. Tenant-scoped when auth is enabled. | +| `GET` | `/v1/jobs/{id}/output` | `runs:read` | Fetch a background job's captured output snapshot (the same payload as the `job_output` tool). | +| `POST` | `/v1/callbacks/{id}/cancel` | `runs:write` | Cancel a pending delayed callback. 501 when no callback manager is configured. | + --- ## Cron, Checkpoints, Recipes, MCP, Networks, Script Workflows, Relay @@ -187,6 +219,8 @@ All subagent routes return 501 when `ServerOptions.SubagentManager` is nil. | `DELETE` | `/v1/cron/jobs/{id}` | `runs:write` | Soft-delete job. Returns HTTP 204. | | `POST` | `/v1/cron/jobs/{id}/pause` | `runs:write` | Pause job. | | `POST` | `/v1/cron/jobs/{id}/resume` | `runs:write` | Resume paused job. | +| `GET` | `/v1/cron/jobs/{id}/executions` | `runs:read` | List a job's execution history. Query: `limit`, `offset`. Job ID only — names are not accepted. | +| `POST` | `/v1/cron/runs` | `runs:write` | Ingress endpoint the embedded/external cron scheduler calls to start a run for a fired job. | ### Checkpoints @@ -262,6 +296,17 @@ All relay routes return 501 when `HARNESS_RELAY_DB` is not set. | `DELETE` | `/v1/relay/workers/{id}` | `runs:write` | Deregister worker. | | `POST` | `/v1/relay/workers/{id}/heartbeat` | `runs:write` | Submit heartbeat. Body: `{"load":N,"status":"online"}`. `status` must be `"online"` or `"draining"`. Workers not heartbeating within 30 seconds transition to `"stale"`. | +The following relay control-plane routes are registered alongside worker CRUD but cover placement, contract composition, capability policy, and operator visibility. All 501 when the relay control plane is not wired. + +| Method | Path | Scope | Notes | +|--------|------|-------|-------| +| `POST` | `/v1/relay/placements` | `runs:write` | Create a work placement across registered workers. | +| `POST` | `/v1/relay/contracts` | `runs:write` | Compose a relay contract. | +| `POST` | `/v1/relay/policy/check` | `runs:read` | Check a single action against capability policy. | +| `POST` | `/v1/relay/policy/filter` | `runs:read` | Filter a set of actions/workers by capability policy. | +| `GET` | `/v1/relay/operator/workers` | `runs:read` | Operator-facing worker inventory view. | +| `GET` | `/v1/relay/capabilities/{worker}` | `runs:read` | Get one worker's capability inventory. Requires the relay worker store configured (separately from the control plane). | + ### Webhooks Webhook routes bypass Bearer auth entirely — they authenticate via HMAC signature headers. Enable each webhook by setting the corresponding secret env var. @@ -287,12 +332,21 @@ Webhook routes bypass Bearer auth entirely — they authenticate via HMAC signat Source: `internal/harness/types.go`. + +`workspace_path` (honored) and the "no default step cap" behavior for `max_steps: 0` land in issues #1372 and #1376 respectively; this page documents the post-merge contract. + + ```json { "prompt": "write a hello world in Go", + "attachments": [], + "plan_mode": false, + "plan_file": "", "model": "gpt-4o", "provider_name": "openai", "workspace_type": "", + "workspace_path": "", + "extra_dirs": [], "allow_fallback": false, "fallback_providers": [], "system_prompt": "", @@ -313,6 +367,7 @@ Source: `internal/harness/types.go`. "max_cost_usd": 0.0, "reasoning_effort": "", "allowed_tools": [], + "denied_tools": [], "mcp_servers": [ {"name": "sqlite", "command": "uvx", "args": ["mcp-server-sqlite", "--db-path", "/tmp/my.db"]} ], @@ -326,18 +381,25 @@ Source: `internal/harness/types.go`. "role_models": { "primary": "", "summarizer": "" - } + }, + "rules": [] } ``` Selected field notes: - `prompt` is required for a direct run. Omit when using `profile` + `skill` via `POST /v1/agents`. +- `attachments` carries typed non-text content (currently images) submitted with the prompt; the run is rejected if the effective model lacks the matching modality. +- `plan_mode` starts the run in enforced read-only planning; mutation is limited to `plan_file` (default `.harness/plan.md`) until the operator approves via `POST /v1/runs/{id}/approve`. - `workspace_type` accepts: `""` (server default), `"local"`, `"worktree"`, `"container"`, `"vm"`. -- `max_steps` and `max_turns`: `0` means runner default/unlimited; negative values are rejected. +- `workspace_path` roots the run's tools in an existing directory when it is an absolute path to a directory that exists; it does not provision a workspace (`workspace_type` does that). Sent by `harnesscli -workspace`. +- `extra_dirs` grants the run read/work access to additional directory roots beyond the workspace root (each must be an absolute path to an existing directory). +- `max_steps` and `max_turns`: `0` means unlimited — there is no default step cap; negative values are rejected. - `max_cost_usd`: `0` means unlimited; the run emits `run.cost_limit_reached` on breach (run still completes normally). +- `denied_tools` lists tool names that must never be offered to or callable from this run, even if `allowed_tools` or an activated skill would otherwise grant them. - `permissions.sandbox`: `"unrestricted"` (default), `"local"`, or `"workspace"`. - `permissions.approval`: `"none"` (default), `"destructive"`, or `"all"`. +- `rules` applies fine-grained allow/ask/deny effects to tool calls; evaluated together with `permissions.rules`, with `rules` appended after. - `initiator_api_key_prefix` is server-populated from the auth context — it is never accepted from the request body. ### `Run` — `GET /v1/runs/{id}` response @@ -417,7 +479,7 @@ Streaming paths (`/events`, `/stream`, `/wait` suffix) bypass the 30-second hand | Env var | Default | Effect | |---------|---------|--------| -| `HARNESS_ADDR` | `:8080` | HTTP listen address | +| `HARNESS_ADDR` | `127.0.0.1:8080` | HTTP listen address | | `HARNESS_AUTH_DISABLED` | `""` (false) | Set `"true"` to bypass all Bearer auth | | `HARNESS_RUN_DB` | `""` | SQLite path; enables `GET /v1/runs` and auth | | `HARNESS_RELAY_DB` | `""` | SQLite path; enables `/v1/relay/workers` routes | diff --git a/website/docs/reference/providers-and-models-reference.md b/website/docs/reference/providers-and-models-reference.md index 04110ec5a..06fe1e790 100644 --- a/website/docs/reference/providers-and-models-reference.md +++ b/website/docs/reference/providers-and-models-reference.md @@ -26,12 +26,17 @@ go-code ships with ten providers pre-wired in the catalog. Each entry specifies | `anthropic` | Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | `https://api.anthropic.com/v1` | | `deepseek` | DeepSeek | `openai_compat` | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` | | `groq` | Groq | `openai_compat` | `GROQ_API_KEY` | `https://api.groq.com/openai/v1` | +| `cerebras` | Cerebras | `openai_compat` | `CEREBRAS_API_KEY` | `https://api.cerebras.ai/v1` | | `xai` | xAI (Grok) | `openai_compat` | `XAI_API_KEY` | `https://api.x.ai/v1` | | `kimi` | Kimi (Moonshot) | `openai_compat` | `MOONSHOT_API_KEY` | `https://api.moonshot.ai/v1` | +| `kimi-subscription` | Kimi Code Subscription | `openai_compat` | (vendor-CLI session import) | `https://api.kimi.com/coding/v1` | | `qwen` | Qwen (DashScope) | `openai_compat` | `DASHSCOPE_API_KEY` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | | `together` | Together AI | `openai_compat` | `TOGETHER_API_KEY` | `https://api.together.xyz/v1` | +| `codex-subscription` | Codex (ChatGPT subscription) | `openai_compat` | (vendor-CLI session import) | `https://chatgpt.com/backend-api/codex` | | `openrouter` | OpenRouter | `openai_compat` | `OPENROUTER_API_KEY` | `https://openrouter.ai/api/v1` | | `gemini` | Google Gemini | `openai_compat` | `GOOGLE_API_KEY` | `https://generativelanguage.googleapis.com/v1beta/openai` | +| `ollama` | Ollama | `openai_compat` | (none — local server) | `http://localhost:11434/v1` | +| `lmstudio` | LM Studio | `openai_compat` | (none — local server) | `http://localhost:1234/v1` | **Protocol values** — `anthropic` means the server uses the native Anthropic messages client (`internal/provider/anthropic`). `openai_compat` means the OpenAI-compatible client (`internal/provider/openai`) is used, even when the upstream API is not from OpenAI. @@ -284,9 +289,9 @@ Aliases are short names that resolve to full model IDs. They are defined per-pro Kimi and Gemini define no aliases in the current catalog. -### OpenRouter dynamic discovery and the `/` heuristic +### Live model discovery and the `/` heuristic -OpenRouter hosts thousands of models not listed in the static catalog. When `OPENROUTER_API_KEY` is set, go-code fetches `https://openrouter.ai/api/v1/models` with a 5-minute TTL cache. Live results are merged additively into the static catalog — static metadata wins on conflicts. +Live discovery is not OpenRouter-only: OpenRouter, OpenAI, Anthropic, and DeepSeek entries all refresh from the provider's own models endpoint on a 5-minute TTL (`internal/provider/openai/discovery.go:24`, `internal/provider/anthropic/discovery.go:24`). When the matching API key is set, go-code fetches the live list (for OpenRouter: `https://openrouter.ai/api/v1/models`) and merges it additively into the static catalog — static metadata wins on ID conflicts, and a failed refresh serves the last successful result rather than removing static models. **The `/` heuristic:** any model ID containing a `/` character is automatically routed to the `openrouter` provider whenever the `openrouter` provider entry is present in the loaded catalog — key configuration is not required for routing. A missing key surfaces as an error at client creation. This means you can write `"model": "openai/gpt-4.1"` directly in a `RunRequest` without setting `provider_name`, and the harness will route it to OpenRouter. diff --git a/website/docs/reference/tools-catalog.md b/website/docs/reference/tools-catalog.md index 3b779feb4..29a1c8db5 100644 --- a/website/docs/reference/tools-catalog.md +++ b/website/docs/reference/tools-catalog.md @@ -166,6 +166,8 @@ Registered when `EnableCron && CronClient != nil`. | `cron_create` | Create a recurring job. Required: `name`, `schedule` (5-field UTC cron), and explicit `execution_type` (`shell` or `harness`). `shell` requires a non-empty `command` for headless execution and records command output in history; `harness` requires a non-empty `prompt`, rejects `command`, and starts an assistant continuation in the creating conversation. `timeout_seconds` defaults to 30. | | `cron_list` | List all cron jobs. | | `cron_get` | Get a job and its 5 most recent executions by ID. | +| `cron_update` | Update an existing job's schedule, command, prompt, execution config, timeout, or tags. Requires at least one field to change; use `cron_pause`/`cron_resume` to change status instead. | +| `cron_history` | List a job's execution history by job ID (names are not accepted). Params: `id` (required), `limit`, `offset`. | | `cron_delete` | Delete a cron job (soft-delete). | | `cron_pause` | Pause a job (sets `status=paused`). | | `cron_resume` | Resume a paused job (sets `status=active`). | @@ -187,6 +189,7 @@ Registered when `EnableAgent && AgentRunner != nil`. | `agent` | Inline sub-agent call via `AgentRunner.RunPrompt`. | | `spawn_agent` | Spawn a recursive child agent. Max fork depth: 5 (`DefaultMaxForkDepth`). | | `task_complete` | Used by child agents to return a result to their parent. Not available at depth 0. | +| `agent_swarm` | Fan out a prompt to multiple sub-agents in parallel and collect their results (`internal/harness/tools/swarm.go:9`). | Registered when `SubagentManager != nil`. @@ -197,6 +200,8 @@ Registered when `SubagentManager != nil`. | `get_subagent` | Poll subagent status by ID. | | `wait_subagent` | Block until a subagent completes. | | `cancel_subagent` | Cancel a running subagent. | +| `message_subagent` | Send a follow-up message to a running subagent by resolving its subagent ID to a run ID, then steering that run. Params: `id`, `message`. | +| `notify_parent` | Send a message back to the parent agent that spawned this run. Fails if this run has no recorded parent (was not spawned as a subagent). Params: `message`. | ### MCP integration @@ -242,6 +247,28 @@ Most profile tools are always registered; `create_profile`, `update_profile`, an | `run_recipe` | `RecipesDir != ""` | Execute a multi-step recipe from a YAML file. | | `create_prompt_extension` | always registered | Create a behavior or talent prompt extension. | +### Model catalog + +| Tool | What it does | +|------|--------------| +| `list_models` | List, filter, and inspect available LLM models from the provider catalog. Params: `action` (`list`/`info`/`providers`), `provider`, `model_id`, `tool_calling`, `streaming`, `speed_tier`, `cost_tier`, `modality`, `best_for`, `strength`, `min_context`, `reasoning`. | + +### Deployment + +Always registered; the built-in adapter registry supports `railway` and `flyio`. + +| Tool | What it does | +|------|--------------| +| `deploy` | Deploy to, check status of, view logs for, or auto-detect a cloud platform (`railway`, `flyio`). Params: `platform` (auto-detected if omitted), `action` (`deploy`/`status`/`logs`/`detect`), `workspace`. | + +### Goals + +Registered when a goals manager is configured (`internal/goals`). + +| Tool | What it does | +|------|--------------| +| `goals` | Manage persistent, multi-session goals with dependency chains and progress tracking. Actions: `create` (with optional `depends_on`), `get`, `update` (status/progress/metadata), `list` (with status filter), `ready` (goals whose dependencies are all completed). Goals survive restarts and support verification criteria for definition-of-done enforcement. | + --- ## Activation and naming @@ -303,7 +330,7 @@ Tool groups are gated by flags and runtime dependencies. A group is silently abs | Tool group | Count | Enabling condition | Source | |------------|-------|--------------------|--------| -| Cron tools | 6 | `EnableCron && CronClient != nil` | `catalog.go:86`, `tools_default.go:273` | +| Cron tools | 8 | `EnableCron && CronClient != nil` | `catalog.go:86`, `tools_default.go:273` | | Callback tools | 3 | `EnableCallbacks && CallbackManager != nil` | `catalog.go:97`, `tools_default.go:283` | | LSP tools | 3 | `EnableLSP` (not in default registry) | `catalog.go:57` | | Sourcegraph | 1 | `Sourcegraph.Endpoint != ""` | `catalog.go:60` | @@ -312,8 +339,12 @@ Tool groups are gated by flags and runtime dependencies. A group is silently abs | Web ops | 3 | `EnableAgent && EnableWebOps && WebFetcher != nil` | `catalog.go:82` | | Recipes | 1 | `RecipesDir != ""` | `tools_default.go:298` | | Agent tools (`agent`, `spawn_agent`, `task_complete`) | 3 | `EnableAgent && AgentRunner != nil` | `tools_default.go:256` | -| Subagent tools (`run_agent`, `start/get/wait/cancel_subagent`) | 5 | `SubagentManager != nil` | `tools_default.go:344` | +| `agent_swarm` | 1 | `AgentSwarmRunner != nil` | `tools_default.go:549` | +| Subagent tools (`run_agent`, `start/get/wait/cancel_subagent`, `message_subagent`, `notify_parent`) | 7 | `SubagentManager != nil` (plus `RunSteerer != nil` for `message_subagent`/`notify_parent`) | `tools_default.go:535-558` | | Workflow tools | 2 | `WorkflowService != nil` | `tools_default.go:331` | +| `list_models` | 1 | `ModelCatalog != nil` | `tools_default.go:461-463` | +| `deploy` | 1 | always registered | `tools_default.go:601` | +| `goals` | 1 | `GoalManager != nil` | `tools_default.go:604-609` | ### Recipes vs workflows vs skills — what's the difference? diff --git a/website/docs/reference/troubleshooting.md b/website/docs/reference/troubleshooting.md index 0a24db246..7ae3227b9 100644 --- a/website/docs/reference/troubleshooting.md +++ b/website/docs/reference/troubleshooting.md @@ -181,7 +181,7 @@ The Slack webhook integration only supports steering existing runs — all Slack You can verify the full list of available MCP tools by calling `GET /v1/mcp/servers`, which returns each connected server's tool list. -The runbook `docs/runbooks/mcp.md` documents the double-underscore format. This is incorrect. The source code at `internal/harness/tools/mcp.go` and `internal/harness/tools/deferred/mcp.go` both use `mcp_{server}_{tool}` (single underscore, `mcp_` prefix). +The runbook `docs/runbooks/mcp.md` documents the double-underscore format. This is incorrect. The source code at `internal/harness/tools/deferred/mcp.go:109` uses `mcp_{server}_{tool}` (single underscore, `mcp_` prefix). (There is no `internal/harness/tools/mcp.go` file — shared MCP registry types live in `internal/harness/tools/types.go`.) --- diff --git a/website/docs/server/expose-as-mcp-server.md b/website/docs/server/expose-as-mcp-server.md index 046007d84..db938efb2 100644 --- a/website/docs/server/expose-as-mcp-server.md +++ b/website/docs/server/expose-as-mcp-server.md @@ -17,7 +17,7 @@ This page covers the second direction — `harnessd` as an MCP server. There are | **`harness-mcp` proxy** | stdio → HTTP proxy | Connecting Claude Desktop to an _already-running_ `harnessd` instance | -These are three separate surfaces with different tool sets and use cases. Connecting Claude Desktop to `harnessd --mcp` (stdio mode) exposes the full harness tool catalog. Connecting via `harness-mcp` exposes five task-management tools that drive the harnessd REST API. Choose based on whether you need the full catalog or a curated run-management interface. +The HTTP MCP server (`/mcp`) and the `harness-mcp` proxy binary share the **same 25-tool, REST-backed dispatcher** (`internal/harnessmcp`) — the only difference is transport (HTTP POST vs. stdio) and which `harnessd` instance they call (`/mcp` calls back into its own daemon; `harness-mcp` calls whatever `HARNESS_ADDR` names). `harnessd --mcp` (stdio mode) is the one surface with a different tool set: it exposes the full in-process harness tool catalog (core + deferred tools an agent run would use), not the run-management API. --- @@ -31,49 +31,64 @@ When `harnessd` starts in normal HTTP mode, it mounts an MCP server on the same | Method | Path | Purpose | |--------|------|---------| | `POST /mcp` | JSON-RPC 2.0 | Tool calls, `initialize`, `tools/list` | -| `GET /mcp` | SSE stream | JSON-RPC 2.0 notifications for subscribed runs | -The MCP server advertises protocol version `"2025-11-25"` and identifies itself as `name = "go-agent-harness"`, `version = "1.0"`. + +`GET /mcp` is **not** an SSE stream — the handler only accepts `POST` and returns `405 Method Not Allowed` for anything else (`internal/harnessmcp/httptransport.go:29-31`). There is no `subscribe_run` tool or push-notification mechanism on this surface; poll `tail_run_events` instead (see the tool table below). + + +The MCP server advertises protocol version `"2025-11-25"` and identifies itself as `name = "harness-mcp"`, `version = "1.0.0"` (`internal/harnessmcp/dispatcher.go:98-99`) — the same identity the `harness-mcp` stdio proxy advertises, because both share the same dispatcher. + + +`/mcp` is mounted via `harnessmcp.NewHTTPHandler` (`cmd/harnessd/runtime_container.go:348-352`), which calls back into the daemon's own REST API over HTTP using a self-referential base URL. It is **not** built on `internal/mcpserver.NewServer` — that constructor has no production caller today; `harnessd` only uses `mcpserver.NewStdioServer` for the `--mcp` stdio surface described below. (`internal/mcpserver` is a separate, unmounted package that advertises `name = "go-agent-harness"`, `version = "0.1.0"` — do not confuse the two if you read its source.) + -### The 10 tools +### The 25 tools + +Both `/mcp` and the `harness-mcp` proxy (below) expose the same 25 REST-backed tools (`internal/harnessmcp/tools.go`): -Tools exposed by the HTTP MCP server +Tools exposed by the HTTP MCP server and the harness-mcp proxy -| Tool | Required arguments | Description | -|------|--------------------|-------------| -| `start_run` | `prompt` | Submit a new agent run; returns `run_id` | -| `get_run_status` | `run_id` | Current status and output | -| `list_runs` | — | List all known runs | -| `steer_run` | `run_id`, `message` | Inject a guidance message into an active run | -| `submit_user_input` | `run_id`, `input` | Respond when a run is paused at `waiting_for_user` | -| `subscribe_run` | `run_id` | Register for SSE notifications; returns `stream_id` | -| `list_conversations` | — | Paginated conversation list (default limit 20) | -| `get_conversation` | `conversation_id` | Full message history for a conversation | +| Tool | Key arguments | Description | +|------|---------------|-------------| +| `start_run` | `prompt`, `model`, `conversation_id`, `max_steps`, `max_cost_usd`, `workspace_type`, `extra_dirs`, `allowed_tools`, `denied_tools`, `profile`, `system_prompt`, `provider_name`, `reasoning_effort`, `max_turns`, `plan_mode`, `plan_file`, `agent_intent`, `task_context` | Start a new agent run; returns `run_id` | +| `get_run_status` | `run_id` | Status, messages, cost, and any error | +| `wait_for_run` | `run_id`, `timeout_seconds` (default 300) | Polls until the run reaches a terminal state | +| `continue_run` | `run_id`, `prompt` | Continue an existing conversation with a follow-up prompt | +| `cancel_run` | `run_id` | Cancel an in-flight run | +| `approve_run` | `run_id` | Approve a run paused awaiting tool/plan approval | +| `deny_run` | `run_id` | Deny a run paused awaiting approval | +| `steer_run` | `run_id`, `prompt` | Inject guidance into an in-flight run | +| `tail_run_events` | `run_id`, `after_event_id`, `max_events` (default 100), `wait_seconds` (default 2) | Poll a run's event stream for progress | +| `get_run_input` | `run_id` | Read the pending question when status is `waiting_for_user` | +| `submit_user_input` | `run_id`, `answers` | Answer a run waiting on a question | +| `get_run_todos` | `run_id` | Read a run's todo list | +| `get_run_summary` | `run_id` | Read a run's summary | +| `get_run_context` | `run_id` | Read a run's context-window usage | +| `compact_run` | `run_id` | Compact a run's context | +| `list_runs` | `conversation_id`, `limit` (default 20) | List recent runs | +| `list_profiles` | — | List profiles usable as `start_run`'s `profile` argument | +| `list_tools` | — | List tool names for `allowed_tools` / `denied_tools` | +| `list_conversations` | — | List recent conversations | +| `get_conversation` | `conversation_id` | Full message history | | `search_conversations` | `query` | Full-text search across conversations | -| `compact_conversation` | `conversation_id` | Trigger context compaction on a conversation | +| `compact_conversation` | `conversation_id` | Compact a conversation's history | +| `list_skills` | — | List skills available to a delegated run | +| `list_models` | — | List models this daemon can route to | +| `list_providers` | — | List providers with configuration/health status | - -The harnessd HTTP MCP server is always constructed via `mcpserver.NewServer` (see `runtime_container.go:188`), which never sets a `ConversationInterface`. `NewServerWithConversations` is not wired into any production code path. As a result, `list_conversations`, `search_conversations`, and `compact_conversation` **always** return `"conversations not available"` through the `/mcp` endpoint — there is no deployment configuration that enables them. The exception is `get_conversation`, which is backed by `runner.ConversationMessages` (via `mcpRunnerAdapter`) and does work. +Every tool is a thin proxy to the corresponding `harnessd` REST route (e.g. `start_run` → `POST /v1/runs`, `get_conversation` → `GET /v1/conversations/{id}`) — there is no separate conversation backend, and no tool is hardcoded to return an unavailable-feature error. -None of the harnessd MCP server surfaces expose MCP resources (`resources/list` / `resources/read`). The `clientManagerRegistry` used for outbound MCP client calls also returns an empty list for `ListResources` and an error for `ReadResource` (`mcp_setup.go:49-57`). + +Neither `/mcp` nor `harness-mcp` expose MCP resources (`resources/list` / `resources/read`) — the dispatcher has no handler for them. This is a separate matter from harnessd's own **outbound** MCP client support: `list_mcp_resources` / `read_mcp_resource` (the in-run tools an agent calls to read resources from an *external* connected MCP server) are implemented (`cmd/harnessd/mcp_setup.go:68-90`, `internal/mcp/mcp.go:231`). -### SSE notifications - -When a client calls `subscribe_run`, the SSE stream from `GET /mcp` delivers JSON-RPC 2.0 notifications as events arrive. Two notification methods are published: - -- `run/event` — emitted on non-terminal status changes; includes `run_id`, `event_type: "status_changed"`, and `status`. -- `run/completed` — emitted when a run reaches a terminal state (`"completed"` or `"failed"`); includes `run_id`, `status`, `cost_usd`, and `error`. Note: `cost_usd` is currently hardcoded to `0` in the poller (`poller.go:119`) and does not reflect the run's actual cost — fetch real cost via `get_run_status` or the REST run object instead. - -The SSE keepalive ping interval is controlled by `HARNESS_SSE_KEEPALIVE_SECONDS` (default: 15 seconds). - --- ## stdio MCP server (`--mcp`) @@ -125,31 +140,11 @@ harness-mcp (StdioTransport → Dispatcher → HarnessClient) harnessd (running at HARNESS_ADDR) ``` -The proxy advertises `name = "harness-mcp"`, `version = "1.0.0"` and protocol version `"2025-11-25"`. - -### The 5 tools +The proxy advertises `name = "harness-mcp"`, `version = "1.0.0"` and protocol version `"2025-11-25"` — identical to `/mcp` above, since both run the same `internal/harnessmcp` dispatcher. - - -Tools exposed by harness-mcp - - - -| Tool | Required arguments | Optional arguments | Description | -|------|--------------------|--------------------|-------------| -| `start_run` | `prompt` | `model`, `conversation_id`, `max_steps`, `max_cost_usd` | Start a new agent run | -| `get_run_status` | `run_id` | — | Returns status, messages, `cost_usd`, and error | -| `wait_for_run` | `run_id` | `timeout_seconds` (default 300) | Polls every 2 seconds until the run reaches `completed`, `failed`, or `waiting_for_user` | -| `continue_run` | `run_id`, `prompt` | — | Fetches the previous run's `conversation_id` and starts a new run in that conversation | -| `list_runs` | — | `conversation_id`, `limit` (default 20) | List runs, optionally filtered by conversation | - - - +### Tools -The proxy makes direct REST calls to `harnessd`: -- `POST /v1/runs` for `start_run` -- `GET /v1/runs/{runID}` for `get_run_status` and `wait_for_run` -- `GET /v1/runs?conversation_id=&limit=` for `list_runs` +`harness-mcp` exposes the same 25 tools as `/mcp` — see [The 25 tools](#the-25-tools) above. Each tool proxies to the matching `harnessd` REST route (`GET`/`POST /v1/runs...`, `/v1/conversations/...`, `/v1/profiles`, `/v1/tools`, `/v1/skills`, `/v1/models`, `/v1/providers`), the only difference from `/mcp` being that requests go to `HARNESS_ADDR` over the network instead of looping back into the same process. ### Build the proxy @@ -207,7 +202,7 @@ Quit and reopen Claude Desktop. The "harness" MCP server will appear in the tool -You can also use `harnessd --mcp` (stdio mode) directly as the Claude Desktop command without the proxy. The difference is that `--mcp` mode exposes the full harness tool catalog, while `harness-mcp` exposes only the five curated run-management tools. The proxy also lets you share one persistent `harnessd` daemon across multiple clients simultaneously. +You can also use `harnessd --mcp` (stdio mode) directly as the Claude Desktop command without the proxy. The difference is that `--mcp` mode exposes the full harness tool catalog (core + deferred tools), while `harness-mcp` exposes the 25 run-management tools described above. The proxy also lets you share one persistent `harnessd` daemon across multiple clients simultaneously. --- @@ -224,10 +219,10 @@ You can also use `harnessd --mcp` (stdio mode) directly as the Claude Desktop co **Use the HTTP MCP endpoint when:** - You are integrating from another service or agent over a network connection. -- You need live SSE notifications via `subscribe_run`. - You want a single `harnessd` process to serve many concurrent MCP clients. +- Polling `tail_run_events` is an acceptable substitute for push notifications — there is no SSE or subscription mechanism on this surface. -The endpoint lives at `POST /mcp` (tool calls) and `GET /mcp` (SSE) on the same port as the REST API (default 8080). No extra build step is required. +The endpoint lives at `POST /mcp` on the same port as the REST API (default `127.0.0.1:8080`). No extra build step is required. @@ -241,8 +236,8 @@ The endpoint lives at `POST /mcp` (tool calls) and `GET /mcp` (SSE) on the same **Use `harness-mcp` when:** -- You want Claude Desktop (or any stdio MCP host) to drive a persistent, separately-managed `harnessd` daemon. -- You want a minimal, curated interface (5 run-management tools) rather than the full catalog. +- You want Claude Desktop (or any stdio MCP host) to drive a persistent, separately-managed `harnessd` daemon over stdio rather than HTTP. +- You want the same 25 run-management tools as `/mcp`, without the client needing to speak HTTP. - Multiple clients or processes share one `harnessd` instance. diff --git a/website/docs/server/harnessd.md b/website/docs/server/harnessd.md index c6a0716f2..93841e220 100644 --- a/website/docs/server/harnessd.md +++ b/website/docs/server/harnessd.md @@ -6,7 +6,7 @@ sidebar_position: 1 import { Callout, Steps, Step, Tabs, TabsList, TabsTrigger, TabsContent, Card, CardHeader, CardTitle, CardContent } from '@site/src/components/ui'; -`harnessd` is the HTTP daemon at the center of go-code. It boots a complete agent runtime — LLM provider, tool registry, memory, cron scheduler, MCP client, skills, and workflow engines — and exposes everything over a REST + SSE (Server-Sent Events) API on a single TCP port (`:8080` by default). Clients like `harnesscli`, the BubbleTea TUI, or any HTTP client can submit agent runs, stream live events, steer running agents, and manage conversations without coupling directly to the Go runtime. +`harnessd` is the HTTP daemon at the center of go-code. It boots a complete agent runtime — LLM provider, tool registry, memory, cron scheduler, MCP client, skills, and workflow engines — and exposes everything over a REST + SSE (Server-Sent Events) API on a single TCP port (`127.0.0.1:8080` by default). Clients like `harnesscli`, the BubbleTea TUI, or any HTTP client can submit agent runs, stream live events, steer running agents, and manage conversations without coupling directly to the Go runtime. If you need a daemon that stays running while you iterate in other terminals, or you want to connect Claude Desktop and other MCP hosts to your local agent runtime, `harnessd` is where you start. @@ -36,7 +36,7 @@ OPENAI_API_KEY=sk-... HARNESS_WORKSPACE=$(pwd) ./harnessd When the daemon is ready it prints: ``` -harness server listening on :8080 +harness server listening on 127.0.0.1:8080 ``` You can then POST runs to `http://localhost:8080/v1/runs` and stream events from `http://localhost:8080/v1/runs/{id}/events`. @@ -152,13 +152,13 @@ The address `harnessd` binds to is resolved in five layers, lowest to highest pr | Priority | Source | Example | |----------|--------|---------| -| 1 — lowest | Built-in default | `:8080` | +| 1 — lowest | Built-in default | `127.0.0.1:8080` | | 2 | `~/.harness/config.toml` (`addr` field) | `:9000` | | 3 | `/.harness/config.toml` (`addr` field) | `:9000` | | 4 | Named profile (via `--profile`) | `:9090` | | 5 — highest | `HARNESS_ADDR` env var | `:8888` | -The resolved address is passed directly to `net/http.Server.Addr`. Use the socket form (`:8080`), not a full URL. +The resolved address is passed directly to `net/http.Server.Addr`. Use the socket form (`127.0.0.1:8080` or `:9000`), not a full URL. Binding beyond loopback is refused at startup unless authentication is configured or `HARNESS_AUTH_DISABLED=true` is set deliberately (`cmd/harnessd/bind_guard.go:29-42`, issue #1328). ### HTTP server timeout constants @@ -263,10 +263,10 @@ The full set of `HARNESS_*` variables is documented on the [Configuration](/docs | Variable | Default | Description | |----------|---------|-------------| -| `HARNESS_ADDR` | `:8080` | HTTP listen address in socket form. | +| `HARNESS_ADDR` | `127.0.0.1:8080` | HTTP listen address in socket form. | | `HARNESS_WORKSPACE` | `.` | Workspace root — anchors all relative paths. | | `HARNESS_MODEL` | `gpt-4.1-mini` | Default LLM model for runs. | -| `HARNESS_MAX_STEPS` | `8` | Max tool-calling steps per run. Set to `0` to remove the cap. | +| `HARNESS_MAX_STEPS` | `0` (unlimited) | Max tool-calling steps per run. There is no default step cap. | | `HARNESS_PROVIDER` | — | Set to `fake` for key-free mode. | | `HARNESS_FAKE_TURNS` | — | Path to the JSON turns file when `HARNESS_PROVIDER=fake`. Required when using fake mode. | | `HARNESS_AUTH_DISABLED` | — | Set to `true` to disable Bearer-token auth. | diff --git a/website/docs/server/http-api-guide.md b/website/docs/server/http-api-guide.md index e7ee3a8a5..bf89aaaf3 100644 --- a/website/docs/server/http-api-guide.md +++ b/website/docs/server/http-api-guide.md @@ -7,7 +7,7 @@ sidebar_position: 3 import { Callout, Steps, Step, Tabs, TabsList, TabsTrigger, TabsContent, Card, CardHeader, CardTitle, CardContent } from '@site/src/components/ui'; import RunRequestBuilder from '@site/src/components/RunRequestBuilder'; -`harnessd` is the HTTP daemon that backs every go-code agent run. It exposes a REST + Server-Sent Events (SSE) API on a single port (default `:8080`). Any process that can make HTTP requests — a shell script, a CI job, another service — can start agent runs, stream their output in real time, and control them mid-flight. +`harnessd` is the HTTP daemon that backs every go-code agent run. It exposes a REST + Server-Sent Events (SSE) API on a single port (default `127.0.0.1:8080`). Any process that can make HTTP requests — a shell script, a CI job, another service — can start agent runs, stream their output in real time, and control them mid-flight. This guide walks you through the two execution models, the full `RunRequest` body, the run-control endpoints, and how to handle errors and limits. The streamed-run examples use the built-in fake provider for key-free local testing, which requires a turns JSON file and `allow_fallback:true` in the request body — see the startup step for details. The `POST /v1/agents` synchronous endpoint requires a real configured provider (it does not support `allow_fallback` in the request body and cannot use the fake provider via the fallback path). @@ -62,7 +62,7 @@ HARNESS_AUTH_DISABLED=true \ go run ./cmd/harnessd ``` -The server prints `harness server listening on :8080` when ready. `HARNESS_PROVIDER=fake` selects the built-in scripted provider (no API key, no network calls), `HARNESS_FAKE_TURNS` tells it which turns file to load, and `HARNESS_AUTH_DISABLED=true` skips Bearer token validation. +The server prints `harness server listening on 127.0.0.1:8080` when ready. `HARNESS_PROVIDER=fake` selects the built-in scripted provider (no API key, no network calls), `HARNESS_FAKE_TURNS` tells it which turns file to load, and `HARNESS_AUTH_DISABLED=true` skips Bearer token validation. @@ -371,6 +371,10 @@ A `steering.received` event will appear on the event stream. #### POST `/v1/runs/{id}/continue` Start a **new run** in the same conversation. The conversation history from the referenced run is carried forward as context. + +**Only works when the referenced run's status is `completed`.** Any other status — `running`, `queued`, `failed`, `cancelled`, `waiting_for_user`, or `waiting_for_approval` — returns HTTP 409 `run_not_completed` (`internal/harness/runner.go:2217`, `internal/server/http_runs.go:817`). A cancelled run cannot be continued at all; a run blocked on a question or approval must be unblocked first via `POST /v1/runs/{id}/input` or `/approve`/`/deny`. + + ```bash curl -s -X POST http://localhost:8080/v1/runs/run_abc123/continue \ -H "Content-Type: application/json" \ @@ -433,7 +437,7 @@ Approval gates are enabled by setting `permissions.approval` to `"destructive"` #### GET and POST `/v1/runs/{id}/input` -When the agent calls the `ask_user_question` tool the run enters `waiting_for_user` status and emits a `run.waiting_for_user` event. Poll `GET /v1/runs/{id}/input` to retrieve the pending question, then `POST` your answers. +When the agent calls the `AskUserQuestion` tool the run enters `waiting_for_user` status and emits a `run.waiting_for_user` event. Poll `GET /v1/runs/{id}/input` to retrieve the pending question, then `POST` your answers (or use `harnesscli input "="`). ```bash # Get the pending question @@ -538,7 +542,7 @@ A run emits dozens of event types. Here are the most commonly consumed ones. For | `run.completed` | Terminal: run finished successfully | | `run.failed` | Terminal: run failed | | `run.cancelled` | Terminal: run was cancelled | -| `run.waiting_for_user` | Agent called `ask_user_question`; run paused | +| `run.waiting_for_user` | Agent called `AskUserQuestion`; run paused | | `run.cost_limit_reached` | `max_cost_usd` ceiling hit; run continues to completion | | `assistant.message.delta` | Streaming text token from the assistant | | `assistant.message` | Full assistant message (no tool calls in this turn) | diff --git a/website/docs/server/script-workflows-api.md b/website/docs/server/script-workflows-api.md index cb727e0b8..a746ff4a9 100644 --- a/website/docs/server/script-workflows-api.md +++ b/website/docs/server/script-workflows-api.md @@ -309,7 +309,7 @@ Common error codes for this API: | HTTP status | Code | When | |-------------|------|------| | 400 | `invalid_request` | `POST .../runs` — workflow name not registered; or `POST .../resume` — run is not in `"failed"` status | -| 404 | `not_found` | `GET /v1/script-workflows/{name}` — workflow name not registered; or `GET/POST /v1/script-workflow-runs/{id}` — run ID does not exist | +| 404 | `not_found` | `GET /v1/script-workflows/{name}` — workflow name not registered; or `GET /v1/script-workflow-runs/{id}` (or its `/events`, `/resume` sub-paths) — run ID does not exist | | 501 | `not_implemented` | `ScriptWorkflows` not configured in `ServerOptions` | --- diff --git a/website/docs/tutorials/claude-desktop-mcp.md b/website/docs/tutorials/claude-desktop-mcp.md index 983fed920..195401936 100644 --- a/website/docs/tutorials/claude-desktop-mcp.md +++ b/website/docs/tutorials/claude-desktop-mcp.md @@ -8,10 +8,10 @@ import { Callout, Steps, Step, Tabs, TabsList, TabsTrigger, TabsContent, Card, C The `harness-mcp` proxy is a small binary that lets Claude Desktop talk to a running `harnessd` instance. Once registered, Claude Desktop can start agent runs, check their status, wait for completion, continue conversations, and list recent runs — all without leaving the chat interface. -This tutorial walks you through the full setup: start `harnessd`, build the proxy, register it with Claude Desktop, and exercise all five tools. +This tutorial walks you through the full setup: start `harnessd`, build the proxy, register it with Claude Desktop, and exercise a few of the proxy's 25 tools. -Three separate MCP surfaces exist: the HTTP endpoint at `/mcp` (always available when harnessd runs in HTTP mode), the stdio server launched with `harnessd --mcp` (exposes the full harness tool catalog), and the `harness-mcp` proxy (five curated run-management tools over stdio). This tutorial uses the proxy path. See [Exposing harnessd as an MCP Server](/docs/server/expose-as-mcp-server) for a comparison of all three. +Three separate MCP surfaces exist: the HTTP endpoint at `/mcp` (always available when harnessd runs in HTTP mode) and the `harness-mcp` proxy (this tutorial) share the same 25 run-management tools; the stdio server launched with `harnessd --mcp` is the one surface with a different tool set — it exposes the full in-process harness tool catalog instead. This tutorial uses the proxy path. See [Exposing harnessd as an MCP Server](/docs/server/expose-as-mcp-server) for a comparison of all three. --- @@ -25,7 +25,7 @@ Claude Desktop harness-mcp ←─ bin you build in this tutorial │ (HTTP REST) ▼ -harnessd ←─ daemon already listening on :8080 +harnessd ←─ daemon already listening on 127.0.0.1:8080 ``` `harness-mcp` reads JSON-RPC 2.0 from stdin, translates each tool call into a REST request against `harnessd`, and writes the JSON-RPC response back to stdout. Claude Desktop launches the proxy as a subprocess; the proxy connects to `harnessd` at the URL given by the `HARNESS_ADDR` environment variable. @@ -58,7 +58,7 @@ go build -o bin/harnessd ./cmd/harnessd OPENAI_API_KEY=sk-... ./bin/harnessd ``` -`harnessd` logs `harness server listening on :8080` when it is ready. +`harnessd` logs `harness server listening on 127.0.0.1:8080` when it is ready. @@ -166,20 +166,20 @@ If it does not appear, check Claude Desktop's MCP log (usually accessible from t ## Step 3 — Use it -With `harnessd` running and the proxy registered, Claude Desktop has access to five tools. +With `harnessd` running and the proxy registered, Claude Desktop has access to all 25 `harness-mcp` tools (see [Exposing harnessd as an MCP Server](/docs/server/expose-as-mcp-server#the-25-tools) for the full list). The five most relevant to this tutorial: -The 5 harness-mcp tools +Core run-management tools | Tool | Required args | Optional args | What it does | |------|--------------|---------------|--------------| | `start_run` | `prompt` | `model`, `conversation_id`, `max_steps`, `max_cost_usd` | Submits a new agent run; returns `run_id` immediately | -| `get_run_status` | `run_id` | — | Returns current status and any error; `messages` and `cost_usd` are always empty in this version — use `wait_for_run` or the REST API to get output | -| `wait_for_run` | `run_id` | `timeout_seconds` (default 300) | Polls every 2 seconds until the run reaches a terminal state; returns status and any error; `messages` and `cost_usd` in the result are always empty in this version | -| `continue_run` | `run_id`, `prompt` | — | Looks up the previous run's `conversation_id` and starts a new run in that conversation | +| `get_run_status` | `run_id` | — | Returns current status, `output`, `cost_usd`, and any error. `messages` is always empty — `GET /v1/runs/{id}` does not return a message list; use `tail_run_events` or the conversation-export REST route for full transcripts. | +| `wait_for_run` | `run_id` | `timeout_seconds` (default 300) | Polls every 2 seconds until the run reaches a terminal state; same fields as `get_run_status` | +| `continue_run` | `run_id`, `prompt` | — | Looks up the previous run's `conversation_id` and starts a new run in that conversation. **Requires the referenced run's status to be `completed`** — returns an error otherwise. | | `list_runs` | — | `conversation_id`, `limit` (default 20) | Lists recent runs, optionally filtered by conversation. **Requires `HARNESS_RUN_DB` to be set** — returns an error if run persistence is not configured. | diff --git a/website/docs/tutorials/embed-in-hono.md b/website/docs/tutorials/embed-in-hono.md index c21ea795f..17cce0fc8 100644 --- a/website/docs/tutorials/embed-in-hono.md +++ b/website/docs/tutorials/embed-in-hono.md @@ -58,7 +58,7 @@ HARNESS_AUTH_DISABLED=true \ go run ./cmd/harnessd ``` -The server prints `harness server listening on :8080` when it is ready. The three environment variables are doing important work here: +The server prints `harness server listening on 127.0.0.1:8080` when it is ready. The three environment variables are doing important work here: - `HARNESS_PROVIDER=fake` — uses a built-in scripted provider instead of a real LLM. No network calls, no API key required. - `HARNESS_FAKE_TURNS=/tmp/turns.json` — path to the turns file above. The fake provider requires this; if it is unset the server exits at startup with a fatal error. diff --git a/website/docs/tutorials/first-go-workflow.md b/website/docs/tutorials/first-go-workflow.md index 6b17a3247..9c25f412b 100644 --- a/website/docs/tutorials/first-go-workflow.md +++ b/website/docs/tutorials/first-go-workflow.md @@ -74,7 +74,7 @@ HARNESS_AUTH_DISABLED=true \ harnessd ``` -The server listens on `:8080` by default. Leave it running in this terminal. +The server listens on `127.0.0.1:8080` by default. Leave it running in this terminal. diff --git a/website/docs/tutorials/sandboxes.md b/website/docs/tutorials/sandboxes.md index 77217be2e..dfb67de0e 100644 --- a/website/docs/tutorials/sandboxes.md +++ b/website/docs/tutorials/sandboxes.md @@ -47,7 +47,7 @@ HARNESS_FAKE_TURNS=/tmp/fake_turns.json \ go run ./cmd/harnessd ``` -The server listens on `:8080` by default (`HARNESS_ADDR`). +The server listens on `127.0.0.1:8080` by default (`HARNESS_ADDR`).