@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,423 @@
|
|
|
1
|
+
import { constants, type Dir } from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { DoctorJournalAuthority, verifyOwnerOnlyFdSecurity } from "@gajae-code/natives";
|
|
5
|
+
import { loadNative as loadNativeBindings } from "../../../../natives/native/loader-state.js";
|
|
6
|
+
import { isDoctorAction } from "./args";
|
|
7
|
+
import { doctorErrno, readDoctorFile } from "./files";
|
|
8
|
+
import { isDoctorTargetId } from "./ids";
|
|
9
|
+
import type { DoctorCheck } from "./types";
|
|
10
|
+
|
|
11
|
+
export type DoctorJournalPhase =
|
|
12
|
+
| "created"
|
|
13
|
+
| "before"
|
|
14
|
+
| "prepared"
|
|
15
|
+
| "applying"
|
|
16
|
+
| "applied_unverified"
|
|
17
|
+
| "verified"
|
|
18
|
+
| "failed"
|
|
19
|
+
| "uncertain";
|
|
20
|
+
export interface DoctorJournalRecord {
|
|
21
|
+
readonly runId: string;
|
|
22
|
+
readonly repairId: string;
|
|
23
|
+
readonly phase: DoctorJournalPhase;
|
|
24
|
+
readonly targetId: string;
|
|
25
|
+
readonly before?: Record<string, unknown>;
|
|
26
|
+
readonly after?: Record<string, unknown>;
|
|
27
|
+
readonly outcome?: string;
|
|
28
|
+
readonly at: string;
|
|
29
|
+
}
|
|
30
|
+
export type DoctorJournalReadStatus = "ok" | "missing" | "malformed" | "unreadable" | "unsupported" | "changed";
|
|
31
|
+
export interface DoctorJournalReadResult {
|
|
32
|
+
readonly status: DoctorJournalReadStatus;
|
|
33
|
+
readonly records: readonly DoctorJournalRecord[];
|
|
34
|
+
readonly malformedLines: readonly number[];
|
|
35
|
+
readonly error?: string;
|
|
36
|
+
}
|
|
37
|
+
export type DoctorReconcileOutcome = "not_applied" | "applied_unverified" | "conflict" | "verified" | "unknown";
|
|
38
|
+
export interface DoctorReconcileEvidence {
|
|
39
|
+
readonly check: DoctorCheck;
|
|
40
|
+
readonly current: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type JournalBindings = {
|
|
44
|
+
DoctorJournalAuthority: typeof DoctorJournalAuthority;
|
|
45
|
+
verifyOwnerOnlyFdSecurity: typeof verifyOwnerOnlyFdSecurity;
|
|
46
|
+
};
|
|
47
|
+
const MAX_BYTES = 128 * 1024;
|
|
48
|
+
const MAX_RECORDS = 64;
|
|
49
|
+
const MAX_UNRESOLVED = 20;
|
|
50
|
+
const MAX_HISTORY = 256;
|
|
51
|
+
const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
52
|
+
const PHASES = new Set<DoctorJournalPhase>([
|
|
53
|
+
"created",
|
|
54
|
+
"before",
|
|
55
|
+
"prepared",
|
|
56
|
+
"applying",
|
|
57
|
+
"applied_unverified",
|
|
58
|
+
"verified",
|
|
59
|
+
"failed",
|
|
60
|
+
"uncertain",
|
|
61
|
+
]);
|
|
62
|
+
const DECIMAL_KEYS = new Set([
|
|
63
|
+
"dev",
|
|
64
|
+
"ino",
|
|
65
|
+
"uid",
|
|
66
|
+
"gid",
|
|
67
|
+
"nlink",
|
|
68
|
+
"size",
|
|
69
|
+
"mode",
|
|
70
|
+
"parentDev",
|
|
71
|
+
"parentIno",
|
|
72
|
+
"mtimeNs",
|
|
73
|
+
]);
|
|
74
|
+
const TYPE_VALUES = new Set([
|
|
75
|
+
"file",
|
|
76
|
+
"directory",
|
|
77
|
+
"symlink",
|
|
78
|
+
"missing",
|
|
79
|
+
"present",
|
|
80
|
+
"boolean",
|
|
81
|
+
"null",
|
|
82
|
+
"string",
|
|
83
|
+
"number",
|
|
84
|
+
"object",
|
|
85
|
+
"array",
|
|
86
|
+
]);
|
|
87
|
+
const OUTCOMES = new Set([
|
|
88
|
+
"created",
|
|
89
|
+
"verified",
|
|
90
|
+
"verified_with_blocker",
|
|
91
|
+
"failed",
|
|
92
|
+
"uncertain",
|
|
93
|
+
"not_applied",
|
|
94
|
+
"not_needed",
|
|
95
|
+
"applied_unverified",
|
|
96
|
+
"conflict",
|
|
97
|
+
"rolled_back",
|
|
98
|
+
"rollback_conflict",
|
|
99
|
+
"cancelled",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
function bindings(): JournalBindings {
|
|
103
|
+
return loadNativeBindings() as JournalBindings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function safeRecord(value: unknown): Record<string, unknown> | undefined {
|
|
107
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
108
|
+
const result: Record<string, unknown> = {};
|
|
109
|
+
for (const [key, item] of Object.entries(value)) {
|
|
110
|
+
if ((key === "value" || key === "exists") && (typeof item === "boolean" || item === null)) result[key] = item;
|
|
111
|
+
else if (DECIMAL_KEYS.has(key)) {
|
|
112
|
+
if (typeof item === "number" && Number.isSafeInteger(item) && item >= 0) result[key] = item;
|
|
113
|
+
else if (typeof item === "string" && /^-?(?:0|[1-9]\d{0,19})$/.test(item)) {
|
|
114
|
+
const number = BigInt(item);
|
|
115
|
+
if (number >= (key === "mtimeNs" ? -9223372036854775808n : 0n) && number <= 18446744073709551615n)
|
|
116
|
+
result[key] = item;
|
|
117
|
+
}
|
|
118
|
+
} else if (key === "type" && typeof item === "string" && TYPE_VALUES.has(item)) result[key] = item;
|
|
119
|
+
else if (key === "reason" && typeof item === "string" && OUTCOMES.has(item)) result[key] = item;
|
|
120
|
+
else if (
|
|
121
|
+
key === "quarantineName" &&
|
|
122
|
+
typeof item === "string" &&
|
|
123
|
+
/^[A-Za-z0-9._-]{1,128}$/.test(item) &&
|
|
124
|
+
item !== "." &&
|
|
125
|
+
item !== ".."
|
|
126
|
+
)
|
|
127
|
+
result[key] = item;
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateRecord(value: unknown): DoctorJournalRecord | undefined {
|
|
133
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
134
|
+
const record = value as Record<string, unknown>;
|
|
135
|
+
if (
|
|
136
|
+
typeof record.runId !== "string" ||
|
|
137
|
+
!RUN_ID.test(record.runId) ||
|
|
138
|
+
typeof record.phase !== "string" ||
|
|
139
|
+
!PHASES.has(record.phase as DoctorJournalPhase) ||
|
|
140
|
+
typeof record.at !== "string" ||
|
|
141
|
+
record.at.length > 32
|
|
142
|
+
)
|
|
143
|
+
return undefined;
|
|
144
|
+
const time = Date.parse(record.at);
|
|
145
|
+
if (!Number.isFinite(time) || new Date(time).toISOString() !== record.at) return undefined;
|
|
146
|
+
const header = record.phase === "created" && record.repairId === "" && record.targetId === "";
|
|
147
|
+
if (
|
|
148
|
+
!header &&
|
|
149
|
+
(!isDoctorAction(record.repairId) || typeof record.targetId !== "string" || !isDoctorTargetId(record.targetId))
|
|
150
|
+
)
|
|
151
|
+
return undefined;
|
|
152
|
+
for (const key of ["before", "after"] as const) {
|
|
153
|
+
if (record[key] !== undefined && JSON.stringify(record[key]) !== JSON.stringify(safeRecord(record[key])))
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
if (record.outcome !== undefined && (typeof record.outcome !== "string" || !OUTCOMES.has(record.outcome)))
|
|
157
|
+
return undefined;
|
|
158
|
+
return {
|
|
159
|
+
runId: record.runId,
|
|
160
|
+
repairId: record.repairId as string,
|
|
161
|
+
targetId: record.targetId as string,
|
|
162
|
+
phase: record.phase as DoctorJournalPhase,
|
|
163
|
+
at: record.at,
|
|
164
|
+
...(record.before !== undefined ? { before: safeRecord(record.before) } : {}),
|
|
165
|
+
...(record.after !== undefined ? { after: safeRecord(record.after) } : {}),
|
|
166
|
+
...(typeof record.outcome === "string" ? { outcome: record.outcome } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export class DoctorJournalCreateError extends Error {
|
|
171
|
+
constructor(
|
|
172
|
+
readonly reasonCode: string,
|
|
173
|
+
readonly sideEffectStarted: boolean,
|
|
174
|
+
) {
|
|
175
|
+
super("Doctor journal creation failed");
|
|
176
|
+
this.name = "DoctorJournalCreateError";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function checkHistory(root: string): Promise<void> {
|
|
181
|
+
const directory = path.join(root, "doctor", "repairs");
|
|
182
|
+
let handle: Dir;
|
|
183
|
+
try {
|
|
184
|
+
const stat = await fs.lstat(directory);
|
|
185
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
186
|
+
throw new DoctorJournalCreateError("unsafe_journal_history", false);
|
|
187
|
+
handle = await fs.opendir(directory, { bufferSize: 16 });
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (doctorErrno(error) === "ENOENT") return;
|
|
190
|
+
if (error instanceof DoctorJournalCreateError) throw error;
|
|
191
|
+
throw new DoctorJournalCreateError("journal_history_unreadable", false);
|
|
192
|
+
}
|
|
193
|
+
let scanned = 0;
|
|
194
|
+
let unresolved = 0;
|
|
195
|
+
for await (const entry of handle) {
|
|
196
|
+
if (++scanned > MAX_HISTORY) throw new DoctorJournalCreateError("journal_history_limit", false);
|
|
197
|
+
if (!entry.isDirectory() || !RUN_ID.test(entry.name))
|
|
198
|
+
throw new DoctorJournalCreateError("unsafe_journal_history", false);
|
|
199
|
+
const read = await DoctorJournal.readDetailed(path.join(directory, entry.name, "journal.ndjson"));
|
|
200
|
+
const last = read.records.at(-1);
|
|
201
|
+
if (read.status !== "ok" || !last || (last.phase !== "verified" && last.phase !== "failed")) unresolved++;
|
|
202
|
+
if (unresolved >= MAX_UNRESOLVED) throw new DoctorJournalCreateError("unresolved_journal_limit", false);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export class DoctorJournal {
|
|
207
|
+
readonly path: string;
|
|
208
|
+
readonly #runId: string;
|
|
209
|
+
readonly #native: DoctorJournalAuthority;
|
|
210
|
+
#closed = false;
|
|
211
|
+
#records = 0;
|
|
212
|
+
#selection: string | undefined;
|
|
213
|
+
|
|
214
|
+
constructor(pathname: string, runId: string, native: DoctorJournalAuthority) {
|
|
215
|
+
this.path = pathname;
|
|
216
|
+
this.#runId = runId;
|
|
217
|
+
this.#native = native;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
static async create(root: string, runId: string): Promise<DoctorJournal> {
|
|
221
|
+
if (!RUN_ID.test(runId)) throw new DoctorJournalCreateError("invalid_run_id", false);
|
|
222
|
+
await checkHistory(root);
|
|
223
|
+
let native: JournalBindings;
|
|
224
|
+
try {
|
|
225
|
+
native = bindings();
|
|
226
|
+
} catch {
|
|
227
|
+
throw new DoctorJournalCreateError("native_unavailable", false);
|
|
228
|
+
}
|
|
229
|
+
if (typeof native.DoctorJournalAuthority?.createExact !== "function")
|
|
230
|
+
throw new DoctorJournalCreateError("native_unavailable", false);
|
|
231
|
+
const result = native.DoctorJournalAuthority.createExact(path.resolve(root), runId);
|
|
232
|
+
if (!result || typeof result.sideEffectStarted !== "boolean")
|
|
233
|
+
throw new DoctorJournalCreateError("native_contract_unknown", true);
|
|
234
|
+
if (!result.authority)
|
|
235
|
+
throw new DoctorJournalCreateError(result.reasonCode ?? "native_create_failed", result.sideEffectStarted);
|
|
236
|
+
const journal = new DoctorJournal(
|
|
237
|
+
path.join(root, "doctor", "repairs", runId, "journal.ndjson"),
|
|
238
|
+
runId,
|
|
239
|
+
result.authority,
|
|
240
|
+
);
|
|
241
|
+
try {
|
|
242
|
+
await journal.append({ repairId: "", phase: "created", targetId: "", outcome: "created" });
|
|
243
|
+
return journal;
|
|
244
|
+
} catch {
|
|
245
|
+
journal.close();
|
|
246
|
+
throw new DoctorJournalCreateError("journal_initial_append_failed", true);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async append(input: Omit<DoctorJournalRecord, "runId" | "at">): Promise<void> {
|
|
251
|
+
if (this.#closed || this.#records >= MAX_RECORDS) throw new Error("journal_not_writable");
|
|
252
|
+
const record: DoctorJournalRecord = {
|
|
253
|
+
runId: this.#runId,
|
|
254
|
+
repairId: input.repairId,
|
|
255
|
+
targetId: input.targetId,
|
|
256
|
+
phase: input.phase,
|
|
257
|
+
at: new Date().toISOString(),
|
|
258
|
+
before: safeRecord(input.before),
|
|
259
|
+
after: safeRecord(input.after),
|
|
260
|
+
...(input.outcome && OUTCOMES.has(input.outcome) ? { outcome: input.outcome } : {}),
|
|
261
|
+
};
|
|
262
|
+
if (!validateRecord(record)) throw new Error("invalid_journal_record");
|
|
263
|
+
if (record.phase === "created" && this.#records !== 0) throw new Error("duplicate_journal_header");
|
|
264
|
+
if (record.phase !== "created") {
|
|
265
|
+
const selection = `${record.repairId}\0${record.targetId}`;
|
|
266
|
+
if (this.#selection !== undefined && this.#selection !== selection)
|
|
267
|
+
throw new Error("journal_selection_changed");
|
|
268
|
+
this.#selection = selection;
|
|
269
|
+
}
|
|
270
|
+
const line = JSON.stringify(record);
|
|
271
|
+
if (Buffer.byteLength(line) + 1 > MAX_BYTES) throw new Error("journal_record_limit");
|
|
272
|
+
this.#native.append(line);
|
|
273
|
+
this.#records++;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
close(): void {
|
|
277
|
+
if (this.#closed) return;
|
|
278
|
+
this.#closed = true;
|
|
279
|
+
this.#native.close();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
static async read(pathname: string): Promise<DoctorJournalRecord[]> {
|
|
283
|
+
return [...(await DoctorJournal.readDetailed(pathname)).records];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static async readDetailed(pathname: string): Promise<DoctorJournalReadResult> {
|
|
287
|
+
const empty = { records: [], malformedLines: [] } as const;
|
|
288
|
+
const observation = await readDoctorFile(pathname, MAX_BYTES);
|
|
289
|
+
if (observation.status === "missing") return { ...empty, status: "missing" };
|
|
290
|
+
if (observation.status === "unreadable") return { ...empty, status: "unreadable", error: observation.errno };
|
|
291
|
+
if (observation.status === "changed") return { ...empty, status: "changed" };
|
|
292
|
+
if (observation.status !== "read") return { ...empty, status: "unsupported" };
|
|
293
|
+
const run = path.dirname(pathname);
|
|
294
|
+
const repairs = path.dirname(run);
|
|
295
|
+
const doctor = path.dirname(repairs);
|
|
296
|
+
if (
|
|
297
|
+
path.basename(pathname) !== "journal.ndjson" ||
|
|
298
|
+
path.basename(repairs) !== "repairs" ||
|
|
299
|
+
path.basename(doctor) !== "doctor" ||
|
|
300
|
+
!RUN_ID.test(path.basename(run))
|
|
301
|
+
)
|
|
302
|
+
return { ...empty, status: "unsupported" };
|
|
303
|
+
try {
|
|
304
|
+
const native = bindings();
|
|
305
|
+
for (const directory of [doctor, repairs, run]) {
|
|
306
|
+
const fd = await fs.open(
|
|
307
|
+
directory,
|
|
308
|
+
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
309
|
+
);
|
|
310
|
+
try {
|
|
311
|
+
if (!native.verifyOwnerOnlyFdSecurity(directory, "directory", fd.fd).ok)
|
|
312
|
+
return { ...empty, status: "unsupported" };
|
|
313
|
+
} finally {
|
|
314
|
+
await fd.close();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const fd = await fs.open(pathname, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
318
|
+
try {
|
|
319
|
+
const stat = await fd.stat({ bigint: true });
|
|
320
|
+
if (
|
|
321
|
+
stat.dev !== observation.exactIdentity.dev ||
|
|
322
|
+
stat.ino !== observation.exactIdentity.ino ||
|
|
323
|
+
!native.verifyOwnerOnlyFdSecurity(pathname, "file", fd.fd).ok
|
|
324
|
+
)
|
|
325
|
+
return { ...empty, status: "changed" };
|
|
326
|
+
} finally {
|
|
327
|
+
await fd.close();
|
|
328
|
+
}
|
|
329
|
+
const after = await readDoctorFile(pathname, MAX_BYTES);
|
|
330
|
+
if (
|
|
331
|
+
after.status !== "read" ||
|
|
332
|
+
after.exactIdentity.dev !== observation.exactIdentity.dev ||
|
|
333
|
+
after.exactIdentity.ino !== observation.exactIdentity.ino ||
|
|
334
|
+
after.exactIdentity.sha256 !== observation.exactIdentity.sha256 ||
|
|
335
|
+
after.exactIdentity.parentDev !== observation.exactIdentity.parentDev ||
|
|
336
|
+
after.exactIdentity.parentIno !== observation.exactIdentity.parentIno
|
|
337
|
+
)
|
|
338
|
+
return { ...empty, status: "changed" };
|
|
339
|
+
} catch (error) {
|
|
340
|
+
return { ...empty, status: "unreadable", error: doctorErrno(error) };
|
|
341
|
+
}
|
|
342
|
+
const records: DoctorJournalRecord[] = [];
|
|
343
|
+
const malformedLines: number[] = [];
|
|
344
|
+
let selection: string | undefined;
|
|
345
|
+
const lines = observation.text.split("\n");
|
|
346
|
+
for (const [index, line] of lines.entries()) {
|
|
347
|
+
if (!line && index === lines.length - 1) continue;
|
|
348
|
+
try {
|
|
349
|
+
const record = validateRecord(JSON.parse(line));
|
|
350
|
+
if (
|
|
351
|
+
!record ||
|
|
352
|
+
record.runId !== path.basename(run) ||
|
|
353
|
+
index >= MAX_RECORDS ||
|
|
354
|
+
(index === 0 ? record.phase !== "created" : record.phase === "created") ||
|
|
355
|
+
index === lines.length - 1
|
|
356
|
+
)
|
|
357
|
+
throw new Error("invalid_record");
|
|
358
|
+
if (record.phase !== "created") {
|
|
359
|
+
const current = `${record.repairId}\0${record.targetId}`;
|
|
360
|
+
if (selection !== undefined && selection !== current) throw new Error("selection_changed");
|
|
361
|
+
selection = current;
|
|
362
|
+
}
|
|
363
|
+
records.push(record);
|
|
364
|
+
} catch {
|
|
365
|
+
malformedLines.push(index + 1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { status: malformedLines.length ? "malformed" : "ok", records, malformedLines };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function comparable(value: Record<string, unknown> | undefined): value is Record<string, unknown> {
|
|
373
|
+
return (
|
|
374
|
+
!!value &&
|
|
375
|
+
(typeof value.value === "boolean" ||
|
|
376
|
+
typeof value.mode === "number" ||
|
|
377
|
+
value.exists === false ||
|
|
378
|
+
["dev", "ino", "parentDev", "parentIno"].every(key => typeof value[key] === "string"))
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Classifies fresh selected-target evidence, never the historical phase by itself. */
|
|
383
|
+
export function reconcileDoctorEvidence(
|
|
384
|
+
record: DoctorJournalRecord,
|
|
385
|
+
evidence?: DoctorReconcileEvidence,
|
|
386
|
+
): DoctorReconcileOutcome {
|
|
387
|
+
if (
|
|
388
|
+
!evidence ||
|
|
389
|
+
evidence.check.targetId !== record.targetId ||
|
|
390
|
+
evidence.check.execution !== "completed" ||
|
|
391
|
+
evidence.check.evidenceLevel !== "observed"
|
|
392
|
+
)
|
|
393
|
+
return "unknown";
|
|
394
|
+
const current = safeRecord(evidence.current);
|
|
395
|
+
if (!comparable(current)) return "unknown";
|
|
396
|
+
if (typeof current.value === "boolean") {
|
|
397
|
+
const field = record.targetId.endsWith(":autoload") ? "autoload" : "enabled";
|
|
398
|
+
if (evidence.check.evidence[field] !== current.value) return "unknown";
|
|
399
|
+
}
|
|
400
|
+
if (typeof current.exists === "boolean" && evidence.check.evidence.present !== current.exists) return "unknown";
|
|
401
|
+
if (
|
|
402
|
+
typeof current.mode === "number" &&
|
|
403
|
+
(evidence.check.evidence.mode !== (current.mode & 0o777) ||
|
|
404
|
+
evidence.check.evidence.aclCoverage !== "verified" ||
|
|
405
|
+
evidence.check.evidence.ownerMatches !== true)
|
|
406
|
+
)
|
|
407
|
+
return "unknown";
|
|
408
|
+
const equal = (expected: Record<string, unknown> | undefined): boolean =>
|
|
409
|
+
comparable(expected) && Object.entries(expected).every(([key, value]) => current[key] === value);
|
|
410
|
+
if (equal(record.after)) return "verified";
|
|
411
|
+
if (equal(record.before)) return "not_applied";
|
|
412
|
+
return comparable(record.before) || comparable(record.after) ? "conflict" : "unknown";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function reconcileDoctorJournal(
|
|
416
|
+
pathname: string,
|
|
417
|
+
evidence?: DoctorReconcileEvidence,
|
|
418
|
+
): Promise<DoctorJournalRecord | undefined> {
|
|
419
|
+
const result = await DoctorJournal.readDetailed(pathname);
|
|
420
|
+
if (result.status !== "ok") return undefined;
|
|
421
|
+
const record = result.records.at(-1);
|
|
422
|
+
return record ? { ...record, outcome: reconcileDoctorEvidence(record, evidence) } : undefined;
|
|
423
|
+
}
|