agentradar-cli 0.3.9 → 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/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 +10 -6
- package/dist/channel.js +94 -28
- package/dist/github.js +209 -0
- package/dist/index.js +31 -11
- package/dist/setup.js +26 -0
- package/dist/skill-install.js +13 -23
- package/dist/tool-run.js +193 -54
- package/dist/vendor-runner/feeder.mjs +53 -12
- package/dist/vendor-runner/session-frame.mjs +72 -37
- package/dist/vendor-runner/supervisor/supervise-agent.sh +6 -1
- package/package.json +3 -2
package/dist/github.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vadym 18123: an agent someone gave GitHub access uses plain `gh` and `git` as itself, with nothing to set up.
|
|
3
|
+
*
|
|
4
|
+
* setup writes the runner's `gh` shim and a git config naming this CLI as git's credential helper for the agent's
|
|
5
|
+
* GitHub host: the AgentRadar proxy host for a linked account (remotes of the client's orgs are rewritten to it;
|
|
6
|
+
* any other github.com remote stays plain git), else
|
|
7
|
+
* github.com for a GitHub App token. The supervisor runs it at launch and puts both on the agent's PATH and
|
|
8
|
+
* GIT_CONFIG; `gh` and `git` then sign in as the agent.
|
|
9
|
+
* credential git's credential helper: mints a fresh `github_token` for each request and hands it to git on its pipe.
|
|
10
|
+
* gh the shim's target: mints a token and runs the real `gh` with it in the environment. A command whose repositories
|
|
11
|
+
* all belong to someone else (a public repo, an open-source checkout, the agent's own account) is refused, with why,
|
|
12
|
+
* except `gh api`: the proxy serves GET on public repositories of any owner (GH-PROXY-PUBLIC-READS).
|
|
13
|
+
* A token is never written to disk or put on a command line. Without one, gh runs as it would have (the machine's login).
|
|
14
|
+
*/
|
|
15
|
+
import { execFile, spawn } from "node:child_process";
|
|
16
|
+
import { constants } from "node:fs";
|
|
17
|
+
import { access, chmod, mkdir, realpath, rename, writeFile } from "node:fs/promises";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { delimiter, join } from "node:path";
|
|
20
|
+
import { awpRequest, RUNNER_ID } from "./tool-run.js";
|
|
21
|
+
export const GITHUB_USAGE = "Usage: agentradar github setup <runner-id> | agentradar github gh <runner-id> -- <gh args> | agentradar github credential <runner-id> get";
|
|
22
|
+
const HOST = /^[A-Za-z0-9.-]+(:\d+)?$/;
|
|
23
|
+
const OWNER = /^[A-Za-z0-9-]+$/;
|
|
24
|
+
export const githubDir = (runnerId, deps) => join(deps.home ?? homedir(), ".agentradar", "run", "github", runnerId);
|
|
25
|
+
const sq = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
26
|
+
/** One `github_token` call with the runner's own key. */
|
|
27
|
+
export async function mintGithub(runnerId, deps) {
|
|
28
|
+
const said = await awpRequest(runnerId, "tools/call", { name: "github_token", arguments: {} }, deps);
|
|
29
|
+
const text = said?.result?.content?.[0]?.text ?? "";
|
|
30
|
+
let body;
|
|
31
|
+
try {
|
|
32
|
+
body = JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { error: text || "AgentRadar did not answer" };
|
|
36
|
+
}
|
|
37
|
+
if (said?.result?.isError || !body.ok || !body.token)
|
|
38
|
+
return { error: body.error ?? text };
|
|
39
|
+
if (body.host !== undefined && !HOST.test(body.host))
|
|
40
|
+
return { error: "AgentRadar returned an invalid GitHub host" };
|
|
41
|
+
const owners = Array.isArray(body.owners) ? body.owners.filter((o) => typeof o === "string" && OWNER.test(o)) : undefined;
|
|
42
|
+
return { token: body.token, host: body.host, owners, ...(typeof body.login === "string" && OWNER.test(body.login) ? { login: body.login } : {}) };
|
|
43
|
+
}
|
|
44
|
+
/** The include file the supervisor points GIT_CONFIG at; written whole and renamed, so git never reads half of it. */
|
|
45
|
+
async function writeGitconfig(runnerId, { host, owners }, bin, deps) {
|
|
46
|
+
const dir = githubDir(runnerId, deps);
|
|
47
|
+
const helper = `!${sq(bin)} github credential ${runnerId}`.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
48
|
+
// The empty helper drops the machine's own helpers (a keychain login) for this host, so git signs in as the agent.
|
|
49
|
+
const lines = [`[credential "https://${host ?? "github.com"}"]`, "\thelper =", `\thelper = "${helper}"`];
|
|
50
|
+
// Only the client's orgs go through the proxy; a public repo or the vendor's own stays plain git. An older AgentRadar
|
|
51
|
+
// that names no owners gets the old whole-github.com rewrite.
|
|
52
|
+
// ponytail: insteadOf is a case-sensitive prefix; a remote spelling the org in other capitals than GitHub's stays plain git.
|
|
53
|
+
if (host)
|
|
54
|
+
for (const owner of owners ? [...new Set(owners.flatMap((o) => [o, o.toLowerCase()]))].map((o) => `${o}/`) : [""]) {
|
|
55
|
+
lines.push(`[url "https://${host}/${owner}"]`, `\tinsteadOf = https://github.com/${owner}`, `\tinsteadOf = git@github.com:${owner}`, `\tinsteadOf = ssh://git@github.com/${owner}`);
|
|
56
|
+
}
|
|
57
|
+
await mkdir(dir, { recursive: true });
|
|
58
|
+
const tmp = join(dir, `gitconfig.${process.pid}`);
|
|
59
|
+
await writeFile(tmp, lines.join("\n") + "\n", { mode: 0o600 });
|
|
60
|
+
await rename(tmp, join(dir, "gitconfig"));
|
|
61
|
+
}
|
|
62
|
+
export async function githubSetup(runnerId, deps) {
|
|
63
|
+
const logError = deps.logError ?? console.error;
|
|
64
|
+
const bin = (deps.env ?? process.env).AR_AGENTRADAR_BIN;
|
|
65
|
+
if (!bin) {
|
|
66
|
+
logError("AR_AGENTRADAR_BIN is not set.");
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const shim = join(githubDir(runnerId, deps), "bin", "gh");
|
|
70
|
+
await mkdir(join(githubDir(runnerId, deps), "bin"), { recursive: true });
|
|
71
|
+
await writeFile(shim, `#!/bin/sh\nexec ${sq(bin)} github gh ${runnerId} -- "$@"\n`);
|
|
72
|
+
await chmod(shim, 0o755);
|
|
73
|
+
const minted = await mintGithub(runnerId, deps);
|
|
74
|
+
// A failed mint keeps the last config: no GitHub access yet, or a passing outage, is not a reason to drop it.
|
|
75
|
+
if ("error" in minted) {
|
|
76
|
+
logError(`No GitHub token for this agent: ${minted.error}`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
await writeGitconfig(runnerId, minted, bin, deps);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
/** git's `credential.helper` protocol: answer `get` for the agent's own host, stay silent for any other. */
|
|
83
|
+
export async function githubCredential(runnerId, op, deps) {
|
|
84
|
+
if (op !== "get")
|
|
85
|
+
return 0;
|
|
86
|
+
const input = await (deps.stdin ?? (async () => { const chunks = []; for await (const chunk of process.stdin)
|
|
87
|
+
chunks.push(chunk); return Buffer.concat(chunks).toString("utf8"); }))();
|
|
88
|
+
const asked = Object.fromEntries(input.split("\n").filter((l) => l.includes("=")).map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1)]));
|
|
89
|
+
const minted = await mintGithub(runnerId, deps);
|
|
90
|
+
if ("error" in minted) {
|
|
91
|
+
(deps.logError ?? console.error)(`agentradar: no GitHub token for this agent (${minted.error}).`);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
// A link or unlink since launch moves the host: rewrite the config so the next git call goes to the right one.
|
|
95
|
+
const bin = (deps.env ?? process.env).AR_AGENTRADAR_BIN;
|
|
96
|
+
if (bin)
|
|
97
|
+
await writeGitconfig(runnerId, minted, bin, deps).catch(() => { });
|
|
98
|
+
if (asked.protocol === "https" && asked.host === (minted.host ?? "github.com")) {
|
|
99
|
+
(deps.log ?? console.log)(`username=x-access-token\npassword=${minted.token}`);
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
/** The real `gh` on PATH, skipping this runner's own shim however PATH spells it (a HOME ending in "/", a symlink). */
|
|
104
|
+
async function realGh(shim, env) {
|
|
105
|
+
const self = await realpath(shim).catch(() => shim);
|
|
106
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
107
|
+
if (!dir || !(await access(join(dir, "gh"), constants.X_OK).then(() => true, () => false)))
|
|
108
|
+
continue;
|
|
109
|
+
if (await realpath(join(dir, "gh")).catch(() => "") !== self)
|
|
110
|
+
return join(dir, "gh");
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const IN_URL = /(?:^|[/@])github\.com[/:]([A-Za-z0-9-]+)(?=\/|$)/gim;
|
|
115
|
+
const ownerOf = (value = "") => value.match(new RegExp(IN_URL.source, "i"))?.[1] ?? value.match(/^([A-Za-z0-9-]+)\/[A-Za-z0-9._-]+$/)?.[1];
|
|
116
|
+
/** The repository owners a gh command names: -R/--repo, `gh repo <verb> OWNER/REPO`, github.com URLs, `gh api` repos|orgs|users paths. */
|
|
117
|
+
export function ghTargets(args) {
|
|
118
|
+
return args.flatMap((arg, i) => {
|
|
119
|
+
const named = arg === "-R" || arg === "--repo" ? ownerOf(args[i + 1]) : arg.startsWith("--repo=") ? ownerOf(arg.slice(7))
|
|
120
|
+
: /^-R./.test(arg) ? ownerOf(arg.slice(2)) : new RegExp(IN_URL.source, "i").test(arg) ? ownerOf(arg)
|
|
121
|
+
: args[0] === "api" ? arg.match(/^\/?(?:repos|orgs|users)\/([A-Za-z0-9-]+)/)?.[1]
|
|
122
|
+
: args[0] === "repo" && i === 2 && !arg.startsWith("-") ? ownerOf(arg) : undefined;
|
|
123
|
+
return named ? [named] : [];
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The owner gh must not run for, or null for the proxy. Named owners decide; without any, the checkout's remotes do
|
|
128
|
+
* (a client's remote is rewritten to the proxy host). Nothing to go on (no repo, `gh api user`) stays on the proxy.
|
|
129
|
+
*/
|
|
130
|
+
async function outsideOwner(args, host, owners, env, cwd) {
|
|
131
|
+
let targets = ghTargets(args);
|
|
132
|
+
if (!targets.length) {
|
|
133
|
+
const remotes = await new Promise((resolve) => execFile("git", ["remote", "-v"], { env, cwd, timeout: 5000 }, (_e, out) => resolve(String(out ?? ""))));
|
|
134
|
+
if (remotes.includes(`://${host}/`))
|
|
135
|
+
return null;
|
|
136
|
+
targets = [...remotes.matchAll(IN_URL)].map((m) => m[1]);
|
|
137
|
+
}
|
|
138
|
+
const client = new Set(owners.map((o) => o.toLowerCase()));
|
|
139
|
+
return targets.length && !targets.some((t) => client.has(t.toLowerCase())) ? targets[0] : null;
|
|
140
|
+
}
|
|
141
|
+
export async function githubGh(runnerId, args, deps) {
|
|
142
|
+
const logError = deps.logError ?? console.error;
|
|
143
|
+
const env = deps.env ?? process.env;
|
|
144
|
+
const gh = await realGh(join(githubDir(runnerId, deps), "bin", "gh"), env);
|
|
145
|
+
if (!gh) {
|
|
146
|
+
logError("agentradar: gh is not installed on this machine.");
|
|
147
|
+
return 127;
|
|
148
|
+
}
|
|
149
|
+
// Already inside the shim (gh running gh, or a PATH the check above still missed): the token is in the environment,
|
|
150
|
+
// so run gh as is and mint nothing. Without this a loop mints a token at every level.
|
|
151
|
+
if (env.AR_GH_SHIMMED === "1")
|
|
152
|
+
return await run(gh, args, env, deps.cwd);
|
|
153
|
+
const minted = await mintGithub(runnerId, deps);
|
|
154
|
+
const childEnv = { ...env, AR_GH_SHIMMED: "1" };
|
|
155
|
+
if ("error" in minted) {
|
|
156
|
+
logError(`agentradar: no GitHub token for this agent (${minted.error}); running gh with this machine's own login.`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
if (env.AR_AGENTRADAR_BIN)
|
|
160
|
+
await writeGitconfig(runnerId, minted, env.AR_AGENTRADAR_BIN, deps).catch(() => { });
|
|
161
|
+
const outside = minted.host && minted.owners && args[0] !== "api" ? await outsideOwner(args, minted.host, minted.owners, env, deps.cwd) : null;
|
|
162
|
+
if (outside) {
|
|
163
|
+
// Not run at all: gh signed out refuses every command ("gh auth login", exit 4), and the agent's token reaches only
|
|
164
|
+
// the client's orgs. Anonymous git still clones public repositories.
|
|
165
|
+
const clients = minted.owners.join(", ");
|
|
166
|
+
const reply = "run `agentradar run $AR_CLI_RUNNER_ID reply` in Slack, or `agentradar run $AR_CLI_RUNNER_ID telegram_reply`, `agentradar run $AR_CLI_RUNNER_ID email_reply`, " +
|
|
167
|
+
"`agentradar run $AR_CLI_RUNNER_ID whatsapp_reply` or `agentradar run $AR_CLI_RUNNER_ID discord_reply` for the conversation you work in";
|
|
168
|
+
logError(outside.toLowerCase() === minted.login?.toLowerCase()
|
|
169
|
+
? `agentradar: ${outside} is your own GitHub account, so gh did not run: repositories under your own account are read-only by design. ` +
|
|
170
|
+
`Read public ones with \`gh api repos/${outside}/<repo>\` or git clone; private ones there are not reachable. Your work belongs in your client's GitHub orgs (${clients}). ` +
|
|
171
|
+
`If you need a repository there, ask your client to create it: ${reply}.`
|
|
172
|
+
: `agentradar: gh works only on your client's GitHub orgs (${clients}), so it did not run for ${outside}. To read a public repository there, ` +
|
|
173
|
+
`use \`gh api repos/${outside}/<repo>\` (reads only) or git clone https://github.com/${outside}/<repo>. If ${outside} is your client's, ask your client to add it: ${reply}.`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
if (minted.host)
|
|
177
|
+
Object.assign(childEnv, { GH_HOST: minted.host, GH_ENTERPRISE_TOKEN: minted.token });
|
|
178
|
+
else
|
|
179
|
+
childEnv.GH_TOKEN = minted.token;
|
|
180
|
+
}
|
|
181
|
+
return await run(gh, args, childEnv, deps.cwd);
|
|
182
|
+
}
|
|
183
|
+
const run = (bin, args, env, cwd) => new Promise((resolve) => {
|
|
184
|
+
const child = spawn(bin, args, { stdio: "inherit", env, cwd });
|
|
185
|
+
child.on("error", () => resolve(127));
|
|
186
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
187
|
+
});
|
|
188
|
+
export async function githubOverCli(args, deps = {}) {
|
|
189
|
+
const [verb, runnerId, ...rest] = args;
|
|
190
|
+
if (!runnerId || !RUNNER_ID.test(runnerId)) {
|
|
191
|
+
(deps.logError ?? console.error)(GITHUB_USAGE);
|
|
192
|
+
return 2;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
if (verb === "setup" && !rest.length)
|
|
196
|
+
return await githubSetup(runnerId, deps);
|
|
197
|
+
if (verb === "credential" && rest.length === 1)
|
|
198
|
+
return await githubCredential(runnerId, rest[0], deps);
|
|
199
|
+
if (verb === "gh" && rest[0] === "--")
|
|
200
|
+
return await githubGh(runnerId, rest.slice(1), deps);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
(deps.logError ?? console.error)(error instanceof Error ? error.message : String(error));
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
(deps.logError ?? console.error)(GITHUB_USAGE);
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=github.js.map
|
package/dist/index.js
CHANGED
|
@@ -4,14 +4,15 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { installVendorSkill } from "./skill-install.js";
|
|
5
5
|
import { startChannel } from "./channel.js";
|
|
6
6
|
import { runRestartSelftest, startVendorRunner } from "./runner.js";
|
|
7
|
-
import { cancelJoin, CODEX_GUARD_REFUSAL, dropStaleMcp, ensureCodexGuard, joinOrganisation, leaveOrganisation, setupVendorAgent } from "./setup.js";
|
|
8
|
-
import { callAwpTool, callToolOverCli, EXIT, parseCallArgs } from "./tool-run.js";
|
|
7
|
+
import { cancelJoin, CODEX_GUARD_REFUSAL, dropStaleMcp, ensureCodexGuard, joinOrganisation, joinRuntime, leaveOrganisation, setupVendorAgent } from "./setup.js";
|
|
8
|
+
import { callAwpTool, callToolOverCli, EXIT, parseCallArgs, schemaOverCli } from "./tool-run.js";
|
|
9
9
|
import { installSyncTimer, syncAgentDir } from "./sync.js";
|
|
10
10
|
import { importVaultOverCli } from "./vault-import.js";
|
|
11
11
|
import { cloneVaultOverCli } from "./vault-clone.js";
|
|
12
12
|
import { ASSET_USAGE, getAssetOverCli } from "./asset-get.js";
|
|
13
13
|
import { secretsOverCli } from "./secrets.js";
|
|
14
14
|
import { adminOverCli } from "./admin.js";
|
|
15
|
+
import { githubOverCli } from "./github.js";
|
|
15
16
|
// `run` is the one prefix every agent-facing command shares, so one harness "always allow agentradar run"
|
|
16
17
|
// covers a whole session: `run <link>` (or `run join <link>`) joins, `run <runner-id> <tool>` calls a tool.
|
|
17
18
|
export const runArgs = (raw) => raw[0] !== "run" ? raw
|
|
@@ -29,22 +30,24 @@ const USAGE = [
|
|
|
29
30
|
" The runner is $AR_CLI_RUNNER_ID, or --runner <id>. --dry-run prints the exact call and sends nothing.",
|
|
30
31
|
" Exit codes: 0 done; 1 refused or unreachable (the reason is on stderr); 2 the command was wrong.",
|
|
31
32
|
"Local commands:",
|
|
32
|
-
" agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--no-service | --service]",
|
|
33
|
+
" agentradar join <join-link> [--name <n>] [--model <m>] [--runtime claude|codex|other] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--no-service | --service]",
|
|
33
34
|
" agentradar join cancel <join-link|request-id>",
|
|
34
35
|
" agentradar setup [--trust] [--harness claude|codex]",
|
|
35
36
|
" agentradar leave <runner-id>",
|
|
36
37
|
" agentradar channel <runner-id>",
|
|
37
38
|
" agentradar selftest <runner-id> [--status|--restart]",
|
|
38
39
|
" agentradar runner start <runner-id>",
|
|
39
|
-
" agentradar call <runner-id> <tool | command> [--<flag> <value> ...] [--
|
|
40
|
+
" agentradar call <runner-id> <tool | command> [--<flag> <value> ...] [--input <json|file|->]",
|
|
40
41
|
" agentradar vault import <runner-id> <dir> [--vault <vault-id>] [--source <name>] [--write]",
|
|
41
42
|
" agentradar vault clone <runner-id> <vault-id> [dir]",
|
|
42
43
|
" agentradar asset get <runner-id> <asset-id> [file]",
|
|
43
44
|
" agentradar secrets pull|renew|env <runner-id> | agentradar secrets exec <runner-id> -- <command>",
|
|
44
45
|
" agentradar secrets upload|rotate <runner-id> --env-file <path> | --from-env <NAME,...> | --prompt <NAME> [--replace]",
|
|
46
|
+
" agentradar github setup <runner-id> | agentradar github gh <runner-id> -- <gh args> | agentradar github credential <runner-id> get",
|
|
45
47
|
" agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]",
|
|
46
48
|
" agentradar admin broadcast --dry-run|--send --target all --text <text> --cta <next step>",
|
|
47
|
-
" agentradar
|
|
49
|
+
" agentradar admin notice --dry-run|--send --agent <id> --ref <key> --file <text file> --cta <next step> [--wake]",
|
|
50
|
+
" agentradar schema <command> | agentradar upgrade | agentradar version [--json]",
|
|
48
51
|
].join("\n");
|
|
49
52
|
async function main() {
|
|
50
53
|
const args = runArgs(process.argv.slice(2));
|
|
@@ -80,8 +83,9 @@ async function main() {
|
|
|
80
83
|
process.exitCode = await leaveOrganisation(args[1]);
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
83
|
-
if (args.length === 1 && (args[0] === "--version" || args[0] === "-v")) {
|
|
84
|
-
|
|
86
|
+
if ((args.length === 1 && (args[0] === "--version" || args[0] === "-v")) || (args[0] === "version" && (args.length === 1 || (args.length === 2 && args[1] === "--json")))) {
|
|
87
|
+
const version = createRequire(import.meta.url)("../package.json").version;
|
|
88
|
+
console.log(args[1] === "--json" ? JSON.stringify({ version }) : version);
|
|
85
89
|
return;
|
|
86
90
|
}
|
|
87
91
|
if (args[0] === "channel" && args.length === 2) {
|
|
@@ -126,18 +130,25 @@ async function main() {
|
|
|
126
130
|
const named = {};
|
|
127
131
|
const urls = [];
|
|
128
132
|
for (let i = 1; i < args.length; i++) {
|
|
129
|
-
if (["--name", "--model", "--attach-tmux", "--session"].includes(args[i]))
|
|
133
|
+
if (["--name", "--model", "--attach-tmux", "--session", "--runtime"].includes(args[i]))
|
|
130
134
|
named[args[i].slice(2)] = args[++i] ?? "-";
|
|
131
135
|
else if (!["--trust", "--attach-current", "--allow-bypass", "--revoke-bypass", "--feeder", "--service", "--no-service"].includes(args[i]))
|
|
132
136
|
urls.push(args[i]);
|
|
133
137
|
}
|
|
134
138
|
if (urls.length !== 1 || [urls[0], ...Object.values(named)].some((value) => value.startsWith("-")) || (named["attach-tmux"] && args.includes("--attach-current")) || (args.includes("--service") && args.includes("--no-service"))) {
|
|
135
|
-
console.error("Usage: agentradar join <join-link> [--name <your name>] [--model <your model id>] [--trust] [--attach-tmux <session[:window.pane]> | --attach-current (Codex and other CLIs)] [--session <claude session id>] [--revoke-bypass] [--no-service | --service]");
|
|
139
|
+
console.error("Usage: agentradar join <join-link> [--name <your name>] [--model <your model id>] [--runtime <claude|codex|other>] [--trust] [--attach-tmux <session[:window.pane]> | --attach-current (Codex and other CLIs)] [--session <claude session id>] [--revoke-bypass] [--no-service | --service]");
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// --revoke-bypass only edits what an earlier join wrote, so it needs no runtime.
|
|
144
|
+
const runtime = args.includes("--revoke-bypass") ? { harness: undefined } : joinRuntime(named.runtime, process.env);
|
|
145
|
+
if ("refusal" in runtime) {
|
|
146
|
+
console.error(runtime.refusal);
|
|
136
147
|
process.exitCode = 1;
|
|
137
148
|
return;
|
|
138
149
|
}
|
|
139
150
|
process.exitCode = await joinOrganisation(urls[0], {
|
|
140
|
-
trust: args.includes("--trust"), agentName: named.name, model: named.model, session: named.session,
|
|
151
|
+
harness: runtime.harness, trust: args.includes("--trust"), agentName: named.name, model: named.model, session: named.session,
|
|
141
152
|
attach: args.includes("--attach-current") ? "current" : named["attach-tmux"],
|
|
142
153
|
revokeBypass: args.includes("--revoke-bypass"), feeder: args.includes("--feeder"), service: args.includes("--service"),
|
|
143
154
|
});
|
|
@@ -157,7 +168,7 @@ async function main() {
|
|
|
157
168
|
const parsed = parseCallArgs(args.slice(1));
|
|
158
169
|
const [runnerId, ...words] = parsed?.words ?? [];
|
|
159
170
|
if (!parsed || !runnerId || words.length > 3 || (!words.length && !parsed.flags.some(([flag]) => flag === "help"))) {
|
|
160
|
-
console.error("Usage: agentradar call <runner-id> <tool | command> [--<flag> <value> ...] [--
|
|
171
|
+
console.error("Usage: agentradar call <runner-id> <tool | command> [--<flag> <value> ...] [--input <json|file|->] | agentradar call <runner-id> [<command>] --help");
|
|
161
172
|
process.exitCode = EXIT.usage;
|
|
162
173
|
return;
|
|
163
174
|
}
|
|
@@ -198,6 +209,11 @@ async function main() {
|
|
|
198
209
|
process.exitCode = await secretsOverCli(args.slice(1));
|
|
199
210
|
return;
|
|
200
211
|
}
|
|
212
|
+
// Vadym 18123: the supervisor's launch-time setup, the `gh` shim and git's credential helper; `github <server command>` stays the server's.
|
|
213
|
+
if (args[0] === "github" && ["setup", "credential", "gh"].includes(args[1] ?? "") && args[2] && !args[2].startsWith("-")) {
|
|
214
|
+
process.exitCode = await githubOverCli(args.slice(1));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
201
217
|
if (args[0] === "admin") {
|
|
202
218
|
process.exitCode = await adminOverCli(args.slice(1));
|
|
203
219
|
return;
|
|
@@ -227,6 +243,10 @@ async function main() {
|
|
|
227
243
|
// #1871: every other command is the server's, `<noun> [<sub-noun>] <verb>`, run as this shell's runner.
|
|
228
244
|
const parsed = parseCallArgs(args);
|
|
229
245
|
const runner = parsed?.flags.find(([flag]) => flag === "runner")?.[1] ?? process.env.AR_CLI_RUNNER_ID;
|
|
246
|
+
if (parsed && runner && parsed.words[0] === "schema" && parsed.words.length > 1 && parsed.words.length <= 4) {
|
|
247
|
+
process.exitCode = await schemaOverCli(runner, parsed.words.slice(1).join(" "));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
230
250
|
if (!parsed || !runner || parsed.words.length > 3 || (!parsed.words.length && !parsed.flags.some(([flag]) => flag === "help"))) {
|
|
231
251
|
console.error(USAGE);
|
|
232
252
|
if (parsed && !runner)
|
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
|