rlmness-cli 0.2.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 (203) hide show
  1. rlmagent/__init__.py +1 -0
  2. rlmagent/__main__.py +8 -0
  3. rlmagent/acp/__init__.py +1 -0
  4. rlmagent/acp/runtime.py +71 -0
  5. rlmagent/acp/server.py +598 -0
  6. rlmagent/acp/updates.py +128 -0
  7. rlmagent/agentnet.py +237 -0
  8. rlmagent/agents/__init__.py +1 -0
  9. rlmagent/agents/approval.py +90 -0
  10. rlmagent/agents/bridge.py +109 -0
  11. rlmagent/agents/budget.py +212 -0
  12. rlmagent/agents/child_handles.py +46 -0
  13. rlmagent/agents/child_mailbox.py +30 -0
  14. rlmagent/agents/kernel_api.py +156 -0
  15. rlmagent/agents/output.py +34 -0
  16. rlmagent/agents/patch.py +107 -0
  17. rlmagent/agents/prompts.py +156 -0
  18. rlmagent/agents/trace.py +290 -0
  19. rlmagent/agents/tree.py +682 -0
  20. rlmagent/agents/wasm_api.py +105 -0
  21. rlmagent/agents/workspace.py +357 -0
  22. rlmagent/autopilot.py +447 -0
  23. rlmagent/backend.py +946 -0
  24. rlmagent/backend_context.py +74 -0
  25. rlmagent/cli/__init__.py +5 -0
  26. rlmagent/cli/config_cmd.py +177 -0
  27. rlmagent/cli/health.py +156 -0
  28. rlmagent/cli/main.py +1194 -0
  29. rlmagent/cli/update.py +188 -0
  30. rlmagent/clipboard.py +63 -0
  31. rlmagent/config/__init__.py +37 -0
  32. rlmagent/config/config.py +297 -0
  33. rlmagent/config/loader.py +119 -0
  34. rlmagent/config/prompts.py +124 -0
  35. rlmagent/config/secure_input.py +213 -0
  36. rlmagent/config/store.py +275 -0
  37. rlmagent/context/__init__.py +47 -0
  38. rlmagent/context/budget.py +557 -0
  39. rlmagent/contract.py +311 -0
  40. rlmagent/contract_run.py +232 -0
  41. rlmagent/conversation.py +2547 -0
  42. rlmagent/cron.py +77 -0
  43. rlmagent/directives.py +893 -0
  44. rlmagent/discovery.py +356 -0
  45. rlmagent/execution_log.py +66 -0
  46. rlmagent/factory.py +189 -0
  47. rlmagent/factory_graph.py +413 -0
  48. rlmagent/harness/__init__.py +63 -0
  49. rlmagent/harness/contracts/__init__.py +1 -0
  50. rlmagent/harness/contracts/stream.py +129 -0
  51. rlmagent/harness/contracts/tooling.py +127 -0
  52. rlmagent/harness/contracts/transcript/__init__.py +68 -0
  53. rlmagent/harness/contracts/transcript/_base.py +29 -0
  54. rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
  55. rlmagent/harness/contracts/transcript/entries.py +180 -0
  56. rlmagent/harness/contracts/transcript/segments.py +64 -0
  57. rlmagent/harness/contracts/values.py +6 -0
  58. rlmagent/harness/driver.py +397 -0
  59. rlmagent/harness/engine.py +489 -0
  60. rlmagent/harness/mending.py +153 -0
  61. rlmagent/harness/provider/__init__.py +1 -0
  62. rlmagent/harness/provider/base.py +27 -0
  63. rlmagent/harness/provider/wire.py +189 -0
  64. rlmagent/harness/py.typed +0 -0
  65. rlmagent/harness/session/__init__.py +5 -0
  66. rlmagent/harness/session/index.py +218 -0
  67. rlmagent/harness/session/lease.py +66 -0
  68. rlmagent/harness/session/records.py +118 -0
  69. rlmagent/harness/session/replay.py +165 -0
  70. rlmagent/harness/session/store.py +155 -0
  71. rlmagent/harness/session/summary.py +46 -0
  72. rlmagent/hints.py +47 -0
  73. rlmagent/history.py +92 -0
  74. rlmagent/hooks/__init__.py +1 -0
  75. rlmagent/hooks/hooks.py +1 -0
  76. rlmagent/instructions.py +157 -0
  77. rlmagent/job_runner.py +117 -0
  78. rlmagent/jobs.py +365 -0
  79. rlmagent/kept_agents.py +397 -0
  80. rlmagent/kernel/__init__.py +5 -0
  81. rlmagent/kernel/session.py +205 -0
  82. rlmagent/kernel/wasm.py +169 -0
  83. rlmagent/logs.py +208 -0
  84. rlmagent/managed_release.py +368 -0
  85. rlmagent/mcp.py +411 -0
  86. rlmagent/mcp_oauth.py +213 -0
  87. rlmagent/model/__init__.py +29 -0
  88. rlmagent/model/_claude/__init__.py +1 -0
  89. rlmagent/model/_claude/courier.py +117 -0
  90. rlmagent/model/_claude/emitter.py +378 -0
  91. rlmagent/model/_oai/__init__.py +1 -0
  92. rlmagent/model/_oai/helpers.py +256 -0
  93. rlmagent/model/_oai/normalize.py +263 -0
  94. rlmagent/model/_oai/parsers.py +317 -0
  95. rlmagent/model/_oai/payloads.py +226 -0
  96. rlmagent/model/_oai/transport.py +127 -0
  97. rlmagent/model/claude.py +552 -0
  98. rlmagent/model/copilot.py +142 -0
  99. rlmagent/model/correlation.py +21 -0
  100. rlmagent/model/deadline.py +19 -0
  101. rlmagent/model/discovery.py +181 -0
  102. rlmagent/model/external/__init__.py +11 -0
  103. rlmagent/model/external/acp.py +202 -0
  104. rlmagent/model/external/acp_install.py +109 -0
  105. rlmagent/model/external/cli.py +248 -0
  106. rlmagent/model/external/textcalls.py +57 -0
  107. rlmagent/model/failover.py +129 -0
  108. rlmagent/model/google.py +199 -0
  109. rlmagent/model/limits.py +46 -0
  110. rlmagent/model/oai_compatible.py +414 -0
  111. rlmagent/model/preferences.py +56 -0
  112. rlmagent/model/py.typed +0 -0
  113. rlmagent/model/registered_oauth.py +195 -0
  114. rlmagent/model/scripted.py +55 -0
  115. rlmagent/model/settings.py +271 -0
  116. rlmagent/model/subscription.py +398 -0
  117. rlmagent/model/token_bounds.py +15 -0
  118. rlmagent/model/transport/__init__.py +1 -0
  119. rlmagent/model/transport/backoff.py +87 -0
  120. rlmagent/model/transport/client.py +87 -0
  121. rlmagent/model/transport/faults.py +65 -0
  122. rlmagent/model/transport/responses_socket.py +223 -0
  123. rlmagent/model/vertex.py +93 -0
  124. rlmagent/network.py +123 -0
  125. rlmagent/packages.py +246 -0
  126. rlmagent/planning.py +84 -0
  127. rlmagent/playbook/__init__.py +1 -0
  128. rlmagent/playbook/apply.py +106 -0
  129. rlmagent/playbook/digest.py +63 -0
  130. rlmagent/playbook/improver.py +272 -0
  131. rlmagent/playbook/plan.py +99 -0
  132. rlmagent/playbook/store.py +67 -0
  133. rlmagent/plugins.py +327 -0
  134. rlmagent/policy.py +105 -0
  135. rlmagent/prompt_refs.py +57 -0
  136. rlmagent/prompts.py +331 -0
  137. rlmagent/prune.py +60 -0
  138. rlmagent/py.typed +0 -0
  139. rlmagent/reasoning.py +135 -0
  140. rlmagent/refinement/__init__.py +1 -0
  141. rlmagent/refinement/controller.py +448 -0
  142. rlmagent/refinement/engine.py +220 -0
  143. rlmagent/refinement/evidence.py +131 -0
  144. rlmagent/refinement/scoped.py +90 -0
  145. rlmagent/refinement/store.py +193 -0
  146. rlmagent/refresh.py +202 -0
  147. rlmagent/routing.py +52 -0
  148. rlmagent/runtime.py +575 -0
  149. rlmagent/safety.py +267 -0
  150. rlmagent/session_html.py +57 -0
  151. rlmagent/session_import.py +52 -0
  152. rlmagent/session_share.py +55 -0
  153. rlmagent/sessions.py +249 -0
  154. rlmagent/shell_sandbox.py +88 -0
  155. rlmagent/skillset.py +320 -0
  156. rlmagent/steering.py +148 -0
  157. rlmagent/stops.py +62 -0
  158. rlmagent/subscriptions.py +219 -0
  159. rlmagent/tasks.py +98 -0
  160. rlmagent/toolhost.py +310 -0
  161. rlmagent/tools/__init__.py +56 -0
  162. rlmagent/tools/_shared.py +210 -0
  163. rlmagent/tools/files.py +462 -0
  164. rlmagent/tools/kernel.py +93 -0
  165. rlmagent/tools/patch_tool.py +68 -0
  166. rlmagent/tools/search.py +188 -0
  167. rlmagent/trust.py +383 -0
  168. rlmagent/tui/__init__.py +1 -0
  169. rlmagent/tui/adapter.py +506 -0
  170. rlmagent/tui/app.py +711 -0
  171. rlmagent/tui/approval_screen.py +57 -0
  172. rlmagent/tui/choice_screen.py +47 -0
  173. rlmagent/tui/completions.py +116 -0
  174. rlmagent/tui/components.py +25 -0
  175. rlmagent/tui/composer.py +40 -0
  176. rlmagent/tui/factory_view.py +86 -0
  177. rlmagent/tui/inspect.py +193 -0
  178. rlmagent/tui/messages.py +215 -0
  179. rlmagent/tui/model_screen.py +307 -0
  180. rlmagent/tui/prompt.py +98 -0
  181. rlmagent/tui/question_screen.py +52 -0
  182. rlmagent/tui/replay.py +141 -0
  183. rlmagent/tui/rlm_agent.tcss +405 -0
  184. rlmagent/tui/search_box.py +91 -0
  185. rlmagent/tui/sidebar.py +182 -0
  186. rlmagent/tui/status.py +49 -0
  187. rlmagent/tui/steps.py +283 -0
  188. rlmagent/tui/suggest.py +778 -0
  189. rlmagent/tui/transcript.py +172 -0
  190. rlmagent/tui/trust_dialog.py +58 -0
  191. rlmagent/tui/worker_attach.py +141 -0
  192. rlmagent/ui/__init__.py +1 -0
  193. rlmagent/ui/render.py +211 -0
  194. rlmagent/undo.py +109 -0
  195. rlmagent/utils/__init__.py +1 -0
  196. rlmagent/websearch.py +75 -0
  197. rlmagent/worker.py +1275 -0
  198. rlmagent/worker_service.py +203 -0
  199. rlmness_cli-0.2.0.dist-info/METADATA +882 -0
  200. rlmness_cli-0.2.0.dist-info/RECORD +203 -0
  201. rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
  202. rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
  203. rlmness_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
rlmagent/trust.py ADDED
@@ -0,0 +1,383 @@
1
+ """Project trust: whether a folder's own instructions and resources may load."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from collections.abc import Awaitable, Callable
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Literal
13
+
14
+ Verdict = Literal["trusted", "untrusted"]
15
+ DefaultPolicy = Literal["ask", "always", "never"]
16
+
17
+ _STORE_NAME = "trust.json"
18
+ _STORE_VERSION = 1
19
+ _VERDICTS: frozenset[str] = frozenset({"trusted", "untrusted"})
20
+
21
+
22
+ class TrustStoreError(Exception):
23
+ """The saved-decision file is unreadable or not in the expected shape."""
24
+
25
+
26
+ class StartupCancelled(Exception):
27
+ """The user dismissed the trust question instead of answering it."""
28
+
29
+
30
+ class Choice(Enum):
31
+ """Answers a frontend may give to a trust question."""
32
+
33
+ TRUST_FOLDER = "trust-folder"
34
+ TRUST_PARENT = "trust-parent"
35
+ TRUST_ONCE = "trust-once"
36
+ DISTRUST_FOLDER = "distrust-folder"
37
+ DISTRUST_ONCE = "distrust-once"
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Paths
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ def canonical_folder(cwd: str | os.PathLike[str]) -> Path:
46
+ """Absolute, symlink-free form of an existing directory."""
47
+ folder = Path(cwd).expanduser().resolve(strict=True)
48
+ if not folder.is_dir():
49
+ raise NotADirectoryError(str(folder))
50
+ return folder
51
+
52
+
53
+ def _key(folder: Path) -> str:
54
+ # Case-insensitive filesystems compare paths case-insensitively.
55
+ return os.path.normcase(str(folder))
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Detection (names and file types only; contents are never read)
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class ProjectInputs:
65
+ """Protected inputs present in a folder, counted by kind."""
66
+
67
+ folder: Path
68
+ counts: dict[str, int] = field(default_factory=dict)
69
+
70
+ @property
71
+ def empty(self) -> bool:
72
+ return not any(self.counts.values())
73
+
74
+ def describe(self) -> str:
75
+ """Short human summary, e.g. ``2 instruction files, 1 skill``."""
76
+ parts = [
77
+ f"{n} {kind if n == 1 else kind + 's'}"
78
+ for kind, n in self.counts.items() if n
79
+ ]
80
+ return ", ".join(parts) or "nothing"
81
+
82
+
83
+ def _entries(directory: Path) -> list[Path]:
84
+ try:
85
+ return list(directory.iterdir())
86
+ except OSError:
87
+ return []
88
+
89
+
90
+ def scan_project_inputs(folder: Path) -> ProjectInputs:
91
+ """Count protected inputs by name and type alone."""
92
+ counts = {
93
+ "instruction file": sum(
94
+ 1 for p in (folder / "AGENTS.md", folder / ".rlm-agent" / "AGENTS.md",
95
+ folder / ".agents" / "AGENTS.md")
96
+ if p.is_symlink() or p.exists()
97
+ ),
98
+ "skill": sum(
99
+ 1
100
+ for base in (folder / ".rlm-agent" / "skills", folder / ".agents" / "skills")
101
+ for entry in _entries(base)
102
+ if (entry / "SKILL.md").is_symlink() or (entry / "SKILL.md").exists()
103
+ ),
104
+ "prompt template": sum(
105
+ 1
106
+ for base in (folder / ".rlm-agent" / "prompts", folder / ".agents" / "prompts")
107
+ for entry in _entries(base)
108
+ if entry.suffix == ".md" and not entry.name.startswith(".")
109
+ ),
110
+ "theme": sum(1 for e in _entries(folder / ".rlm-agent" / "themes") if e.suffix == ".json"),
111
+ "settings file": int((folder / ".rlm-agent" / "settings.toml").is_file()),
112
+ "plugin": sum(
113
+ 1
114
+ for entry in _entries(folder / ".rlm-agent" / "extensions")
115
+ if not entry.name.startswith(("_", "."))
116
+ and (entry.suffix == ".py" or (entry / "extension.py").exists())
117
+ ),
118
+ }
119
+ return ProjectInputs(folder=folder, counts=counts)
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Saved decisions
124
+ # ---------------------------------------------------------------------------
125
+
126
+
127
+ class TrustStore:
128
+ """Saved per-folder decisions in ``<rlm-agent home>/trust.json``."""
129
+
130
+ def __init__(self, path: Path) -> None:
131
+ self.path = path
132
+
133
+ @classmethod
134
+ def default(cls) -> TrustStore:
135
+ from rlmagent.discovery import default_paths
136
+
137
+ return cls(default_paths().home / _STORE_NAME)
138
+
139
+ def decisions(self) -> dict[Path, Verdict]:
140
+ if not self.path.exists():
141
+ return {}
142
+ try:
143
+ data = json.loads(self.path.read_text(encoding="utf-8"))
144
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
145
+ raise TrustStoreError(f"{self.path} is unreadable: {exc}") from exc
146
+ if not isinstance(data, dict) or set(data) != {"version", "decisions"}:
147
+ raise TrustStoreError(f"{self.path} does not have the expected fields")
148
+ if data["version"] != _STORE_VERSION:
149
+ raise TrustStoreError(f"{self.path} has unsupported version {data['version']!r}")
150
+ if not isinstance(data["decisions"], list):
151
+ raise TrustStoreError(f"{self.path}: 'decisions' must be a list")
152
+ found: dict[Path, Verdict] = {}
153
+ seen: set[str] = set()
154
+ for item in data["decisions"]:
155
+ if not isinstance(item, dict) or set(item) != {"path", "decision"}:
156
+ raise TrustStoreError(f"{self.path}: malformed decision {item!r}")
157
+ raw, verdict = item["path"], item["decision"]
158
+ if not isinstance(raw, str) or not os.path.isabs(raw):
159
+ raise TrustStoreError(f"{self.path}: path must be absolute: {raw!r}")
160
+ if os.path.normpath(raw) != raw:
161
+ raise TrustStoreError(f"{self.path}: path is not normalized: {raw!r}")
162
+ if verdict not in _VERDICTS:
163
+ raise TrustStoreError(f"{self.path}: unknown decision {verdict!r}")
164
+ if _key(Path(raw)) in seen:
165
+ raise TrustStoreError(f"{self.path}: duplicate path {raw!r}")
166
+ seen.add(_key(Path(raw)))
167
+ found[Path(raw)] = verdict
168
+ return found
169
+
170
+ def nearest(self, folder: Path) -> tuple[Path, Verdict] | None:
171
+ """The saved decision for ``folder`` or its closest ancestor."""
172
+ saved = {_key(p): (p, v) for p, v in self.decisions().items()}
173
+ for candidate in (folder, *folder.parents):
174
+ hit = saved.get(_key(candidate))
175
+ if hit is not None:
176
+ return hit
177
+ return None
178
+
179
+ def save(self, folder: Path, verdict: Verdict, *, replacing: Path | None = None) -> None:
180
+ """Record a decision atomically, optionally dropping one other entry."""
181
+ current = self.decisions() # refuses to build on an unusable file
182
+ updated = {_key(p): (p, v) for p, v in current.items()}
183
+ if replacing is not None:
184
+ updated.pop(_key(replacing), None)
185
+ updated[_key(folder)] = (folder, verdict)
186
+ body = {
187
+ "version": _STORE_VERSION,
188
+ "decisions": [
189
+ {"path": str(p), "decision": v}
190
+ for p, v in sorted(updated.values(), key=lambda pair: str(pair[0]))
191
+ ],
192
+ }
193
+ self.path.parent.mkdir(parents=True, exist_ok=True)
194
+ fd, staged = tempfile.mkstemp(dir=self.path.parent, prefix=".trust-", suffix=".json")
195
+ try:
196
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
197
+ json.dump(body, handle, indent=2)
198
+ handle.write("\n")
199
+ handle.flush()
200
+ os.fsync(handle.fileno())
201
+ try:
202
+ os.chmod(staged, 0o600)
203
+ except OSError:
204
+ pass
205
+ os.replace(staged, self.path)
206
+ except BaseException:
207
+ Path(staged).unlink(missing_ok=True)
208
+ raise
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Resolution
213
+ # ---------------------------------------------------------------------------
214
+
215
+
216
+ @dataclass(frozen=True, slots=True)
217
+ class TrustRequest:
218
+ """What a frontend shows when asking: the folder and what it would load."""
219
+
220
+ folder: Path
221
+ inputs: ProjectInputs
222
+ store_problem: str | None = None
223
+
224
+ @property
225
+ def parent(self) -> Path | None:
226
+ parent = self.folder.parent
227
+ return None if parent == self.folder else parent
228
+
229
+
230
+ @dataclass(frozen=True, slots=True)
231
+ class TrustOutcome:
232
+ """The resolved decision for one folder in this run."""
233
+
234
+ folder: Path | None
235
+ trusted: bool
236
+ reason: str
237
+ inputs: ProjectInputs | None = None
238
+ notice: str | None = None
239
+
240
+
241
+ Asker = Callable[[TrustRequest], Awaitable[Choice | None]]
242
+
243
+ # Completed resolutions for this process, keyed by canonical folder.
244
+ _resolved: dict[str, TrustOutcome] = {}
245
+ _engaged = False
246
+
247
+
248
+ def reset_trust_cache() -> None:
249
+ """Forget every resolution made in this process (tests, reload)."""
250
+ global _engaged
251
+ _resolved.clear()
252
+ _engaged = False
253
+
254
+
255
+ async def resolve_project_trust(
256
+ cwd: str | os.PathLike[str],
257
+ *,
258
+ override: bool | None = None,
259
+ default: DefaultPolicy = "ask",
260
+ ask: Asker | None = None,
261
+ store: TrustStore | None = None,
262
+ ) -> TrustOutcome:
263
+ """Decide whether ``cwd``'s project inputs may load; see module docs."""
264
+ global _engaged
265
+ _engaged = True
266
+ try:
267
+ folder = canonical_folder(cwd)
268
+ except OSError as exc:
269
+ return TrustOutcome(None, False, f"working folder could not be resolved ({exc})")
270
+
271
+ cached = _resolved.get(_key(folder))
272
+ if cached is not None:
273
+ return cached
274
+
275
+ inputs = scan_project_inputs(folder)
276
+ if override is not None:
277
+ flag = "--approve" if override else "--no-approve"
278
+ outcome = TrustOutcome(folder, override, f"{flag} for this run", inputs)
279
+ elif inputs.empty:
280
+ # Nothing to protect. Not cached: files added later must still ask.
281
+ return TrustOutcome(folder, True, "no project inputs", inputs)
282
+ else:
283
+ outcome = await _decide(folder, inputs, default, ask, store or TrustStore.default())
284
+
285
+ _resolved[_key(folder)] = outcome
286
+ return outcome
287
+
288
+
289
+ async def _decide(
290
+ folder: Path,
291
+ inputs: ProjectInputs,
292
+ default: DefaultPolicy,
293
+ ask: Asker | None,
294
+ store: TrustStore,
295
+ ) -> TrustOutcome:
296
+ problem: str | None = None
297
+ try:
298
+ saved = store.nearest(folder)
299
+ except TrustStoreError as exc:
300
+ saved, problem = None, str(exc)
301
+
302
+ if saved is not None:
303
+ where, verdict = saved
304
+ scope = "this folder" if where == folder else f"parent {where}"
305
+ return TrustOutcome(folder, verdict == "trusted", f"saved decision for {scope}", inputs)
306
+ # An unusable store can grant nothing implicitly; only a run-only
307
+ # answer or --approve can load project inputs until it is fixed.
308
+ if problem is None and default == "always":
309
+ return TrustOutcome(folder, True, "user default: always trust", inputs)
310
+ if problem is None and default == "never":
311
+ return TrustOutcome(folder, False, "user default: never trust", inputs)
312
+ if ask is None:
313
+ reason = problem or "no saved decision and no way to ask"
314
+ return TrustOutcome(folder, False, reason, inputs)
315
+
316
+ choice = await ask(TrustRequest(folder, inputs, problem))
317
+ if choice is None:
318
+ raise StartupCancelled(f"No trust decision for {folder}")
319
+ return _apply(choice, folder, inputs, store)
320
+
321
+
322
+ def _apply(choice: Choice, folder: Path, inputs: ProjectInputs, store: TrustStore) -> TrustOutcome:
323
+ if choice is Choice.TRUST_ONCE:
324
+ return TrustOutcome(folder, True, "trusted for this run", inputs)
325
+ if choice is Choice.DISTRUST_ONCE:
326
+ return TrustOutcome(folder, False, "not trusted for this run", inputs)
327
+
328
+ target, verdict, replacing = folder, "trusted", None
329
+ if choice is Choice.TRUST_PARENT and folder.parent != folder:
330
+ target, replacing = folder.parent, folder
331
+ elif choice is Choice.DISTRUST_FOLDER:
332
+ verdict = "untrusted"
333
+ try:
334
+ store.save(target, verdict, replacing=replacing) # type: ignore[arg-type]
335
+ except (OSError, TrustStoreError) as exc:
336
+ # A remembered grant only counts once it is actually remembered.
337
+ notice = f"Could not save trust decision ({exc}); project inputs stay off."
338
+ return TrustOutcome(folder, False, "decision not saved", inputs, notice)
339
+ trusted = verdict == "trusted"
340
+ scope = "this folder" if target == folder else f"parent {target}"
341
+ return TrustOutcome(folder, trusted, f"saved: {verdict} ({scope})", inputs)
342
+
343
+
344
+ def project_inputs_allowed(project: str | os.PathLike[str]) -> bool:
345
+ """Whether loaders may read ``project``'s own inputs right now."""
346
+ if not _engaged:
347
+ return True
348
+ try:
349
+ folder = canonical_folder(project)
350
+ except OSError:
351
+ return False
352
+ outcome = _resolved.get(_key(folder))
353
+ if outcome is not None:
354
+ return outcome.trusted
355
+ return scan_project_inputs(folder).empty
356
+
357
+
358
+ def project_settings_allowed(project: str | os.PathLike[str]) -> bool:
359
+ """Whether a project's settings file may be applied.
360
+
361
+ Stricter than :func:`project_inputs_allowed`: before the trust decision
362
+ has been made for this process, the answer is no. Settings choose the
363
+ provider and model, so they wait for the folder to be trusted.
364
+ """
365
+ return _engaged and project_inputs_allowed(project)
366
+
367
+
368
+ __all__ = [
369
+ "Choice",
370
+ "project_settings_allowed",
371
+ "DefaultPolicy",
372
+ "ProjectInputs",
373
+ "StartupCancelled",
374
+ "TrustOutcome",
375
+ "TrustRequest",
376
+ "TrustStore",
377
+ "TrustStoreError",
378
+ "canonical_folder",
379
+ "project_inputs_allowed",
380
+ "reset_trust_cache",
381
+ "resolve_project_trust",
382
+ "scan_project_inputs",
383
+ ]
@@ -0,0 +1 @@
1
+ """Textual TUI, consumed strictly behind an adapter boundary"""