@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,176 @@
|
|
|
1
|
+
import type { DoctorCheck, DoctorMode, DoctorRepair, DoctorReport, DoctorSummary, JsonValue, Verdict } from "./types";
|
|
2
|
+
|
|
3
|
+
export const DOCTOR_EXIT_CODES = {
|
|
4
|
+
ok: 0,
|
|
5
|
+
findings: 1,
|
|
6
|
+
usage: 2,
|
|
7
|
+
incomplete: 3,
|
|
8
|
+
mutationFailure: 4,
|
|
9
|
+
interrupted: 130,
|
|
10
|
+
} as const;
|
|
11
|
+
export interface FinalizeInput {
|
|
12
|
+
readonly schemaVersion?: 1;
|
|
13
|
+
readonly runId: string;
|
|
14
|
+
readonly mode: DoctorMode;
|
|
15
|
+
readonly generatedAt: string;
|
|
16
|
+
readonly durationMs: number;
|
|
17
|
+
readonly subject: DoctorReport["subject"];
|
|
18
|
+
readonly selection: DoctorReport["selection"];
|
|
19
|
+
readonly coverage: DoctorReport["coverage"];
|
|
20
|
+
readonly checks: readonly DoctorCheck[];
|
|
21
|
+
readonly repairs?: readonly DoctorRepair[];
|
|
22
|
+
readonly limits?: DoctorReport["limits"];
|
|
23
|
+
readonly invocationError?: string;
|
|
24
|
+
readonly interrupted?: boolean;
|
|
25
|
+
}
|
|
26
|
+
function sanitize(value: unknown, depth = 0): unknown {
|
|
27
|
+
if (depth > 4) return "[bounded]";
|
|
28
|
+
if (typeof value === "string") return value.replace(/[\u0000-\u001f\u007f]/g, "?").slice(0, 512);
|
|
29
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return value;
|
|
30
|
+
if (Array.isArray(value)) return value.slice(0, 100).map(item => sanitize(item, depth + 1));
|
|
31
|
+
if (typeof value === "object" && value !== null) {
|
|
32
|
+
const out: Record<string, unknown> = {};
|
|
33
|
+
for (const [k, v] of Object.entries(value).slice(0, 100)) {
|
|
34
|
+
out[k] = sanitize(v, depth + 1);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
return "[omitted]";
|
|
39
|
+
}
|
|
40
|
+
const EVIDENCE_KEYS = new Set([
|
|
41
|
+
"runtimeVersion",
|
|
42
|
+
"platform",
|
|
43
|
+
"arch",
|
|
44
|
+
"channel",
|
|
45
|
+
"schemaCoverage",
|
|
46
|
+
"startupStatus",
|
|
47
|
+
"runtimeProbed",
|
|
48
|
+
"aclCoverage",
|
|
49
|
+
"mode",
|
|
50
|
+
"ownerMatches",
|
|
51
|
+
"contentsRead",
|
|
52
|
+
"authenticationProbed",
|
|
53
|
+
"present",
|
|
54
|
+
"errno",
|
|
55
|
+
"schemaLocation",
|
|
56
|
+
"enabled",
|
|
57
|
+
"counts",
|
|
58
|
+
"fileKind",
|
|
59
|
+
"version",
|
|
60
|
+
"integrityResult",
|
|
61
|
+
"processObservation",
|
|
62
|
+
"generation",
|
|
63
|
+
"occupancy",
|
|
64
|
+
"readiness",
|
|
65
|
+
"rootId",
|
|
66
|
+
"safeLabel",
|
|
67
|
+
"candidateCount",
|
|
68
|
+
"autoload",
|
|
69
|
+
"hasAuth",
|
|
70
|
+
"status",
|
|
71
|
+
"sourceClass",
|
|
72
|
+
"artifactStatus",
|
|
73
|
+
"transport",
|
|
74
|
+
"count",
|
|
75
|
+
"warningCount",
|
|
76
|
+
]);
|
|
77
|
+
function sanitizeEvidence(value: DoctorCheck["evidence"]): DoctorCheck["evidence"] {
|
|
78
|
+
const filtered: Record<string, JsonValue> = {};
|
|
79
|
+
for (const [key, raw] of Object.entries(value))
|
|
80
|
+
if (EVIDENCE_KEYS.has(key)) filtered[key] = sanitize(raw) as JsonValue;
|
|
81
|
+
return filtered;
|
|
82
|
+
}
|
|
83
|
+
export function finalizeReport(input: FinalizeInput): DoctorReport {
|
|
84
|
+
const checks = input.checks.map(check => ({ ...check, evidence: sanitizeEvidence(check.evidence) }));
|
|
85
|
+
const repairs = (input.repairs ?? []).map(repair => ({
|
|
86
|
+
...repair,
|
|
87
|
+
candidates: repair.candidates.map(candidate => ({
|
|
88
|
+
...candidate,
|
|
89
|
+
evidence: sanitizeEvidence(candidate.evidence),
|
|
90
|
+
})),
|
|
91
|
+
}));
|
|
92
|
+
const presentCheckIds = new Set(checks.map(check => check.id));
|
|
93
|
+
const historicalCheckIds = new Set(
|
|
94
|
+
repairs
|
|
95
|
+
.filter(repair => repair.afterCheckIds.length > 0 && repair.afterCheckIds.every(id => presentCheckIds.has(id)))
|
|
96
|
+
.flatMap(repair => repair.beforeCheckIds),
|
|
97
|
+
);
|
|
98
|
+
const currentChecks = checks.filter(check => !historicalCheckIds.has(check.id));
|
|
99
|
+
let verdict: Verdict = "healthy";
|
|
100
|
+
if (input.interrupted) verdict = "inconclusive";
|
|
101
|
+
else if (
|
|
102
|
+
currentChecks.some(
|
|
103
|
+
check =>
|
|
104
|
+
check.execution === "timeout" ||
|
|
105
|
+
check.execution === "blocked" ||
|
|
106
|
+
check.execution === "unsupported" ||
|
|
107
|
+
check.health === "unknown",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
verdict = "inconclusive";
|
|
111
|
+
else if (currentChecks.some(check => check.health === "error")) verdict = "unhealthy";
|
|
112
|
+
else if (currentChecks.some(check => check.health === "warning")) verdict = "degraded";
|
|
113
|
+
const mutationFailure =
|
|
114
|
+
input.mode === "fix" &&
|
|
115
|
+
repairs.some(repair => {
|
|
116
|
+
if (repair.state === "verified" || repair.state === "not_needed") return false;
|
|
117
|
+
if (repair.sideEffectStarted === true) return true;
|
|
118
|
+
// Physical states establish effects even when an adapter incorrectly supplies a negative flag.
|
|
119
|
+
return ["pending_activation", "uncertain", "rolled_back", "rollback_conflict"].includes(repair.state);
|
|
120
|
+
});
|
|
121
|
+
if (mutationFailure) verdict = "inconclusive";
|
|
122
|
+
const incomplete = input.coverage.timedOut > 0 || input.coverage.blocked > 0 || input.coverage.unsupported > 0;
|
|
123
|
+
const readinessBlocked =
|
|
124
|
+
input.mode === "fix" && repairs.some(repair => repair.state !== "verified" && repair.state !== "not_needed");
|
|
125
|
+
const exitCode = input.interrupted
|
|
126
|
+
? DOCTOR_EXIT_CODES.interrupted
|
|
127
|
+
: input.invocationError
|
|
128
|
+
? DOCTOR_EXIT_CODES.usage
|
|
129
|
+
: mutationFailure
|
|
130
|
+
? DOCTOR_EXIT_CODES.mutationFailure
|
|
131
|
+
: incomplete || verdict === "inconclusive" || readinessBlocked
|
|
132
|
+
? DOCTOR_EXIT_CODES.incomplete
|
|
133
|
+
: verdict === "unhealthy"
|
|
134
|
+
? DOCTOR_EXIT_CODES.findings
|
|
135
|
+
: DOCTOR_EXIT_CODES.ok;
|
|
136
|
+
const summary: DoctorSummary = { verdict, exitCode };
|
|
137
|
+
return {
|
|
138
|
+
schemaVersion: 1,
|
|
139
|
+
command: "doctor",
|
|
140
|
+
runId: input.runId,
|
|
141
|
+
mode: input.mode,
|
|
142
|
+
generatedAt: input.generatedAt,
|
|
143
|
+
durationMs: input.durationMs,
|
|
144
|
+
subject: input.subject,
|
|
145
|
+
selection: input.selection,
|
|
146
|
+
coverage: input.coverage,
|
|
147
|
+
summary,
|
|
148
|
+
checks,
|
|
149
|
+
repairs,
|
|
150
|
+
limits: input.limits ?? ({} as DoctorReport["limits"]),
|
|
151
|
+
invocationError: input.invocationError,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function renderDoctorJson(report: DoctorReport): string {
|
|
155
|
+
return `${JSON.stringify(report)}\n`;
|
|
156
|
+
}
|
|
157
|
+
export function renderDoctorText(report: DoctorReport): string {
|
|
158
|
+
const lines = [
|
|
159
|
+
`gjc doctor (${report.mode})`,
|
|
160
|
+
`verdict: ${report.summary.verdict}`,
|
|
161
|
+
`exit: ${report.summary.exitCode}`,
|
|
162
|
+
`checks: ${report.checks.length}`,
|
|
163
|
+
`repairs: ${report.repairs.length}`,
|
|
164
|
+
];
|
|
165
|
+
for (const check of report.checks) {
|
|
166
|
+
lines.push(`- ${check.id}: ${check.health} (${check.execution})`);
|
|
167
|
+
lines.push(` target: ${check.targetId}`);
|
|
168
|
+
if (check.reasonCode) lines.push(` reason: ${check.reasonCode}`);
|
|
169
|
+
}
|
|
170
|
+
for (const repair of report.repairs) {
|
|
171
|
+
lines.push(`- repair ${repair.id}: ${repair.state}`);
|
|
172
|
+
lines.push(` target: ${repair.targetId}`);
|
|
173
|
+
if (repair.readiness.length) lines.push(` readiness: ${repair.readiness.join(", ")}`);
|
|
174
|
+
}
|
|
175
|
+
return `${lines.join("\n")}\n`;
|
|
176
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { VERSION } from "@gajae-code/utils/dirs";
|
|
3
|
+
import type { DoctorOptions } from "./args";
|
|
4
|
+
import { BASIC_DOCTOR_COLLECTORS, type DoctorCollector, doctorCheck } from "./checks";
|
|
5
|
+
import { createDoctorContext, type DoctorContext } from "./context";
|
|
6
|
+
import { collectPluginTargets } from "./plugin-targets";
|
|
7
|
+
import { collectDoctorProbe } from "./probe-client";
|
|
8
|
+
import { executeSelectedDoctorRepair, planSelectedDoctorRepair, refuseUnsettledDoctorRepair } from "./repairs";
|
|
9
|
+
import { finalizeReport } from "./report";
|
|
10
|
+
import { collectServiceTargets } from "./service-targets";
|
|
11
|
+
import type { DoctorCheck, DoctorCoverage, DoctorRepair, DoctorReport } from "./types";
|
|
12
|
+
|
|
13
|
+
const NATIVE_COLLECTOR: DoctorCollector = {
|
|
14
|
+
id: "native",
|
|
15
|
+
dependsOn: [],
|
|
16
|
+
timeoutMs: 5_000,
|
|
17
|
+
async collect(context) {
|
|
18
|
+
const result = await collectDoctorProbe(context, "native");
|
|
19
|
+
return [
|
|
20
|
+
doctorCheck("native.exports", `t1:native:${context.installRoot.rootId}`, {
|
|
21
|
+
execution:
|
|
22
|
+
result.status === "timeout" ? "timeout" : result.status === "cancelled" ? "cancelled" : "completed",
|
|
23
|
+
health: result.status === "completed" ? "ok" : result.status === "failed" ? "error" : "unknown",
|
|
24
|
+
evidenceLevel: "observed",
|
|
25
|
+
reasonCode: result.reasonCode,
|
|
26
|
+
evidence: { runtimeProbed: result.status === "completed" },
|
|
27
|
+
remediationIds: context.compiled ? ["install.restore-binary"] : [],
|
|
28
|
+
}),
|
|
29
|
+
];
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PROJECTION_COLLECTOR: DoctorCollector = {
|
|
34
|
+
id: "projection",
|
|
35
|
+
dependsOn: ["config"],
|
|
36
|
+
timeoutMs: 5_000,
|
|
37
|
+
async collect(context) {
|
|
38
|
+
const result = await collectDoctorProbe(context, "projection");
|
|
39
|
+
if (result.status === "completed" && result.receipt) return [...result.receipt.checks];
|
|
40
|
+
return [
|
|
41
|
+
doctorCheck("projection.coverage", `t1:projection:${context.agentRoot.rootId}`, {
|
|
42
|
+
execution:
|
|
43
|
+
result.status === "timeout" ? "timeout" : result.status === "cancelled" ? "cancelled" : "blocked",
|
|
44
|
+
health: "unknown",
|
|
45
|
+
evidenceLevel: "observed",
|
|
46
|
+
reasonCode: result.reasonCode,
|
|
47
|
+
dependsOn: ["config"],
|
|
48
|
+
}),
|
|
49
|
+
];
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const SERVICE_COLLECTOR: DoctorCollector = {
|
|
54
|
+
id: "service",
|
|
55
|
+
dependsOn: [],
|
|
56
|
+
timeoutMs: 3_000,
|
|
57
|
+
collect: collectServiceTargets,
|
|
58
|
+
};
|
|
59
|
+
const PLUGIN_COLLECTOR: DoctorCollector = {
|
|
60
|
+
id: "plugin",
|
|
61
|
+
dependsOn: [],
|
|
62
|
+
timeoutMs: 3_000,
|
|
63
|
+
collect: collectPluginTargets,
|
|
64
|
+
};
|
|
65
|
+
const COLLECTORS: readonly DoctorCollector[] = [
|
|
66
|
+
...BASIC_DOCTOR_COLLECTORS,
|
|
67
|
+
SERVICE_COLLECTOR,
|
|
68
|
+
PLUGIN_COLLECTOR,
|
|
69
|
+
NATIVE_COLLECTOR,
|
|
70
|
+
PROJECTION_COLLECTOR,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
function coverage(checks: readonly DoctorCheck[], requested: number): DoctorCoverage {
|
|
74
|
+
return {
|
|
75
|
+
requested,
|
|
76
|
+
expanded: checks.length,
|
|
77
|
+
attempted: checks.filter(check => check.execution !== "not_selected").length,
|
|
78
|
+
completed: checks.filter(check => check.execution === "completed").length,
|
|
79
|
+
blocked: checks.filter(check => check.execution === "blocked" || check.execution === "cancelled").length,
|
|
80
|
+
timedOut: checks.filter(check => check.execution === "timeout").length,
|
|
81
|
+
unsupported: checks.filter(check => check.execution === "unsupported").length,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function collectFailure(
|
|
86
|
+
context: DoctorContext,
|
|
87
|
+
collector: DoctorCollector,
|
|
88
|
+
execution: "blocked" | "cancelled" | "timeout",
|
|
89
|
+
reasonCode: string,
|
|
90
|
+
): DoctorCheck[] {
|
|
91
|
+
return [
|
|
92
|
+
doctorCheck(`${collector.id}.coverage`, `t1:inspection:${context.agentRoot.rootId}:${collector.id}`, {
|
|
93
|
+
execution,
|
|
94
|
+
health: "unknown",
|
|
95
|
+
evidenceLevel: "observed",
|
|
96
|
+
reasonCode,
|
|
97
|
+
dependsOn: collector.dependsOn,
|
|
98
|
+
}),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* One collector run plus whether its own work is over.
|
|
104
|
+
*
|
|
105
|
+
* `settled: false` means the collector never finished: it either lost the
|
|
106
|
+
* timeout race and is still running against the shared {@link DoctorContext},
|
|
107
|
+
* or it was refused before it could observe anything. Nothing here can stop an
|
|
108
|
+
* in-flight collector — a plain `collect(context)` promise is not cancellable —
|
|
109
|
+
* so the fact is carried out instead and repair planning refuses on it.
|
|
110
|
+
*/
|
|
111
|
+
interface CollectorRun {
|
|
112
|
+
readonly checks: DoctorCheck[];
|
|
113
|
+
readonly settled: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runCollector(context: DoctorContext, collector: DoctorCollector): Promise<CollectorRun> {
|
|
117
|
+
if (context.options.signal?.aborted)
|
|
118
|
+
return { checks: collectFailure(context, collector, "cancelled", "cancelled"), settled: false };
|
|
119
|
+
const started = performance.now();
|
|
120
|
+
const remaining = Math.min(collector.timeoutMs, context.deadline - started);
|
|
121
|
+
if (remaining <= 0)
|
|
122
|
+
return { checks: collectFailure(context, collector, "timeout", "deadline_exceeded"), settled: false };
|
|
123
|
+
const expired = Promise.withResolvers<{ checks: DoctorCheck[]; settled: false }>();
|
|
124
|
+
const timer = setTimeout(
|
|
125
|
+
() =>
|
|
126
|
+
expired.resolve({
|
|
127
|
+
checks: collectFailure(context, collector, "timeout", "collector_timeout"),
|
|
128
|
+
settled: false,
|
|
129
|
+
}),
|
|
130
|
+
remaining,
|
|
131
|
+
);
|
|
132
|
+
try {
|
|
133
|
+
const run = await Promise.race([
|
|
134
|
+
collector
|
|
135
|
+
.collect(context)
|
|
136
|
+
.then(checks => ({ checks, settled: true }))
|
|
137
|
+
// A rejection is still a finished collector: it is no longer writing to the context.
|
|
138
|
+
.catch(() => ({
|
|
139
|
+
checks: collectFailure(context, collector, "blocked", "collector_failed"),
|
|
140
|
+
settled: true,
|
|
141
|
+
})),
|
|
142
|
+
expired.promise,
|
|
143
|
+
]);
|
|
144
|
+
return {
|
|
145
|
+
checks: run.checks.map(check => ({ ...check, durationMs: Math.round(performance.now() - started) })),
|
|
146
|
+
settled: run.settled,
|
|
147
|
+
};
|
|
148
|
+
} finally {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function selectedCollectors(options: DoctorOptions): { collectors: DoctorCollector[]; error?: string } {
|
|
154
|
+
const wanted = new Set<string>();
|
|
155
|
+
const registry = new Map(COLLECTORS.map(collector => [collector.id, collector]));
|
|
156
|
+
const add = (id: string) => {
|
|
157
|
+
if (wanted.has(id)) return;
|
|
158
|
+
const collector = registry.get(id);
|
|
159
|
+
if (!collector) return;
|
|
160
|
+
wanted.add(id);
|
|
161
|
+
for (const dependency of collector.dependsOn) add(dependency);
|
|
162
|
+
};
|
|
163
|
+
if (options.checks.length === 0) {
|
|
164
|
+
for (const collector of COLLECTORS)
|
|
165
|
+
if (options.mode !== "dry-run" || (collector.id !== "native" && collector.id !== "projection"))
|
|
166
|
+
add(collector.id);
|
|
167
|
+
} else {
|
|
168
|
+
for (const selector of options.checks) {
|
|
169
|
+
const id = selector.split(".")[0];
|
|
170
|
+
if (!registry.has(id)) return { collectors: [], error: "unknown_check" };
|
|
171
|
+
add(id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (options.repair) {
|
|
175
|
+
const domain = options.repair.split(".")[0];
|
|
176
|
+
add(domain === "install" ? "installation" : domain);
|
|
177
|
+
}
|
|
178
|
+
return { collectors: COLLECTORS.filter(collector => wanted.has(collector.id)) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface CollectedSelection {
|
|
182
|
+
readonly checks: DoctorCheck[];
|
|
183
|
+
readonly acceptedSelectors: string[];
|
|
184
|
+
readonly invocationError?: string;
|
|
185
|
+
/** False when a selected collector timed out, was cancelled, or is still running against the context. */
|
|
186
|
+
readonly settled: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function collectSelectedChecks(context: DoctorContext): Promise<CollectedSelection> {
|
|
190
|
+
const options = context.options;
|
|
191
|
+
const selected = selectedCollectors(options);
|
|
192
|
+
const results = new Map<string, DoctorCheck[]>();
|
|
193
|
+
let settled = true;
|
|
194
|
+
const pending = [...selected.collectors];
|
|
195
|
+
while (pending.length > 0) {
|
|
196
|
+
const available = pending.filter(collector => collector.dependsOn.every(dependency => results.has(dependency)));
|
|
197
|
+
if (available.length === 0) {
|
|
198
|
+
for (const collector of pending)
|
|
199
|
+
results.set(collector.id, collectFailure(context, collector, "blocked", "dependency_unavailable"));
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
const batch = available.slice(0, 3);
|
|
203
|
+
await Promise.all(
|
|
204
|
+
batch.map(async collector => {
|
|
205
|
+
pending.splice(pending.indexOf(collector), 1);
|
|
206
|
+
const failedDependency = collector.dependsOn.some(dependency =>
|
|
207
|
+
results.get(dependency)?.some(check => check.execution !== "completed" || check.health === "error"),
|
|
208
|
+
);
|
|
209
|
+
if (failedDependency) {
|
|
210
|
+
results.set(collector.id, collectFailure(context, collector, "blocked", "dependency_failed"));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const run = await runCollector(context, collector);
|
|
214
|
+
if (!run.settled) settled = false;
|
|
215
|
+
results.set(collector.id, run.checks);
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const collectedChecks = selected.collectors.flatMap(collector => results.get(collector.id) ?? []);
|
|
220
|
+
const registryIds = new Set(COLLECTORS.map(collector => collector.id));
|
|
221
|
+
const explicitGroups = new Set(options.checks.map(selector => selector.split(".")[0]));
|
|
222
|
+
const acceptedSelectors = options.checks.filter(
|
|
223
|
+
selector => registryIds.has(selector) || collectedChecks.some(check => check.id === selector),
|
|
224
|
+
);
|
|
225
|
+
const unresolvedSelectors = options.checks.filter(selector => !acceptedSelectors.includes(selector));
|
|
226
|
+
const unresolvedInspection = unresolvedSelectors.some(selector =>
|
|
227
|
+
results.get(selector.split(".")[0])?.some(check => check.execution !== "completed"),
|
|
228
|
+
);
|
|
229
|
+
const invocationError =
|
|
230
|
+
selected.error ?? (unresolvedSelectors.length > 0 && !unresolvedInspection ? "unknown_check" : undefined);
|
|
231
|
+
const checks =
|
|
232
|
+
options.checks.length === 0
|
|
233
|
+
? collectedChecks
|
|
234
|
+
: selected.collectors.flatMap(collector => {
|
|
235
|
+
const items = results.get(collector.id) ?? [];
|
|
236
|
+
if (!explicitGroups.has(collector.id) || options.checks.includes(collector.id)) return items;
|
|
237
|
+
return items.filter(
|
|
238
|
+
check =>
|
|
239
|
+
options.checks.includes(check.id) ||
|
|
240
|
+
check.targetId === options.targetId ||
|
|
241
|
+
(unresolvedInspection && check.execution !== "completed"),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
return { checks, acceptedSelectors, invocationError, settled };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Diagnose first; only a resolved, authorized single action can enter its domain transaction. */
|
|
248
|
+
export async function collectDoctorReport(options: DoctorOptions): Promise<DoctorReport> {
|
|
249
|
+
const started = performance.now();
|
|
250
|
+
const runId = options.runId ?? randomUUID();
|
|
251
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(runId)) throw new Error("invalid_doctor_run_id");
|
|
252
|
+
const context = createDoctorContext(options);
|
|
253
|
+
const before = await collectSelectedChecks(context);
|
|
254
|
+
let checks = before.checks;
|
|
255
|
+
let currentChecks = checks;
|
|
256
|
+
let invocationError = before.invocationError;
|
|
257
|
+
const repairs: DoctorRepair[] = [];
|
|
258
|
+
// A collector that lost its timeout race is still running and still writing
|
|
259
|
+
// into this context's target map. Planning a repair against that state would
|
|
260
|
+
// bind the action to a partially-populated snapshot, so the whole repair lane
|
|
261
|
+
// is refused here; the diagnosis above still reports what was collected.
|
|
262
|
+
// `invocationError` stays untouched: this is not a usage error, so the run
|
|
263
|
+
// reports incomplete (exit 3) rather than exit 2.
|
|
264
|
+
const unsettled = before.settled ? undefined : refuseUnsettledDoctorRepair(context, checks);
|
|
265
|
+
if (unsettled) repairs.push(unsettled);
|
|
266
|
+
const plan = unsettled ? undefined : planSelectedDoctorRepair(context, checks);
|
|
267
|
+
if (plan && !invocationError) {
|
|
268
|
+
if (plan.readiness.includes("target_resolution_incomplete")) {
|
|
269
|
+
checks = [
|
|
270
|
+
...checks,
|
|
271
|
+
doctorCheck("repair.target_resolution", plan.targetId, {
|
|
272
|
+
execution: "blocked",
|
|
273
|
+
health: "unknown",
|
|
274
|
+
evidenceLevel: "not_probed",
|
|
275
|
+
reasonCode: "target_resolution_incomplete",
|
|
276
|
+
}),
|
|
277
|
+
];
|
|
278
|
+
currentChecks = checks;
|
|
279
|
+
}
|
|
280
|
+
if (plan.reasonCode === "unknown_target" || plan.reasonCode === "action_target_mismatch")
|
|
281
|
+
invocationError = plan.reasonCode;
|
|
282
|
+
if (options.mode !== "fix" || invocationError) {
|
|
283
|
+
repairs.push(plan);
|
|
284
|
+
} else {
|
|
285
|
+
const result = await executeSelectedDoctorRepair(context, runId, plan, async () => {
|
|
286
|
+
const remaining = context.deadline - performance.now();
|
|
287
|
+
if (remaining <= 0)
|
|
288
|
+
return [
|
|
289
|
+
doctorCheck("repair.postcheck", plan.targetId, {
|
|
290
|
+
execution: "timeout",
|
|
291
|
+
health: "unknown",
|
|
292
|
+
evidenceLevel: "not_probed",
|
|
293
|
+
reasonCode: "postcheck_deadline_exceeded",
|
|
294
|
+
}),
|
|
295
|
+
];
|
|
296
|
+
const freshContext = createDoctorContext({ ...options, mode: "diagnose", timeoutMs: remaining });
|
|
297
|
+
const fresh = await collectSelectedChecks(freshContext);
|
|
298
|
+
return fresh.checks;
|
|
299
|
+
});
|
|
300
|
+
invocationError = result.invocationError;
|
|
301
|
+
if (result.afterChecks?.length) {
|
|
302
|
+
const historical = before.checks.map(check => ({ ...check, id: `before.${check.id}` }));
|
|
303
|
+
currentChecks = [...result.afterChecks];
|
|
304
|
+
checks = [...historical, ...currentChecks];
|
|
305
|
+
repairs.push({
|
|
306
|
+
...result.repair,
|
|
307
|
+
beforeCheckIds: historical.map(check => check.id),
|
|
308
|
+
afterCheckIds: currentChecks.map(check => check.id),
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
repairs.push(result.repair);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return finalizeReport({
|
|
316
|
+
runId,
|
|
317
|
+
mode: options.mode,
|
|
318
|
+
generatedAt: new Date().toISOString(),
|
|
319
|
+
durationMs: Math.round(performance.now() - started),
|
|
320
|
+
subject: {
|
|
321
|
+
gjcVersion: VERSION,
|
|
322
|
+
platform: process.platform,
|
|
323
|
+
arch: process.arch,
|
|
324
|
+
channel: context.compiled ? "standalone" : "source",
|
|
325
|
+
scope: options.scope,
|
|
326
|
+
rootIds: [
|
|
327
|
+
...new Set([
|
|
328
|
+
context.agentRoot.rootId,
|
|
329
|
+
context.installRoot.rootId,
|
|
330
|
+
...context.sources.map(source => source.root.rootId),
|
|
331
|
+
]),
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
selection: { checks: before.acceptedSelectors, repair: options.repair, targetId: options.targetId },
|
|
335
|
+
coverage: coverage(currentChecks, options.checks.length || currentChecks.length),
|
|
336
|
+
checks,
|
|
337
|
+
repairs,
|
|
338
|
+
invocationError,
|
|
339
|
+
interrupted: options.signal?.aborted || repairs.some(repair => repair.reasonCode === "cancelled"),
|
|
340
|
+
limits: { fileBytes: 1024 * 1024, probeOutputBytes: 64 * 1024, timeoutMs: options.timeoutMs ?? 15_000 },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { verifyOwnerOnlyFdSecurity } from "@gajae-code/natives";
|
|
5
|
+
|
|
6
|
+
export interface DoctorSafetyResult {
|
|
7
|
+
readonly safe: boolean;
|
|
8
|
+
readonly reasonCode?: string;
|
|
9
|
+
readonly platform?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DoctorExpectedIdentity {
|
|
12
|
+
readonly dev: bigint;
|
|
13
|
+
readonly ino: bigint;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function authorizePosixConfigPermission(
|
|
17
|
+
filePath: string,
|
|
18
|
+
expected?: DoctorExpectedIdentity,
|
|
19
|
+
): Promise<DoctorSafetyResult> {
|
|
20
|
+
if (process.platform === "win32") return { safe: false, reasonCode: "unsupported", platform: process.platform };
|
|
21
|
+
let handle: fs.FileHandle | undefined;
|
|
22
|
+
try {
|
|
23
|
+
handle = await fs.open(filePath, constants.O_RDWR | constants.O_NOFOLLOW);
|
|
24
|
+
const stat = await handle.stat({ bigint: true });
|
|
25
|
+
if (!stat.isFile()) return { safe: false, reasonCode: "not_regular" };
|
|
26
|
+
if (stat.nlink !== 1n) return { safe: false, reasonCode: "hardlink" };
|
|
27
|
+
if (expected && (stat.dev !== expected.dev || stat.ino !== expected.ino))
|
|
28
|
+
return { safe: false, reasonCode: "identity_mismatch" };
|
|
29
|
+
const parent = await fs.realpath(path.dirname(filePath));
|
|
30
|
+
if (path.dirname(path.resolve(filePath)) !== parent) return { safe: false, reasonCode: "unsafe_parent" };
|
|
31
|
+
const verified = verifyOwnerOnlyFdSecurity(filePath, "file", handle.fd);
|
|
32
|
+
return verified.ok
|
|
33
|
+
? { safe: true, platform: process.platform }
|
|
34
|
+
: { safe: false, reasonCode: verified.code, platform: process.platform };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return { safe: false, reasonCode: error instanceof Error ? error.name : "unavailable" };
|
|
37
|
+
} finally {
|
|
38
|
+
await handle?.close();
|
|
39
|
+
}
|
|
40
|
+
}
|