agentradar-cli 0.3.5 → 0.3.6
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 +39 -0
- package/dist/agent-guard/hooks/hooks.json +3 -3
- package/dist/agent-guard/scripts/compaction-report.mjs +76 -0
- package/dist/agentradar-vendor/SKILL.md +11 -9
- package/dist/channel.js +21 -20
- package/dist/index.js +11 -3
- package/dist/runner.js +16 -11
- package/dist/secrets.js +3 -1
- package/dist/setup.js +77 -24
- package/dist/skill-install.js +8 -74
- package/dist/tool-run.js +4 -25
- package/dist/update.js +27 -2
- package/dist/vendor-runner/feeder.mjs +117 -16
- package/dist/vendor-runner/session-frame.mjs +12 -35
- package/dist/vendor-runner/skill-zip.mjs +76 -0
- package/package.json +3 -2
package/dist/admin.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADMIN-BROADCAST (Vadym 12310/12828): `agentradar admin broadcast` for an instance admin. Authenticates with a board
|
|
3
|
+
* API key in AGENTRADAR_ADMIN_TOKEN, never a runner key; the server refuses anyone who is not an instance admin.
|
|
4
|
+
*/
|
|
5
|
+
export const ADMIN_BROADCAST_USAGE = "Usage: agentradar admin broadcast --dry-run|--send --target all --text <text> --cta <next step>"
|
|
6
|
+
+ " (env AGENTRADAR_ADMIN_TOKEN = an instance admin's board API key; AGENTRADAR_API_URL = the API base)";
|
|
7
|
+
export async function adminOverCli(args, deps = {}) {
|
|
8
|
+
const env = deps.env ?? process.env, log = deps.log ?? console.log, logError = deps.logError ?? console.error;
|
|
9
|
+
const named = {};
|
|
10
|
+
const modes = args.slice(1).filter((arg) => arg === "--dry-run" || arg === "--send");
|
|
11
|
+
for (let i = 1; i < args.length; i++) {
|
|
12
|
+
if (["--target", "--text", "--cta"].includes(args[i]))
|
|
13
|
+
named[args[i].slice(2)] = args[++i] ?? "";
|
|
14
|
+
else if (!modes.includes(args[i]))
|
|
15
|
+
modes.push("?");
|
|
16
|
+
}
|
|
17
|
+
if (args[0] !== "broadcast" || modes.length !== 1 || modes[0] === "?" || named.target !== "all" || !named.text || !named.cta) {
|
|
18
|
+
logError(ADMIN_BROADCAST_USAGE);
|
|
19
|
+
return 2;
|
|
20
|
+
}
|
|
21
|
+
const token = env.AGENTRADAR_ADMIN_TOKEN?.trim();
|
|
22
|
+
if (!token) {
|
|
23
|
+
logError("Set AGENTRADAR_ADMIN_TOKEN to an instance admin's board API key");
|
|
24
|
+
return 2;
|
|
25
|
+
}
|
|
26
|
+
const base = (env.AGENTRADAR_API_URL ?? "https://vendor.agentradar.com/api").replace(/\/+$/, "");
|
|
27
|
+
const res = await (deps.fetch ?? fetch)(`${base}/admin/agent-broadcast`, {
|
|
28
|
+
method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
29
|
+
body: JSON.stringify({ target: "all", text: named.text, cta: named.cta, dryRun: modes[0] === "--dry-run" }),
|
|
30
|
+
});
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
logError(`Refused (${res.status}): ${text}`);
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
log(text);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=admin.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Marks the session as guarded (the runner reports ready only then); blocks going idle while AgentRadar work is owed; reminds of the house rules every AR_DISCIPLINE_EVERY turns; guards AgentRadar compact-then-switch model changes (a crashed PreModelSwitch hook exits 2 and refuses).",
|
|
2
|
+
"description": "Marks the session as guarded (the runner reports ready only then); blocks going idle while AgentRadar work is owed; reminds of the house rules every AR_DISCIPLINE_EVERY turns; reports compactions to AgentRadar; guards AgentRadar compact-then-switch model changes (a crashed PreModelSwitch hook exits 2 and refuses).",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" start", "timeout": 10 }] }],
|
|
5
5
|
"Stop": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" stop", "timeout": 30 }] }],
|
|
6
6
|
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" prompt", "timeout": 10 }] }],
|
|
7
|
-
"PreCompact": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PreCompact", "timeout": 30 }] }],
|
|
8
|
-
"PostCompact": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PostCompact", "timeout": 30 }] }],
|
|
7
|
+
"PreCompact": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PreCompact", "timeout": 30 }, { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/compaction-report.mjs\" PreCompact", "timeout": 30 }] }],
|
|
8
|
+
"PostCompact": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PostCompact", "timeout": 30 }, { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/compaction-report.mjs\" PostCompact", "timeout": 30 }] }],
|
|
9
9
|
"PreModelSwitch": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PreModelSwitch || exit 2", "timeout": 30 }] }],
|
|
10
10
|
"PostModelSwitch": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PostModelSwitch", "timeout": 30 }] }]
|
|
11
11
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reports this session's compactions to AgentRadar (Vadym 11657). Registered for PreCompact and PostCompact as
|
|
3
|
+
// `compaction-report.mjs <event>`; JSON on stdin. Observation only: it never blocks (exit 0 whatever happens).
|
|
4
|
+
// PostCompact stdin carries no token counts, so they come from the transcript's compact_boundary line.
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { closeSync, existsSync, fstatSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
const ID = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
12
|
+
const TAIL_BYTES = 1024 * 1024;
|
|
13
|
+
/** The last lines of a transcript, read from its last 1 MB only (a long session's file is far larger). */
|
|
14
|
+
const tail = (path) => {
|
|
15
|
+
let fd;
|
|
16
|
+
try {
|
|
17
|
+
fd = openSync(path, "r");
|
|
18
|
+
const { size } = fstatSync(fd);
|
|
19
|
+
const buf = Buffer.alloc(Math.min(size, TAIL_BYTES));
|
|
20
|
+
readSync(fd, buf, 0, buf.length, size - buf.length);
|
|
21
|
+
return buf.toString("utf8").split("\n").slice(-200);
|
|
22
|
+
} catch { return []; } finally { if (fd !== undefined) closeSync(fd); }
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** The newest compact_boundary written at or after `sinceMs`, as { preTokens, postTokens, durationMs }. */
|
|
26
|
+
export function lastBoundary(transcriptPath, sinceMs) {
|
|
27
|
+
for (const line of tail(transcriptPath).reverse()) {
|
|
28
|
+
if (!line.includes('"compact_boundary"')) continue;
|
|
29
|
+
try {
|
|
30
|
+
const entry = JSON.parse(line);
|
|
31
|
+
if (Date.parse(entry.timestamp) < sinceMs) return null;
|
|
32
|
+
const { preTokens = null, postTokens = null, durationMs = null } = entry.compactMetadata ?? {};
|
|
33
|
+
return { preTokens, postTokens, durationMs };
|
|
34
|
+
} catch { /* partial line: keep looking */ }
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One hook call. `send` posts a report; `stateDir` holds the start time between the two hooks. */
|
|
40
|
+
export async function reportCompaction(event, input, { send, followUp, stateDir = tmpdir(), now = Date.now }) {
|
|
41
|
+
if (input?.hook_event_name !== event || !ID.test(input.session_id ?? "")) return;
|
|
42
|
+
const marker = join(stateDir, `agentradar-compaction-${input.session_id}.json`);
|
|
43
|
+
const trigger = typeof input.trigger === "string" ? input.trigger.slice(0, 40) : null;
|
|
44
|
+
if (event === "PreCompact") {
|
|
45
|
+
writeFileSync(marker, JSON.stringify({ at: now() }), { mode: 0o600 });
|
|
46
|
+
return send({ phase: "start", trigger });
|
|
47
|
+
}
|
|
48
|
+
if (event !== "PostCompact") return;
|
|
49
|
+
let startedAt = now();
|
|
50
|
+
if (existsSync(marker)) { try { startedAt = JSON.parse(readFileSync(marker, "utf8")).at; } catch { /* keep now */ } }
|
|
51
|
+
rmSync(marker, { force: true });
|
|
52
|
+
// The compact_boundary line (the only source of token counts) is written after this hook returns, so it is
|
|
53
|
+
// reported separately, by a detached follow-up; this report must not wait for it.
|
|
54
|
+
send({ phase: "end", outcome: "ok", trigger, durationMs: now() - startedAt });
|
|
55
|
+
return followUp?.({ transcriptPath: input.transcript_path, startedAt, trigger });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Detached follow-up: waits up to `waitMs` for the boundary this compaction wrote, then reports its token counts. */
|
|
59
|
+
export async function reportTokens({ transcriptPath, startedAt, trigger }, { send, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), waitMs = 60_000, pollMs = 500 }) {
|
|
60
|
+
for (let waited = 0; waited <= waitMs; waited += pollMs) {
|
|
61
|
+
const boundary = lastBoundary(transcriptPath, startedAt - 1_000);
|
|
62
|
+
if (boundary) return send({ phase: "tokens", trigger, ...boundary });
|
|
63
|
+
await sleep(pollMs);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sendToServer = (report) => spawnSync(process.env.AR_AGENTRADAR_BIN || "agentradar",
|
|
68
|
+
["call", process.env.AR_CLI_RUNNER_ID || "hosted", "report_compaction", "--args", JSON.stringify(report)], { encoding: "utf8", timeout: 20_000 });
|
|
69
|
+
const spawnFollowUp = (job) => spawn(process.execPath, [fileURLToPath(import.meta.url), "tokens", JSON.stringify(job)], { detached: true, stdio: "ignore" }).unref();
|
|
70
|
+
|
|
71
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
72
|
+
try {
|
|
73
|
+
if (process.argv[2] === "tokens") await reportTokens(JSON.parse(process.argv[3]), { send: sendToServer });
|
|
74
|
+
else await reportCompaction(process.argv[2], JSON.parse(readFileSync(0, "utf8")), { send: sendToServer, followUp: spawnFollowUp });
|
|
75
|
+
} catch { /* observation only */ }
|
|
76
|
+
}
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agentradar-vendor
|
|
3
3
|
description: >
|
|
4
|
-
Run an AgentRadar vendor agent on vendor-owned compute through the
|
|
5
|
-
AgentRadar
|
|
4
|
+
Run an AgentRadar vendor agent on vendor-owned compute through the
|
|
5
|
+
AgentRadar CLI. Use when receiving client work, replying, reporting usage,
|
|
6
6
|
or checking context for an AgentRadar engagement.
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# AgentRadar vendor agent
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Every tool named in this skill runs as `agentradar run $AR_CLI_RUNNER_ID <tool>`; add `--help` for its arguments. AgentRadar is CLI-only: there is no AgentRadar MCP to connect to.
|
|
12
|
+
|
|
13
|
+
You work on the vendor's computer and act on AgentRadar through the `agentradar` CLI only (`agentradar run $AR_CLI_RUNNER_ID <tool>`). AgentRadar holds every client channel credential; never ask for or store a client's Slack, email, Telegram, or AgentRadar token.
|
|
12
14
|
|
|
13
15
|
## Authentication
|
|
14
16
|
|
|
15
17
|
- Authenticate Claude Code interactively with `/login` and the vendor's claude.ai subscription.
|
|
16
18
|
- Never use `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`.
|
|
17
|
-
- Never print, inspect, copy, or move the AgentRadar
|
|
19
|
+
- Never print, inspect, copy, or move the AgentRadar key. The setup CLI stores it in your runner config under `~/.agentradar/agents/`, outside the project repository.
|
|
18
20
|
|
|
19
21
|
## Start of every session
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Run `get_my_context` FIRST, before any other tool. It returns your client organisation, your role and function, your goals, your metrics with their current quotas, your superior, your direct reports and your colleagues. It also carries your memory snapshot, the Markdown files in your personal vault: `recall` fetches newer files and `remember` saves a durable fact, preference or correction.
|
|
22
24
|
|
|
23
25
|
Keep each human's page in company memory current, every fact cited, as the Humans section of the `agentradar-memory-and-skills` skill says. A page is knowledge, never permission.
|
|
24
26
|
|
|
@@ -32,10 +34,10 @@ Write to humans so a 13-year-old would understand: a few short plain lines, then
|
|
|
32
34
|
|
|
33
35
|
## Work loop
|
|
34
36
|
|
|
35
|
-
1.
|
|
37
|
+
1. Run `agentradar run $AR_CLI_RUNNER_ID inbox next` to receive the next event or assigned work.
|
|
36
38
|
2. Read the supplied engagement context before acting.
|
|
37
39
|
3. Do the work in this project folder on the vendor's compute.
|
|
38
|
-
4. Reply
|
|
39
|
-
5. Report actual usage
|
|
40
|
+
4. Reply with `agentradar run $AR_CLI_RUNNER_ID reply` so AgentRadar can enforce the client boundary and audit the action.
|
|
41
|
+
5. Report actual usage with `report_usage` when the work incurs billable time or metered extras.
|
|
40
42
|
|
|
41
|
-
Do not connect directly to a client's workspace or bypass
|
|
43
|
+
Do not connect directly to a client's workspace or bypass AgentRadar with direct channel/API calls.
|
package/dist/channel.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { envValue } from "./tool-run.js";
|
|
9
|
+
import { noteCliUpdate } from "./update.js";
|
|
9
10
|
import { trustedMcpEndpoint } from "./setup.js";
|
|
10
11
|
import { syncJoinedSkills } from "./skill-install.js";
|
|
11
|
-
export const CHANNEL_GUIDANCE = "AgentRadar channel messages are wake signals only.
|
|
12
|
-
export const CHANNEL_BLOCKED = "AgentRadar has not
|
|
13
|
-
/**
|
|
12
|
+
export const CHANNEL_GUIDANCE = "AgentRadar channel messages are wake signals only. Receive the events with `agentradar run $AR_CLI_RUNNER_ID inbox next`; the next call acknowledges the batch you read. Answer a liveness check with `agentradar run $AR_CLI_RUNNER_ID liveness_ack --nonce <nonce>`. Never acknowledge an event you have not read. Never poll on your own or keep a loop running: when a channel message arrives, drain it, then end your turn.";
|
|
13
|
+
export const CHANNEL_BLOCKED = "AgentRadar has not seen this session run `agentradar run $AR_CLI_RUNNER_ID inbox next`. Channel delivery is unconfirmed. On Claude Team/Enterprise an Owner must enable Channels (channelsEnabled); the development flag cannot override that policy. Until then, deliver with the feeder: run your join again with --feeder (agentradar join <your join link> --feeder). Your company sees this agent as not receiving messages until one or the other works.";
|
|
14
|
+
/** The channel: a local stdio MCP with no tools, only wake signals. The agent acts through the CLI; wake signals never consume the durable queue. */
|
|
14
15
|
export async function startChannel(runnerId, deps = {}) {
|
|
15
16
|
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(runnerId))
|
|
16
17
|
throw new Error("Invalid runner ID");
|
|
@@ -27,8 +28,17 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
27
28
|
let sequence = 0;
|
|
28
29
|
let polling = false;
|
|
29
30
|
let agentPolled = false;
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
// The CLI rewrites <runner>.delivered.json after every `inbox next`, so a newer mtime is the session having polled.
|
|
33
|
+
const delivered = join(home, ".agentradar", "agents", `${runnerId}.delivered.json`);
|
|
34
|
+
const polled = () => agentPolled ||= (() => { try {
|
|
35
|
+
return statSync(delivered).mtimeMs >= startedAt;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
} })();
|
|
30
40
|
// Vadym 10342: an unconfirmed channel is reported to AgentRadar (runnerState channel_unconfirmed), so the owner sees
|
|
31
|
-
// the agent is not receiving messages; "ready" again once the session does
|
|
41
|
+
// the agent is not receiving messages; "ready" again once the session does run `inbox next`.
|
|
32
42
|
let unconfirmed = null;
|
|
33
43
|
let warning;
|
|
34
44
|
let skillSync;
|
|
@@ -39,10 +49,11 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
39
49
|
const response = await (deps.fetch ?? fetch)(endpoint, {
|
|
40
50
|
method: "POST",
|
|
41
51
|
redirect: "error",
|
|
42
|
-
headers: { "x-agentradar-cli": createRequire(import.meta.url)("../package.json").version, authorization: `Bearer ${bearer}`, "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
52
|
+
headers: { "x-agentradar-cli": createRequire(import.meta.url)("../package.json").version, "x-agentradar-surface": "channel", authorization: `Bearer ${bearer}`, "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
43
53
|
body: JSON.stringify({ jsonrpc: "2.0", id: ++sequence, method, params }),
|
|
44
54
|
signal: AbortSignal.any([stop.signal, AbortSignal.timeout(230_000)]),
|
|
45
55
|
});
|
|
56
|
+
await noteCliUpdate(response.headers, logError);
|
|
46
57
|
if (!response.ok) {
|
|
47
58
|
const said = response.status === 426 ? await response.json().then((body) => body.error?.message).catch(() => null) : null;
|
|
48
59
|
throw new Error(said ?? `AgentRadar refused the channel call (${response.status})`);
|
|
@@ -81,7 +92,7 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
81
92
|
await next({ runnerState: "channel_unconfirmed" });
|
|
82
93
|
unconfirmed = "reported";
|
|
83
94
|
}
|
|
84
|
-
if (unconfirmed === "reported" &&
|
|
95
|
+
if (unconfirmed === "reported" && polled())
|
|
85
96
|
unconfirmed = null;
|
|
86
97
|
// While reported, no runnerState: the stored state stands, and the poll still drains for wake signals.
|
|
87
98
|
// AGENT-GUARD-ACTIVE: the supervisor's marker, written by agent-guard's SessionStart hook in this session.
|
|
@@ -95,7 +106,7 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
95
106
|
const wake = JSON.stringify([events.map((event) => event.eventId), payload.livenessProbe?.nonce, Boolean(payload.onboarding)]);
|
|
96
107
|
if ((events.length || payload.livenessProbe || payload.onboarding) && (wake !== lastWake || Date.now() - lastSent >= 30_000)) {
|
|
97
108
|
await send({ jsonrpc: "2.0", method: "notifications/claude/channel", params: {
|
|
98
|
-
content: `${payload.firstStep ? `${payload.firstStep} ` : ""}${events.length} new AgentRadar events;
|
|
109
|
+
content: `${payload.firstStep ? `${payload.firstStep} ` : ""}${events.length} new AgentRadar events; run \`agentradar run ${runnerId} inbox next\`${payload.livenessProbe ? " to receive your liveness check" : ""}.`,
|
|
99
110
|
meta: { count: String(events.length) },
|
|
100
111
|
} });
|
|
101
112
|
lastWake = wake;
|
|
@@ -116,7 +127,7 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
116
127
|
if (message.method === "initialize") {
|
|
117
128
|
await send({ jsonrpc: "2.0", id: message.id, result: {
|
|
118
129
|
protocolVersion: "2024-11-05",
|
|
119
|
-
capabilities: {
|
|
130
|
+
capabilities: { experimental: { "claude/channel": {} } },
|
|
120
131
|
serverInfo: { name: "agentradar", version: createRequire(import.meta.url)("../package.json").version },
|
|
121
132
|
instructions: CHANNEL_GUIDANCE,
|
|
122
133
|
} });
|
|
@@ -124,7 +135,7 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
124
135
|
else if (message.method === "notifications/initialized") {
|
|
125
136
|
if (!polling) {
|
|
126
137
|
polling = true;
|
|
127
|
-
warning = setTimeout(() => { if (!
|
|
138
|
+
warning = setTimeout(() => { if (!polled()) {
|
|
128
139
|
logError(CHANNEL_BLOCKED);
|
|
129
140
|
unconfirmed = "pending";
|
|
130
141
|
} }, deps.confirmMs ?? 30_000);
|
|
@@ -142,16 +153,6 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
142
153
|
if (message.method === "ping") {
|
|
143
154
|
await send({ jsonrpc: "2.0", id: message.id, result: {} });
|
|
144
155
|
}
|
|
145
|
-
else if (message.method === "tools/list" || message.method === "tools/call") {
|
|
146
|
-
const params = message.params ?? {};
|
|
147
|
-
const isNext = message.method === "tools/call" && params.name === "next_event";
|
|
148
|
-
const response = isNext ? await next(params.arguments ?? {}) : await remote(message.method, params);
|
|
149
|
-
if (isNext && !response.error && !response.result?.isError) {
|
|
150
|
-
agentPolled = true;
|
|
151
|
-
clearTimeout(warning);
|
|
152
|
-
}
|
|
153
|
-
await send({ jsonrpc: "2.0", id: message.id, ...(response.error ? { error: response.error } : { result: response.result }) });
|
|
154
|
-
}
|
|
155
156
|
else {
|
|
156
157
|
await send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Method not found" } });
|
|
157
158
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { installVendorSkill } from "./skill-install.js";
|
|
4
5
|
import { startChannel } from "./channel.js";
|
|
5
6
|
import { runRestartSelftest, startVendorRunner } from "./runner.js";
|
|
6
|
-
import { cancelJoin, CODEX_GUARD_REFUSAL, ensureCodexGuard, joinOrganisation, leaveOrganisation, setupVendorAgent } from "./setup.js";
|
|
7
|
+
import { cancelJoin, CODEX_GUARD_REFUSAL, dropStaleMcp, ensureCodexGuard, joinOrganisation, leaveOrganisation, setupVendorAgent } from "./setup.js";
|
|
7
8
|
import { callAwpTool, callToolOverCli, createTaskOverCli, INBOX_SEARCH_USAGE, METRIC_REPORT_USAGE, PROPOSE_HUMAN_USAGE, inboxSearchOverCli, LISTING_DESCRIBE_USAGE, listServiceToolsOverCli, parseCallArgs, PERMISSION_VERB_TOOLS, runServiceToolOverCli, TASK_VERB_TOOLS } from "./tool-run.js";
|
|
8
9
|
import { installSyncTimer, syncAgentDir } from "./sync.js";
|
|
9
10
|
import { importVaultOverCli } from "./vault-import.js";
|
|
10
11
|
import { cloneVaultOverCli } from "./vault-clone.js";
|
|
11
12
|
import { secretsOverCli } from "./secrets.js";
|
|
13
|
+
import { adminOverCli } from "./admin.js";
|
|
12
14
|
// `run` is the one prefix every agent-facing command shares, so one harness "always allow agentradar run"
|
|
13
15
|
// covers a whole session: `run <link>` (or `run join <link>`) joins, `run <runner-id> <tool>` calls a tool.
|
|
14
16
|
export const runArgs = (raw) => raw[0] !== "run" ? raw
|
|
@@ -18,7 +20,7 @@ export const runArgs = (raw) => raw[0] !== "run" ? raw
|
|
|
18
20
|
async function main() {
|
|
19
21
|
const args = runArgs(process.argv.slice(2));
|
|
20
22
|
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
21
|
-
console.log("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] [--harness claude|codex] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat|make-recurring <runner-id> --args '<json>' | agentradar inbox search <runner-id> [--query <text>] [--<filter> <value> ...] [--json] | agentradar listing describe <runner-id> --capabilities <text> | agentradar permissions mine|roles|set|request|grant-tool <runner-id> [--<argument> <value> ...] | agentradar vault import <runner-id> <dir> [--vault <vault-id>] [--source <name>] [--write] | agentradar vault clone <runner-id> <vault-id> [dir] | agentradar metric report <runner-id> --metric-id <id> --value <n> --period-start <iso> --period-end <iso> [--evidence <text>] | agentradar secrets pull|renew <runner-id> | agentradar secrets exec <runner-id> -- <command> | agentradar propose-human <runner-id> --name <name> --email <email> --role-id <id> | agentradar secrets upload <runner-id> --env-file <path> | --from-env <NAME,...> | --prompt <NAME> [--replace] | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
|
|
23
|
+
console.log("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] [--harness claude|codex] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat|make-recurring <runner-id> --args '<json>' | agentradar inbox search <runner-id> [--query <text>] [--<filter> <value> ...] [--json] | agentradar listing describe <runner-id> --capabilities <text> | agentradar permissions mine|roles|set|request|grant-tool <runner-id> [--<argument> <value> ...] | agentradar vault import <runner-id> <dir> [--vault <vault-id>] [--source <name>] [--write] | agentradar vault clone <runner-id> <vault-id> [dir] | agentradar metric report <runner-id> --metric-id <id> --value <n> --period-start <iso> --period-end <iso> [--evidence <text>] | agentradar secrets pull|renew <runner-id> | agentradar secrets exec <runner-id> -- <command> | agentradar propose-human <runner-id> --name <name> --email <email> --role-id <id> | agentradar secrets upload <runner-id> --env-file <path> | --from-env <NAME,...> | --prompt <NAME> [--replace] | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>] | agentradar admin broadcast --dry-run|--send --target all --text <text> --cta <next step>");
|
|
22
24
|
console.log([
|
|
23
25
|
"agentradar run <runner-id> <noun> <verb> [--<argument> <value> ...] [--dry-run] [--json]",
|
|
24
26
|
" Every agent tool, grouped in AgentRadar terms: inbox, thread, message, task, metric, vault, ...",
|
|
@@ -39,6 +41,7 @@ async function main() {
|
|
|
39
41
|
if (args.length === 1 && args[0] === "upgrade") {
|
|
40
42
|
const { selfUpdate, runningVersion } = await import("./update.js");
|
|
41
43
|
const installed = await selfUpdate();
|
|
44
|
+
await dropStaleMcp(process.cwd(), homedir());
|
|
42
45
|
console.log(installed ? `agentradar upgraded to ${installed}` : `agentradar ${await runningVersion()} is the latest`);
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
@@ -226,6 +229,10 @@ async function main() {
|
|
|
226
229
|
process.exitCode = await secretsOverCli(args.slice(1));
|
|
227
230
|
return;
|
|
228
231
|
}
|
|
232
|
+
if (args[0] === "admin") {
|
|
233
|
+
process.exitCode = await adminOverCli(args.slice(1));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
229
236
|
if (args[0] === "task" && args[1] && Object.hasOwn(TASK_VERB_TOOLS, args[1])) {
|
|
230
237
|
if (args.length !== 5 || args[3] !== "--args") {
|
|
231
238
|
console.error("Usage: agentradar task create|repeat|make-recurring <runner-id> --args '<json>'");
|
|
@@ -253,11 +260,12 @@ async function main() {
|
|
|
253
260
|
return;
|
|
254
261
|
}
|
|
255
262
|
if (args[0] === "sync" && args.length <= 2) {
|
|
263
|
+
await dropStaleMcp(args[1] ?? process.cwd(), homedir());
|
|
256
264
|
process.exitCode = await syncAgentDir(args[1]);
|
|
257
265
|
return;
|
|
258
266
|
}
|
|
259
267
|
const help = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
|
|
260
|
-
(help ? console.log : console.error)("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat|make-recurring <runner-id> --args '<json>' | agentradar inbox search <runner-id> [--query <text>] [--<filter> <value> ...] [--json] | agentradar listing describe <runner-id> --capabilities <text> | agentradar permissions mine|roles|set|request|grant-tool <runner-id> [--<argument> <value> ...] | agentradar metric report <runner-id> --metric-id <id> --value <n> --period-start <iso> --period-end <iso> [--evidence <text>] | agentradar secrets pull|renew <runner-id> | agentradar secrets exec <runner-id> -- <command> | agentradar propose-human <runner-id> --name <name> --email <email> --role-id <id> | agentradar secrets upload <runner-id> --env-file <path> | --from-env <NAME,...> | --prompt <NAME> [--replace] | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
|
|
268
|
+
(help ? console.log : console.error)("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat|make-recurring <runner-id> --args '<json>' | agentradar inbox search <runner-id> [--query <text>] [--<filter> <value> ...] [--json] | agentradar listing describe <runner-id> --capabilities <text> | agentradar permissions mine|roles|set|request|grant-tool <runner-id> [--<argument> <value> ...] | agentradar metric report <runner-id> --metric-id <id> --value <n> --period-start <iso> --period-end <iso> [--evidence <text>] | agentradar secrets pull|renew <runner-id> | agentradar secrets exec <runner-id> -- <command> | agentradar propose-human <runner-id> --name <name> --email <email> --role-id <id> | agentradar secrets upload <runner-id> --env-file <path> | --from-env <NAME,...> | --prompt <NAME> [--replace] | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>] | agentradar admin broadcast --dry-run|--send --target all --text <text> --cta <next step>");
|
|
261
269
|
if (!help)
|
|
262
270
|
process.exitCode = 1;
|
|
263
271
|
}
|
package/dist/runner.js
CHANGED
|
@@ -6,14 +6,14 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { shellValue, trustedMcpEndpoint, writePrivateFile } from "./setup.js";
|
|
8
8
|
import { callAwpTool, envValue } from "./tool-run.js";
|
|
9
|
-
import { installedVersion, revertCli, runningVersion, selfUpdate } from "./update.js";
|
|
9
|
+
import { cliHeader, installedVersion, noteCliUpdate, revertCli, runningVersion, selfUpdate } from "./update.js";
|
|
10
10
|
const RUNNER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
11
11
|
const ENGAGEMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12
12
|
// Per-hire bearers are ordinary agent keys (`pcp_…`); the runner's own is an AWP key (`ar_…`).
|
|
13
13
|
const HIRE_BEARER = /^(ar|pcp)_[A-Za-z0-9_-]+$/;
|
|
14
14
|
// What a hire's env file sets for itself; the catalog agent's values for these never reach it.
|
|
15
15
|
// A hire always runs Claude, whose MCP config it owns; a Codex home would be the catalog agent's.
|
|
16
|
-
const HIRE_OWN_KEYS = /^(export\s+)?(AGENT_NAME|AR_BASE|AWP_MCP_URL|AWP_BEARER|AWP_BEARER_FILE|MCP_CONFIG|TMUX_SESSION|AGENT_PID_PATTERN|AR_AGENT_HARNESS|CODEX_HOME)=/;
|
|
16
|
+
const HIRE_OWN_KEYS = /^(export\s+)?(AGENT_NAME|AR_CLI_RUNNER_ID|CLAUDE_CONFIG_DIR|AR_BASE|AWP_MCP_URL|AWP_BEARER|AWP_BEARER_FILE|MCP_CONFIG|TMUX_SESSION|AGENT_PID_PATTERN|AR_AGENT_HARNESS|CODEX_HOME)=/;
|
|
17
17
|
const CODEX_HARNESS = /^(export\s+)?AR_AGENT_HARNESS=\s*["']?codex\b/m;
|
|
18
18
|
// While a pane shows this, its agent is mid-turn (the feeder's in-progress hint, same for Claude and Codex).
|
|
19
19
|
const MID_TURN = /esc to interrupt|\besc to cancel\b/i;
|
|
@@ -285,12 +285,16 @@ export async function startVendorRunner(runnerId, home = homedir(), deps = {}) {
|
|
|
285
285
|
const killSession = (name) => spawnChild("bash", [
|
|
286
286
|
"-c", 'PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"; exec "${TMUX_BIN:-tmux}" kill-session -t "$1"', "tmux", `=_${name}`,
|
|
287
287
|
]).once("error", () => { });
|
|
288
|
-
const post = (path, body, token = bearer) =>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
const post = async (path, body, token = bearer) => {
|
|
289
|
+
const response = await (deps.fetch ?? fetch)(new URL(path, mcpUrl), {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: { ...(await cliHeader("feeder")), authorization: `Bearer ${token}`, accept: "application/json", "content-type": "application/json", "cache-control": "no-store" },
|
|
292
|
+
body: JSON.stringify(body ?? {}),
|
|
293
|
+
signal: AbortSignal.timeout(30_000),
|
|
294
|
+
});
|
|
295
|
+
await noteCliUpdate(response.headers);
|
|
296
|
+
return response;
|
|
297
|
+
};
|
|
294
298
|
const launchAll = async () => {
|
|
295
299
|
for (const file of await readdir(agentDir)) {
|
|
296
300
|
const engagementId = file.startsWith(prefix) && file.endsWith(".env") ? file.slice(prefix.length, -".env".length) : "";
|
|
@@ -332,9 +336,9 @@ export async function startVendorRunner(runnerId, home = homedir(), deps = {}) {
|
|
|
332
336
|
const configDir = join(home, ".agentradar", "config", name);
|
|
333
337
|
await mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
334
338
|
const mcpConfig = join(configDir, "awp-only.json");
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}, null, 2)}\n`);
|
|
339
|
+
// Vadym 12915: no MCP entry for AgentRadar. The hire acts with `agentradar run $AR_CLI_RUNNER_ID <tool>`,
|
|
340
|
+
// which reads this hire's own env file; --strict-mcp-config still confines the pane to this empty list.
|
|
341
|
+
await writePrivateFile(mcpConfig, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`);
|
|
338
342
|
// Every setting the vendor put in the catalog agent's env (tool env, harness flags) is copied, as
|
|
339
343
|
// read when the runner started, except the ones this hire sets for itself — the shared bearer among
|
|
340
344
|
// them. Copied rather than sourced: sourcing would put the shared bearer in the hire's supervisor.
|
|
@@ -343,6 +347,7 @@ export async function startVendorRunner(runnerId, home = homedir(), deps = {}) {
|
|
|
343
347
|
// ponytail: line by line, so a multi-line quoted value is copied only in part; parse it when one appears.
|
|
344
348
|
const own = [
|
|
345
349
|
`AGENT_NAME=${shellValue(name)}`,
|
|
350
|
+
`AR_CLI_RUNNER_ID=${shellValue(name)}`,
|
|
346
351
|
`AR_BASE=${shellValue(folderOf(hire.engagementId))}`,
|
|
347
352
|
`AWP_MCP_URL=${shellValue(mcpUrl.toString())}`,
|
|
348
353
|
`AWP_BEARER=${shellValue(hire.bearer)}`,
|
package/dist/secrets.js
CHANGED
|
@@ -15,6 +15,7 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { awpRequest, envValue } from "./tool-run.js";
|
|
18
|
+
import { cliHeader, noteCliUpdate } from "./update.js";
|
|
18
19
|
export const SECRETS_FILE = join(".agentradar", "secrets.enc");
|
|
19
20
|
export const SECRETS_USAGE = "Usage: agentradar secrets pull|renew|env <runner-id> | agentradar secrets exec <runner-id> -- <command> [args...]"
|
|
20
21
|
+ " | agentradar secrets upload <runner-id> --env-file <path> | --from-env <NAME,...> | --prompt <NAME> [--only <NAME,...>] [--replace --approval-details '<json>'] [--token <token>]"
|
|
@@ -34,8 +35,9 @@ async function credentials(runnerId, deps) {
|
|
|
34
35
|
async function post(runnerId, path, body, deps) {
|
|
35
36
|
const { origin, bearer } = await credentials(runnerId, deps);
|
|
36
37
|
const res = await (deps.fetch ?? fetch)(`${origin}/api/agent/${path}`, {
|
|
37
|
-
method: "POST", headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" }, body: JSON.stringify(body ?? {}),
|
|
38
|
+
method: "POST", headers: { ...(await cliHeader()), authorization: `Bearer ${bearer}`, "content-type": "application/json" }, body: JSON.stringify(body ?? {}),
|
|
38
39
|
});
|
|
40
|
+
await noteCliUpdate(res.headers, deps.logError);
|
|
39
41
|
const json = await res.json().catch(() => ({}));
|
|
40
42
|
if (!res.ok)
|
|
41
43
|
throw new Error(json.error ?? `AgentRadar answered ${res.status}`);
|