@gajae-code/coding-agent 0.16.7 → 0.17.1

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 (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -0,0 +1,242 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import {
5
+ inspectConfigFilePermissionRepair,
6
+ type NativeExactFileIdentity,
7
+ type NativePermissionRepairResult,
8
+ repairConfigFilePermissions,
9
+ } from "@gajae-code/natives";
10
+ import { DoctorJournal, DoctorJournalCreateError } from "./journal";
11
+ import type { DoctorCheck, DoctorRepair, DoctorScope } from "./types";
12
+
13
+ export interface DoctorPermissionRepairRequest {
14
+ readonly filePath: string;
15
+ readonly rootPath: string;
16
+ readonly scope: DoctorScope;
17
+ readonly targetId: string;
18
+ readonly repairId: string;
19
+ readonly runId: string;
20
+ readonly mode: "dry-run" | "fix";
21
+ readonly authorization?: readonly string[];
22
+ readonly journalRoot: string;
23
+ readonly expected: { readonly identity: NativeExactFileIdentity; readonly mode: number };
24
+ readonly collectAfterChecks: () => Promise<readonly DoctorCheck[]>;
25
+ }
26
+ export interface DoctorPermissionRepairResult {
27
+ readonly repair: DoctorRepair;
28
+ readonly native: NativePermissionRepairResult;
29
+ readonly journalPath?: string;
30
+ readonly afterChecks?: readonly DoctorCheck[];
31
+ }
32
+ function base(r: DoctorPermissionRepairRequest, state: DoctorRepair["state"]): DoctorRepair {
33
+ return {
34
+ id: r.repairId,
35
+ targetId: r.targetId,
36
+ riskClasses: ["permission-change"],
37
+ authorization: r.authorization ?? [],
38
+ readiness: [],
39
+ candidates: [],
40
+ preconditions: ["native exact identity", "owner-preserving mode reduction", "ACL absence"],
41
+ state,
42
+ sideEffectStarted: false,
43
+ beforeCheckIds: [],
44
+ afterCheckIds: [],
45
+ restartRequired: false,
46
+ nonrollbackableEffects: [],
47
+ };
48
+ }
49
+ function refused(code: string): NativePermissionRepairResult {
50
+ return { status: "refused", changed: false, verified: false, code };
51
+ }
52
+ function failureCode(error: unknown): string {
53
+ return error instanceof Error ? error.name : "repair_failed";
54
+ }
55
+ function sameIdentity(
56
+ stat: BigIntStats,
57
+ parent: BigIntStats,
58
+ identity: NativeExactFileIdentity,
59
+ mode: number,
60
+ ): boolean {
61
+ return (
62
+ stat.isFile() &&
63
+ !stat.isSymbolicLink() &&
64
+ stat.dev === identity.dev &&
65
+ stat.ino === identity.ino &&
66
+ stat.nlink === (identity.nlink ?? 1n) &&
67
+ stat.size === identity.size &&
68
+ stat.mtimeNs === identity.mtimeNs &&
69
+ parent.dev === identity.parentDev &&
70
+ parent.ino === identity.parentIno &&
71
+ Number(stat.mode) === mode
72
+ );
73
+ }
74
+ async function collectVerifiedChecks(request: DoctorPermissionRepairRequest): Promise<readonly DoctorCheck[]> {
75
+ const checks = await request.collectAfterChecks();
76
+ const desiredMode = request.expected.mode & 0o700;
77
+ if (
78
+ !checks.some(
79
+ check =>
80
+ check.targetId === request.targetId &&
81
+ check.execution === "completed" &&
82
+ check.evidenceLevel === "observed" &&
83
+ check.evidence.mode === desiredMode &&
84
+ check.evidence.ownerMatches === true,
85
+ )
86
+ )
87
+ throw new Error("permission_postcheck_failed");
88
+ const security = inspectConfigFilePermissionRepair(
89
+ request.filePath,
90
+ request.expected.identity,
91
+ request.expected.mode & ~0o077,
92
+ );
93
+ if (security.status !== "verified" || !security.verified) throw new Error("permission_security_postcheck_failed");
94
+ return checks.map(check =>
95
+ check.targetId === request.targetId
96
+ ? { ...check, evidence: { ...check.evidence, aclCoverage: "verified" as const } }
97
+ : check,
98
+ );
99
+ }
100
+ export async function applyDoctorPermissionRepair(
101
+ r: DoctorPermissionRepairRequest,
102
+ ): Promise<DoctorPermissionRepairResult> {
103
+ const preparing = base(r, "preparing");
104
+ if (!r.authorization?.includes("permission-change"))
105
+ return {
106
+ repair: { ...preparing, state: "blocked", reasonCode: "authorization_missing" },
107
+ native: refused("authorization_missing"),
108
+ };
109
+ if (
110
+ r.scope !== "user" ||
111
+ r.repairId !== "permissions.restrict-owned-config" ||
112
+ typeof r.collectAfterChecks !== "function"
113
+ )
114
+ return {
115
+ repair: { ...preparing, state: "blocked", reasonCode: "invalid_permission_request" },
116
+ native: refused("invalid_permission_request"),
117
+ };
118
+ const absolute = path.resolve(r.filePath);
119
+ const root = path.resolve(r.rootPath);
120
+ if (absolute !== root && !absolute.startsWith(`${root}${path.sep}`))
121
+ return {
122
+ repair: { ...preparing, state: "blocked", reasonCode: "target_resolution_incomplete" },
123
+ native: refused("root_escape"),
124
+ };
125
+ if (r.mode === "dry-run") return { repair: { ...preparing, state: "planned" }, native: refused("preview_only") };
126
+ let stat: BigIntStats;
127
+ try {
128
+ stat = (await fs.lstat(absolute, { bigint: true })) as BigIntStats;
129
+ const parent = (await fs.lstat(path.dirname(absolute), { bigint: true })) as BigIntStats;
130
+ if (!sameIdentity(stat, parent, r.expected.identity, r.expected.mode))
131
+ return {
132
+ repair: { ...preparing, state: "blocked", reasonCode: "identity_recheck_required" },
133
+ native: refused("identity_recheck_required"),
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ repair: { ...preparing, state: "blocked", reasonCode: failureCode(error) },
138
+ native: refused("preflight_failed"),
139
+ };
140
+ }
141
+ let native: NativePermissionRepairResult;
142
+ try {
143
+ native = inspectConfigFilePermissionRepair(absolute, r.expected.identity, r.expected.mode);
144
+ if (native.status === "verified" && !native.changed) {
145
+ const afterChecks = await collectVerifiedChecks(r);
146
+ return {
147
+ repair: { ...preparing, state: "not_needed", outcome: { mutationVerified: true } },
148
+ native,
149
+ afterChecks,
150
+ };
151
+ }
152
+ if (native.status !== "ready")
153
+ return {
154
+ repair: { ...preparing, state: "blocked", reasonCode: native.code ?? "permission_preflight_failed" },
155
+ native,
156
+ };
157
+ } catch {
158
+ return {
159
+ repair: { ...preparing, state: "blocked", reasonCode: "permission_preflight_failed" },
160
+ native: refused("permission_preflight_failed"),
161
+ };
162
+ }
163
+ let journal: DoctorJournal;
164
+ try {
165
+ journal = await DoctorJournal.create(r.journalRoot, r.runId);
166
+ } catch (error) {
167
+ const started = error instanceof DoctorJournalCreateError ? error.sideEffectStarted : true;
168
+ return {
169
+ repair: {
170
+ ...preparing,
171
+ state: started ? "uncertain" : "failed",
172
+ sideEffectStarted: started,
173
+ reasonCode: failureCode(error),
174
+ },
175
+ native: refused("journal_create_failed"),
176
+ };
177
+ }
178
+ try {
179
+ await journal.append({
180
+ repairId: r.repairId,
181
+ targetId: r.targetId,
182
+ phase: "before",
183
+ before: { mode: r.expected.mode },
184
+ });
185
+ await journal.append({ repairId: r.repairId, targetId: r.targetId, phase: "applying" });
186
+ const result = repairConfigFilePermissions(absolute, r.expected.identity, r.expected.mode);
187
+ if (result.status === "uncertain" || result.status === "refused") {
188
+ await journal.append({
189
+ repairId: r.repairId,
190
+ targetId: r.targetId,
191
+ phase: "failed",
192
+ outcome: result.code ?? result.status,
193
+ });
194
+ return {
195
+ repair: {
196
+ ...preparing,
197
+ state: result.status === "uncertain" ? "uncertain" : "failed",
198
+ sideEffectStarted: true,
199
+ reasonCode: result.code ?? result.status,
200
+ },
201
+ native: result,
202
+ journalPath: journal.path,
203
+ };
204
+ }
205
+ if (!result.verified) throw new Error("permission_mutation_unverified");
206
+ const afterChecks = await collectVerifiedChecks(r);
207
+ await journal.append({
208
+ repairId: r.repairId,
209
+ targetId: r.targetId,
210
+ phase: "verified",
211
+ after: { mode: r.expected.mode & ~0o077 },
212
+ outcome: "verified",
213
+ });
214
+ return {
215
+ repair: {
216
+ ...preparing,
217
+ state: result.changed ? "verified" : "not_needed",
218
+ sideEffectStarted: true,
219
+ outcome: { mutationVerified: result.verified },
220
+ },
221
+ native: result,
222
+ journalPath: journal.path,
223
+ afterChecks,
224
+ };
225
+ } catch (error) {
226
+ try {
227
+ await journal.append({
228
+ repairId: r.repairId,
229
+ targetId: r.targetId,
230
+ phase: "failed",
231
+ outcome: failureCode(error),
232
+ });
233
+ } catch {}
234
+ return {
235
+ repair: { ...preparing, state: "uncertain", sideEffectStarted: true, reasonCode: failureCode(error) },
236
+ native: refused(failureCode(error)),
237
+ journalPath: journal.path,
238
+ };
239
+ } finally {
240
+ journal.close();
241
+ }
242
+ }
@@ -0,0 +1,377 @@
1
+ /**
2
+ * D7 plugin.quarantine-selected — core-owned durable disable/quarantine writer
3
+ * for exactly one selected (family, scope, name) target, across all three
4
+ * plugin families (GJC bundles, npm packages, marketplace-installed plugins).
5
+ *
6
+ * This module NEVER installs, fetches, reinstalls, imports, or executes
7
+ * plugin code. It only flips the durable "enabled" bit each family's real
8
+ * session-startup consumer already honors:
9
+ * - gjc: GjcPluginRegistryEntry.enabled (readEffectiveRegistryUnpersisted /
10
+ * loadEffectiveGjcPluginRegistry -> session-validation -> activation).
11
+ * - npm (user): gjc-plugins.lock.json PluginRuntimeState.enabled, consumed by
12
+ * getEnabledPlugins() (extensibility/plugins/loader.ts).
13
+ * - npm (project): .gjc/plugin-overrides.json `disabled` list, consumed by the
14
+ * same getEnabledPlugins().
15
+ * - marketplace: installed_plugins.json InstalledPluginEntry.enabled, consumed
16
+ * by listClaudePluginRoots() (discovery/helpers.ts) via
17
+ * `entry.enabled === false` skip.
18
+ *
19
+ * Disable-only: this action never re-enables (D7 has no enable path; an
20
+ * unsafe target requires only plugin-change authorization to quarantine, but
21
+ * re-enabling it is a deliberate separate decision outside this module).
22
+ */
23
+
24
+ import { invalidateClaudePluginRoots, listClaudePluginRoots } from "../../discovery/helpers";
25
+ import {
26
+ type GjcLifecycleContext,
27
+ getGjcBundleEnablementState,
28
+ setGjcBundleQuarantineExpectedBaseline,
29
+ } from "../../extensibility/gjc-plugins/lifecycle";
30
+ import { readEffectiveRegistryUnpersisted } from "../../extensibility/gjc-plugins/registry";
31
+ import type { GjcBundleIdentity } from "../../extensibility/gjc-plugins/types";
32
+ import { getEnabledPlugins } from "../../extensibility/plugins/loader";
33
+ import { PluginManager } from "../../extensibility/plugins/manager";
34
+ import {
35
+ getInstalledPluginEnablementState,
36
+ getInstalledPluginsRegistryPath,
37
+ setInstalledPluginEnabled,
38
+ } from "../../extensibility/plugins/marketplace/registry";
39
+ import { pluginTargetId } from "./ids";
40
+ import { DoctorJournal, DoctorJournalCreateError } from "./journal";
41
+
42
+ export type PluginQuarantineFamily = "gjc" | "npm" | "marketplace";
43
+
44
+ export interface PluginQuarantineTarget {
45
+ family: PluginQuarantineFamily;
46
+ scope: "user" | "project";
47
+ name: string;
48
+ }
49
+
50
+ export interface PluginQuarantineRequest extends PluginQuarantineTarget {
51
+ /** Absolute project cwd; required for gjc/npm project-scope targets. */
52
+ cwd: string;
53
+ /** Home identity used for the marketplace startup-filter reread. */
54
+ home: string;
55
+ /** Explicit override for the marketplace installed_plugins.json path (tests). */
56
+ registryPath?: string;
57
+ /** Journal identity — required by applyPluginQuarantine, ignored by preview. */
58
+ journalRoot?: string;
59
+ runId?: string;
60
+ rootId: string;
61
+ }
62
+
63
+ export type PluginQuarantineBlockReason =
64
+ | "invalid_target"
65
+ | "not_installed"
66
+ | "unsafe_link"
67
+ | "registry_unreadable"
68
+ | "malformed_registry"
69
+ | "identity_mismatch";
70
+
71
+ export interface PluginQuarantinePlan {
72
+ target: PluginQuarantineTarget;
73
+ targetId?: string;
74
+ enabled: boolean;
75
+ baseline: string;
76
+ status: "ready" | "not_needed" | "blocked";
77
+ reason?: PluginQuarantineBlockReason;
78
+ }
79
+
80
+ export type PluginQuarantineApplyStatus = "verified" | "not_needed" | "conflict" | "blocked" | "uncertain";
81
+
82
+ export interface PluginQuarantineApplyResult {
83
+ status: PluginQuarantineApplyStatus;
84
+ reason?: string;
85
+ /** True once any durable mutation attempt started (journal `applying` appended). */
86
+ sideEffectStarted: boolean;
87
+ /** Fresh post-commit startup-filter observation for the selected target only. */
88
+ targetExcludedFromStartup?: boolean;
89
+ journalPath?: string;
90
+ }
91
+
92
+ const VALID_NAME = /^[A-Za-z0-9_.@-]{1,256}$/;
93
+ const VALID_NPM_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/i;
94
+
95
+ function authOk(authorizations: readonly string[] | undefined): boolean {
96
+ return authorizations?.includes("plugin-change") === true;
97
+ }
98
+
99
+ function invalidTarget(target: PluginQuarantineTarget): boolean {
100
+ return (
101
+ !target.name ||
102
+ target.name === "." ||
103
+ target.name === ".." ||
104
+ !(target.family === "npm"
105
+ ? target.name.length <= 214 && VALID_NPM_NAME.test(target.name)
106
+ : VALID_NAME.test(target.name))
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Read-only preview: resolves the exact family writer's current durable
112
+ * enablement state and CAS baseline for one (family, scope, name) target.
113
+ * Never imports, fetches, or executes plugin code, and performs no writes.
114
+ */
115
+ export async function previewPluginQuarantine(input: PluginQuarantineRequest): Promise<PluginQuarantinePlan> {
116
+ if (invalidTarget(input) || !/^r[0-9a-f]{64}$/.test(input.rootId))
117
+ return { target: input, enabled: false, baseline: "", status: "blocked", reason: "invalid_target" };
118
+ const targetId = pluginTargetId(input.rootId, input.family, input.scope, input.name);
119
+
120
+ if (input.family === "gjc") {
121
+ const result = await getGjcBundleEnablementState(
122
+ { cwd: input.cwd },
123
+ { kind: "gjc-bundle", scope: input.scope, name: input.name },
124
+ );
125
+ if (!result.ok) {
126
+ const reason: PluginQuarantineBlockReason =
127
+ result.error.code === "registry_unreadable" ? "registry_unreadable" : "not_installed";
128
+ return { target: input, targetId, enabled: false, baseline: "", status: "blocked", reason };
129
+ }
130
+ const enabled = result.value.enabled;
131
+ return {
132
+ target: input,
133
+ targetId,
134
+ enabled,
135
+ baseline: result.value.baseline,
136
+ status: enabled ? "ready" : "not_needed",
137
+ };
138
+ }
139
+
140
+ if (input.family === "npm") {
141
+ const manager = new PluginManager(input.cwd);
142
+ const state = await manager.getEnablementState(input.name, input.scope);
143
+ if (state.status === "malformed")
144
+ return {
145
+ target: input,
146
+ targetId,
147
+ enabled: false,
148
+ baseline: "",
149
+ status: "blocked",
150
+ reason: "malformed_registry",
151
+ };
152
+ if (state.status === "not_installed")
153
+ return { target: input, targetId, enabled: false, baseline: "", status: "blocked", reason: "not_installed" };
154
+ return {
155
+ target: input,
156
+ targetId,
157
+ enabled: state.enabled,
158
+ baseline: state.baseline,
159
+ status: state.enabled ? "ready" : "not_needed",
160
+ };
161
+ }
162
+
163
+ // marketplace
164
+ const filePath = input.registryPath ?? getInstalledPluginsRegistryPath();
165
+ const state = await getInstalledPluginEnablementState(filePath, input.name, input.scope);
166
+ if (state.status === "unsafe_link")
167
+ return { target: input, targetId, enabled: false, baseline: "", status: "blocked", reason: "unsafe_link" };
168
+ if (state.status === "malformed")
169
+ return { target: input, targetId, enabled: false, baseline: "", status: "blocked", reason: "malformed_registry" };
170
+ if (state.status === "not_installed")
171
+ return { target: input, targetId, enabled: false, baseline: "", status: "blocked", reason: "not_installed" };
172
+ // Marketplace quarantine is metadata-only: an absent or unreadable cached
173
+ // artifact at installPath never blocks disabling. That is a D6 concern.
174
+ return {
175
+ target: input,
176
+ targetId,
177
+ enabled: state.enabled,
178
+ baseline: state.baseline,
179
+ status: state.enabled ? "ready" : "not_needed",
180
+ };
181
+ }
182
+
183
+ /** Fresh, independent re-read of the exact startup consumer for one family/scope/name target. */
184
+ async function targetExcludedFromStartup(input: PluginQuarantineRequest): Promise<boolean> {
185
+ if (input.family === "gjc") {
186
+ const effective = await readEffectiveRegistryUnpersisted(input.scope, input.cwd);
187
+ const entry = effective.plugins.find(p => p.name === input.name);
188
+ return !entry?.enabled;
189
+ }
190
+ if (input.family === "npm") {
191
+ const enabled = await getEnabledPlugins(input.cwd);
192
+ return !enabled.some(p => p.name === input.name);
193
+ }
194
+ await invalidateClaudePluginRoots(input.home, input.cwd);
195
+ const { roots } = await listClaudePluginRoots(input.home, input.cwd);
196
+ return !roots.some(r => r.id === input.name && r.scope === input.scope);
197
+ }
198
+
199
+ async function otherTargetsPreserved(input: PluginQuarantineRequest, before: Set<string>): Promise<boolean> {
200
+ if (input.family === "gjc") {
201
+ const effective = await readEffectiveRegistryUnpersisted(input.scope, input.cwd);
202
+ const after = new Set(effective.plugins.filter(p => p.enabled).map(p => `${p.scope}:${p.name}`));
203
+ for (const key of before) {
204
+ if (key === `${input.scope}:${input.name}`) continue;
205
+ if (!after.has(key)) return false;
206
+ }
207
+ return true;
208
+ }
209
+ if (input.family === "npm") {
210
+ const enabled = await getEnabledPlugins(input.cwd);
211
+ const after = new Set(enabled.map(p => p.name));
212
+ for (const key of before) {
213
+ if (key === input.name) continue;
214
+ if (!after.has(key)) return false;
215
+ }
216
+ return true;
217
+ }
218
+ const { roots } = await listClaudePluginRoots(input.home, input.cwd);
219
+ const after = new Set(roots.map(r => `${r.scope}:${r.id}`));
220
+ for (const key of before) {
221
+ if (key === `${input.scope}:${input.name}`) continue;
222
+ if (!after.has(key)) return false;
223
+ }
224
+ return true;
225
+ }
226
+
227
+ async function snapshotOthers(input: PluginQuarantineRequest): Promise<Set<string>> {
228
+ if (input.family === "gjc") {
229
+ const effective = await readEffectiveRegistryUnpersisted(input.scope, input.cwd);
230
+ return new Set(effective.plugins.filter(p => p.enabled).map(p => `${p.scope}:${p.name}`));
231
+ }
232
+ if (input.family === "npm") {
233
+ const enabled = await getEnabledPlugins(input.cwd);
234
+ return new Set(enabled.map(p => p.name));
235
+ }
236
+ const { roots } = await listClaudePluginRoots(input.home, input.cwd);
237
+ return new Set(roots.map(r => `${r.scope}:${r.id}`));
238
+ }
239
+
240
+ /**
241
+ * Mutating action. Authorization is checked before any risk evaluation, lock
242
+ * acquisition, or journal access — an unauthorized caller learns nothing
243
+ * beyond "authorization required" and causes zero side effects. D7 is
244
+ * disable-only: `enabled: true` is never accepted here.
245
+ *
246
+ * Runs the canonical create -> before -> applying -> verified|failed -> close
247
+ * journal cycle (one journal per run, `plugin.quarantine-selected` selection),
248
+ * then independently re-reads the real startup consumer for the selected
249
+ * target only before reporting `verified` (never a fabricated generic ready).
250
+ */
251
+ export async function applyPluginQuarantine(
252
+ input: PluginQuarantineRequest,
253
+ authorizations: readonly string[],
254
+ expected: PluginQuarantinePlan,
255
+ ): Promise<PluginQuarantineApplyResult> {
256
+ if (!authOk(authorizations)) return { status: "blocked", reason: "authorization_missing", sideEffectStarted: false };
257
+
258
+ if (
259
+ !expected ||
260
+ expected.target.family !== input.family ||
261
+ expected.target.scope !== input.scope ||
262
+ expected.target.name !== input.name ||
263
+ expected.targetId !== pluginTargetId(input.rootId, input.family, input.scope, input.name)
264
+ )
265
+ return { status: "blocked", reason: "original_target_missing", sideEffectStarted: false };
266
+ const plan = await previewPluginQuarantine(input);
267
+ if (plan.baseline !== expected.baseline)
268
+ return { status: "conflict", reason: "stale_baseline", sideEffectStarted: false };
269
+ if (plan.status === "blocked") return { status: "blocked", reason: plan.reason, sideEffectStarted: false };
270
+ if (plan.status === "not_needed") {
271
+ const excluded = await targetExcludedFromStartup(input);
272
+ return {
273
+ status: excluded ? "not_needed" : "blocked",
274
+ reason: excluded ? undefined : "startup_filter_not_confirmed",
275
+ sideEffectStarted: false,
276
+ targetExcludedFromStartup: excluded,
277
+ };
278
+ }
279
+
280
+ if (!input.journalRoot || !input.runId)
281
+ return { status: "blocked", reason: "journal_identity_missing", sideEffectStarted: false };
282
+ const targetId = plan.targetId ?? pluginTargetId(input.rootId, input.family, input.scope, input.name);
283
+ const repairId = "plugin.quarantine-selected" as const;
284
+
285
+ const others = await snapshotOthers(input);
286
+ let journal: DoctorJournal;
287
+ try {
288
+ journal = await DoctorJournal.create(input.journalRoot, input.runId);
289
+ } catch (error) {
290
+ if (error instanceof DoctorJournalCreateError)
291
+ return {
292
+ status: error.sideEffectStarted ? "uncertain" : "blocked",
293
+ reason: error.reasonCode,
294
+ sideEffectStarted: error.sideEffectStarted,
295
+ };
296
+ return { status: "uncertain", reason: "journal_unavailable", sideEffectStarted: true };
297
+ }
298
+
299
+ const sideEffectStarted = true;
300
+ try {
301
+ await journal.append({ repairId, targetId, phase: "before", before: { value: plan.enabled } });
302
+ await journal.append({ repairId, targetId, phase: "applying" });
303
+
304
+ let mutationOutcome: "updated" | "not_needed";
305
+ if (input.family === "gjc") {
306
+ const result = await setGjcBundleQuarantineExpectedBaseline(
307
+ { cwd: input.cwd } as GjcLifecycleContext,
308
+ { kind: "gjc-bundle", scope: input.scope, name: input.name } as GjcBundleIdentity,
309
+ false,
310
+ plan.baseline,
311
+ );
312
+ if (!result.ok) {
313
+ const code = result.error.code;
314
+ await journal.append({ repairId, targetId, phase: "failed", outcome: "failed" });
315
+ return { status: code === "stale_baseline" ? "conflict" : "blocked", reason: code, sideEffectStarted };
316
+ }
317
+ mutationOutcome = result.value.mutated ? "updated" : "not_needed";
318
+ } else if (input.family === "npm") {
319
+ const manager = new PluginManager(input.cwd);
320
+ try {
321
+ const result = await manager.setEnabled(input.name, false, input.scope, plan.baseline);
322
+ mutationOutcome = result.status;
323
+ } catch (error) {
324
+ const code = (error as { code?: string }).code ?? "repair_execution_unverified";
325
+ await journal.append({ repairId, targetId, phase: "failed", outcome: "failed" });
326
+ return { status: code === "stale_baseline" ? "conflict" : "blocked", reason: code, sideEffectStarted };
327
+ }
328
+ } else {
329
+ const filePath = input.registryPath ?? getInstalledPluginsRegistryPath();
330
+ try {
331
+ mutationOutcome = await setInstalledPluginEnabled(filePath, input.name, input.scope, false, plan.baseline);
332
+ } catch (error) {
333
+ const code = (error as { code?: string }).code ?? "repair_execution_unverified";
334
+ await journal.append({ repairId, targetId, phase: "failed", outcome: "failed" });
335
+ return { status: code === "stale_baseline" ? "conflict" : "blocked", reason: code, sideEffectStarted };
336
+ }
337
+ }
338
+
339
+ // Independent post-commit reread of the actual startup consumer for
340
+ // ONLY the selected target, plus confirmation every other currently
341
+ // enabled target in this family/scope was left untouched.
342
+ const excluded = await targetExcludedFromStartup(input);
343
+ const preserved = await otherTargetsPreserved(input, others);
344
+ if (!excluded || !preserved) {
345
+ await journal.append({ repairId, targetId, phase: "uncertain", outcome: "conflict" });
346
+ return {
347
+ status: "conflict",
348
+ reason: "startup_filter_not_confirmed",
349
+ sideEffectStarted,
350
+ targetExcludedFromStartup: excluded,
351
+ };
352
+ }
353
+ await journal.append({
354
+ repairId,
355
+ targetId,
356
+ phase: "verified",
357
+ after: { value: false },
358
+ outcome: mutationOutcome === "updated" ? "verified" : "not_needed",
359
+ });
360
+ return { status: "verified", sideEffectStarted, targetExcludedFromStartup: true, journalPath: journal.path };
361
+ } catch (error) {
362
+ try {
363
+ await journal.append({ repairId, targetId, phase: "uncertain", outcome: "uncertain" });
364
+ } catch {
365
+ // A terminal result has already been selected; the journal append
366
+ // failure is reported via the outer uncertain result below.
367
+ }
368
+ return {
369
+ status: "uncertain",
370
+ reason: error instanceof Error ? error.name : "repair_execution_unverified",
371
+ sideEffectStarted,
372
+ journalPath: journal.path,
373
+ };
374
+ } finally {
375
+ journal.close();
376
+ }
377
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ applyGjcBundleRestore,
3
+ authorizeGjcBundleRestore,
4
+ type GjcLifecycleContext,
5
+ previewGjcBundleRestore,
6
+ } from "../../extensibility/gjc-plugins/lifecycle";
7
+ import type {
8
+ GjcBundleIdentity,
9
+ GjcLifecycleResult,
10
+ GjcUpdateApplyResult,
11
+ LocalRestorePlanV1,
12
+ ReviewedRestoreTokenV1,
13
+ } from "../../extensibility/gjc-plugins/types";
14
+ import type {
15
+ MarketplaceManager,
16
+ MarketplaceRestoreApplyResult,
17
+ MarketplaceRestorePlanV1,
18
+ MarketplaceReviewedRestoreTokenV1,
19
+ } from "../../extensibility/plugins/marketplace/manager";
20
+
21
+ export interface PluginRestoreTargetV1 {
22
+ identity: GjcBundleIdentity;
23
+ cwd: string;
24
+ }
25
+
26
+ export type PluginRestoreActionV1 =
27
+ | { kind: "preview"; target: PluginRestoreTargetV1 }
28
+ | { kind: "authorize"; target: PluginRestoreTargetV1; plan: LocalRestorePlanV1; authorizations: readonly string[] }
29
+ | { kind: "apply"; target: PluginRestoreTargetV1; token: ReviewedRestoreTokenV1 }
30
+ | { kind: "marketplace-preview"; manager: MarketplaceManager; pluginId: string; scope: "user" | "project" }
31
+ | {
32
+ kind: "marketplace-authorize";
33
+ manager: MarketplaceManager;
34
+ plan: MarketplaceRestorePlanV1;
35
+ authorizations: readonly string[];
36
+ }
37
+ | { kind: "marketplace-apply"; manager: MarketplaceManager; token: MarketplaceReviewedRestoreTokenV1 };
38
+
39
+ /**
40
+ * Typed doctor dispatch surface for both restore lanes (GJC bundles and
41
+ * proven-private marketplace plugins). Every branch is action + explicit
42
+ * authorization before any effect; `action` alone never authorizes a write.
43
+ */
44
+ export type PluginRestoreActionResultV1 =
45
+ | GjcLifecycleResult<LocalRestorePlanV1 | ReviewedRestoreTokenV1 | GjcUpdateApplyResult>
46
+ | MarketplaceRestorePlanV1
47
+ | MarketplaceReviewedRestoreTokenV1
48
+ | MarketplaceRestoreApplyResult;
49
+
50
+ export async function runPluginRestoreAction(action: PluginRestoreActionV1): Promise<PluginRestoreActionResultV1> {
51
+ if (action.kind === "marketplace-preview") return action.manager.previewPluginRestore(action.pluginId, action.scope);
52
+ if (action.kind === "marketplace-authorize")
53
+ return action.manager.authorizePluginRestore(action.plan, action.authorizations);
54
+ if (action.kind === "marketplace-apply") return action.manager.applyPluginRestore(action.token);
55
+ const ctx: GjcLifecycleContext = { cwd: action.target.cwd };
56
+ if (action.kind === "preview") return previewGjcBundleRestore(ctx, action.target.identity);
57
+ if (action.kind === "authorize") return authorizeGjcBundleRestore(ctx, action.plan, action.authorizations);
58
+ return applyGjcBundleRestore(ctx, action.token);
59
+ }