Skip to main content
Glama
README.md
# Agent Teams

English | [简体中文](README.zh-CN.md)

An MCP server that runs work as visible DSH worker sessions instead of hidden subagents.
Each dispatch pins an exact provider, model and reasoning effort, writes a durable job
journal under `stateDir`, and reports progress back to the chat that started it.

This is an experimental public repository extracted from a private deployment. An
end-to-end install on a clean machine has not been verified: external supervisor identity
records and an authenticated DSH owner remain prerequisites. The
release targets **Windows**, **Node.js 24 LTS**, the **DSH 0.2.1-rc.2 plugin interface**
and the loopback endpoint **`http://127.0.0.1:3080`**. Ownership attestation is a Windows
PowerShell script (`scripts/verify-owner.ps1`). Other platforms, Node lines, DSH plugin
versions and endpoints are outside this release.

## Requirements

- Windows. Attestation runs `scripts/verify-owner.ps1` through Windows PowerShell 5.1.
- Node.js 24 LTS; tested on 24.14.0. Codex thread state is read through `node:sqlite`,
  imported without a flag.
- A running DSH owner at `http://127.0.0.1:3080` that you can log in to. The server
  refuses any other URL and the hub plugin answers only that exact host, so the endpoint
  is part of this release's contract rather than a setting.
- The DSH worker plugin in `hub/` installed into your DSH profile (see below).

## Install

```powershell
git clone https://github.com/Phantomthedog/agent-teams-mcp.git
Set-Location agent-teams-mcp
npm ci
Copy-Item config.example.json config.json
npm test
```

`config.json` is read from the repository root by default; pass a different path as
`argv[2]` when starting the server. The angle-bracket entries in `config.example.json`
(`<provider>`, `<model>`, `<you>`) are placeholders that must be replaced. The remaining
values are example defaults: review them and adjust paths and settings for your deployment.
`package.json` declares `os: ["win32"]`, so `npm ci` refuses to install on another
platform. Tests use temp fixtures and need no `config.json`.

### 1. State and workspace roots

`stateDir` holds the job journal, the async store and the hub token. `allowedRoots`
lists the directories a worker's `cwd` and task files may resolve inside; anything
outside is refused before a worker is created.

### 2. Hub token

The MCP server asks DSH to create a worker over HTTP, authenticated with a bearer
token. Create it once, at the path the plugin reads:

```powershell
node -e "const fs=require('node:fs'),path=require('node:path'),c=require('node:crypto');const f=process.argv[1];fs.mkdirSync(path.dirname(f),{recursive:true});fs.writeFileSync(f,c.randomBytes(32).toString('hex'),{encoding:'utf8',flag:'wx'})" "C:/agent-teams/state/hub-token"
```

That writes 64 lowercase hex characters as UTF-8 without a BOM, creates the parent
directory, and `flag: 'wx'` fails instead of overwriting an existing token (you get an
`EEXIST` error and the file is left as it was). Shell redirection is not used because
Windows PowerShell 5.1 would write UTF-16LE. The plugin
reads `<stateDir>/hub-token` and requires exactly `/^[a-f0-9]{64}$/`; a UTF-16LE file,
which is what a PowerShell redirection would produce, is rejected. Set
`dsh.hubTokenFile` to the same path.

### 3. Owner login

`dsh.authFile` points at a JSON file holding the DSH owner's browser session cookie:

```json
{ "cookie": "<session cookie value>" }
```

The server sends that value as a `Cookie` header to the owner and refuses with
`auth-unavailable: DSH owner login is unavailable.` when the file is missing or holds
no non-empty `cookie` string. How you obtain the cookie is your deployment's business;
DSH has no standard file for it.

### 4. DSH worker plugin

`hub/` is a DSH plugin package: it exports one plugin that registers a single
`POST /_codex/dispatch/create` endpoint on the DSH web server. The DSH plugin manager
installs a local plugin straight from an **absolute directory path** — it reads
`hub/package.json`, requires the `dsh.bundle` field that is already there, and records
the path with pnpm — so no packaging or publishing step is needed:

1. Edit `hub/cordis.patch.yml`: `stateDir` and `allowedRoots` must match `config.json`.
2. Install that absolute path into the profile named by `dsh.profile`, then restart
   the profile.

A relative path is refused (`a local path must be absolute`).

### 5. Owner attestation (required)

Dispatch fails closed until your deployment can prove which process owns DSH. The
server runs `scripts/verify-owner.ps1` before it sends any credential or creates any
worker, and refuses with:

```
owner-unverified: The DSH owner verifier is not configured for restart recovery; no credentials sent.
```

Set these four `dsh` fields, all published by your DSH supervisor:

| Field | What it must point at |
| --- | --- |
| `receiptFile` | Lifecycle receipt written when the owner started: `port`, `pid`, `startedAt`, `executable`, `home`. |
| `identityFile` | Runtime identity of that same process: `pid`, `port`, `startedAt`, `home`, optional `platform`. |
| `expectedHome` | The DSH home directory the owner must be running from. |
| `expectedEntrypoint` | The script the owner process must be executing, compared against its real command line. |

The verifier requires that exactly one process owns `127.0.0.1:<port>` and that it
executes `nodeExecutable` (or the receipt's `executable`). The receipt counts as
current only when its `pid` equals the live process PID and its `startedAt` is within
two seconds of that process's start time; otherwise the runtime identity must describe
that same pid, port and home, and must have been published between process start and
ten minutes later. The owner's
real command line must name `<expectedEntrypoint>` as its script argument and must
carry `--profile <dsh.profile>`; a `--host`, `--port` or `--home` that disagrees with
the configuration is refused. That is the shape a DSH supervisor starts:

```
node <expectedEntrypoint> ... --profile <dsh.profile> --host 127.0.0.1 --port 3080 --home <expectedHome>
```

If your deployment publishes neither a matching receipt nor a runtime identity, there
is no supported way to dispatch.

### 6. Register with Codex

Add one stanza to `~/.codex/config.toml` and leave every other entry as it is:

```toml
[mcp_servers.agent-teams]
command = "C:/Program Files/nodejs/node.exe"
args = ["<checkout>/src/server.mjs", "<checkout>/config.json"]
tool_timeout_sec = 3700
```

Command and arguments alone are not a complete registration. Codex applies its own
MCP tool timeout, which is far shorter than `dispatch_wait`'s 600000 ms default budget,
so a long wait would be cut off by the client. `tool_timeout_sec = 3700` covers the
3600000 ms maximum wait plus process start-up. Point `command` at the Node executable
you run the server with and `args` at this checkout's absolute paths.

## Worked example

`scripts/call.mjs` runs one tool over stdio against `config.json`. It needs the
prerequisites above: a live owner, the hub plugin, a hub token and owner records.
`<your-host>` and `<parent-thread-uuid>` are the calling chat's own values; they are
never inferred from the server environment.

```powershell
# 1. Dispatch one worker. request_id is yours; reuse it verbatim on a retry.
Set-Content -Path args.json -Encoding utf8 -Value '{"request_id":"demo-0001","cwd":"C:/agent-teams/workspace","task":"List the files in this directory."}'
node scripts/call.mjs dispatch_dsh args.json
# -> {"job_id":"dispatch-<32 hex>","sessionId":"session-...","status":"queued",...}

# 2. Bind the chat that will receive events. Registration also arms session follow.
Set-Content -Path args.json -Encoding utf8 -Value '{"action":"register","job_id":"dispatch-<32 hex>","parent":{"host":"<your-host>","threadUuid":"<parent-thread-uuid>"}}'
node scripts/call.mjs dispatch_watch args.json

# 3. Wait for the first outcome. Pass the same parent so receipts are accounted once.
Set-Content -Path args.json -Encoding utf8 -Value '{"job_id":"dispatch-<32 hex>","parent":{"host":"<your-host>","threadUuid":"<parent-thread-uuid>"},"budget_ms":600000}'
node scripts/call.mjs dispatch_wait args.json
# -> {"job_id":"...","status":"completed","terminal":true,"actionable":true,"delivery":{"event_id":"..."},"wait":{...}}

# 4. Acknowledge receipt before reviewing the work. Receipt is not acceptance.
Set-Content -Path args.json -Encoding utf8 -Value '{"action":"ack","job_id":"dispatch-<32 hex>","parent":{"host":"<your-host>","threadUuid":"<parent-thread-uuid>"},"event_id":"<event_id>"}'
node scripts/call.mjs dispatch_watch args.json
```

`Set-Content -Encoding utf8` produces a BOM under Windows PowerShell 5.1 and none under
PowerShell 7; both are accepted, because the server and `call.mjs` strip a leading BOM
before parsing JSON. The hub token is the one file that must stay plain UTF-8.

`dispatch_status` is the manual check when a watch was never registered or an event
looks stale. It is not the routine progress loop.

## Lifecycle limits

- One watch binding per job, created by `register`; `ensure` recovers its observer,
  `detach` stops it. A killed watcher helper or a reboot still needs `ensure`.
- Reports stay in the tool channel. Nothing wakes an idle chat, so a parent turn that
  has ended will not see a later tool result.
- `dispatch_wait` holds one call open: 600000 ms by default, 3600000 ms at most. The
  client timeout must exceed the budget, which is why registration sets 3700 s.
- A turn that ends as `aborted` is terminal: the job status becomes `cancelled` and
  `dispatch_wait` returns it like any other end.
- Uncertain about a turn? `dispatch_status` reports the recorded attempt and whether
  its `turn/end` evidence exists. To keep working, continue the same session with
  `dispatch_followup` (default `queue` adds a next turn; `steer` works only while a turn
  is running) and a new `request_id`. To bind a turn that already exists, use
  `dispatch_adopt_turn` with that turn number and the `rpcId` of the user message that
  started it; it refuses running, unknown, earlier or unrelated turns. Do not dispatch a
  duplicate job to get past an uncertain state.

## Legacy queue delivery

The source supports enqueueing reports into a parent chat through the Codex
app-server. That path needs a logged-in Codex CLI and its private thread database,
is unverified in this release, and is off by default
(`notifications.queueEnabled: false`). Treat it as historical: this candidate
delivers through tool results only.

## Tools

| Tool | Purpose |
| --- | --- |
| `dispatch_models` | List live DSH routes and their supported efforts. |
| `dispatch_dsh` | Create one visible worker with an immutable task and exact route. |
| `dispatch_status` | Recovery check of one job's turn: requires the correlated `turn/end`. |
| `dispatch_cancel` | Request cancellation of a bridge-owned task. |
| `dispatch_adopt_turn` | Bind an interrupted job to a later turn in its own session. |
| `dispatch_followup` | Steer a running turn or queue the next one, with replay-safe IDs. |
| `dispatch_watch` | Register the initiating chat, recover the observer, read and ack events. |
| `handoff_prepare` | Write an immutable task file without spawning anything. |
| `dispatch_wait` | Wait for the first selected job to finish, fail or need input. |

Retry an uncertain dispatch with the same `request_id` and payload; DSH deduplicates
the replay. Never substitute a different model to get past a failure.

## Scripts

- `scripts/call.mjs` — call one tool over stdio without a GUI.
- `scripts/smoke-mcp.mjs` — assert the server starts and lists exactly the nine tools.
- `scripts/watch.mjs` — the detached watcher helper the server spawns.
- `scripts/watch-activate.mjs` — replace a watcher that is running stale code.
- `scripts/worker-report.mjs` — a worker's own milestone/blocked report.
- `scripts/recover-writer-lock.mjs` — clear a job writer lock left by a killed process.
- `scripts/verify-owner.ps1` — the owner attestation script.

## Tests

`npm test` runs the suite with `node --test`. Tests use temp fixtures and fake
thread IDs, and need no `config.json`. The owner-attestation tests run the real
verifier against temp receipts and skip the live-owner cases when this host has no
owner records. `scripts/smoke-mcp.mjs` and `scripts/call.mjs` do start the server,
so they need `config.json` to exist.

## License

This project's code is released under **GPL-3.0-only**. The full, authoritative English
license text is [`LICENSE`](LICENSE). Copyright (c) 2026 The Agent Teams contributors.
The npm dependencies keep their own licenses — see
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).