my-pi-agent 0.1.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/README.md +318 -0
- package/package.json +45 -0
- package/pyproject.toml +50 -0
- package/src/my_agent_core/__init__.py +123 -0
- package/src/my_agent_core/agent.py +441 -0
- package/src/my_agent_core/background.py +121 -0
- package/src/my_agent_core/context.py +505 -0
- package/src/my_agent_core/events.py +153 -0
- package/src/my_agent_core/extensions/__init__.py +9 -0
- package/src/my_agent_core/extensions/core.py +197 -0
- package/src/my_agent_core/hooks.py +130 -0
- package/src/my_agent_core/loop.py +709 -0
- package/src/my_agent_core/main.py +134 -0
- package/src/my_agent_core/memory.py +241 -0
- package/src/my_agent_core/message_queue.py +110 -0
- package/src/my_agent_core/plugins.py +212 -0
- package/src/my_agent_core/registry.py +186 -0
- package/src/my_agent_core/session/__init__.py +79 -0
- package/src/my_agent_core/session/entries.py +197 -0
- package/src/my_agent_core/session/jsonl.py +60 -0
- package/src/my_agent_core/session/memory.py +137 -0
- package/src/my_agent_core/session/session.py +400 -0
- package/src/my_agent_core/session/storage.py +245 -0
- package/src/my_agent_core/session/store.py +131 -0
- package/src/my_agent_core/session/tree.py +86 -0
- package/src/my_agent_core/skills.py +149 -0
- package/src/my_agent_core/subagent_tasks.py +170 -0
- package/src/my_agent_core/subagents.py +148 -0
- package/src/my_agent_core/task_store.py +248 -0
- package/src/my_agent_core/tool_history.py +189 -0
- package/src/my_agent_core/tools/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/task.py +30 -0
- package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
- package/src/my_agent_core/tools/core.py +239 -0
- package/src/my_agent_llm/__init__.py +45 -0
- package/src/my_agent_llm/auth/__init__.py +46 -0
- package/src/my_agent_llm/auth/antigravity.py +209 -0
- package/src/my_agent_llm/auth/manager.py +259 -0
- package/src/my_agent_llm/auth/quota.py +56 -0
- package/src/my_agent_llm/auth/schema.py +94 -0
- package/src/my_agent_llm/client.py +116 -0
- package/src/my_agent_llm/config.py +17 -0
- package/src/my_agent_llm/events.py +84 -0
- package/src/my_agent_llm/models.py +195 -0
- package/src/my_agent_llm/providers/__init__.py +4 -0
- package/src/my_agent_llm/providers/_base.py +94 -0
- package/src/my_agent_llm/providers/anthropic.py +298 -0
- package/src/my_agent_llm/providers/antigravity.py +480 -0
- package/src/my_agent_llm/providers/deepseek.py +196 -0
- package/src/my_agent_llm/providers/openai.py +364 -0
- package/src/my_agent_llm/providers/registry.py +16 -0
- package/src/my_agent_llm/stream.py +218 -0
- package/src/my_coding_agent/__init__.py +66 -0
- package/src/my_coding_agent/agent.py +208 -0
- package/src/my_coding_agent/cli.py +78 -0
- package/src/my_coding_agent/file_reference.py +80 -0
- package/src/my_coding_agent/macro.py +408 -0
- package/src/my_coding_agent/mcp.py +243 -0
- package/src/my_coding_agent/mutation_queue.py +37 -0
- package/src/my_coding_agent/paths.py +119 -0
- package/src/my_coding_agent/permissions.py +84 -0
- package/src/my_coding_agent/prompt.py +54 -0
- package/src/my_coding_agent/rpc_server.py +2817 -0
- package/src/my_coding_agent/settings.py +126 -0
- package/src/my_coding_agent/tools/__init__.py +55 -0
- package/src/my_coding_agent/tools/base.py +58 -0
- package/src/my_coding_agent/tools/bash.py +206 -0
- package/src/my_coding_agent/tools/edit.py +226 -0
- package/src/my_coding_agent/tools/find.py +118 -0
- package/src/my_coding_agent/tools/grep.py +177 -0
- package/src/my_coding_agent/tools/ls.py +112 -0
- package/src/my_coding_agent/tools/read.py +113 -0
- package/src/my_coding_agent/tools/write.py +72 -0
- package/tui/README.md +27 -0
- package/tui/bin/my-agent.js +98 -0
- package/tui/dist/app.d.ts +41 -0
- package/tui/dist/app.js +110 -0
- package/tui/dist/bridge/event-translator.d.ts +92 -0
- package/tui/dist/bridge/event-translator.js +216 -0
- package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
- package/tui/dist/bridge/kernel-bridge.js +132 -0
- package/tui/dist/client.d.ts +63 -0
- package/tui/dist/client.js +239 -0
- package/tui/dist/components/assistant-message.d.ts +19 -0
- package/tui/dist/components/assistant-message.js +90 -0
- package/tui/dist/components/compaction-summary-message.d.ts +19 -0
- package/tui/dist/components/compaction-summary-message.js +46 -0
- package/tui/dist/components/custom-editor.d.ts +18 -0
- package/tui/dist/components/custom-editor.js +56 -0
- package/tui/dist/components/dynamic-border.d.ts +9 -0
- package/tui/dist/components/dynamic-border.js +14 -0
- package/tui/dist/components/footer.d.ts +39 -0
- package/tui/dist/components/footer.js +199 -0
- package/tui/dist/components/header.d.ts +4 -0
- package/tui/dist/components/header.js +21 -0
- package/tui/dist/components/keys.d.ts +5 -0
- package/tui/dist/components/keys.js +12 -0
- package/tui/dist/components/login-selector.d.ts +26 -0
- package/tui/dist/components/login-selector.js +181 -0
- package/tui/dist/components/logout-selector.d.ts +19 -0
- package/tui/dist/components/logout-selector.js +88 -0
- package/tui/dist/components/model-selector.d.ts +40 -0
- package/tui/dist/components/model-selector.js +268 -0
- package/tui/dist/components/session-selector.d.ts +54 -0
- package/tui/dist/components/session-selector.js +393 -0
- package/tui/dist/components/settings-selector.d.ts +24 -0
- package/tui/dist/components/settings-selector.js +146 -0
- package/tui/dist/components/status-indicator.d.ts +25 -0
- package/tui/dist/components/status-indicator.js +60 -0
- package/tui/dist/components/theme-selector.d.ts +14 -0
- package/tui/dist/components/theme-selector.js +77 -0
- package/tui/dist/components/thinking-selector.d.ts +21 -0
- package/tui/dist/components/thinking-selector.js +128 -0
- package/tui/dist/components/tool-execution.d.ts +31 -0
- package/tui/dist/components/tool-execution.js +206 -0
- package/tui/dist/components/tree-selector.d.ts +40 -0
- package/tui/dist/components/tree-selector.js +173 -0
- package/tui/dist/components/user-message-selector.d.ts +21 -0
- package/tui/dist/components/user-message-selector.js +103 -0
- package/tui/dist/components/user-message.d.ts +5 -0
- package/tui/dist/components/user-message.js +15 -0
- package/tui/dist/index.d.ts +11 -0
- package/tui/dist/index.js +11 -0
- package/tui/dist/interactive/chat-viewport.d.ts +19 -0
- package/tui/dist/interactive/chat-viewport.js +41 -0
- package/tui/dist/interactive/components.d.ts +1 -0
- package/tui/dist/interactive/components.js +1 -0
- package/tui/dist/interactive/interactive-mode.d.ts +89 -0
- package/tui/dist/interactive/interactive-mode.js +1625 -0
- package/tui/dist/interactive/theme.d.ts +1 -0
- package/tui/dist/interactive/theme.js +1 -0
- package/tui/dist/interactive/tui-renderer.d.ts +8 -0
- package/tui/dist/interactive/tui-renderer.js +10 -0
- package/tui/dist/protocol.d.ts +78 -0
- package/tui/dist/protocol.js +1 -0
- package/tui/dist/theme/dark.json +54 -0
- package/tui/dist/theme/light.json +71 -0
- package/tui/dist/theme/theme.d.ts +20 -0
- package/tui/dist/theme/theme.js +86 -0
- package/tui/package.json +25 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""工具声明与分发 —— 核心模型与装饰器(Pydantic 驱动)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, get_type_hints, overload
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, ValidationError, create_model
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ToolResult:
|
|
16
|
+
"""工具执行结果:成功/失败 + 数据或错误消息 + 结构化元数据。"""
|
|
17
|
+
|
|
18
|
+
ok: bool
|
|
19
|
+
data: Any = None
|
|
20
|
+
error: str | None = None
|
|
21
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
22
|
+
terminate: bool = False
|
|
23
|
+
|
|
24
|
+
def serialize(self) -> str:
|
|
25
|
+
"""转成写入 messages 的字符串。失败时返回错误文本。"""
|
|
26
|
+
if self.ok:
|
|
27
|
+
return str(self.data)
|
|
28
|
+
return self.error or "Unknown error"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_FRAMEWORK_RESERVED_PARAMS: frozenset[str] = frozenset(
|
|
32
|
+
{"on_update", "signal", "tool_call_id"}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Tool:
|
|
37
|
+
"""一个可被模型调用的工具:函数本体 + 参数模型/原始Schema + 协议转换 + 超时与并发配置。"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
func: Callable[..., Any],
|
|
42
|
+
*,
|
|
43
|
+
name: str | None = None,
|
|
44
|
+
description: str | None = None,
|
|
45
|
+
params_model: type[BaseModel] | None = None,
|
|
46
|
+
raw_schema: dict[str, Any] | None = None,
|
|
47
|
+
timeout: float | None = None,
|
|
48
|
+
is_parallel_safe: bool = False,
|
|
49
|
+
):
|
|
50
|
+
self.func = func
|
|
51
|
+
self.name = name or func.__name__
|
|
52
|
+
self.description = description or (inspect.getdoc(func) or "")
|
|
53
|
+
self.raw_schema = raw_schema
|
|
54
|
+
self.timeout = timeout
|
|
55
|
+
self.is_parallel_safe = is_parallel_safe
|
|
56
|
+
self.is_async = inspect.iscoroutinefunction(func)
|
|
57
|
+
sig = inspect.signature(func)
|
|
58
|
+
self._accepts_on_update = "on_update" in sig.parameters
|
|
59
|
+
self._accepts_signal = "signal" in sig.parameters
|
|
60
|
+
self._accepts_tool_call_id = "tool_call_id" in sig.parameters
|
|
61
|
+
self.params_model = params_model or (
|
|
62
|
+
None if raw_schema else self._create_params_model(func)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _create_params_model(self, func: Callable[..., Any]) -> type[BaseModel]:
|
|
66
|
+
"""从函数签名动态建模(pydantic create_model)。"""
|
|
67
|
+
hints = get_type_hints(func)
|
|
68
|
+
fields: dict[str, Any] = {}
|
|
69
|
+
for param_name, param in inspect.signature(func).parameters.items():
|
|
70
|
+
if param_name in _FRAMEWORK_RESERVED_PARAMS:
|
|
71
|
+
continue
|
|
72
|
+
if param.kind in (
|
|
73
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
74
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
75
|
+
):
|
|
76
|
+
raise TypeError(
|
|
77
|
+
f"tool '{func.__name__}': parameter '{param_name}' is "
|
|
78
|
+
"*args/**kwargs, which is not supported(不支持)"
|
|
79
|
+
)
|
|
80
|
+
if param_name not in hints:
|
|
81
|
+
raise TypeError(
|
|
82
|
+
f"tool '{func.__name__}': parameter '{param_name}' "
|
|
83
|
+
"has no type annotation(没有类型标注)"
|
|
84
|
+
)
|
|
85
|
+
default = ... if param.default is inspect.Parameter.empty else param.default
|
|
86
|
+
fields[param_name] = (hints[param_name], default)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
model = create_model(
|
|
90
|
+
f"{func.__name__}_Args",
|
|
91
|
+
__config__=ConfigDict(extra="forbid"), # 多余参数 → 校验错误
|
|
92
|
+
**fields,
|
|
93
|
+
)
|
|
94
|
+
return model
|
|
95
|
+
except Exception as exc: # 无法建模的类型 → 装饰时明确失败
|
|
96
|
+
raise TypeError(
|
|
97
|
+
f"tool '{func.__name__}': cannot build parameter schema: {exc}"
|
|
98
|
+
) from exc
|
|
99
|
+
|
|
100
|
+
def to_openai_schema(self) -> dict[str, Any]:
|
|
101
|
+
"""生成 OpenAI tools 参数。"""
|
|
102
|
+
if self.raw_schema is not None:
|
|
103
|
+
params = self.raw_schema
|
|
104
|
+
elif self.params_model is not None:
|
|
105
|
+
params = self.params_model.model_json_schema()
|
|
106
|
+
else:
|
|
107
|
+
params = {}
|
|
108
|
+
return {
|
|
109
|
+
"type": "function",
|
|
110
|
+
"function": {
|
|
111
|
+
"name": self.name,
|
|
112
|
+
"description": self.description,
|
|
113
|
+
"parameters": params,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async def execute(
|
|
118
|
+
self,
|
|
119
|
+
args: dict[str, Any],
|
|
120
|
+
signal: Any | None = None,
|
|
121
|
+
on_update: Callable[[Any], None] | None = None,
|
|
122
|
+
tool_call_id: str | None = None,
|
|
123
|
+
) -> ToolResult:
|
|
124
|
+
"""校验 + 执行,永不抛(错误全部转 ToolResult)。自动适配 sync/async 函数。"""
|
|
125
|
+
accepting_updates = True
|
|
126
|
+
|
|
127
|
+
def guarded_on_update(partial: Any) -> None:
|
|
128
|
+
if accepting_updates and on_update is not None:
|
|
129
|
+
on_update(partial)
|
|
130
|
+
|
|
131
|
+
extra_kwargs: dict[str, Any] = {}
|
|
132
|
+
if self._accepts_on_update:
|
|
133
|
+
extra_kwargs["on_update"] = guarded_on_update
|
|
134
|
+
if self._accepts_signal:
|
|
135
|
+
extra_kwargs["signal"] = signal
|
|
136
|
+
if self._accepts_tool_call_id and tool_call_id is not None:
|
|
137
|
+
extra_kwargs["tool_call_id"] = tool_call_id
|
|
138
|
+
|
|
139
|
+
if self.params_model is not None:
|
|
140
|
+
try:
|
|
141
|
+
validated = self.params_model.model_validate(args)
|
|
142
|
+
except ValidationError as exc:
|
|
143
|
+
return ToolResult(ok=False, error=str(exc))
|
|
144
|
+
kwargs = {**validated.model_dump(), **extra_kwargs}
|
|
145
|
+
|
|
146
|
+
def func_call() -> Any:
|
|
147
|
+
return self.func(**kwargs)
|
|
148
|
+
|
|
149
|
+
async def async_func_call() -> Any:
|
|
150
|
+
return await self.func(**kwargs)
|
|
151
|
+
else:
|
|
152
|
+
|
|
153
|
+
def func_call() -> Any:
|
|
154
|
+
if extra_kwargs:
|
|
155
|
+
return self.func(args, **extra_kwargs)
|
|
156
|
+
return self.func(args)
|
|
157
|
+
|
|
158
|
+
async def async_func_call() -> Any:
|
|
159
|
+
if extra_kwargs:
|
|
160
|
+
return await self.func(args, **extra_kwargs)
|
|
161
|
+
return await self.func(args)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
if self.is_async:
|
|
165
|
+
result = await async_func_call()
|
|
166
|
+
else:
|
|
167
|
+
result = await asyncio.to_thread(func_call)
|
|
168
|
+
except Exception as exc: # 工具错误 → 消息,喂回模型
|
|
169
|
+
return ToolResult(
|
|
170
|
+
ok=False, error=f"Error executing tool '{self.name}': {exc}"
|
|
171
|
+
)
|
|
172
|
+
finally:
|
|
173
|
+
accepting_updates = False
|
|
174
|
+
|
|
175
|
+
if isinstance(result, ToolResult):
|
|
176
|
+
return result
|
|
177
|
+
return ToolResult(ok=True, data=result)
|
|
178
|
+
|
|
179
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
180
|
+
"""直接调用工具函数本体。"""
|
|
181
|
+
return self.func(*args, **kwargs)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@overload
|
|
185
|
+
def tool(
|
|
186
|
+
func: Callable[..., Any],
|
|
187
|
+
*,
|
|
188
|
+
name: str | None = None,
|
|
189
|
+
description: str | None = None,
|
|
190
|
+
params_model: type[BaseModel] | None = None,
|
|
191
|
+
raw_schema: dict[str, Any] | None = None,
|
|
192
|
+
timeout: float | None = None,
|
|
193
|
+
is_parallel_safe: bool = False,
|
|
194
|
+
) -> Tool: ...
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@overload
|
|
198
|
+
def tool(
|
|
199
|
+
func: None = None,
|
|
200
|
+
*,
|
|
201
|
+
name: str | None = None,
|
|
202
|
+
description: str | None = None,
|
|
203
|
+
params_model: type[BaseModel] | None = None,
|
|
204
|
+
raw_schema: dict[str, Any] | None = None,
|
|
205
|
+
timeout: float | None = None,
|
|
206
|
+
is_parallel_safe: bool = False,
|
|
207
|
+
) -> Callable[[Callable[..., Any]], Tool]: ...
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def tool(
|
|
211
|
+
func: Callable[..., Any] | None = None,
|
|
212
|
+
*,
|
|
213
|
+
name: str | None = None,
|
|
214
|
+
description: str | None = None,
|
|
215
|
+
params_model: type[BaseModel] | None = None,
|
|
216
|
+
raw_schema: dict[str, Any] | None = None,
|
|
217
|
+
timeout: float | None = None,
|
|
218
|
+
is_parallel_safe: bool = False,
|
|
219
|
+
) -> Tool | Callable[[Callable[..., Any]], Tool]:
|
|
220
|
+
"""@tool 装饰器:支持 @tool 与 @tool(name=..., description=..., params_model=...)。
|
|
221
|
+
|
|
222
|
+
schema 生成由 pydantic 驱动:参数类型支持 pydantic 全集(list/dict/Optional/嵌套等),
|
|
223
|
+
允许默认值;无标注参数与 *args/**kwargs 在装饰时抛 TypeError(明确失败)。
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def decorator(f: Callable[..., Any]) -> Tool:
|
|
227
|
+
return Tool(
|
|
228
|
+
func=f,
|
|
229
|
+
name=name,
|
|
230
|
+
description=description,
|
|
231
|
+
params_model=params_model,
|
|
232
|
+
raw_schema=raw_schema,
|
|
233
|
+
timeout=timeout,
|
|
234
|
+
is_parallel_safe=is_parallel_safe,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if func is None:
|
|
238
|
+
return decorator
|
|
239
|
+
return decorator(func)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""统一 LLM 客户端包(模型边界层)。"""
|
|
2
|
+
|
|
3
|
+
from .client import LLM
|
|
4
|
+
from .config import Config
|
|
5
|
+
from .events import ( # pyright: ignore[reportMissingImports]
|
|
6
|
+
StreamDoneEvent,
|
|
7
|
+
StreamErrorEvent,
|
|
8
|
+
StreamEvent,
|
|
9
|
+
StreamStartEvent,
|
|
10
|
+
TextDeltaEvent,
|
|
11
|
+
ThinkingDeltaEvent,
|
|
12
|
+
ToolCallDeltaEvent,
|
|
13
|
+
ToolCallDoneEvent,
|
|
14
|
+
)
|
|
15
|
+
from .models import (
|
|
16
|
+
Message,
|
|
17
|
+
Response,
|
|
18
|
+
StreamChunk,
|
|
19
|
+
ToolCall,
|
|
20
|
+
TurnOutcome,
|
|
21
|
+
normalize_finish_reason,
|
|
22
|
+
)
|
|
23
|
+
from .providers import Provider
|
|
24
|
+
from .stream import StreamAccumulator # pyright: ignore[reportMissingImports]
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"LLM",
|
|
28
|
+
"Config",
|
|
29
|
+
"Message",
|
|
30
|
+
"Response",
|
|
31
|
+
"StreamAccumulator",
|
|
32
|
+
"StreamChunk",
|
|
33
|
+
"StreamDoneEvent",
|
|
34
|
+
"StreamErrorEvent",
|
|
35
|
+
"StreamEvent",
|
|
36
|
+
"StreamStartEvent",
|
|
37
|
+
"TextDeltaEvent",
|
|
38
|
+
"ThinkingDeltaEvent",
|
|
39
|
+
"ToolCallDeltaEvent",
|
|
40
|
+
"ToolCallDoneEvent",
|
|
41
|
+
"ToolCall",
|
|
42
|
+
"TurnOutcome",
|
|
43
|
+
"normalize_finish_reason",
|
|
44
|
+
"Provider",
|
|
45
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""凭据管理与鉴权模块。"""
|
|
2
|
+
|
|
3
|
+
from .antigravity import ( # pyright: ignore[reportMissingImports]
|
|
4
|
+
ANTIGRAVITY_USER_AGENT,
|
|
5
|
+
DEFAULT_ANTIGRAVITY_ENDPOINT,
|
|
6
|
+
GOOGLE_OAUTH_TOKEN_URL,
|
|
7
|
+
AntigravityAuthResolver,
|
|
8
|
+
AntigravityCredentials,
|
|
9
|
+
)
|
|
10
|
+
from .manager import ( # pyright: ignore[reportMissingImports]
|
|
11
|
+
DEFAULT_ANTIGRAVITY_CLIENT_ID,
|
|
12
|
+
DEFAULT_ANTIGRAVITY_CLIENT_SECRET,
|
|
13
|
+
GOOGLE_TOKEN_URL,
|
|
14
|
+
AuthManager,
|
|
15
|
+
)
|
|
16
|
+
from .quota import ( # pyright: ignore[reportMissingImports]
|
|
17
|
+
QuotaBucket,
|
|
18
|
+
retrieve_user_quota_summary,
|
|
19
|
+
)
|
|
20
|
+
from .schema import ( # pyright: ignore[reportMissingImports]
|
|
21
|
+
ApiKeyCredential,
|
|
22
|
+
AuthStore,
|
|
23
|
+
Credential,
|
|
24
|
+
CredentialType,
|
|
25
|
+
OAuthCredential,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ANTIGRAVITY_USER_AGENT",
|
|
30
|
+
"DEFAULT_ANTIGRAVITY_ENDPOINT",
|
|
31
|
+
"GOOGLE_OAUTH_TOKEN_URL",
|
|
32
|
+
"AntigravityAuthResolver",
|
|
33
|
+
"AntigravityCredentials",
|
|
34
|
+
"QuotaBucket",
|
|
35
|
+
"retrieve_user_quota_summary",
|
|
36
|
+
# AuthManager & 凭据模型
|
|
37
|
+
"DEFAULT_ANTIGRAVITY_CLIENT_ID",
|
|
38
|
+
"DEFAULT_ANTIGRAVITY_CLIENT_SECRET",
|
|
39
|
+
"GOOGLE_TOKEN_URL",
|
|
40
|
+
"AuthManager",
|
|
41
|
+
"ApiKeyCredential",
|
|
42
|
+
"AuthStore",
|
|
43
|
+
"Credential",
|
|
44
|
+
"CredentialType",
|
|
45
|
+
"OAuthCredential",
|
|
46
|
+
]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
DEFAULT_ANTIGRAVITY_ENDPOINT = "https://cloudcode-pa.googleapis.com"
|
|
16
|
+
ANTIGRAVITY_USER_AGENT = "antigravity/cli/1.1.23 (aidev_client; os_type=windows; arch=amd64; auth_method=consumer)"
|
|
17
|
+
GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token" # noqa: S105
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AntigravityCredentials:
|
|
22
|
+
"""Antigravity / Google Cloud Code 鉴权凭据实体。"""
|
|
23
|
+
|
|
24
|
+
access_token: str
|
|
25
|
+
refresh_token: str | None = None
|
|
26
|
+
expires_at: int = 0
|
|
27
|
+
project_id: str = "aicode-consumers"
|
|
28
|
+
email: str | None = None
|
|
29
|
+
auth_file_path: Path | None = None
|
|
30
|
+
client_id: str | None = None
|
|
31
|
+
client_secret: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AntigravityAuthResolver:
|
|
35
|
+
"""Antigravity / Google Cloud Code Assist 凭据解析与自动刷新器(对标 pi-antigravity)。"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
pi_auth_path: Path | None = None,
|
|
40
|
+
credentials_path: Path | None = None,
|
|
41
|
+
workspace: Path | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
home = Path(os.environ.get("USERPROFILE") or os.environ.get("HOME") or "~").expanduser()
|
|
44
|
+
self.workspace_auth_path = (Path(workspace).resolve() / ".my_agent" / "auth.json") if workspace else None
|
|
45
|
+
if pi_auth_path is not None:
|
|
46
|
+
self.pi_auth_path = Path(pi_auth_path).resolve()
|
|
47
|
+
elif "MY_AGENT_HOME" not in os.environ:
|
|
48
|
+
default_pi_auth = home / ".pi" / "agent" / "auth.json"
|
|
49
|
+
self.pi_auth_path = default_pi_auth if default_pi_auth.exists() else None
|
|
50
|
+
else:
|
|
51
|
+
self.pi_auth_path = None
|
|
52
|
+
|
|
53
|
+
if credentials_path is not None:
|
|
54
|
+
self.credentials_path = Path(credentials_path).resolve()
|
|
55
|
+
else:
|
|
56
|
+
self.credentials_path = home / ".my_agent" / "credentials.json"
|
|
57
|
+
|
|
58
|
+
def resolve_credentials_raw(self) -> AntigravityCredentials | None:
|
|
59
|
+
"""从工作区/项目凭据或环境变量动态获取凭据,零硬编码,隔离外部环境。"""
|
|
60
|
+
# 1. 环境变量优先
|
|
61
|
+
env_token = os.environ.get("ANTIGRAVITY_ACCESS_TOKEN") or os.environ.get("ANTIGRAVITY_API_KEY")
|
|
62
|
+
if env_token:
|
|
63
|
+
return AntigravityCredentials(
|
|
64
|
+
access_token=env_token,
|
|
65
|
+
project_id=os.environ.get("ANTIGRAVITY_PROJECT_ID", "aicode-consumers"),
|
|
66
|
+
client_id=os.environ.get("GOOGLE_OAUTH_CLIENT_ID"),
|
|
67
|
+
client_secret=os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET"),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# 2. 依次读取凭据文件
|
|
71
|
+
candidate_paths: list[Path] = []
|
|
72
|
+
if self.pi_auth_path:
|
|
73
|
+
candidate_paths.append(self.pi_auth_path)
|
|
74
|
+
if self.workspace_auth_path:
|
|
75
|
+
candidate_paths.append(self.workspace_auth_path)
|
|
76
|
+
if self.credentials_path:
|
|
77
|
+
candidate_paths.append(self.credentials_path)
|
|
78
|
+
|
|
79
|
+
for target_path in candidate_paths:
|
|
80
|
+
if target_path and target_path.exists():
|
|
81
|
+
try:
|
|
82
|
+
text_content = target_path.read_text(encoding="utf-8")
|
|
83
|
+
data = json.loads(text_content)
|
|
84
|
+
if not isinstance(data, dict):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
entry = data.get("antigravity") or data.get("google-antigravity")
|
|
88
|
+
if entry and isinstance(entry, dict):
|
|
89
|
+
access_token = entry.get("access") or entry.get("access_token")
|
|
90
|
+
if access_token:
|
|
91
|
+
expires_raw = entry.get("expires") or entry.get("expires_at", 0)
|
|
92
|
+
try:
|
|
93
|
+
exp_val = int(expires_raw)
|
|
94
|
+
except (ValueError, TypeError):
|
|
95
|
+
exp_val = 0
|
|
96
|
+
return AntigravityCredentials(
|
|
97
|
+
access_token=access_token,
|
|
98
|
+
refresh_token=entry.get("refresh") or entry.get("refresh_token"),
|
|
99
|
+
expires_at=exp_val,
|
|
100
|
+
project_id=entry.get("projectId") or entry.get("project_id", "aicode-consumers"),
|
|
101
|
+
email=entry.get("email"),
|
|
102
|
+
auth_file_path=target_path,
|
|
103
|
+
client_id=entry.get("client_id") or os.environ.get("GOOGLE_OAUTH_CLIENT_ID"),
|
|
104
|
+
client_secret=entry.get("client_secret")
|
|
105
|
+
or os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET"),
|
|
106
|
+
)
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
logger.debug("Failed to read credentials from %s: %s", target_path, exc)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def resolve_credentials(self) -> AntigravityCredentials | None:
|
|
114
|
+
"""获取凭据别名,对齐规范。"""
|
|
115
|
+
return self.resolve_credentials_raw()
|
|
116
|
+
|
|
117
|
+
def is_expired(self, creds: AntigravityCredentials | None) -> bool:
|
|
118
|
+
"""检查凭据是否已过期或即将过期(剩余不足 5 分钟)。"""
|
|
119
|
+
if not creds:
|
|
120
|
+
return True
|
|
121
|
+
if creds.expires_at <= 0:
|
|
122
|
+
return False
|
|
123
|
+
try:
|
|
124
|
+
return creds.expires_at <= int(time.time() * 1000) + 300000
|
|
125
|
+
except Exception:
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
def refresh(self, creds: AntigravityCredentials) -> AntigravityCredentials:
|
|
129
|
+
"""向 Google OAuth 端点发起刷新请求换取新 token 并持久化写回。"""
|
|
130
|
+
if not creds.refresh_token:
|
|
131
|
+
raise RuntimeError("Cannot refresh Antigravity token: missing refresh_token.")
|
|
132
|
+
|
|
133
|
+
client_id = creds.client_id or os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
|
|
134
|
+
client_secret = creds.client_secret or os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
|
|
135
|
+
if not client_id or not client_secret:
|
|
136
|
+
raise RuntimeError("Cannot refresh Antigravity token: missing client_id/client_secret in auth.json or env.")
|
|
137
|
+
|
|
138
|
+
payload = {
|
|
139
|
+
"client_id": client_id,
|
|
140
|
+
"client_secret": client_secret,
|
|
141
|
+
"refresh_token": creds.refresh_token,
|
|
142
|
+
"grant_type": "refresh_token",
|
|
143
|
+
}
|
|
144
|
+
res = httpx.post(GOOGLE_OAUTH_TOKEN_URL, data=payload, timeout=15.0)
|
|
145
|
+
if res.status_code != 200:
|
|
146
|
+
raise RuntimeError(f"Failed to refresh Antigravity token: {res.status_code} {res.text}")
|
|
147
|
+
|
|
148
|
+
data = res.json()
|
|
149
|
+
new_access = data["access_token"]
|
|
150
|
+
expires_in = data.get("expires_in", 3600)
|
|
151
|
+
try:
|
|
152
|
+
new_expires_at = int(time.time() * 1000) + (expires_in * 1000) - (300 * 1000)
|
|
153
|
+
except Exception:
|
|
154
|
+
new_expires_at = 0
|
|
155
|
+
|
|
156
|
+
updated_creds = AntigravityCredentials(
|
|
157
|
+
access_token=new_access,
|
|
158
|
+
refresh_token=creds.refresh_token,
|
|
159
|
+
expires_at=new_expires_at,
|
|
160
|
+
project_id=creds.project_id,
|
|
161
|
+
email=creds.email,
|
|
162
|
+
auth_file_path=creds.auth_file_path,
|
|
163
|
+
client_id=client_id,
|
|
164
|
+
client_secret=client_secret,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# 写回持久化文件
|
|
168
|
+
if creds.auth_file_path and creds.auth_file_path.exists():
|
|
169
|
+
try:
|
|
170
|
+
raw_text = creds.auth_file_path.read_text(encoding="utf-8")
|
|
171
|
+
raw_data = json.loads(raw_text)
|
|
172
|
+
target_key = (
|
|
173
|
+
"antigravity"
|
|
174
|
+
if "antigravity" in raw_data
|
|
175
|
+
else ("google-antigravity" if "google-antigravity" in raw_data else "antigravity")
|
|
176
|
+
)
|
|
177
|
+
if target_key not in raw_data:
|
|
178
|
+
raw_data[target_key] = {}
|
|
179
|
+
if "access" in raw_data[target_key] or "access_token" not in raw_data[target_key]:
|
|
180
|
+
raw_data[target_key]["access"] = new_access
|
|
181
|
+
if "access_token" in raw_data[target_key]:
|
|
182
|
+
raw_data[target_key]["access_token"] = new_access
|
|
183
|
+
raw_data[target_key]["expires"] = new_expires_at
|
|
184
|
+
# 原子写入
|
|
185
|
+
tmp_file = creds.auth_file_path.with_suffix(".tmp")
|
|
186
|
+
tmp_file.write_text(json.dumps(raw_data, indent=2), encoding="utf-8")
|
|
187
|
+
tmp_file.replace(creds.auth_file_path)
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
logger.debug("Failed to write back refreshed credentials: %s", exc)
|
|
190
|
+
|
|
191
|
+
return updated_creds
|
|
192
|
+
|
|
193
|
+
def refresh_token(self, creds: AntigravityCredentials) -> AntigravityCredentials:
|
|
194
|
+
"""refresh 的别名方法。"""
|
|
195
|
+
return self.refresh(creds)
|
|
196
|
+
|
|
197
|
+
def get_valid_credentials(self) -> AntigravityCredentials:
|
|
198
|
+
"""获取有效凭据。优先直接使用 auth.json 凭据;若过期且配置了密钥则自动刷新。"""
|
|
199
|
+
creds = self.resolve_credentials_raw()
|
|
200
|
+
if not creds:
|
|
201
|
+
raise RuntimeError(
|
|
202
|
+
f"No Antigravity credentials found. Please ensure {self.pi_auth_path} exists or set ANTIGRAVITY_ACCESS_TOKEN."
|
|
203
|
+
)
|
|
204
|
+
client_id = creds.client_id or os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
|
|
205
|
+
client_secret = creds.client_secret or os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
|
|
206
|
+
if self.is_expired(creds) and creds.refresh_token and client_id and client_secret:
|
|
207
|
+
with contextlib.suppress(Exception):
|
|
208
|
+
creds = self.refresh(creds)
|
|
209
|
+
return creds
|