@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,149 @@
|
|
|
1
|
+
import type { Stats } from "node:fs";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { NativeExactFileIdentity } from "@gajae-code/natives";
|
|
6
|
+
|
|
7
|
+
export const DOCTOR_FILE_LIMIT = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export interface DoctorFileIdentity {
|
|
10
|
+
readonly device: number;
|
|
11
|
+
readonly inode: number;
|
|
12
|
+
readonly size: number;
|
|
13
|
+
readonly modifiedMs: number;
|
|
14
|
+
readonly changedMs: number;
|
|
15
|
+
readonly mode: number;
|
|
16
|
+
readonly owner: number;
|
|
17
|
+
readonly links: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type DoctorFileObservation =
|
|
21
|
+
| {
|
|
22
|
+
readonly status: "read";
|
|
23
|
+
readonly text: string;
|
|
24
|
+
readonly identity: DoctorFileIdentity;
|
|
25
|
+
/** Private commit authority, never serialized into the public diagnostic report. */
|
|
26
|
+
readonly exactIdentity: NativeExactFileIdentity;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
readonly status: "missing" | "unreadable" | "symlink" | "not_regular" | "limit_exceeded" | "changed";
|
|
30
|
+
readonly errno?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const SAFE_ERRNOS = new Set([
|
|
34
|
+
"ENOENT",
|
|
35
|
+
"EACCES",
|
|
36
|
+
"EPERM",
|
|
37
|
+
"EISDIR",
|
|
38
|
+
"ENOTDIR",
|
|
39
|
+
"ELOOP",
|
|
40
|
+
"EMFILE",
|
|
41
|
+
"ENFILE",
|
|
42
|
+
"EIO",
|
|
43
|
+
"ESTALE",
|
|
44
|
+
"EINVAL",
|
|
45
|
+
"ENOSPC",
|
|
46
|
+
"EROFS",
|
|
47
|
+
"EBUSY",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export function doctorErrno(error: unknown): string {
|
|
51
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return "unknown_io_error";
|
|
52
|
+
return typeof error.code === "string" && SAFE_ERRNOS.has(error.code) ? error.code : "unknown_io_error";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function identity(stat: Stats): DoctorFileIdentity {
|
|
56
|
+
return {
|
|
57
|
+
device: stat.dev,
|
|
58
|
+
inode: stat.ino,
|
|
59
|
+
size: stat.size,
|
|
60
|
+
modifiedMs: stat.mtimeMs,
|
|
61
|
+
changedMs: stat.ctimeMs,
|
|
62
|
+
mode: stat.mode,
|
|
63
|
+
owner: stat.uid,
|
|
64
|
+
links: stat.nlink,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function sameDoctorFile(left: DoctorFileIdentity, right: DoctorFileIdentity): boolean {
|
|
69
|
+
return (
|
|
70
|
+
left.device === right.device &&
|
|
71
|
+
left.inode === right.inode &&
|
|
72
|
+
left.size === right.size &&
|
|
73
|
+
left.modifiedMs === right.modifiedMs &&
|
|
74
|
+
left.changedMs === right.changedMs &&
|
|
75
|
+
left.mode === right.mode &&
|
|
76
|
+
left.owner === right.owner &&
|
|
77
|
+
left.links === right.links
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Read one bounded regular file; absence is never inferred from access failure. */
|
|
82
|
+
export async function readDoctorFile(filePath: string, limit = DOCTOR_FILE_LIMIT): Promise<DoctorFileObservation> {
|
|
83
|
+
let handle: fs.FileHandle | undefined;
|
|
84
|
+
try {
|
|
85
|
+
const before = await fs.lstat(filePath);
|
|
86
|
+
if (before.isSymbolicLink()) return { status: "symlink" };
|
|
87
|
+
if (!before.isFile()) return { status: "not_regular" };
|
|
88
|
+
if (before.size > limit) return { status: "limit_exceeded" };
|
|
89
|
+
handle = await fs.open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
90
|
+
const opened = await handle.stat();
|
|
91
|
+
const openedExact = await handle.stat({ bigint: true });
|
|
92
|
+
const parent = await fs.lstat(path.dirname(filePath), { bigint: true });
|
|
93
|
+
if (parent.isSymbolicLink() || !parent.isDirectory()) return { status: "symlink" };
|
|
94
|
+
if (!opened.isFile() || !sameDoctorFile(identity(before), identity(opened))) return { status: "changed" };
|
|
95
|
+
// Read at most one byte past the bound, including when a concurrent writer grows the file.
|
|
96
|
+
const bytes = new Uint8Array(limit + 1);
|
|
97
|
+
let length = 0;
|
|
98
|
+
while (length < bytes.length) {
|
|
99
|
+
const result = await handle.read(bytes, length, bytes.length - length, length);
|
|
100
|
+
if (result.bytesRead === 0) break;
|
|
101
|
+
length += result.bytesRead;
|
|
102
|
+
}
|
|
103
|
+
if (length > limit) return { status: "limit_exceeded" };
|
|
104
|
+
const after = await handle.stat();
|
|
105
|
+
const afterExact = await handle.stat({ bigint: true });
|
|
106
|
+
const lexical = await fs.lstat(filePath);
|
|
107
|
+
const parentAfter = await fs.lstat(path.dirname(filePath), { bigint: true });
|
|
108
|
+
if (
|
|
109
|
+
lexical.isSymbolicLink() ||
|
|
110
|
+
parentAfter.isSymbolicLink() ||
|
|
111
|
+
parent.dev !== parentAfter.dev ||
|
|
112
|
+
parent.ino !== parentAfter.ino ||
|
|
113
|
+
openedExact.dev !== afterExact.dev ||
|
|
114
|
+
openedExact.ino !== afterExact.ino ||
|
|
115
|
+
openedExact.mtimeNs !== afterExact.mtimeNs ||
|
|
116
|
+
openedExact.ctimeNs !== afterExact.ctimeNs ||
|
|
117
|
+
!sameDoctorFile(identity(opened), identity(after)) ||
|
|
118
|
+
!sameDoctorFile(identity(after), identity(lexical))
|
|
119
|
+
)
|
|
120
|
+
return { status: "changed" };
|
|
121
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length));
|
|
122
|
+
return {
|
|
123
|
+
status: "read",
|
|
124
|
+
text,
|
|
125
|
+
identity: identity(after),
|
|
126
|
+
exactIdentity: {
|
|
127
|
+
dev: afterExact.dev,
|
|
128
|
+
ino: afterExact.ino,
|
|
129
|
+
nlink: afterExact.nlink,
|
|
130
|
+
size: afterExact.size,
|
|
131
|
+
mtimeNs: afterExact.mtimeNs,
|
|
132
|
+
parentDev: parent.dev,
|
|
133
|
+
parentIno: parent.ino,
|
|
134
|
+
sha256: new Bun.CryptoHasher("sha256").update(bytes.subarray(0, length)).digest("hex"),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const errno = doctorErrno(error);
|
|
139
|
+
return { status: errno === "ENOENT" ? "missing" : "unreadable", errno };
|
|
140
|
+
} finally {
|
|
141
|
+
await handle?.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function doctorMapping(value: unknown): Record<string, unknown> | undefined {
|
|
146
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
147
|
+
? (value as Record<string, unknown>)
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { DoctorScope } from "./types";
|
|
5
|
+
|
|
6
|
+
export type RootKind = "config-user" | "config-project" | "agent" | "install";
|
|
7
|
+
export interface ResolvedRoot {
|
|
8
|
+
readonly kind: RootKind;
|
|
9
|
+
readonly locator: string;
|
|
10
|
+
readonly rootId: string;
|
|
11
|
+
readonly resolution: "resolved" | "resolution_unknown";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function canonicalDigest(parts: readonly string[]): string {
|
|
15
|
+
return createHash("sha256")
|
|
16
|
+
.update(JSON.stringify(["gjc-doctor-logical-id-v1", ...parts]))
|
|
17
|
+
.digest("hex");
|
|
18
|
+
}
|
|
19
|
+
function rootToken(rootId: string): string {
|
|
20
|
+
if (!/^r[0-9a-f]{64}$/.test(rootId))
|
|
21
|
+
throw new TypeError("rootId must be r followed by 64 lowercase hexadecimal characters");
|
|
22
|
+
return rootId;
|
|
23
|
+
}
|
|
24
|
+
export function resolveDoctorRoot(kind: RootKind, locator: string): ResolvedRoot {
|
|
25
|
+
const absolute = path.resolve(locator);
|
|
26
|
+
let current = absolute;
|
|
27
|
+
let resolution: ResolvedRoot["resolution"] = "resolved";
|
|
28
|
+
const missing: string[] = [];
|
|
29
|
+
try {
|
|
30
|
+
while (true) {
|
|
31
|
+
try {
|
|
32
|
+
fs.lstatSync(current);
|
|
33
|
+
break;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
36
|
+
resolution = "resolution_unknown";
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
const parent = path.dirname(current);
|
|
40
|
+
if (parent === current) {
|
|
41
|
+
resolution = "resolution_unknown";
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
missing.unshift(path.basename(current));
|
|
45
|
+
current = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (resolution === "resolved") current = path.join(fs.realpathSync.native(current), ...missing);
|
|
49
|
+
} catch {
|
|
50
|
+
resolution = "resolution_unknown";
|
|
51
|
+
}
|
|
52
|
+
const normalizedCurrent = process.platform === "win32" ? current.replaceAll("\\", "/").toLowerCase() : current;
|
|
53
|
+
const normalizedAbsolute = process.platform === "win32" ? absolute.replaceAll("\\", "/").toLowerCase() : absolute;
|
|
54
|
+
const normalized = resolution === "resolved" ? normalizedCurrent : normalizedAbsolute;
|
|
55
|
+
return { kind, locator: normalized, rootId: `r${canonicalDigest([kind, normalized])}`, resolution };
|
|
56
|
+
}
|
|
57
|
+
export function configTargetId(rootId: string, scope: DoctorScope, schemaKey: string): string {
|
|
58
|
+
if (schemaKey !== "skills.enabled" && schemaKey !== "skills.enableSkillCommands")
|
|
59
|
+
throw new RangeError(`unsupported schema key: ${schemaKey}`);
|
|
60
|
+
return `t1:config:${rootToken(rootId)}:${scope}:${schemaKey}`;
|
|
61
|
+
}
|
|
62
|
+
export function mcpTargetId(
|
|
63
|
+
rootId: string,
|
|
64
|
+
scope: DoctorScope,
|
|
65
|
+
serverLogicalName: string,
|
|
66
|
+
field: "autoload" | "enabled",
|
|
67
|
+
): string {
|
|
68
|
+
return `t1:mcp:${rootToken(rootId)}:${scope}:n${canonicalDigest([serverLogicalName])}:${field}`;
|
|
69
|
+
}
|
|
70
|
+
export function permissionTargetId(rootId: string): string {
|
|
71
|
+
return `t1:permission:${rootToken(rootId)}:user:config-yml`;
|
|
72
|
+
}
|
|
73
|
+
export function binaryTargetId(rootId: string, locator: string): string {
|
|
74
|
+
return `t1:binary:${rootToken(rootId)}:standalone:p${canonicalDigest([locator])}`;
|
|
75
|
+
}
|
|
76
|
+
export function linkTargetId(rootId: string, linkName: string): string {
|
|
77
|
+
return `t1:link:${rootToken(rootId)}:p${canonicalDigest([linkName])}`;
|
|
78
|
+
}
|
|
79
|
+
export function pluginTargetId(
|
|
80
|
+
rootId: string,
|
|
81
|
+
family: "gjc" | "npm" | "marketplace",
|
|
82
|
+
scope: DoctorScope,
|
|
83
|
+
name: string,
|
|
84
|
+
): string {
|
|
85
|
+
return `t1:plugin:${rootToken(rootId)}:${family}:${scope}:n${canonicalDigest([name])}`;
|
|
86
|
+
}
|
|
87
|
+
export function serviceTargetId(rootId: string, service: "broker" | "telegram" | "discord" | "slack"): string {
|
|
88
|
+
return `t1:service:${rootToken(rootId)}:${service}`;
|
|
89
|
+
}
|
|
90
|
+
export function artifactTargetId(
|
|
91
|
+
rootId: string,
|
|
92
|
+
service: "broker" | "telegram" | "discord" | "slack",
|
|
93
|
+
slot: "discovery" | "owner-lock" | "startup-marker",
|
|
94
|
+
): string {
|
|
95
|
+
return `t1:artifact:${rootToken(rootId)}:${service}:${slot}`;
|
|
96
|
+
}
|
|
97
|
+
export function candidateLinkId(rootId: string, kind: "source" | "wrapper" | "binary", locator: string): string {
|
|
98
|
+
return `c1:link:${rootToken(rootId)}:${kind}:p${canonicalDigest([locator])}`;
|
|
99
|
+
}
|
|
100
|
+
export function isDoctorTargetId(value: string): boolean {
|
|
101
|
+
const digest = "[0-9a-f]{64}";
|
|
102
|
+
const root = `r${digest}`;
|
|
103
|
+
return (
|
|
104
|
+
new RegExp(`^t1:config:${root}:(user|project):(skills\\.(enabled|enableSkillCommands))$`).test(value) ||
|
|
105
|
+
new RegExp(`^t1:mcp:${root}:(user|project):n${digest}:(autoload|enabled)$`).test(value) ||
|
|
106
|
+
new RegExp(`^t1:permission:${root}:user:config-yml$`).test(value) ||
|
|
107
|
+
new RegExp(`^t1:binary:${root}:standalone:p${digest}$`).test(value) ||
|
|
108
|
+
new RegExp(`^t1:link:${root}:p${digest}$`).test(value) ||
|
|
109
|
+
new RegExp(`^t1:plugin:${root}:(gjc|npm|marketplace):(user|project):n${digest}$`).test(value) ||
|
|
110
|
+
new RegExp(`^t1:service:${root}:(broker|telegram|discord|slack)$`).test(value) ||
|
|
111
|
+
new RegExp(`^t1:artifact:${root}:(broker|telegram|discord|slack):(discovery|owner-lock|startup-marker)$`).test(
|
|
112
|
+
value,
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `install.restore-binary` (D4) domain logic: read-only descriptor plus the
|
|
3
|
+
* bounded standalone-binary restore transaction.
|
|
4
|
+
*
|
|
5
|
+
* All mutation flows through the shared `install-activation.ts` authority so
|
|
6
|
+
* `gjc doctor` and `gjc update` never maintain competing activation state:
|
|
7
|
+
* this module never calls the native `exactReplaceRetained` primitive
|
|
8
|
+
* directly, it only builds/writes the v1 record and then delegates the
|
|
9
|
+
* actual promotion (and every retry of an interrupted one) to
|
|
10
|
+
* `reconcileActivationRecord`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { realpathSync } from "node:fs";
|
|
15
|
+
import * as fs from "node:fs/promises";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { isCompiledBinary, VERSION } from "@gajae-code/utils";
|
|
18
|
+
import { isUpdateChannel } from "../../config/update-channel";
|
|
19
|
+
import {
|
|
20
|
+
activationRecordPath,
|
|
21
|
+
type DirectoryIdentity,
|
|
22
|
+
type FileIdentity,
|
|
23
|
+
readActivationRecord,
|
|
24
|
+
sameDirectoryIdentity,
|
|
25
|
+
sameFileIdentity,
|
|
26
|
+
snapshotDirectory,
|
|
27
|
+
snapshotRegularFile,
|
|
28
|
+
} from "../install-activation";
|
|
29
|
+
import {
|
|
30
|
+
acquireBinaryUpdateLock,
|
|
31
|
+
fetchAndVerifyOfficialPinnedCandidate,
|
|
32
|
+
isProtectedSourcePathForInstall,
|
|
33
|
+
replaceBinaryForUpdate,
|
|
34
|
+
smokeTestPinnedCandidate,
|
|
35
|
+
} from "../update-cli";
|
|
36
|
+
|
|
37
|
+
export type InstallSourceKind = "standalone" | "source" | "npm-wrapper" | "unknown";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The trust anchor for "this target is the exact binary the current process
|
|
41
|
+
* was dispatched from": a real ELF/PE/Mach-O header on disk proves nothing
|
|
42
|
+
* about ownership by itself. Only the currently-running compiled image's own
|
|
43
|
+
* resolved path counts. Injectable so tests can simulate a compiled dispatch
|
|
44
|
+
* without spawning a real binary.
|
|
45
|
+
*/
|
|
46
|
+
export interface InstallDispatchAuthority {
|
|
47
|
+
readonly compiled: boolean;
|
|
48
|
+
/** Realpath of the currently dispatched compiled image (process.execPath). */
|
|
49
|
+
readonly selfPath: string;
|
|
50
|
+
readonly version: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function currentInstallDispatchAuthority(): InstallDispatchAuthority {
|
|
54
|
+
let selfPath: string;
|
|
55
|
+
try {
|
|
56
|
+
selfPath = realpathSync(process.execPath);
|
|
57
|
+
} catch {
|
|
58
|
+
selfPath = path.resolve(process.execPath);
|
|
59
|
+
}
|
|
60
|
+
return { compiled: isCompiledBinary(), selfPath, version: VERSION };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface InstallRestoreDescriptorV1 {
|
|
64
|
+
readonly schemaVersion: 1;
|
|
65
|
+
readonly kind: "install.restore-binary";
|
|
66
|
+
readonly targetPath: string;
|
|
67
|
+
readonly channel: string;
|
|
68
|
+
readonly version: string;
|
|
69
|
+
readonly ref?: string;
|
|
70
|
+
readonly owned: boolean;
|
|
71
|
+
readonly sourceKind: InstallSourceKind;
|
|
72
|
+
readonly targetIdentity?: FileIdentity;
|
|
73
|
+
readonly parentIdentity?: DirectoryIdentity;
|
|
74
|
+
readonly targetDigest?: string;
|
|
75
|
+
readonly platform: NodeJS.Platform;
|
|
76
|
+
readonly arch: string;
|
|
77
|
+
readonly writes: false;
|
|
78
|
+
readonly fetch: false;
|
|
79
|
+
readonly execute: false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RestoreCandidate {
|
|
83
|
+
readonly ref: string;
|
|
84
|
+
readonly channel: string;
|
|
85
|
+
readonly sha256: string;
|
|
86
|
+
readonly version: string;
|
|
87
|
+
readonly os: NodeJS.Platform;
|
|
88
|
+
readonly arch: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface InstallRepairDependencies {
|
|
92
|
+
readonly fetchCandidate: (candidate: RestoreCandidate, stagingPath: string) => Promise<void>;
|
|
93
|
+
readonly verifyCandidate: (stagingPath: string, candidate: RestoreCandidate) => Promise<void>;
|
|
94
|
+
readonly smokeCandidate: (stagingPath: string, candidate: RestoreCandidate) => Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const DEFAULT_INSTALL_REPAIR_DEPENDENCIES: InstallRepairDependencies = {
|
|
98
|
+
fetchCandidate: fetchAndVerifyOfficialPinnedCandidate,
|
|
99
|
+
verifyCandidate: async (stagingPath, candidate) => {
|
|
100
|
+
const snapshot = await snapshotRegularFile(stagingPath);
|
|
101
|
+
if (!snapshot || snapshot.identity.sha256.toLowerCase() !== candidate.sha256.toLowerCase())
|
|
102
|
+
throw new Error("candidate_digest_mismatch");
|
|
103
|
+
},
|
|
104
|
+
smokeCandidate: (stagingPath, candidate) => smokeTestPinnedCandidate(candidate, stagingPath),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type InstallRepairState = "verified" | "not_needed" | "pending_activation" | "blocked" | "conflict" | "failed";
|
|
108
|
+
|
|
109
|
+
export interface InstallRepairResult {
|
|
110
|
+
readonly state: InstallRepairState;
|
|
111
|
+
readonly reason?: string;
|
|
112
|
+
readonly recordPath?: string;
|
|
113
|
+
readonly sideEffectStarted: boolean;
|
|
114
|
+
/** Present only for a pre-mutation Windows sharing violation (nothing was renamed). */
|
|
115
|
+
readonly windowsErrorCode?: string;
|
|
116
|
+
readonly before?: { readonly identity?: FileIdentity; readonly digest?: string };
|
|
117
|
+
readonly after?: { readonly identity?: FileIdentity; readonly digest?: string; readonly version?: string };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isNpmWrapper(targetPath: string): boolean {
|
|
121
|
+
return (
|
|
122
|
+
/\.(cmd|ps1)$/i.test(targetPath) &&
|
|
123
|
+
path
|
|
124
|
+
.basename(targetPath)
|
|
125
|
+
.replace(/\.(cmd|ps1)$/i, "")
|
|
126
|
+
.toLowerCase() === "gjc"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function classifySource(
|
|
131
|
+
targetPath: string,
|
|
132
|
+
authority: InstallDispatchAuthority,
|
|
133
|
+
targetSnapshotOk: boolean,
|
|
134
|
+
): Promise<InstallSourceKind> {
|
|
135
|
+
if (isNpmWrapper(targetPath)) return "npm-wrapper";
|
|
136
|
+
if (isProtectedSourcePathForInstall(targetPath)) return "source";
|
|
137
|
+
if (!authority.compiled || !targetSnapshotOk) return "unknown";
|
|
138
|
+
try {
|
|
139
|
+
const real = await fs.realpath(path.resolve(targetPath));
|
|
140
|
+
return real === authority.selfPath ? "standalone" : "unknown";
|
|
141
|
+
} catch {
|
|
142
|
+
return "unknown";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Read-only restore preview. Never writes, fetches, or executes anything.
|
|
148
|
+
* Tolerant of any on-disk state: a missing/symlinked/foreign target reports
|
|
149
|
+
* `sourceKind: "unknown"` with `owned: false` rather than throwing, since a
|
|
150
|
+
* dry-run/diagnostic descriptor must never fail on account of the very
|
|
151
|
+
* conditions it exists to report.
|
|
152
|
+
*/
|
|
153
|
+
export async function describeInstallRestore(
|
|
154
|
+
targetPath: string,
|
|
155
|
+
authority: InstallDispatchAuthority = currentInstallDispatchAuthority(),
|
|
156
|
+
channel = "stable",
|
|
157
|
+
ref?: string,
|
|
158
|
+
): Promise<InstallRestoreDescriptorV1> {
|
|
159
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
160
|
+
let targetSnapshot: { readonly header: Uint8Array; readonly identity: FileIdentity } | undefined;
|
|
161
|
+
try {
|
|
162
|
+
targetSnapshot = await snapshotRegularFile(resolvedTarget);
|
|
163
|
+
} catch {
|
|
164
|
+
targetSnapshot = undefined;
|
|
165
|
+
}
|
|
166
|
+
let parentIdentity: DirectoryIdentity | undefined;
|
|
167
|
+
try {
|
|
168
|
+
parentIdentity = await snapshotDirectory(path.dirname(resolvedTarget));
|
|
169
|
+
} catch {
|
|
170
|
+
parentIdentity = undefined;
|
|
171
|
+
}
|
|
172
|
+
const sourceKind = await classifySource(resolvedTarget, authority, targetSnapshot !== undefined);
|
|
173
|
+
return {
|
|
174
|
+
schemaVersion: 1,
|
|
175
|
+
kind: "install.restore-binary",
|
|
176
|
+
targetPath: resolvedTarget,
|
|
177
|
+
channel,
|
|
178
|
+
version: authority.version,
|
|
179
|
+
...(ref ? { ref } : {}),
|
|
180
|
+
owned: sourceKind === "standalone",
|
|
181
|
+
sourceKind,
|
|
182
|
+
targetIdentity: targetSnapshot?.identity,
|
|
183
|
+
parentIdentity,
|
|
184
|
+
targetDigest: targetSnapshot?.identity.sha256,
|
|
185
|
+
platform: process.platform,
|
|
186
|
+
arch: process.arch,
|
|
187
|
+
writes: false,
|
|
188
|
+
fetch: false,
|
|
189
|
+
execute: false,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Repair a proven-standalone target by staging, verifying, and promoting a
|
|
195
|
+
* pinned candidate through the shared activation authority.
|
|
196
|
+
*
|
|
197
|
+
* An unresolved transaction is resumed only for the identical explicit pin.
|
|
198
|
+
* Both fresh and resumed publication use the normal updater's mandatory
|
|
199
|
+
* installed verification and identity-bound compensation; no second candidate
|
|
200
|
+
* is activated implicitly.
|
|
201
|
+
*/
|
|
202
|
+
export async function repairStandaloneBinary(
|
|
203
|
+
targetPath: string,
|
|
204
|
+
descriptor: InstallRestoreDescriptorV1,
|
|
205
|
+
candidate: RestoreCandidate,
|
|
206
|
+
authorizations: readonly string[],
|
|
207
|
+
deps: InstallRepairDependencies = DEFAULT_INSTALL_REPAIR_DEPENDENCIES,
|
|
208
|
+
): Promise<InstallRepairResult> {
|
|
209
|
+
const before = { identity: descriptor.targetIdentity, digest: descriptor.targetDigest };
|
|
210
|
+
const recordPath = activationRecordPath(targetPath);
|
|
211
|
+
const blocked = (reason: string): InstallRepairResult => ({
|
|
212
|
+
state: "blocked",
|
|
213
|
+
reason,
|
|
214
|
+
sideEffectStarted: false,
|
|
215
|
+
before,
|
|
216
|
+
recordPath,
|
|
217
|
+
});
|
|
218
|
+
if (
|
|
219
|
+
!authorizations.includes("install-replace") ||
|
|
220
|
+
!authorizations.includes("network") ||
|
|
221
|
+
!authorizations.includes("external-execution")
|
|
222
|
+
)
|
|
223
|
+
return blocked("authorization_missing");
|
|
224
|
+
if (!isUpdateChannel(candidate.channel) || candidate.os !== process.platform || candidate.arch !== process.arch)
|
|
225
|
+
return blocked("candidate_platform_or_channel_mismatch");
|
|
226
|
+
if (!/^[a-f0-9]{64}$/i.test(candidate.sha256)) return blocked("candidate_digest_invalid");
|
|
227
|
+
if (
|
|
228
|
+
!descriptor.owned ||
|
|
229
|
+
descriptor.sourceKind !== "standalone" ||
|
|
230
|
+
descriptor.targetPath !== path.resolve(targetPath)
|
|
231
|
+
)
|
|
232
|
+
return blocked("source_not_owned_standalone");
|
|
233
|
+
if (
|
|
234
|
+
candidate.version !== descriptor.version ||
|
|
235
|
+
candidate.channel !== descriptor.channel ||
|
|
236
|
+
(descriptor.ref && candidate.ref !== descriptor.ref)
|
|
237
|
+
)
|
|
238
|
+
return blocked("candidate_provenance_mismatch");
|
|
239
|
+
if (!descriptor.targetIdentity || !descriptor.parentIdentity || !descriptor.targetDigest)
|
|
240
|
+
return blocked("original_snapshot_incomplete");
|
|
241
|
+
const current = await snapshotRegularFile(targetPath);
|
|
242
|
+
const parent = await snapshotDirectory(path.dirname(path.resolve(targetPath)));
|
|
243
|
+
if (
|
|
244
|
+
!current ||
|
|
245
|
+
!sameFileIdentity(current.identity, descriptor.targetIdentity) ||
|
|
246
|
+
!sameDirectoryIdentity(parent, descriptor.parentIdentity)
|
|
247
|
+
)
|
|
248
|
+
return { ...blocked("target_or_parent_changed"), state: "conflict" };
|
|
249
|
+
const observedRecord = await readActivationRecord(targetPath);
|
|
250
|
+
if (observedRecord.status === "malformed" || observedRecord.status === "foreign")
|
|
251
|
+
return blocked("activation_record_untrusted");
|
|
252
|
+
const pending =
|
|
253
|
+
observedRecord.status === "valid" &&
|
|
254
|
+
observedRecord.record.phase !== "verified" &&
|
|
255
|
+
observedRecord.record.phase !== "rolled_back";
|
|
256
|
+
if (!pending && descriptor.targetDigest === candidate.sha256.toLowerCase())
|
|
257
|
+
return {
|
|
258
|
+
state: "not_needed",
|
|
259
|
+
sideEffectStarted: false,
|
|
260
|
+
before,
|
|
261
|
+
after: { identity: current.identity, digest: current.identity.sha256, version: candidate.version },
|
|
262
|
+
recordPath,
|
|
263
|
+
};
|
|
264
|
+
if (
|
|
265
|
+
pending &&
|
|
266
|
+
observedRecord.status === "valid" &&
|
|
267
|
+
(observedRecord.record.candidate.digest !== candidate.sha256.toLowerCase() ||
|
|
268
|
+
observedRecord.record.candidate.version !== candidate.version ||
|
|
269
|
+
observedRecord.record.candidate.ref !== candidate.ref ||
|
|
270
|
+
observedRecord.record.candidate.channel !== candidate.channel)
|
|
271
|
+
)
|
|
272
|
+
return blocked("candidate_provenance_mismatch");
|
|
273
|
+
const release = await acquireBinaryUpdateLock(targetPath);
|
|
274
|
+
let started = false;
|
|
275
|
+
try {
|
|
276
|
+
const rechecked = await snapshotRegularFile(targetPath);
|
|
277
|
+
const parentNow = await snapshotDirectory(path.dirname(path.resolve(targetPath)));
|
|
278
|
+
if (
|
|
279
|
+
!rechecked ||
|
|
280
|
+
!sameFileIdentity(rechecked.identity, descriptor.targetIdentity) ||
|
|
281
|
+
!sameDirectoryIdentity(parentNow, descriptor.parentIdentity)
|
|
282
|
+
)
|
|
283
|
+
return { ...blocked("target_or_parent_changed"), state: "conflict" };
|
|
284
|
+
const stagingPath =
|
|
285
|
+
pending && observedRecord.status === "valid"
|
|
286
|
+
? observedRecord.record.stagingPath
|
|
287
|
+
: `${path.resolve(targetPath)}.restore.${randomUUID()}`;
|
|
288
|
+
started = true;
|
|
289
|
+
if (!pending) await deps.fetchCandidate(candidate, stagingPath);
|
|
290
|
+
const verification = await replaceBinaryForUpdate({
|
|
291
|
+
targetPath,
|
|
292
|
+
tempPath: stagingPath,
|
|
293
|
+
backupPath: `${path.resolve(targetPath)}.restore-backup.${randomUUID()}`,
|
|
294
|
+
expectedVersion: candidate.version,
|
|
295
|
+
originalTarget: descriptor.targetIdentity,
|
|
296
|
+
originalParent: descriptor.parentIdentity,
|
|
297
|
+
candidate: { channel: candidate.channel, ref: candidate.ref, sha256: candidate.sha256.toLowerCase() },
|
|
298
|
+
verifyStagedVersion: async file => {
|
|
299
|
+
await deps.verifyCandidate(file, candidate);
|
|
300
|
+
await deps.smokeCandidate(file, candidate);
|
|
301
|
+
},
|
|
302
|
+
verifyInstalledVersion: async version => {
|
|
303
|
+
await deps.verifyCandidate(targetPath, candidate);
|
|
304
|
+
await deps.smokeCandidate(targetPath, candidate);
|
|
305
|
+
return { ok: true, actual: version, path: targetPath };
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
const after = await snapshotRegularFile(targetPath);
|
|
309
|
+
if (!verification.ok || !after || after.identity.sha256 !== candidate.sha256.toLowerCase())
|
|
310
|
+
return { state: "failed", reason: "postcheck_failed", sideEffectStarted: true, before, recordPath };
|
|
311
|
+
return {
|
|
312
|
+
state: "verified",
|
|
313
|
+
sideEffectStarted: true,
|
|
314
|
+
before,
|
|
315
|
+
after: { identity: after.identity, digest: after.identity.sha256, version: candidate.version },
|
|
316
|
+
recordPath,
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const pendingActivation = error instanceof Error && error.message === "installation_pending_activation";
|
|
320
|
+
const reason = pendingActivation
|
|
321
|
+
? "sharing_violation"
|
|
322
|
+
: error instanceof Error && /^[a-z][a-z0-9_]{0,63}$/.test(error.message)
|
|
323
|
+
? error.message
|
|
324
|
+
: "installation_execution_unverified";
|
|
325
|
+
return {
|
|
326
|
+
state: pendingActivation ? "pending_activation" : started ? "failed" : "blocked",
|
|
327
|
+
reason,
|
|
328
|
+
sideEffectStarted: started,
|
|
329
|
+
before,
|
|
330
|
+
recordPath,
|
|
331
|
+
};
|
|
332
|
+
} finally {
|
|
333
|
+
await release();
|
|
334
|
+
}
|
|
335
|
+
}
|