mercury-agent 0.8.5 → 0.8.7
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.
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import { homedir, tmpdir } from "node:os";
|
|
13
13
|
import { delimiter, dirname, join } from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getPiAuthCredential,
|
|
16
|
+
parseOAuthTokenEnv,
|
|
17
|
+
} from "mercury-agent/storage/pi-auth";
|
|
15
18
|
|
|
16
19
|
const KNOWLEDGE_DIR = "knowledge";
|
|
17
20
|
const VAULT_DIRS = ["people", "projects", "references", "daily", "episodes", "weekly", "monthly", "templates"];
|
|
@@ -422,10 +425,25 @@ export default function (mercury: {
|
|
|
422
425
|
if (process.env.MERCURY_ANTHROPIC_API_KEY) {
|
|
423
426
|
env.ANTHROPIC_API_KEY = process.env.MERCURY_ANTHROPIC_API_KEY;
|
|
424
427
|
}
|
|
428
|
+
// Console-provisioned agents carry a JSON credential blob here, not a bare
|
|
429
|
+
// token — same handling as container-runner, via the shared parser.
|
|
430
|
+
let oauthBlobCorrupt = false;
|
|
425
431
|
if (process.env.MERCURY_ANTHROPIC_OAUTH_TOKEN) {
|
|
426
|
-
|
|
432
|
+
const parsed = parseOAuthTokenEnv(
|
|
433
|
+
process.env.MERCURY_ANTHROPIC_OAUTH_TOKEN,
|
|
434
|
+
);
|
|
435
|
+
if (parsed.status === "token") {
|
|
436
|
+
env.ANTHROPIC_API_KEY = parsed.token;
|
|
437
|
+
} else if (parsed.status === "blob") {
|
|
438
|
+
env.ANTHROPIC_OAUTH_TOKEN = parsed.access;
|
|
439
|
+
} else if (parsed.status === "corrupt-blob") {
|
|
440
|
+
oauthBlobCorrupt = true;
|
|
441
|
+
}
|
|
442
|
+
// "empty" — whitespace-only value, treat as unset and fall through
|
|
443
|
+
}
|
|
444
|
+
if (env.ANTHROPIC_API_KEY || env.ANTHROPIC_OAUTH_TOKEN) {
|
|
445
|
+
return { ok: true, env };
|
|
427
446
|
}
|
|
428
|
-
if (env.ANTHROPIC_API_KEY) return { ok: true, env };
|
|
429
447
|
|
|
430
448
|
// Spawned pi inherits process.env, so an unprefixed host key also works.
|
|
431
449
|
if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN) {
|
|
@@ -450,8 +468,9 @@ export default function (mercury: {
|
|
|
450
468
|
|
|
451
469
|
// Fail fast, mirroring the container-runner guard: without a credential
|
|
452
470
|
// every pi spawn exits 1, one per space per pending date, every run.
|
|
453
|
-
const reason =
|
|
454
|
-
|
|
471
|
+
const reason = oauthBlobCorrupt
|
|
472
|
+
? "MERCURY_ANTHROPIC_OAUTH_TOKEN holds a corrupt credential blob — re-provision it or set MERCURY_ANTHROPIC_API_KEY"
|
|
473
|
+
: cred.status === "refresh-failed"
|
|
455
474
|
? `Anthropic OAuth refresh failed for ${authPath} — re-authenticate on the host (run mercury auth login from the project directory that owns that file) or set MERCURY_ANTHROPIC_API_KEY`
|
|
456
475
|
: `no Anthropic credential configured (checked ${authPath}) — run mercury auth login or set MERCURY_ANTHROPIC_API_KEY / MERCURY_ANTHROPIC_OAUTH_TOKEN`;
|
|
457
476
|
return { ok: false, reason };
|
package/package.json
CHANGED
package/src/storage/pi-auth.ts
CHANGED
|
@@ -33,6 +33,34 @@ function writeAuthFile(authPath: string, auth: AuthFile): void {
|
|
|
33
33
|
fs.chmodSync(authPath, 0o600);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export type OAuthTokenEnvValue =
|
|
37
|
+
| { status: "token"; token: string }
|
|
38
|
+
| { status: "blob"; access: string }
|
|
39
|
+
| { status: "corrupt-blob" }
|
|
40
|
+
/** Whitespace-only value — treat the variable as unset. */
|
|
41
|
+
| { status: "empty" };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Interpret the raw value of MERCURY_ANTHROPIC_OAUTH_TOKEN. Console-provisioned
|
|
45
|
+
* agents receive a full credential blob ({"access":"...","refresh":"...",
|
|
46
|
+
* "expires":...}) rather than a bare token, so any code spawning pi on the host
|
|
47
|
+
* must extract the access token instead of passing the value through verbatim.
|
|
48
|
+
*/
|
|
49
|
+
export function parseOAuthTokenEnv(raw: string): OAuthTokenEnvValue {
|
|
50
|
+
const trimmed = raw.trim();
|
|
51
|
+
if (!trimmed) return { status: "empty" };
|
|
52
|
+
if (!trimmed.startsWith("{")) return { status: "token", token: trimmed };
|
|
53
|
+
try {
|
|
54
|
+
const blob = JSON.parse(trimmed) as { access?: unknown };
|
|
55
|
+
if (typeof blob.access === "string" && blob.access) {
|
|
56
|
+
return { status: "blob", access: blob.access };
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// fall through to corrupt-blob
|
|
60
|
+
}
|
|
61
|
+
return { status: "corrupt-blob" };
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
export type PiAuthCredential =
|
|
37
65
|
| { status: "ok"; apiKey: string }
|
|
38
66
|
/** No usable oauth entry (or an env override takes precedence). */
|
|
@@ -40,6 +68,10 @@ export type PiAuthCredential =
|
|
|
40
68
|
/** An oauth entry exists but could not be turned into a usable key. */
|
|
41
69
|
| { status: "refresh-failed"; error?: Error };
|
|
42
70
|
|
|
71
|
+
// Deduplicate concurrent OAuth refreshes — refresh tokens are single-use, so
|
|
72
|
+
// two parallel calls with the same token race and one always fails.
|
|
73
|
+
const inflightRefresh = new Map<string, Promise<PiAuthCredential>>();
|
|
74
|
+
|
|
43
75
|
export async function getPiAuthCredential(options: {
|
|
44
76
|
provider: string;
|
|
45
77
|
authPath: string;
|
|
@@ -55,6 +87,25 @@ export async function getPiAuthCredential(options: {
|
|
|
55
87
|
return { status: "none" };
|
|
56
88
|
}
|
|
57
89
|
|
|
90
|
+
// Coalesce concurrent refreshes for the same auth file so only one
|
|
91
|
+
// token-endpoint call is made; the rest share its result.
|
|
92
|
+
const key = options.authPath;
|
|
93
|
+
const existing = inflightRefresh.get(key);
|
|
94
|
+
if (existing) return existing;
|
|
95
|
+
|
|
96
|
+
const promise = doGetPiAuthCredential(options);
|
|
97
|
+
inflightRefresh.set(key, promise);
|
|
98
|
+
try {
|
|
99
|
+
return await promise;
|
|
100
|
+
} finally {
|
|
101
|
+
inflightRefresh.delete(key);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function doGetPiAuthCredential(options: {
|
|
106
|
+
provider: string;
|
|
107
|
+
authPath: string;
|
|
108
|
+
}): Promise<PiAuthCredential> {
|
|
58
109
|
const authPath = options.authPath;
|
|
59
110
|
const auth = readAuthFile(authPath);
|
|
60
111
|
|