@prohost/cli 0.8.0 → 0.8.2
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 +35 -0
- package/README.md +6 -0
- package/dist/agent/capabilities.d.ts +59 -0
- package/dist/agent/capabilities.js +158 -0
- package/dist/agent/contract.d.ts +6 -0
- package/dist/agent/contract.js +2 -0
- package/dist/agent/prompt.d.ts +11 -0
- package/dist/agent/prompt.js +47 -4
- package/dist/agent/run.d.ts +7 -0
- package/dist/agent/run.js +9 -0
- 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,41 @@ 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.8.2
|
|
8
|
+
|
|
9
|
+
A ProhostAI back-office agent is told its cross-account reads are allowed.
|
|
10
|
+
|
|
11
|
+
- **Back-office read tools change the credential sentence.** Before each run
|
|
12
|
+
whose ProhostAI tools are wired, the harness asks the MCP server's own
|
|
13
|
+
`tools/list` (cached 10 minutes; a failure reads as "absent" and is retried
|
|
14
|
+
after a minute). When it lists both `backoffice_describe_readonly_schema` and
|
|
15
|
+
`backoffice_query_readonly` — shown only to an agent-bound key on ProhostAI's
|
|
16
|
+
internal account holding `backoffice:read` — and a read-only call to the
|
|
17
|
+
schema tool goes through (its rollout flag is enforced only at call time),
|
|
18
|
+
the prompt replaces "your
|
|
19
|
+
credential is deliberately narrow" with: read-only cross-account
|
|
20
|
+
investigation is pre-authorized, look before answering (schema, then a query
|
|
21
|
+
filtered to the customer's account), never ask a human before a read-only
|
|
22
|
+
query, writes and customer-facing sends still follow approval discipline,
|
|
23
|
+
and summarize rather than paste rows. Ungated runs are also told that
|
|
24
|
+
read-only tools on the machine (operator-added MCP servers, CLIs) serve the
|
|
25
|
+
same purpose. Every other agent's prompt is byte-for-byte unchanged.
|
|
26
|
+
|
|
27
|
+
## 0.8.1
|
|
28
|
+
|
|
29
|
+
A long message no longer reaches a paired agent as a fragment it can't recover.
|
|
30
|
+
|
|
31
|
+
- **Clipped trigger text is called out.** When the server reports that the
|
|
32
|
+
message that woke the agent was clipped (`trigger.text_truncated: true`, new
|
|
33
|
+
in the run event), the prompt says so and, when the agent has ProhostAI tools
|
|
34
|
+
and a conversation, tells it to read the full message with
|
|
35
|
+
`get_conversation_messages` before answering. An agent without tools is told
|
|
36
|
+
to say the message reached it truncated instead of answering the fragment.
|
|
37
|
+
Keyed on the explicit flag, never on a trailing `…` a person could have typed;
|
|
38
|
+
a server that predates the flag reads as "not clipped". Pairs with the server raising the excerpt
|
|
39
|
+
cap from 500 to 16,000 characters and returning full message bodies from
|
|
40
|
+
`get_conversation_messages`, so this path is now rare.
|
|
41
|
+
|
|
7
42
|
## 0.8.0
|
|
8
43
|
|
|
9
44
|
A paired agent woken as a bystander can now stay silent.
|
package/README.md
CHANGED
|
@@ -172,6 +172,12 @@ reservations or listings — those tools return a permission error. The prompt
|
|
|
172
172
|
tells the agent to say what it couldn't reach rather than answer as if it had,
|
|
173
173
|
and refused tools are printed in the run log.
|
|
174
174
|
|
|
175
|
+
The one exception is a ProhostAI back-office agent. Before a run, the CLI asks
|
|
176
|
+
the MCP server's `tools/list` (cached for ten minutes). If the list includes the
|
|
177
|
+
back-office read tools, which only an internal key with `backoffice:read` sees,
|
|
178
|
+
and a read-only call to the schema tool is allowed, the prompt tells the agent that read-only cross-account investigation is
|
|
179
|
+
pre-authorized. Writes and customer-facing sends still go through approval.
|
|
180
|
+
|
|
175
181
|
**It reads the reply out of JSON.** The run is invoked with
|
|
176
182
|
`--output-format json`, and the `result` field is what gets posted. If that
|
|
177
183
|
output isn't JSON — a login prompt, a crash banner — the run is reported failed
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the paired credential can reach, as far as the prompt needs to know.
|
|
3
|
+
*
|
|
4
|
+
* Today that is one question: does this agent hold ProhostAI's back-office
|
|
5
|
+
* read tools (`backoffice_describe_readonly_schema` + `backoffice_query_readonly`)?
|
|
6
|
+
* The answer changes what the prompt says — an agent told its credential is
|
|
7
|
+
* "deliberately narrow" refuses or hedges on exactly the cross-account
|
|
8
|
+
* investigations those tools exist for.
|
|
9
|
+
*
|
|
10
|
+
* Nothing the CLI stores can answer it. The pairing response carries no scopes,
|
|
11
|
+
* and the gate is not a scope alone: the server shows the tools only to an
|
|
12
|
+
* agent-bound key on ProhostAI's internal account, holding the literal
|
|
13
|
+
* `backoffice:read` scope, with a flag on — any of which can change after
|
|
14
|
+
* pairing. So the server is asked: `tools/list` first, and — because its
|
|
15
|
+
* visibility check leaves out the rollout flag, which is enforced only when a
|
|
16
|
+
* tool is called — then the read-only, side-effect-free
|
|
17
|
+
* `backoffice_describe_readonly_schema`, which runs the whole gate. Two small
|
|
18
|
+
* requests, cached, and every failure reads as "absent": the prompt then says
|
|
19
|
+
* what it has always said.
|
|
20
|
+
*/
|
|
21
|
+
/** Both have to be listed — the schema call is what makes the query usable. */
|
|
22
|
+
export declare const BACKOFFICE_READ_TOOLS: readonly ["backoffice_describe_readonly_schema", "backoffice_query_readonly"];
|
|
23
|
+
export interface RunCapabilities {
|
|
24
|
+
/** The back-office read tools are in this credential's `tools/list`. */
|
|
25
|
+
backofficeRead: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare const NO_CAPABILITIES: RunCapabilities;
|
|
28
|
+
/** Short: a run waits on this before its agent starts. */
|
|
29
|
+
export declare const CAPABILITY_PROBE_TIMEOUT_MS = 5000;
|
|
30
|
+
/** How long an answer is reused. A grant or revoke shows up within this. */
|
|
31
|
+
export declare const CAPABILITY_CACHE_MS: number;
|
|
32
|
+
/** How long a failed probe is reused, so an outage costs one timeout a minute, not one per run. */
|
|
33
|
+
export declare const CAPABILITY_FAILURE_CACHE_MS = 60000;
|
|
34
|
+
export interface ToolListOptions {
|
|
35
|
+
mcpUrl: string;
|
|
36
|
+
apiKey: string;
|
|
37
|
+
fetchImpl?: typeof fetch;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/** The tool names the server lists for this key, or `null` when it could not be asked. */
|
|
41
|
+
export declare function listMcpToolNames(options: ToolListOptions): Promise<string[] | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a back-office read actually goes through for this key: `true` when
|
|
44
|
+
* the schema call answers with its schema, `false` when the server refuses it
|
|
45
|
+
* (`backoffice_forbidden` — e.g. the rollout flag is off), `null` when it could
|
|
46
|
+
* not be asked.
|
|
47
|
+
*/
|
|
48
|
+
export declare function backofficeReadAllowed(options: ToolListOptions): Promise<boolean | null>;
|
|
49
|
+
export declare function capabilitiesFromToolNames(names: readonly string[]): RunCapabilities;
|
|
50
|
+
/**
|
|
51
|
+
* A cached `tools/list` probe for one daemon's credential.
|
|
52
|
+
*
|
|
53
|
+
* A daemon is long-lived, so the answer is re-asked every
|
|
54
|
+
* {@link CAPABILITY_CACHE_MS} rather than fixed at start-up: a scope granted or
|
|
55
|
+
* revoked on the server reaches the prompt without a restart.
|
|
56
|
+
*/
|
|
57
|
+
export declare function capabilityProbe(options: ToolListOptions & {
|
|
58
|
+
now?: () => number;
|
|
59
|
+
}): () => Promise<RunCapabilities>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the paired credential can reach, as far as the prompt needs to know.
|
|
3
|
+
*
|
|
4
|
+
* Today that is one question: does this agent hold ProhostAI's back-office
|
|
5
|
+
* read tools (`backoffice_describe_readonly_schema` + `backoffice_query_readonly`)?
|
|
6
|
+
* The answer changes what the prompt says — an agent told its credential is
|
|
7
|
+
* "deliberately narrow" refuses or hedges on exactly the cross-account
|
|
8
|
+
* investigations those tools exist for.
|
|
9
|
+
*
|
|
10
|
+
* Nothing the CLI stores can answer it. The pairing response carries no scopes,
|
|
11
|
+
* and the gate is not a scope alone: the server shows the tools only to an
|
|
12
|
+
* agent-bound key on ProhostAI's internal account, holding the literal
|
|
13
|
+
* `backoffice:read` scope, with a flag on — any of which can change after
|
|
14
|
+
* pairing. So the server is asked: `tools/list` first, and — because its
|
|
15
|
+
* visibility check leaves out the rollout flag, which is enforced only when a
|
|
16
|
+
* tool is called — then the read-only, side-effect-free
|
|
17
|
+
* `backoffice_describe_readonly_schema`, which runs the whole gate. Two small
|
|
18
|
+
* requests, cached, and every failure reads as "absent": the prompt then says
|
|
19
|
+
* what it has always said.
|
|
20
|
+
*/
|
|
21
|
+
import { USER_AGENT } from '../version.js';
|
|
22
|
+
/** Both have to be listed — the schema call is what makes the query usable. */
|
|
23
|
+
export const BACKOFFICE_READ_TOOLS = ['backoffice_describe_readonly_schema', 'backoffice_query_readonly'];
|
|
24
|
+
export const NO_CAPABILITIES = { backofficeRead: false };
|
|
25
|
+
/** Short: a run waits on this before its agent starts. */
|
|
26
|
+
export const CAPABILITY_PROBE_TIMEOUT_MS = 5_000;
|
|
27
|
+
/** How long an answer is reused. A grant or revoke shows up within this. */
|
|
28
|
+
export const CAPABILITY_CACHE_MS = 10 * 60_000;
|
|
29
|
+
/** How long a failed probe is reused, so an outage costs one timeout a minute, not one per run. */
|
|
30
|
+
export const CAPABILITY_FAILURE_CACHE_MS = 60_000;
|
|
31
|
+
/** Cap on the body read — a full catalog is well under this. */
|
|
32
|
+
const MAX_BODY_CHARS = 2_000_000;
|
|
33
|
+
/**
|
|
34
|
+
* Pull the JSON-RPC result out of a response that may be plain JSON or a
|
|
35
|
+
* streamable-HTTP SSE stream (`data: {...}` lines). `undefined` when neither
|
|
36
|
+
* shape holds a result.
|
|
37
|
+
*/
|
|
38
|
+
function jsonRpcResult(body) {
|
|
39
|
+
const candidates = body.trimStart().startsWith('{')
|
|
40
|
+
? [body]
|
|
41
|
+
: body
|
|
42
|
+
.split(/\r?\n/)
|
|
43
|
+
.filter((line) => line.startsWith('data:'))
|
|
44
|
+
.map((line) => line.slice('data:'.length).trim());
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(candidate);
|
|
48
|
+
if (parsed && typeof parsed.result === 'object' && parsed.result !== null) {
|
|
49
|
+
return parsed.result;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Not JSON — keep looking.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One JSON-RPC request to the ProhostAI MCP server; its `result`, or `null`
|
|
60
|
+
* when it could not be asked (network, timeout, non-2xx, unparseable, error).
|
|
61
|
+
*
|
|
62
|
+
* Authenticates exactly as the brain's own MCP config does (`X-API-Key`), so
|
|
63
|
+
* the answer is what the agent itself will see. The server runs stateless
|
|
64
|
+
* HTTP, so no `initialize` round-trip is needed first.
|
|
65
|
+
*/
|
|
66
|
+
async function mcpRequest(options, method, params) {
|
|
67
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? CAPABILITY_PROBE_TIMEOUT_MS);
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetchImpl(options.mcpUrl, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
Accept: 'application/json, text/event-stream',
|
|
76
|
+
'X-API-Key': options.apiKey,
|
|
77
|
+
'User-Agent': USER_AGENT,
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
if (response.status < 200 || response.status >= 300)
|
|
83
|
+
return null;
|
|
84
|
+
return jsonRpcResult((await response.text()).slice(0, MAX_BODY_CHARS)) ?? null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** The tool names the server lists for this key, or `null` when it could not be asked. */
|
|
94
|
+
export async function listMcpToolNames(options) {
|
|
95
|
+
const tools = (await mcpRequest(options, 'tools/list', {}))?.tools;
|
|
96
|
+
if (!Array.isArray(tools))
|
|
97
|
+
return null;
|
|
98
|
+
return tools
|
|
99
|
+
.map((tool) => (tool && typeof tool === 'object' ? tool.name : undefined))
|
|
100
|
+
.filter((name) => typeof name === 'string');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether a back-office read actually goes through for this key: `true` when
|
|
104
|
+
* the schema call answers with its schema, `false` when the server refuses it
|
|
105
|
+
* (`backoffice_forbidden` — e.g. the rollout flag is off), `null` when it could
|
|
106
|
+
* not be asked.
|
|
107
|
+
*/
|
|
108
|
+
export async function backofficeReadAllowed(options) {
|
|
109
|
+
const result = await mcpRequest(options, 'tools/call', {
|
|
110
|
+
name: 'backoffice_describe_readonly_schema',
|
|
111
|
+
arguments: {},
|
|
112
|
+
});
|
|
113
|
+
if (!result)
|
|
114
|
+
return null;
|
|
115
|
+
if (result.isError === true)
|
|
116
|
+
return false;
|
|
117
|
+
const content = Array.isArray(result.content) ? result.content : [];
|
|
118
|
+
const text = content
|
|
119
|
+
.map((part) => (part && typeof part === 'object' ? part.text : undefined))
|
|
120
|
+
.filter((part) => typeof part === 'string')
|
|
121
|
+
.join('');
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(text);
|
|
124
|
+
return parsed.error === undefined && parsed.tables !== undefined;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function capabilitiesFromToolNames(names) {
|
|
131
|
+
return { backofficeRead: BACKOFFICE_READ_TOOLS.every((tool) => names.includes(tool)) };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A cached `tools/list` probe for one daemon's credential.
|
|
135
|
+
*
|
|
136
|
+
* A daemon is long-lived, so the answer is re-asked every
|
|
137
|
+
* {@link CAPABILITY_CACHE_MS} rather than fixed at start-up: a scope granted or
|
|
138
|
+
* revoked on the server reaches the prompt without a restart.
|
|
139
|
+
*/
|
|
140
|
+
export function capabilityProbe(options) {
|
|
141
|
+
const now = options.now ?? Date.now;
|
|
142
|
+
let cached;
|
|
143
|
+
return async () => {
|
|
144
|
+
if (cached && now() < cached.until)
|
|
145
|
+
return cached.value;
|
|
146
|
+
const names = await listMcpToolNames(options);
|
|
147
|
+
let value = names ? capabilitiesFromToolNames(names) : NO_CAPABILITIES;
|
|
148
|
+
let answered = names !== null;
|
|
149
|
+
if (value.backofficeRead) {
|
|
150
|
+
// Listed is not the same as allowed: confirm the gate end to end.
|
|
151
|
+
const allowed = await backofficeReadAllowed(options);
|
|
152
|
+
answered = allowed !== null;
|
|
153
|
+
value = { backofficeRead: allowed === true };
|
|
154
|
+
}
|
|
155
|
+
cached = { value, until: now() + (answered ? CAPABILITY_CACHE_MS : CAPABILITY_FAILURE_CACHE_MS) };
|
|
156
|
+
return value;
|
|
157
|
+
};
|
|
158
|
+
}
|
package/dist/agent/contract.d.ts
CHANGED
|
@@ -235,6 +235,12 @@ export interface AgentRunRequest {
|
|
|
235
235
|
trigger_message_id?: string;
|
|
236
236
|
/** Capped inline excerpt of the message that woke the agent. */
|
|
237
237
|
trigger_text?: string;
|
|
238
|
+
/**
|
|
239
|
+
* The server clipped `trigger_text` (`trigger.text_truncated`). Explicit
|
|
240
|
+
* metadata rather than a guess from a trailing ellipsis, which a person can
|
|
241
|
+
* also type. Absent on servers that predate it — read as "not clipped".
|
|
242
|
+
*/
|
|
243
|
+
trigger_text_truncated?: boolean;
|
|
238
244
|
/**
|
|
239
245
|
* Images and files on the message that woke the agent.
|
|
240
246
|
*
|
package/dist/agent/contract.js
CHANGED
|
@@ -337,6 +337,8 @@ export function parseRunRequest(data) {
|
|
|
337
337
|
trigger_source: str(trigger.source) ?? str(data.trigger_source),
|
|
338
338
|
trigger_message_id: str(trigger.message_id),
|
|
339
339
|
trigger_text: str(trigger.text_excerpt),
|
|
340
|
+
// Only an explicit `true` counts: anything else is an unclipped message.
|
|
341
|
+
trigger_text_truncated: trigger.text_truncated === true,
|
|
340
342
|
trigger_attachments: attachmentList(trigger.attachments ?? data.trigger_attachments),
|
|
341
343
|
trigger_user_id: str(trigger.user_id),
|
|
342
344
|
trigger_brief: brief(trigger.brief),
|
package/dist/agent/prompt.d.ts
CHANGED
|
@@ -30,6 +30,17 @@ export interface PromptContext {
|
|
|
30
30
|
* to guess at it.
|
|
31
31
|
*/
|
|
32
32
|
mcpToolPattern?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The ProhostAI tools include the back-office read pair
|
|
35
|
+
* (`backoffice_describe_readonly_schema` → `backoffice_query_readonly`).
|
|
36
|
+
*
|
|
37
|
+
* Taken from the server's own `tools/list` for this key (see
|
|
38
|
+
* `capabilities.ts`), never assumed. It changes what the prompt says about
|
|
39
|
+
* the credential: an agent told its access is "deliberately narrow" refused
|
|
40
|
+
* or asked permission for exactly the cross-account reads these tools
|
|
41
|
+
* pre-authorize. Unset or false, the prompt is byte-for-byte what it was.
|
|
42
|
+
*/
|
|
43
|
+
backofficeRead?: boolean;
|
|
33
44
|
/**
|
|
34
45
|
* Tools are behind permission prompts this run (`--safe-tools`).
|
|
35
46
|
*
|
package/dist/agent/prompt.js
CHANGED
|
@@ -207,6 +207,23 @@ export function buildPrompt(run, context) {
|
|
|
207
207
|
// The server caps this excerpt, so say so rather than let an agent assume
|
|
208
208
|
// it has the whole message.
|
|
209
209
|
lines.push('Message that woke you (may be truncated by the server):', run.trigger_text, '');
|
|
210
|
+
// An agent that is only told the text "may be" truncated answers the
|
|
211
|
+
// fragment — or asks the sender to repost — when the whole message is one
|
|
212
|
+
// tool call away. Keyed on the server's explicit flag, never on a trailing
|
|
213
|
+
// ellipsis a person could have typed.
|
|
214
|
+
if (run.trigger_text_truncated) {
|
|
215
|
+
lines.push(...(context.mcpServerName && run.conversation_id
|
|
216
|
+
? [
|
|
217
|
+
'ProhostAI cut that message short. Before answering, read it in full with',
|
|
218
|
+
`the ProhostAI get_conversation_messages tool (conversation ${run.conversation_id})`,
|
|
219
|
+
'and answer the whole message, not the excerpt above.',
|
|
220
|
+
]
|
|
221
|
+
: [
|
|
222
|
+
'ProhostAI cut that message short, and you have no tool to read the rest.',
|
|
223
|
+
'Answer what you can see and say plainly that the message reached you',
|
|
224
|
+
'truncated.',
|
|
225
|
+
]), '');
|
|
226
|
+
}
|
|
210
227
|
}
|
|
211
228
|
else if (triggerAttachments.length > 0) {
|
|
212
229
|
// Distinct from "the server didn't send the text": here the message really
|
|
@@ -311,10 +328,13 @@ export function buildPrompt(run, context) {
|
|
|
311
328
|
context.mcpToolPattern
|
|
312
329
|
? ` (${context.mcpToolPattern})`
|
|
313
330
|
: `, from the MCP server registered as "${context.mcpServerName}"`}. They are the sanctioned ` +
|
|
314
|
-
'way to look up workspace data, so use them rather than guessing.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
'way to look up workspace data, so use them rather than guessing. ' +
|
|
332
|
+
(context.backofficeRead
|
|
333
|
+
? backofficeGuidance(Boolean(context.toolsGated))
|
|
334
|
+
: 'Your credential is ' +
|
|
335
|
+
'deliberately narrow and does not reach every part of the workspace: if a tool returns ' +
|
|
336
|
+
'a permission error, that is a real limit rather than a glitch — say what you could not ' +
|
|
337
|
+
'look up instead of retrying it or answering as if you had.')
|
|
318
338
|
: '- You have no ProhostAI tools in this run. Everything you know about this ' +
|
|
319
339
|
'workspace is what is written above.', '');
|
|
320
340
|
// Postability is a separate question from what the run is about: a task run
|
|
@@ -334,6 +354,29 @@ export function buildPrompt(run, context) {
|
|
|
334
354
|
: 'stdout is recorded but not posted anywhere — act through your own tools.');
|
|
335
355
|
return `${lines.join('\n')}\n`;
|
|
336
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* What replaces the "deliberately narrow" sentence for a back-office agent.
|
|
359
|
+
*
|
|
360
|
+
* Field-observed 2026-10-01: an internal support agent holding these tools
|
|
361
|
+
* answered "I couldn't read the workflow" and asked a human before a read-only
|
|
362
|
+
* query, because the only thing the prompt said about its access was that it
|
|
363
|
+
* was narrow. The read is pre-authorized, so the prompt says so — and keeps
|
|
364
|
+
* writes, customer-facing sends and personal data exactly as guarded as before.
|
|
365
|
+
*/
|
|
366
|
+
function backofficeGuidance(toolsGated) {
|
|
367
|
+
return ('You are a ProhostAI back-office agent: read-only cross-account investigation is ' +
|
|
368
|
+
'pre-authorized and expected. When a support or engineering question is about a ' +
|
|
369
|
+
'specific customer account, look before you answer — backoffice_describe_readonly_schema ' +
|
|
370
|
+
'for the tables, then backoffice_query_readonly filtered to that customer\'s account. ' +
|
|
371
|
+
'Never ask a human for permission to run a read-only query. ' +
|
|
372
|
+
(toolsGated
|
|
373
|
+
? ''
|
|
374
|
+
: 'Read-only investigation tools on this machine — MCP servers your operator added, ' +
|
|
375
|
+
'CLIs such as `gh` — are usable for the same purpose. ') +
|
|
376
|
+
'Writes and anything sent to a customer still follow approval discipline. Keep personal ' +
|
|
377
|
+
'data and secrets out of your reply: summarize what you found, do not paste rows. If a ' +
|
|
378
|
+
'tool returns a permission error, that is a real limit — say what you could not look up.');
|
|
379
|
+
}
|
|
337
380
|
/**
|
|
338
381
|
* Normalize an agent's stdout into a postable message.
|
|
339
382
|
*
|
package/dist/agent/run.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { spawn } from 'node:child_process';
|
|
|
26
26
|
import WebSocket from 'ws';
|
|
27
27
|
import type { CommandRunner } from './accounts.js';
|
|
28
28
|
import type { AgentCredentials } from './credentials.js';
|
|
29
|
+
import type { RunCapabilities } from './capabilities.js';
|
|
29
30
|
import type { QuotaProvider } from './runtime_status.js';
|
|
30
31
|
/**
|
|
31
32
|
* Default ceiling on one agent invocation: none.
|
|
@@ -144,6 +145,12 @@ export interface AgentRunOptions {
|
|
|
144
145
|
* `codex login status`). Tests inject a fake so no real CLI is asked.
|
|
145
146
|
*/
|
|
146
147
|
accountCommandRunner?: CommandRunner;
|
|
148
|
+
/**
|
|
149
|
+
* Asks what the paired credential can reach (see `capabilities.ts`).
|
|
150
|
+
* Defaults to a cached `tools/list` against the pairing's MCP endpoint; tests
|
|
151
|
+
* inject a fake so no extra request lands in their recorded calls.
|
|
152
|
+
*/
|
|
153
|
+
capabilityProbe?: () => Promise<RunCapabilities>;
|
|
147
154
|
}
|
|
148
155
|
export interface AgentRunMetrics {
|
|
149
156
|
connections: number;
|
package/dist/agent/run.js
CHANGED
|
@@ -38,6 +38,7 @@ import { ensureWorkspace, streamUrlFor, workspacePath } from './credentials.js';
|
|
|
38
38
|
import { execAgent } from './exec.js';
|
|
39
39
|
import { HeartbeatLoop } from './heartbeat.js';
|
|
40
40
|
import { buildPrompt, replyFromStdout } from './prompt.js';
|
|
41
|
+
import { NO_CAPABILITIES, capabilityProbe } from './capabilities.js';
|
|
41
42
|
import { RunStore } from './runstore.js';
|
|
42
43
|
import { RUNTIME_STATUS_FEATURE, RuntimeStatusReporter, claudeQuotaProvider, codexConfiguredModel, codexQuotaProvider, modelFromExec, parseVersion, readCodexObservation, } from './runtime_status.js';
|
|
43
44
|
import { SessionStore, sessionKeyFor } from './sessionstore.js';
|
|
@@ -255,6 +256,7 @@ function buildRuntime(options, log) {
|
|
|
255
256
|
brain,
|
|
256
257
|
skipPermissions,
|
|
257
258
|
prepareMcp: () => ({ mcpWired: false }),
|
|
259
|
+
capabilities: async () => NO_CAPABILITIES,
|
|
258
260
|
runtimeName,
|
|
259
261
|
observe,
|
|
260
262
|
model,
|
|
@@ -336,6 +338,10 @@ function buildRuntime(options, log) {
|
|
|
336
338
|
sessions,
|
|
337
339
|
brain,
|
|
338
340
|
prepareMcp: prepareWithAccount,
|
|
341
|
+
capabilities: options.capabilityProbe ??
|
|
342
|
+
(mcpUrl
|
|
343
|
+
? capabilityProbe({ mcpUrl, apiKey: options.credentials.api_key, fetchImpl: options.fetchImpl })
|
|
344
|
+
: async () => NO_CAPABILITIES),
|
|
339
345
|
skipPermissions,
|
|
340
346
|
runtimeName,
|
|
341
347
|
observe,
|
|
@@ -841,6 +847,8 @@ async function handleRun(run, options, runtime, api, log, sleep, control) {
|
|
|
841
847
|
const surface = run.conversation_id ?? run.kind ?? 'no conversation';
|
|
842
848
|
log(`${timestamp()} ▶ ${label} started (${surface})`);
|
|
843
849
|
const mcp = runtime.prepareMcp();
|
|
850
|
+
// Only worth asking when the tools it describes are wired into this run.
|
|
851
|
+
const capabilities = mcp.mcpWired ? await runtime.capabilities() : NO_CAPABILITIES;
|
|
844
852
|
const prompt = buildPrompt(run, {
|
|
845
853
|
agentName: options.credentials.agent?.name ?? 'your agent',
|
|
846
854
|
// Named only when the tools are actually reachable from this invocation.
|
|
@@ -852,6 +860,7 @@ async function handleRun(run, options, runtime, api, log, sleep, control) {
|
|
|
852
860
|
// `plain` command is still told the server name only: we have no idea what
|
|
853
861
|
// it is. See PromptContext.mcpToolPattern.
|
|
854
862
|
mcpToolPattern: runtime.brain !== 'plain' && mcp.mcpWired ? MCP_TOOL_PATTERN : undefined,
|
|
863
|
+
backofficeRead: capabilities.backofficeRead,
|
|
855
864
|
toolsGated: !runtime.skipPermissions,
|
|
856
865
|
// The real numbers, not a vague "there is a limit" — see PromptContext.
|
|
857
866
|
timeoutMs: options.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS,
|
package/dist/version.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export declare const CLI_VERSION = "0.8.
|
|
10
|
+
export declare const CLI_VERSION = "0.8.2";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.8.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.8.2";
|
package/dist/version.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export const CLI_VERSION = '0.8.
|
|
10
|
+
export const CLI_VERSION = '0.8.2';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|