mycode-coding-agent 0.1.0__py3-none-any.whl
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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/mcp/trust.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Presentation-neutral project MCP trust resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal, Protocol
|
|
11
|
+
from urllib.parse import urlsplit
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
|
15
|
+
|
|
16
|
+
from mycode.config import MYCODE_CONFIG_DIR_NAME
|
|
17
|
+
from mycode.mcp.config import (
|
|
18
|
+
MCPConfig,
|
|
19
|
+
MCPLoadedConfig,
|
|
20
|
+
MCPStdioServerConfig,
|
|
21
|
+
MCPStreamableHTTPServerConfig,
|
|
22
|
+
merge_mcp_configs,
|
|
23
|
+
)
|
|
24
|
+
from mycode.project import ProjectIdentity
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DEFAULT_MCP_TRUST_FILE = "mcp-trust.json"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MCPTrustStore(BaseModel):
|
|
31
|
+
model_config = ConfigDict(extra="forbid")
|
|
32
|
+
version: Literal[1] = 1
|
|
33
|
+
projects: dict[str, str] = Field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
@field_validator("projects")
|
|
36
|
+
@classmethod
|
|
37
|
+
def _valid_digests(cls, value: dict[str, str]) -> dict[str, str]:
|
|
38
|
+
if any(
|
|
39
|
+
not _is_sha256(key) or not _is_sha256(digest)
|
|
40
|
+
for key, digest in value.items()
|
|
41
|
+
):
|
|
42
|
+
raise ValueError("project keys and fingerprints must be SHA-256 digests")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
MCPTrustTransport = Literal["stdio", "streamable_http"]
|
|
47
|
+
MCPTrustWarningCode = Literal["invalid_trust_store", "persistence_failed"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class MCPTrustServer:
|
|
52
|
+
"""Safe, displayable project MCP information without secret values."""
|
|
53
|
+
|
|
54
|
+
alias: str
|
|
55
|
+
transport: MCPTrustTransport
|
|
56
|
+
command: str | None = None
|
|
57
|
+
args: tuple[str, ...] = ()
|
|
58
|
+
env_keys: tuple[str, ...] = ()
|
|
59
|
+
url_template: str | None = None
|
|
60
|
+
destination: str | None = None
|
|
61
|
+
header_keys: tuple[str, ...] = ()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class MCPTrustRequest:
|
|
66
|
+
servers: tuple[MCPTrustServer, ...]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class MCPTrustWarning:
|
|
71
|
+
code: MCPTrustWarningCode
|
|
72
|
+
message: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class MCPTrustResolution:
|
|
77
|
+
config: MCPConfig
|
|
78
|
+
approved: bool | None = None
|
|
79
|
+
warnings: tuple[MCPTrustWarning, ...] = ()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MCPTrustConfirmer(Protocol):
|
|
83
|
+
def confirm(self, request: MCPTrustRequest) -> bool:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def default_mcp_trust_file() -> Path:
|
|
88
|
+
return Path.home() / MYCODE_CONFIG_DIR_NAME / DEFAULT_MCP_TRUST_FILE
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def project_mcp_fingerprint(loaded: MCPLoadedConfig) -> str | None:
|
|
92
|
+
entries: list[dict[str, object]] = []
|
|
93
|
+
for alias in sorted(loaded.project.mcp_servers):
|
|
94
|
+
resolved = loaded.project.mcp_servers[alias]
|
|
95
|
+
unresolved = loaded.project_unresolved.mcp_servers[alias]
|
|
96
|
+
if isinstance(resolved, MCPStdioServerConfig) and isinstance(
|
|
97
|
+
unresolved, MCPStdioServerConfig
|
|
98
|
+
):
|
|
99
|
+
entries.append(
|
|
100
|
+
{
|
|
101
|
+
"alias": alias,
|
|
102
|
+
"transport": "stdio",
|
|
103
|
+
"command": {
|
|
104
|
+
"unresolved": unresolved.command,
|
|
105
|
+
"resolved": resolved.command,
|
|
106
|
+
},
|
|
107
|
+
"args": {
|
|
108
|
+
"unresolved": unresolved.args,
|
|
109
|
+
"resolved": resolved.args,
|
|
110
|
+
},
|
|
111
|
+
"env": unresolved.env,
|
|
112
|
+
"connect_timeout": resolved.connect_timeout,
|
|
113
|
+
"tool_timeout": resolved.tool_timeout,
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
continue
|
|
117
|
+
if isinstance(resolved, MCPStreamableHTTPServerConfig) and isinstance(
|
|
118
|
+
unresolved, MCPStreamableHTTPServerConfig
|
|
119
|
+
):
|
|
120
|
+
entries.append(
|
|
121
|
+
{
|
|
122
|
+
"alias": alias,
|
|
123
|
+
"transport": "streamable_http",
|
|
124
|
+
"url": {
|
|
125
|
+
"unresolved": unresolved.url,
|
|
126
|
+
"resolved": resolved.url,
|
|
127
|
+
},
|
|
128
|
+
"headers": unresolved.headers,
|
|
129
|
+
"connect_timeout": resolved.connect_timeout,
|
|
130
|
+
"tool_timeout": resolved.tool_timeout,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
continue
|
|
134
|
+
raise TypeError("MCP project config layers do not match")
|
|
135
|
+
if not entries:
|
|
136
|
+
return None
|
|
137
|
+
canonical = json.dumps(
|
|
138
|
+
{"version": 1, "servers": entries},
|
|
139
|
+
ensure_ascii=False,
|
|
140
|
+
separators=(",", ":"),
|
|
141
|
+
sort_keys=True,
|
|
142
|
+
).encode("utf-8")
|
|
143
|
+
return hashlib.sha256(canonical).hexdigest()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def resolve_project_mcp_trust(
|
|
147
|
+
loaded: MCPLoadedConfig,
|
|
148
|
+
project: ProjectIdentity,
|
|
149
|
+
*,
|
|
150
|
+
confirmer: MCPTrustConfirmer,
|
|
151
|
+
trust_file: str | Path | None = None,
|
|
152
|
+
) -> MCPTrustResolution:
|
|
153
|
+
fingerprint = project_mcp_fingerprint(loaded)
|
|
154
|
+
if fingerprint is None:
|
|
155
|
+
return MCPTrustResolution(config=loaded.merged)
|
|
156
|
+
|
|
157
|
+
path = default_mcp_trust_file() if trust_file is None else Path(trust_file)
|
|
158
|
+
store, invalid = _read_trust_store(path)
|
|
159
|
+
warnings: list[MCPTrustWarning] = []
|
|
160
|
+
if invalid:
|
|
161
|
+
warning = MCPTrustWarning(
|
|
162
|
+
code="invalid_trust_store",
|
|
163
|
+
message=(
|
|
164
|
+
"MCP trust store is invalid; project MCP will be treated as untrusted."
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
warnings.append(warning)
|
|
168
|
+
_report_warning(confirmer, warning)
|
|
169
|
+
if store.projects.get(project.key) == fingerprint:
|
|
170
|
+
return MCPTrustResolution(config=loaded.merged, warnings=tuple(warnings))
|
|
171
|
+
|
|
172
|
+
approved = confirmer.confirm(_build_trust_request(loaded))
|
|
173
|
+
if not approved:
|
|
174
|
+
return MCPTrustResolution(
|
|
175
|
+
config=merge_mcp_configs(loaded.user, MCPConfig()),
|
|
176
|
+
approved=False,
|
|
177
|
+
warnings=tuple(warnings),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
store.projects[project.key] = fingerprint
|
|
181
|
+
try:
|
|
182
|
+
_write_trust_store(path, store)
|
|
183
|
+
except OSError:
|
|
184
|
+
warning = MCPTrustWarning(
|
|
185
|
+
code="persistence_failed",
|
|
186
|
+
message=(
|
|
187
|
+
"MCP trust store could not be saved; this run remains enabled."
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
warnings.append(warning)
|
|
191
|
+
_report_warning(confirmer, warning)
|
|
192
|
+
return MCPTrustResolution(
|
|
193
|
+
config=loaded.merged,
|
|
194
|
+
approved=True,
|
|
195
|
+
warnings=tuple(warnings),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def apply_project_mcp_trust(
|
|
200
|
+
loaded: MCPLoadedConfig,
|
|
201
|
+
project: ProjectIdentity,
|
|
202
|
+
*,
|
|
203
|
+
confirmer: MCPTrustConfirmer,
|
|
204
|
+
trust_file: str | Path | None = None,
|
|
205
|
+
) -> MCPConfig:
|
|
206
|
+
"""Compatibility name for callers that only need the resolved config."""
|
|
207
|
+
|
|
208
|
+
return resolve_project_mcp_trust(
|
|
209
|
+
loaded,
|
|
210
|
+
project,
|
|
211
|
+
confirmer=confirmer,
|
|
212
|
+
trust_file=trust_file,
|
|
213
|
+
).config
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _build_trust_request(loaded: MCPLoadedConfig) -> MCPTrustRequest:
|
|
217
|
+
servers: list[MCPTrustServer] = []
|
|
218
|
+
for alias in sorted(loaded.project_unresolved.mcp_servers):
|
|
219
|
+
unresolved = loaded.project_unresolved.mcp_servers[alias]
|
|
220
|
+
resolved = loaded.project.mcp_servers[alias]
|
|
221
|
+
if isinstance(unresolved, MCPStdioServerConfig) and isinstance(
|
|
222
|
+
resolved, MCPStdioServerConfig
|
|
223
|
+
):
|
|
224
|
+
servers.append(
|
|
225
|
+
MCPTrustServer(
|
|
226
|
+
alias=alias,
|
|
227
|
+
transport="stdio",
|
|
228
|
+
command=unresolved.command,
|
|
229
|
+
args=tuple(unresolved.args),
|
|
230
|
+
env_keys=tuple(sorted(unresolved.env)),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
if isinstance(unresolved, MCPStreamableHTTPServerConfig) and isinstance(
|
|
235
|
+
resolved, MCPStreamableHTTPServerConfig
|
|
236
|
+
):
|
|
237
|
+
servers.append(
|
|
238
|
+
MCPTrustServer(
|
|
239
|
+
alias=alias,
|
|
240
|
+
transport="streamable_http",
|
|
241
|
+
url_template=_safe_url_template(unresolved.url),
|
|
242
|
+
destination=_safe_http_destination(resolved.url),
|
|
243
|
+
header_keys=tuple(sorted(unresolved.headers)),
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
continue
|
|
247
|
+
raise TypeError("MCP project config layers do not match")
|
|
248
|
+
return MCPTrustRequest(servers=tuple(servers))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _report_warning(confirmer: MCPTrustConfirmer, warning: MCPTrustWarning) -> None:
|
|
252
|
+
reporter = getattr(confirmer, "report_warning", None)
|
|
253
|
+
if reporter is not None:
|
|
254
|
+
reporter(warning)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _safe_url_template(url: str) -> str:
|
|
258
|
+
if url.startswith("${") and url.endswith("}") and url.count("${") == 1:
|
|
259
|
+
return url
|
|
260
|
+
sanitized = _safe_http_url(url, include_path=True)
|
|
261
|
+
return "<unavailable>" if sanitized is None else sanitized
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _safe_http_destination(url: str) -> str | None:
|
|
265
|
+
return _safe_http_url(url, include_path=False)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _safe_http_url(url: str, *, include_path: bool) -> str | None:
|
|
269
|
+
try:
|
|
270
|
+
parsed = urlsplit(url)
|
|
271
|
+
if parsed.scheme not in {"http", "https"} or parsed.hostname is None:
|
|
272
|
+
return None
|
|
273
|
+
host = parsed.hostname
|
|
274
|
+
if ":" in host and not host.startswith("["):
|
|
275
|
+
host = f"[{host}]"
|
|
276
|
+
port = parsed.port
|
|
277
|
+
except (TypeError, ValueError):
|
|
278
|
+
return None
|
|
279
|
+
authority = host if port is None else f"{host}:{port}"
|
|
280
|
+
path = parsed.path if include_path else ""
|
|
281
|
+
return f"{parsed.scheme}://{authority}{path}"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _read_trust_store(path: Path) -> tuple[MCPTrustStore, bool]:
|
|
285
|
+
try:
|
|
286
|
+
if not path.exists():
|
|
287
|
+
return MCPTrustStore(), False
|
|
288
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
289
|
+
return MCPTrustStore.model_validate(payload), False
|
|
290
|
+
except (OSError, UnicodeError, json.JSONDecodeError, ValidationError):
|
|
291
|
+
return MCPTrustStore(), True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _write_trust_store(path: Path, store: MCPTrustStore) -> None:
|
|
295
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
temporary = path.with_name(f".{path.name}.{uuid4().hex}.tmp")
|
|
297
|
+
try:
|
|
298
|
+
temporary.write_text(
|
|
299
|
+
json.dumps(store.model_dump(), indent=2, sort_keys=True) + "\n",
|
|
300
|
+
encoding="utf-8",
|
|
301
|
+
)
|
|
302
|
+
os.replace(temporary, path)
|
|
303
|
+
finally:
|
|
304
|
+
try:
|
|
305
|
+
temporary.unlink(missing_ok=True)
|
|
306
|
+
except OSError:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _is_sha256(value: str) -> bool:
|
|
311
|
+
return len(value) == 64 and all(
|
|
312
|
+
character in "0123456789abcdef" for character in value
|
|
313
|
+
)
|