zjcode 0.0.1__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 (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,545 @@
1
+ """User-level credential storage for model providers.
2
+
3
+ Persists API keys (and, in the future, OAuth tokens) under
4
+ `~/.deepagents/.state/auth.json` (file mode 0600, parent 0700) so users can
5
+ enter credentials directly in the TUI rather than exporting environment
6
+ variables before launch.
7
+
8
+ Security notes:
9
+
10
+ - The stored value (`ApiKeyCredential.key`) must never be logged, formatted
11
+ via `%r`/`!r`, or interpolated into exception messages — every helper here
12
+ reports only structural facts ("set credential for provider X").
13
+ - The file is written via `O_EXCL | 0o600` to a temp path, then atomically
14
+ replaced. A second `chmod 0600` runs on the final path so filesystems that
15
+ ignore the create-mode argument still end up with private perms. Permission
16
+ failures are reported back to the caller in `WriteOutcome.warnings` so the
17
+ UI can surface them to the user — `logger.warning` alone is invisible
18
+ inside a Textual TUI session.
19
+ - On Windows, POSIX mode bits don't apply; the chmod calls are best-effort
20
+ and skipped silently.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import contextlib
26
+ import json
27
+ import logging
28
+ import os
29
+ import stat
30
+ from dataclasses import dataclass, field
31
+ from datetime import UTC, datetime
32
+ from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict
33
+
34
+ if TYPE_CHECKING:
35
+ from pathlib import Path
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ _STORAGE_VERSION = 1
40
+ """Schema version stamped into `auth.json`; bump on incompatible shape changes."""
41
+
42
+
43
+ class ApiKeyCredential(TypedDict):
44
+ """A persisted API key credential.
45
+
46
+ The `type` field is the discriminator that lets `OAuthCredential` (added
47
+ later) coexist in the same file without migration.
48
+ """
49
+
50
+ type: Literal["api_key"]
51
+ """Credential kind discriminator."""
52
+
53
+ key: str
54
+ """The API key value as entered by the user. Never log this field."""
55
+
56
+ added_at: str
57
+ """ISO-8601 UTC timestamp recording when the credential was stored."""
58
+
59
+ base_url: NotRequired[str]
60
+ """Optional provider endpoint paired with this key.
61
+
62
+ Stored only when the user supplied one in `/auth`. A key and its endpoint
63
+ form a coherent pair — applying the key also applies (or, when this is
64
+ absent, resets to the provider default) the base URL, so a personal key is
65
+ never sent to a gateway it doesn't belong to. Not treated as a secret (it is
66
+ logged when malformed and surfaced in hints); avoid embedding credentials in
67
+ the URL.
68
+ """
69
+
70
+ project: NotRequired[str]
71
+ """Optional LangSmith project name paired with this credential.
72
+
73
+ Set only for the `langsmith` tracing service when the user supplies a
74
+ custom project in `/auth`; absent means traces fall back to the default
75
+ (`deepagents-code`). Not a secret — it is shown in the `/auth` advanced
76
+ panel and applied to `LANGSMITH_PROJECT` at startup.
77
+ """
78
+
79
+
80
+ class OAuthCredential(TypedDict):
81
+ """A persisted OAuth subscription credential.
82
+
83
+ Stub kept here so the `StoredCredential` discriminated union narrows
84
+ correctly today and the OAuth implementation lands as a pure addition.
85
+ No code path produces or consumes this shape yet.
86
+ """
87
+
88
+ type: Literal["oauth"]
89
+ """Credential kind discriminator."""
90
+
91
+ access_token: str
92
+ """OAuth access token. Never log."""
93
+
94
+ refresh_token: str
95
+ """OAuth refresh token. Never log."""
96
+
97
+ expires_at: str
98
+ """ISO-8601 UTC expiry timestamp."""
99
+
100
+
101
+ StoredCredential = ApiKeyCredential | OAuthCredential
102
+ """Tagged union of every persisted credential shape, narrowed by `type`."""
103
+
104
+
105
+ @dataclass(frozen=True, slots=True)
106
+ class WriteOutcome:
107
+ """Result of a credential write that may have warnings to surface."""
108
+
109
+ warnings: tuple[str, ...] = field(default_factory=tuple)
110
+ """User-visible warning strings (e.g., chmod failures). Empty on success."""
111
+
112
+
113
+ @dataclass(frozen=True, slots=True)
114
+ class DeleteOutcome:
115
+ """Result of a credential delete that may have warnings to surface.
116
+
117
+ A delete rewrites the whole store, so it can hit the same chmod failures as
118
+ a write. `warnings` carries them symmetrically with `WriteOutcome` so a
119
+ caller's "removed" confirmation doesn't paper over a store the delete
120
+ failed to lock down to owner-only.
121
+ """
122
+
123
+ removed: bool
124
+ """`True` if a credential was removed, `False` if none was stored."""
125
+
126
+ warnings: tuple[str, ...] = field(default_factory=tuple)
127
+ """chmod-failure warnings from the rewrite. Always empty for a no-op delete
128
+ (`removed=False`), which performs no write."""
129
+
130
+ def __post_init__(self) -> None:
131
+ """Reject the one field combination the docstring forbids.
132
+
133
+ A no-op delete performs no write, so it can raise no chmod warnings.
134
+ Enforcing it here makes "no warnings when nothing was removed" a
135
+ construction-time guarantee rather than a producer-side convention a
136
+ future edit (or a second producer) could quietly break.
137
+
138
+ Raises:
139
+ ValueError: If `warnings` is non-empty while `removed` is `False`.
140
+ """
141
+ if self.warnings and not self.removed:
142
+ msg = "DeleteOutcome cannot carry warnings when removed=False"
143
+ raise ValueError(msg)
144
+
145
+
146
+ def auth_path() -> Path:
147
+ """Return the resolved path to the credential store (`auth.json`).
148
+
149
+ Resolved at call time (not import time) so tests can redirect storage by
150
+ monkeypatching `deepagents_code.model_config.DEFAULT_STATE_DIR` — same
151
+ pattern `mcp_auth._tokens_dir` uses.
152
+ """
153
+ from deepagents_code.model_config import DEFAULT_STATE_DIR
154
+
155
+ return DEFAULT_STATE_DIR / "auth.json"
156
+
157
+
158
+ def _read_raw() -> dict | None:
159
+ """Read and validate the on-disk auth file.
160
+
161
+ Returns:
162
+ The decoded JSON object, or `None` when the file is missing.
163
+
164
+ Raises:
165
+ RuntimeError: If the file exists but cannot be parsed or has an
166
+ unsupported schema version.
167
+ """
168
+ path = auth_path()
169
+ try:
170
+ raw = path.read_text(encoding="utf-8")
171
+ data = json.loads(raw)
172
+ except FileNotFoundError:
173
+ return None
174
+ except OSError as exc:
175
+ msg = (
176
+ f"Failed to read credential file {path}: {exc}. "
177
+ "Check the file permissions on the parent directory."
178
+ )
179
+ raise RuntimeError(msg) from exc
180
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
181
+ # `UnicodeDecodeError` (a `ValueError`, not an `OSError`) escapes the
182
+ # handler above when the file holds non-UTF-8 bytes; treat a decode
183
+ # failure as corruption so callers get the same `RuntimeError` hint
184
+ # instead of an unhandled traceback.
185
+ msg = (
186
+ f"Failed to parse credential file {path}: {exc}. "
187
+ "Delete the file and re-add credentials via /auth if it is corrupt."
188
+ )
189
+ raise RuntimeError(msg) from exc
190
+ if not isinstance(data, dict):
191
+ msg = (
192
+ f"Credential file {path} is not a JSON object. "
193
+ "Delete it and re-add credentials via /auth."
194
+ )
195
+ # `RuntimeError` (not `TypeError`) is intentional: every corruption
196
+ # path here surfaces the same error class so callers can render one
197
+ # remediation hint regardless of the specific shape problem.
198
+ raise RuntimeError(msg) # noqa: TRY004
199
+ version = data.get("version")
200
+ if version != _STORAGE_VERSION:
201
+ msg = (
202
+ f"Credential file {path} has unsupported version {version!r} "
203
+ f"(expected {_STORAGE_VERSION}). Delete it and re-add credentials via "
204
+ "/auth."
205
+ )
206
+ raise RuntimeError(msg)
207
+ return data
208
+
209
+
210
+ def _write_raw(data: dict) -> tuple[str, ...]:
211
+ """Atomically write `data` as the new auth file with 0600 perms.
212
+
213
+ Mirrors `mcp_auth.FileTokenStorage._write` so the security posture is
214
+ consistent across both stores. If you change this, update
215
+ `mcp_auth.FileTokenStorage._write` too — they share threat model.
216
+
217
+ Returns:
218
+ Tuple of warning strings for chmod failures the caller should
219
+ surface to the user. Empty when permissions were locked down
220
+ successfully (or on Windows where POSIX modes don't apply).
221
+ """
222
+ path = auth_path()
223
+ path.parent.mkdir(parents=True, exist_ok=True)
224
+ warnings: list[str] = []
225
+ if hasattr(os, "chmod"):
226
+ try:
227
+ path.parent.chmod(stat.S_IRWXU)
228
+ except OSError as exc:
229
+ warnings.append(
230
+ f"Could not set mode 0700 on {path.parent}: {exc}. "
231
+ "Stored API keys may be readable by other local users."
232
+ )
233
+ tmp = path.with_suffix(path.suffix + ".tmp")
234
+ payload = json.dumps(data, separators=(",", ":")).encode("utf-8")
235
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
236
+ if tmp.exists():
237
+ # A leftover `.tmp` from a prior crashed write is the only path
238
+ # `os.open(O_EXCL)` can fail without an actual conflict. Log so the
239
+ # operator knows a stale write was cleaned up — silent suppression
240
+ # masked recovery from a previous interrupted save.
241
+ logger.warning(
242
+ "Removing stale credential temp file %s left over from a prior write",
243
+ tmp,
244
+ )
245
+ with contextlib.suppress(OSError):
246
+ tmp.unlink()
247
+ fd = os.open(str(tmp), flags, 0o600)
248
+ try:
249
+ with os.fdopen(fd, "wb") as fh:
250
+ fh.write(payload)
251
+ except Exception:
252
+ with contextlib.suppress(OSError):
253
+ tmp.unlink()
254
+ raise
255
+ try:
256
+ tmp.replace(path)
257
+ except Exception:
258
+ with contextlib.suppress(OSError):
259
+ tmp.unlink()
260
+ raise
261
+ if hasattr(os, "chmod"):
262
+ try:
263
+ path.chmod(stat.S_IRUSR | stat.S_IWUSR)
264
+ except OSError as exc:
265
+ warnings.append(
266
+ f"Could not set mode 0600 on {path}: {exc}. "
267
+ "Stored API keys may be world-readable."
268
+ )
269
+ for warning in warnings:
270
+ logger.warning("%s", warning)
271
+ return tuple(warnings)
272
+
273
+
274
+ def _write_raw_or_raise(data: dict) -> tuple[str, ...]:
275
+ """Write `data` via `_write_raw`, converting write failures to `RuntimeError`.
276
+
277
+ `_write_raw` lets `OSError` from the atomic write (no disk space, an
278
+ unwritable state directory, a cross-device rename of the temp file)
279
+ propagate. The public writers document `RuntimeError` for unrecoverable
280
+ store failures, so translate here with a remediation hint instead of
281
+ leaking a raw traceback to the caller (CLI or TUI). The message never
282
+ includes the credential value.
283
+
284
+ Returns:
285
+ The chmod-warning tuple from `_write_raw` on success.
286
+
287
+ Raises:
288
+ RuntimeError: If the underlying write fails with an `OSError`.
289
+ """
290
+ try:
291
+ return _write_raw(data)
292
+ except OSError as exc:
293
+ msg = (
294
+ f"Failed to write credential file {auth_path()}: {exc}. "
295
+ "Check available disk space and the permissions on the parent "
296
+ "directory."
297
+ )
298
+ raise RuntimeError(msg) from exc
299
+
300
+
301
+ def load_credentials() -> dict[str, StoredCredential]:
302
+ """Return all stored credentials keyed by provider name.
303
+
304
+ Returns:
305
+ Mapping of provider name to its stored credential. Empty when no
306
+ credentials are persisted yet.
307
+
308
+ Raises:
309
+ RuntimeError: If the file exists but is corrupt or has an unsupported
310
+ schema version. Caller is expected to surface a remediation hint.
311
+ """ # noqa: DOC502 - re-raised from `_read_raw`
312
+ data = _read_raw()
313
+ if data is None:
314
+ return {}
315
+ creds_raw = data.get("credentials")
316
+ if not isinstance(creds_raw, dict):
317
+ return {}
318
+ result: dict[str, StoredCredential] = {}
319
+ for provider, entry in creds_raw.items():
320
+ coerced = _coerce_credential(entry)
321
+ if coerced is not None:
322
+ result[provider] = coerced
323
+ return result
324
+
325
+
326
+ def _coerce_credential(raw: Any) -> StoredCredential | None: # noqa: ANN401
327
+ # `raw: Any` because entries come from `json.loads`; the body is
328
+ # what enforces the `StoredCredential` contract.
329
+ """Validate one raw credential entry, returning `None` on shape mismatch.
330
+
331
+ Centralizes the runtime check against the `StoredCredential` union so
332
+ `load_credentials` doesn't repeat the per-field guard logic and so a
333
+ single helper can grow as new variants are added.
334
+
335
+ Returns:
336
+ The coerced `StoredCredential`, or `None` when the entry doesn't
337
+ match any known variant's shape.
338
+ """
339
+ if not isinstance(raw, dict):
340
+ return None
341
+ cred_type = raw.get("type")
342
+ if cred_type == "api_key":
343
+ key = raw.get("key")
344
+ if not isinstance(key, str) or not key:
345
+ return None
346
+ added_at = raw.get("added_at")
347
+ if not isinstance(added_at, str):
348
+ added_at = ""
349
+ credential = ApiKeyCredential(type="api_key", key=key, added_at=added_at)
350
+ base_url = raw.get("base_url")
351
+ if isinstance(base_url, str) and base_url:
352
+ credential["base_url"] = base_url
353
+ elif base_url is not None:
354
+ # Present but not a usable string (e.g. a hand-edit left an int or
355
+ # an empty value). Dropping it silently would pair the key with the
356
+ # provider default — possibly the wrong endpoint — with no trace, so
357
+ # log it. `base_url` is non-secret, so logging the value is safe.
358
+ logger.warning(
359
+ "Ignoring malformed base_url for a stored credential: %r", base_url
360
+ )
361
+ project = raw.get("project")
362
+ if isinstance(project, str) and project:
363
+ credential["project"] = project
364
+ elif project is not None:
365
+ # Same rationale as `base_url`: a hand-edited non-string value is
366
+ # dropped with a trace. `project` is non-secret, so logging is safe.
367
+ logger.warning(
368
+ "Ignoring malformed project for a stored credential: %r", project
369
+ )
370
+ return credential
371
+ # OAuth is reserved for a future PR — silently skip until the producer
372
+ # path lands. `cred_type in {"oauth"}` falls through to None here.
373
+ return None
374
+
375
+
376
+ def get_stored_key(provider: str) -> str | None:
377
+ """Return the stored API key for `provider`, or `None` if unset.
378
+
379
+ Returns `None` for stored OAuth credentials too — callers that need
380
+ OAuth tokens should read `load_credentials()` directly and narrow on
381
+ `type`.
382
+
383
+ Raises:
384
+ RuntimeError: If the credential file is corrupt.
385
+ """ # noqa: DOC502 - re-raised from `_read_raw` via `load_credentials`
386
+ creds = load_credentials()
387
+ entry = creds.get(provider)
388
+ if entry is None or entry["type"] != "api_key":
389
+ return None
390
+ return entry["key"] or None
391
+
392
+
393
+ def get_stored_base_url(provider: str) -> str | None:
394
+ """Return the base URL paired with `provider`'s stored key, or `None`.
395
+
396
+ Returns `None` both when no key is stored and when a key is stored without
397
+ an accompanying base URL (the user left the field blank, meaning "use the
398
+ provider default"). Callers distinguish the two via `get_stored_key`.
399
+
400
+ Raises:
401
+ RuntimeError: If the credential file is corrupt.
402
+ """ # noqa: DOC502 - re-raised from `_read_raw` via `load_credentials`
403
+ creds = load_credentials()
404
+ entry = creds.get(provider)
405
+ if entry is None or entry["type"] != "api_key":
406
+ return None
407
+ return entry.get("base_url") or None
408
+
409
+
410
+ def get_stored_project(provider: str) -> str | None:
411
+ """Return the LangSmith project paired with `provider`'s stored key, or `None`.
412
+
413
+ Returns `None` when no key is stored and when a key is stored without a
414
+ custom project (the user left the field blank, meaning "use the default").
415
+
416
+ Raises:
417
+ RuntimeError: If the credential file is corrupt.
418
+ """ # noqa: DOC502 - re-raised from `_read_raw` via `load_credentials`
419
+ creds = load_credentials()
420
+ entry = creds.get(provider)
421
+ if entry is None or entry["type"] != "api_key":
422
+ return None
423
+ return entry.get("project") or None
424
+
425
+
426
+ def set_stored_key(
427
+ provider: str,
428
+ key: str,
429
+ *,
430
+ base_url: str | None = None,
431
+ project: str | None = None,
432
+ ) -> WriteOutcome:
433
+ """Persist an API key for `provider`.
434
+
435
+ Empty / whitespace-only keys are rejected so callers don't accidentally
436
+ write a sentinel that masks a working environment variable (see
437
+ `apply_stored_credentials` in `model_config` — a stored empty would
438
+ unconditionally overwrite the env var).
439
+
440
+ This rewrites the whole credential record: `base_url` and `project` are
441
+ *not* merged with any previously stored values. Passing blank/`None` for
442
+ either clears it, so a caller rotating a key while wanting to keep the
443
+ existing endpoint/project must read it back (e.g. via `get_stored_base_url`
444
+ / `get_stored_project`) and pass it in again.
445
+
446
+ Args:
447
+ provider: Provider identifier (e.g., `"anthropic"`).
448
+ key: The API key value. Whitespace is stripped before storage.
449
+ base_url: Optional provider endpoint to pair with the key. Whitespace
450
+ is stripped; blank/`None` stores no endpoint, meaning the key uses
451
+ the provider default rather than any inherited (e.g. gateway) URL.
452
+ project: Optional LangSmith project name to pair with the key. Valid
453
+ only for the `langsmith` tracing service. Whitespace is stripped;
454
+ blank/`None` stores no project, meaning traces use the default
455
+ project.
456
+
457
+ Returns:
458
+ A `WriteOutcome` whose `warnings` tuple lists chmod failures the
459
+ caller should surface to the user. Empty on a clean save.
460
+
461
+ Raises:
462
+ ValueError: If `provider` or the stripped `key` is empty, or a non-empty
463
+ `project` is paired with a provider other than the `langsmith`
464
+ service.
465
+ RuntimeError: If the credential file is corrupt and cannot be read, or
466
+ the new file cannot be written (e.g. no disk space or an
467
+ unwritable state directory).
468
+ """ # noqa: DOC502 - `RuntimeError` re-raised from `_read_raw`/`_write_raw_or_raise`
469
+ if not provider:
470
+ msg = "Provider name cannot be empty"
471
+ raise ValueError(msg)
472
+ cleaned = key.strip()
473
+ if not cleaned:
474
+ msg = "API key cannot be empty"
475
+ raise ValueError(msg)
476
+ data = _read_raw() or {}
477
+ creds = data.get("credentials")
478
+ if not isinstance(creds, dict):
479
+ creds = {}
480
+ entry: dict[str, str] = {
481
+ "type": "api_key",
482
+ "key": cleaned,
483
+ "added_at": datetime.now(tz=UTC).isoformat(timespec="seconds"),
484
+ }
485
+ cleaned_base_url = base_url.strip() if base_url else ""
486
+ if cleaned_base_url:
487
+ entry["base_url"] = cleaned_base_url
488
+ cleaned_project = project.strip() if project else ""
489
+ if cleaned_project:
490
+ # A project name is meaningful only for the LangSmith tracing service;
491
+ # enforce the invariant at the write boundary so a stray project can
492
+ # never be persisted onto an unrelated provider, regardless of caller.
493
+ # Lazy import avoids a circular dependency (model_config imports this
494
+ # module), matching the pattern in `auth_path`.
495
+ from deepagents_code.model_config import is_langsmith
496
+
497
+ if not is_langsmith(provider):
498
+ msg = f"project is only valid for the langsmith service, not {provider!r}"
499
+ raise ValueError(msg)
500
+ entry["project"] = cleaned_project
501
+ creds[provider] = entry
502
+ data["version"] = _STORAGE_VERSION
503
+ data["credentials"] = creds
504
+ warnings = _write_raw_or_raise(data)
505
+ logger.debug("Stored credential for provider %s", provider)
506
+ return WriteOutcome(warnings=warnings)
507
+
508
+
509
+ def delete_stored_key(provider: str) -> DeleteOutcome:
510
+ """Remove a stored credential for `provider`.
511
+
512
+ Args:
513
+ provider: Provider identifier.
514
+
515
+ Returns:
516
+ A `DeleteOutcome` whose `removed` flag reports whether a credential was
517
+ present, and whose `warnings` tuple lists chmod failures from the
518
+ rewrite (empty for a no-op delete, which performs no write).
519
+
520
+ Raises:
521
+ RuntimeError: If the credential file is corrupt and cannot be read, or
522
+ the rewrite cannot be written (e.g. no disk space or an unwritable
523
+ state directory).
524
+ """ # noqa: DOC502 - re-raised from `_read_raw`/`_write_raw_or_raise`
525
+ data = _read_raw()
526
+ if data is None:
527
+ return DeleteOutcome(removed=False)
528
+ creds = data.get("credentials")
529
+ if not isinstance(creds, dict) or provider not in creds:
530
+ return DeleteOutcome(removed=False)
531
+ del creds[provider]
532
+ data["version"] = _STORAGE_VERSION
533
+ data["credentials"] = creds
534
+ warnings = _write_raw_or_raise(data)
535
+ logger.debug("Deleted credential for provider %s", provider)
536
+ return DeleteOutcome(removed=True, warnings=warnings)
537
+
538
+
539
+ def list_configured_providers() -> list[str]:
540
+ """Return providers that currently have a stored credential, sorted.
541
+
542
+ Raises:
543
+ RuntimeError: If the credential file is corrupt.
544
+ """ # noqa: DOC502 - re-raised from `_read_raw` via `load_credentials`
545
+ return sorted(load_credentials().keys())
@@ -0,0 +1,5 @@
1
+ """Built-in skills that ship with the Deep Agents Code.
2
+
3
+ These skills are always available at the lowest precedence level. User and
4
+ project skills with the same name will override them.
5
+ """
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: remember
3
+ description: "Review the current conversation and capture valuable knowledge — best practices, coding conventions, architecture decisions, workflows, and user feedback — into persistent memory (AGENTS.md) or reusable skills. Use when the user says: (1) remember this, (2) save what we learned, (3) update memory, (4) capture learnings."
4
+ license: MIT
5
+ compatibility: designed for deepagents-code
6
+ ---
7
+
8
+ Review our conversation and capture valuable knowledge. Focus especially on **best practices** we discussed or discovered—these are the most important things to preserve.
9
+
10
+ ## Step 1: Identify Best Practices and Key Learnings
11
+
12
+ Scan the conversation for:
13
+
14
+ ### Best Practices (highest priority)
15
+ - **Patterns that worked well** - approaches, techniques, or solutions we found effective
16
+ - **Anti-patterns to avoid** - mistakes, gotchas, or approaches that caused problems
17
+ - **Quality standards** - criteria we established for good code, documentation, or processes
18
+ - **Decision rationale** - why we chose one approach over another
19
+
20
+ ### Other Valuable Knowledge
21
+ - Coding conventions and style preferences
22
+ - Project architecture decisions
23
+ - Workflows and processes we developed
24
+ - Tools, libraries, or techniques worth remembering
25
+ - Feedback I gave about your behavior or outputs
26
+
27
+ ## Step 2: Decide Where to Store Each Learning
28
+
29
+ For each best practice or learning, choose the right destination:
30
+
31
+ ### -> Memory (AGENTS.md) for preferences and guidelines
32
+ Use memory when the knowledge is:
33
+ - A preference or guideline (not a multi-step process)
34
+ - Something to always keep in mind
35
+ - A simple rule or pattern
36
+
37
+ **Global** (`~/.deepagents/agent/AGENTS.md`): Universal preferences across all projects
38
+ **Project** (`.deepagents/AGENTS.md`): Project-specific conventions and decisions
39
+
40
+ ### -> Skill for reusable workflows and methodologies
41
+ **Create a skill when** we developed:
42
+ - A multi-step process worth reusing
43
+ - A methodology for a specific type of task
44
+ - A workflow with best practices baked in
45
+ - A procedure that should be followed consistently
46
+
47
+ Skills are more powerful than memory entries because they can encode **how** to do something well, not just **what** to remember.
48
+
49
+ ## Step 3: Create Skills for Significant Best Practices
50
+
51
+ If we established best practices around a workflow or process, capture them in a skill.
52
+
53
+ **Example:** If we discussed best practices for code review, create a `code-review` skill that encodes those practices into a reusable workflow.
54
+
55
+ ### Skill Location
56
+ `~/.deepagents/agent/skills/<skill-name>/SKILL.md`
57
+
58
+ ### Skill Structure
59
+ ```
60
+ skill-name/
61
+ ├── SKILL.md (required - main instructions with best practices)
62
+ ├── scripts/ (optional - executable code)
63
+ ├── references/ (optional - detailed documentation)
64
+ └── assets/ (optional - templates, examples)
65
+ ```
66
+
67
+ ### SKILL.md Format
68
+ ```markdown
69
+ ---
70
+ name: skill-name
71
+ description: "What this skill does AND when to use it. Include triggers like 'when the user asks to X' or 'when working with Y'. This description determines when the skill activates."
72
+ ---
73
+
74
+ # Skill Name
75
+
76
+ ## Overview
77
+ Brief explanation of what this skill accomplishes.
78
+
79
+ ## Best Practices
80
+ Capture the key best practices upfront:
81
+ - Best practice 1: explanation
82
+ - Best practice 2: explanation
83
+
84
+ ## Process
85
+ Step-by-step instructions (imperative form):
86
+ 1. First, do X
87
+ 2. Then, do Y
88
+ 3. Finally, do Z
89
+
90
+ ## Common Pitfalls
91
+ - Pitfall to avoid and why
92
+ - Another anti-pattern we discovered
93
+ ```
94
+
95
+ ### Key Principles
96
+ 1. **Encode best practices prominently** - Put them near the top so they guide the entire workflow
97
+ 2. **Concise is key** - Only include non-obvious knowledge. Every paragraph should justify its token cost.
98
+ 3. **Clear triggers** - The description determines when the skill activates. Be specific.
99
+ 4. **Imperative form** - Write as commands: "Create a file" not "You should create a file"
100
+ 5. **Include anti-patterns** - What NOT to do is often as valuable as what to do
101
+
102
+ ## Step 4: Update Memory for Simpler Learnings
103
+
104
+ For preferences, guidelines, and simple rules that don't warrant a full skill:
105
+
106
+ ```markdown
107
+ ## Best Practices
108
+ - When doing X, always Y because Z
109
+ - Avoid A because it leads to B
110
+ ```
111
+
112
+ Use `edit_file` to update existing files or `write_file` to create new ones.
113
+
114
+ ## Step 5: Summarize Changes
115
+
116
+ List what you captured and where you stored it:
117
+ - Skills created (with key best practices encoded)
118
+ - Memory entries added (with location)