@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,373 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getPluginsDir, getPluginsLockfile, getProjectPluginOverridesPath } from "@gajae-code/utils";
|
|
5
|
+
import { resolveOrDefaultProjectRegistryPath } from "../../discovery/helpers";
|
|
6
|
+
import { GJC_BUNDLE_KIND, type LocalRestorePlanV1 } from "../../extensibility/gjc-plugins/types";
|
|
7
|
+
import {
|
|
8
|
+
getInstalledPluginsRegistryPath,
|
|
9
|
+
getMarketplacesCacheDir,
|
|
10
|
+
getMarketplacesRegistryPath,
|
|
11
|
+
getPluginsCacheDir,
|
|
12
|
+
MarketplaceManager,
|
|
13
|
+
} from "../../extensibility/plugins/marketplace/index.js";
|
|
14
|
+
import type { MarketplaceRestorePlanV1 } from "../../extensibility/plugins/marketplace/manager";
|
|
15
|
+
import { sourcePin } from "../../extensibility/plugins/marketplace/source-resolver";
|
|
16
|
+
import { doctorCheck } from "./checks";
|
|
17
|
+
import type { DoctorConfigSource, DoctorContext } from "./context";
|
|
18
|
+
import { type DoctorFileObservation, doctorMapping, readDoctorFile } from "./files";
|
|
19
|
+
import { pluginTargetId } from "./ids";
|
|
20
|
+
import { type PluginQuarantinePlan, previewPluginQuarantine } from "./plugin-quarantine";
|
|
21
|
+
import { runPluginRestoreAction } from "./plugin-restore";
|
|
22
|
+
import type { DoctorCheck } from "./types";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Construct the marketplace manager exactly as the ordinary plugin CLI does, so
|
|
26
|
+
* doctor reads and repairs the same canonical registries and cache roots the
|
|
27
|
+
* product itself owns. Never a doctor-private layout.
|
|
28
|
+
*/
|
|
29
|
+
export async function createDoctorMarketplaceManager(cwd: string): Promise<MarketplaceManager> {
|
|
30
|
+
return new MarketplaceManager({
|
|
31
|
+
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
32
|
+
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
|
33
|
+
projectInstalledRegistryPath: await resolveOrDefaultProjectRegistryPath(cwd),
|
|
34
|
+
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
35
|
+
pluginsCacheDir: getPluginsCacheDir(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The immutable commit the private-marketplace catalog records for this plugin.
|
|
41
|
+
* Uses the product's own pin resolver so doctor and the restore lane agree on
|
|
42
|
+
* what counts as immutable; a mutable source yields no pin and stays
|
|
43
|
+
* unauthorizable.
|
|
44
|
+
*/
|
|
45
|
+
function marketplacePin(plan: MarketplaceRestorePlanV1 | undefined): string | undefined {
|
|
46
|
+
if (!plan) return undefined;
|
|
47
|
+
const pin = sourcePin(plan.source);
|
|
48
|
+
return pin.immutable ? pin.sha : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Family = "gjc" | "npm" | "marketplace";
|
|
52
|
+
interface PluginEntry {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly enabled: unknown;
|
|
55
|
+
readonly restoreRef?: string;
|
|
56
|
+
readonly restoreArtifactDigest?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Stable artifact-tree digest of the recorded bundle: sorted relative path plus
|
|
61
|
+
* per-file content hash, exactly the shape the restore lifecycle recomputes from
|
|
62
|
+
* the resolved source. Derived only from already-read registry metadata, so it
|
|
63
|
+
* remains identical across runs and can be authorized before any effect.
|
|
64
|
+
*/
|
|
65
|
+
function storedArtifactDigest(entry: Record<string, unknown>): string | undefined {
|
|
66
|
+
const files = entry.copiedFiles;
|
|
67
|
+
if (!Array.isArray(files) || files.length === 0 || files.length > 10_000) return undefined;
|
|
68
|
+
const rows: Array<readonly [string, string]> = [];
|
|
69
|
+
for (const raw of files) {
|
|
70
|
+
const file = doctorMapping(raw);
|
|
71
|
+
if (
|
|
72
|
+
!file ||
|
|
73
|
+
typeof file.relativePath !== "string" ||
|
|
74
|
+
typeof file.sha256 !== "string" ||
|
|
75
|
+
!/^[0-9a-f]{64}$/.test(file.sha256)
|
|
76
|
+
)
|
|
77
|
+
return undefined;
|
|
78
|
+
rows.push([file.relativePath, file.sha256]);
|
|
79
|
+
}
|
|
80
|
+
const hash = createHash("sha256");
|
|
81
|
+
for (const [relativePath, sha256] of rows.sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
82
|
+
hash.update(relativePath);
|
|
83
|
+
hash.update("\0");
|
|
84
|
+
hash.update(sha256);
|
|
85
|
+
hash.update("\0");
|
|
86
|
+
}
|
|
87
|
+
return hash.digest("hex");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Immutable `--ref` a restore must be pinned to, derived only from the already-read
|
|
92
|
+
* registry record. Never resolves, fetches, or inspects the source itself; a source
|
|
93
|
+
* that carries no immutable identity yields no pin, so restore stays unauthorizable.
|
|
94
|
+
*/
|
|
95
|
+
function storedRestoreRef(entry: Record<string, unknown>): string | undefined {
|
|
96
|
+
const source = doctorMapping(entry.source);
|
|
97
|
+
if (!source) return undefined;
|
|
98
|
+
if (source.kind === "git")
|
|
99
|
+
return typeof source.sha === "string" && /^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(source.sha)
|
|
100
|
+
? source.sha
|
|
101
|
+
: undefined;
|
|
102
|
+
if (source.kind === "path")
|
|
103
|
+
return typeof entry.manifestHash === "string" && /^[0-9a-f]{64}$/.test(entry.manifestHash)
|
|
104
|
+
? `local:${entry.manifestHash}`
|
|
105
|
+
: undefined;
|
|
106
|
+
if (source.kind === "tarball")
|
|
107
|
+
return typeof entry.version === "string" && entry.version.length > 0 && entry.version.length <= 128
|
|
108
|
+
? `version:${entry.version}`
|
|
109
|
+
: undefined;
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function registryFailure(
|
|
114
|
+
source: DoctorConfigSource,
|
|
115
|
+
family: Family,
|
|
116
|
+
observation: DoctorFileObservation,
|
|
117
|
+
malformed = false,
|
|
118
|
+
): DoctorCheck {
|
|
119
|
+
return doctorCheck(
|
|
120
|
+
`plugin.${family}.${source.scope}.registry`,
|
|
121
|
+
`t1:plugin-registry:${source.root.rootId}:${family}:${source.scope}`,
|
|
122
|
+
{
|
|
123
|
+
scope: source.scope,
|
|
124
|
+
execution: malformed || observation.status === "missing" ? "completed" : "blocked",
|
|
125
|
+
health: malformed ? "error" : observation.status === "missing" ? "not_applicable" : "unknown",
|
|
126
|
+
evidenceLevel: "observed",
|
|
127
|
+
reasonCode: malformed ? "plugin_registry_invalid" : `plugin_registry_${observation.status}`,
|
|
128
|
+
evidence: {
|
|
129
|
+
present: observation.status === "read",
|
|
130
|
+
runtimeProbed: false,
|
|
131
|
+
...("errno" in observation ? { errno: observation.errno } : {}),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parse(observation: DoctorFileObservation): Record<string, unknown> | undefined {
|
|
138
|
+
if (observation.status !== "read") return undefined;
|
|
139
|
+
try {
|
|
140
|
+
return doctorMapping(JSON.parse(observation.text));
|
|
141
|
+
} catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function addEntries(
|
|
147
|
+
context: DoctorContext,
|
|
148
|
+
source: DoctorConfigSource,
|
|
149
|
+
family: Family,
|
|
150
|
+
entries: readonly PluginEntry[],
|
|
151
|
+
checks: DoctorCheck[],
|
|
152
|
+
registryPath?: string,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
if (entries.length > 1000)
|
|
155
|
+
checks.push(
|
|
156
|
+
doctorCheck(
|
|
157
|
+
`plugin.${family}.${source.scope}.coverage`,
|
|
158
|
+
`t1:plugin-registry:${source.root.rootId}:${family}:${source.scope}`,
|
|
159
|
+
{
|
|
160
|
+
scope: source.scope,
|
|
161
|
+
execution: "blocked",
|
|
162
|
+
health: "unknown",
|
|
163
|
+
evidenceLevel: "observed",
|
|
164
|
+
reasonCode: "limit_exceeded",
|
|
165
|
+
evidence: { count: 1000 },
|
|
166
|
+
},
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
for (const entry of entries.slice(0, 1000)) {
|
|
170
|
+
if (!entry.name || entry.name.length > 256) continue;
|
|
171
|
+
const targetId = pluginTargetId(source.root.rootId, family, source.scope, entry.name);
|
|
172
|
+
let quarantinePlan: PluginQuarantinePlan | undefined;
|
|
173
|
+
if (context.options.repair === "plugin.quarantine-selected" && context.options.targetId === targetId) {
|
|
174
|
+
quarantinePlan = await previewPluginQuarantine({
|
|
175
|
+
family,
|
|
176
|
+
scope: source.scope,
|
|
177
|
+
name: entry.name,
|
|
178
|
+
cwd: context.cwd,
|
|
179
|
+
home: os.homedir(),
|
|
180
|
+
rootId: source.root.rootId,
|
|
181
|
+
registryPath,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Read-only observation only: preview never resolves a source, fetches, stages, or writes.
|
|
185
|
+
let restorePlan: LocalRestorePlanV1 | undefined;
|
|
186
|
+
if (
|
|
187
|
+
family === "gjc" &&
|
|
188
|
+
entry.restoreRef &&
|
|
189
|
+
entry.restoreArtifactDigest &&
|
|
190
|
+
context.options.repair === "plugin.restore-known-artifact" &&
|
|
191
|
+
context.options.targetId === targetId
|
|
192
|
+
) {
|
|
193
|
+
const preview = await runPluginRestoreAction({
|
|
194
|
+
kind: "preview",
|
|
195
|
+
target: { identity: { kind: GJC_BUNDLE_KIND, scope: source.scope, name: entry.name }, cwd: context.cwd },
|
|
196
|
+
});
|
|
197
|
+
if ("ok" in preview && preview.ok && "value" in preview) {
|
|
198
|
+
const value = preview.value;
|
|
199
|
+
if (
|
|
200
|
+
typeof value === "object" &&
|
|
201
|
+
value !== null &&
|
|
202
|
+
"kind" in value &&
|
|
203
|
+
value.kind === "gjc-plugin.restore-artifact" &&
|
|
204
|
+
"artifact" in value
|
|
205
|
+
)
|
|
206
|
+
restorePlan = value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Private-marketplace lane: the same read-only preview contract. It reads the
|
|
210
|
+
// installed registry, the marketplace catalog and the cached artifact; it never
|
|
211
|
+
// resolves a source, fetches, stages, or writes.
|
|
212
|
+
let marketplaceRestorePlan: MarketplaceRestorePlanV1 | undefined;
|
|
213
|
+
if (
|
|
214
|
+
family === "marketplace" &&
|
|
215
|
+
context.options.repair === "plugin.restore-known-artifact" &&
|
|
216
|
+
context.options.targetId === targetId
|
|
217
|
+
) {
|
|
218
|
+
try {
|
|
219
|
+
const preview = await runPluginRestoreAction({
|
|
220
|
+
kind: "marketplace-preview",
|
|
221
|
+
manager: await createDoctorMarketplaceManager(context.cwd),
|
|
222
|
+
pluginId: entry.name,
|
|
223
|
+
scope: source.scope,
|
|
224
|
+
});
|
|
225
|
+
if (
|
|
226
|
+
typeof preview === "object" &&
|
|
227
|
+
preview !== null &&
|
|
228
|
+
"kind" in preview &&
|
|
229
|
+
preview.kind === "marketplace-plugin.restore-artifact" &&
|
|
230
|
+
"artifact" in preview
|
|
231
|
+
)
|
|
232
|
+
marketplaceRestorePlan = preview;
|
|
233
|
+
} catch {
|
|
234
|
+
// A refused preview leaves the target unrepairable rather than guessing a plan.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
context.targets.set(targetId, {
|
|
238
|
+
kind: "plugin",
|
|
239
|
+
targetId,
|
|
240
|
+
family,
|
|
241
|
+
scope: source.scope,
|
|
242
|
+
name: entry.name,
|
|
243
|
+
root: source.root,
|
|
244
|
+
quarantinePlan,
|
|
245
|
+
restorePlan,
|
|
246
|
+
marketplaceRestorePlan,
|
|
247
|
+
// The marketplace pin is the catalog's recorded immutable commit, observed
|
|
248
|
+
// read-only during preview. A version alone is a moving selector and must
|
|
249
|
+
// never authorize a restore.
|
|
250
|
+
restoreRef: entry.restoreRef ?? marketplacePin(marketplaceRestorePlan),
|
|
251
|
+
// For the marketplace family the pin is the CATALOG fingerprint, never the
|
|
252
|
+
// installed artifact's digest: the artifact is the thing being repaired, so
|
|
253
|
+
// pinning it would make an absent or unreadable tree — D6's whole purpose —
|
|
254
|
+
// permanently unrepairable, and would authorize the damaged bytes rather
|
|
255
|
+
// than the intended ones. The candidate bytes are still bound: the apply
|
|
256
|
+
// path independently recomputes the resolved source digest.
|
|
257
|
+
restoreArtifactDigest: entry.restoreArtifactDigest ?? marketplaceRestorePlan?.catalogFingerprint,
|
|
258
|
+
registryPath,
|
|
259
|
+
});
|
|
260
|
+
const valid = entry.enabled === undefined || typeof entry.enabled === "boolean";
|
|
261
|
+
checks.push(
|
|
262
|
+
doctorCheck(`plugin.${family}.${source.scope}.${targetId.split(":").at(-1)}`, targetId, {
|
|
263
|
+
scope: source.scope,
|
|
264
|
+
execution: "completed",
|
|
265
|
+
health: valid ? "ok" : "error",
|
|
266
|
+
evidenceLevel: "observed",
|
|
267
|
+
reasonCode: valid ? "plugin_registry_entry_observed" : "plugin_enabled_invalid",
|
|
268
|
+
evidence: {
|
|
269
|
+
enabled: quarantinePlan?.enabled ?? entry.enabled !== false,
|
|
270
|
+
sourceClass: family,
|
|
271
|
+
runtimeProbed: false,
|
|
272
|
+
...(restorePlan ? { artifactStatus: restorePlan.artifact.status } : {}),
|
|
273
|
+
...(marketplaceRestorePlan ? { artifactStatus: marketplaceRestorePlan.artifact.status } : {}),
|
|
274
|
+
},
|
|
275
|
+
remediationIds: ["plugin.quarantine-selected", "plugin.restore-known-artifact"],
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Inventory only: registry data is read, never source-resolved or executed. */
|
|
282
|
+
export async function collectPluginTargets(context: DoctorContext): Promise<DoctorCheck[]> {
|
|
283
|
+
const checks: DoctorCheck[] = [];
|
|
284
|
+
const npm = await readDoctorFile(getPluginsLockfile());
|
|
285
|
+
const npmRegistry = parse(npm);
|
|
286
|
+
const npmPlugins = doctorMapping(npmRegistry?.plugins);
|
|
287
|
+
const marketplacePath = path.join(getPluginsDir(), "installed_plugins.json");
|
|
288
|
+
const marketplace = await readDoctorFile(marketplacePath);
|
|
289
|
+
const marketplaceRegistry = parse(marketplace);
|
|
290
|
+
const marketplacePlugins = doctorMapping(marketplaceRegistry?.plugins);
|
|
291
|
+
for (const source of context.sources) {
|
|
292
|
+
// Same canonical layout as registryPathForScope; keep this collector free
|
|
293
|
+
// of installer/compiler imports so broken native code cannot block it.
|
|
294
|
+
const gjc = await readDoctorFile(path.join(source.root.locator, "gjc-plugins", "registry.json"));
|
|
295
|
+
const registry = parse(gjc);
|
|
296
|
+
if (gjc.status !== "read") checks.push(registryFailure(source, "gjc", gjc));
|
|
297
|
+
else if (registry?.version !== 1 || registry.scope !== source.scope || !Array.isArray(registry.plugins))
|
|
298
|
+
checks.push(registryFailure(source, "gjc", gjc, true));
|
|
299
|
+
else {
|
|
300
|
+
const entries: PluginEntry[] = [];
|
|
301
|
+
let malformed = false;
|
|
302
|
+
for (const raw of registry.plugins) {
|
|
303
|
+
const entry = doctorMapping(raw);
|
|
304
|
+
if (!entry || typeof entry.name !== "string" || entry.scope !== source.scope) {
|
|
305
|
+
malformed = true;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
entries.push({
|
|
309
|
+
name: entry.name,
|
|
310
|
+
enabled: entry.enabled,
|
|
311
|
+
restoreRef: storedRestoreRef(entry),
|
|
312
|
+
restoreArtifactDigest: storedArtifactDigest(entry),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if (malformed) checks.push(registryFailure(source, "gjc", gjc, true));
|
|
316
|
+
await addEntries(context, source, "gjc", entries, checks);
|
|
317
|
+
}
|
|
318
|
+
if (npm.status !== "read") checks.push(registryFailure(source, "npm", npm));
|
|
319
|
+
else if (!npmPlugins) checks.push(registryFailure(source, "npm", npm, true));
|
|
320
|
+
else {
|
|
321
|
+
let disabled: readonly unknown[] = [];
|
|
322
|
+
let validOverrides = true;
|
|
323
|
+
if (source.scope === "project") {
|
|
324
|
+
const overrides = await readDoctorFile(getProjectPluginOverridesPath(context.cwd));
|
|
325
|
+
const parsed = parse(overrides);
|
|
326
|
+
if (
|
|
327
|
+
overrides.status !== "missing" &&
|
|
328
|
+
(overrides.status !== "read" ||
|
|
329
|
+
!parsed ||
|
|
330
|
+
(parsed.disabled !== undefined && !Array.isArray(parsed.disabled)))
|
|
331
|
+
) {
|
|
332
|
+
checks.push(registryFailure(source, "npm", overrides, overrides.status === "read"));
|
|
333
|
+
validOverrides = false;
|
|
334
|
+
} else if (Array.isArray(parsed?.disabled)) disabled = parsed.disabled;
|
|
335
|
+
}
|
|
336
|
+
if (validOverrides)
|
|
337
|
+
await addEntries(
|
|
338
|
+
context,
|
|
339
|
+
source,
|
|
340
|
+
"npm",
|
|
341
|
+
Object.entries(npmPlugins).map(([name, value]) => ({
|
|
342
|
+
name,
|
|
343
|
+
enabled: disabled.includes(name) ? false : doctorMapping(value)?.enabled,
|
|
344
|
+
})),
|
|
345
|
+
checks,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (marketplace.status !== "read") checks.push(registryFailure(source, "marketplace", marketplace));
|
|
349
|
+
else if (!marketplacePlugins || marketplaceRegistry?.version !== 2)
|
|
350
|
+
checks.push(registryFailure(source, "marketplace", marketplace, true));
|
|
351
|
+
else {
|
|
352
|
+
const entries: PluginEntry[] = [];
|
|
353
|
+
let malformed = false;
|
|
354
|
+
for (const [name, values] of Object.entries(marketplacePlugins)) {
|
|
355
|
+
if (!Array.isArray(values)) {
|
|
356
|
+
malformed = true;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
for (const raw of values) {
|
|
360
|
+
const value = doctorMapping(raw);
|
|
361
|
+
if (!value) {
|
|
362
|
+
malformed = true;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (value.scope === source.scope) entries.push({ name, enabled: value.enabled });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (malformed) checks.push(registryFailure(source, "marketplace", marketplace, true));
|
|
369
|
+
await addEntries(context, source, "marketplace", entries, checks, marketplacePath);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return checks;
|
|
373
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { DoctorContext } from "./context";
|
|
4
|
+
import { doctorMapping } from "./files";
|
|
5
|
+
import type { DoctorProbeKind, DoctorProbeReceipt } from "./probe-types";
|
|
6
|
+
|
|
7
|
+
const PROBE_OUTPUT_LIMIT = 64 * 1024;
|
|
8
|
+
const PROBE_ENV_KEYS = [
|
|
9
|
+
"PATH",
|
|
10
|
+
"HOME",
|
|
11
|
+
"USERPROFILE",
|
|
12
|
+
"SYSTEMROOT",
|
|
13
|
+
"WINDIR",
|
|
14
|
+
"TMPDIR",
|
|
15
|
+
"TEMP",
|
|
16
|
+
"TMP",
|
|
17
|
+
"LANG",
|
|
18
|
+
"LC_ALL",
|
|
19
|
+
"TZ",
|
|
20
|
+
"GJC_CONFIG_DIR",
|
|
21
|
+
"PI_CONFIG_DIR",
|
|
22
|
+
"GJC_CODING_AGENT_DIR",
|
|
23
|
+
"PI_CODING_AGENT_DIR",
|
|
24
|
+
"XDG_CONFIG_HOME",
|
|
25
|
+
"XDG_DATA_HOME",
|
|
26
|
+
"XDG_STATE_HOME",
|
|
27
|
+
"XDG_CACHE_HOME",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export interface DoctorProbeResult {
|
|
31
|
+
readonly status: "completed" | "failed" | "timeout" | "cancelled";
|
|
32
|
+
readonly reasonCode: string;
|
|
33
|
+
readonly receipt?: DoctorProbeReceipt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function probeEnvironment(): NodeJS.ProcessEnv {
|
|
37
|
+
const environment: NodeJS.ProcessEnv = { NO_COLOR: "1", BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0" };
|
|
38
|
+
for (const key of PROBE_ENV_KEYS) {
|
|
39
|
+
const value = process.env[key];
|
|
40
|
+
if (value !== undefined) environment[key] = value;
|
|
41
|
+
}
|
|
42
|
+
return environment;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readBoundedStream(stream: ReadableStream<Uint8Array>): Promise<string> {
|
|
46
|
+
const reader = stream.getReader();
|
|
47
|
+
const chunks: Uint8Array[] = [];
|
|
48
|
+
let size = 0;
|
|
49
|
+
try {
|
|
50
|
+
for (;;) {
|
|
51
|
+
const result = await reader.read();
|
|
52
|
+
if (result.done) break;
|
|
53
|
+
size += result.value.byteLength;
|
|
54
|
+
if (size > PROBE_OUTPUT_LIMIT) throw new Error("probe_output_limit");
|
|
55
|
+
chunks.push(result.value);
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
}
|
|
60
|
+
const buffer = new Uint8Array(size);
|
|
61
|
+
let offset = 0;
|
|
62
|
+
for (const chunk of chunks) {
|
|
63
|
+
buffer.set(chunk, offset);
|
|
64
|
+
offset += chunk.byteLength;
|
|
65
|
+
}
|
|
66
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseReceipt(text: string, kind: DoctorProbeKind): DoctorProbeReceipt | undefined {
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(text);
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const record = doctorMapping(parsed);
|
|
77
|
+
if (
|
|
78
|
+
record?.schemaVersion !== 1 ||
|
|
79
|
+
record.kind !== kind ||
|
|
80
|
+
(record.status !== "completed" && record.status !== "failed") ||
|
|
81
|
+
typeof record.reasonCode !== "string" ||
|
|
82
|
+
!/^[a-z_]{1,80}$/.test(record.reasonCode) ||
|
|
83
|
+
!Array.isArray(record.checks) ||
|
|
84
|
+
record.checks.length > 2000
|
|
85
|
+
)
|
|
86
|
+
return undefined;
|
|
87
|
+
const executions = new Set(["completed", "blocked", "timeout", "cancelled", "unsupported", "not_selected"]);
|
|
88
|
+
const healthStates = new Set(["ok", "warning", "error", "unknown", "not_applicable"]);
|
|
89
|
+
const evidenceLevels = new Set(["observed", "not_probed"]);
|
|
90
|
+
const safeId = (value: unknown): value is string =>
|
|
91
|
+
typeof value === "string" && value.length <= 512 && /^[a-zA-Z0-9_.:-]+$/.test(value);
|
|
92
|
+
// The fixed core child is still a process boundary. Unknown states are not executable facts.
|
|
93
|
+
if (
|
|
94
|
+
record.checks.some(value => {
|
|
95
|
+
const check = doctorMapping(value);
|
|
96
|
+
return (
|
|
97
|
+
!check ||
|
|
98
|
+
!safeId(check.id) ||
|
|
99
|
+
!safeId(check.targetId) ||
|
|
100
|
+
typeof check.execution !== "string" ||
|
|
101
|
+
!executions.has(check.execution) ||
|
|
102
|
+
typeof check.health !== "string" ||
|
|
103
|
+
!healthStates.has(check.health) ||
|
|
104
|
+
typeof check.evidenceLevel !== "string" ||
|
|
105
|
+
!evidenceLevels.has(check.evidenceLevel) ||
|
|
106
|
+
!doctorMapping(check.evidence) ||
|
|
107
|
+
!Array.isArray(check.dependsOn) ||
|
|
108
|
+
check.dependsOn.length > 32 ||
|
|
109
|
+
!check.dependsOn.every(safeId) ||
|
|
110
|
+
!Array.isArray(check.remediationIds) ||
|
|
111
|
+
check.remediationIds.length > 16 ||
|
|
112
|
+
!check.remediationIds.every(safeId) ||
|
|
113
|
+
(check.scope !== undefined && check.scope !== "user" && check.scope !== "project") ||
|
|
114
|
+
(check.reasonCode !== undefined &&
|
|
115
|
+
(typeof check.reasonCode !== "string" || !/^[a-z_]{1,80}$/.test(check.reasonCode)))
|
|
116
|
+
);
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
return undefined;
|
|
120
|
+
return record as unknown as DoctorProbeReceipt;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Run a bounded child of the current product runtime, never a PATH-discovered executable. */
|
|
124
|
+
export async function collectDoctorProbe(context: DoctorContext, kind: DoctorProbeKind): Promise<DoctorProbeResult> {
|
|
125
|
+
if (context.options.mode === "dry-run") return { status: "failed", reasonCode: "probe_not_permitted_in_preview" };
|
|
126
|
+
if (context.options.signal?.aborted) return { status: "cancelled", reasonCode: "cancelled" };
|
|
127
|
+
const timeoutMs = Math.min(4_000, context.deadline - performance.now());
|
|
128
|
+
if (timeoutMs <= 0) return { status: "timeout", reasonCode: "deadline_exceeded" };
|
|
129
|
+
const args = context.compiled
|
|
130
|
+
? [process.execPath, "--internal-doctor-probe", kind]
|
|
131
|
+
: [
|
|
132
|
+
process.execPath,
|
|
133
|
+
"--no-env-file",
|
|
134
|
+
`--config=${fileURLToPath(new URL("../../sdk/broker/internal-source.bunfig.toml", import.meta.url))}`,
|
|
135
|
+
context.cliPath ?? "",
|
|
136
|
+
"--internal-doctor-probe",
|
|
137
|
+
kind,
|
|
138
|
+
];
|
|
139
|
+
if (!path.isAbsolute(args[0]) || (!context.compiled && !context.cliPath))
|
|
140
|
+
return { status: "failed", reasonCode: "runtime_identity_unavailable" };
|
|
141
|
+
let child: Bun.Subprocess<"ignore", "pipe", "pipe">;
|
|
142
|
+
try {
|
|
143
|
+
child = Bun.spawn(args, {
|
|
144
|
+
cwd: context.cwd,
|
|
145
|
+
env: probeEnvironment(),
|
|
146
|
+
stdin: "ignore",
|
|
147
|
+
stdout: "pipe",
|
|
148
|
+
stderr: "pipe",
|
|
149
|
+
});
|
|
150
|
+
} catch {
|
|
151
|
+
return { status: "failed", reasonCode: "probe_spawn_failed" };
|
|
152
|
+
}
|
|
153
|
+
const interrupted = Promise.withResolvers<DoctorProbeResult>();
|
|
154
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
155
|
+
const abort = () => interrupted.resolve({ status: "cancelled", reasonCode: "cancelled" });
|
|
156
|
+
context.options.signal?.addEventListener("abort", abort, { once: true });
|
|
157
|
+
timeout = setTimeout(() => interrupted.resolve({ status: "timeout", reasonCode: "probe_timeout" }), timeoutMs);
|
|
158
|
+
const output = Promise.all([readBoundedStream(child.stdout), readBoundedStream(child.stderr), child.exited])
|
|
159
|
+
.then(([stdout, _stderr, exitCode]): DoctorProbeResult => {
|
|
160
|
+
const receipt = parseReceipt(stdout, kind);
|
|
161
|
+
if (!receipt)
|
|
162
|
+
return {
|
|
163
|
+
status: "failed",
|
|
164
|
+
reasonCode: child.signalCode ? "probe_signalled" : "probe_invalid_receipt",
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
status: exitCode === 0 && receipt.status === "completed" ? "completed" : "failed",
|
|
168
|
+
reasonCode: receipt.reasonCode,
|
|
169
|
+
receipt,
|
|
170
|
+
};
|
|
171
|
+
})
|
|
172
|
+
.catch((): DoctorProbeResult => ({ status: "failed", reasonCode: "probe_output_unavailable" }));
|
|
173
|
+
let result: DoctorProbeResult;
|
|
174
|
+
try {
|
|
175
|
+
result = await Promise.race([output, interrupted.promise]);
|
|
176
|
+
} finally {
|
|
177
|
+
clearTimeout(timeout);
|
|
178
|
+
context.options.signal?.removeEventListener("abort", abort);
|
|
179
|
+
}
|
|
180
|
+
if (child.exitCode === null) {
|
|
181
|
+
// The only killable process here is this retained, fixed core probe child.
|
|
182
|
+
try {
|
|
183
|
+
child.kill("SIGKILL");
|
|
184
|
+
const stopped = Promise.withResolvers<void>();
|
|
185
|
+
const cleanupTimer = setTimeout(() => stopped.resolve(), 1_000);
|
|
186
|
+
await Promise.race([child.exited, stopped.promise]);
|
|
187
|
+
clearTimeout(cleanupTimer);
|
|
188
|
+
} catch {
|
|
189
|
+
return { status: "failed", reasonCode: "probe_termination_unverified" };
|
|
190
|
+
}
|
|
191
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
192
|
+
return { status: "failed", reasonCode: "probe_termination_unverified" };
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DoctorCheck } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Maximum UTF-8 payload accepted by the doctor probe transport. */
|
|
4
|
+
export const PROBE_OUTPUT_LIMIT_BYTES = 64 * 1024;
|
|
5
|
+
/**
|
|
6
|
+
* Producer-side budget for a projection receipt. Kept below the transport limit
|
|
7
|
+
* so the probe never has to discard a whole surface: the producer sheds its own
|
|
8
|
+
* lowest-value item checks instead. The headroom covers the trailing newline
|
|
9
|
+
* and transport framing.
|
|
10
|
+
*/
|
|
11
|
+
export const PROJECTION_RECEIPT_BUDGET_BYTES = PROBE_OUTPUT_LIMIT_BYTES - 1024;
|
|
12
|
+
|
|
13
|
+
export type DoctorProbeKind = "native" | "projection";
|
|
14
|
+
|
|
15
|
+
export interface DoctorProbeReceipt {
|
|
16
|
+
readonly schemaVersion: 1;
|
|
17
|
+
readonly kind: DoctorProbeKind;
|
|
18
|
+
readonly status: "completed" | "failed";
|
|
19
|
+
readonly reasonCode: string;
|
|
20
|
+
readonly checks: readonly DoctorCheck[];
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { logger } from "@gajae-code/utils";
|
|
2
|
+
import type { DoctorProbeKind, DoctorProbeReceipt } from "./probe-types";
|
|
3
|
+
import { PROBE_OUTPUT_LIMIT_BYTES } from "./probe-types";
|
|
4
|
+
|
|
5
|
+
const loaders = {
|
|
6
|
+
native: () => import("./native-probe"),
|
|
7
|
+
projection: () => import("./projection-probe"),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Only the fixed product probes are executable; no caller-provided program or module path. */
|
|
11
|
+
export async function runDoctorProbe(kind: DoctorProbeKind): Promise<void> {
|
|
12
|
+
// The projection probe loads real product modules, and any warning they emit
|
|
13
|
+
// would otherwise create `.gjc/logs/` entries. Diagnosis must not write
|
|
14
|
+
// product state, so the rotating file sink is disabled before those modules
|
|
15
|
+
// are loaded; the receipt still travels over stdout.
|
|
16
|
+
logger.setTransports({ console: false, file: false });
|
|
17
|
+
let receipt: DoctorProbeReceipt;
|
|
18
|
+
try {
|
|
19
|
+
receipt =
|
|
20
|
+
kind === "native"
|
|
21
|
+
? await (await loaders.native()).collectNativeProbe()
|
|
22
|
+
: await (await loaders.projection()).collectProjectionProbe();
|
|
23
|
+
} catch {
|
|
24
|
+
receipt = {
|
|
25
|
+
schemaVersion: 1,
|
|
26
|
+
kind,
|
|
27
|
+
status: "failed",
|
|
28
|
+
reasonCode: kind === "native" ? "native_load_failed" : "projection_load_failed",
|
|
29
|
+
checks: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const text = JSON.stringify(receipt);
|
|
33
|
+
if (Buffer.byteLength(text) > PROBE_OUTPUT_LIMIT_BYTES) {
|
|
34
|
+
process.stdout.write(
|
|
35
|
+
`${JSON.stringify({ schemaVersion: 1, kind, status: "failed", reasonCode: "limit_exceeded", checks: [] })}\n`,
|
|
36
|
+
);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
process.stdout.write(`${text}\n`);
|
|
41
|
+
process.exitCode = receipt.status === "completed" ? 0 : 1;
|
|
42
|
+
}
|