mercury-agent 0.8.2 → 0.8.3
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/package.json
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export type ContainerFailureReason =
|
|
1
|
+
export type ContainerFailureReason =
|
|
2
|
+
| "timeout"
|
|
3
|
+
| "oom"
|
|
4
|
+
| "aborted"
|
|
5
|
+
| "error"
|
|
6
|
+
| "no-credentials";
|
|
2
7
|
|
|
3
8
|
export class ContainerError extends Error {
|
|
4
9
|
readonly reason: ContainerFailureReason;
|
|
@@ -39,6 +44,14 @@ export class ContainerError extends Error {
|
|
|
39
44
|
);
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
static noCredentials(spaceId: string, detail: string): ContainerError {
|
|
48
|
+
return new ContainerError(
|
|
49
|
+
"no-credentials",
|
|
50
|
+
null,
|
|
51
|
+
`Refusing to start container for space ${spaceId}: ${detail}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
static error(exitCode: number, output: string): ContainerError {
|
|
43
56
|
return new ContainerError(
|
|
44
57
|
"error",
|
|
@@ -8,7 +8,7 @@ import { mintCallerToken } from "../core/caller-token.js";
|
|
|
8
8
|
import { scanOutbox } from "../core/outbox.js";
|
|
9
9
|
import type { ExtImageBuildState } from "../extensions/image-builder.js";
|
|
10
10
|
import { type Logger, logger } from "../logger.js";
|
|
11
|
-
import {
|
|
11
|
+
import { getPiAuthCredential } from "../storage/pi-auth.js";
|
|
12
12
|
import type {
|
|
13
13
|
ContainerResult,
|
|
14
14
|
MessageAttachment,
|
|
@@ -677,11 +677,6 @@ export class AgentContainerRunner {
|
|
|
677
677
|
);
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
const authFromPi = await getApiKeyFromPiAuthFile({
|
|
681
|
-
provider: this.config.modelProvider,
|
|
682
|
-
authPath: this.config.authPath ?? path.join(globalDir, "auth.json"),
|
|
683
|
-
});
|
|
684
|
-
|
|
685
680
|
// Env vars that should never be passed to containers
|
|
686
681
|
const BLOCKED_ENV_VARS = new Set([
|
|
687
682
|
"MERCURY_API_SECRET",
|
|
@@ -762,6 +757,7 @@ export class AgentContainerRunner {
|
|
|
762
757
|
p.key === "ANTHROPIC_OAUTH_TOKEN" &&
|
|
763
758
|
p.value.trimStart().startsWith("{"),
|
|
764
759
|
);
|
|
760
|
+
let oauthBlobCorrupt = false;
|
|
765
761
|
if (anthOauthIdx !== -1) {
|
|
766
762
|
const raw = passthroughEnvPairs[anthOauthIdx]?.value ?? "";
|
|
767
763
|
passthroughEnvPairs.splice(anthOauthIdx, 1);
|
|
@@ -769,6 +765,7 @@ export class AgentContainerRunner {
|
|
|
769
765
|
try {
|
|
770
766
|
parsedCreds = JSON.parse(raw) as AnthropicOAuthCreds;
|
|
771
767
|
} catch {
|
|
768
|
+
oauthBlobCorrupt = true;
|
|
772
769
|
logger.warn("Anthropic OAuth blob corrupt; skipping token injection");
|
|
773
770
|
}
|
|
774
771
|
if (parsedCreds) {
|
|
@@ -794,18 +791,41 @@ export class AgentContainerRunner {
|
|
|
794
791
|
}
|
|
795
792
|
|
|
796
793
|
// Check for pi auth file fallback for Anthropic
|
|
797
|
-
const hasAnthropicKey =
|
|
798
|
-
(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
794
|
+
const hasAnthropicKey =
|
|
795
|
+
passthroughEnvPairs.some(
|
|
796
|
+
(p) =>
|
|
797
|
+
(p.key === "ANTHROPIC_API_KEY" ||
|
|
798
|
+
p.key === "ANTHROPIC_OAUTH_TOKEN") &&
|
|
799
|
+
p.value,
|
|
800
|
+
) ||
|
|
801
|
+
Boolean(
|
|
802
|
+
input.extraEnv?.ANTHROPIC_API_KEY ||
|
|
803
|
+
input.extraEnv?.ANTHROPIC_OAUTH_TOKEN,
|
|
804
|
+
);
|
|
805
|
+
if (!hasAnthropicKey && this.config.modelProvider === "anthropic") {
|
|
806
|
+
const piAuth = await getPiAuthCredential({
|
|
807
|
+
provider: this.config.modelProvider,
|
|
808
|
+
authPath: this.config.authPath ?? path.join(globalDir, "auth.json"),
|
|
808
809
|
});
|
|
810
|
+
if (piAuth.status === "ok") {
|
|
811
|
+
passthroughEnvPairs.push({
|
|
812
|
+
key: "ANTHROPIC_OAUTH_TOKEN",
|
|
813
|
+
value: piAuth.apiKey,
|
|
814
|
+
});
|
|
815
|
+
} else {
|
|
816
|
+
// Fail fast: without a credential, pi inside the container exits 1
|
|
817
|
+
// ("No API key found for anthropic") after a full container start,
|
|
818
|
+
// and the user gets a misleading "try again" message.
|
|
819
|
+
const detail = oauthBlobCorrupt
|
|
820
|
+
? "MERCURY_ANTHROPIC_OAUTH_TOKEN holds a corrupt credential blob — re-provision it or set MERCURY_ANTHROPIC_API_KEY"
|
|
821
|
+
: piAuth.status === "refresh-failed"
|
|
822
|
+
? "Anthropic OAuth refresh failed — re-authenticate on the host (mercury auth login) or set MERCURY_ANTHROPIC_API_KEY"
|
|
823
|
+
: "no Anthropic credential configured — run mercury auth login or set MERCURY_ANTHROPIC_API_KEY / MERCURY_ANTHROPIC_OAUTH_TOKEN";
|
|
824
|
+
logger.error(`Refusing to start container: ${detail}`, {
|
|
825
|
+
spaceId: input.spaceId,
|
|
826
|
+
});
|
|
827
|
+
throw ContainerError.noCredentials(input.spaceId, detail);
|
|
828
|
+
}
|
|
809
829
|
}
|
|
810
830
|
|
|
811
831
|
const envPairs = [
|
|
@@ -7,7 +7,7 @@ export type UserErrorCategory =
|
|
|
7
7
|
| "generic";
|
|
8
8
|
|
|
9
9
|
const AUTH_RE =
|
|
10
|
-
/\b401\b|\b403\b|invalid\s+api\s+key|incorrect\s+api\s+key|authentication\s+failed|invalid\s+authentication|unauthorized|access\s+denied/i;
|
|
10
|
+
/\b401\b|\b403\b|invalid\s+api\s+key|incorrect\s+api\s+key|no\s+api\s+key\s+found|authentication\s+failed|invalid\s+authentication|unauthorized|access\s+denied/i;
|
|
11
11
|
|
|
12
12
|
const KEY_LIMIT_RE = /quota|billing|usage\s+limit|spending\s+limit/i;
|
|
13
13
|
|
package/src/core/runtime.ts
CHANGED
|
@@ -641,6 +641,19 @@ export class MercuryCoreRuntime {
|
|
|
641
641
|
type: "denied",
|
|
642
642
|
reason: "Container was killed (possibly out of memory).",
|
|
643
643
|
};
|
|
644
|
+
case "no-credentials": {
|
|
645
|
+
// Host refused to start the container (no model credential).
|
|
646
|
+
// Auth-category message: "try again" would be misleading here.
|
|
647
|
+
logger.error("Container start refused: no model credentials", {
|
|
648
|
+
detail: error.message,
|
|
649
|
+
});
|
|
650
|
+
const reason = friendlyErrorMessage(
|
|
651
|
+
"auth",
|
|
652
|
+
this.config.apiKeyMode,
|
|
653
|
+
this.config.consoleUrl,
|
|
654
|
+
);
|
|
655
|
+
return { type: "denied", reason };
|
|
656
|
+
}
|
|
644
657
|
case "error": {
|
|
645
658
|
logger.error(
|
|
646
659
|
"Container error",
|
package/src/storage/pi-auth.ts
CHANGED
|
@@ -33,19 +33,26 @@ function writeAuthFile(authPath: string, auth: AuthFile): void {
|
|
|
33
33
|
fs.chmodSync(authPath, 0o600);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export
|
|
36
|
+
export type PiAuthCredential =
|
|
37
|
+
| { status: "ok"; apiKey: string }
|
|
38
|
+
/** No usable oauth entry (or an env override takes precedence). */
|
|
39
|
+
| { status: "none" }
|
|
40
|
+
/** An oauth entry exists but could not be turned into a usable key. */
|
|
41
|
+
| { status: "refresh-failed"; error?: Error };
|
|
42
|
+
|
|
43
|
+
export async function getPiAuthCredential(options: {
|
|
37
44
|
provider: string;
|
|
38
45
|
authPath: string;
|
|
39
|
-
}): Promise<
|
|
46
|
+
}): Promise<PiAuthCredential> {
|
|
40
47
|
if (
|
|
41
48
|
process.env.MERCURY_ANTHROPIC_API_KEY ||
|
|
42
49
|
process.env.MERCURY_ANTHROPIC_OAUTH_TOKEN
|
|
43
50
|
) {
|
|
44
|
-
return
|
|
51
|
+
return { status: "none" };
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
if (options.provider !== "anthropic") {
|
|
48
|
-
return
|
|
55
|
+
return { status: "none" };
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
const authPath = options.authPath;
|
|
@@ -53,13 +60,15 @@ export async function getApiKeyFromPiAuthFile(options: {
|
|
|
53
60
|
|
|
54
61
|
const entry = auth.anthropic;
|
|
55
62
|
if (!entry || typeof entry !== "object" || entry.type !== "oauth") {
|
|
56
|
-
return
|
|
63
|
+
return { status: "none" };
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
const access = typeof entry.access === "string" ? entry.access : undefined;
|
|
60
67
|
const refresh = typeof entry.refresh === "string" ? entry.refresh : undefined;
|
|
61
68
|
const expires = typeof entry.expires === "number" ? entry.expires : undefined;
|
|
62
|
-
if (!access || !refresh || typeof expires !== "number")
|
|
69
|
+
if (!access || !refresh || typeof expires !== "number") {
|
|
70
|
+
return { status: "none" };
|
|
71
|
+
}
|
|
63
72
|
|
|
64
73
|
try {
|
|
65
74
|
const result = await getOAuthApiKey("anthropic" satisfies OAuthProviderId, {
|
|
@@ -70,7 +79,7 @@ export async function getApiKeyFromPiAuthFile(options: {
|
|
|
70
79
|
},
|
|
71
80
|
});
|
|
72
81
|
|
|
73
|
-
if (!result) return
|
|
82
|
+
if (!result) return { status: "refresh-failed" };
|
|
74
83
|
|
|
75
84
|
const nextAuth = {
|
|
76
85
|
...auth,
|
|
@@ -84,12 +93,24 @@ export async function getApiKeyFromPiAuthFile(options: {
|
|
|
84
93
|
logger.debug("Loaded anthropic oauth token from pi auth.json", {
|
|
85
94
|
authPath,
|
|
86
95
|
});
|
|
87
|
-
return result.apiKey;
|
|
96
|
+
return { status: "ok", apiKey: result.apiKey };
|
|
88
97
|
} catch (error) {
|
|
89
98
|
logger.warn(
|
|
90
99
|
"Failed to load anthropic oauth token from pi auth.json",
|
|
91
100
|
error instanceof Error ? error : undefined,
|
|
92
101
|
);
|
|
93
|
-
return
|
|
102
|
+
return {
|
|
103
|
+
status: "refresh-failed",
|
|
104
|
+
error: error instanceof Error ? error : undefined,
|
|
105
|
+
};
|
|
94
106
|
}
|
|
95
107
|
}
|
|
108
|
+
|
|
109
|
+
/** Back-compat wrapper: returns the key on success, undefined otherwise. */
|
|
110
|
+
export async function getApiKeyFromPiAuthFile(options: {
|
|
111
|
+
provider: string;
|
|
112
|
+
authPath: string;
|
|
113
|
+
}): Promise<string | undefined> {
|
|
114
|
+
const result = await getPiAuthCredential(options);
|
|
115
|
+
return result.status === "ok" ? result.apiKey : undefined;
|
|
116
|
+
}
|