clinkx 1.1.1 → 1.2.0
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 +60 -0
- package/README.md +97 -13
- package/clinkx-workflows/dist/allowed-roots.js +2 -0
- package/clinkx-workflows/dist/allowed-roots.js.map +1 -1
- package/clinkx-workflows/dist/architecture-comparison/admission.d.ts +5 -0
- package/clinkx-workflows/dist/architecture-comparison/admission.js +50 -0
- package/clinkx-workflows/dist/architecture-comparison/admission.js.map +1 -0
- package/clinkx-workflows/dist/architecture-comparison/contract.d.ts +995 -0
- package/clinkx-workflows/dist/architecture-comparison/contract.js +149 -0
- package/clinkx-workflows/dist/architecture-comparison/contract.js.map +1 -0
- package/clinkx-workflows/dist/architecture-comparison/runtime.d.ts +29 -0
- package/clinkx-workflows/dist/architecture-comparison/runtime.js +387 -0
- package/clinkx-workflows/dist/architecture-comparison/runtime.js.map +1 -0
- package/clinkx-workflows/dist/child-env.d.ts +3 -1
- package/clinkx-workflows/dist/child-env.js +6 -2
- package/clinkx-workflows/dist/child-env.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +28 -5
- package/clinkx-workflows/dist/config.js +137 -20
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/contained-file.js +10 -1
- package/clinkx-workflows/dist/contained-file.js.map +1 -1
- package/clinkx-workflows/dist/council-execution-policy.d.ts +4 -2
- package/clinkx-workflows/dist/council-execution-policy.js +5 -1
- package/clinkx-workflows/dist/council-execution-policy.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +72 -72
- package/clinkx-workflows/dist/dashboard/contracts.d.ts +1564 -122
- package/clinkx-workflows/dist/dashboard/contracts.js +4 -0
- package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +12 -12
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +8 -8
- package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
- package/clinkx-workflows/dist/dashboard/scanner.js +27 -0
- package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +48 -48
- package/clinkx-workflows/dist/dashboard/session-log-identity.js +2 -1
- package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +2 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js +4 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/verified-repair-contract.d.ts +191 -0
- package/clinkx-workflows/dist/dashboard/verified-repair-contract.js +28 -0
- package/clinkx-workflows/dist/dashboard/verified-repair-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/index-BdpmbPps.js +115 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/style-Im5i1iAs.css +1 -0
- package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
- package/clinkx-workflows/dist/engine.d.ts +6 -0
- package/clinkx-workflows/dist/engine.js +78 -7
- package/clinkx-workflows/dist/engine.js.map +1 -1
- package/clinkx-workflows/dist/index.d.ts +3 -0
- package/clinkx-workflows/dist/index.js +3 -2
- package/clinkx-workflows/dist/index.js.map +1 -1
- package/clinkx-workflows/dist/reducers/index.d.ts +9 -1
- package/clinkx-workflows/dist/reducers/index.js +22 -0
- package/clinkx-workflows/dist/reducers/index.js.map +1 -1
- package/clinkx-workflows/dist/root-path-validation.d.ts +12 -0
- package/clinkx-workflows/dist/root-path-validation.js +63 -0
- package/clinkx-workflows/dist/root-path-validation.js.map +1 -0
- package/clinkx-workflows/dist/rosters.d.ts +1 -0
- package/clinkx-workflows/dist/rosters.js +16 -6
- package/clinkx-workflows/dist/rosters.js.map +1 -1
- package/clinkx-workflows/dist/server.js +5 -2
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +13 -0
- package/clinkx-workflows/dist/state.d.ts +7 -1
- package/clinkx-workflows/dist/state.js +90 -19
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/validators/architecture-comparison.d.ts +2 -0
- package/clinkx-workflows/dist/validators/architecture-comparison.js +12 -0
- package/clinkx-workflows/dist/validators/architecture-comparison.js.map +1 -0
- package/clinkx-workflows/dist/validators/index.js +4 -0
- package/clinkx-workflows/dist/validators/index.js.map +1 -1
- package/clinkx-workflows/dist/validators/verified-repair.d.ts +2 -0
- package/clinkx-workflows/dist/validators/verified-repair.js +12 -0
- package/clinkx-workflows/dist/validators/verified-repair.js.map +1 -0
- package/clinkx-workflows/dist/verification-proof.d.ts +254 -0
- package/clinkx-workflows/dist/verification-proof.js +195 -0
- package/clinkx-workflows/dist/verification-proof.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/admission.d.ts +72 -0
- package/clinkx-workflows/dist/verified-repair/admission.js +63 -0
- package/clinkx-workflows/dist/verified-repair/admission.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/contract.d.ts +1910 -0
- package/clinkx-workflows/dist/verified-repair/contract.js +127 -0
- package/clinkx-workflows/dist/verified-repair/contract.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/host.d.ts +32 -0
- package/clinkx-workflows/dist/verified-repair/host.js +2 -0
- package/clinkx-workflows/dist/verified-repair/host.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/prompts.d.ts +12 -0
- package/clinkx-workflows/dist/verified-repair/prompts.js +58 -0
- package/clinkx-workflows/dist/verified-repair/prompts.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/result.d.ts +16 -0
- package/clinkx-workflows/dist/verified-repair/result.js +102 -0
- package/clinkx-workflows/dist/verified-repair/result.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/runtime.d.ts +28 -0
- package/clinkx-workflows/dist/verified-repair/runtime.js +186 -0
- package/clinkx-workflows/dist/verified-repair/runtime.js.map +1 -0
- package/clinkx-workflows/dist/workflow-completion.d.ts +83 -0
- package/clinkx-workflows/dist/workflow-completion.js +250 -0
- package/clinkx-workflows/dist/workflow-completion.js.map +1 -0
- package/clinkx-workflows/dist/workflow-tools.d.ts +7 -3
- package/clinkx-workflows/dist/workflow-tools.js +487 -46
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/clinkx-workflows/dist/workflow-verification-contract.d.ts +539 -0
- package/clinkx-workflows/dist/workflow-verification-contract.js +52 -0
- package/clinkx-workflows/dist/workflow-verification-contract.js.map +1 -0
- package/clinkx-workflows/dist/workflow-verification.d.ts +15 -0
- package/clinkx-workflows/dist/workflow-verification.js +218 -0
- package/clinkx-workflows/dist/workflow-verification.js.map +1 -0
- package/clinkx-workflows/templates/architecture-comparison/candidate.txt +29 -0
- package/clinkx-workflows/templates/architecture-comparison/judge.txt +31 -0
- package/clinkx-workflows/templates/verified-repair/investigate.txt +20 -0
- package/clinkx-workflows/templates/verified-repair/repair.txt +11 -0
- package/clinkx-workflows/templates/verified-repair/review.txt +18 -0
- package/clinkx-workflows/workflows/architecture-comparison.yaml +133 -0
- package/clinkx-workflows/workflows/verified-repair.yaml +104 -0
- package/dist/allowed-roots.d.ts +1 -1
- package/dist/allowed-roots.js +128 -15
- package/dist/allowed-roots.js.map +1 -1
- package/dist/codex/app-server-client.d.ts +1 -0
- package/dist/codex/app-server-client.js +3 -2
- package/dist/codex/app-server-client.js.map +1 -1
- package/dist/context-builder/config.d.ts +1 -0
- package/dist/context-builder/config.js +21 -9
- package/dist/context-builder/config.js.map +1 -1
- package/dist/context-builder/diagnostics.d.ts +1 -0
- package/dist/context-builder/diagnostics.js +1 -1
- package/dist/context-builder/diagnostics.js.map +1 -1
- package/dist/context-builder/discovery.d.ts +18 -1
- package/dist/context-builder/discovery.js +69 -1
- package/dist/context-builder/discovery.js.map +1 -1
- package/dist/context-builder/handler.js +8 -1
- package/dist/context-builder/handler.js.map +1 -1
- package/dist/context-builder/lifecycle-contract.d.ts +24 -24
- package/dist/context-builder/mutation-lock.d.ts +4 -0
- package/dist/context-builder/mutation-lock.js +7 -1
- package/dist/context-builder/mutation-lock.js.map +1 -1
- package/dist/context-builder/refine-handler.js +16 -1
- package/dist/context-builder/refine-handler.js.map +1 -1
- package/dist/context-builder/secrets.d.ts +11 -0
- package/dist/context-builder/secrets.js +75 -0
- package/dist/context-builder/secrets.js.map +1 -1
- package/dist/context-builder/storage.d.ts +10 -2
- package/dist/context-builder/storage.js +213 -16
- package/dist/context-builder/storage.js.map +1 -1
- package/dist/context-builder/versions.d.ts +5 -2
- package/dist/context-builder/versions.js +5 -2
- package/dist/context-builder/versions.js.map +1 -1
- package/dist/dashboard-cli.d.ts +6 -0
- package/dist/dashboard-cli.js +18 -32
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-gateway/main.js +8 -3
- package/dist/dashboard-gateway/main.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/project-verification/contract.d.ts +537 -0
- package/dist/project-verification/contract.js +40 -0
- package/dist/project-verification/contract.js.map +1 -0
- package/dist/project-verification/files.d.ts +23 -0
- package/dist/project-verification/files.js +80 -0
- package/dist/project-verification/files.js.map +1 -0
- package/dist/project-verification/process.d.ts +7 -0
- package/dist/project-verification/process.js +115 -0
- package/dist/project-verification/process.js.map +1 -0
- package/dist/project-verification/runner.d.ts +35 -0
- package/dist/project-verification/runner.js +185 -0
- package/dist/project-verification/runner.js.map +1 -0
- package/dist/project-verification/service.d.ts +37 -0
- package/dist/project-verification/service.js +151 -0
- package/dist/project-verification/service.js.map +1 -0
- package/dist/provider-doctor.d.ts +22 -0
- package/dist/provider-doctor.js +22 -0
- package/dist/provider-doctor.js.map +1 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +5 -1
- package/dist/registry.js.map +1 -1
- package/dist/server.js +41 -6
- package/dist/server.js.map +1 -1
- package/dist/verification-tools.d.ts +90 -0
- package/dist/verification-tools.js +120 -0
- package/dist/verification-tools.js.map +1 -0
- package/dist/verified-repair/files.d.ts +22 -0
- package/dist/verified-repair/files.js +114 -0
- package/dist/verified-repair/files.js.map +1 -0
- package/dist/verified-repair/host.d.ts +14 -0
- package/dist/verified-repair/host.js +612 -0
- package/dist/verified-repair/host.js.map +1 -0
- package/docs/configuration.md +224 -12
- package/docs/engineering-recipes.md +415 -0
- package/docs/security.md +3 -0
- package/docs/verification/README.md +59 -0
- package/docs/verification/dashboard-real-disk.md +436 -0
- package/docs/verification/project-checks.example.json +14 -0
- package/docs/verification/project-checks.md +444 -0
- package/docs/verification/workflow-handoff.md +113 -0
- package/package.json +10 -2
- package/scripts/provider-doctor-core.mjs +380 -0
- package/scripts/provider-doctor-probe.mjs +34 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +0 -106
- package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +0 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { probeProvider } from "./provider-doctor-probe.mjs";
|
|
7
|
+
|
|
8
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const VERSION_TIMEOUT_MS = 10_000;
|
|
10
|
+
const MAX_TEXT = 200;
|
|
11
|
+
const MAX_CAPTURE = 4096;
|
|
12
|
+
const MIN_SCRUB_LENGTH = 6;
|
|
13
|
+
const BASELINE_ENV = new Set(["PATH", "HOME", "USER", "LANG", "TMPDIR"]);
|
|
14
|
+
|
|
15
|
+
export class DoctorInputError extends Error {}
|
|
16
|
+
|
|
17
|
+
async function loadModules(withProbe) {
|
|
18
|
+
const dist = (path) => import(pathToFileURL(join(PACKAGE_ROOT, path)).href);
|
|
19
|
+
const [registry, env, redaction, workflowsConfig, resolver, diagnostics, handler] = await Promise.all([
|
|
20
|
+
dist("dist/registry.js"),
|
|
21
|
+
dist("dist/env.js"),
|
|
22
|
+
dist("dist/elaris/redact.js"),
|
|
23
|
+
dist("clinkx-workflows/dist/config.js"),
|
|
24
|
+
dist("clinkx-workflows/dist/context-mode-resolver.js"),
|
|
25
|
+
dist("dist/context-builder/diagnostics.js"),
|
|
26
|
+
withProbe ? dist("dist/handler.js") : Promise.resolve(undefined),
|
|
27
|
+
]);
|
|
28
|
+
return {
|
|
29
|
+
AdapterRegistry: registry.AdapterRegistry,
|
|
30
|
+
buildEnv: env.buildEnv,
|
|
31
|
+
redactSecrets: env.redactSecrets,
|
|
32
|
+
redactCredentials: (text) => redaction.redactWithOptions(text, {}),
|
|
33
|
+
preflightEnabled: () => process.env["CLINKX_WORKFLOWS_CONTEXT_PREFLIGHT_ENABLED"]?.trim().toLowerCase() !== "false",
|
|
34
|
+
getClinkxHome: workflowsConfig.getClinkxHome,
|
|
35
|
+
getChildConfigPath: workflowsConfig.getChildConfigPath,
|
|
36
|
+
getDefaultCurator: workflowsConfig.getWorkflowsContextDefaultCurator,
|
|
37
|
+
getContextDefault: workflowsConfig.getWorkflowsContextDefault,
|
|
38
|
+
defaultFullDelegateProviders: resolver.defaultFullDelegateProviders,
|
|
39
|
+
sanitizeDiagnosticText: diagnostics.sanitizeDiagnosticText,
|
|
40
|
+
handleClink: handler?.handleClink,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createCleaner(m) {
|
|
45
|
+
const hostSecrets = Object.entries(m.redactSecrets({ ...process.env }))
|
|
46
|
+
.filter(([, value]) => value === "[REDACTED]")
|
|
47
|
+
.map(([key]) => process.env[key] ?? "");
|
|
48
|
+
|
|
49
|
+
return (text, adapter) => {
|
|
50
|
+
const values = new Set(hostSecrets);
|
|
51
|
+
if (adapter !== undefined) {
|
|
52
|
+
for (const [key, value] of Object.entries(m.buildEnv(adapter))) {
|
|
53
|
+
if (!BASELINE_ENV.has(key)) values.add(value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let scrubbed = String(text);
|
|
57
|
+
for (const value of [...values].filter((v) => v.length >= MIN_SCRUB_LENGTH).sort((a, b) => b.length - a.length)) {
|
|
58
|
+
scrubbed = scrubbed.split(value).join("[redacted-env]");
|
|
59
|
+
}
|
|
60
|
+
const sanitized = m.sanitizeDiagnosticText(m.redactCredentials(scrubbed));
|
|
61
|
+
return sanitized.length > MAX_TEXT ? `${sanitized.slice(0, MAX_TEXT - 3)}...` : sanitized;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function firstLine(text) {
|
|
66
|
+
return text.split(/\r?\n/).map((line) => line.trim()).find((line) => line !== "") ?? "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function cleanupVersionProbe(child) {
|
|
70
|
+
if (child?.pid === undefined) return true;
|
|
71
|
+
const target = process.platform === "win32" ? child.pid : -child.pid;
|
|
72
|
+
child.stdout?.destroy();
|
|
73
|
+
child.stderr?.destroy();
|
|
74
|
+
try {
|
|
75
|
+
process.kill(target, "SIGKILL");
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error?.code === "ESRCH") return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
for (let attempt = 0; attempt <= 40; attempt += 1) {
|
|
81
|
+
try {
|
|
82
|
+
process.kill(target, 0);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return error?.code === "ESRCH";
|
|
85
|
+
}
|
|
86
|
+
if (attempt < 40) await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checkExecutable(adapter, m, clean, signal) {
|
|
92
|
+
signal.throwIfAborted();
|
|
93
|
+
const command = adapter.backend === "app_server"
|
|
94
|
+
? adapter.app_server?.command ?? adapter.command
|
|
95
|
+
: adapter.command;
|
|
96
|
+
return new Promise((resolveCheck, rejectCheck) => {
|
|
97
|
+
let finishing = false;
|
|
98
|
+
let child;
|
|
99
|
+
let stdout = "";
|
|
100
|
+
let stderr = "";
|
|
101
|
+
const finish = async (result) => {
|
|
102
|
+
if (finishing) return;
|
|
103
|
+
finishing = true;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
const cleanupOk = await cleanupVersionProbe(child);
|
|
106
|
+
signal.removeEventListener("abort", abort);
|
|
107
|
+
if (signal.aborted) {
|
|
108
|
+
rejectCheck(signal.reason);
|
|
109
|
+
} else {
|
|
110
|
+
resolveCheck(cleanupOk ? result : {
|
|
111
|
+
executable: "ok", version: "unknown", cleanup_failed: true,
|
|
112
|
+
reason: clean("version probe process cleanup could not be confirmed within 1 s", adapter),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const abort = () => { void finish({ executable: "ok", version: "unknown" }); };
|
|
117
|
+
const missing = (error) => {
|
|
118
|
+
const why = error?.code === "ENOENT"
|
|
119
|
+
? `command "${command}" not found`
|
|
120
|
+
: error?.code === "EACCES"
|
|
121
|
+
? `command "${command}" is not executable`
|
|
122
|
+
: `command "${command}" could not be spawned (${error?.code ?? "error"})`;
|
|
123
|
+
return { executable: "missing", version: "unknown", reason: clean(why, adapter) };
|
|
124
|
+
};
|
|
125
|
+
const timer = setTimeout(() => { void finish({ executable: "ok", version: "unknown" }); }, VERSION_TIMEOUT_MS);
|
|
126
|
+
try {
|
|
127
|
+
child = spawn(command, ["--version"], {
|
|
128
|
+
shell: false,
|
|
129
|
+
detached: true,
|
|
130
|
+
cwd: process.cwd(),
|
|
131
|
+
env: m.buildEnv(adapter),
|
|
132
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
void finish(missing(error));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
139
|
+
if (signal.aborted) { abort(); return; }
|
|
140
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
141
|
+
if (stdout.length < MAX_CAPTURE) stdout += chunk.slice(0, MAX_CAPTURE - stdout.length);
|
|
142
|
+
});
|
|
143
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
144
|
+
if (stderr.length < MAX_CAPTURE) stderr += chunk.slice(0, MAX_CAPTURE - stderr.length);
|
|
145
|
+
});
|
|
146
|
+
child.once("error", (error) => { void finish(missing(error)); });
|
|
147
|
+
child.once("close", (code) => {
|
|
148
|
+
const raw = code === 0 ? firstLine(stdout) || firstLine(stderr) : "";
|
|
149
|
+
void finish({ executable: "ok", version: raw === "" ? "unknown" : clean(raw, adapter) });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function blankLine(adapter, extra) {
|
|
155
|
+
return {
|
|
156
|
+
adapter,
|
|
157
|
+
backend: null,
|
|
158
|
+
roles: [],
|
|
159
|
+
config: "invalid",
|
|
160
|
+
executable: "not_run",
|
|
161
|
+
version: "unknown",
|
|
162
|
+
auth: "not_run",
|
|
163
|
+
startup_required: [],
|
|
164
|
+
...extra,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function inspectRegistry(registry, required, probes, m, clean, tag, signal) {
|
|
169
|
+
const lines = [];
|
|
170
|
+
const blocked = [];
|
|
171
|
+
const adapters = registry.getAdapterNames().map((name) => registry.resolveAdapter(name));
|
|
172
|
+
const checks = await Promise.allSettled(adapters.map((adapter) => checkExecutable(adapter, m, clean, signal)));
|
|
173
|
+
signal.throwIfAborted();
|
|
174
|
+
const executables = checks.map((check) => {
|
|
175
|
+
if (check.status === "rejected") throw check.reason;
|
|
176
|
+
return check.value;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
for (const [index, adapter] of adapters.entries()) {
|
|
180
|
+
signal.throwIfAborted();
|
|
181
|
+
const need = required.get(adapter.name);
|
|
182
|
+
const exe = executables[index];
|
|
183
|
+
const line = {
|
|
184
|
+
adapter: adapter.name,
|
|
185
|
+
backend: adapter.backend,
|
|
186
|
+
roles: Object.keys(adapter.roles).sort(),
|
|
187
|
+
config: "ok",
|
|
188
|
+
executable: exe.executable,
|
|
189
|
+
version: exe.version,
|
|
190
|
+
auth: "not_run",
|
|
191
|
+
startup_required: need?.labels ?? [],
|
|
192
|
+
...tag,
|
|
193
|
+
};
|
|
194
|
+
const reasons = [];
|
|
195
|
+
if (exe.cleanup_failed) reasons.push(exe.reason);
|
|
196
|
+
if (need !== undefined) {
|
|
197
|
+
const startup = `startup ${need.labels.join("+")}`;
|
|
198
|
+
for (const [role, labels] of need.roles) {
|
|
199
|
+
if (adapter.roles[role] == null) reasons.push(`startup ${labels.join("+")} requires role "${role}"`);
|
|
200
|
+
}
|
|
201
|
+
if (adapter.backend === "app_server") reasons.push(`${startup} cannot use backend app_server`);
|
|
202
|
+
if (reasons.length > 0) line.config = "invalid";
|
|
203
|
+
if (exe.executable !== "ok") reasons.push(`${startup} executable missing: ${exe.reason}`);
|
|
204
|
+
} else if (exe.executable !== "ok") {
|
|
205
|
+
line.reason = exe.reason;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (probes.includes(adapter.name) && !exe.cleanup_failed) {
|
|
209
|
+
if (exe.executable !== "ok") {
|
|
210
|
+
reasons.push("probe requested but the executable is missing");
|
|
211
|
+
} else if (adapter.roles.default == null) {
|
|
212
|
+
reasons.push('probe requires role "default", which this adapter does not define');
|
|
213
|
+
} else {
|
|
214
|
+
const probe = await probeProvider(adapter, registry, m, clean, { signal });
|
|
215
|
+
signal.throwIfAborted();
|
|
216
|
+
line.auth = probe.auth;
|
|
217
|
+
if (probe.auth !== "ok") reasons.push(`probe failed: ${probe.reason}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (reasons.length > 0) {
|
|
222
|
+
line.reason = reasons.join("; ");
|
|
223
|
+
blocked.push({ adapter: adapter.name, reason: line.reason });
|
|
224
|
+
}
|
|
225
|
+
lines.push(line);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const [name, need] of required) {
|
|
229
|
+
if (adapters.some((adapter) => adapter.name === name)) continue;
|
|
230
|
+
const reason = `startup ${need.labels.join("+")} adapter is not configured`;
|
|
231
|
+
lines.push(blankLine(name, { startup_required: need.labels, reason, ...tag }));
|
|
232
|
+
blocked.push({ adapter: name, reason });
|
|
233
|
+
}
|
|
234
|
+
return { lines, blocked };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function configFiles(path) {
|
|
238
|
+
try {
|
|
239
|
+
if (!statSync(path).isDirectory()) return [path];
|
|
240
|
+
return readdirSync(path).filter((file) => file.endsWith(".json")).sort().map((file) => join(path, file));
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function jsonFilesInDirectory(dir) {
|
|
247
|
+
if (!existsSync(dir)) return [];
|
|
248
|
+
try {
|
|
249
|
+
return readdirSync(dir).filter((file) => file.endsWith(".json")).sort().map((file) => join(dir, file));
|
|
250
|
+
} catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Mirrors discoverConfigPaths() in src/registry.ts; a CLINKX_HOME error has already failed AdapterRegistry.load() with its own message.
|
|
256
|
+
function standardConfigFiles(getClinkxHome) {
|
|
257
|
+
const envPath = process.env["CLINKX_CONFIG_PATH"];
|
|
258
|
+
if (envPath != null && envPath !== "") return configFiles(envPath);
|
|
259
|
+
const xdgHome = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
|
|
260
|
+
let homeTier = [];
|
|
261
|
+
try {
|
|
262
|
+
homeTier = [join(getClinkxHome(), "adapters")];
|
|
263
|
+
} catch {
|
|
264
|
+
homeTier = [];
|
|
265
|
+
}
|
|
266
|
+
return [...homeTier, join(xdgHome, "clinkx", "adapters"), join(PACKAGE_ROOT, "conf", "adapters")]
|
|
267
|
+
.flatMap(jsonFilesInDirectory);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function jsonErrorLocation(message, raw) {
|
|
271
|
+
const lineColumn = /line (\d+) column (\d+)/.exec(message);
|
|
272
|
+
if (lineColumn !== null) return ` at ${lineColumn[1]}:${lineColumn[2]}`;
|
|
273
|
+
const position = /at position (\d+)/.exec(message);
|
|
274
|
+
if (position === null) return "";
|
|
275
|
+
const before = raw.slice(0, Number(position[1]));
|
|
276
|
+
return ` at ${before.split("\n").length}:${before.length - before.lastIndexOf("\n")}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// V8's JSON.parse message quotes the text around the error, which can be part of a secret value.
|
|
280
|
+
function describeLoadError(error, files) {
|
|
281
|
+
if (!(error instanceof SyntaxError)) return error instanceof Error ? error.message : String(error);
|
|
282
|
+
for (const file of files) {
|
|
283
|
+
let raw = "";
|
|
284
|
+
try {
|
|
285
|
+
raw = readFileSync(file, "utf8");
|
|
286
|
+
JSON.parse(raw);
|
|
287
|
+
} catch (parseError) {
|
|
288
|
+
if (parseError instanceof SyntaxError) {
|
|
289
|
+
return `JSON syntax error in ${basename(file)}${jsonErrorLocation(parseError.message, raw)}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return "JSON syntax error in an adapter config file";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function runDoctor(probes, signal) {
|
|
297
|
+
signal.throwIfAborted();
|
|
298
|
+
if (!Array.isArray(probes) || probes.some((name) => typeof name !== "string" || name.trim() === "" || name !== name.trim()) || new Set(probes).size !== probes.length) {
|
|
299
|
+
throw new DoctorInputError("probes must be an array of unique nonempty adapter names");
|
|
300
|
+
}
|
|
301
|
+
const m = await loadModules(probes.length > 0);
|
|
302
|
+
signal.throwIfAborted();
|
|
303
|
+
const clean = createCleaner(m);
|
|
304
|
+
const lines = [];
|
|
305
|
+
const blocked = [];
|
|
306
|
+
const fail = (adapter, raw, extra = {}) => {
|
|
307
|
+
const reason = clean(raw);
|
|
308
|
+
lines.push(blankLine(adapter, { reason, ...extra }));
|
|
309
|
+
blocked.push({ adapter, reason });
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
let standard;
|
|
313
|
+
try {
|
|
314
|
+
standard = m.AdapterRegistry.load();
|
|
315
|
+
} catch (error) {
|
|
316
|
+
fail(null, `adapter registry failed to load: ${describeLoadError(error, standardConfigFiles(m.getClinkxHome))}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const childPath = m.getChildConfigPath();
|
|
320
|
+
let child;
|
|
321
|
+
if (childPath !== undefined) {
|
|
322
|
+
try {
|
|
323
|
+
child = m.AdapterRegistry.loadFromPath(childPath);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
fail(null, `CLINKX_WORKFLOWS_CHILD_CONFIG_PATH registry failed to load: ${describeLoadError(error, configFiles(childPath))}`, { registry: "workflow_child" });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (standard !== undefined) {
|
|
330
|
+
for (const name of probes) {
|
|
331
|
+
let adapter;
|
|
332
|
+
try {
|
|
333
|
+
adapter = standard.resolveAdapter(name);
|
|
334
|
+
} catch {
|
|
335
|
+
throw new DoctorInputError(`probe ${name}: no such adapter (available: ${standard.getAdapterNames().join(", ") || "none"})`);
|
|
336
|
+
}
|
|
337
|
+
if (adapter.backend === "app_server") {
|
|
338
|
+
throw new DoctorInputError(`probe ${name}: app_server adapters cannot be probed through the clink pipeline`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const required = new Map();
|
|
344
|
+
if (m.preflightEnabled()) {
|
|
345
|
+
try {
|
|
346
|
+
m.getContextDefault();
|
|
347
|
+
for (const provider of m.defaultFullDelegateProviders(m.getDefaultCurator())) {
|
|
348
|
+
const entry = required.get(provider.cli_name) ?? { labels: [], roles: new Map() };
|
|
349
|
+
entry.labels.push(provider.label);
|
|
350
|
+
entry.roles.set(provider.role, [...(entry.roles.get(provider.role) ?? []), provider.label]);
|
|
351
|
+
required.set(provider.cli_name, entry);
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
fail(null, error instanceof Error ? error.message : String(error));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const contextRegistry = childPath === undefined ? standard : child;
|
|
359
|
+
if (standard !== undefined) {
|
|
360
|
+
const result = await inspectRegistry(
|
|
361
|
+
standard,
|
|
362
|
+
contextRegistry === standard ? required : new Map(),
|
|
363
|
+
probes,
|
|
364
|
+
m,
|
|
365
|
+
clean,
|
|
366
|
+
{},
|
|
367
|
+
signal,
|
|
368
|
+
);
|
|
369
|
+
lines.push(...result.lines);
|
|
370
|
+
blocked.push(...result.blocked);
|
|
371
|
+
}
|
|
372
|
+
if (child !== undefined) {
|
|
373
|
+
const result = await inspectRegistry(child, required, [], m, clean, { registry: "workflow_child" }, signal);
|
|
374
|
+
lines.push(...result.lines);
|
|
375
|
+
blocked.push(...result.blocked);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
signal.throwIfAborted();
|
|
379
|
+
return { version: 1, adapters: lines, verdict: blocked.length === 0 ? "ready" : "blocked", blocked };
|
|
380
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const PROBE_TIMEOUT_MS = 120_000;
|
|
2
|
+
|
|
3
|
+
export async function probeProvider(adapter, registry, m, clean, { signal, deadlineMs = PROBE_TIMEOUT_MS }) {
|
|
4
|
+
const deadline = new AbortController();
|
|
5
|
+
const timer = setTimeout(() => deadline.abort(new DOMException("Probe deadline exceeded", "TimeoutError")), deadlineMs);
|
|
6
|
+
const probeSignal = AbortSignal.any([signal, deadline.signal]);
|
|
7
|
+
const timedOut = () => ({ auth: "failed", reason: `probe exceeded ${deadlineMs / 1_000} s deadline` });
|
|
8
|
+
try {
|
|
9
|
+
const result = await m.handleClink(
|
|
10
|
+
{ prompt: "Reply with OK", cli_name: adapter.name, role: "default", timeout_seconds: PROBE_TIMEOUT_MS / 1_000, debug: true },
|
|
11
|
+
{ registry, signal: probeSignal },
|
|
12
|
+
);
|
|
13
|
+
if (deadline.signal.aborted) return timedOut();
|
|
14
|
+
const text = result.content[0]?.text ?? "";
|
|
15
|
+
if (result.isError === true) return { auth: "failed", reason: clean(text, adapter) };
|
|
16
|
+
// Consume only host completion/provenance fields; never publish the debug block.
|
|
17
|
+
let completion;
|
|
18
|
+
try { completion = JSON.parse(result.content.at(-1)?.text ?? "")._debug; } catch {}
|
|
19
|
+
const validatedResultFile = completion?.parse_source === "result_contract" && completion?.result_contract_status === "ok";
|
|
20
|
+
if (completion?.exit_code !== 0 || completion?.termination_reason !== "none"
|
|
21
|
+
|| (!(completion?.stdout_bytes > 0) && !validatedResultFile) || typeof completion?.parse_source !== "string"
|
|
22
|
+
|| completion.parse_source === "stderr_fallback") {
|
|
23
|
+
return { auth: "failed", reason: "probe did not produce successful result evidence" };
|
|
24
|
+
}
|
|
25
|
+
if (text.trim() === "") return { auth: "failed", reason: "probe returned an empty response" };
|
|
26
|
+
if (text.trim() !== "OK") return { auth: "failed", reason: "probe did not return exactly OK" };
|
|
27
|
+
return { auth: "ok" };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (deadline.signal.aborted) return timedOut();
|
|
30
|
+
return { auth: "failed", reason: clean(error instanceof Error ? error.message : String(error), adapter) };
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
}
|