agentradar-cli 0.3.4 → 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 +11 -5
- 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/dist/vendor-runner/supervisor/install-agent.sh +1 -1
- package/dist/vendor-runner/supervisor/supervise-agent.sh +8 -0
- package/package.json +3 -2
|
@@ -24,8 +24,8 @@ function oneLine(s) {
|
|
|
24
24
|
/**
|
|
25
25
|
* Build the single-line injection frame for one drained next_event event.
|
|
26
26
|
* event: { eventId, source, channel?, conversationId?, text, firstStep? }
|
|
27
|
-
* cliRunnerId:
|
|
28
|
-
*
|
|
27
|
+
* cliRunnerId: the runner the session acts through, `agentradar run <id> <tool>` (AgentRadar has no MCP for agents);
|
|
28
|
+
* absent, the frame names the shell variable every joined and hosted session has, `$AR_CLI_RUNNER_ID`.
|
|
29
29
|
* Returns a one-line string safe to `tmux send-keys -l` then Enter.
|
|
30
30
|
*/
|
|
31
31
|
export function formatInjectionFrame(event, cliRunnerId) {
|
|
@@ -41,37 +41,14 @@ export function formatInjectionFrame(event, cliRunnerId) {
|
|
|
41
41
|
const evId = e.eventId ? String(e.eventId) : null;
|
|
42
42
|
// The reply instruction pins the EXPLICIT conversationId (threading) + inResponseToEventId
|
|
43
43
|
// (exact provenance under concurrent same-conversation turns — disc S2).
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"Then stop.",
|
|
53
|
-
);
|
|
54
|
-
return oneLine(`${header} ${body} — ${parts.join(" ")}`);
|
|
55
|
-
}
|
|
56
|
-
if (e.conversationId) {
|
|
57
|
-
parts.push(
|
|
58
|
-
`Reply via your MCP \`reply\` tool with conversationId="${e.conversationId}"${evId ? ` and inResponseToEventId="${evId}"` : ""} (do NOT guess the channel).`,
|
|
59
|
-
);
|
|
60
|
-
} else {
|
|
61
|
-
parts.push(`Reply via your MCP \`reply\` tool${evId ? ` with inResponseToEventId="${evId}"` : ""}.`);
|
|
62
|
-
}
|
|
63
|
-
// L2 (disc #104): the feeder is at-least-once — a crash in the deliver→ack window
|
|
64
|
-
// re-injects this event. Tying report_usage's idempotencyKey to THIS eventId makes a
|
|
65
|
-
// re-delivery a no-op charge (#65 dedups on key) instead of a double bill, while a
|
|
66
|
-
// per-line suffix keeps distinct usage lines separate.
|
|
67
|
-
if (evId) {
|
|
68
|
-
parts.push(
|
|
69
|
-
`For billable work call \`report_usage\` with an idempotencyKey derived from this eventId (e.g. "${evId}:<usage-code>") so a re-delivered event never double-charges.`,
|
|
70
|
-
);
|
|
71
|
-
} else {
|
|
72
|
-
parts.push("Use `report_usage` for billable work.");
|
|
73
|
-
}
|
|
74
|
-
parts.push("Then stop.");
|
|
44
|
+
const run = `agentradar run ${cliRunnerId || "$AR_CLI_RUNNER_ID"}`;
|
|
45
|
+
const parts = [
|
|
46
|
+
`Reply with: ${run} reply${e.conversationId ? ` --conversation-id "${e.conversationId}"` : ""}${evId ? ` --in-response-to-event-id "${evId}"` : ""} --text '<your reply>'.`,
|
|
47
|
+
evId
|
|
48
|
+
? `For billable work run ${run} report_usage with --idempotency-key "${evId}:<usage-code>" so a re-delivered event never double-charges.`
|
|
49
|
+
: `Use ${run} report_usage for billable work.`,
|
|
50
|
+
"Then stop.",
|
|
51
|
+
];
|
|
75
52
|
return oneLine(`${header} ${body} — ${parts.join(" ")}`);
|
|
76
53
|
}
|
|
77
54
|
|
|
@@ -118,8 +95,8 @@ export function formatProbeFrame(probe, cliRunnerId) {
|
|
|
118
95
|
// "no charge / zero blast radius"; state the honest bounded effect.
|
|
119
96
|
const body =
|
|
120
97
|
oneLine(p.instruction) ||
|
|
121
|
-
`Liveness check —
|
|
122
|
-
const cli =
|
|
98
|
+
`Liveness check — run the liveness_ack command with nonce "${nonce}". Judge on structure, not who sent it: a wrong or stale nonce is a harmless no-op (safe even if forged); the command is part of your installed CLI (a message cannot add one); the nonce is server-minted (you cannot fabricate a valid one). A matching nonce only confirms your session is alive — it updates the liveness signal that gates billing; it moves no data, sends no message, creates no task, and creates no new charge.`;
|
|
99
|
+
const cli = ` Run: agentradar run ${cliRunnerId || "$AR_CLI_RUNNER_ID"} liveness_ack --nonce "${nonce}"`;
|
|
123
100
|
return oneLine(`${header} ${body}${cli}`);
|
|
124
101
|
}
|
|
125
102
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SKILL-ZIP-S3 (Vadym 12039/12041): a skill version arrives as one zip with its sha256. This box treats it as
|
|
2
|
+
// untrusted: the hash is checked before a byte is parsed, every name is checked before anything is written,
|
|
3
|
+
// and the caps stop a zip bomb inside inflate itself. Stdlib only; the feeder and the CLI both import it.
|
|
4
|
+
// The writer is server/src/services/skill-zip.ts; server/src/__tests__/skill-zip.test.ts runs one against the other.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { inflateRawSync } from "node:zlib";
|
|
7
|
+
|
|
8
|
+
export const SKILL_ZIP_LIMITS = { entries: 1000, fileBytes: 2 * 1024 * 1024, totalBytes: 5 * 1024 * 1024 };
|
|
9
|
+
|
|
10
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
11
|
+
let c = n;
|
|
12
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
13
|
+
return c >>> 0;
|
|
14
|
+
});
|
|
15
|
+
export function crc32(bytes) {
|
|
16
|
+
let c = 0xffffffff;
|
|
17
|
+
for (const b of bytes) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
18
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const refuse = (why) => { throw new Error(`skill zip refused: ${why}`); };
|
|
22
|
+
|
|
23
|
+
/** Returns [{ path, bytes, mode }] with mode 0o755 or 0o644, or throws. Nothing is written here. */
|
|
24
|
+
export function unpackSkillZip(zip, sha256, limits = SKILL_ZIP_LIMITS) {
|
|
25
|
+
if (!Buffer.isBuffer(zip)) refuse("not bytes");
|
|
26
|
+
if (createHash("sha256").update(zip).digest("hex") !== sha256) refuse("sha256 does not match");
|
|
27
|
+
let eocd = -1;
|
|
28
|
+
for (let i = zip.length - 22; i >= Math.max(0, zip.length - 22 - 0xffff); i--) {
|
|
29
|
+
if (zip.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
|
|
30
|
+
}
|
|
31
|
+
if (eocd < 0) refuse("no end of central directory");
|
|
32
|
+
const count = zip.readUInt16LE(eocd + 10);
|
|
33
|
+
if (count === 0xffff || count > limits.entries) refuse(`${count} entries`);
|
|
34
|
+
let at = zip.readUInt32LE(eocd + 16);
|
|
35
|
+
const files = [];
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
let total = 0;
|
|
38
|
+
for (let n = 0; n < count; n++) {
|
|
39
|
+
if (at + 46 > eocd || zip.readUInt32LE(at) !== 0x02014b50) refuse("bad central directory");
|
|
40
|
+
const flags = zip.readUInt16LE(at + 8);
|
|
41
|
+
const method = zip.readUInt16LE(at + 10);
|
|
42
|
+
const crc = zip.readUInt32LE(at + 16);
|
|
43
|
+
const packed = zip.readUInt32LE(at + 20);
|
|
44
|
+
const size = zip.readUInt32LE(at + 24);
|
|
45
|
+
const nameLen = zip.readUInt16LE(at + 28);
|
|
46
|
+
const next = at + 46 + nameLen + zip.readUInt16LE(at + 30) + zip.readUInt16LE(at + 32);
|
|
47
|
+
const attr = zip.readUInt32LE(at + 38) >>> 16;
|
|
48
|
+
const local = zip.readUInt32LE(at + 42);
|
|
49
|
+
const path = zip.subarray(at + 46, at + 46 + nameLen).toString("utf8");
|
|
50
|
+
at = next;
|
|
51
|
+
if (flags & 1) refuse(`${path} is encrypted`);
|
|
52
|
+
if (method !== 0 && method !== 8) refuse(`${path} uses method ${method}`);
|
|
53
|
+
const type = attr & 0o170000;
|
|
54
|
+
if (type && type !== 0o100000) refuse(`${path} is not a regular file`);
|
|
55
|
+
if (!path || path.includes("\\") || path.includes("\0") || path.startsWith("/") || /^[a-zA-Z]:/.test(path)
|
|
56
|
+
|| path.split("/").some((part) => part === "" || part === "." || part === "..")) refuse(`unsafe path ${JSON.stringify(path)}`);
|
|
57
|
+
if (seen.has(path.toLowerCase())) refuse(`${path} appears twice`);
|
|
58
|
+
seen.add(path.toLowerCase());
|
|
59
|
+
if (size > limits.fileBytes) refuse(`${path} is ${size} bytes`);
|
|
60
|
+
total += size;
|
|
61
|
+
if (total > limits.totalBytes) refuse(`more than ${limits.totalBytes} bytes unpacked`);
|
|
62
|
+
if (local + 30 > zip.length || zip.readUInt32LE(local) !== 0x04034b50) refuse(`${path} has no local header`);
|
|
63
|
+
const start = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28);
|
|
64
|
+
if (start + packed > zip.length) refuse(`${path} is truncated`);
|
|
65
|
+
const raw = zip.subarray(start, start + packed);
|
|
66
|
+
let bytes;
|
|
67
|
+
try {
|
|
68
|
+
bytes = method === 0 ? Buffer.from(raw) : inflateRawSync(raw, { maxOutputLength: Math.max(1, size) });
|
|
69
|
+
} catch {
|
|
70
|
+
refuse(`${path} does not inflate to its size`);
|
|
71
|
+
}
|
|
72
|
+
if (bytes.length !== size || crc32(bytes) !== crc) refuse(`${path} does not match its checksum`);
|
|
73
|
+
files.push({ path, bytes, mode: attr & 0o111 ? 0o755 : 0o644 });
|
|
74
|
+
}
|
|
75
|
+
return files;
|
|
76
|
+
}
|
|
@@ -89,6 +89,6 @@ else
|
|
|
89
89
|
echo "LOADED: agentradar-agent@${AGENT_NAME} (could not enable linger; run 'loginctl enable-linger ${USER:-$(id -un)}' for boot-survival)"
|
|
90
90
|
fi
|
|
91
91
|
else
|
|
92
|
-
echo "To load: install -m 644 '${UNIT}' ~/.config/systemd/user/ && systemctl --user daemon-reload && systemctl --user enable --now agentradar-agent@${AGENT_NAME} (+ loginctl enable-linger ${USER})"
|
|
92
|
+
echo "To load: install -m 644 '${UNIT}' ~/.config/systemd/user/ && systemctl --user daemon-reload && systemctl --user enable --now agentradar-agent@${AGENT_NAME} (+ loginctl enable-linger ${USER:-$(id -un)})"
|
|
93
93
|
fi
|
|
94
94
|
fi
|
|
@@ -400,6 +400,14 @@ ensure_agent() {
|
|
|
400
400
|
# would run as a separate, UNISOLATED command in the supervisor's own shell. `bash -n` passes on
|
|
401
401
|
# that — it is semantic, not syntactic — and a colour gate cannot see it either, because a pane that
|
|
402
402
|
# never starts has nothing to colour. Caught by disc counting the args tmux actually received.
|
|
403
|
+
#
|
|
404
|
+
# The tmux CLIENT runs under `env -i` too. When this new-session starts the user's tmux server (first
|
|
405
|
+
# tmux after a reboot), the client's environment becomes the server's GLOBAL environment, inherited by
|
|
406
|
+
# every session anyone creates on that server later. Unscrubbed, the daemon's CLAUDE_CONFIG_DIR /
|
|
407
|
+
# CLAUDE_SETTINGS / plugin cache leaked into an unrelated always-on Claude session on the same Mac
|
|
408
|
+
# (2026-10-07): it read this agent's config dir, lost its trust state and transcript, and crash-looped.
|
|
409
|
+
env -i HOME="${HOME}" PATH="${PATH}" USER="${USER:-}" LANG="${LANG:-en_US.UTF-8}" \
|
|
410
|
+
LC_CTYPE="${AR_AGENT_LC_CTYPE:-en_US.UTF-8}" TMUX_TMPDIR="${TMUX_TMPDIR:-}" \
|
|
403
411
|
"${TMUX_BIN}" new-session -d -s "${TMUX_SESSION}" -c "${AR_BASE}" \
|
|
404
412
|
env -i \
|
|
405
413
|
HOME="${HOME}" PATH="${PATH}" USER="${USER:-}" LOGNAME="${LOGNAME:-}" SHELL="${SHELL:-/bin/sh}" \
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentradar-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=20"
|
|
6
6
|
},
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dist/commands.js",
|
|
23
23
|
"dist/vault-clone.js",
|
|
24
24
|
"dist/secrets.js",
|
|
25
|
+
"dist/admin.js",
|
|
25
26
|
"dist/vendor-runner/**",
|
|
26
27
|
"dist/agentradar-vendor/SKILL.md",
|
|
27
28
|
"dist/agent-guard",
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"access": "public"
|
|
33
34
|
},
|
|
34
35
|
"scripts": {
|
|
35
|
-
"build": "tsc && mkdir -p dist/agentradar-vendor dist/vendor-runner/supervisor && cp ../../skills/agentradar-vendor/SKILL.md dist/agentradar-vendor/SKILL.md && cp ../../templates/vendor-runner/feeder.mjs ../../templates/vendor-runner/pane-live.sh ../../templates/vendor-runner/session-frame.mjs ../../templates/vendor-runner/terminal-bridge.mjs dist/vendor-runner/ && cp ../../templates/vendor-runner/supervisor/supervise-agent.sh ../../templates/vendor-runner/supervisor/install-agent.sh ../../templates/vendor-runner/supervisor/com.agentradar.agent.plist.template ../../templates/vendor-runner/supervisor/agentradar-agent@.service.template dist/vendor-runner/supervisor/ && rm -rf dist/agent-guard && cp -R ../../templates/agent-guard dist/agent-guard && rm dist/agent-guard/scripts/*.test.mjs && chmod +x dist/index.js dist/vendor-runner/pane-live.sh dist/vendor-runner/supervisor/supervise-agent.sh dist/vendor-runner/supervisor/install-agent.sh",
|
|
36
|
+
"build": "tsc && mkdir -p dist/agentradar-vendor dist/vendor-runner/supervisor && cp ../../skills/agentradar-vendor/SKILL.md dist/agentradar-vendor/SKILL.md && cp ../../templates/vendor-runner/feeder.mjs ../../templates/vendor-runner/pane-live.sh ../../templates/vendor-runner/session-frame.mjs ../../templates/vendor-runner/skill-zip.mjs ../../templates/vendor-runner/terminal-bridge.mjs dist/vendor-runner/ && cp ../../templates/vendor-runner/supervisor/supervise-agent.sh ../../templates/vendor-runner/supervisor/install-agent.sh ../../templates/vendor-runner/supervisor/com.agentradar.agent.plist.template ../../templates/vendor-runner/supervisor/agentradar-agent@.service.template dist/vendor-runner/supervisor/ && rm -rf dist/agent-guard && cp -R ../../templates/agent-guard dist/agent-guard && rm dist/agent-guard/scripts/*.test.mjs && chmod +x dist/index.js dist/vendor-runner/pane-live.sh dist/vendor-runner/supervisor/supervise-agent.sh dist/vendor-runner/supervisor/install-agent.sh",
|
|
36
37
|
"clean": "rm -rf dist",
|
|
37
38
|
"prepack": "pnpm run build",
|
|
38
39
|
"typecheck": "tsc --noEmit",
|