agentradar-cli 0.3.7 → 0.3.8
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/dist/channel.js +1 -1
- package/dist/tool-run.js +35 -12
- package/package.json +1 -1
package/dist/channel.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
71
71
|
});
|
|
72
72
|
await noteCliUpdate(response.headers, logError);
|
|
73
73
|
if (!response.ok) {
|
|
74
|
-
const said =
|
|
74
|
+
const said = await response.json().then((body) => body.error?.message).catch(() => null);
|
|
75
75
|
throw new Error(said ?? `AgentRadar refused the channel call (${response.status})`);
|
|
76
76
|
}
|
|
77
77
|
const text = await response.text();
|
package/dist/tool-run.js
CHANGED
|
@@ -129,6 +129,23 @@ export function parseCallArgs(rest) {
|
|
|
129
129
|
const pathOf = (tool) => typeof tool._meta?.cli?.path === "string" ? tool._meta.cli.path : undefined;
|
|
130
130
|
const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
131
131
|
const firstSentence = (text = "") => text.split(/(?<=[.!?])\s/)[0].trim();
|
|
132
|
+
// Vadym 14075: a hosted agent tried ~10 key sets for approvalDetails on request_egress and never found the real ones,
|
|
133
|
+
// so --help spells out a JSON argument's fields and these commands carry one complete example each.
|
|
134
|
+
// The approvals and browser skills quote this output word for word (tool-run.test.ts checks it).
|
|
135
|
+
const APPROVAL_EXAMPLES = {
|
|
136
|
+
request_egress: `--host supplier.example.com --reason 'The price list task needs this supplier website.' --approval-details '{"context":"The price list task needs prices from a supplier website.","reason":"No connector has these prices, and the website is the only source.","ifDeclined":"The price list stays unfinished.","decisionDetail":"Let my browser reach supplier.example.com."}'`,
|
|
137
|
+
request_spend_limit: `--monthly-limit-cents 5000 --reason 'The weekly reports need more AI time.' --approval-details '{"context":"I used most of my monthly AI limit by the 20th.","reason":"The weekly reports added this month need more AI time.","ifDeclined":"I pause the weekly reports until next month.","decisionDetail":"Raise my monthly AI limit to $50."}'`,
|
|
138
|
+
request_hire: `--title 'Support agent' --approval-details '{"context":"Customer emails wait two days for an answer.","reason":"Nobody on the team has time to answer them.","ifDeclined":"Emails keep waiting and some customers may leave.","decisionDetail":"Hire one support agent to answer customer emails."}'`,
|
|
139
|
+
request_permission: `--area projects --op write --approval-details '{"context":"I was asked to create a project for the spring launch.","reason":"My role can read projects but not create them.","ifDeclined":"Someone else has to create the project.","decisionDetail":"Let my role create and edit projects."}'`,
|
|
140
|
+
};
|
|
141
|
+
/** ` context (required, 1-600 characters): What happened: ...` for each field of a JSON argument. */
|
|
142
|
+
const fieldLines = (name, schema, root) => [`--${kebab(name)} is JSON with these fields:`,
|
|
143
|
+
...Object.entries(schema.properties ?? {}).map(([field, own]) => {
|
|
144
|
+
// A field reusing another's rule arrives as {"$ref": "#/properties/approvalDetails/properties/context"} plus its own description.
|
|
145
|
+
const f = { ...own.$ref?.split("/").slice(1).reduce((at, key) => at?.[key], root), ...own };
|
|
146
|
+
const size = f.minLength !== undefined && f.maxLength !== undefined ? `, ${f.minLength}-${f.maxLength} characters` : "";
|
|
147
|
+
return ` ${field} (${schema.required?.includes(field) ? "required" : "optional"}${size})${f.description ? `: ${f.description}` : ""}`;
|
|
148
|
+
})];
|
|
132
149
|
async function listedTools(runnerId, deps) {
|
|
133
150
|
const listed = await awpRequest(runnerId, "tools/list", {}, deps);
|
|
134
151
|
return listed ? listed.result?.tools ?? [] : null;
|
|
@@ -179,6 +196,8 @@ function leafHelp(tool) {
|
|
|
179
196
|
"USAGE", ` agentradar ${path}${required.map((name) => ` --${kebab(name)} ${typeOf(properties[name] ?? {})}`).join("")}${optional.length ? " [flags]" : ""}`,
|
|
180
197
|
...(required.length ? ["REQUIRED", ...required.filter((name) => name in properties).map(flag)] : []),
|
|
181
198
|
"FLAGS", ...optional.map(flag), " --dry-run print the exact call and send nothing --json raw JSON --runner <id> the runner (default $AR_CLI_RUNNER_ID)",
|
|
199
|
+
...Object.entries(properties).flatMap(([name, schema]) => schema.properties ? fieldLines(name, schema, tool.inputSchema) : []),
|
|
200
|
+
...(tool.name && APPROVAL_EXAMPLES[tool.name] ? [`Example: agentradar ${path} ${APPROVAL_EXAMPLES[tool.name]}`] : []),
|
|
182
201
|
EXIT_CODES,
|
|
183
202
|
].join("\n");
|
|
184
203
|
}
|
|
@@ -194,8 +213,10 @@ async function flagArguments(entry, flags, deps) {
|
|
|
194
213
|
for (let [flag, value] of flags) {
|
|
195
214
|
// --body-file page.md: the argument read from a file, so a multi-line page never sits in a quoted
|
|
196
215
|
// shell argument (Claude Code prompts on a newline followed by # inside one).
|
|
216
|
+
// --content-base64-file page.html: a *Base64 argument gets the file's bytes as base64, any type (ANY-FILE-SEND).
|
|
197
217
|
if (flag.endsWith("-file") && !(flag in schema) && !(camel(flag) in schema)) {
|
|
198
|
-
const
|
|
218
|
+
const binary = camel(flag.slice(0, -"-file".length)).endsWith("Base64");
|
|
219
|
+
const text = await readArgumentFile(value, deps.cwd ?? process.cwd(), deps.home ?? homedir(), binary).catch((error) => error);
|
|
199
220
|
if (text instanceof Error) {
|
|
200
221
|
logError(`--${flag}: ${text.message}`);
|
|
201
222
|
return null;
|
|
@@ -222,7 +243,9 @@ async function flagArguments(entry, flags, deps) {
|
|
|
222
243
|
}
|
|
223
244
|
const typed = coerce(type, value);
|
|
224
245
|
if (typed === undefined) {
|
|
225
|
-
logError(
|
|
246
|
+
logError(schema[key].properties
|
|
247
|
+
? `--${flag} must be JSON with the fields ${(schema[key].required ?? Object.keys(schema[key].properties)).join(", ")}; \`agentradar ${pathOf(entry) ?? `call <runner-id> ${entry.name}`} --help\` shows each one.`
|
|
248
|
+
: `--${flag} must be a ${type}`);
|
|
226
249
|
return null;
|
|
227
250
|
}
|
|
228
251
|
out[key] = typed;
|
|
@@ -255,21 +278,21 @@ function listFlagError(flag, itemType, schema) {
|
|
|
255
278
|
}
|
|
256
279
|
/**
|
|
257
280
|
* `agentradar run` is always-allowed, so a file flag must not read and upload any file the agent names:
|
|
258
|
-
* only a regular file inside the working folder (after symlinks), and at most 256 KB — and never one in
|
|
281
|
+
* only a regular file inside the working folder (after symlinks), and at most 256 KB (7 MB as base64) — and never one in
|
|
259
282
|
* ~/.agentradar, which holds the runner's own key even when the working folder is $HOME.
|
|
260
283
|
* ponytail: a hard link to the key is not caught (same inode, different path); making one is the agent's own act.
|
|
261
284
|
*/
|
|
262
|
-
async function readArgumentFile(path, cwd, home) {
|
|
285
|
+
async function readArgumentFile(path, cwd, home, binary = false) {
|
|
263
286
|
const real = await realpath(resolve(cwd, path)).catch(() => { throw new Error(`cannot read ${path}`); });
|
|
264
287
|
const within = (dir) => { const inside = relative(dir, real); return inside !== ".." && !inside.startsWith(`..${sep}`) && !isAbsolute(inside); };
|
|
265
288
|
const own = await realpath(join(home, ".agentradar")).catch(() => null);
|
|
266
289
|
if (own && within(own))
|
|
267
290
|
throw new Error(`${path} is refused: a file flag never reads AgentRadar's own folder (~/.agentradar), which holds this agent's key`);
|
|
268
291
|
const info = await stat(real);
|
|
269
|
-
if (!info.isFile() || !within(await realpath(cwd)) || info.size > 256 * 1024) {
|
|
270
|
-
throw new Error(`${path} is refused: a file flag reads only a regular file inside the current folder, up to 256 KB`);
|
|
292
|
+
if (!info.isFile() || !within(await realpath(cwd)) || info.size > (binary ? 7 * 1024 * 1024 : 256 * 1024)) {
|
|
293
|
+
throw new Error(`${path} is refused: a file flag reads only a regular file inside the current folder, up to ${binary ? "7 MB" : "256 KB"}`);
|
|
271
294
|
}
|
|
272
|
-
return await readFile(real, "utf8");
|
|
295
|
+
return binary ? (await readFile(real)).toString("base64") : await readFile(real, "utf8");
|
|
273
296
|
}
|
|
274
297
|
function coerce(type, value) {
|
|
275
298
|
if (type === "string")
|
|
@@ -339,7 +362,8 @@ export async function callAwpTool(runnerId, name, toolArguments, deps) {
|
|
|
339
362
|
// A refusal is the server's sentence and belongs on stderr with a non-zero exit, or a script that
|
|
340
363
|
// pipes stdout reads "the tool ran" from a body that says it was refused.
|
|
341
364
|
if (body.result?.isError) {
|
|
342
|
-
|
|
365
|
+
// The server's JSON, then its plain "AgentRadar refused <tool>: ... Now: ..." line when it sends one.
|
|
366
|
+
logError([said, ...(body.result.content ?? []).slice(1).map((c) => c.text).filter((t) => typeof t === "string")].join("\n"));
|
|
343
367
|
return 1;
|
|
344
368
|
}
|
|
345
369
|
log(withCliUpdate(said));
|
|
@@ -415,15 +439,14 @@ async function postAwp(url, bearer, method, params, deps) {
|
|
|
415
439
|
});
|
|
416
440
|
await noteCliUpdate(response.headers, logError);
|
|
417
441
|
if (!response.ok) {
|
|
418
|
-
// 426
|
|
419
|
-
const said =
|
|
420
|
-
// 401: the server says only "unauthorized" for every cause, so a revoked key and an ended hire read the same here.
|
|
442
|
+
// 426 says how to upgrade; 401 says why the key was refused and how to re-join.
|
|
443
|
+
const said = await response.json().then((body) => body.error?.message).catch(() => null);
|
|
421
444
|
logError(said ?? (response.status === 401 ? UNAUTHORIZED : `AgentRadar refused the tool call (${response.status}).`));
|
|
422
445
|
return null;
|
|
423
446
|
}
|
|
424
447
|
const body = await response.json();
|
|
425
448
|
if (body.error) {
|
|
426
|
-
logError(typeof body.error.message === "string" ? body.error.message :
|
|
449
|
+
logError(typeof body.error.message === "string" ? body.error.message : `AgentRadar returned an error to ${method === "tools/call" ? String(params.name) : method}. Now: run \`agentradar upgrade\`, then try again.`);
|
|
427
450
|
return null;
|
|
428
451
|
}
|
|
429
452
|
return body;
|