@prohost/cli 0.2.2 → 0.2.3
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 +115 -0
- package/README.md +147 -20
- package/dist/agent/claude.d.ts +21 -3
- package/dist/agent/claude.js +7 -1
- package/dist/agent/codex.d.ts +215 -11
- package/dist/agent/codex.js +555 -23
- package/dist/agent/extras.d.ts +73 -0
- package/dist/agent/extras.js +118 -0
- package/dist/agent/mcp.d.ts +7 -6
- package/dist/agent/mcp.js +7 -6
- package/dist/agent/prompt.d.ts +6 -5
- package/dist/agent/prompt.js +16 -0
- package/dist/agent/run.d.ts +33 -0
- package/dist/agent/run.js +216 -61
- package/dist/index.js +10 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operator's opt-in extra MCP servers (`$PROHOST_HOME/extra-mcp.json`).
|
|
3
|
+
*
|
|
4
|
+
* Both brains this harness shapes get exactly one MCP server by default — ours
|
|
5
|
+
* — and that isolation is a security boundary rather than an implementation
|
|
6
|
+
* detail. A paired run executes with permission prompts off, on prompt text
|
|
7
|
+
* written by anyone who can mention the agent in the workspace, so every server
|
|
8
|
+
* reachable from that run is a server a workspace teammate can drive. Claude
|
|
9
|
+
* Code is held to it by `--strict-mcp-config`; Codex is held to it by
|
|
10
|
+
* explicitly disabling the servers its own config defines.
|
|
11
|
+
*
|
|
12
|
+
* That leaves no way at all to give the agent a tool the operator *does* want
|
|
13
|
+
* it to have, which is why this file exists. It is **never created by the
|
|
14
|
+
* CLI** — its presence is the operator's deliberate act, taken after reading
|
|
15
|
+
* what it costs. Nothing here weakens the boundary; it moves the decision to a
|
|
16
|
+
* file whose only purpose is to record it.
|
|
17
|
+
*
|
|
18
|
+
* Everything in this module fails soft, in the sense `claude.ts` states: a
|
|
19
|
+
* malformed file is a warning and a run without extras, never a run that
|
|
20
|
+
* doesn't happen. The operator's agent going quiet is a worse outcome than
|
|
21
|
+
* their extra server being missing, and the missing server is the one they can
|
|
22
|
+
* actually see in the log.
|
|
23
|
+
*/
|
|
24
|
+
/** Filename of the operator-authored extras file, inside `$PROHOST_HOME`. */
|
|
25
|
+
export declare const EXTRA_MCP_FILENAME = "extra-mcp.json";
|
|
26
|
+
/** One server definition, passed through to the brain untouched. */
|
|
27
|
+
export type ExtraServer = Record<string, unknown>;
|
|
28
|
+
export interface ExtraMcpServers {
|
|
29
|
+
/** Accepted definitions, keyed by server name. Empty when there are none. */
|
|
30
|
+
servers: Record<string, ExtraServer>;
|
|
31
|
+
/**
|
|
32
|
+
* Lines for the operator to see when the current grant set is loaded.
|
|
33
|
+
*
|
|
34
|
+
* Both the good news and the bad travel this way: which extras loaded is as
|
|
35
|
+
* worth printing as which were rejected, because "I added a server and the
|
|
36
|
+
* agent still can't use it" is otherwise a silent, unfalsifiable complaint.
|
|
37
|
+
*/
|
|
38
|
+
warnings: string[];
|
|
39
|
+
}
|
|
40
|
+
/** Absolute path of the extras file. */
|
|
41
|
+
export declare function extraMcpPath(env?: NodeJS.ProcessEnv): string;
|
|
42
|
+
export interface LoadExtraOptions {
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
44
|
+
/** Injectable read, so a test can exercise the parse without a filesystem. */
|
|
45
|
+
readImpl?: (file: string) => string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read and validate `extra-mcp.json`.
|
|
49
|
+
*
|
|
50
|
+
* The file's shape is deliberately the same object Claude Code's
|
|
51
|
+
* `--mcp-config` takes — `{"mcpServers": {"<name>": {...}}}` — so an operator
|
|
52
|
+
* can lift a block straight out of a `.mcp.json` they already have rather than
|
|
53
|
+
* learn a format that exists only here.
|
|
54
|
+
*
|
|
55
|
+
* Four rejections, each returning a warning rather than an exception:
|
|
56
|
+
*
|
|
57
|
+
* * **No file.** Not a warning at all — the overwhelmingly common case, and
|
|
58
|
+
* the CLI never creates one, so its absence is the default state rather
|
|
59
|
+
* than something the operator did wrong.
|
|
60
|
+
* * **Unparseable JSON.** A warning and zero extras. The alternative — refuse
|
|
61
|
+
* to start — would take a working agent offline over a trailing comma in an
|
|
62
|
+
* optional enhancement.
|
|
63
|
+
* * **A server named `prohost`.** Rejected loudly and never merged. The name
|
|
64
|
+
* is how the prompt, the `--allowedTools` pattern and both generated
|
|
65
|
+
* configs refer to the ProhostAI server; letting a file on disk take it
|
|
66
|
+
* over would silently redirect a run's workspace lookups — with the agent
|
|
67
|
+
* still reporting, in good faith, that it looked them up.
|
|
68
|
+
* * **A definition that is not an object.** Rejected per entry, because a
|
|
69
|
+
* `"github": "https://…"` typo would otherwise reach the brain as a config
|
|
70
|
+
* it cannot parse and take the whole config down with it — including our
|
|
71
|
+
* server.
|
|
72
|
+
*/
|
|
73
|
+
export declare function loadExtraMcpServers(options?: LoadExtraOptions): ExtraMcpServers;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operator's opt-in extra MCP servers (`$PROHOST_HOME/extra-mcp.json`).
|
|
3
|
+
*
|
|
4
|
+
* Both brains this harness shapes get exactly one MCP server by default — ours
|
|
5
|
+
* — and that isolation is a security boundary rather than an implementation
|
|
6
|
+
* detail. A paired run executes with permission prompts off, on prompt text
|
|
7
|
+
* written by anyone who can mention the agent in the workspace, so every server
|
|
8
|
+
* reachable from that run is a server a workspace teammate can drive. Claude
|
|
9
|
+
* Code is held to it by `--strict-mcp-config`; Codex is held to it by
|
|
10
|
+
* explicitly disabling the servers its own config defines.
|
|
11
|
+
*
|
|
12
|
+
* That leaves no way at all to give the agent a tool the operator *does* want
|
|
13
|
+
* it to have, which is why this file exists. It is **never created by the
|
|
14
|
+
* CLI** — its presence is the operator's deliberate act, taken after reading
|
|
15
|
+
* what it costs. Nothing here weakens the boundary; it moves the decision to a
|
|
16
|
+
* file whose only purpose is to record it.
|
|
17
|
+
*
|
|
18
|
+
* Everything in this module fails soft, in the sense `claude.ts` states: a
|
|
19
|
+
* malformed file is a warning and a run without extras, never a run that
|
|
20
|
+
* doesn't happen. The operator's agent going quiet is a worse outcome than
|
|
21
|
+
* their extra server being missing, and the missing server is the one they can
|
|
22
|
+
* actually see in the log.
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { prohostHome } from './credentials.js';
|
|
27
|
+
import { MCP_SERVER_NAME } from './mcp.js';
|
|
28
|
+
/** Filename of the operator-authored extras file, inside `$PROHOST_HOME`. */
|
|
29
|
+
export const EXTRA_MCP_FILENAME = 'extra-mcp.json';
|
|
30
|
+
/** Absolute path of the extras file. */
|
|
31
|
+
export function extraMcpPath(env = process.env) {
|
|
32
|
+
return path.join(prohostHome(env), EXTRA_MCP_FILENAME);
|
|
33
|
+
}
|
|
34
|
+
function isPlainObject(value) {
|
|
35
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read and validate `extra-mcp.json`.
|
|
39
|
+
*
|
|
40
|
+
* The file's shape is deliberately the same object Claude Code's
|
|
41
|
+
* `--mcp-config` takes — `{"mcpServers": {"<name>": {...}}}` — so an operator
|
|
42
|
+
* can lift a block straight out of a `.mcp.json` they already have rather than
|
|
43
|
+
* learn a format that exists only here.
|
|
44
|
+
*
|
|
45
|
+
* Four rejections, each returning a warning rather than an exception:
|
|
46
|
+
*
|
|
47
|
+
* * **No file.** Not a warning at all — the overwhelmingly common case, and
|
|
48
|
+
* the CLI never creates one, so its absence is the default state rather
|
|
49
|
+
* than something the operator did wrong.
|
|
50
|
+
* * **Unparseable JSON.** A warning and zero extras. The alternative — refuse
|
|
51
|
+
* to start — would take a working agent offline over a trailing comma in an
|
|
52
|
+
* optional enhancement.
|
|
53
|
+
* * **A server named `prohost`.** Rejected loudly and never merged. The name
|
|
54
|
+
* is how the prompt, the `--allowedTools` pattern and both generated
|
|
55
|
+
* configs refer to the ProhostAI server; letting a file on disk take it
|
|
56
|
+
* over would silently redirect a run's workspace lookups — with the agent
|
|
57
|
+
* still reporting, in good faith, that it looked them up.
|
|
58
|
+
* * **A definition that is not an object.** Rejected per entry, because a
|
|
59
|
+
* `"github": "https://…"` typo would otherwise reach the brain as a config
|
|
60
|
+
* it cannot parse and take the whole config down with it — including our
|
|
61
|
+
* server.
|
|
62
|
+
*/
|
|
63
|
+
export function loadExtraMcpServers(options = {}) {
|
|
64
|
+
const file = extraMcpPath(options.env ?? process.env);
|
|
65
|
+
const read = options.readImpl ?? ((target) => readFileSync(target, 'utf8'));
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = read(file);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
// ENOENT is the normal state and must stay silent. Anything else — a
|
|
72
|
+
// permission problem, a directory where the file should be — the operator
|
|
73
|
+
// wants to hear about, because they put the file there on purpose.
|
|
74
|
+
const code = err.code;
|
|
75
|
+
if (code === 'ENOENT')
|
|
76
|
+
return { servers: {}, warnings: [] };
|
|
77
|
+
return {
|
|
78
|
+
servers: {},
|
|
79
|
+
warnings: [`could not read ${file} (${err instanceof Error ? err.message : String(err)}); no extra MCP servers`],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(raw);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
servers: {},
|
|
89
|
+
warnings: [`${file} is not valid JSON (${err instanceof Error ? err.message : String(err)}); no extra MCP servers`],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (!isPlainObject(parsed) || !isPlainObject(parsed.mcpServers)) {
|
|
93
|
+
return {
|
|
94
|
+
servers: {},
|
|
95
|
+
warnings: [`${file} has no "mcpServers" object; no extra MCP servers`],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const servers = {};
|
|
99
|
+
const warnings = [];
|
|
100
|
+
for (const [name, definition] of Object.entries(parsed.mcpServers)) {
|
|
101
|
+
if (name === MCP_SERVER_NAME) {
|
|
102
|
+
warnings.push(`${file} defines a server named "${MCP_SERVER_NAME}" — that name is reserved for the ` +
|
|
103
|
+
'ProhostAI server and the entry was ignored. Rename it to use it.');
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!isPlainObject(definition)) {
|
|
107
|
+
warnings.push(`${file}: server "${name}" is not an object; the entry was ignored`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
servers[name] = definition;
|
|
111
|
+
}
|
|
112
|
+
const names = Object.keys(servers);
|
|
113
|
+
if (names.length > 0) {
|
|
114
|
+
warnings.push(`extra MCP servers loaded from ${EXTRA_MCP_FILENAME}: ${names.join(', ')} — ` +
|
|
115
|
+
'anyone who can trigger this agent can use them, with permission prompts off.');
|
|
116
|
+
}
|
|
117
|
+
return { servers, warnings };
|
|
118
|
+
}
|
package/dist/agent/mcp.d.ts
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const MCP_SERVER_NAME = "prohost";
|
|
14
14
|
/**
|
|
15
|
-
* How
|
|
15
|
+
* How both shaped brains spell the wired-in tools.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* Claude Code's spelling was known from its documentation. Codex's was an open
|
|
18
|
+
* question until 0.2.3 — the prompt named the server and left the pattern out
|
|
19
|
+
* rather than guess — and the answer, measured by registering a stub MCP server
|
|
20
|
+
* against a real `codex-cli 0.144.1` and asking the model what it had just
|
|
21
|
+
* called, is that Codex uses the same `mcp__<server>__<tool>` convention. So
|
|
22
|
+
* both brains now get the pattern. See {@link ../prompt.ts}.
|
|
22
23
|
*/
|
|
23
24
|
export declare const MCP_TOOL_PATTERN = "mcp__prohost__*";
|
package/dist/agent/mcp.js
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export const MCP_SERVER_NAME = 'prohost';
|
|
14
14
|
/**
|
|
15
|
-
* How
|
|
15
|
+
* How both shaped brains spell the wired-in tools.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* Claude Code's spelling was known from its documentation. Codex's was an open
|
|
18
|
+
* question until 0.2.3 — the prompt named the server and left the pattern out
|
|
19
|
+
* rather than guess — and the answer, measured by registering a stub MCP server
|
|
20
|
+
* against a real `codex-cli 0.144.1` and asking the model what it had just
|
|
21
|
+
* called, is that Codex uses the same `mcp__<server>__<tool>` convention. So
|
|
22
|
+
* both brains now get the pattern. See {@link ../prompt.ts}.
|
|
22
23
|
*/
|
|
23
24
|
export const MCP_TOOL_PATTERN = `mcp__${MCP_SERVER_NAME}__*`;
|
package/dist/agent/prompt.d.ts
CHANGED
|
@@ -22,11 +22,12 @@ export interface PromptContext {
|
|
|
22
22
|
* How this particular brain spells the wired-in tools, e.g. `mcp__prohost__*`.
|
|
23
23
|
*
|
|
24
24
|
* Set only when the harness has actually verified the spelling for the brain
|
|
25
|
-
* it is driving — Claude Code,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
25
|
+
* it is driving — Claude Code and, since 0.2.3, Codex, which turned out to
|
|
26
|
+
* use the same convention. Left unset for every other `--exec` command,
|
|
27
|
+
* whose namespacing we have no way to know: the prompt then names the server
|
|
28
|
+
* without claiming a prefix, because a wrong tool name is the same failure as
|
|
29
|
+
* a wrong endpoint. The agent can see its own tool list; it does not need us
|
|
30
|
+
* to guess at it.
|
|
30
31
|
*/
|
|
31
32
|
mcpToolPattern?: string;
|
|
32
33
|
/**
|
package/dist/agent/prompt.js
CHANGED
|
@@ -145,6 +145,16 @@ export function buildPrompt(run, context) {
|
|
|
145
145
|
// could. Field-observed on the first 0.2.0 deployment; phrased
|
|
146
146
|
// conditionally because other `--exec` brains have no ToolSearch.
|
|
147
147
|
'- If you have a ToolSearch tool, part of your toolkit is deferred and', ' invisible until loaded — web browsing (WebSearch, WebFetch) usually', ' is. Search ToolSearch for a capability before telling anyone you', ' lack it.',
|
|
148
|
+
// The correction to the bullet above, and it is here because the bullet
|
|
149
|
+
// above caused a real production failure. A paired brain searched
|
|
150
|
+
// ToolSearch for "bash", found nothing — ToolSearch indexes only the
|
|
151
|
+
// *deferred* tools, and Bash is a direct one — and concluded across a
|
|
152
|
+
// whole resumed session that it had no shell, while
|
|
153
|
+
// `--dangerously-skip-permissions` had been handing it the full toolset
|
|
154
|
+
// the entire time. "Search before you claim you lack it" quietly taught
|
|
155
|
+
// the opposite of what it meant: that a ToolSearch miss is evidence of
|
|
156
|
+
// absence. It is not evidence of anything.
|
|
157
|
+
'- A ToolSearch miss proves nothing. ToolSearch indexes only the deferred', ' tools; the tools listed directly to you are the authoritative set, and', ' a tool can be fully available while ToolSearch has never heard of it', ' (Bash is exactly this). Never reason from a ToolSearch miss to "I do', ' not have that tool".',
|
|
148
158
|
// The counterpart to the ToolSearch bullet, and the same failure one level
|
|
149
159
|
// up: a brain that assumes it is boxed in reports "I have no access to the
|
|
150
160
|
// codebase" from a machine with the checkout, the credentials and the CLI
|
|
@@ -161,6 +171,12 @@ export function buildPrompt(run, context) {
|
|
|
161
171
|
' lives, `gh auth status`, whether the command is on PATH. A quick',
|
|
162
172
|
' look, not an audit — this run has a hard time limit and somebody',
|
|
163
173
|
' waiting at the end of it.',
|
|
174
|
+
'- "Looking" means running the command, not inspecting your own tool',
|
|
175
|
+
' list. Say you have no shell, no file access, or no checkout only',
|
|
176
|
+
' after an attempt failed and you can quote what it said. Introspection',
|
|
177
|
+
' is not evidence here: you were started with your tools unrestricted,',
|
|
178
|
+
' so a capability you cannot find in a list is far more likely to be',
|
|
179
|
+
' one you have not tried.',
|
|
164
180
|
]),
|
|
165
181
|
// Only when there is something to look at. Said unconditionally it is a
|
|
166
182
|
// paragraph of advice about a situation that does not exist, which is how a
|
package/dist/agent/run.d.ts
CHANGED
|
@@ -59,6 +59,18 @@ export interface AgentRunOptions {
|
|
|
59
59
|
maxConnections?: number;
|
|
60
60
|
/** Injectable sleep so tests don't wait out the reconnect backoff. */
|
|
61
61
|
sleepImpl?: (ms: number) => Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Injectable short-command runner, used before Codex runs to ask which MCP
|
|
64
|
+
* servers the operator's current config defines. Separate from
|
|
65
|
+
* {@link spawnImpl} because it is synchronous and captures stdout, and
|
|
66
|
+
* because a test wants to answer this question without answering the other.
|
|
67
|
+
* The second argument is the exact cwd the eventual Codex process receives;
|
|
68
|
+
* Codex discovers project-local config from it.
|
|
69
|
+
*/
|
|
70
|
+
probeImpl?: (command: string, cwd?: string) => {
|
|
71
|
+
status: number | null;
|
|
72
|
+
stdout: string;
|
|
73
|
+
};
|
|
62
74
|
/** Environment used to locate the run ledger (tests point this at a temp dir). */
|
|
63
75
|
env?: NodeJS.ProcessEnv;
|
|
64
76
|
}
|
|
@@ -81,6 +93,26 @@ export interface AgentRunMetrics {
|
|
|
81
93
|
*/
|
|
82
94
|
fatal?: string;
|
|
83
95
|
}
|
|
96
|
+
interface PreparedMcp {
|
|
97
|
+
/** Claude Code: path of the strict generated MCP config. */
|
|
98
|
+
mcpConfigPath?: string;
|
|
99
|
+
/** Codex: entries in the single verified `mcp_servers` override. */
|
|
100
|
+
codexMcpEntries?: string[];
|
|
101
|
+
/** Credentials moved out of Codex's process argv. */
|
|
102
|
+
execEnv?: NodeJS.ProcessEnv;
|
|
103
|
+
/** ProhostAI tools are actually reachable from this invocation. */
|
|
104
|
+
mcpWired: boolean;
|
|
105
|
+
/** A fail-closed preparation error. When set, the agent is never spawned. */
|
|
106
|
+
error?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Prepare a fresh Codex MCP override immediately before each invocation.
|
|
110
|
+
*
|
|
111
|
+
* No cache is safe here: Codex rereads its config per invocation and the
|
|
112
|
+
* extras file is the operator's live grant list. Fresh reads make both a new
|
|
113
|
+
* configured server and a revoked extra visible to the very next run.
|
|
114
|
+
*/
|
|
115
|
+
export declare function codexMcpEntryProvider(options: AgentRunOptions, log: (line: string) => void, workdir?: string): () => PreparedMcp;
|
|
84
116
|
/**
|
|
85
117
|
* Run the harness until the abort signal fires (or ``maxConnections`` is hit).
|
|
86
118
|
*
|
|
@@ -88,3 +120,4 @@ export interface AgentRunMetrics {
|
|
|
88
120
|
* is fatal, because retrying a rejected credential just hammers the server.
|
|
89
121
|
*/
|
|
90
122
|
export declare function runAgentHarness(options: AgentRunOptions): Promise<AgentRunMetrics>;
|
|
123
|
+
export {};
|