@agent-proxy/agy 0.14.1 → 0.15.1

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.
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Antigravity CLI(agy)凭据的只读消费。
3
3
  *
4
- * 凭据文件是 CLI 写入的 `~/.gemini/antigravity-cli/antigravity-oauth-token`
5
- * (keyring 保存失败时的文件回退路径,本机即此形态)。网关只读 access_token 与
6
- * 过期时间,绝不刷新、绝不写回、绝不把令牌值带进日志或错误信息:
7
- * access_token 有效期整 1 小时,由 agy 进程(含 remote-control daemon)惰性刷新。
4
+ * 凭据有两处落点,macOS 按钥匙串优先的顺序只读消费:
5
+ * macOS 钥匙串条目 `gemini/antigravity`(`go-keyring-base64:` 前缀 + base64 的
6
+ * JSON)有可用令牌时优先采用,凭据文件被完全屏蔽,哪怕其内容本身是对的;
7
+ * 钥匙串无可用令牌(缺失/损坏/过期)才回落
8
+ * `~/.gemini/antigravity-cli/antigravity-oauth-token` 文件。
9
+ * 网关只读 access_token 与过期时间,绝不刷新、绝不写回、绝不把令牌值带进日志或
10
+ * 错误信息:access_token 有效期整 1 小时,由 agy 进程(含 daemon)惰性刷新并
11
+ * 写回上述存储;每次请求重新读取,写回自然生效。
8
12
  */
9
13
  export declare class AgyCredentialError extends Error {
10
14
  constructor(message: string);
@@ -23,13 +27,27 @@ export interface AgyCredentialOptions {
23
27
  credentialFile?: string;
24
28
  home?: string;
25
29
  readFile?: (file: string) => string;
30
+ /** 钥匙串条目读取(测试注入);缺省经 security 命令读 gemini/antigravity 的值。 */
31
+ readKeychain?: () => string;
32
+ os?: NodeJS.Platform;
26
33
  }
27
34
  export declare function defaultAgyCredentialFile(home?: string, os?: NodeJS.Platform): string;
28
- /** 只探测凭据文件是否存在(Web UI 开关显隐用);不读取也不解析内容。 */
29
- export declare function agyCredentialsPresent(credentialFile?: string): boolean;
35
+ /** 只探测凭据来源是否存在(Web UI 开关显隐用);不读取令牌值。 */
36
+ export declare function agyCredentialsPresent(credentialFile?: string, options?: {
37
+ os?: NodeJS.Platform;
38
+ probeKeychain?: () => boolean;
39
+ }): boolean;
30
40
  /**
31
- * 读取并校验 agy 凭据。文件缺失与损坏分开报错,损坏时带文件路径与重建指引,
32
- * 不静默当作未登录。
41
+ * 读取并校验 agy 凭据。网关不执行原生 CLI 的 refresh_token 续期逻辑:
42
+ *
43
+ * 1. 钥匙串条目(darwin)存在且令牌未被判为过期 → 采用,凭据文件被完全屏蔽
44
+ * (哪怕文件内容本身是合法的);
45
+ * 2. 钥匙串无可用令牌(缺失/损坏/过期)→ 回落凭据文件;
46
+ * 3. 文件也拿不到时,若钥匙串至少解析出凭据(已过期)则原样返回,交由请求层
47
+ * 按过期 401 给出刷新指引;两边都无凭据才报未登录(附钥匙串侧的具体问题)。
48
+ *
49
+ * 过期判定保留 10 秒余量;未知过期时间仍交由上游校验。每次请求重新读取,
50
+ * agy 对相应存储的写回自然生效。
33
51
  */
34
52
  export declare function loadAgyCredentials(options?: AgyCredentialOptions): AgyCredentials;
35
53
  /** access_token 是否已过期待判定:过期即快速失败,网关不做任何刷新。 */
@@ -1,19 +1,32 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+ import { execFileSync } from "node:child_process";
4
5
  /**
5
6
  * Antigravity CLI(agy)凭据的只读消费。
6
7
  *
7
- * 凭据文件是 CLI 写入的 `~/.gemini/antigravity-cli/antigravity-oauth-token`
8
- * (keyring 保存失败时的文件回退路径,本机即此形态)。网关只读 access_token 与
9
- * 过期时间,绝不刷新、绝不写回、绝不把令牌值带进日志或错误信息:
10
- * access_token 有效期整 1 小时,由 agy 进程(含 remote-control daemon)惰性刷新。
8
+ * 凭据有两处落点,macOS 按钥匙串优先的顺序只读消费:
9
+ * macOS 钥匙串条目 `gemini/antigravity`(`go-keyring-base64:` 前缀 + base64 的
10
+ * JSON)有可用令牌时优先采用,凭据文件被完全屏蔽,哪怕其内容本身是对的;
11
+ * 钥匙串无可用令牌(缺失/损坏/过期)才回落
12
+ * `~/.gemini/antigravity-cli/antigravity-oauth-token` 文件。
13
+ * 网关只读 access_token 与过期时间,绝不刷新、绝不写回、绝不把令牌值带进日志或
14
+ * 错误信息:access_token 有效期整 1 小时,由 agy 进程(含 daemon)惰性刷新并
15
+ * 写回上述存储;每次请求重新读取,写回自然生效。
11
16
  */
12
17
  export class AgyCredentialError extends Error {
13
18
  constructor(message) { super(message); this.name = "AgyCredentialError"; }
14
19
  }
15
20
  /** access_token 判定过期时预留的时钟偏移余量。 */
16
21
  const EXPIRY_SKEW_MS = 10_000;
22
+ /** 新版 agy CLI 的 macOS 钥匙串条目(go-keyring 惯例:服务 gemini、账户 antigravity)。 */
23
+ const AGY_KEYCHAIN_SERVICE = "gemini";
24
+ const AGY_KEYCHAIN_ACCOUNT = "antigravity";
25
+ const GO_KEYRING_PREFIX = "go-keyring-base64:";
26
+ const KEYCHAIN_TIMEOUT_MS = 5_000;
27
+ function readAgyKeychainValue() {
28
+ return execFileSync("security", ["find-generic-password", "-s", AGY_KEYCHAIN_SERVICE, "-a", AGY_KEYCHAIN_ACCOUNT, "-w"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: KEYCHAIN_TIMEOUT_MS }).trim();
29
+ }
17
30
  export function defaultAgyCredentialFile(home = process.env.HOME ?? "", os = process.platform) {
18
31
  if (os === "win32") {
19
32
  return path.join(home, "AppData", "Local", "Google", "antigravity-cli", "antigravity-oauth-token");
@@ -28,55 +41,116 @@ function parseExpiry(value) {
28
41
  const parsed = Date.parse(value);
29
42
  return Number.isFinite(parsed) ? parsed : 0;
30
43
  }
31
- /** 只探测凭据文件是否存在(Web UI 开关显隐用);不读取也不解析内容。 */
32
- export function agyCredentialsPresent(credentialFile = defaultAgyCredentialFile()) {
44
+ /** 只探测凭据来源是否存在(Web UI 开关显隐用);不读取令牌值。 */
45
+ export function agyCredentialsPresent(credentialFile = defaultAgyCredentialFile(), options = {}) {
33
46
  try {
34
47
  return fs.statSync(credentialFile).isFile();
35
48
  }
36
- catch {
49
+ catch { /* 继续探测钥匙串。 */ }
50
+ if ((options.os ?? process.platform) !== "darwin")
37
51
  return false;
38
- }
52
+ const probe = options.probeKeychain ?? (() => {
53
+ try {
54
+ execFileSync("security", ["find-generic-password", "-s", AGY_KEYCHAIN_SERVICE, "-a", AGY_KEYCHAIN_ACCOUNT], { stdio: ["ignore", "ignore", "ignore"], timeout: KEYCHAIN_TIMEOUT_MS });
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ });
61
+ return probe();
39
62
  }
40
63
  /**
41
- * 读取并校验 agy 凭据。文件缺失与损坏分开报错,损坏时带文件路径与重建指引,
42
- * 不静默当作未登录。
64
+ * 读取并校验 agy 凭据。网关不执行原生 CLI 的 refresh_token 续期逻辑:
65
+ *
66
+ * 1. 钥匙串条目(darwin)存在且令牌未被判为过期 → 采用,凭据文件被完全屏蔽
67
+ * (哪怕文件内容本身是合法的);
68
+ * 2. 钥匙串无可用令牌(缺失/损坏/过期)→ 回落凭据文件;
69
+ * 3. 文件也拿不到时,若钥匙串至少解析出凭据(已过期)则原样返回,交由请求层
70
+ * 按过期 401 给出刷新指引;两边都无凭据才报未登录(附钥匙串侧的具体问题)。
71
+ *
72
+ * 过期判定保留 10 秒余量;未知过期时间仍交由上游校验。每次请求重新读取,
73
+ * agy 对相应存储的写回自然生效。
43
74
  */
44
75
  export function loadAgyCredentials(options = {}) {
45
76
  const file = options.credentialFile
46
- ?? defaultAgyCredentialFile(options.home ?? process.env.HOME ?? "");
77
+ ?? defaultAgyCredentialFile(options.home ?? process.env.HOME ?? "", options.os ?? process.platform);
47
78
  const read = options.readFile ?? ((source) => fs.readFileSync(source, "utf8"));
48
- let raw;
49
- try {
50
- raw = read(file);
51
- }
52
- catch {
53
- throw new AgyCredentialError(`未检测到 Antigravity CLI 登录(${file} 不存在);请先运行 agy 并完成 Google 账号登录,或执行 agy remote-control start 保持登录态刷新`);
54
- }
55
- let parsed;
56
- try {
57
- parsed = JSON.parse(raw);
58
- }
59
- catch {
60
- throw new AgyCredentialError(`Antigravity 凭据文件损坏(${file} 不是有效 JSON);请在 agy 内退出登录后重新登录以重建该文件`);
79
+ const keychainLabel = `keychain:${AGY_KEYCHAIN_SERVICE}/${AGY_KEYCHAIN_ACCOUNT}`;
80
+ /** 解析单个来源的 JSON 文本;损坏与缺字段分开报错(带来源标注)。 */
81
+ const parse = (raw, label) => {
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(raw);
85
+ }
86
+ catch {
87
+ throw new AgyCredentialError(`Antigravity 凭据损坏(${label} 不是有效 JSON);请在 agy 内退出登录后重新登录以重建`);
88
+ }
89
+ const root = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
90
+ ? parsed : undefined;
91
+ const tokenRaw = root?.token;
92
+ const token = tokenRaw !== null && tokenRaw !== undefined && typeof tokenRaw === "object" && !Array.isArray(tokenRaw)
93
+ ? tokenRaw : undefined;
94
+ const accessToken = typeof token?.access_token === "string" ? token.access_token : "";
95
+ if (!token || !accessToken) {
96
+ throw new AgyCredentialError(`Antigravity 凭据缺少 access_token(${label});请运行 agy 重新登录后重试`);
97
+ }
98
+ const seed = typeof token.refresh_token === "string" && token.refresh_token
99
+ ? token.refresh_token
100
+ : typeof root?.id_token === "string" && root.id_token ? root.id_token : accessToken;
101
+ return {
102
+ accessToken,
103
+ expiryMs: parseExpiry(token.expiry),
104
+ identity: createHash("sha256").update(seed).digest("hex"),
105
+ authMethod: typeof root?.auth_method === "string" && root.auth_method ? root.auth_method : "consumer",
106
+ };
107
+ };
108
+ /** 读凭据文件;缺失/不可读返回 undefined(由调用方决定回退或报错),损坏抛错。 */
109
+ const readFileCredentials = () => {
110
+ let raw;
111
+ try {
112
+ raw = read(file);
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ return parse(raw, file);
118
+ };
119
+ if ((options.os ?? process.platform) === "darwin") {
120
+ const readKeychain = options.readKeychain ?? readAgyKeychainValue;
121
+ let keychainCredentials;
122
+ let keychainProblem;
123
+ try {
124
+ const raw = readKeychain();
125
+ if (!raw.startsWith(GO_KEYRING_PREFIX)) {
126
+ throw new AgyCredentialError(`Antigravity 钥匙串条目格式异常(${AGY_KEYCHAIN_SERVICE}/${AGY_KEYCHAIN_ACCOUNT} 不以 ${GO_KEYRING_PREFIX} 开头);请在 agy 内退出登录后重新登录以重建`);
127
+ }
128
+ keychainCredentials = parse(Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf8"), keychainLabel);
129
+ }
130
+ catch (error) {
131
+ // 子进程错误可能携带 stdout(令牌值),只允许预定义的解析错误进入提示。
132
+ keychainProblem = error instanceof AgyCredentialError ? error.message : "钥匙串不可读或读取超时";
133
+ }
134
+ // 钥匙串有合法(未过期)令牌:直接采用,文件被完全屏蔽。
135
+ if (keychainCredentials && !agyTokenStale(keychainCredentials))
136
+ return keychainCredentials;
137
+ // 钥匙串过期/损坏/缺失均回落文件。
138
+ const fromFile = readFileCredentials();
139
+ if (fromFile)
140
+ return fromFile;
141
+ // 文件也没有:过期钥匙串凭据原样返回(请求层按过期给指引),否则报未登录。
142
+ if (keychainCredentials)
143
+ return keychainCredentials;
144
+ throw new AgyCredentialError(`未检测到 Antigravity CLI 登录(钥匙串条目 ${AGY_KEYCHAIN_SERVICE}/${AGY_KEYCHAIN_ACCOUNT} 无可用令牌`
145
+ + `${keychainProblem ? `:${keychainProblem}` : ""},且 ${file} 不存在或不可读);`
146
+ + "请先运行 agy 并完成 Google 账号登录,或执行 agy remote-control start 保持登录态刷新");
61
147
  }
62
- const root = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
63
- ? parsed : undefined;
64
- const tokenRaw = root?.token;
65
- const token = tokenRaw !== null && tokenRaw !== undefined && typeof tokenRaw === "object" && !Array.isArray(tokenRaw)
66
- ? tokenRaw : undefined;
67
- const accessToken = typeof token?.access_token === "string" ? token.access_token : "";
68
- if (!token || !accessToken) {
69
- throw new AgyCredentialError(`Antigravity 凭据缺少 access_token(${file});请运行 agy 重新登录后重试`);
148
+ const fromFile = readFileCredentials();
149
+ if (!fromFile) {
150
+ throw new AgyCredentialError(`未检测到 Antigravity CLI 登录(${file} 不存在,且当前平台无钥匙串回退);`
151
+ + "请先运行 agy 并完成 Google 账号登录,或执行 agy remote-control start 保持登录态刷新");
70
152
  }
71
- const seed = typeof token.refresh_token === "string" && token.refresh_token
72
- ? token.refresh_token
73
- : typeof root?.id_token === "string" && root.id_token ? root.id_token : accessToken;
74
- return {
75
- accessToken,
76
- expiryMs: parseExpiry(token.expiry),
77
- identity: createHash("sha256").update(seed).digest("hex"),
78
- authMethod: typeof root?.auth_method === "string" && root.auth_method ? root.auth_method : "consumer",
79
- };
153
+ return fromFile;
80
154
  }
81
155
  /** access_token 是否已过期待判定:过期即快速失败,网关不做任何刷新。 */
82
156
  export function agyTokenStale(credentials, now = Date.now()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-proxy/agy",
3
- "version": "0.14.1",
3
+ "version": "0.15.1",
4
4
  "description": "Agent Proxy agy 插件;保留基线转换行为。",
5
5
  "type": "module",
6
6
  "exports": {
@@ -35,10 +35,10 @@
35
35
  "url": "git+https://github.com/aceHubert/codex-cliproxy-gateway.git"
36
36
  },
37
37
  "devDependencies": {
38
- "@agent-proxy/models": "0.14.1"
38
+ "@agent-proxy/models": "0.15.0"
39
39
  },
40
40
  "dependencies": {
41
- "@agent-proxy/plugin-api": "0.14.1"
41
+ "@agent-proxy/plugin-api": "0.15.0"
42
42
  },
43
- "gitHead": "f71d2cc6197937dd5f64365b6fc94a4fa8cf5400"
43
+ "gitHead": "cfb03c81e31c64da3a7314c43c51d954f5302111"
44
44
  }