@allocator-one/harness-cli 1.11.0 → 1.12.0
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/README.md +31 -0
- package/dist/cli.js +206 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,37 @@ Headless contexts (agents, CI): mint an `aone_…` API token (admin → API
|
|
|
39
39
|
tokens) and set `HARNESS_API_TOKEN`. Do not set it in interactive sessions —
|
|
40
40
|
the browser login attributes writes to the real user.
|
|
41
41
|
|
|
42
|
+
### Provider keys
|
|
43
|
+
|
|
44
|
+
Admins store the org's provider API keys (Anthropic, OpenAI, Google,
|
|
45
|
+
OpenRouter, Brave Search, Perplexity) in Harness under `Settings → Model
|
|
46
|
+
keys`. The CLI hands them to tooling without ever printing them:
|
|
47
|
+
|
|
48
|
+
- `harness keys list` — provider slugs the org has keys for and the env var
|
|
49
|
+
each maps to (`--json` for `{providers: [{provider, env}]}`); values are
|
|
50
|
+
never shown.
|
|
51
|
+
- `harness keys exec [--only <slugs>] [--override] -- <command> [args…]` —
|
|
52
|
+
runs the command with the keys in its environment and exits with the
|
|
53
|
+
command's exit code (128 + signal number when it was signalled). `--only
|
|
54
|
+
brave,perplexity` passes just those providers and withholds your shell's
|
|
55
|
+
own copies of the other provider variables, so the tool sees no more than
|
|
56
|
+
it needs; a requested provider the org has no key for keeps your shell's
|
|
57
|
+
variable (and says so on stderr). Slugs are trimmed and case-insensitive,
|
|
58
|
+
and an allowlist that names nothing (`--only ,`) is a usage error, not
|
|
59
|
+
"everything". Otherwise a variable already set in your shell wins unless
|
|
60
|
+
`--override` is given; a blank variable counts as unset. `--json` is
|
|
61
|
+
accepted for uniformity but changes nothing: stdout belongs to the command. A SIGTERM sent to
|
|
62
|
+
`harness` is forwarded to the command; Ctrl+C reaches the command from the
|
|
63
|
+
terminal directly and `harness` waits to report its exit. macOS and Linux
|
|
64
|
+
only.
|
|
65
|
+
|
|
66
|
+
Slug → variable, the same table rcl uses: anthropic → `ANTHROPIC_API_KEY`,
|
|
67
|
+
openai → `OPENAI_API_KEY`, google → `GEMINI_API_KEY`, openrouter →
|
|
68
|
+
`OPENROUTER_API_KEY`, brave → `BRAVE_API_KEY`, perplexity →
|
|
69
|
+
`PERPLEXITY_API_KEY`. Reading keys needs the `model-keys:read` scope: login
|
|
70
|
+
tokens carry it, `aone_` tokens must be minted with it. There is deliberately
|
|
71
|
+
no MCP tool for this — a tool result is model-visible text.
|
|
72
|
+
|
|
42
73
|
## Environment variables
|
|
43
74
|
|
|
44
75
|
| Variable | Effect |
|
package/dist/cli.js
CHANGED
|
@@ -88,6 +88,188 @@ import {
|
|
|
88
88
|
import { createInterface as createInterface2 } from "readline";
|
|
89
89
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
90
90
|
|
|
91
|
+
// src/commands/keys.ts
|
|
92
|
+
import { spawn } from "child_process";
|
|
93
|
+
import { constants } from "os";
|
|
94
|
+
var KEY_ENV_VARS = /* @__PURE__ */ new Map([
|
|
95
|
+
["anthropic", "ANTHROPIC_API_KEY"],
|
|
96
|
+
["openai", "OPENAI_API_KEY"],
|
|
97
|
+
["google", "GEMINI_API_KEY"],
|
|
98
|
+
["openrouter", "OPENROUTER_API_KEY"],
|
|
99
|
+
["brave", "BRAVE_API_KEY"],
|
|
100
|
+
["perplexity", "PERPLEXITY_API_KEY"]
|
|
101
|
+
]);
|
|
102
|
+
function isRecord(value) {
|
|
103
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
104
|
+
}
|
|
105
|
+
async function fetchKeys(ctx) {
|
|
106
|
+
let body;
|
|
107
|
+
try {
|
|
108
|
+
body = await ctx.client.get("/api/v1/model-keys");
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error instanceof HttpError && error.status === 403 && error.message.includes("model-keys:read")) {
|
|
111
|
+
throw new CommandError(
|
|
112
|
+
`${error.message} \u2014 login tokens carry model-keys:read; aone_ API tokens must be minted with it`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const data = isRecord(body) ? body["data"] : void 0;
|
|
118
|
+
const keys2 = isRecord(data) ? data["keys"] : void 0;
|
|
119
|
+
if (!isRecord(keys2)) throw new CommandError("unexpected model-keys response: no keys map");
|
|
120
|
+
const stored = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const [slug, value] of Object.entries(keys2)) {
|
|
122
|
+
const canonical = slug.trim().toLowerCase();
|
|
123
|
+
if (stored.has(canonical)) continue;
|
|
124
|
+
if (typeof value === "string" && value.trim() !== "" && !value.includes("\0")) stored.set(canonical, value);
|
|
125
|
+
}
|
|
126
|
+
return stored;
|
|
127
|
+
}
|
|
128
|
+
function envVarFor(slug) {
|
|
129
|
+
return KEY_ENV_VARS.get(slug.trim().toLowerCase());
|
|
130
|
+
}
|
|
131
|
+
function normalizeSlugs(only) {
|
|
132
|
+
return [...new Set(only.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""))];
|
|
133
|
+
}
|
|
134
|
+
function selectSlugs(stored, only) {
|
|
135
|
+
if (only === void 0 || only.length === 0) return new Map(stored);
|
|
136
|
+
const wanted = new Set(only.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""));
|
|
137
|
+
return new Map([...stored].filter(([slug]) => wanted.has(slug)));
|
|
138
|
+
}
|
|
139
|
+
function unknownSlugs(only) {
|
|
140
|
+
return only.map((slug) => slug.trim()).filter((slug) => slug !== "" && !KEY_ENV_VARS.has(slug.toLowerCase()));
|
|
141
|
+
}
|
|
142
|
+
function withholdOthers(base, requested) {
|
|
143
|
+
const keep = new Set(requested.map((slug) => envVarFor(slug)));
|
|
144
|
+
const env = { ...base };
|
|
145
|
+
for (const name of KEY_ENV_VARS.values()) {
|
|
146
|
+
if (!keep.has(name)) delete env[name];
|
|
147
|
+
}
|
|
148
|
+
return env;
|
|
149
|
+
}
|
|
150
|
+
function unstoredSlugs(requested, stored) {
|
|
151
|
+
const canonical = [...new Set(requested.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""))];
|
|
152
|
+
return canonical.filter((slug) => !stored.has(slug));
|
|
153
|
+
}
|
|
154
|
+
function toEnv(stored) {
|
|
155
|
+
const env = {};
|
|
156
|
+
for (const [slug, value] of stored) {
|
|
157
|
+
const name = envVarFor(slug);
|
|
158
|
+
if (name !== void 0) env[name] = value;
|
|
159
|
+
}
|
|
160
|
+
return env;
|
|
161
|
+
}
|
|
162
|
+
function childEnv(base, fetched, override) {
|
|
163
|
+
const env = {};
|
|
164
|
+
for (const [name, value] of Object.entries(base)) {
|
|
165
|
+
if (value !== void 0) env[name] = value;
|
|
166
|
+
}
|
|
167
|
+
for (const [name, value] of Object.entries(fetched)) {
|
|
168
|
+
const existing = env[name];
|
|
169
|
+
if (override || existing === void 0 || existing.trim() === "") env[name] = value;
|
|
170
|
+
}
|
|
171
|
+
return env;
|
|
172
|
+
}
|
|
173
|
+
function exitCodeFor(code, signal) {
|
|
174
|
+
if (code !== null) return code;
|
|
175
|
+
if (signal !== null) {
|
|
176
|
+
const number = constants.signals[signal];
|
|
177
|
+
if (typeof number === "number") return 128 + number;
|
|
178
|
+
}
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
async function list2(ctx) {
|
|
182
|
+
const stored = await fetchKeys(ctx);
|
|
183
|
+
const providers = [...stored.keys()].sort().map((provider) => ({ provider, env: envVarFor(provider) ?? null }));
|
|
184
|
+
const human = providers.map(({ provider, env }) => `${clean(provider)} ${env ?? "(no env var)"}
|
|
185
|
+
`).join("");
|
|
186
|
+
return { json: { providers }, human };
|
|
187
|
+
}
|
|
188
|
+
var FORWARDED_SIGNALS = ["SIGTERM"];
|
|
189
|
+
var HELD_SIGNALS = ["SIGINT"];
|
|
190
|
+
async function exec(ctx, opts) {
|
|
191
|
+
const [program2, ...args] = opts.command;
|
|
192
|
+
if (program2 === void 0 || program2 === "") {
|
|
193
|
+
throw new UsageError("keys exec: name the command to run after `--`, e.g. `harness keys exec -- npm test`");
|
|
194
|
+
}
|
|
195
|
+
if (process.platform === "win32") {
|
|
196
|
+
throw new LocalError("keys exec is available on macOS and Linux only");
|
|
197
|
+
}
|
|
198
|
+
const onlyGiven = opts.only !== void 0 && opts.only.length > 0;
|
|
199
|
+
if (onlyGiven && normalizeSlugs(opts.only ?? []).length === 0) {
|
|
200
|
+
throw new UsageError(`keys exec: --only needs at least one provider slug \u2014 known: ${[...KEY_ENV_VARS.keys()].join(", ")}`);
|
|
201
|
+
}
|
|
202
|
+
const unknown = unknownSlugs(opts.only ?? []);
|
|
203
|
+
if (unknown.length > 0) {
|
|
204
|
+
throw new UsageError(
|
|
205
|
+
`keys exec: unknown provider slug ${unknown.map(clean).join(", ")} \u2014 known: ${[...KEY_ENV_VARS.keys()].join(", ")}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const stored = await fetchKeys(ctx);
|
|
209
|
+
const requested = normalizeSlugs(opts.only ?? []);
|
|
210
|
+
const narrowed = requested.length > 0;
|
|
211
|
+
if (narrowed) {
|
|
212
|
+
const missing = unstoredSlugs(requested, stored);
|
|
213
|
+
if (missing.length > 0) {
|
|
214
|
+
process.stderr.write(
|
|
215
|
+
`harness: no key stored for ${missing.join(", ")} \u2014 the command gets your shell's own variable for those, if any
|
|
216
|
+
`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const env = childEnv(
|
|
221
|
+
narrowed ? withholdOthers(process.env, requested) : process.env,
|
|
222
|
+
toEnv(selectSlugs(stored, requested)),
|
|
223
|
+
opts.override
|
|
224
|
+
);
|
|
225
|
+
const name = clean(program2);
|
|
226
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
227
|
+
let child;
|
|
228
|
+
try {
|
|
229
|
+
child = spawn(program2, args, { stdio: "inherit", env });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const code = error.code ?? "invalid arguments";
|
|
232
|
+
reject(new CommandError(`cannot start ${name}: ${code}`));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const handlers = [];
|
|
236
|
+
for (const signal of FORWARDED_SIGNALS) {
|
|
237
|
+
const handler = () => {
|
|
238
|
+
child.kill(signal);
|
|
239
|
+
};
|
|
240
|
+
process.on(signal, handler);
|
|
241
|
+
handlers.push([signal, handler]);
|
|
242
|
+
}
|
|
243
|
+
for (const signal of HELD_SIGNALS) {
|
|
244
|
+
const handler = () => {
|
|
245
|
+
};
|
|
246
|
+
process.on(signal, handler);
|
|
247
|
+
handlers.push([signal, handler]);
|
|
248
|
+
}
|
|
249
|
+
let settled = false;
|
|
250
|
+
let spawned = false;
|
|
251
|
+
const settle = (outcome) => {
|
|
252
|
+
if (settled) return;
|
|
253
|
+
settled = true;
|
|
254
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
255
|
+
outcome();
|
|
256
|
+
};
|
|
257
|
+
child.once("spawn", () => {
|
|
258
|
+
spawned = true;
|
|
259
|
+
});
|
|
260
|
+
child.on("error", (error) => {
|
|
261
|
+
settle(() => {
|
|
262
|
+
const code = error.code ?? error.message;
|
|
263
|
+
reject(new CommandError(spawned ? `${name} failed: ${code}` : `cannot run ${name}: ${code}`));
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
child.on("exit", (code, signal) => {
|
|
267
|
+
settle(() => resolve(exitCodeFor(code, signal)));
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
return { json: { exit_code: exitCode }, human: "", exitCode };
|
|
271
|
+
}
|
|
272
|
+
|
|
91
273
|
// src/core/auth.ts
|
|
92
274
|
import { createHash, randomBytes } from "crypto";
|
|
93
275
|
import { execFile } from "child_process";
|
|
@@ -744,10 +926,10 @@ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
|
744
926
|
}
|
|
745
927
|
|
|
746
928
|
// src/core/upgrade.ts
|
|
747
|
-
import { spawn, spawnSync } from "child_process";
|
|
929
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
748
930
|
import {
|
|
749
931
|
accessSync as accessSync2,
|
|
750
|
-
constants,
|
|
932
|
+
constants as constants2,
|
|
751
933
|
existsSync,
|
|
752
934
|
linkSync,
|
|
753
935
|
mkdirSync,
|
|
@@ -925,7 +1107,7 @@ function spawnRefresh() {
|
|
|
925
1107
|
const self = process.argv[1];
|
|
926
1108
|
if (self === void 0 || self === "") return;
|
|
927
1109
|
try {
|
|
928
|
-
const child =
|
|
1110
|
+
const child = spawn2(process.execPath, [self, UPDATE_CHECK_COMMAND], {
|
|
929
1111
|
detached: true,
|
|
930
1112
|
stdio: "ignore"
|
|
931
1113
|
});
|
|
@@ -1046,7 +1228,7 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1046
1228
|
};
|
|
1047
1229
|
}
|
|
1048
1230
|
try {
|
|
1049
|
-
accessSync2(globalRoot,
|
|
1231
|
+
accessSync2(globalRoot, constants2.W_OK);
|
|
1050
1232
|
} catch {
|
|
1051
1233
|
return {
|
|
1052
1234
|
ok: false,
|
|
@@ -1328,7 +1510,7 @@ async function execute(config, jsonFlag, mutating, run) {
|
|
|
1328
1510
|
process.stderr.write(hostNotice);
|
|
1329
1511
|
if (output.notice !== void 0) process.stderr.write(output.notice);
|
|
1330
1512
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1331
|
-
return exitWith(output.partial === true ? 1 : 0);
|
|
1513
|
+
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1332
1514
|
} catch (error) {
|
|
1333
1515
|
return fail(error, hostNotice);
|
|
1334
1516
|
}
|
|
@@ -1914,6 +2096,25 @@ program.command("mcp").description("run the MCP server over stdio (for agents)")
|
|
|
1914
2096
|
const { startMcpServer } = await import("./server-5YNUXJO2.js");
|
|
1915
2097
|
await startMcpServer();
|
|
1916
2098
|
});
|
|
2099
|
+
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
2100
|
+
keys.command("list").description("provider slugs the org has keys for and the env var each maps to \u2014 never the values").option("--json", "emit JSON").action(async (opts) => {
|
|
2101
|
+
await withConfig(opts.json === true, false, (ctx) => list2(ctx));
|
|
2102
|
+
});
|
|
2103
|
+
keys.command("exec").description(
|
|
2104
|
+
"run a command with the org's keys in its environment and exit with its exit code: harness keys exec [--override] -- <command> [args...]"
|
|
2105
|
+
).argument("[command...]", "the command and its arguments").option("--override", "let fetched keys replace variables already set in this shell").option(
|
|
2106
|
+
"--only <slugs>",
|
|
2107
|
+
"pass only these provider slugs (comma-separated, repeatable); default: everything stored",
|
|
2108
|
+
collect,
|
|
2109
|
+
[]
|
|
2110
|
+
).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (command, opts) => {
|
|
2111
|
+
const only = opts.only.flatMap((value) => value.split(","));
|
|
2112
|
+
await withConfig(
|
|
2113
|
+
false,
|
|
2114
|
+
false,
|
|
2115
|
+
(ctx) => exec(ctx, { command, override: opts.override === true, only })
|
|
2116
|
+
);
|
|
2117
|
+
});
|
|
1917
2118
|
program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option(
|
|
1918
2119
|
"--timeout <ms>",
|
|
1919
2120
|
"how long to wait for the browser callback",
|