@prohost/cli 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +169 -0
- package/README.md +170 -21
- package/dist/agent/api.d.ts +53 -5
- package/dist/agent/api.js +51 -11
- package/dist/agent/claude.d.ts +74 -4
- package/dist/agent/claude.js +172 -21
- package/dist/agent/codex.d.ts +215 -11
- package/dist/agent/codex.js +555 -23
- package/dist/agent/command.js +11 -2
- package/dist/agent/contract.d.ts +39 -0
- package/dist/agent/contract.js +47 -0
- package/dist/agent/exec.d.ts +42 -4
- package/dist/agent/exec.js +104 -18
- package/dist/agent/extras.d.ts +73 -0
- package/dist/agent/extras.js +118 -0
- package/dist/agent/heartbeat.d.ts +114 -0
- package/dist/agent/heartbeat.js +165 -0
- package/dist/agent/mcp.d.ts +7 -6
- package/dist/agent/mcp.js +7 -6
- package/dist/agent/prompt.d.ts +17 -5
- package/dist/agent/prompt.js +66 -2
- package/dist/agent/run.d.ts +58 -3
- package/dist/agent/run.js +525 -97
- package/dist/index.js +13 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,175 @@ Versions follow [semver](https://semver.org/). Publishing is automated: merging
|
|
|
4
4
|
a version bump to `main` triggers `.github/workflows/npm-publish-cli.yml`, which
|
|
5
5
|
builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
6
6
|
|
|
7
|
+
## 0.3.0
|
|
8
|
+
|
|
9
|
+
Long runs. Every change here comes out of one production failure: a paired
|
|
10
|
+
brain was given a real coding task, worked on it for five minutes, and was
|
|
11
|
+
killed mid-edit by a timeout that had been chosen when "a run" meant "answer a
|
|
12
|
+
mention". The person waiting got `timed out after 300000ms` and nothing else —
|
|
13
|
+
no partial answer, no way to resume the session it had built up, and no way to
|
|
14
|
+
tell from the thread that the agent had been doing anything at all.
|
|
15
|
+
|
|
16
|
+
- **The default ceiling is two hours, and `--timeout 0` removes it.** A coding
|
|
17
|
+
agent's unit of work is not a chat reply. The timeout is now a backstop
|
|
18
|
+
against a genuinely wedged process rather than a shape imposed on the work,
|
|
19
|
+
and `0` — previously rejected — means the run ends when the agent ends it,
|
|
20
|
+
when somebody cancels it, or when the server stops renewing its lease. The
|
|
21
|
+
timeout message names the way out (`raise the ceiling with --timeout
|
|
22
|
+
<seconds>`) because that string does not stay in `agent.log`: the server
|
|
23
|
+
echoes a failed run's error into the thread, so the person waiting reads it.
|
|
24
|
+
- **Heartbeats keep long runs alive, and are how "stop" reaches a laptop.**
|
|
25
|
+
Every run this process has accepted is reported once a minute — the executing
|
|
26
|
+
one *and* every run queued behind it, since a run waiting behind a four-hour
|
|
27
|
+
job is `running` server-side for four hours and is reaped on exactly the same
|
|
28
|
+
clock. The response carries `cancel_requested`, which is the only channel by
|
|
29
|
+
which a stop pressed in ProhostAI can reach a machine that accepts no inbound
|
|
30
|
+
connections. Cancelling signals the command's whole process group the same way
|
|
31
|
+
a timeout does (SIGTERM, then SIGKILL five seconds later), reports the run
|
|
32
|
+
with the marker the server words as a cancellation, and posts no reply. A run
|
|
33
|
+
the server has already closed out (409) is abandoned outright: the child is
|
|
34
|
+
killed and nothing is posted, because there is nothing left to post to.
|
|
35
|
+
- **Against an older ProhostAI server, nothing changes.** The heartbeat endpoint
|
|
36
|
+
answers `404` there; the CLI says so once per run, stops asking, and behaves
|
|
37
|
+
exactly as 0.2.3 did. No `Depends-on`, no coordinated upgrade.
|
|
38
|
+
- **Claude Code runs are read as they happen (`--output-format stream-json`).**
|
|
39
|
+
Three things fall out of that, none of which could work when stdout was read
|
|
40
|
+
at exit. The session id is recorded the moment the run announces it, so a run
|
|
41
|
+
killed at hour two can still be resumed — where before, the id arrived with
|
|
42
|
+
the final envelope a killed run never produces, and two hours of context went
|
|
43
|
+
with it. The heartbeat carries what the agent is doing (the tool it just
|
|
44
|
+
called, or the opening of what it is writing). And a killed or cancelled run
|
|
45
|
+
reports the tail of the answer it had been writing rather than only the fact
|
|
46
|
+
that it stopped. A rejected reply still un-records the session, so the rule
|
|
47
|
+
that used to be expressed by recording late is kept. Codex and plain `--exec`
|
|
48
|
+
commands are untouched; heartbeats and cancellation apply to them all the
|
|
49
|
+
same, they just have no progress to report.
|
|
50
|
+
- **The prompt states the actual budget.** It used to say "this run has a hard
|
|
51
|
+
time limit" — the same sentence whether the limit was five minutes or four
|
|
52
|
+
hours, which is how an agent with two hours triages work it had time to
|
|
53
|
+
finish. It now says roughly how long, or that there is no fixed limit and a
|
|
54
|
+
long silence gets reported as a stalled run.
|
|
55
|
+
- **A dropped completion is retried for about ten minutes**, up from thirteen
|
|
56
|
+
seconds. The old schedule was sized for a five-minute run; losing the
|
|
57
|
+
completion of a four-hour one is worth waiting out a wifi drop for. The run
|
|
58
|
+
keeps being heartbeated throughout, so the row it is trying to close is not
|
|
59
|
+
reaped underneath the retries.
|
|
60
|
+
|
|
61
|
+
## 0.2.3
|
|
62
|
+
|
|
63
|
+
Three things, all of them about a run knowing what is actually true about
|
|
64
|
+
itself: a prompt bullet that taught agents to doubt tools they had, a way to
|
|
65
|
+
give an agent a tool on purpose, and closing the gaps 0.2.2 shipped with an
|
|
66
|
+
honest note attached.
|
|
67
|
+
|
|
68
|
+
- **A ToolSearch miss no longer reads as proof.** 0.2.1 told the agent to
|
|
69
|
+
"search ToolSearch for a capability before telling anyone you lack it", which
|
|
70
|
+
is right about deferred web browsing and badly wrong as a general rule. In
|
|
71
|
+
production a paired brain searched ToolSearch for `bash`, found nothing —
|
|
72
|
+
ToolSearch indexes only the *deferred* tools, and Bash is a direct one — and
|
|
73
|
+
spent an entire resumed session reporting it had no shell, while
|
|
74
|
+
`--dangerously-skip-permissions` had been handing it the full toolset the
|
|
75
|
+
whole time. The prompt now states that the directly-listed tools are the
|
|
76
|
+
authoritative set, that a ToolSearch miss is evidence of nothing, and that
|
|
77
|
+
"looking" means running the command rather than reading one's own tool list:
|
|
78
|
+
a claim of no shell, no file access or no checkout has to come after an
|
|
79
|
+
attempt that failed and can be quoted. The original bullet stays — deferred
|
|
80
|
+
browsing is real, and dropping it would just swap one wrong inference for the
|
|
81
|
+
other.
|
|
82
|
+
- **`~/.prohost/extra-mcp.json` — opt-in extra MCP servers.** A run gets the
|
|
83
|
+
ProhostAI server and nothing else, and that isolation is a security boundary:
|
|
84
|
+
a run executes with permission prompts off on prompt text written by whoever
|
|
85
|
+
mentioned the agent. This adds a way to widen it deliberately without weakening
|
|
86
|
+
the default. Write the file yourself — the CLI never creates it — holding the
|
|
87
|
+
same `{"mcpServers": {…}}` object Claude Code's `--mcp-config` takes; it works
|
|
88
|
+
on both brains. The name `prohost` is reserved and an entry using it is
|
|
89
|
+
rejected out loud rather than silently honoured; a malformed file is a warning
|
|
90
|
+
and a run without extras rather than a run that doesn't happen; and which
|
|
91
|
+
servers loaded is printed when the grant set changes so `agent.log` can answer "did my server
|
|
92
|
+
load". Every server in that file is exercisable by anyone who can trigger the
|
|
93
|
+
agent, which the README, the file's own documentation and `--help` all say in
|
|
94
|
+
those words.
|
|
95
|
+
- **Codex gets the isolation Claude Code has.** 0.2.2 shipped with "there is no
|
|
96
|
+
Codex equivalent of `--strict-mcp-config`", which turned out to be true of the
|
|
97
|
+
two mechanisms that look like one and false overall. `-c mcp_servers={…}` does
|
|
98
|
+
not replace the table — Codex deep-merges overrides — and `--ignore-user-config`
|
|
99
|
+
works but discards the operator's model, profile and sandbox settings along
|
|
100
|
+
with their servers. The one that works is per-server: the CLI now asks the
|
|
101
|
+
binary what it has (`codex mcp list --json`) and passes a single
|
|
102
|
+
`-c mcp_servers={"<name>"={enabled=false}, …}`, verified end to end against
|
|
103
|
+
`codex-cli 0.144.1` — the tools are genuinely gone from the model's list
|
|
104
|
+
mid-run, each server's own definition survives, and servers not named are
|
|
105
|
+
untouched. Asking the binary rather than parsing `~/.codex/config.toml`
|
|
106
|
+
matters twice over: on a normal install `codex mcp list` reports servers that
|
|
107
|
+
appear nowhere in that file, and the name has to go inside the value because
|
|
108
|
+
a server called `fly.io` written as `-c mcp_servers.fly.io.enabled=false`
|
|
109
|
+
addresses a three-segment path and makes Codex reject the entire config. The
|
|
110
|
+
override is also **verified against the binary before it is used**, which one
|
|
111
|
+
case genuinely needs: `{enabled=false}` merges onto a server's existing
|
|
112
|
+
definition, and a plugin-contributed server has none to merge onto, so the
|
|
113
|
+
override leaves a table with no transport and Codex refuses to start —
|
|
114
|
+
shipped unverified that would have broken every run for anyone with a Codex
|
|
115
|
+
plugin installed. The CLI falls back to checking each server on its own and
|
|
116
|
+
fails the run before Codex starts if any cannot be disabled. If the
|
|
117
|
+
enumeration fails outright the run also fails closed instead of proceeding
|
|
118
|
+
under `--yolo` with an unprovable boundary. Same treatment for `extra-mcp.json` definitions:
|
|
119
|
+
rejected ones are dropped with a warning rather than taking the run with them.
|
|
120
|
+
Every probe now runs in the exact cwd of the eventual Codex process as well,
|
|
121
|
+
so a trusted repo's project-local `.codex/config.toml` cannot appear only
|
|
122
|
+
after a clean-directory survey. Default `$PROHOST_HOME/workspace` and an
|
|
123
|
+
explicit harness `--workdir` both use that same path end to end.
|
|
124
|
+
|
|
125
|
+
Three further things the binary had to be asked rather than assumed. Every
|
|
126
|
+
server goes in **one** `-c mcp_servers={…}`, ProhostAI included: `-c`
|
|
127
|
+
overrides apply in order and a later whole-table assignment *replaces* an
|
|
128
|
+
earlier dotted one, so the obvious two-override shape silently dropped the
|
|
129
|
+
ProhostAI server on every run that disabled anything. An extra whose name
|
|
130
|
+
your Codex config already uses is **refused**, because Codex merges the two
|
|
131
|
+
definitions and the existing server's `Authorization` header would be sent to
|
|
132
|
+
the extra's URL. The extras file and Codex survey are refreshed before every
|
|
133
|
+
run, so deleting a grant or installing a new server takes effect immediately
|
|
134
|
+
in a long-lived daemon. Literal header and stdio environment secrets from the
|
|
135
|
+
Claude-shaped extras file are moved into Codex's child environment instead of
|
|
136
|
+
being exposed in process arguments.
|
|
137
|
+
|
|
138
|
+
One gap stays open and is documented rather than papered over: a `--exec`
|
|
139
|
+
running Codex under a named profile (`-p work`) can have MCP servers defined
|
|
140
|
+
only in `work.config.toml`, and `codex mcp list` cannot be asked about a
|
|
141
|
+
profile at all — `-p`/`--profile` are rejected and `-c profile="…"` is refused
|
|
142
|
+
as a removed legacy form. The CLI now fails the run before spawning Codex
|
|
143
|
+
when it sees a profile flag, including clap's joined `-pwork` spelling. It
|
|
144
|
+
also fails closed on `-c` / `--config`, feature toggles,
|
|
145
|
+
`--ignore-user-config`, and cwd-changing `-C` / `--cd` flags, because those
|
|
146
|
+
can make the real run's plugin/MCP inventory differ from the plain verifier
|
|
147
|
+
command. Finally, an
|
|
148
|
+
unrecognised item inside otherwise-valid `mcp list --json` output now makes
|
|
149
|
+
the entire survey unknown instead of being silently mistaken for no server.
|
|
150
|
+
Recognisable Codex commands containing shell quoting, expansion, glob,
|
|
151
|
+
comment or control syntax now fail before spawn instead of falling through to
|
|
152
|
+
the unshaped path; in particular, quoting an inventory-changing flag cannot
|
|
153
|
+
hide it from the verifier. The final probe must also report the ProhostAI
|
|
154
|
+
server and every accepted extra enabled — a successful empty or partial list
|
|
155
|
+
is no longer accepted as proof. Finally, all environment names referenced by
|
|
156
|
+
every extra are reserved before literal-header secret variables are
|
|
157
|
+
allocated, preventing one extra from naming and reading another extra's
|
|
158
|
+
generated credential, including through differently-cased aliases on Windows.
|
|
159
|
+
Ordinary unquoted Windows executable and workspace paths remain accepted by
|
|
160
|
+
the shell-syntax guard while `cmd.exe` expansion and control syntax still
|
|
161
|
+
fail closed.
|
|
162
|
+
- **Codex reports refused tools.** Also filed under "no equivalent" in 0.2.2.
|
|
163
|
+
Codex has no `permission_denials` field, but a refused call arrives as an
|
|
164
|
+
`mcp_tool_call` item with `status: "failed"` and `user cancelled MCP tool
|
|
165
|
+
call`, which is what every run with nobody at the terminal produces. Those are
|
|
166
|
+
now printed on the same line the Claude Code path uses. A call that failed for
|
|
167
|
+
another reason — a server returning 502 — is deliberately not counted, so the
|
|
168
|
+
log doesn't send you to your permissions over a network problem.
|
|
169
|
+
- **Codex is told how it spells its own tools.** The prompt used to name the MCP
|
|
170
|
+
server and leave the prefix out, because guessing a tool name is the same
|
|
171
|
+
failure as guessing an endpoint. Measured against the real binary by
|
|
172
|
+
registering a stub server and asking the model what it had just called: Codex
|
|
173
|
+
uses the same `mcp__<server>__<tool>` convention as Claude Code, so both
|
|
174
|
+
brains now get the pattern.
|
|
175
|
+
|
|
7
176
|
## 0.2.2
|
|
8
177
|
|
|
9
178
|
Two agents paired to the same workspace behaved like different products: the
|
package/README.md
CHANGED
|
@@ -179,23 +179,88 @@ word of the command *and* print mode is asked for. Anything else — `npx claude
|
|
|
179
179
|
redirect, or `&&` — keeps the plain stdin/stdout contract untouched. So
|
|
180
180
|
`--exec 'sh -c "claude -p"'` is the way to opt out.
|
|
181
181
|
|
|
182
|
+
### Giving the agent other MCP servers
|
|
183
|
+
|
|
184
|
+
One server by default is a boundary, not an oversight: a run executes with
|
|
185
|
+
permission prompts off, on prompt text written by whoever mentioned your agent.
|
|
186
|
+
Every MCP server reachable from that run is a server your workspace teammates
|
|
187
|
+
can drive.
|
|
188
|
+
|
|
189
|
+
If you want to widen it anyway, create `~/.prohost/extra-mcp.json` by hand. The
|
|
190
|
+
CLI never creates this file — its existence is you saying yes on purpose. The
|
|
191
|
+
shape is the same `mcpServers` object Claude Code's `--mcp-config` takes, so you
|
|
192
|
+
can lift a block straight out of an existing `.mcp.json`:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"mcpServers": {
|
|
197
|
+
"linear": {
|
|
198
|
+
"type": "http",
|
|
199
|
+
"url": "https://mcp.linear.app/mcp",
|
|
200
|
+
"headers": { "Authorization": "Bearer lin_api_…" }
|
|
201
|
+
},
|
|
202
|
+
"notes": {
|
|
203
|
+
"command": "npx",
|
|
204
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/notes"]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Read this before you use it.** Every server in that file is exercisable by
|
|
211
|
+
anyone who can trigger your agent — a teammate, a guest whose message routes to
|
|
212
|
+
it — with permission prompts off and no human in the loop. A filesystem server
|
|
213
|
+
scoped at `/` is a filesystem server they can read. Scope each one as narrowly
|
|
214
|
+
as it will go.
|
|
215
|
+
|
|
216
|
+
Works on both Claude Code and Codex. Details:
|
|
217
|
+
|
|
218
|
+
- The name `prohost` is **reserved**. An entry using it is rejected with a line
|
|
219
|
+
in the log and the ProhostAI server stays pointed where pairing put it.
|
|
220
|
+
- **On Codex, a name your own config already uses is also rejected.** Codex
|
|
221
|
+
merges the two definitions rather than replacing one with the other, so an
|
|
222
|
+
extra that changes only the URL would inherit the configured server's headers
|
|
223
|
+
and send its credential to the new host. Rename the extra to use it.
|
|
224
|
+
- Malformed JSON costs you the extras, not the run: the CLI warns and starts
|
|
225
|
+
without them. The same is true of an entry whose value isn't an object.
|
|
226
|
+
- The file is re-read before every run. Deleting an entry revokes it for the
|
|
227
|
+
next invocation of an already-running daemon; no restart is required.
|
|
228
|
+
- Which extras loaded is printed when the grant set changes, so `agent.log` answers "did my
|
|
229
|
+
server actually load" without guessing.
|
|
230
|
+
- On Codex the non-secret parts of each definition become `-c` overrides, which
|
|
231
|
+
means URLs, commands, and arguments land on a command line other users can
|
|
232
|
+
read via `ps`. Literal `headers`, `http_headers`, and stdio `env` values are
|
|
233
|
+
moved to the child environment instead. On Claude Code the definitions go
|
|
234
|
+
into `~/.prohost/mcp.json`, which is `chmod 600`.
|
|
235
|
+
- Also on Codex: a value with something TOML can't express (an infinity, say)
|
|
236
|
+
makes that server skipped with a warning, rather than emitted as an override
|
|
237
|
+
that would fail to parse and take the ProhostAI server down with it. Any
|
|
238
|
+
server *name* works, dots included.
|
|
239
|
+
- Delete the file to go back to one server. There is no flag to disable it,
|
|
240
|
+
because a file you have to write by hand is already the opt-in.
|
|
241
|
+
|
|
182
242
|
### If your agent is Codex
|
|
183
243
|
|
|
184
244
|
When `--exec` starts with `codex` and uses the `exec` subcommand, the CLI does
|
|
185
245
|
the same three things it does for Claude Code, through Codex's own flags. The
|
|
186
|
-
detection is just as narrow: interactive `codex`, a wrapper script, a
|
|
187
|
-
|
|
188
|
-
carrying its own prompt (`codex exec
|
|
246
|
+
detection is just as narrow: interactive `codex`, a wrapper script, a command
|
|
247
|
+
that already says `codex exec resume` / `codex exec review`, or one
|
|
248
|
+
carrying its own unquoted prompt (`codex exec Be-terse`) keep the plain
|
|
189
249
|
stdin/stdout contract untouched. That last one matters: Codex takes exactly one
|
|
190
|
-
prompt positional, so the CLI cannot add its own alongside yours.
|
|
191
|
-
|
|
192
|
-
|
|
250
|
+
prompt positional, so the CLI cannot add its own alongside yours. Shell syntax
|
|
251
|
+
inside a recognisable Codex command — quoting, escapes, expansions, globs,
|
|
252
|
+
comments, redirects, pipelines or lists — instead fails before Codex starts.
|
|
253
|
+
The harness cannot safely make a whitespace scan agree with both POSIX shells
|
|
254
|
+
and `cmd.exe`, and treating such a command as plain would silently drop MCP
|
|
255
|
+
isolation. Ordinary unquoted Windows path separators remain supported. Put
|
|
256
|
+
standing instructions in the agent's ProhostAI profile instead — they arrive at
|
|
257
|
+
the top of the prompt, ahead of everything else.
|
|
193
258
|
|
|
194
259
|
| | Claude Code | Codex |
|
|
195
260
|
| --- | --- | --- |
|
|
196
261
|
| Reply read from | `--output-format json` | `--json` (JSONL event stream) |
|
|
197
262
|
| Conversation kept with | `--resume <id>` | `codex exec resume <thread>` |
|
|
198
|
-
| Tools wired via | `--mcp-config` | `-c mcp_servers
|
|
263
|
+
| Tools wired via | `--mcp-config` | one verified `-c mcp_servers={…}` override |
|
|
199
264
|
| API key travels in | `~/.prohost/mcp.json` (`chmod 600`) | the child's environment |
|
|
200
265
|
| Tools ungated by | `--dangerously-skip-permissions` | `--yolo` |
|
|
201
266
|
|
|
@@ -210,22 +275,83 @@ does for Claude Code, and it *removes* a bypass flag you wrote yourself rather
|
|
|
210
275
|
than just declining to add one, so the gating it promises is real. (It covers
|
|
211
276
|
the full bypass only; `--full-auto` still auto-approves inside Codex's sandbox.)
|
|
212
277
|
|
|
213
|
-
**
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
278
|
+
**Your own MCP servers are switched off for agent runs.** Codex has no
|
|
279
|
+
`--strict-mcp-config`, so the CLI does the same job a different way: before each run
|
|
280
|
+
it asks your binary what servers it has (`codex mcp list --json`) and passes one
|
|
281
|
+
`-c mcp_servers={"<name>"={enabled=false}, …}` covering all of them. A run then
|
|
282
|
+
holds the ProhostAI server, plus anything in your `extra-mcp.json`, and nothing
|
|
283
|
+
else — the same boundary the Claude Code path gets. Each server's own definition
|
|
284
|
+
is left intact; only this invocation sees it disabled.
|
|
285
|
+
|
|
286
|
+
Two mechanisms that look like they should do this do not, and are worth naming
|
|
287
|
+
so nobody reaches for them. Replacing the `mcp_servers` table does not replace
|
|
288
|
+
it — Codex deep-merges config overrides, so your servers survive alongside
|
|
289
|
+
whatever the override adds. And `--ignore-user-config` works, but throws away
|
|
290
|
+
your model, profile and sandbox settings along with your servers.
|
|
291
|
+
|
|
292
|
+
The enumeration asks the binary rather than reading `~/.codex/config.toml`,
|
|
293
|
+
because that file is not the whole story: on a normal install `codex mcp list`
|
|
294
|
+
reports servers that appear nowhere in it. It also handles names a dotted-path
|
|
295
|
+
override cannot address — a server called `fly.io` written as
|
|
296
|
+
`-c mcp_servers.fly.io.enabled=false` addresses a three-segment path and Codex
|
|
297
|
+
rejects the whole config, which is why the name goes inside the value instead.
|
|
298
|
+
Every probe also runs in the exact directory the eventual Codex process will
|
|
299
|
+
use. That matters because a trusted project's `.codex/config.toml` is another
|
|
300
|
+
config layer: probing where the harness was launched and then running in
|
|
301
|
+
`$PROHOST_HOME/workspace` or an explicit `--workdir` would certify one MCP
|
|
302
|
+
inventory and execute another.
|
|
303
|
+
|
|
304
|
+
**The override is checked before it is used**, and one case needs it.
|
|
305
|
+
`{enabled=false}` merges onto a server's existing definition, and a server
|
|
306
|
+
contributed by a **plugin** has no `mcp_servers` entry to merge onto — so the
|
|
307
|
+
override leaves a table with no transport and Codex refuses to start at all.
|
|
308
|
+
The CLI therefore runs the disable past `codex mcp list` first, falls back to
|
|
309
|
+
checking each server individually if the combined form is rejected, and then
|
|
310
|
+
verifies the exact final override. If any configured server cannot be disabled,
|
|
311
|
+
or if the final listing does not show the ProhostAI server and every accepted
|
|
312
|
+
extra enabled, the run fails before Codex starts. Remove that server/plugin or
|
|
313
|
+
use Claude Code.
|
|
314
|
+
|
|
315
|
+
If the enumeration fails outright — an older Codex without `mcp list --json`,
|
|
316
|
+
or output the CLI doesn't recognise — the run fails before Codex starts. A
|
|
317
|
+
paired run never proceeds under `--yolo` when its MCP boundary cannot be proven.
|
|
318
|
+
Nothing here changes what your Codex config does outside agent runs.
|
|
319
|
+
|
|
320
|
+
**One gap that cannot be closed from here: profiles.** If your `--exec` runs
|
|
321
|
+
Codex under a named profile (`codex exec -p work`), that profile's
|
|
322
|
+
`work.config.toml` can define MCP servers of its own — and `codex mcp list` has
|
|
323
|
+
no way to be asked about a profile (`-p` and `--profile` are rejected, and the
|
|
324
|
+
`-c profile="…"` form is refused as a removed legacy spelling). Servers defined
|
|
325
|
+
only inside a profile are therefore invisible to the sweep. The CLI fails the
|
|
326
|
+
run before Codex starts when it sees a profile flag; run the agent without `-p`.
|
|
327
|
+
|
|
328
|
+
The same fail-closed rule applies to `-c` / `--config`, `--enable`, `--disable`,
|
|
329
|
+
`--ignore-user-config`, and `-C` / `--cd` in `--exec`. Those flags can change
|
|
330
|
+
config, plugin loading, or the project config discovery directory for the real
|
|
331
|
+
invocation, while the separate `codex mcp list` verifier cannot safely replay
|
|
332
|
+
an arbitrary shell command's arguments. Use the harness's `--workdir` when you
|
|
333
|
+
need a different directory; dedicated flags such as `--model` and `--sandbox`
|
|
334
|
+
remain supported.
|
|
335
|
+
|
|
336
|
+
The survey is retaken before every invocation, because
|
|
337
|
+
`install-daemon` runs for weeks and Codex rereads its configuration on every
|
|
338
|
+
invocation — a server you install on Tuesday would otherwise be reachable from
|
|
339
|
+
the next run. The log stays quiet unless the answer changes.
|
|
340
|
+
|
|
341
|
+
**Refused tools are reported**, as of 0.2.3. Codex has no `permission_denials`
|
|
342
|
+
field, but a refused call arrives as an `mcp_tool_call` item with
|
|
343
|
+
`status: "failed"` and `user cancelled MCP tool call`, and the CLI prints those
|
|
344
|
+
in the same line the Claude Code path uses. A tool call that failed for any
|
|
345
|
+
other reason — an MCP server that 500s — is deliberately not reported as a
|
|
346
|
+
denial, so the log doesn't send you to your permissions when the problem is
|
|
347
|
+
your network.
|
|
225
348
|
|
|
226
349
|
Your ProhostAI API key is passed to Codex in the child process's environment
|
|
227
350
|
rather than in the `-c` override, because a command line is readable by anyone
|
|
228
|
-
on the machine via `ps` and a process environment is not.
|
|
351
|
+
on the machine via `ps` and a process environment is not. Literal `headers`,
|
|
352
|
+
`http_headers`, and stdio `env` values in `extra-mcp.json` get the same
|
|
353
|
+
treatment: the CLI translates them to Codex's environment-indirection fields,
|
|
354
|
+
so their values do not appear in process arguments.
|
|
229
355
|
|
|
230
356
|
Runs are handled one at a time, and each run executes **at most once on this
|
|
231
357
|
machine** — even across restarts. Started runs are recorded in
|
|
@@ -274,7 +400,7 @@ same judgement.
|
|
|
274
400
|
| Flag | Default | What it does |
|
|
275
401
|
| --- | --- | --- |
|
|
276
402
|
| `--exec` | *(required)* | Command to run for each requested run. Prompt arrives on stdin. |
|
|
277
|
-
| `--timeout` | `
|
|
403
|
+
| `--timeout` | `7200` | Seconds one run may take before the command is killed. `0` removes the local cap entirely — see "Long runs". |
|
|
278
404
|
| `--dry-run` | off | Print the reply that would be posted; write nothing. |
|
|
279
405
|
| `--workdir` | `~/.prohost/workspace` | Working directory for the command. See "Where your agent runs". |
|
|
280
406
|
| `--cwd` | — | Deprecated alias for `--workdir`. |
|
|
@@ -283,6 +409,28 @@ same judgement.
|
|
|
283
409
|
| `--allow-unverified` | off | Run events whose signature can't be verified. See below. |
|
|
284
410
|
| `--safe-tools` | off | Gate the agent's tools behind permission prompts instead of bypassing them. Claude Code and Codex only. Alias: `--no-skip-permissions`. |
|
|
285
411
|
|
|
412
|
+
### Long runs
|
|
413
|
+
|
|
414
|
+
A paired brain is often asked to do work, not just answer — write the change,
|
|
415
|
+
run the tests, come back. That takes minutes or hours, and the harness is built
|
|
416
|
+
for it:
|
|
417
|
+
|
|
418
|
+
- **Two hours by default**, and `--timeout 0` removes the local cap entirely.
|
|
419
|
+
The timeout is a backstop against a genuinely wedged process, not a shape for
|
|
420
|
+
the work.
|
|
421
|
+
- **Heartbeats keep it alive.** Every run this CLI has accepted — the one
|
|
422
|
+
executing *and* any queued behind it — is reported to ProhostAI once a minute,
|
|
423
|
+
which is what stops the server reaping a run that is still being worked on.
|
|
424
|
+
Against an older ProhostAI server the heartbeat endpoint answers `404`; the
|
|
425
|
+
CLI says so once per run and otherwise behaves exactly as it did before.
|
|
426
|
+
- **Cancel works.** Stopping a run in ProhostAI is answered on the next
|
|
427
|
+
heartbeat: the command's whole process group gets `SIGTERM`, then `SIGKILL`
|
|
428
|
+
five seconds later, and the run is reported as cancelled. No reply is posted.
|
|
429
|
+
- **A killed run is not silent.** With Claude Code the CLI reads the run's
|
|
430
|
+
output as it is produced, so the run's session id is saved immediately (the
|
|
431
|
+
next turn on that surface can still resume it) and a run that is killed or
|
|
432
|
+
cancelled reports the tail of whatever the agent had written.
|
|
433
|
+
|
|
286
434
|
### Keeping it running
|
|
287
435
|
|
|
288
436
|
`agent run` in a terminal window stops being an agent the moment you close the
|
|
@@ -363,6 +511,7 @@ Everything the CLI stores lives under `~/.prohost` (`$PROHOST_HOME` moves it):
|
|
|
363
511
|
| `runs.json` | Runs already executed on this machine, so a redelivery can't run your agent twice. |
|
|
364
512
|
| `sessions.json` | Conversation → Claude Code session / Codex thread, for continuity. Safe to delete; costs the agent its memory of open threads. |
|
|
365
513
|
| `mcp.json` | Generated MCP config. Holds your API key — `chmod 600`. |
|
|
514
|
+
| `extra-mcp.json` | Optional, written by you. Extra MCP servers to expose to agent runs — see "Giving the agent other MCP servers". Never created by the CLI. |
|
|
366
515
|
| `workspace/` | Where your agent command runs by default. |
|
|
367
516
|
| `agent.log` | Daemon output. |
|
|
368
517
|
|
package/dist/agent/api.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
2
|
+
* The HTTP writes the harness makes back into ProhostAI: post the reply, say a
|
|
3
|
+
* long run is still alive, and close the run out.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* a result object rather than throwing — the runner has to keep
|
|
7
|
-
* still complete the run) whichever one fails.
|
|
5
|
+
* All of them authenticate with the paired API key over ``X-API-Key`` and all
|
|
6
|
+
* of them return a result object rather than throwing — the runner has to keep
|
|
7
|
+
* going (and still complete the run) whichever one fails.
|
|
8
8
|
*
|
|
9
9
|
* The paths come from the run event itself (`reply_instructions.path` /
|
|
10
10
|
* `completion.path`), which is why these take a path rather than building one.
|
|
@@ -20,6 +20,13 @@ export interface ApiResult {
|
|
|
20
20
|
ok: boolean;
|
|
21
21
|
status: number;
|
|
22
22
|
error?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The response body, when the caller asked for it.
|
|
25
|
+
*
|
|
26
|
+
* Only the heartbeat reads a success body — it is the one call whose answer
|
|
27
|
+
* carries information (``cancel_requested``) rather than just an acceptance.
|
|
28
|
+
*/
|
|
29
|
+
body?: string;
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
25
32
|
* Post the agent's reply into the conversation that triggered the run.
|
|
@@ -34,6 +41,47 @@ export interface ApiResult {
|
|
|
34
41
|
* a CLI release.
|
|
35
42
|
*/
|
|
36
43
|
export declare function postReply(options: ApiOptions, path: string, bodyKey: string, message: string, runId: string, extra?: Record<string, unknown>): Promise<ApiResult>;
|
|
44
|
+
/**
|
|
45
|
+
* What one heartbeat told us about a run.
|
|
46
|
+
*
|
|
47
|
+
* Four outcomes, and the harness does something different with each — which is
|
|
48
|
+
* why this is a tagged union rather than an {@link ApiResult}. In particular
|
|
49
|
+
* ``unknown`` (404) is not an error to report: it is what **every server built
|
|
50
|
+
* before heartbeats existed** answers, and the only correct response to it is
|
|
51
|
+
* to stop asking and behave exactly as this CLI did before.
|
|
52
|
+
*/
|
|
53
|
+
export type HeartbeatOutcome =
|
|
54
|
+
/** The run is still open server-side. ``cancelRequested`` means: stop now. */
|
|
55
|
+
{
|
|
56
|
+
kind: 'alive';
|
|
57
|
+
cancelRequested: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** 404 — an older server, or a run this credential no longer owns. */
|
|
60
|
+
| {
|
|
61
|
+
kind: 'unknown';
|
|
62
|
+
}
|
|
63
|
+
/** 409 — the run already reached a terminal state without us. */
|
|
64
|
+
| {
|
|
65
|
+
kind: 'resolved';
|
|
66
|
+
}
|
|
67
|
+
/** Anything else, including a network failure (``status: 0``). */
|
|
68
|
+
| {
|
|
69
|
+
kind: 'error';
|
|
70
|
+
status: number;
|
|
71
|
+
detail?: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Tell the server a run is still being worked on, and ask whether it should be.
|
|
75
|
+
*
|
|
76
|
+
* Sent on a cadence for every run this process has accepted and not yet
|
|
77
|
+
* reported — the one being executed *and* the ones queued behind it, because a
|
|
78
|
+
* run queued behind a four-hour job is exactly as reapable as the job itself.
|
|
79
|
+
*
|
|
80
|
+
* ``progress`` is a courtesy for whoever is watching the run in the UI, never a
|
|
81
|
+
* requirement: a queued run has none, and the brains that don't stream their
|
|
82
|
+
* work send none either.
|
|
83
|
+
*/
|
|
84
|
+
export declare function heartbeatRun(options: ApiOptions, path: string, progress?: string): Promise<HeartbeatOutcome>;
|
|
37
85
|
/**
|
|
38
86
|
* Report a run's terminal state. Called for every run — including one whose
|
|
39
87
|
* command crashed — so the server never waits on a run this harness abandoned.
|
package/dist/agent/api.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
2
|
+
* The HTTP writes the harness makes back into ProhostAI: post the reply, say a
|
|
3
|
+
* long run is still alive, and close the run out.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* a result object rather than throwing — the runner has to keep
|
|
7
|
-
* still complete the run) whichever one fails.
|
|
5
|
+
* All of them authenticate with the paired API key over ``X-API-Key`` and all
|
|
6
|
+
* of them return a result object rather than throwing — the runner has to keep
|
|
7
|
+
* going (and still complete the run) whichever one fails.
|
|
8
8
|
*
|
|
9
9
|
* The paths come from the run event itself (`reply_instructions.path` /
|
|
10
10
|
* `completion.path`), which is why these take a path rather than building one.
|
|
11
11
|
*/
|
|
12
12
|
import { USER_AGENT } from '../version.js';
|
|
13
|
-
import { MAX_ERROR_CHARS, RUN_STATUS_FAILED, RUN_STATUS_SUCCEEDED } from './contract.js';
|
|
13
|
+
import { MAX_ERROR_CHARS, MAX_PROGRESS_CHARS, RUN_STATUS_FAILED, RUN_STATUS_SUCCEEDED, } from './contract.js';
|
|
14
14
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
-
|
|
15
|
+
/** Cap on a captured response body. Every one we parse is a small object. */
|
|
16
|
+
const MAX_BODY_CHARS = 2_000;
|
|
17
|
+
async function post(options, path, body, idempotencyKey, captureBody = false) {
|
|
16
18
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
17
19
|
const controller = new AbortController();
|
|
18
20
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
@@ -30,15 +32,18 @@ async function post(options, path, body, idempotencyKey) {
|
|
|
30
32
|
});
|
|
31
33
|
const ok = response.status >= 200 && response.status < 300;
|
|
32
34
|
let error;
|
|
33
|
-
|
|
35
|
+
let text;
|
|
36
|
+
if (!ok || captureBody) {
|
|
34
37
|
// Surface the server's reason — a scope or tier denial is the most
|
|
35
38
|
// likely failure here and the host can act on it.
|
|
36
|
-
|
|
39
|
+
text = await response
|
|
37
40
|
.text()
|
|
38
|
-
.then((t) => t.slice(0,
|
|
41
|
+
.then((t) => t.slice(0, MAX_BODY_CHARS))
|
|
39
42
|
.catch(() => undefined);
|
|
43
|
+
if (!ok)
|
|
44
|
+
error = text?.slice(0, 500);
|
|
40
45
|
}
|
|
41
|
-
return { ok, status: response.status, error };
|
|
46
|
+
return { ok, status: response.status, error, body: captureBody ? text : undefined };
|
|
42
47
|
}
|
|
43
48
|
catch (err) {
|
|
44
49
|
return {
|
|
@@ -69,6 +74,41 @@ export async function postReply(options, path, bodyKey, message, runId, extra) {
|
|
|
69
74
|
// thing in this request that must survive.
|
|
70
75
|
return post(options, path, { ...extra, [bodyKey]: message }, `prohost-cli-run-${runId}`);
|
|
71
76
|
}
|
|
77
|
+
function booleanField(body, key) {
|
|
78
|
+
if (!body)
|
|
79
|
+
return false;
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(body);
|
|
82
|
+
if (!parsed || typeof parsed !== 'object')
|
|
83
|
+
return false;
|
|
84
|
+
return parsed[key] === true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Tell the server a run is still being worked on, and ask whether it should be.
|
|
92
|
+
*
|
|
93
|
+
* Sent on a cadence for every run this process has accepted and not yet
|
|
94
|
+
* reported — the one being executed *and* the ones queued behind it, because a
|
|
95
|
+
* run queued behind a four-hour job is exactly as reapable as the job itself.
|
|
96
|
+
*
|
|
97
|
+
* ``progress`` is a courtesy for whoever is watching the run in the UI, never a
|
|
98
|
+
* requirement: a queued run has none, and the brains that don't stream their
|
|
99
|
+
* work send none either.
|
|
100
|
+
*/
|
|
101
|
+
export async function heartbeatRun(options, path, progress) {
|
|
102
|
+
const trimmed = progress?.trim();
|
|
103
|
+
const result = await post(options, path, trimmed ? { progress: trimmed.slice(0, MAX_PROGRESS_CHARS) } : {}, undefined, true);
|
|
104
|
+
if (result.ok)
|
|
105
|
+
return { kind: 'alive', cancelRequested: booleanField(result.body, 'cancel_requested') };
|
|
106
|
+
if (result.status === 404)
|
|
107
|
+
return { kind: 'unknown' };
|
|
108
|
+
if (result.status === 409)
|
|
109
|
+
return { kind: 'resolved' };
|
|
110
|
+
return { kind: 'error', status: result.status, detail: result.error };
|
|
111
|
+
}
|
|
72
112
|
/**
|
|
73
113
|
* Report a run's terminal state. Called for every run — including one whose
|
|
74
114
|
* command crashed — so the server never waits on a run this harness abandoned.
|