agentradar-cli 0.3.8 → 0.3.10
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/admin.js +27 -7
- package/dist/agent-guard/hooks/hooks.json +3 -1
- package/dist/agent-guard/scripts/compaction-report.mjs +13 -7
- package/dist/agent-guard/scripts/guard.mjs +52 -20
- package/dist/agent-guard/scripts/model-switch.mjs +3 -2
- package/dist/agent-guard/scripts/rm-guard.mjs +122 -0
- package/dist/agent-guard/skills/agentradar-discipline/SKILL.md +13 -8
- package/dist/agentradar-vendor/SKILL.md +3 -2
- package/dist/asset-get.js +34 -0
- package/dist/channel.js +131 -25
- package/dist/github.js +209 -0
- package/dist/index.js +42 -11
- package/dist/setup.js +26 -0
- package/dist/skill-install.js +13 -23
- package/dist/tool-run.js +272 -63
- package/dist/vault-import.js +7 -2
- package/dist/vendor-runner/feeder.mjs +161 -13
- package/dist/vendor-runner/session-frame.mjs +123 -19
- package/dist/vendor-runner/supervisor/supervise-agent.sh +6 -1
- package/dist/vendor-runner/terminal-bridge.mjs +39 -3
- package/package.json +4 -2
package/dist/setup.js
CHANGED
|
@@ -702,6 +702,26 @@ async function callerSession(deps, env, home, projectPath) {
|
|
|
702
702
|
* restarts the service) instead of filing a second request. The key is written only into the 0600 runner
|
|
703
703
|
* env file and is never printed.
|
|
704
704
|
*/
|
|
705
|
+
/**
|
|
706
|
+
* UAT-30 (Vadym 13405): a join names the runtime it runs on. `--runtime claude|codex|other`, else what the shell
|
|
707
|
+
* itself proves (AGENTRADAR_AGENT_BIN, CLAUDECODE, CODEX_HOME). Nothing is assumed: an unknown or unsupported one is
|
|
708
|
+
* refused before any request, with the command to run next.
|
|
709
|
+
*/
|
|
710
|
+
export function joinRuntime(flag, env) {
|
|
711
|
+
const map = { claude: "claude", codex: "codex", other: "generic" };
|
|
712
|
+
if (flag !== undefined) {
|
|
713
|
+
const key = flag.trim().toLowerCase();
|
|
714
|
+
const harness = Object.hasOwn(map, key) ? map[key] : undefined;
|
|
715
|
+
return harness ? { harness } : { refusal: `Unsupported runtime ${JSON.stringify(flag)}: AgentRadar joins Claude Code (claude), Codex (codex) or another CLI you launch yourself (other). Nothing was requested. Now: run the same command again with --runtime other and AGENTRADAR_AGENT_BIN set to your CLI's command, or ask your company which runtime it expects.` };
|
|
716
|
+
}
|
|
717
|
+
if (env.AGENTRADAR_AGENT_BIN?.trim())
|
|
718
|
+
return { harness: "generic" };
|
|
719
|
+
if (env.CLAUDECODE)
|
|
720
|
+
return { harness: "claude" };
|
|
721
|
+
if (env.CODEX_HOME)
|
|
722
|
+
return { harness: "codex" };
|
|
723
|
+
return { refusal: "AgentRadar cannot tell which runtime you run on, so nothing was requested. Now: run the same command again with --runtime claude (Claude Code), --runtime codex (Codex) or --runtime other (any other CLI; set AGENTRADAR_AGENT_BIN to its command first). Use the one that is true for you; do not guess." };
|
|
724
|
+
}
|
|
705
725
|
export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
706
726
|
const fetchImpl = deps.fetch ?? fetch;
|
|
707
727
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -858,6 +878,12 @@ and AR_CLI_RUNNER_ID=${runnerId} in its environment. Accept the one-time develop
|
|
|
858
878
|
return startJoined(state.requestId, state.agentId, { mcpEndpoint: envValue(saved, "AWP_MCP_URL") ?? mcpEndpoint, bearer }, approvedBypass, bypass);
|
|
859
879
|
}
|
|
860
880
|
if (!state) {
|
|
881
|
+
// index.ts skips the runtime gate for --revoke-bypass (it edits an earlier join); a NEW join still needs a proven runtime.
|
|
882
|
+
if (deps.revokeBypass && !deps.harness) {
|
|
883
|
+
const proven = joinRuntime(undefined, env);
|
|
884
|
+
if ("refusal" in proven)
|
|
885
|
+
throw new Error(proven.refusal);
|
|
886
|
+
}
|
|
861
887
|
const agentName = (deps.agentName ?? process.env.AGENTRADAR_AGENT_NAME ?? basename(cwd)).trim();
|
|
862
888
|
if (!agentName)
|
|
863
889
|
throw new Error("Run join from the agent project folder");
|
package/dist/skill-install.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmod, lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { basename,
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
4
|
import { awpRequest, envValue } from "./tool-run.js";
|
|
5
5
|
const MANAGED_MARK = ".agentradar-managed";
|
|
6
6
|
async function ensureDirectory(path) {
|
|
@@ -45,8 +45,8 @@ export async function installVendorSkill(deps = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
const runnerSkills = () => import(new URL(import.meta.url.endsWith(".ts")
|
|
47
47
|
? "../../../templates/vendor-runner/feeder.mjs" : "./vendor-runner/feeder.mjs", import.meta.url).href);
|
|
48
|
-
export async function writeManagedSkills(skillsDir, skills) {
|
|
49
|
-
return (await runnerSkills()).writeManagedSkills(skillsDir, skills);
|
|
48
|
+
export async function writeManagedSkills(skillsDir, skills, runner) {
|
|
49
|
+
return (await runnerSkills()).writeManagedSkills(skillsDir, skills, runner);
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* R650 — a joined agent's AgentRadar skills (`my_skills`: the bundled set plus the ones linked to it), written
|
|
@@ -54,17 +54,6 @@ export async function writeManagedSkills(skillsDir, skills) {
|
|
|
54
54
|
* Codex `$CODEX_HOME/skills`. Done here because a session that acts only through `agentradar run` has no
|
|
55
55
|
* feeder; where the service runs, its feeder keeps them current.
|
|
56
56
|
*/
|
|
57
|
-
const BRIEF_MARK = "<!-- Written by AgentRadar and replaced on every sync; edits here are lost. -->";
|
|
58
|
-
/** Replaces the file itself, never a symlink's target, and never a file it did not write (a vendor's AGENTS.md). */
|
|
59
|
-
async function writeBrief(file, text) {
|
|
60
|
-
const current = await lstat(file).catch(() => null);
|
|
61
|
-
if (current && (!current.isFile() || !(await readFile(file, "utf8")).startsWith(BRIEF_MARK)))
|
|
62
|
-
return;
|
|
63
|
-
await mkdir(dirname(file), { recursive: true });
|
|
64
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
65
|
-
await writeFile(temporary, `${BRIEF_MARK}\n${text.trim()}\n`);
|
|
66
|
-
await rename(temporary, file);
|
|
67
|
-
}
|
|
68
57
|
/** Where each CLI reads skills. The same table as templates/vendor-runner/feeder.mjs; skill-install.test.ts pins them equal. */
|
|
69
58
|
export const SKILL_DIRS = {
|
|
70
59
|
claude: "$AR_BASE/.claude/skills",
|
|
@@ -89,23 +78,24 @@ export async function syncJoinedSkills(runnerId, deps) {
|
|
|
89
78
|
const skillsDir = skillsDirFor(harness, (name) => name === "HOME" ? deps.home : value(name));
|
|
90
79
|
if (!skillsDir)
|
|
91
80
|
return;
|
|
92
|
-
const
|
|
93
|
-
const root = (codex ? value("CODEX_HOME") : value("AR_BASE")) ?? "";
|
|
81
|
+
const runner = value("AR_CLI_RUNNER_ID") ?? undefined;
|
|
94
82
|
// Vadym 9988: the server's start instructions with this agent's brief, before its first session starts.
|
|
95
83
|
// Same file and mark as the runner's feeder, which keeps it fresh from then on.
|
|
96
84
|
const init = await awpRequest(runnerId, "initialize", {
|
|
97
85
|
protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "agentradar-cli", version: "1.0.0" },
|
|
98
|
-
}, { home: deps.home, fetch: deps.fetch, logError: deps.log, surface: "skill-sync" });
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
86
|
+
}, { home: deps.home, fetch: deps.fetch, logError: deps.log, surface: "skill-sync", signal: deps.signal });
|
|
87
|
+
// The feeder's own brief path and writer: one file per runner (MULTI-ORG-PUSH §0).
|
|
88
|
+
const skills = await runnerSkills();
|
|
89
|
+
const briefFile = skills.briefFileFor(harness ?? "claude", { AR_BASE: value("AR_BASE") ?? undefined, CODEX_HOME: value("CODEX_HOME") ?? undefined, AR_CLI_RUNNER_ID: runner });
|
|
90
|
+
if (briefFile && init?.result?.instructions)
|
|
91
|
+
skills.writeBrief(briefFile, init.result.instructions);
|
|
92
|
+
const have = skills.heldSkillZips(skillsDir);
|
|
93
|
+
const body = await awpRequest(runnerId, "tools/call", { name: "my_skills", arguments: { have } }, { home: deps.home, fetch: deps.fetch, logError: deps.log, surface: "skill-sync", signal: deps.signal });
|
|
104
94
|
const text = body?.result?.content?.find((c) => c.type === "text")?.text;
|
|
105
95
|
const answer = (text ? JSON.parse(text) : {});
|
|
106
96
|
if (!answer.ok || !Array.isArray(answer.skills))
|
|
107
97
|
throw new Error("AgentRadar did not return your skills");
|
|
108
|
-
const written = await writeManagedSkills(skillsDir, answer.skills);
|
|
98
|
+
const written = await writeManagedSkills(skillsDir, answer.skills, runner);
|
|
109
99
|
deps.log(`Installed ${written.length} AgentRadar skill(s) in ${skillsDir}.`);
|
|
110
100
|
}
|
|
111
101
|
//# sourceMappingURL=skill-install.js.map
|
package/dist/tool-run.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, realpath, stat, utimes, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { hostIsTeammate } from "./channel.js";
|
|
4
5
|
import { cliHeader, noteCliUpdate, withCliUpdate } from "./update.js";
|
|
5
|
-
const UNAUTHORIZED = "AgentRadar refused the tool call (401): this agent's key is no longer valid on this server (it was revoked, or the hire it was issued for has ended). Nothing was done. Stop calling tools with this key and tell the
|
|
6
|
+
const UNAUTHORIZED = "AgentRadar refused the tool call (401): this agent's key is no longer valid on this server (it was revoked, or the hire it was issued for has ended). Nothing was done. Stop calling tools with this key and tell the human who runs you.";
|
|
6
7
|
/**
|
|
7
8
|
* One `KEY='value'` line as `setup.ts` wrote it, single-quoted with `'` escaped as `'\''`.
|
|
8
9
|
* Nothing here is printed: the file holds the runner's bearer.
|
|
@@ -25,7 +26,8 @@ export async function callToolOverCli(runnerId, command, argumentsJson, deps = {
|
|
|
25
26
|
const switches = new Set(flags.filter(([flag]) => ["help", "dry-run", "json"].includes(flag)).map(([flag]) => flag));
|
|
26
27
|
flags = flags.filter(([flag]) => !switches.has(flag));
|
|
27
28
|
let tool = command;
|
|
28
|
-
|
|
29
|
+
let entry;
|
|
30
|
+
const parameters = argumentsJson === undefined ? {} : await parseArgumentsObject(argumentsJson, deps);
|
|
29
31
|
if (!parameters)
|
|
30
32
|
return EXIT.usage;
|
|
31
33
|
// A bare tool name with nothing to type goes straight to the server: the hot `inbox next` poll and the get_self probe
|
|
@@ -34,11 +36,11 @@ export async function callToolOverCli(runnerId, command, argumentsJson, deps = {
|
|
|
34
36
|
const tools = await listedTools(runnerId, deps);
|
|
35
37
|
if (!tools)
|
|
36
38
|
return EXIT.refused;
|
|
37
|
-
|
|
39
|
+
entry = tools.find((candidate) => pathOf(candidate) === command) ?? tools.find((candidate) => candidate.name === command);
|
|
38
40
|
if (!entry?.name)
|
|
39
41
|
return groupHelp(runnerId, command, tools, switches.has("help"), deps);
|
|
40
42
|
if (switches.has("help")) {
|
|
41
|
-
(deps.log ?? console.log)(leafHelp(entry));
|
|
43
|
+
(deps.log ?? console.log)(switches.has("json") ? leafHelpJson(entry) : leafHelp(entry));
|
|
42
44
|
return EXIT.ok;
|
|
43
45
|
}
|
|
44
46
|
tool = entry.name;
|
|
@@ -51,8 +53,8 @@ export async function callToolOverCli(runnerId, command, argumentsJson, deps = {
|
|
|
51
53
|
}
|
|
52
54
|
if (flags.length) {
|
|
53
55
|
const typed = await flagArguments(entry, flags, deps);
|
|
54
|
-
if (
|
|
55
|
-
return
|
|
56
|
+
if (Array.isArray(typed))
|
|
57
|
+
return showInputErrors({ path: pathOf(entry) ?? command, flags, json: switches.has("json"), entry }, typed, deps);
|
|
56
58
|
Object.assign(parameters, typed);
|
|
57
59
|
}
|
|
58
60
|
// Provenance for the execution ledger (R572), not permission: the server treats it as the self-report it is.
|
|
@@ -68,12 +70,21 @@ export async function callToolOverCli(runnerId, command, argumentsJson, deps = {
|
|
|
68
70
|
const log = deps.log ?? console.log;
|
|
69
71
|
deps = { ...deps, log: (text) => log(inboxLines(text)) };
|
|
70
72
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
const shown = { path: (entry && pathOf(entry)) ?? command, flags, json: switches.has("json"), entry };
|
|
74
|
+
if (!RUNNER_ID.test(runnerId))
|
|
75
|
+
return await callAwpTool(runnerId, tool, parameters, deps, shown);
|
|
76
|
+
const delivered = join(deps.home ?? homedir(), ".agentradar", "agents", `${runnerId}.delivered.json`);
|
|
77
|
+
if (tool !== "next_event") {
|
|
78
|
+
// Any call after a channel wake proves the agent took it in: touch the file the channel watches before acking the batch.
|
|
79
|
+
const code = await callAwpTool(runnerId, tool, parameters, deps, shown);
|
|
80
|
+
const now = new Date();
|
|
81
|
+
await utimes(delivered, now, now).catch(() => mkdir(dirname(delivered), { recursive: true })
|
|
82
|
+
.then(() => writeFile(delivered, "[]", { mode: 0o600, flag: "wx" }))).catch(() => undefined);
|
|
83
|
+
return code;
|
|
84
|
+
}
|
|
73
85
|
// A bare poll acknowledges the batch the previous poll printed, so the command stays byte-identical and
|
|
74
86
|
// an operator can allow it once. Asking for the next event means the last one was read; a lost or
|
|
75
87
|
// failed poll leaves the file as it was, so delivery stays at-least-once.
|
|
76
|
-
const delivered = join(deps.home ?? homedir(), ".agentradar", "agents", `${runnerId}.delivered.json`);
|
|
77
88
|
if (parameters.ackEventIds === undefined) {
|
|
78
89
|
const ids = await readFile(delivered, "utf8").then((text) => JSON.parse(text), () => []);
|
|
79
90
|
if (Array.isArray(ids) && ids.length)
|
|
@@ -82,7 +93,7 @@ export async function callToolOverCli(runnerId, command, argumentsJson, deps = {
|
|
|
82
93
|
// The agent's own poll answers at once: the channel process holds the long poll, and a held agent call stalls its whole turn.
|
|
83
94
|
parameters.waitMs ??= 0;
|
|
84
95
|
let said = "";
|
|
85
|
-
const code = await callAwpTool(runnerId, tool, parameters, { ...deps, log: (text) => { said = text; (deps.log ?? console.log)(text); } });
|
|
96
|
+
const code = await callAwpTool(runnerId, tool, parameters, { ...deps, log: (text) => { said = text; (deps.log ?? console.log)(text); } }, shown);
|
|
86
97
|
if (code !== 0)
|
|
87
98
|
return code;
|
|
88
99
|
let events = [];
|
|
@@ -101,7 +112,7 @@ export async function callToolOverCli(runnerId, command, argumentsJson, deps = {
|
|
|
101
112
|
*/
|
|
102
113
|
export const EXIT = { ok: 0, refused: 1, usage: 2 };
|
|
103
114
|
const EXIT_CODES = "EXIT CODES\n 0 done 1 refused or unreachable (the reason is on stderr) 2 fix the command";
|
|
104
|
-
/** Split `<word> ... [--
|
|
115
|
+
/** Split `<word> ... [--input <json|file|->] [--<argument> <value> ...]`; null when a flag has no value. */
|
|
105
116
|
export function parseCallArgs(rest) {
|
|
106
117
|
const words = [];
|
|
107
118
|
const flags = [];
|
|
@@ -116,11 +127,16 @@ export function parseCallArgs(rest) {
|
|
|
116
127
|
flags.push([arg.slice(2), ""]);
|
|
117
128
|
continue;
|
|
118
129
|
}
|
|
130
|
+
// The reply refusal for a literal \n says "pass --force-format", bare; `--force-format true` works too.
|
|
131
|
+
if (arg === "--force-format" && !/^(true|false)$/.test(rest[i + 1] ?? "")) {
|
|
132
|
+
flags.push(["force-format", "true"]);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
119
135
|
const value = rest[++i];
|
|
120
136
|
if (value === undefined)
|
|
121
137
|
return null;
|
|
122
|
-
if (arg === "--args")
|
|
123
|
-
json = value;
|
|
138
|
+
if (arg === "--input" || arg === "--args")
|
|
139
|
+
json = value; // --args: hidden alias, older templates and agent prompts still say it
|
|
124
140
|
else
|
|
125
141
|
flags.push([arg.slice(2), value]);
|
|
126
142
|
}
|
|
@@ -159,6 +175,14 @@ const serviceTools = (runnerId, tools) => {
|
|
|
159
175
|
+ ` and \`agentradar ${command("run_service_tool")} --tool-key <tool> --arguments '<its arguments as JSON>'\` runs one.`
|
|
160
176
|
: "";
|
|
161
177
|
};
|
|
178
|
+
/** Top-level `--help` headings (main 10-15: ~6 sections so 37 groups scan). */
|
|
179
|
+
const SECTIONS = {
|
|
180
|
+
Work: ["inbox", "threads", "tasks", "projects", "outcomes", "escalations", "interventions", "incidents", "briefs", "feedback", "me", "team", "browser", "files"],
|
|
181
|
+
Channels: ["channels", "contacts", "webhooks", "integrations", "join-links", "onboarding"],
|
|
182
|
+
Access: ["permissions", "approvals", "secrets", "roles", "org-chart", "org"],
|
|
183
|
+
Data: ["vaults", "memory", "skills", "metrics", "model", "hosting", "subscriptions", "selftest"],
|
|
184
|
+
Money: ["spend", "wallet", "usage", "marketplace", "placement", "karma"],
|
|
185
|
+
};
|
|
162
186
|
/** `agentradar <group> --help`: one line per child; anything else that names no command is a usage error that lists the groups. */
|
|
163
187
|
function groupHelp(runnerId, command, tools, help, deps) {
|
|
164
188
|
const prefix = command ? `${command} ` : "";
|
|
@@ -171,86 +195,241 @@ function groupHelp(runnerId, command, tools, help, deps) {
|
|
|
171
195
|
children.set(word, [...children.get(word) ?? [], tool]);
|
|
172
196
|
}
|
|
173
197
|
if (!children.size) {
|
|
174
|
-
const
|
|
175
|
-
|
|
198
|
+
const paths = tools.map(pathOf).filter((path) => !!path);
|
|
199
|
+
const groups = [...new Set(paths.map((path) => path.split(" ")[0]))].sort();
|
|
200
|
+
const near = closest(command, [...new Set(paths.flatMap((path) => path.split(" ").map((_, i, words) => words.slice(0, i + 1).join(" "))))]);
|
|
201
|
+
(deps.logError ?? console.error)(`${command} is not a command.${near ? ` Did you mean \`agentradar ${near}\`?` : ""} Groups: ${groups.join(" ")}. \`agentradar <group> --help\` lists its commands.${serviceTools(runnerId, tools)}`);
|
|
176
202
|
return EXIT.usage;
|
|
177
203
|
}
|
|
178
|
-
const
|
|
204
|
+
const line = ([word, leaves]) => ` ${word.padEnd(16)} ${leaves.length === 1 && pathOf(leaves[0]) === `${prefix}${word}` ? firstSentence(leaves[0].description) : `(${leaves.length} commands)`}`;
|
|
205
|
+
const sorted = [...children].sort(([a], [b]) => a.localeCompare(b));
|
|
206
|
+
// The top level scans in headed sections (Local, the commands that run on this machine, is in `agentradar --help`); a group the table doesn't name lands under More.
|
|
207
|
+
const lines = command ? sorted.map(line) : [...Object.keys(SECTIONS), "More"].flatMap((section) => {
|
|
208
|
+
const rows = sorted.filter(([word]) => (SECTIONS[section]?.includes(word) ? section : "More") === section);
|
|
209
|
+
return rows.length ? [`${section.toUpperCase()}`, ...rows.map(line)] : [];
|
|
210
|
+
});
|
|
179
211
|
(help ? deps.log ?? console.log : deps.logError ?? console.error)([
|
|
180
212
|
`agentradar ${prefix}<${command ? "command" : "group"}>`, ...lines,
|
|
181
213
|
`Add --help to any of them for its flags.${command ? "" : serviceTools(runnerId, tools)}`,
|
|
182
214
|
].join("\n"));
|
|
183
215
|
return help ? EXIT.ok : EXIT.usage;
|
|
184
216
|
}
|
|
185
|
-
/**
|
|
186
|
-
function
|
|
217
|
+
/** What a flag takes, from its schema: `a|b` for an enum, `<number>`, `key=<type>,... (repeatable)` for a list of small objects, `<type> (a list: ...)` for a list of scalars. */
|
|
218
|
+
function typeOf(schema) {
|
|
219
|
+
const type = [schema.type].flat()[0];
|
|
220
|
+
if (schema.enum)
|
|
221
|
+
return schema.enum.join("|");
|
|
222
|
+
if (type === "array") {
|
|
223
|
+
const fields = objectItems(schema);
|
|
224
|
+
const item = fields ? ` (one item as ${Object.entries(fields).map(([key, field]) => `${key}=<${[field.type].flat()[0] ?? "value"}>`).join(",")})` : "";
|
|
225
|
+
return `${typeOf(schema.items ?? {})} (a list: repeat the flag, or pass one JSON array)${item}`;
|
|
226
|
+
}
|
|
227
|
+
return `<${type ?? "value"}>`;
|
|
228
|
+
}
|
|
229
|
+
const objectItems = (schema) => [schema.type].flat()[0] === "array" && [schema.items?.type].flat()[0] === "object" ? schema.items?.properties : undefined;
|
|
230
|
+
const singular = (word) => word.endsWith("ies") ? `${word.slice(0, -3)}y` : word.endsWith("s") && !word.endsWith("ss") ? word.slice(0, -1) : word;
|
|
231
|
+
/** Design §4.2: the four approval explanation fields are flags named for the question the owner reads; the stored keys stay. */
|
|
232
|
+
const APPROVAL_FLAGS = { context: "what-happened", reason: "why", ifDeclined: "if-declined", decisionDetail: "need" };
|
|
233
|
+
/**
|
|
234
|
+
* Design §4.1: every top-level argument is a flag; one level of nested object is flattened into flags with no prefix
|
|
235
|
+
* (`approvalDetails.taskId` is `--task-id`; the four explanation fields are `--what-happened --why --if-declined --need`); a list of small objects is a repeated `key=value` flag named for one
|
|
236
|
+
* item (`contextLinks` is `--context-link`). A flattened name that a top-level argument already took stays on the
|
|
237
|
+
* top-level one.
|
|
238
|
+
*/
|
|
239
|
+
function flagTable(tool) {
|
|
187
240
|
const properties = tool.inputSchema?.properties ?? {};
|
|
188
241
|
const required = tool.inputSchema?.required ?? [];
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
const
|
|
242
|
+
const table = [];
|
|
243
|
+
const named = (name, schema) => kebab(objectItems(schema) ? singular(name) : name);
|
|
244
|
+
const flattened = Object.entries(properties).filter(([, schema]) => [schema.type].flat()[0] === "object" && schema.properties);
|
|
245
|
+
for (const [name, schema] of Object.entries(properties)) {
|
|
246
|
+
if (!flattened.some(([group]) => group === name))
|
|
247
|
+
table.push({ flag: kebab(name), path: [name], schema, required: required.includes(name) });
|
|
248
|
+
}
|
|
249
|
+
for (const [group, schema] of flattened) {
|
|
250
|
+
for (const [name, nested] of Object.entries(schema.properties)) {
|
|
251
|
+
const flag = group === "approvalDetails" ? APPROVAL_FLAGS[name] ?? named(name, nested) : named(name, nested);
|
|
252
|
+
if (!table.some((taken) => taken.flag === flag))
|
|
253
|
+
table.push({ flag, path: [group, name], schema: nested, required: required.includes(group) && !!schema.required?.includes(name) });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// The whole object as one JSON flag still works (the approvals and browser skills quote `--approval-details '{...}'`, #1882).
|
|
257
|
+
for (const [group, schema] of flattened)
|
|
258
|
+
if (!table.some((taken) => taken.flag === kebab(group)))
|
|
259
|
+
table.push({ flag: kebab(group), path: [group], schema, required: false });
|
|
260
|
+
return table;
|
|
261
|
+
}
|
|
262
|
+
/** `agentradar <path> --help`: the description, usage, required and optional flags, all generated from the inputSchema. */
|
|
263
|
+
function leafHelp(tool) {
|
|
264
|
+
const table = flagTable(tool);
|
|
265
|
+
const flag = (entry) => ` --${entry.flag} ${typeOf(entry.schema)}${entry.schema.description ? ` ${firstSentence(entry.schema.description)}` : ""}`;
|
|
266
|
+
const required = table.filter((entry) => entry.required);
|
|
267
|
+
const optional = table.filter((entry) => !entry.required);
|
|
193
268
|
const path = pathOf(tool) ?? `call <runner-id> ${tool.name}`;
|
|
194
269
|
return [
|
|
195
270
|
tool.description ?? "", "",
|
|
196
|
-
"USAGE", ` agentradar ${path}${required.map((
|
|
197
|
-
...(required.length ? ["REQUIRED", ...required.
|
|
198
|
-
"FLAGS", ...optional.map(flag), " --
|
|
199
|
-
|
|
271
|
+
"USAGE", ` agentradar ${path}${required.map((entry) => ` --${entry.flag} ${typeOf(entry.schema)}`).join("")}${optional.length ? " [flags]" : ""}`,
|
|
272
|
+
...(required.length ? ["REQUIRED", ...required.map(flag)] : []),
|
|
273
|
+
"FLAGS", ...optional.map(flag), " --input <json|file|-> the whole call as one JSON object, checked the same way",
|
|
274
|
+
" --dry-run print the exact call and send nothing",
|
|
275
|
+
" --json raw JSON",
|
|
276
|
+
" --runner <id> the runner (default $AR_CLI_RUNNER_ID)",
|
|
277
|
+
...Object.entries(tool.inputSchema?.properties ?? {}).flatMap(([name, schema]) => schema.properties ? fieldLines(name, schema, tool.inputSchema) : []),
|
|
200
278
|
...(tool.name && APPROVAL_EXAMPLES[tool.name] ? [`Example: agentradar ${path} ${APPROVAL_EXAMPLES[tool.name]}`] : []),
|
|
201
279
|
EXIT_CODES,
|
|
202
280
|
].join("\n");
|
|
203
281
|
}
|
|
282
|
+
/** `--help --json`: the same facts as `leafHelp`, for a program; `schema` is the tool's own JSON Schema. */
|
|
283
|
+
function leafHelpJson(tool) {
|
|
284
|
+
return JSON.stringify({
|
|
285
|
+
command: pathOf(tool) ?? tool.name, description: tool.description ?? "",
|
|
286
|
+
flags: flagTable(tool).map((entry) => ({ flag: `--${entry.flag}`, type: typeOf(entry.schema), required: entry.required, description: entry.schema.description ?? "" })),
|
|
287
|
+
schema: tool.inputSchema ?? {},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** `agentradar schema <command>`: the JSON Schema the server checks that command's input against. */
|
|
291
|
+
export async function schemaOverCli(runnerId, command, deps = {}) {
|
|
292
|
+
const tools = await listedTools(runnerId, deps);
|
|
293
|
+
if (!tools)
|
|
294
|
+
return EXIT.refused;
|
|
295
|
+
const entry = tools.find((candidate) => pathOf(candidate) === command);
|
|
296
|
+
if (!entry)
|
|
297
|
+
return groupHelp(runnerId, command, tools, false, deps);
|
|
298
|
+
(deps.log ?? console.log)(JSON.stringify({ command, schema: entry.inputSchema ?? {} }));
|
|
299
|
+
return EXIT.ok;
|
|
300
|
+
}
|
|
301
|
+
/** Set `value` at `path`, or append to the list there; one level of object, as `flagTable` flattens. */
|
|
302
|
+
function place(out, entry, value) {
|
|
303
|
+
const into = entry.path.length === 2 ? (out[entry.path[0]] ??= {}) : out;
|
|
304
|
+
const key = entry.path[entry.path.length - 1];
|
|
305
|
+
const list = [entry.schema.type].flat()[0] === "array";
|
|
306
|
+
into[key] = list ? [...(into[key] ?? []), value] : value;
|
|
307
|
+
}
|
|
308
|
+
/** `label=Invoice,url=https://a.b/c?d=e` as an object, split only before a key the item has; null when it has none of them. */
|
|
309
|
+
function keyValues(text, fields) {
|
|
310
|
+
const names = Object.keys(fields);
|
|
311
|
+
const parts = text.split(new RegExp(`,(?=(?:${names.join("|")})=)`));
|
|
312
|
+
const out = {};
|
|
313
|
+
for (const part of parts) {
|
|
314
|
+
const at = part.indexOf("=");
|
|
315
|
+
const key = at < 0 ? "" : part.slice(0, at);
|
|
316
|
+
if (!names.includes(key))
|
|
317
|
+
return `must look like ${names.map((name) => `${name}=<${[fields[name].type].flat()[0] ?? "value"}>`).join(",")}`;
|
|
318
|
+
const typed = coerce([fields[key].type].flat()[0], part.slice(at + 1));
|
|
319
|
+
if (typed === undefined)
|
|
320
|
+
return `${key} must be a ${[fields[key].type].flat()[0]}`;
|
|
321
|
+
out[key] = typed;
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
204
325
|
/**
|
|
205
326
|
* `--text "hi" --thread-id X` as the tool's own arguments, typed by the inputSchema the server
|
|
206
327
|
* advertises, so an agent never types JSON (braces and quotes) into a shell command.
|
|
207
328
|
*/
|
|
208
329
|
async function flagArguments(entry, flags, deps) {
|
|
209
|
-
const
|
|
210
|
-
const schema = entry.inputSchema?.properties ?? {};
|
|
330
|
+
const table = flagTable(entry);
|
|
211
331
|
const out = {};
|
|
212
|
-
const
|
|
332
|
+
const problems = [];
|
|
333
|
+
const fieldOf = (target) => target.path.join(".");
|
|
213
334
|
for (let [flag, value] of flags) {
|
|
214
335
|
// --body-file page.md: the argument read from a file, so a multi-line page never sits in a quoted
|
|
215
336
|
// shell argument (Claude Code prompts on a newline followed by # inside one).
|
|
216
337
|
// --content-base64-file page.html: a *Base64 argument gets the file's bytes as base64, any type (ANY-FILE-SEND).
|
|
217
|
-
if (flag.endsWith("-file") && !(
|
|
338
|
+
if (flag.endsWith("-file") && !table.some((candidate) => candidate.flag === kebab(flag))) {
|
|
218
339
|
const binary = camel(flag.slice(0, -"-file".length)).endsWith("Base64");
|
|
219
340
|
const text = await readArgumentFile(value, deps.cwd ?? process.cwd(), deps.home ?? homedir(), binary).catch((error) => error);
|
|
220
341
|
if (text instanceof Error) {
|
|
221
|
-
|
|
222
|
-
|
|
342
|
+
problems.push({ field: flag, problem: text.message });
|
|
343
|
+
continue;
|
|
223
344
|
}
|
|
224
345
|
[flag, value] = [flag.slice(0, -"-file".length), text];
|
|
225
346
|
}
|
|
226
347
|
// --text is what agents reach for on append_task_log, whose argument is --entry.
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
348
|
+
const wanted = kebab(flag) === "text" && !table.some((candidate) => candidate.flag === "text") ? "entry" : kebab(flag);
|
|
349
|
+
const target = table.find((candidate) => candidate.flag === wanted);
|
|
350
|
+
if (!target) {
|
|
351
|
+
const near = closest(kebab(flag), table.map((candidate) => candidate.flag));
|
|
352
|
+
problems.push({ field: flag, problem: "unknown flag", ...(near ? { suggestion: fieldOf(table.find((candidate) => candidate.flag === near)) } : {}) });
|
|
353
|
+
continue;
|
|
231
354
|
}
|
|
232
|
-
const type = [schema
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
355
|
+
const type = [target.schema.type].flat()[0];
|
|
356
|
+
const fields = objectItems(target.schema);
|
|
357
|
+
// A list is one JSON array or a repeated flag (#1752): --message-ids a --message-ids b, or --tools '[{...},{...}]'.
|
|
358
|
+
if (type === "array" && (!fields || /^\s*[[{]/.test(value))) {
|
|
359
|
+
const itemType = [target.schema.items?.type].flat()[0];
|
|
236
360
|
const items = parseList(value, itemType);
|
|
237
|
-
if (!items)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
361
|
+
if (!items)
|
|
362
|
+
problems.push({ field: fieldOf(target), problem: listFlagError(target.flag, itemType, target.schema) });
|
|
363
|
+
else
|
|
364
|
+
for (const item of items)
|
|
365
|
+
place(out, target, item);
|
|
242
366
|
continue;
|
|
243
367
|
}
|
|
244
|
-
const typed = coerce(type, value);
|
|
245
|
-
if (typed === undefined) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
368
|
+
const typed = fields ? keyValues(value, fields) : coerce(type, value);
|
|
369
|
+
if (typed === undefined || typeof typed === "string" && fields) {
|
|
370
|
+
problems.push({ field: fieldOf(target), problem: typeof typed === "string" ? typed : target.schema.properties
|
|
371
|
+
? `must be JSON with the fields ${(target.schema.required ?? Object.keys(target.schema.properties)).join(", ")}; \`agentradar ${pathOf(entry) ?? `call <runner-id> ${entry.name}`} --help\` shows each one.`
|
|
372
|
+
: `must be ${type === "array" ? "a list of " + target.schema.items?.type : "a " + type}` });
|
|
373
|
+
continue;
|
|
250
374
|
}
|
|
251
|
-
|
|
375
|
+
// A repeated flag builds a list: --message-ids a --message-ids b.
|
|
376
|
+
place(out, target, typed);
|
|
252
377
|
}
|
|
253
|
-
return out;
|
|
378
|
+
return problems.length ? problems : out;
|
|
379
|
+
}
|
|
380
|
+
const camel = (flag) => flag.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
381
|
+
/** The flag that sets a field (`approvalDetails.reason` is `--why`); a field no flag sets stays the JSON key the agent wrote. */
|
|
382
|
+
const flagFor = (table) => (field) => {
|
|
383
|
+
const found = table.find((entry) => entry.path.join(".") === field);
|
|
384
|
+
return found ? `--${found.flag}` : /[.[]/.test(field) ? field : `--${kebab(field)}`;
|
|
385
|
+
};
|
|
386
|
+
const shellWord = (value) => /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
387
|
+
function distance(a, b) {
|
|
388
|
+
let row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
389
|
+
for (let i = 1; i <= a.length; i++) {
|
|
390
|
+
const next = [i];
|
|
391
|
+
for (let j = 1; j <= b.length; j++)
|
|
392
|
+
next[j] = Math.min(row[j] + 1, next[j - 1] + 1, row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
393
|
+
row = next;
|
|
394
|
+
}
|
|
395
|
+
return row[b.length];
|
|
396
|
+
}
|
|
397
|
+
/** The candidate within 2 edits of `word`, nearest first; the server does the same over fields and their labels. */
|
|
398
|
+
const closest = (word, candidates) => candidates.map((candidate) => [candidate, distance(word, candidate)]).filter(([, d]) => d <= 2).sort((a, b) => a[1] - b[1])[0]?.[0];
|
|
399
|
+
/**
|
|
400
|
+
* Design §5.1: what was wrong, flag by flag; the agent's own command with only the bad flags fixed; one next step.
|
|
401
|
+
* Stderr, exit 2; with --json the same on stdout.
|
|
402
|
+
*/
|
|
403
|
+
function showInputErrors(shown, errors, deps) {
|
|
404
|
+
const table = shown.entry ? flagTable(shown.entry) : [];
|
|
405
|
+
const flagOf = flagFor(table);
|
|
406
|
+
const byField = new Map(errors.map((error) => [error.field, error]));
|
|
407
|
+
const fixed = shown.flags.flatMap(([flag, value]) => {
|
|
408
|
+
const error = byField.get(flag) ?? byField.get(camel(flag)) ?? byField.get(table.find((entry) => entry.flag === kebab(flag))?.path.join(".") ?? "");
|
|
409
|
+
if (error?.problem.startsWith("unknown"))
|
|
410
|
+
return error.suggestion ? [`${flagOf(error.suggestion)} ${shellWord(value)}`] : [];
|
|
411
|
+
return [`--${flag} ${shellWord(value)}`];
|
|
412
|
+
});
|
|
413
|
+
const missing = errors.filter((error) => error.problem === "required" && !/[.[]/.test(error.field)).map((error) => `${flagOf(error.field)} "..."`);
|
|
414
|
+
const example = `agentradar ${shown.path}${[...fixed, ...missing].map((part) => ` ${part}`).join("")}`;
|
|
415
|
+
const help = `agentradar ${shown.path} --help`;
|
|
416
|
+
if (shown.json) {
|
|
417
|
+
(deps.log ?? console.log)(JSON.stringify({ ok: false, error: { code: "E_VALIDATION", command: shown.path,
|
|
418
|
+
errors: errors.map((error) => ({ ...error, flag: flagOf(error.field), ...(error.suggestion ? { suggestion: flagOf(error.suggestion) } : {}) })), example, help } }));
|
|
419
|
+
return EXIT.usage;
|
|
420
|
+
}
|
|
421
|
+
const width = Math.max(...errors.map((error) => flagOf(error.field).length));
|
|
422
|
+
const lines = errors.map((error) => ` ${flagOf(error.field).padEnd(width)} ${error.problem.replace(/\.$/, "")}.${error.expected ? ` ${error.expected}.` : ""}${error.suggestion ? ` Did you mean ${flagOf(error.suggestion)}?` : ""}`);
|
|
423
|
+
const properties = Object.keys(shown.entry?.inputSchema?.properties ?? {});
|
|
424
|
+
const guessless = errors.some((error) => error.problem.startsWith("unknown") && !error.suggestion) && table.length;
|
|
425
|
+
(deps.logError ?? console.error)([
|
|
426
|
+
`error: ${errors.length} problem${errors.length === 1 ? "" : "s"} with \`agentradar ${shown.path}\`. Nothing was sent${properties.includes("approvalDetails") ? "; your owner has not been asked yet" : ""}.`,
|
|
427
|
+
...lines,
|
|
428
|
+
...(guessless ? [`This command takes: ${table.map((entry) => `--${entry.flag}`).join(" ")}`] : []),
|
|
429
|
+
"example:", ` ${example}`,
|
|
430
|
+
`next: fix it and run it again. Every flag: ${help}`,
|
|
431
|
+
].join("\n"));
|
|
432
|
+
return EXIT.usage;
|
|
254
433
|
}
|
|
255
434
|
/** One flag's items: a JSON array spreads (a nested-list schema keeps it whole), anything else is a single item; null when an item does not fit the type. */
|
|
256
435
|
function parseList(value, itemType) {
|
|
@@ -335,41 +514,65 @@ export function namedToolFlags(flags) {
|
|
|
335
514
|
});
|
|
336
515
|
return { tools, flags: flags.filter(([flag]) => flag !== "tool") };
|
|
337
516
|
}
|
|
338
|
-
|
|
517
|
+
/** `--input` is the JSON itself, a file inside the working folder, or `-` for standard input. */
|
|
518
|
+
async function readInput(value, deps) {
|
|
519
|
+
if (/^\s*[{[]/.test(value))
|
|
520
|
+
return value;
|
|
521
|
+
if (value === "-")
|
|
522
|
+
return await (deps.stdin ?? (async () => { const chunks = []; for await (const chunk of process.stdin)
|
|
523
|
+
chunks.push(chunk); return Buffer.concat(chunks).toString("utf8"); }))();
|
|
524
|
+
return await readArgumentFile(value, deps.cwd ?? process.cwd(), deps.home ?? homedir());
|
|
525
|
+
}
|
|
526
|
+
async function parseArgumentsObject(argumentsJson, deps) {
|
|
527
|
+
const logError = deps.logError ?? console.error;
|
|
339
528
|
let parsed;
|
|
340
529
|
try {
|
|
341
|
-
parsed = JSON.parse(argumentsJson);
|
|
530
|
+
parsed = JSON.parse(await readInput(argumentsJson, deps));
|
|
342
531
|
}
|
|
343
|
-
catch {
|
|
344
|
-
logError("--
|
|
532
|
+
catch (error) {
|
|
533
|
+
logError(error instanceof SyntaxError || !(error instanceof Error) ? "--input must be a JSON object, a file holding one, or - for standard input, e.g. --input '{\"title\":\"a real issue\"}'" : `--input ${error.message}`);
|
|
345
534
|
return null;
|
|
346
535
|
}
|
|
347
536
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
348
|
-
logError("--
|
|
537
|
+
logError("--input must be a JSON object");
|
|
349
538
|
return null;
|
|
350
539
|
}
|
|
351
540
|
return parsed;
|
|
352
541
|
}
|
|
353
542
|
/** One `tools/call` on the runner's AWP MCP endpoint with the runner's own key; the answer is printed verbatim. */
|
|
354
|
-
export async function callAwpTool(runnerId, name, toolArguments, deps) {
|
|
543
|
+
export async function callAwpTool(runnerId, name, toolArguments, deps, shown = { path: name, flags: [], json: false }) {
|
|
355
544
|
const log = deps.log ?? console.log;
|
|
356
545
|
const logError = deps.logError ?? console.error;
|
|
357
546
|
const body = await awpRequest(runnerId, "tools/call", { name, arguments: toolArguments }, deps);
|
|
358
547
|
if (!body)
|
|
359
548
|
return 1;
|
|
360
549
|
const text = body.result?.content?.[0]?.text;
|
|
361
|
-
|
|
550
|
+
// qa-sec-6 (#1880): server CTAs name the shell variable, which an attached pane never has; this command knows the runner.
|
|
551
|
+
const named = (t) => t.replaceAll("agentradar run $AR_CLI_RUNNER_ID", `agentradar run ${runnerId}`);
|
|
552
|
+
const said = named(typeof text === "string" ? text : JSON.stringify(body.result ?? {}));
|
|
362
553
|
// A refusal is the server's sentence and belongs on stderr with a non-zero exit, or a script that
|
|
363
554
|
// pipes stdout reads "the tool ran" from a body that says it was refused.
|
|
364
555
|
if (body.result?.isError) {
|
|
556
|
+
// A server that names fields (#1871 slice 2) gets them shown as flags; an older one's sentence prints as it is.
|
|
557
|
+
let refusal = {};
|
|
558
|
+
try {
|
|
559
|
+
refusal = JSON.parse(said);
|
|
560
|
+
}
|
|
561
|
+
catch { /* a plain sentence */ }
|
|
562
|
+
if (refusal.code === "E_VALIDATION" && Array.isArray(refusal.errors) && refusal.errors.length)
|
|
563
|
+
return showInputErrors(shown, refusal.errors, deps);
|
|
365
564
|
// 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"));
|
|
565
|
+
logError([said, ...(body.result.content ?? []).slice(1).map((c) => c.text).filter((t) => typeof t === "string").map(named)].join("\n"));
|
|
367
566
|
return 1;
|
|
368
567
|
}
|
|
369
568
|
log(withCliUpdate(said));
|
|
569
|
+
// A notice the server adds beside the answer (a routine run held while you are mid-turn) goes to stderr, so a piped stdout stays the answer.
|
|
570
|
+
const notices = (body.result?.content ?? []).slice(1).map((c) => c.text).filter((t) => typeof t === "string");
|
|
571
|
+
if (notices.length)
|
|
572
|
+
logError(notices.map(named).join("\n"));
|
|
370
573
|
return 0;
|
|
371
574
|
}
|
|
372
|
-
const RUNNER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
575
|
+
export const RUNNER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
373
576
|
/** One JSON-RPC request on the runner's AWP MCP endpoint with the runner's own key; null after logging why it failed. */
|
|
374
577
|
export async function awpRequest(runnerId, method, params, deps) {
|
|
375
578
|
const target = await awpEndpoint(runnerId, deps);
|
|
@@ -425,6 +628,11 @@ async function postAwp(url, bearer, method, params, deps) {
|
|
|
425
628
|
method: "POST",
|
|
426
629
|
headers: {
|
|
427
630
|
...(await cliHeader(deps.surface)),
|
|
631
|
+
// Vadym 16706: a sub-agent never acts as the agent. The server refuses its send and act tools on this header.
|
|
632
|
+
// A teammate pane is found by its --agent-id ancestor; an in-process sub-agent by agent-guard's AR_CALLER (guard.mjs bash).
|
|
633
|
+
...((deps.teammate ?? ((deps.env ?? process.env).AR_CALLER === "teammate" || hostIsTeammate())) ? { "x-agentradar-caller": "teammate" }
|
|
634
|
+
// 18229: the feeder's retrospective side-run (AR_CALLER=retro) reads an untrusted digest; the server lets it task only the agent itself.
|
|
635
|
+
: (deps.env ?? process.env).AR_CALLER === "retro" ? { "x-agentradar-caller": "retro" } : {}),
|
|
428
636
|
authorization: `Bearer ${bearer}`,
|
|
429
637
|
"content-type": "application/json",
|
|
430
638
|
// Both, because the endpoint answers JSON only when the client says it takes JSON.
|
|
@@ -436,6 +644,7 @@ async function postAwp(url, bearer, method, params, deps) {
|
|
|
436
644
|
method,
|
|
437
645
|
params,
|
|
438
646
|
}),
|
|
647
|
+
...(deps.signal ? { signal: deps.signal } : {}),
|
|
439
648
|
});
|
|
440
649
|
await noteCliUpdate(response.headers, logError);
|
|
441
650
|
if (!response.ok) {
|