@agent-proxy/agy 0.15.0 → 0.16.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/dist/credentials.d.ts +26 -8
- package/dist/credentials.js +115 -41
- package/dist/request.js +57 -0
- package/package.json +4 -4
package/dist/credentials.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Antigravity CLI(agy)凭据的只读消费。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
29
|
-
export declare function agyCredentialsPresent(credentialFile?: string
|
|
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 是否已过期待判定:过期即快速失败,网关不做任何刷新。 */
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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/dist/request.js
CHANGED
|
@@ -16,6 +16,61 @@ function fail(message) { throw new AgyRequestError(message); }
|
|
|
16
16
|
function record(value) {
|
|
17
17
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
18
|
}
|
|
19
|
+
/** AGY 保持独立转换:增补信封先提升为声明,避免进入普通历史回放。 */
|
|
20
|
+
function normalizeAdditionalTools(body) {
|
|
21
|
+
if (!Array.isArray(body.input))
|
|
22
|
+
return body;
|
|
23
|
+
const envelopes = body.input.filter((item) => record(item) && item.type === "additional_tools");
|
|
24
|
+
if (envelopes.length === 0)
|
|
25
|
+
return body;
|
|
26
|
+
if (body.tools !== undefined && !Array.isArray(body.tools))
|
|
27
|
+
fail("additional_tools 合并要求 tools 为数组");
|
|
28
|
+
const groups = [body.tools ?? []];
|
|
29
|
+
for (const envelope of envelopes) {
|
|
30
|
+
if (!Array.isArray(envelope.tools))
|
|
31
|
+
fail("additional_tools.tools 必须是数组");
|
|
32
|
+
groups.push(envelope.tools);
|
|
33
|
+
}
|
|
34
|
+
const merge = (entries, inNamespace = false) => {
|
|
35
|
+
const tools = [];
|
|
36
|
+
const indices = new Map();
|
|
37
|
+
for (const group of entries) {
|
|
38
|
+
for (const raw of group) {
|
|
39
|
+
if (!record(raw) || typeof raw.type !== "string" || !raw.type)
|
|
40
|
+
fail("additional_tools.tools 包含无效工具声明");
|
|
41
|
+
if (["function", "custom", "namespace"].includes(raw.type) && (typeof raw.name !== "string" || !raw.name)) {
|
|
42
|
+
fail("additional_tools 工具 name 必须是非空字符串");
|
|
43
|
+
}
|
|
44
|
+
if (raw.type === "function" && !record(raw.parameters))
|
|
45
|
+
fail(`additional_tools function 工具 ${String(raw.name)} 缺少对象 parameters`);
|
|
46
|
+
let tool = raw;
|
|
47
|
+
if (raw.type === "namespace") {
|
|
48
|
+
if (inNamespace || !Array.isArray(raw.tools))
|
|
49
|
+
fail("additional_tools namespace.tools 必须是工具数组且不得嵌套 namespace");
|
|
50
|
+
tool = { ...raw, tools: merge([raw.tools], true) };
|
|
51
|
+
}
|
|
52
|
+
// function/custom 共用名称身份,顶层声明优先于历史信封中的旧类型。
|
|
53
|
+
const type = tool.type === "function" || tool.type === "custom" ? "callable" : tool.type;
|
|
54
|
+
const key = JSON.stringify([type, tool.namespace ?? null, tool.name ?? null]);
|
|
55
|
+
const index = indices.get(key);
|
|
56
|
+
if (index === undefined) {
|
|
57
|
+
indices.set(key, tools.length);
|
|
58
|
+
tools.push(tool);
|
|
59
|
+
}
|
|
60
|
+
else if (tool.type === "namespace") {
|
|
61
|
+
const existing = tools[index];
|
|
62
|
+
tools[index] = { ...existing, tools: merge([existing.tools, tool.tools], true) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return tools;
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
...body,
|
|
70
|
+
tools: merge(groups),
|
|
71
|
+
input: body.input.filter((item) => !record(item) || item.type !== "additional_tools"),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
19
74
|
function string(value, field) {
|
|
20
75
|
if (typeof value !== "string")
|
|
21
76
|
fail(`${field} 必须是字符串`);
|
|
@@ -476,6 +531,8 @@ function normalizeSourceItems(body, to) {
|
|
|
476
531
|
export function translateAgyRequest(body, upstreamModel, to = "responses") {
|
|
477
532
|
if (typeof upstreamModel !== "string" || !upstreamModel)
|
|
478
533
|
fail("上游模型不能为空");
|
|
534
|
+
if (to === "responses")
|
|
535
|
+
body = normalizeAdditionalTools(body);
|
|
479
536
|
body = normalizeSourceItems(body, to);
|
|
480
537
|
if (body.previous_response_id !== undefined && body.previous_response_id !== null && body.previous_response_id !== "") {
|
|
481
538
|
fail("Antigravity 不支持 previous_response_id;请提供完整 input 历史");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-proxy/agy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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.
|
|
38
|
+
"@agent-proxy/models": "0.16.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@agent-proxy/plugin-api": "0.
|
|
41
|
+
"@agent-proxy/plugin-api": "0.16.0"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "062121f545d3391665789b2e8baae842029262be"
|
|
44
44
|
}
|