@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.
- package/CHANGELOG.md +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- 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
|
+
}
|