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.
Files changed (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. 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
+ )