agentradar-cli 0.3.9 → 0.3.11
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/scripts/compaction-report.mjs +13 -7
- package/dist/agent-guard/scripts/guard.mjs +9 -7
- package/dist/agent-guard/scripts/model-switch.mjs +1 -1
- package/dist/agent-guard/scripts/rm-guard.mjs +76 -4
- package/dist/agent-guard/skills/agentradar-discipline/SKILL.md +11 -7
- package/dist/agentradar-vendor/SKILL.md +1 -1
- package/dist/asset-get.js +4 -0
- package/dist/channel.js +128 -34
- package/dist/github.js +209 -0
- package/dist/index.js +31 -11
- package/dist/runner.js +10 -1
- package/dist/setup.js +225 -38
- package/dist/skill-install.js +13 -23
- package/dist/tool-run.js +196 -54
- package/dist/vendor-runner/feeder.mjs +53 -12
- package/dist/vendor-runner/session-frame.mjs +91 -47
- package/dist/vendor-runner/supervisor/supervise-agent.sh +6 -1
- package/package.json +3 -2
package/dist/runner.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import { shellValue, trustedMcpEndpoint, writePrivateFile } from "./setup.js";
|
|
7
|
+
import { claimPendingJoin, removeAgentService, shellValue, trustedMcpEndpoint, writePrivateFile } from "./setup.js";
|
|
8
8
|
import { callAwpTool, envValue } from "./tool-run.js";
|
|
9
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}$/;
|
|
@@ -224,7 +224,16 @@ export async function startVendorRunner(runnerId, home = homedir(), deps = {}) {
|
|
|
224
224
|
if (!RUNNER_ID.test(runnerId))
|
|
225
225
|
throw new Error("Invalid runner ID");
|
|
226
226
|
const log = deps.log ?? console.log;
|
|
227
|
+
// UAT-17: a join installed before its approval waits here for the key; a no-op once the env has one.
|
|
228
|
+
if (!await claimPendingJoin(runnerId, { home, fetch: deps.fetch, sleep: deps.sleep, log }))
|
|
229
|
+
return 1;
|
|
227
230
|
const envPath = join(home, ".agentradar", "agents", `${runnerId}.env`);
|
|
231
|
+
// UAT-17-INPLACE: the user's own launcher runs an in-place session, so this service only claimed the key; it removes itself.
|
|
232
|
+
if (envValue(await readFile(envPath, "utf8").catch(() => ""), "AR_INPLACE") === "1") {
|
|
233
|
+
log("Key claimed for the in-place session; its own launcher runs it with the AgentRadar channel. This service is done and removes itself.");
|
|
234
|
+
await (deps.removeService ?? ((id) => removeAgentService(id, { home })))(runnerId);
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
228
237
|
const agentDir = dirname(envPath);
|
|
229
238
|
const supervisor = join(dirname(fileURLToPath(import.meta.url)), "vendor-runner", "supervisor", "supervise-agent.sh");
|
|
230
239
|
const spawnChild = deps.spawn ?? ((command, args) => spawn(command, args, { stdio: "inherit", env: process.env }));
|
package/dist/setup.js
CHANGED
|
@@ -427,12 +427,15 @@ async function codexHookHashes(codexHome, env) {
|
|
|
427
427
|
child.kill();
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
|
-
|
|
430
|
+
const CODEX_BYPASS = "--dangerously-bypass-approvals-and-sandbox";
|
|
431
|
+
const CLAUDE_BYPASS = "--permission-mode bypassPermissions";
|
|
432
|
+
/** `pending`: a join not approved yet. The env carries no key, only where the service claims it (claimPendingJoin). */
|
|
433
|
+
async function installRunner(home, projectPath, approved, service, feed, pending) {
|
|
431
434
|
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(approved.runnerId)) {
|
|
432
435
|
throw new Error("AgentRadar returned an invalid runner ID");
|
|
433
436
|
}
|
|
434
437
|
const endpoint = trustedMcpEndpoint(approved.installation.mcpEndpoint);
|
|
435
|
-
if (!endpoint || !BEARER_SHAPE.test(approved.installation.bearer)) {
|
|
438
|
+
if (!endpoint || (!pending && !BEARER_SHAPE.test(approved.installation.bearer))) {
|
|
436
439
|
throw new Error("AgentRadar returned an invalid setup payload");
|
|
437
440
|
}
|
|
438
441
|
const agentDir = join(home, ".agentradar", "agents");
|
|
@@ -455,7 +458,7 @@ async function installRunner(home, projectPath, approved, service, feed) {
|
|
|
455
458
|
const { codexHome, keys } = await codexServiceHome(home, approved.runnerId, service.env);
|
|
456
459
|
joined.push(`CODEX_HOME=${shellValue(codexHome)}`);
|
|
457
460
|
if (service.bypass)
|
|
458
|
-
joined.push(`CODEX_EXTRA_FLAGS=${shellValue(
|
|
461
|
+
joined.push(`CODEX_EXTRA_FLAGS=${shellValue(CODEX_BYPASS)}`);
|
|
459
462
|
toolKeys = keys;
|
|
460
463
|
}
|
|
461
464
|
else if (harness === "generic") {
|
|
@@ -493,7 +496,7 @@ async function installRunner(home, projectPath, approved, service, feed) {
|
|
|
493
496
|
}, null, 2)}\n`);
|
|
494
497
|
if (!service.inPlace) {
|
|
495
498
|
if (service.bypass)
|
|
496
|
-
joined.push(`CLAUDE_EXTRA_FLAGS=${shellValue(
|
|
499
|
+
joined.push(`CLAUDE_EXTRA_FLAGS=${shellValue(CLAUDE_BYPASS)}`);
|
|
497
500
|
joined.push(`CLAUDE_SETTINGS=${shellValue(settings)}`, `CLAUDE_CODE_PLUGIN_CACHE_DIR=${shellValue(join(configDir, "plugins"))}`);
|
|
498
501
|
// Vadym 13444: the agent keeps every MCP and channel it already has; AgentRadar's channel is added next to them.
|
|
499
502
|
joined.push("AR_MCP_STRICT=0");
|
|
@@ -523,7 +526,9 @@ async function installRunner(home, projectPath, approved, service, feed) {
|
|
|
523
526
|
`AGENT_NAME=${shellValue(approved.runnerId)}`,
|
|
524
527
|
`AR_BASE=${shellValue(projectPath)}`,
|
|
525
528
|
`AWP_MCP_URL=${shellValue(endpoint.toString())}`,
|
|
526
|
-
|
|
529
|
+
...(pending
|
|
530
|
+
? [`AR_JOIN_STATE=${shellValue(pending.state)}`, `AR_JOIN_CLAIM_URL=${shellValue(pending.claimUrl)}`]
|
|
531
|
+
: [`AWP_BEARER=${shellValue(approved.installation.bearer)}`]),
|
|
527
532
|
`MCP_CONFIG=${shellValue(mcpConfig)}`,
|
|
528
533
|
// Every convention line in the brief and skills says `agentradar run $AR_CLI_RUNNER_ID <tool>`.
|
|
529
534
|
`AR_CLI_RUNNER_ID=${shellValue(approved.runnerId)}`,
|
|
@@ -674,6 +679,106 @@ async function installAgentService(runnerId, home, env) {
|
|
|
674
679
|
env: { ...env, HOME: home, NODE_BIN: process.execPath, AGENTRADAR_JS: join(dist, "index.js") },
|
|
675
680
|
});
|
|
676
681
|
}
|
|
682
|
+
/** What a key claim's answer means: null = not decided yet, keep polling; else the refusal in plain words. */
|
|
683
|
+
function claimRefusal(status, said) {
|
|
684
|
+
// A deploy's 502/503 or a rate limit is not the company's answer; refusing on one would remove the service.
|
|
685
|
+
if (status >= 500 || status === 429 || (status === 409 && said.includes("must be approved")))
|
|
686
|
+
return null;
|
|
687
|
+
if (status === 409 && said.includes("rejected"))
|
|
688
|
+
return "Your company declined this join request. Ask it for a new join link.";
|
|
689
|
+
if (status === 409 && said.includes("withdrawn"))
|
|
690
|
+
return "This join request was withdrawn (agentradar join cancel). Ask your company for a new join link.";
|
|
691
|
+
if (status === 409 && /already (?:used|claimed)/.test(said))
|
|
692
|
+
return "This join link's key was already claimed. A key is issued once and cannot be claimed again; ask your company for a new join link.";
|
|
693
|
+
return said;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* UAT-17 (lead ruling B): an agent's shell tool kills a command that runs for minutes, so a join that
|
|
697
|
+
* waited for the approval in the foreground never saw it. The service is now installed with the request, and
|
|
698
|
+
* `runner start` calls this first: while the runner env holds no key it polls the claim every 10 s, writes the
|
|
699
|
+
* key, and records the approval in the join state the foreground command watches. Then the supervisor starts
|
|
700
|
+
* the session, whose first poll brings the welcome. A refusal is recorded there too, and the service removes
|
|
701
|
+
* itself. The service is the only claimant, so a key is never claimed twice. False = stopped without a key.
|
|
702
|
+
*/
|
|
703
|
+
export async function claimPendingJoin(runnerId, deps = {}) {
|
|
704
|
+
const home = deps.home ?? homedir();
|
|
705
|
+
const log = deps.log ?? console.log;
|
|
706
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
707
|
+
const { envValue } = await import("./tool-run.js");
|
|
708
|
+
const envPath = join(home, ".agentradar", "agents", `${runnerId}.env`);
|
|
709
|
+
const saved = await readFile(envPath, "utf8").catch(() => "");
|
|
710
|
+
const statePath = envValue(saved, "AR_JOIN_STATE");
|
|
711
|
+
if (!statePath || envValue(saved, "AWP_BEARER"))
|
|
712
|
+
return true;
|
|
713
|
+
const leave = deps.leave ?? (() => leaveOrganisation(runnerId, { home, log }));
|
|
714
|
+
const state = await readFile(statePath, "utf8").then((text) => JSON.parse(text), () => null);
|
|
715
|
+
const refuse = async (said) => {
|
|
716
|
+
// Another claimant (the foreground fallback when the service install threw) already wrote the key: start on it, never remove it.
|
|
717
|
+
if (envValue(await readFile(envPath, "utf8").catch(() => ""), "AWP_BEARER"))
|
|
718
|
+
return true;
|
|
719
|
+
if (state)
|
|
720
|
+
await writePrivateFile(statePath, JSON.stringify({ ...state, refused: said }));
|
|
721
|
+
log(said);
|
|
722
|
+
await leave();
|
|
723
|
+
return false;
|
|
724
|
+
};
|
|
725
|
+
// The state file goes with `agentradar join cancel`; the claim secret goes only to the AgentRadar host that issued it.
|
|
726
|
+
if (!state)
|
|
727
|
+
return refuse("This join request was withdrawn (agentradar join cancel). Ask your company for a new join link.");
|
|
728
|
+
const claimUrl = trustedMcpEndpoint(envValue(saved, "AR_JOIN_CLAIM_URL") ?? "");
|
|
729
|
+
if (!claimUrl)
|
|
730
|
+
return refuse(`${envPath} names no AgentRadar address to claim the key from; run the join again.`);
|
|
731
|
+
for (let poll = 1; poll <= (deps.polls ?? Infinity); poll++) {
|
|
732
|
+
let response;
|
|
733
|
+
try {
|
|
734
|
+
response = await postJson(deps.fetch ?? fetch, claimUrl, { claimSecret: state.claimSecret, runnerId });
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
log(`Could not reach ${claimUrl.host}; retrying…`);
|
|
738
|
+
await sleep(10_000);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (response.status === 201) {
|
|
742
|
+
const claimed = await response.json();
|
|
743
|
+
if (typeof claimed.token !== "string" || !BEARER_SHAPE.test(claimed.token) || typeof claimed.agentId !== "string") {
|
|
744
|
+
return refuse("AgentRadar returned an invalid key claim");
|
|
745
|
+
}
|
|
746
|
+
// Bypass-permissions only when the operator consented at approval, and never for a pane the caller runs.
|
|
747
|
+
const token = claimed.token;
|
|
748
|
+
const approvedBypass = claimed.bypassPermissions === true;
|
|
749
|
+
const bypass = approvedBypass && envValue(saved, "AR_ATTACH") !== "1" && envValue(saved, "AR_INPLACE") !== "1";
|
|
750
|
+
const harness = envValue(saved, "AR_AGENT_HARNESS") ?? "claude";
|
|
751
|
+
// The same file installRunner writes for an approved join, line for line.
|
|
752
|
+
const lines = saved.split("\n").filter((line) => !line.startsWith("AR_JOIN_CLAIM_URL="))
|
|
753
|
+
.map((line) => line.startsWith("AR_JOIN_STATE=") ? `AWP_BEARER=${shellValue(token)}` : line);
|
|
754
|
+
const insert = (flag, at) => lines.splice(at < 0 ? lines.length - 1 : at, 0, flag);
|
|
755
|
+
if (bypass && harness === "codex")
|
|
756
|
+
insert(`CODEX_EXTRA_FLAGS=${shellValue(CODEX_BYPASS)}`, lines.findIndex((line) => line.startsWith("CODEX_HOME=")) + 1 || -1);
|
|
757
|
+
if (bypass && harness === "claude") {
|
|
758
|
+
insert(`CLAUDE_EXTRA_FLAGS=${shellValue(CLAUDE_BYPASS)}`, lines.findIndex((line) => line.startsWith("CLAUDE_SETTINGS=")));
|
|
759
|
+
const settings = join(home, ".agentradar", "config", runnerId, "agentradar-settings.json");
|
|
760
|
+
const current = JSON.parse(await readFile(settings, "utf8"));
|
|
761
|
+
await writePrivateFile(settings, `${JSON.stringify({ ...current, skipDangerousModePermissionPrompt: true }, null, 2)}\n`);
|
|
762
|
+
}
|
|
763
|
+
// The key on disk first: a state that says approved always has its key.
|
|
764
|
+
await writePrivateFile(envPath, lines.join("\n"));
|
|
765
|
+
await writePrivateFile(statePath, JSON.stringify({ ...state, agentId: claimed.agentId, ...(approvedBypass ? { bypassPermissions: true } : {}) }));
|
|
766
|
+
log("Approved: key claimed, starting the agent.");
|
|
767
|
+
// Best effort: the feeder syncs them again.
|
|
768
|
+
await syncJoinedSkills(runnerId, { home, fetch: deps.fetch, log }).catch((error) => {
|
|
769
|
+
log(`Your AgentRadar skills are not installed yet (${String(error?.message ?? error).split("\n")[0]}); the runner installs them once it is running.`);
|
|
770
|
+
});
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
const refusal = claimRefusal(response.status, await failureMessage(response));
|
|
774
|
+
if (refusal === null) {
|
|
775
|
+
await sleep(10_000);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
return refuse(refusal);
|
|
779
|
+
}
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
677
782
|
/**
|
|
678
783
|
* The Claude conversation that ran `agentradar join`: `--session`, else the CLAUDE_CODE_SESSION_ID Claude Code
|
|
679
784
|
* sets in its shell, else the newest transcript Claude Code keeps for this folder. Never a fresh one.
|
|
@@ -702,6 +807,26 @@ async function callerSession(deps, env, home, projectPath) {
|
|
|
702
807
|
* restarts the service) instead of filing a second request. The key is written only into the 0600 runner
|
|
703
808
|
* env file and is never printed.
|
|
704
809
|
*/
|
|
810
|
+
/**
|
|
811
|
+
* UAT-30 (Vadym 13405): a join names the runtime it runs on. `--runtime claude|codex|other`, else what the shell
|
|
812
|
+
* itself proves (AGENTRADAR_AGENT_BIN, CLAUDECODE, CODEX_HOME). Nothing is assumed: an unknown or unsupported one is
|
|
813
|
+
* refused before any request, with the command to run next.
|
|
814
|
+
*/
|
|
815
|
+
export function joinRuntime(flag, env) {
|
|
816
|
+
const map = { claude: "claude", codex: "codex", other: "generic" };
|
|
817
|
+
if (flag !== undefined) {
|
|
818
|
+
const key = flag.trim().toLowerCase();
|
|
819
|
+
const harness = Object.hasOwn(map, key) ? map[key] : undefined;
|
|
820
|
+
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.` };
|
|
821
|
+
}
|
|
822
|
+
if (env.AGENTRADAR_AGENT_BIN?.trim())
|
|
823
|
+
return { harness: "generic" };
|
|
824
|
+
if (env.CLAUDECODE)
|
|
825
|
+
return { harness: "claude" };
|
|
826
|
+
if (env.CODEX_HOME)
|
|
827
|
+
return { harness: "codex" };
|
|
828
|
+
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." };
|
|
829
|
+
}
|
|
705
830
|
export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
706
831
|
const fetchImpl = deps.fetch ?? fetch;
|
|
707
832
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -783,21 +908,29 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
783
908
|
log(`Your AgentRadar skills are not installed yet (${String(error?.message ?? error).split("\n")[0]}); the runner installs them once it is running.`);
|
|
784
909
|
});
|
|
785
910
|
if (inPlace) {
|
|
786
|
-
const config = join(home, ".agentradar", "config", runnerId);
|
|
787
911
|
if (approvedBypass)
|
|
788
912
|
log("Your company allowed bypass-permissions mode; this session keeps the permission mode it already runs in.");
|
|
789
|
-
log(`Connected, nothing started: your session, its MCPs and its channels are untouched. To receive client messages,
|
|
790
|
-
--mcp-config ${join(config, "awp-only.json")} --dangerously-load-development-channels server:agentradar --settings ${join(config, "agentradar-settings.json")}
|
|
791
|
-
and AR_CLI_RUNNER_ID=${runnerId} in its environment. Accept the one-time development-channel prompt when it appears. To stop: agentradar leave ${runnerId}, then drop those flags.`);
|
|
913
|
+
log(`Connected, nothing started: your session, its MCPs and its channels are untouched. To receive client messages, ${restartInPlace(runnerId)}`);
|
|
792
914
|
return 0;
|
|
793
915
|
}
|
|
794
916
|
try {
|
|
795
|
-
await
|
|
917
|
+
await installService(runnerId);
|
|
796
918
|
}
|
|
797
919
|
catch (error) {
|
|
798
920
|
log(`Could not start the service: ${error.message.split("\n")[0]}. Start it by hand with: agentradar runner start ${runnerId}`);
|
|
799
921
|
return 1;
|
|
800
922
|
}
|
|
923
|
+
return connected(runnerId, projectPath, approvedBypass, bypass);
|
|
924
|
+
};
|
|
925
|
+
const restartInPlace = (runnerId) => {
|
|
926
|
+
const config = join(home, ".agentradar", "config", runnerId);
|
|
927
|
+
return `restart your agent ONCE with these flags added to its own launch command (keep what it already has, including --continue):
|
|
928
|
+
--mcp-config ${join(config, "awp-only.json")} --dangerously-load-development-channels server:agentradar --settings ${join(config, "agentradar-settings.json")}
|
|
929
|
+
and AR_CLI_RUNNER_ID=${runnerId} in its environment. Accept the one-time development-channel prompt when it appears. To stop: agentradar leave ${runnerId}, then drop those flags.`;
|
|
930
|
+
};
|
|
931
|
+
const installService = deps.installService ?? ((id) => installAgentService(id, home, env));
|
|
932
|
+
const connected = (runnerId, projectPath, approvedBypass, bypass) => {
|
|
933
|
+
const { attach } = where;
|
|
801
934
|
if (attach) {
|
|
802
935
|
log(`Connected. Your welcome and every client message are typed into this session (tmux ${attach}), and you answer here. Nothing else was started. To stop: agentradar leave ${runnerId}`);
|
|
803
936
|
if (approvedBypass)
|
|
@@ -858,6 +991,12 @@ and AR_CLI_RUNNER_ID=${runnerId} in its environment. Accept the one-time develop
|
|
|
858
991
|
return startJoined(state.requestId, state.agentId, { mcpEndpoint: envValue(saved, "AWP_MCP_URL") ?? mcpEndpoint, bearer }, approvedBypass, bypass);
|
|
859
992
|
}
|
|
860
993
|
if (!state) {
|
|
994
|
+
// index.ts skips the runtime gate for --revoke-bypass (it edits an earlier join); a NEW join still needs a proven runtime.
|
|
995
|
+
if (deps.revokeBypass && !deps.harness) {
|
|
996
|
+
const proven = joinRuntime(undefined, env);
|
|
997
|
+
if ("refusal" in proven)
|
|
998
|
+
throw new Error(proven.refusal);
|
|
999
|
+
}
|
|
861
1000
|
const agentName = (deps.agentName ?? process.env.AGENTRADAR_AGENT_NAME ?? basename(cwd)).trim();
|
|
862
1001
|
if (!agentName)
|
|
863
1002
|
throw new Error("Run join from the agent project folder");
|
|
@@ -887,6 +1026,59 @@ and AR_CLI_RUNNER_ID=${runnerId} in its environment. Accept the one-time develop
|
|
|
887
1026
|
const claimUrl = api(`join-requests/${encodeURIComponent(state.requestId)}/claim-api-key`);
|
|
888
1027
|
// Named off the join request, which exists before the key does, so the server can record it at claim.
|
|
889
1028
|
const runnerId = `join-${state.requestId}`;
|
|
1029
|
+
{
|
|
1030
|
+
// UAT-17: the service waits for the approval and claims the key (claimPendingJoin); this command only watches.
|
|
1031
|
+
// In place (UAT-17-INPLACE) the service only claims: the user's own launcher runs the session the channel pushes into.
|
|
1032
|
+
const { refused: _retried, ...pending } = state;
|
|
1033
|
+
await writePrivateFile(statePath, JSON.stringify(pending));
|
|
1034
|
+
const projectPath = harness === "claude"
|
|
1035
|
+
? await installProjectMcp(claudeConfigPath(home, env), cwd, null, deps.trust === true)
|
|
1036
|
+
: await realpath(cwd);
|
|
1037
|
+
// A re-run never rewrites an env the service already holds: it may be writing the claimed key into it right now.
|
|
1038
|
+
const runnerEnv = join(home, ".agentradar", "agents", `${runnerId}.env`);
|
|
1039
|
+
if (!/^(?:AR_JOIN_STATE|AWP_BEARER)=/m.test(await readFile(runnerEnv, "utf8").catch(() => ""))) {
|
|
1040
|
+
await installRunner(home, projectPath, { status: "approved", agentId: "", runnerId, installation: { mcpEndpoint, bearer: "" }, harness }, { sessionId: where.sessionId ?? state.requestId, env, attach: where.attach, inPlace }, channel ? "push" : undefined, { state: statePath, claimUrl: claimUrl.toString() });
|
|
1041
|
+
}
|
|
1042
|
+
let serviced = true;
|
|
1043
|
+
try {
|
|
1044
|
+
await installService(runnerId);
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
serviced = false;
|
|
1048
|
+
log(`Could not start the service yet (${error.message.split("\n")[0]}); keep this command running until it says Connected.`);
|
|
1049
|
+
}
|
|
1050
|
+
if (serviced) {
|
|
1051
|
+
log(inPlace
|
|
1052
|
+
? `The AgentRadar service is installed and waits for the approval by itself. Once your company approves, it claims your key, and your welcome arrives in this session through the AgentRadar channel. Your session, its MCPs and its channels are untouched. To receive the welcome and client messages, ${restartInPlace(runnerId)}
|
|
1053
|
+
You can restart now or after the approval: until then the channel waits. You do not need to run anything again, and stopping this command is safe.`
|
|
1054
|
+
: `The AgentRadar service is installed and waits for the approval by itself. Once your company approves, it claims your key and delivers your welcome to your agent (${where.attach ? `typed into tmux ${where.attach}` : `tmux session ${runnerId}`}). You do not need to run anything again, and stopping this command is safe.`);
|
|
1055
|
+
for (let poll = 1;; poll++) {
|
|
1056
|
+
const now = await readFile(statePath, "utf8").then((text) => JSON.parse(text), () => ({}));
|
|
1057
|
+
if (now.refused)
|
|
1058
|
+
throw new Error(now.refused);
|
|
1059
|
+
if (now.agentId) {
|
|
1060
|
+
log("Approved.");
|
|
1061
|
+
log(`Runner installed → ${runnerEnv}`);
|
|
1062
|
+
if (inPlace) {
|
|
1063
|
+
log("Your key is claimed. Your welcome arrives through the AgentRadar channel once your agent runs with the flags above.");
|
|
1064
|
+
return 0;
|
|
1065
|
+
}
|
|
1066
|
+
const approvedBypass = now.bypassPermissions === true;
|
|
1067
|
+
const bypass = localBypass(approvedBypass, false);
|
|
1068
|
+
if (bypass)
|
|
1069
|
+
log(bypassOnNotice(enrolmentUrl));
|
|
1070
|
+
return connected(runnerId, projectPath, approvedBypass, bypass);
|
|
1071
|
+
}
|
|
1072
|
+
if (poll > 1 && (poll - 1) % 3 === 0)
|
|
1073
|
+
log(`Still waiting for your company to approve (${(poll - 1) * 10} s). The service connects you by itself once they do; stopping this command is safe.`);
|
|
1074
|
+
if (poll > (deps.polls ?? Infinity)) {
|
|
1075
|
+
log("Still waiting for approval. The service connects you by itself once your company approves; you do not need to run anything again.");
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
await sleep(10_000);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
890
1082
|
for (let poll = 1;; poll++) {
|
|
891
1083
|
if (poll > (deps.polls ?? Infinity)) {
|
|
892
1084
|
log(`Still waiting for approval. Run this same command again: agentradar run ${enrolmentUrl}`);
|
|
@@ -913,21 +1105,10 @@ and AR_CLI_RUNNER_ID=${runnerId} in its environment. Accept the one-time develop
|
|
|
913
1105
|
const joined = { ...state, agentId: claimed.agentId, ...(approvedBypass ? { bypassPermissions: true } : {}) };
|
|
914
1106
|
return startJoined(state.requestId, claimed.agentId, { mcpEndpoint, bearer: claimed.token }, approvedBypass, bypass, () => writePrivateFile(statePath, JSON.stringify(joined)));
|
|
915
1107
|
}
|
|
916
|
-
const
|
|
917
|
-
if (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
if (response.status === 409 && said.includes("rejected")) {
|
|
922
|
-
throw new Error("Your company declined this join request. Ask it for a new join link.");
|
|
923
|
-
}
|
|
924
|
-
if (response.status === 409 && said.includes("withdrawn")) {
|
|
925
|
-
throw new Error("This join request was withdrawn (agentradar join cancel). Ask your company for a new join link.");
|
|
926
|
-
}
|
|
927
|
-
if (response.status === 409 && /already (?:used|claimed)/.test(said)) {
|
|
928
|
-
throw new Error("This join link's key was already claimed. A key is issued once and cannot be claimed again; ask your company for a new join link.");
|
|
929
|
-
}
|
|
930
|
-
throw new Error(said);
|
|
1108
|
+
const refusal = claimRefusal(response.status, await failureMessage(response));
|
|
1109
|
+
if (refusal !== null)
|
|
1110
|
+
throw new Error(refusal);
|
|
1111
|
+
await sleep(10_000);
|
|
931
1112
|
}
|
|
932
1113
|
}
|
|
933
1114
|
/**
|
|
@@ -980,6 +1161,19 @@ export async function cancelJoin(target, deps = {}) {
|
|
|
980
1161
|
* `agentradar leave <runner-id>`: stop and remove the service, end the agent's session, and delete its env
|
|
981
1162
|
* file, bearer and config. The folder the agent worked in stays, with the work.
|
|
982
1163
|
*/
|
|
1164
|
+
/** The runner's LaunchAgent / systemd unit, unit file first: stopping it last ends a service that removes itself. */
|
|
1165
|
+
export async function removeAgentService(runnerId, deps = {}) {
|
|
1166
|
+
const home = deps.home ?? homedir();
|
|
1167
|
+
const execute = deps.execute ?? ((command, args) => promisify(execFile)(command, args));
|
|
1168
|
+
const label = `com.agentradar.agent.${runnerId}`;
|
|
1169
|
+
if ((deps.platform ?? process.platform) === "darwin") {
|
|
1170
|
+
for (const dir of [join(home, "Library", "LaunchAgents"), join(home, ".agentradar", "staged")])
|
|
1171
|
+
await rm(join(dir, `${label}.plist`), { force: true });
|
|
1172
|
+
await execute("launchctl", ["bootout", `gui/${process.getuid?.()}/${label}`]).catch(() => undefined);
|
|
1173
|
+
}
|
|
1174
|
+
else
|
|
1175
|
+
await execute("systemctl", ["--user", "disable", "--now", `agentradar-agent@${runnerId}`]).catch(() => undefined);
|
|
1176
|
+
}
|
|
983
1177
|
export async function leaveOrganisation(runnerId, deps = {}) {
|
|
984
1178
|
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(runnerId))
|
|
985
1179
|
throw new Error("Invalid runner ID");
|
|
@@ -993,23 +1187,16 @@ export async function leaveOrganisation(runnerId, deps = {}) {
|
|
|
993
1187
|
return 1;
|
|
994
1188
|
}
|
|
995
1189
|
const { envValue } = await import("./tool-run.js");
|
|
996
|
-
|
|
997
|
-
const label = `com.agentradar.agent.${runnerId}`;
|
|
998
|
-
await execute("launchctl", ["bootout", `gui/${process.getuid?.()}/${label}`]).catch(() => undefined);
|
|
999
|
-
await rm(join(home, "Library", "LaunchAgents", `${label}.plist`), { force: true });
|
|
1000
|
-
await rm(join(home, ".agentradar", "staged", `${label}.plist`), { force: true });
|
|
1001
|
-
}
|
|
1002
|
-
else {
|
|
1003
|
-
await execute("systemctl", ["--user", "disable", "--now", `agentradar-agent@${runnerId}`]).catch(() => undefined);
|
|
1004
|
-
}
|
|
1005
|
-
// The service stopping leaves the session running; `=` matches the tmux session name exactly.
|
|
1006
|
-
// An attached pane or an in-place join is the caller's own session: it stays.
|
|
1007
|
-
if (envValue(saved, "AR_ATTACH") !== "1" && envValue(saved, "AR_INPLACE") !== "1")
|
|
1008
|
-
await execute(envValue(saved, "TMUX_BIN") ?? "tmux", ["kill-session", "-t", `=${envValue(saved, "TMUX_SESSION") ?? runnerId}`]).catch(() => undefined);
|
|
1190
|
+
// Files first: a service that leaves by itself (a refused join, claimPendingJoin) ends when it is stopped.
|
|
1009
1191
|
for (const path of [envPath, join(home, ".agentradar", "agents", `${runnerId}.delivered.json`)])
|
|
1010
1192
|
await rm(path, { force: true });
|
|
1011
1193
|
for (const dir of ["run", "config", "codex"])
|
|
1012
1194
|
await rm(join(home, ".agentradar", dir, runnerId), { recursive: true, force: true });
|
|
1195
|
+
await removeAgentService(runnerId, { ...deps, home, execute });
|
|
1196
|
+
// The service stopping leaves the session running; `=` matches the tmux session name exactly.
|
|
1197
|
+
// An attached pane or an in-place join is the caller's own session: it stays.
|
|
1198
|
+
if (envValue(saved, "AR_ATTACH") !== "1" && envValue(saved, "AR_INPLACE") !== "1")
|
|
1199
|
+
await execute(envValue(saved, "TMUX_BIN") ?? "tmux", ["kill-session", "-t", `=${envValue(saved, "TMUX_SESSION") ?? runnerId}`]).catch(() => undefined);
|
|
1013
1200
|
log(`Left: the service is stopped and removed, and its key is deleted. The folder ${envValue(saved, "AR_BASE") ?? ""} stays, with the work.`);
|
|
1014
1201
|
return 0;
|
|
1015
1202
|
}
|
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
|