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,6 +1,6 @@
1
1
  {
2
2
  "name": "mercury-agent",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Personal AI assistant for chat platforms (WhatsApp, Slack, Discord, Telegram)",
5
5
  "license": "MIT",
6
6
  "author": "Avishai Tsabari",
@@ -1,4 +1,9 @@
1
- export type ContainerFailureReason = "timeout" | "oom" | "aborted" | "error";
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 { getApiKeyFromPiAuthFile } from "../storage/pi-auth.js";
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 = passthroughEnvPairs.some(
798
- (p) => p.key === "ANTHROPIC_API_KEY" || p.key === "ANTHROPIC_OAUTH_TOKEN",
799
- );
800
- if (
801
- !hasAnthropicKey &&
802
- this.config.modelProvider === "anthropic" &&
803
- authFromPi
804
- ) {
805
- passthroughEnvPairs.push({
806
- key: "ANTHROPIC_OAUTH_TOKEN",
807
- value: authFromPi,
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
 
@@ -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",
@@ -33,19 +33,26 @@ function writeAuthFile(authPath: string, auth: AuthFile): void {
33
33
  fs.chmodSync(authPath, 0o600);
34
34
  }
35
35
 
36
- export async function getApiKeyFromPiAuthFile(options: {
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<string | undefined> {
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 undefined;
51
+ return { status: "none" };
45
52
  }
46
53
 
47
54
  if (options.provider !== "anthropic") {
48
- return undefined;
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 undefined;
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") return undefined;
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 undefined;
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 undefined;
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
+ }