karox-runtime 5.0.0rc2__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 (209) hide show
  1. karox/__init__.py +6 -0
  2. karox/access_mode.py +248 -0
  3. karox/action_execution.py +231 -0
  4. karox/action_policy.py +610 -0
  5. karox/activity_stream.py +439 -0
  6. karox/affected_checks.py +962 -0
  7. karox/agent.py +2576 -0
  8. karox/agent_mcp.py +92 -0
  9. karox/agent_modes.py +168 -0
  10. karox/agent_protocol.py +347 -0
  11. karox/artifacts.py +306 -0
  12. karox/autonomy_benchmark.py +1012 -0
  13. karox/autonomy_runtime.py +3207 -0
  14. karox/background_orchestration.py +344 -0
  15. karox/background_worker.py +58 -0
  16. karox/bridge.py +465 -0
  17. karox/bridge_handoff.py +185 -0
  18. karox/browser_access.py +1892 -0
  19. karox/browser_approval.py +451 -0
  20. karox/browser_bootstrap.py +218 -0
  21. karox/browser_credential_injection.py +128 -0
  22. karox/browser_credentials.py +224 -0
  23. karox/browser_errors.py +144 -0
  24. karox/browser_extension/__init__.py +1 -0
  25. karox/browser_extension/content.js +509 -0
  26. karox/browser_extension/dom_helpers.js +442 -0
  27. karox/browser_extension/manifest.json +57 -0
  28. karox/browser_extension/newtab.css +1 -0
  29. karox/browser_extension/newtab.html +21 -0
  30. karox/browser_extension/newtab.js +1 -0
  31. karox/browser_extension/page_console_bridge.js +93 -0
  32. karox/browser_extension/service_worker.js +1426 -0
  33. karox/browser_extension/sidepanel.css +1 -0
  34. karox/browser_extension/sidepanel.html +27 -0
  35. karox/browser_extension/sidepanel.js +1 -0
  36. karox/browser_session.py +521 -0
  37. karox/build_identity.py +124 -0
  38. karox/cache_scheduler.py +263 -0
  39. karox/chatgpt_project_binding.py +307 -0
  40. karox/check_jobs.py +1523 -0
  41. karox/checkpoints.py +75 -0
  42. karox/cli.py +6897 -0
  43. karox/cli_shortcuts.py +128 -0
  44. karox/clickup_setup.py +1412 -0
  45. karox/client_capabilities.py +279 -0
  46. karox/clipboard.py +175 -0
  47. karox/command_guard.py +217 -0
  48. karox/connection_controller.py +581 -0
  49. karox/connection_runtime.py +556 -0
  50. karox/connection_status.py +413 -0
  51. karox/connection_tests.py +391 -0
  52. karox/connections.py +1456 -0
  53. karox/constitution.py +288 -0
  54. karox/context_ab.py +300 -0
  55. karox/context_bus.py +425 -0
  56. karox/context_compiler.py +375 -0
  57. karox/core.py +2603 -0
  58. karox/core_tools.py +908 -0
  59. karox/cost_intelligence.py +648 -0
  60. karox/credentials.py +273 -0
  61. karox/delegated_provider.py +252 -0
  62. karox/desktop_apps.py +1058 -0
  63. karox/detached_process.py +92 -0
  64. karox/disk_maintenance.py +1099 -0
  65. karox/economy_engine.py +306 -0
  66. karox/economy_eval.py +179 -0
  67. karox/ecosystem.py +268 -0
  68. karox/effort.py +442 -0
  69. karox/effort_signals.py +324 -0
  70. karox/ellipsis_api.py +459 -0
  71. karox/ellipsis_bridge_server.py +382 -0
  72. karox/ellipsis_checkpoint.py +367 -0
  73. karox/ellipsis_cli.py +409 -0
  74. karox/ellipsis_helpers.py +407 -0
  75. karox/ellipsis_runtime.py +810 -0
  76. karox/ellipsis_watchdog.py +115 -0
  77. karox/entrypoint.py +70 -0
  78. karox/event_bus.py +343 -0
  79. karox/evidence_packets.py +443 -0
  80. karox/extension_browser.py +1586 -0
  81. karox/file_mentions.py +108 -0
  82. karox/handoff.py +208 -0
  83. karox/hosted_bridge.py +1328 -0
  84. karox/hosted_project_orchestration.py +139 -0
  85. karox/hosted_tools_runtime.py +3080 -0
  86. karox/hot_worker.py +341 -0
  87. karox/image_attachments.py +109 -0
  88. karox/intelligence_pool.py +508 -0
  89. karox/launch_support.py +219 -0
  90. karox/lsp_diagnostics.py +362 -0
  91. karox/managed_browser.py +661 -0
  92. karox/managed_process_cleanup.py +63 -0
  93. karox/map_service.py +798 -0
  94. karox/mcp_client.py +1171 -0
  95. karox/mcp_oauth.py +268 -0
  96. karox/mcp_status.py +302 -0
  97. karox/memory.py +672 -0
  98. karox/migration.py +245 -0
  99. karox/mission_control.py +462 -0
  100. karox/mission_control_server.py +320 -0
  101. karox/mission_control_worker.py +112 -0
  102. karox/mobile_mission.py +309 -0
  103. karox/mode_artifacts.py +141 -0
  104. karox/model_auto.py +124 -0
  105. karox/model_delegation_broker.py +213 -0
  106. karox/model_grant_requests.py +496 -0
  107. karox/model_grants.py +1084 -0
  108. karox/model_hot_actions.py +467 -0
  109. karox/model_micro_benchmark.py +157 -0
  110. karox/model_quality_benchmark.py +316 -0
  111. karox/models.py +251 -0
  112. karox/notion_mcp.py +60 -0
  113. karox/oauth_bridge.py +1465 -0
  114. karox/openapi_bridge.py +392 -0
  115. karox/openrouter_delegation.py +472 -0
  116. karox/orchestration_cli.py +861 -0
  117. karox/orchestration_native.py +319 -0
  118. karox/orchestration_planning.py +170 -0
  119. karox/orchestration_presenter.py +372 -0
  120. karox/orchestration_recipes.py +162 -0
  121. karox/orchestration_recovery.py +287 -0
  122. karox/orchestration_routing.py +481 -0
  123. karox/orchestrator.py +1715 -0
  124. karox/orchestrator_advisor.py +252 -0
  125. karox/orchestrator_delegation.py +329 -0
  126. karox/output_policy.py +151 -0
  127. karox/packs.py +951 -0
  128. karox/paths.py +160 -0
  129. karox/plan_act.py +97 -0
  130. karox/plan_executor.py +1605 -0
  131. karox/policy.py +212 -0
  132. karox/port_allocation.py +144 -0
  133. karox/port_ownership.py +593 -0
  134. karox/process_identity.py +430 -0
  135. karox/process_launcher.py +150 -0
  136. karox/project_context.py +506 -0
  137. karox/project_map.py +363 -0
  138. karox/project_registry.py +250 -0
  139. karox/prompt_benchmark.py +239 -0
  140. karox/prompt_cache_plan.py +64 -0
  141. karox/promptql_outbound.py +276 -0
  142. karox/provider_adapters.py +1321 -0
  143. karox/provider_controller.py +661 -0
  144. karox/provider_credential_capture.py +200 -0
  145. karox/provider_factory.py +50 -0
  146. karox/provider_presets.py +378 -0
  147. karox/provider_pricing.py +209 -0
  148. karox/providers.py +1123 -0
  149. karox/proxy.py +252 -0
  150. karox/proxy_server.py +2049 -0
  151. karox/quickstart.py +387 -0
  152. karox/quota_brain.py +136 -0
  153. karox/recipe_registry.py +187 -0
  154. karox/registry.py +659 -0
  155. karox/release_hot_actions.py +577 -0
  156. karox/remote_lease.py +280 -0
  157. karox/remote_tools.py +806 -0
  158. karox/repo_context.py +1950 -0
  159. karox/repository_lease.py +492 -0
  160. karox/research_subagent.py +613 -0
  161. karox/result_envelope.py +197 -0
  162. karox/risk_engine.py +648 -0
  163. karox/risk_mapping.py +583 -0
  164. karox/route_health.py +170 -0
  165. karox/routing.py +686 -0
  166. karox/runtime_restart.py +510 -0
  167. karox/saved_bridge_autostart.py +889 -0
  168. karox/saved_bridge_supervisor.py +1134 -0
  169. karox/security.py +157 -0
  170. karox/service_supervisor.py +758 -0
  171. karox/session_view.py +1129 -0
  172. karox/sessions.py +825 -0
  173. karox/shadow_economy.py +229 -0
  174. karox/skills.py +873 -0
  175. karox/smart_stop.py +173 -0
  176. karox/static_server.py +45 -0
  177. karox/subscription_cli.py +640 -0
  178. karox/system_chrome.py +321 -0
  179. karox/tailscale.py +938 -0
  180. karox/tailscale_routes.py +389 -0
  181. karox/task_state.py +469 -0
  182. karox/tool_catalog.py +66 -0
  183. karox/tool_telemetry.py +425 -0
  184. karox/tool_universe.py +187 -0
  185. karox/transcript.py +407 -0
  186. karox/transcript_shadow.py +435 -0
  187. karox/tui.py +16842 -0
  188. karox/tui_connections.py +9408 -0
  189. karox/tui_dashboard.py +1192 -0
  190. karox/tui_workspace.py +332 -0
  191. karox/unified_patch.py +295 -0
  192. karox/usage_analytics.py +467 -0
  193. karox/usage_report.py +376 -0
  194. karox/user_commands.py +232 -0
  195. karox/verification.py +351 -0
  196. karox/web_bridge_launcher.py +6698 -0
  197. karox/web_bridge_profiles.py +552 -0
  198. karox/worker_adapter_registry.py +101 -0
  199. karox/worker_adapters.py +236 -0
  200. karox/workspace_change_guard.py +269 -0
  201. karox/workspace_transaction.py +490 -0
  202. karox/workspace_worker.py +639 -0
  203. karox/worktree_pool.py +576 -0
  204. karox_runtime-5.0.0rc2.dist-info/METADATA +448 -0
  205. karox_runtime-5.0.0rc2.dist-info/RECORD +209 -0
  206. karox_runtime-5.0.0rc2.dist-info/WHEEL +5 -0
  207. karox_runtime-5.0.0rc2.dist-info/entry_points.txt +4 -0
  208. karox_runtime-5.0.0rc2.dist-info/licenses/LICENSE +21 -0
  209. karox_runtime-5.0.0rc2.dist-info/top_level.txt +1 -0
karox/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """KaroX 5 hybrid local runtime for native and hosted AI agents."""
2
+
3
+ from .models import AccessProfile, Capability, Origin, OriginKind
4
+
5
+ __all__ = ["AccessProfile", "Capability", "Origin", "OriginKind"]
6
+ __version__ = "5.0.0rc2"
karox/access_mode.py ADDED
@@ -0,0 +1,248 @@
1
+ """Common Bypass (access mode) contract for every KaroX connection family.
2
+
3
+ RU: "Bypass rezhim" -- minimum vnutrennikh ogranichenij KaroX dlya avtonomnoj
4
+ razrabotki. EN: "Bypass mode" -- minimal KaroX restrictions for autonomous
5
+ development.
6
+
7
+ Bypass is an opt-in, per-connection mode expressed entirely through the
8
+ permission architecture that already exists: the saved record's
9
+ ``AccessProfile`` plus the ``CapabilityPolicy`` layer that enforces it at
10
+ runtime. ON selects the highest existing developer capability profile
11
+ (``AccessProfile.ELEVATED``); OFF returns to the normal protected Project
12
+ access contract (``AccessProfile.WORKSPACE_WRITE``). No second permission
13
+ architecture, no parallel store.
14
+
15
+ What Bypass is NOT:
16
+
17
+ * it does not touch OS / provider / platform security;
18
+ * it does not disable credential isolation or the keyring boundary;
19
+ * it does not skip confirmation of irreversible external actions;
20
+ * it does not mean automatic git push, publishing, or payments;
21
+ * it never rotates secrets, connection identity, or bridge URLs.
22
+
23
+ Hyperagent's earlier "Full developer access" toggle is this very mode; its
24
+ canonical saved-profile builder lives here now and Hyperagent is the first
25
+ user of the shared implementation. A record written before this mode
26
+ existed reads as OFF, and a legacy Hyperagent profile that already carries
27
+ the full elevated contract reads as ON -- no migration rewrite, no schema
28
+ version bump.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from dataclasses import replace
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+ __all__ = [
38
+ "BYPASS_TOOL_NAMES",
39
+ "build_saved_profile_bypass",
40
+ "provider_access_profile",
41
+ "provider_bypass_enabled",
42
+ "saved_profile_bypass_enabled",
43
+ "session_access_profile",
44
+ "set_provider_bypass",
45
+ ]
46
+
47
+ # The tool names a coding connection gains under Bypass beyond its normal
48
+ # project-access catalogue. They are appended when the mode is first enabled
49
+ # and then kept stable across ON/OFF transitions: several clients cache the
50
+ # MCP tool catalogue for the lifetime of a thread, so only the runtime
51
+ # permission profile may change on a toggle.
52
+ BYPASS_TOOL_NAMES: tuple[str, ...] = (
53
+ "karox.command.run",
54
+ "karox.command.start",
55
+ "karox.command.status",
56
+ "karox.command.logs",
57
+ "karox.command.cancel",
58
+ "karox.git.commit",
59
+ )
60
+
61
+
62
+ def saved_profile_bypass_enabled(profile: Any) -> bool:
63
+ """Whether a saved web-bridge profile explicitly enables Bypass.
64
+
65
+ New profiles persist a dedicated ``bypass`` bit so Advanced/elevated access
66
+ can exist without silently authorizing destructive deletion. ``from_dict``
67
+ preserves legacy elevated profiles as Bypass during migration.
68
+ """
69
+
70
+ try:
71
+ if hasattr(profile, "bypass"):
72
+ return bool(getattr(profile, "bypass"))
73
+ from .models import AccessProfile
74
+ return getattr(profile, "access_profile", None) == AccessProfile.ELEVATED
75
+ except Exception:
76
+ return False
77
+
78
+
79
+ def build_saved_profile_bypass(profile: Any, enabled: bool) -> Any:
80
+ """Return the canonical saved profile for Bypass ON/OFF.
81
+
82
+ The returned record keeps its identity: name, target, repository,
83
+ projects, port, tunnel, public URL, and credential references are never
84
+ rewritten, so flipping the mode cannot rotate a secret, change a bridge
85
+ URL, or spawn a duplicate. Hyperagent additionally opts into its
86
+ historical full-developer browser contract; every other target keeps its
87
+ browser policy exactly as configured.
88
+ """
89
+
90
+ if getattr(profile, "target_profile", "") == "hyperagent-web":
91
+ return _build_hyperagent_bypass(profile, enabled)
92
+
93
+ from .models import AccessProfile
94
+
95
+ kwargs: dict[str, Any] = {
96
+ "access_profile": (
97
+ AccessProfile.ELEVATED if enabled else AccessProfile.WORKSPACE_WRITE
98
+ ),
99
+ "bypass": bool(enabled),
100
+ }
101
+ if enabled:
102
+ tools = list(getattr(profile, "tools", ()) or ())
103
+ extended = False
104
+ for name in BYPASS_TOOL_NAMES:
105
+ if name not in tools:
106
+ tools.append(name)
107
+ extended = True
108
+ if extended:
109
+ kwargs["tools"] = tuple(dict.fromkeys(tools))
110
+ return replace(profile, **kwargs)
111
+
112
+
113
+ def _build_hyperagent_bypass(profile: Any, enabled: bool) -> Any:
114
+ """The canonical Hyperagent profile for Full access / Bypass ON-OFF.
115
+
116
+ ON deliberately means one coherent trusted developer contract rather than
117
+ a pile of independent checkboxes: elevated repo/process/check
118
+ capabilities, the whole coding tool surface, Git commit, an unrestricted
119
+ developer command runner, and a headed browser that may navigate public
120
+ HTTPS with network inspection and takeover. OFF returns to the normal
121
+ protected Project access contract.
122
+ """
123
+
124
+ repository_raw = str(getattr(profile, "repository", "") or "").strip()
125
+ if not repository_raw:
126
+ raise ValueError("Hyperagent profile has no repository")
127
+ repository = Path(repository_raw).expanduser().resolve(strict=True)
128
+ if not repository.is_dir():
129
+ raise ValueError("Hyperagent repository is not a directory")
130
+
131
+ from .hosted_tools_runtime import server_profiles_for_repository
132
+ from .models import AccessProfile
133
+ from .verification import discover_verification_commands
134
+ from .web_bridge_launcher import (
135
+ CHECKS_RUN_TOOL,
136
+ DEFAULT_WEB_TOOLS,
137
+ EXTERNAL_BROWSER_TOOL_NAMES,
138
+ WRITE_WEB_TOOLS,
139
+ )
140
+
141
+ verification = discover_verification_commands(repository)
142
+ if not verification:
143
+ verification = tuple(getattr(profile, "verification_commands", ()) or ())
144
+ runtime_profiles = server_profiles_for_repository(repository)
145
+ server_profiles = tuple(item.to_public_dict() for item in runtime_profiles)
146
+
147
+ tools = list(DEFAULT_WEB_TOOLS)
148
+ tools.extend(WRITE_WEB_TOOLS)
149
+ if verification:
150
+ tools.append(CHECKS_RUN_TOOL)
151
+ if not server_profiles:
152
+ tools = [
153
+ item
154
+ for item in tools
155
+ if item not in {
156
+ "karox.dev_server.start",
157
+ "karox.dev_server.stop",
158
+ "karox.dev_server.restart",
159
+ }
160
+ ]
161
+ # Hyperagent may cache the MCP tool catalogue for the lifetime of a thread.
162
+ # Keep the advertised tool names stable across Project access <-> Bypass
163
+ # and change only the runtime permission/profile. Otherwise flipping the
164
+ # switch can leave a perfectly healthy elevated bridge behind a stale
165
+ # client catalogue that still contains the smaller Project-access set.
166
+ tools.extend(BYPASS_TOOL_NAMES)
167
+ tools.extend(EXTERNAL_BROWSER_TOOL_NAMES)
168
+
169
+ return replace(
170
+ profile,
171
+ tools=tuple(dict.fromkeys(tools)),
172
+ verification_commands=tuple(verification),
173
+ server_profiles=server_profiles,
174
+ access_profile=(
175
+ AccessProfile.ELEVATED if enabled else AccessProfile.WORKSPACE_WRITE
176
+ ),
177
+ bypass=bool(enabled),
178
+ browser_external_https=bool(enabled),
179
+ # Empty allowlist + external_https=True means any public HTTPS host
180
+ # that passes KaroX DNS/private-network checks: the intended normal
181
+ # browser.
182
+ browser_allowed_domains=(),
183
+ browser_headed=bool(enabled),
184
+ browser_user_takeover=bool(enabled),
185
+ browser_network_inspection=bool(enabled),
186
+ # Spending/subscription confirmation remains a separate explicit gate.
187
+ browser_payment_confirmation=False,
188
+ )
189
+
190
+
191
+ def provider_bypass_enabled(record: Any) -> bool:
192
+ """Whether an API/provider connection requests Bypass for new sessions."""
193
+
194
+ return bool(getattr(record, "bypass", False))
195
+
196
+
197
+ def set_provider_bypass(record: Any, enabled: bool) -> Any:
198
+ """Return the provider record with the persisted Bypass preference.
199
+
200
+ The provider record only carries the preference; enforcement happens when
201
+ a KaroX agent session is created for that provider. Provider HTTP auth,
202
+ transport configuration, and credential storage never change here.
203
+ """
204
+
205
+ if bool(getattr(record, "bypass", False)) == bool(enabled):
206
+ return record
207
+ return replace(record, bypass=bool(enabled))
208
+
209
+
210
+ def provider_access_profile(record: Any, base: Any = None) -> Any:
211
+ """The ``AccessProfile`` a new agent session for this provider gets.
212
+
213
+ Bypass ON maps to the existing elevated developer contract; OFF keeps the
214
+ caller's normal profile (or the protected Project access default).
215
+ """
216
+
217
+ from .models import AccessProfile
218
+
219
+ if provider_bypass_enabled(record):
220
+ return AccessProfile.ELEVATED
221
+ if base is not None:
222
+ return base
223
+ return AccessProfile.WORKSPACE_WRITE
224
+
225
+
226
+ def session_access_profile(records: Any, base: Any = None) -> Any:
227
+ """The profile for a session that may be served by several providers.
228
+
229
+ A routed session can fall back from its primary provider to another one
230
+ mid-run, and the session's profile is fixed when the session is created.
231
+ The narrowest safe rule is therefore unanimity: the session is elevated
232
+ only when *every* provider that could serve it has Bypass ON. One
233
+ fallback route left in normal mode keeps the whole session protected
234
+ rather than quietly lending it elevated capabilities.
235
+
236
+ An empty set of records (direct ``--base-url`` mode, which persists no
237
+ provider record and therefore has nowhere to store the preference) is not
238
+ unanimous consent and keeps the normal profile.
239
+ """
240
+
241
+ from .models import AccessProfile
242
+
243
+ items = list(records)
244
+ if items and all(provider_bypass_enabled(item) for item in items):
245
+ return AccessProfile.ELEVATED
246
+ if base is not None:
247
+ return base
248
+ return AccessProfile.WORKSPACE_WRITE
@@ -0,0 +1,231 @@
1
+ """Capability-first execution layer over the stable Extended Core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable, Optional
6
+
7
+ from .action_policy import (
8
+ ActionConfirmationRequired,
9
+ ActionDecisionEngine,
10
+ ActionDisposition,
11
+ ActionHardBlocked,
12
+ )
13
+ from .core_tools import ExtendedCoreRuntime
14
+ from .models import CoreCommand
15
+ from .risk_engine import ConfirmationRejected
16
+ from .risk_mapping import action_for_command
17
+
18
+
19
+ class CapabilityCoreRuntime(ExtendedCoreRuntime):
20
+ """Extended Core with consequence-based action authorization."""
21
+
22
+ def __init__(
23
+ self,
24
+ *args: Any,
25
+ action_decisions: ActionDecisionEngine | None = None,
26
+ rollback_checkpoint_id: str | None = None,
27
+ checkpoint_factory: Callable[[], Optional[str]] | None = None,
28
+ bypass_mode: bool = False,
29
+ **kwargs: Any,
30
+ ) -> None:
31
+ risk = kwargs.get("risk")
32
+ if action_decisions is not None:
33
+ if risk is None:
34
+ kwargs["risk"] = action_decisions.risk
35
+ elif risk is not action_decisions.risk:
36
+ raise ValueError(
37
+ "action decision engine and risk engine must share one ledger"
38
+ )
39
+ self._action_decisions = action_decisions
40
+ self._rollback_checkpoint_id = rollback_checkpoint_id
41
+ self._checkpoint_factory = checkpoint_factory
42
+ self._bypass_mode = bool(bypass_mode)
43
+ super().__init__(*args, **kwargs)
44
+
45
+ def _apply_smart_stop(self, command: CoreCommand) -> None:
46
+ engine = self._action_decisions
47
+ if engine is None:
48
+ super()._apply_smart_stop(command)
49
+ return
50
+
51
+ if command.name == "repo.command":
52
+ payload = command.arguments.get("payload")
53
+ if isinstance(payload, dict) and payload.get("dry_run") is True:
54
+ # A dry run mutates nothing: it exists so the agent can inspect
55
+ # the impact of a batch before executing it. Keeping the
56
+ # preview free is what makes consequence-based decisions cheap.
57
+ return
58
+
59
+ bypass_mode = self._bypass_mode
60
+ try:
61
+ record = self.sessions.load(command.session_id)
62
+ # Hosted callers carry the active workstream objective out-of-band
63
+ # on CoreCommand. Native callers intentionally leave it empty and
64
+ # inherit the durable session task. This keeps scoped authority
65
+ # current without putting user intent into a tool payload.
66
+ user_intent = command.user_intent or record.task
67
+ # Checkpoints remain useful evidence/rollback support, but they no
68
+ # longer silently convert source deletion into ordinary autonomy.
69
+ reversible_by_checkpoint = bool(self._rollback_checkpoint_id)
70
+ except Exception:
71
+ user_intent = ""
72
+ reversible_by_checkpoint = False
73
+ action = action_for_command(
74
+ command,
75
+ repository=self.repository,
76
+ reversible_by_checkpoint=reversible_by_checkpoint,
77
+ )
78
+
79
+ try:
80
+ decision = engine.authorize(
81
+ action,
82
+ user_intent=user_intent,
83
+ confirmation_token=command.confirmation_token,
84
+ bypass_mode=bypass_mode,
85
+ )
86
+ except ActionConfirmationRequired as stop:
87
+ # Confirmation-worthy operations stay pending. Do not mutate state
88
+ # merely to try to dissolve the prompt: the agent can continue
89
+ # independent work and surface the deferred gate only when needed.
90
+ decision = stop.decision
91
+ self._publish_risk(
92
+ decision.assessment, allowed=False, reason="confirmation_required"
93
+ )
94
+ self._audit(
95
+ "core.command.stopped",
96
+ {
97
+ "session_id": command.session_id,
98
+ "origin": command.origin.key,
99
+ "command": command.name,
100
+ "correlation_id": command.correlation_id,
101
+ "risk": decision.assessment.level.value,
102
+ "disposition": decision.disposition.value,
103
+ "consequence": decision.consequence.value,
104
+ "reasons": list(decision.reasons),
105
+ },
106
+ )
107
+ raise
108
+ except ActionHardBlocked as blocked:
109
+ decision = blocked.decision
110
+ self._publish_risk(decision.assessment, allowed=False, reason="hard_block")
111
+ self._audit(
112
+ "core.command.hard_blocked",
113
+ {
114
+ "session_id": command.session_id,
115
+ "origin": command.origin.key,
116
+ "command": command.name,
117
+ "correlation_id": command.correlation_id,
118
+ "risk": decision.assessment.level.value,
119
+ "disposition": decision.disposition.value,
120
+ "consequence": decision.consequence.value,
121
+ "reasons": list(decision.reasons),
122
+ },
123
+ )
124
+ raise
125
+ except ConfirmationRejected as rejected:
126
+ decision = engine.decide(
127
+ action,
128
+ user_intent=user_intent,
129
+ bypass_mode=bypass_mode,
130
+ )
131
+ self._publish_risk(
132
+ decision.assessment, allowed=False, reason=rejected.reason
133
+ )
134
+ self._audit(
135
+ "core.command.confirmation_rejected",
136
+ {
137
+ "session_id": command.session_id,
138
+ "origin": command.origin.key,
139
+ "command": command.name,
140
+ "correlation_id": command.correlation_id,
141
+ "reason": rejected.reason,
142
+ "disposition": decision.disposition.value,
143
+ },
144
+ )
145
+ raise
146
+
147
+ self._publish_risk(
148
+ decision.assessment,
149
+ allowed=True,
150
+ reason=decision.disposition.value,
151
+ )
152
+ if decision.disposition is ActionDisposition.GUARDED_AUTO:
153
+ self._audit(
154
+ "core.command.guarded_auto",
155
+ {
156
+ "session_id": command.session_id,
157
+ "origin": command.origin.key,
158
+ "command": command.name,
159
+ "correlation_id": command.correlation_id,
160
+ "risk": decision.assessment.level.value,
161
+ "consequence": decision.consequence.value,
162
+ "intent_authorized": decision.intent_authorized,
163
+ "reasons": list(decision.reasons),
164
+ },
165
+ )
166
+
167
+ def _auto_guard_with_checkpoint(
168
+ self,
169
+ command: CoreCommand,
170
+ stop: ActionConfirmationRequired,
171
+ user_intent: str,
172
+ ):
173
+ """Reversibility adapter: dissolve a deletion CONFIRM with a checkpoint.
174
+
175
+ When the only reason a repository-scoped deletion needs confirmation is
176
+ that no rollback guarantee exists yet, the runtime creates one on demand
177
+ and lets the decision engine re-evaluate honestly. Safety increases
178
+ autonomy instead of prompting. System, external, user-data and
179
+ out-of-repository boundaries are never widened: the engine re-decides
180
+ with the same rules and only a WORKSPACE deletion backed by a real
181
+ checkpoint becomes GUARDED_AUTO.
182
+ """
183
+
184
+ engine = self._action_decisions
185
+ factory = self._checkpoint_factory
186
+ if engine is None or factory is None:
187
+ return None
188
+ if self._rollback_checkpoint_id:
189
+ # The current turn already has a checkpoint; the stop had a
190
+ # different cause that a second checkpoint cannot dissolve.
191
+ return None
192
+ decision = stop.decision
193
+ if "delete_not_scoped_by_user" not in decision.reasons:
194
+ return None
195
+ try:
196
+ checkpoint_id = factory()
197
+ except Exception:
198
+ checkpoint_id = None
199
+ if not isinstance(checkpoint_id, str) or not checkpoint_id:
200
+ return None
201
+ self._rollback_checkpoint_id = checkpoint_id
202
+ guarded = action_for_command(
203
+ command,
204
+ repository=self.repository,
205
+ reversible_by_checkpoint=True,
206
+ )
207
+ try:
208
+ recovered = engine.authorize(
209
+ guarded,
210
+ user_intent=user_intent,
211
+ confirmation_token=command.confirmation_token,
212
+ )
213
+ except (ActionConfirmationRequired, ActionHardBlocked):
214
+ return None
215
+ self._audit(
216
+ "core.command.auto_guarded_checkpoint",
217
+ {
218
+ "session_id": command.session_id,
219
+ "origin": command.origin.key,
220
+ "command": command.name,
221
+ "correlation_id": command.correlation_id,
222
+ "rollback_checkpoint_id": checkpoint_id,
223
+ "disposition": recovered.disposition.value,
224
+ "consequence": recovered.consequence.value,
225
+ "reasons": list(recovered.reasons),
226
+ },
227
+ )
228
+ return recovered
229
+
230
+
231
+ __all__ = ["CapabilityCoreRuntime"]