athena-agent-launcher 0.2.0 → 0.3.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/bin/launcher.mjs +125 -24
- package/package.json +1 -1
package/bin/launcher.mjs
CHANGED
|
@@ -181,6 +181,83 @@ async function ensureHermesInstalled() {
|
|
|
181
181
|
return await locateHermes();
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
function sleep(ms) {
|
|
185
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Open a URL in the user's default browser (best-effort, cross-platform).
|
|
189
|
+
function openBrowser(url) {
|
|
190
|
+
const cmd =
|
|
191
|
+
process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
192
|
+
const cmdArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
193
|
+
try {
|
|
194
|
+
spawn(cmd, cmdArgs, { stdio: "ignore", detached: true }).unref();
|
|
195
|
+
} catch {
|
|
196
|
+
/* best-effort; the URL is also printed */
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// SSO device pairing: open the browser to authorize, poll until a key bound to
|
|
201
|
+
// the verified identity is issued. Returns the ak_ key.
|
|
202
|
+
async function pairAndGetKey(baseUrl, agentId) {
|
|
203
|
+
const startRes = await fetch(`${baseUrl}/api/launcher/pair/start`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: { "content-type": "application/json" },
|
|
206
|
+
body: JSON.stringify({ agent_id: agentId }),
|
|
207
|
+
});
|
|
208
|
+
if (!startRes.ok) {
|
|
209
|
+
throw new Error(`pair/start failed (${startRes.status}): ${await startRes.text()}`);
|
|
210
|
+
}
|
|
211
|
+
const { device_code, user_code, verification_url } = await startRes.json();
|
|
212
|
+
process.stderr.write(
|
|
213
|
+
`\n[athena-agent] Sign in with your Sonance account to authorize this device:\n` +
|
|
214
|
+
`\n ${verification_url}\n\n` +
|
|
215
|
+
` (opening your browser… code: ${user_code})\n\n`,
|
|
216
|
+
);
|
|
217
|
+
openBrowser(verification_url);
|
|
218
|
+
|
|
219
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
220
|
+
let notified = false;
|
|
221
|
+
while (Date.now() < deadline) {
|
|
222
|
+
await sleep(3000);
|
|
223
|
+
let j;
|
|
224
|
+
try {
|
|
225
|
+
const pollRes = await fetch(`${baseUrl}/api/launcher/pair/poll`, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "content-type": "application/json" },
|
|
228
|
+
body: JSON.stringify({ device_code }),
|
|
229
|
+
});
|
|
230
|
+
j = await pollRes.json();
|
|
231
|
+
} catch {
|
|
232
|
+
continue; // transient network blip — keep polling
|
|
233
|
+
}
|
|
234
|
+
if (j.status === "approved" && j.api_key) {
|
|
235
|
+
process.stderr.write("[athena-agent] ✓ authorized.\n");
|
|
236
|
+
return j.api_key;
|
|
237
|
+
}
|
|
238
|
+
if (j.status === "denied") throw new Error(`Authorization denied: ${j.error || "no access"}`);
|
|
239
|
+
if (j.status === "expired") throw new Error("Pairing expired — run the command again.");
|
|
240
|
+
if (!notified && j.status === "pending") {
|
|
241
|
+
process.stderr.write("[athena-agent] waiting for you to approve in the browser…\n");
|
|
242
|
+
notified = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
throw new Error("Pairing timed out — run the command again.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Persist a per-agent key (new format) without clobbering other agents' keys
|
|
249
|
+
// or the legacy flat `api_key`.
|
|
250
|
+
function saveAgentKey(agentId, apiKey, baseUrl) {
|
|
251
|
+
const credsPath = join(homedir(), ".athena", "credentials.json");
|
|
252
|
+
const creds = loadCredsFile();
|
|
253
|
+
if (!creds.base_url) creds.base_url = baseUrl;
|
|
254
|
+
creds.agent_keys = creds.agent_keys || {};
|
|
255
|
+
creds.agent_keys[agentId] = apiKey;
|
|
256
|
+
mkdirSync(dirname(credsPath), { recursive: true });
|
|
257
|
+
writeFileSync(credsPath, JSON.stringify(creds, null, 2), "utf8");
|
|
258
|
+
chmodSync(credsPath, 0o600);
|
|
259
|
+
}
|
|
260
|
+
|
|
184
261
|
async function fetchManifest({ baseUrl, agentId, apiKey }) {
|
|
185
262
|
const res = await fetch(`${baseUrl}/api/agents/${agentId}/launcher-manifest`, {
|
|
186
263
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
@@ -232,28 +309,36 @@ async function main() {
|
|
|
232
309
|
}
|
|
233
310
|
|
|
234
311
|
const agentId = args._[1];
|
|
235
|
-
|
|
236
|
-
|
|
312
|
+
const creds = loadCredsFile();
|
|
313
|
+
const baseUrl =
|
|
237
314
|
args.baseUrl || process.env.ATHENA_BASE_URL || creds.base_url || DEFAULT_BASE_URL;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
315
|
+
// Key precedence: explicit flag/env > per-agent saved key > legacy flat key.
|
|
316
|
+
let apiKey =
|
|
317
|
+
args.apiKey ||
|
|
318
|
+
process.env.ATHENA_API_KEY ||
|
|
319
|
+
creds.agent_keys?.[agentId] ||
|
|
320
|
+
creds.api_key;
|
|
321
|
+
|
|
322
|
+
// No saved key for this agent → authorize this device via Sonance SSO. The
|
|
323
|
+
// browser flow verifies the user's Okta identity and (if they hold an access
|
|
324
|
+
// grant) hands back a key bound to their account, which we cache per-agent.
|
|
243
325
|
if (!apiKey && !args.apiKey) {
|
|
244
326
|
process.stderr.write(
|
|
245
|
-
|
|
327
|
+
`[athena-agent] no saved key for ${agentId} — starting SSO authorization.\n`,
|
|
246
328
|
);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
329
|
+
try {
|
|
330
|
+
apiKey = await pairAndGetKey(baseUrl, agentId);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
process.stderr.write(`[athena-agent] ${err.message}\n`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
saveAgentKey(agentId, apiKey, baseUrl);
|
|
251
336
|
}
|
|
252
337
|
|
|
253
338
|
if (!apiKey) {
|
|
254
339
|
process.stderr.write(
|
|
255
340
|
"Error: no API key. Pass --api-key ak_..., set $ATHENA_API_KEY, " +
|
|
256
|
-
"or
|
|
341
|
+
"or run without flags to authorize via Sonance SSO.\n",
|
|
257
342
|
);
|
|
258
343
|
process.exit(2);
|
|
259
344
|
}
|
|
@@ -262,17 +347,33 @@ async function main() {
|
|
|
262
347
|
try {
|
|
263
348
|
manifest = await fetchManifest({ baseUrl, agentId, apiKey });
|
|
264
349
|
} catch (err) {
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
350
|
+
// A saved key that's been revoked server-side returns 401 — re-authorize
|
|
351
|
+
// via SSO once and retry, unless the key was supplied explicitly.
|
|
352
|
+
if (/\(401\)/.test(err.message) && !args.apiKey && !process.env.ATHENA_API_KEY) {
|
|
353
|
+
process.stderr.write(
|
|
354
|
+
"[athena-agent] saved key was rejected (revoked?) — re-authorizing via SSO.\n",
|
|
355
|
+
);
|
|
356
|
+
try {
|
|
357
|
+
apiKey = await pairAndGetKey(baseUrl, agentId);
|
|
358
|
+
saveAgentKey(agentId, apiKey, baseUrl);
|
|
359
|
+
manifest = await fetchManifest({ baseUrl, agentId, apiKey });
|
|
360
|
+
} catch (err2) {
|
|
361
|
+
process.stderr.write(`[athena-agent] ${err2.message}\n`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
// `fetch failed` from undici is opaque on its own — the real cause
|
|
366
|
+
// (ENOTFOUND, ECONNREFUSED, TLS, etc.) lives on err.cause.
|
|
367
|
+
const cause = err?.cause
|
|
368
|
+
? ` (${err.cause.code || err.cause.message || err.cause})`
|
|
369
|
+
: "";
|
|
370
|
+
process.stderr.write(
|
|
371
|
+
`[athena-agent] ${err.message}${cause}\n` +
|
|
372
|
+
` base URL: ${baseUrl}\n` +
|
|
373
|
+
` agent: ${agentId}\n`,
|
|
374
|
+
);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
276
377
|
}
|
|
277
378
|
|
|
278
379
|
if (args.manifestOnly) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "athena-agent-launcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Run an Athena-configured agent locally. Resolves runtime + bindings from the Athena control plane and spawns the correct agent binary (Anthropic SDK, OpenClaw, or Hermes).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|