create-principles-disciple 1.146.9 → 1.147.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/_release/product-identity.json +1 -1
- package/codex-adapter/dist/ingestion/explicit-owner-statement.d.ts +6 -0
- package/codex-adapter/dist/ingestion/explicit-owner-statement.js +6 -0
- package/codex-adapter/dist/pd-hook.js +5 -2
- package/codex-adapter/dist/worker/workspace-worker.d.ts +13 -1
- package/codex-adapter/dist/worker/workspace-worker.js +26 -3
- package/codex-adapter/package.json +1 -1
- package/console/dist/shared/prompt-injection-contract.d.ts +1 -1
- package/console/dist/web/assets/app.js +31 -31
- package/core/dist/prompt-builder/size-guard.d.ts.map +1 -1
- package/core/dist/prompt-builder/size-guard.js +6 -1
- package/core/dist/prompt-builder/size-guard.js.map +1 -1
- package/core/dist/prompt-builder/types.d.ts +2 -1
- package/core/dist/prompt-builder/types.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +12 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js +12 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-budget-policy.d.ts +42 -11
- package/core/dist/runtime-v2/activation/prompt-budget-policy.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-budget-policy.js +82 -30
- package/core/dist/runtime-v2/activation/prompt-budget-policy.js.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-catalog.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-catalog.js +17 -5
- package/core/dist/runtime-v2/adapter/pi-ai-catalog.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +2 -2
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +1 -1
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/intent/intent-friction-block.d.ts +2 -2
- package/core/dist/runtime-v2/intent/intent-friction-block.js +2 -2
- package/core/dist/runtime-v2/pain-ingress-payload.d.ts +13 -1
- package/core/dist/runtime-v2/pain-ingress-payload.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-ingress-payload.js +21 -2
- package/core/dist/runtime-v2/pain-ingress-payload.js.map +1 -1
- package/core/dist/runtime-v2/pain-ingress.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-ingress.js +3 -0
- package/core/dist/runtime-v2/pain-ingress.js.map +1 -1
- package/core/dist/runtime-v2/signal-collector/confidence-gate.d.ts +35 -0
- package/core/dist/runtime-v2/signal-collector/confidence-gate.d.ts.map +1 -0
- package/core/dist/runtime-v2/signal-collector/confidence-gate.js +48 -0
- package/core/dist/runtime-v2/signal-collector/confidence-gate.js.map +1 -0
- package/core/dist/runtime-v2/signal-collector/index.d.ts +1 -0
- package/core/dist/runtime-v2/signal-collector/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/signal-collector/index.js +1 -0
- package/core/dist/runtime-v2/signal-collector/index.js.map +1 -1
- package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts +4 -6
- package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts.map +1 -1
- package/core/dist/runtime-v2/signal-collector/keyword-stage.js +100 -9
- package/core/dist/runtime-v2/signal-collector/keyword-stage.js.map +1 -1
- package/core/dist/runtime-v2/store/context/sqlite-context-assembler.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/context/sqlite-context-assembler.js +8 -4
- package/core/dist/runtime-v2/store/context/sqlite-context-assembler.js.map +1 -1
- package/core/dist/runtime-v2/types/event-types.d.ts +2 -2
- package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/event-types.js +1 -0
- package/core/dist/runtime-v2/types/event-types.js.map +1 -1
- package/core/package.json +1 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +52 -1
- package/dist/installer.js.map +1 -1
- package/dist/installers/mcp-registration-conflict.d.ts +35 -0
- package/dist/installers/mcp-registration-conflict.d.ts.map +1 -0
- package/dist/installers/mcp-registration-conflict.js +559 -0
- package/dist/installers/mcp-registration-conflict.js.map +1 -0
- package/dist/installers/openclaw-host-installer.d.ts.map +1 -1
- package/dist/installers/openclaw-host-installer.js +20 -0
- package/dist/installers/openclaw-host-installer.js.map +1 -1
- package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/dist/installers/workbuddy-host-installer.js +124 -2
- package/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/dist/mcp-server-layout-compat.d.ts +18 -0
- package/dist/mcp-server-layout-compat.d.ts.map +1 -0
- package/dist/mcp-server-layout-compat.js +22 -0
- package/dist/mcp-server-layout-compat.js.map +1 -0
- package/host-runtime/dist/governance-signal-admission.d.ts +147 -4
- package/host-runtime/dist/governance-signal-admission.js +1065 -60
- package/host-runtime/dist/index.d.ts +2 -0
- package/host-runtime/dist/index.js +2 -0
- package/host-runtime/dist/prompt-injection-budget-observation.js +4 -2
- package/host-runtime/dist/signal-classifier.d.ts +46 -0
- package/host-runtime/dist/signal-classifier.js +167 -0
- package/host-runtime/dist/signal-stage2-cycle.d.ts +42 -0
- package/host-runtime/dist/signal-stage2-cycle.js +184 -0
- package/host-runtime/package.json +1 -1
- package/install-layout/dist/index.d.ts +15 -0
- package/install-layout/dist/index.js +10 -0
- package/install-layout/package.json +1 -1
- package/package.json +9 -7
- package/pd-cli/dist/commands/health-codex.d.ts.map +1 -1
- package/pd-cli/dist/commands/health-codex.js +12 -0
- package/pd-cli/dist/commands/health-codex.js.map +1 -1
- package/pd-cli/dist/commands/health-mcp.d.ts +74 -0
- package/pd-cli/dist/commands/health-mcp.d.ts.map +1 -0
- package/pd-cli/dist/commands/health-mcp.js +723 -0
- package/pd-cli/dist/commands/health-mcp.js.map +1 -0
- package/pd-cli/dist/commands/health.d.ts +2 -0
- package/pd-cli/dist/commands/health.d.ts.map +1 -1
- package/pd-cli/dist/commands/health.js +11 -6
- package/pd-cli/dist/commands/health.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/pd-mcp-server/dist/budget.d.ts +15 -0
- package/pd-mcp-server/dist/budget.js +19 -0
- package/pd-mcp-server/dist/deadline.d.ts +75 -0
- package/pd-mcp-server/dist/deadline.js +136 -0
- package/pd-mcp-server/dist/envelope.d.ts +52 -0
- package/pd-mcp-server/dist/envelope.js +63 -0
- package/pd-mcp-server/dist/index.d.ts +10 -0
- package/pd-mcp-server/dist/index.js +76 -0
- package/pd-mcp-server/dist/logging.d.ts +12 -0
- package/pd-mcp-server/dist/logging.js +42 -0
- package/pd-mcp-server/dist/server.d.ts +90 -0
- package/pd-mcp-server/dist/server.js +178 -0
- package/pd-mcp-server/dist/version.d.ts +13 -0
- package/pd-mcp-server/dist/version.js +13 -0
- package/pd-mcp-server/dist/workspace.d.ts +23 -0
- package/pd-mcp-server/dist/workspace.js +77 -0
- package/pd-mcp-server/package.json +49 -0
- package/plugin/dist/bundle.js +414 -412
- package/plugin/dist/core/mcp-binding.d.ts +41 -0
- package/plugin/dist/governance-audit.js +2 -2
- package/plugin/dist/index.d.ts +33 -0
- package/plugin/dist/openclaw.plugin.json +10 -1
- package/plugin/dist/utils/workspace-resolver.d.ts +17 -0
- package/plugin/openclaw.plugin.json +10 -1
- package/plugin/package.json +2 -1
- package/release-manager/dist/installer.js +52 -1
- package/release-manager/dist/installers/mcp-registration-conflict.js +559 -0
- package/release-manager/dist/installers/openclaw-host-installer.js +20 -0
- package/release-manager/dist/installers/workbuddy-host-installer.js +124 -2
- package/release-manager/dist/mcp-server-layout-compat.js +22 -0
- package/release-manager/package.json +7 -5
- package/workbuddy-adapter/dist/codec/input-decoder.js +3 -1
- package/workbuddy-adapter/dist/pd-hook.js +28 -5
- package/workbuddy-adapter/dist/worker/workspace-worker.d.ts +6 -1
- package/workbuddy-adapter/dist/worker/workspace-worker.js +24 -3
- package/workbuddy-adapter/package.json +1 -1
- package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +2 -2
- package/workbuddy-adapter/plugin/.mcp.json +10 -0
- package/workbuddy-adapter/plugin/mcp/launcher.cjs +347 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared MCP lifecycle diagnostics for `pd health` — SPEC-PD-MCP-GATE4-001
|
|
3
|
+
* §7.8 (PRI-1071 P2).
|
|
4
|
+
*
|
|
5
|
+
* One read-only assessment used by every host health surface. It answers:
|
|
6
|
+
* - is the host + PD plugin payload installed?
|
|
7
|
+
* - does the installed server entry exist, and which version are the bytes?
|
|
8
|
+
* - does that version match the Runtime Pin (Codex runtime-version.json) or
|
|
9
|
+
* the pin recorded in the binding (OpenClaw / WorkBuddy)?
|
|
10
|
+
* - is the PD-owned mcp-binding.json present, schema/host valid, canonical,
|
|
11
|
+
* and free of symlink escapes?
|
|
12
|
+
* - does the binding workspace still exist?
|
|
13
|
+
* - is the host's own MCP config registered?
|
|
14
|
+
* - does the host need a restart to pick up the current binding?
|
|
15
|
+
*
|
|
16
|
+
* Boundaries:
|
|
17
|
+
* - never executes the MCP server or a business Tool (no probe is spawned);
|
|
18
|
+
* - never reads workspace business data (workspace is stat-only);
|
|
19
|
+
* - every unverifiable layer reports `unverified` with a reason + nextAction,
|
|
20
|
+
* never a guessed healthy value (SPEC §7.8: unknown is not healthy).
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as os from 'os';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
import { getInstallLayoutPaths } from '@principles/install-layout';
|
|
26
|
+
import { findNewestRolloutFile, resolveCodexHome, selectMarketplaceCacheVersionDir, } from '../services/codex-plugin-install.js';
|
|
27
|
+
export const MCP_BINDING_SCHEMA = 'pd.mcp-binding.v1';
|
|
28
|
+
const SERVER_ENTRY_SEGMENTS = {
|
|
29
|
+
codex: ['node_modules', '@principles', 'pd-mcp-server', 'dist', 'index.js'],
|
|
30
|
+
openclaw: ['pd-mcp-server', 'dist', 'index.js'],
|
|
31
|
+
workbuddy: ['pd-mcp-server', 'dist', 'index.js'],
|
|
32
|
+
};
|
|
33
|
+
const SETUP_NEXT_ACTION = 'Re-run the PD host setup for this host so mcp-binding.json and the pinned MCP server are (re)installed, then restart the host.';
|
|
34
|
+
function isRecord(value) {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function readJsonRecord(file) {
|
|
38
|
+
if (file === null)
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
42
|
+
return isRecord(parsed) ? parsed : null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function readString(record, key) {
|
|
49
|
+
if (record === null || !Object.hasOwn(record, key))
|
|
50
|
+
return null;
|
|
51
|
+
const value = record[key];
|
|
52
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
53
|
+
}
|
|
54
|
+
function readNestedString(record, key, nested) {
|
|
55
|
+
if (record === null || !Object.hasOwn(record, key))
|
|
56
|
+
return null;
|
|
57
|
+
const inner = record[key];
|
|
58
|
+
return isRecord(inner) ? readString(inner, nested) : null;
|
|
59
|
+
}
|
|
60
|
+
function isAbsoluteString(value) {
|
|
61
|
+
return value !== null && path.isAbsolute(value);
|
|
62
|
+
}
|
|
63
|
+
function statIsDirectory(target) {
|
|
64
|
+
try {
|
|
65
|
+
return fs.statSync(target).isDirectory();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const code = isRecord(error) && typeof error.code === 'string' ? error.code : null;
|
|
69
|
+
return code === 'ENOENT' || code === 'ENOTDIR' ? false : 'unverified';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function exists(target) {
|
|
73
|
+
try {
|
|
74
|
+
return fs.existsSync(target);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function resolveLayout(host, homeDir, env) {
|
|
81
|
+
const paths = getInstallLayoutPaths(homeDir);
|
|
82
|
+
if (host === 'codex') {
|
|
83
|
+
const codexHome = resolveCodexHome(env, homeDir);
|
|
84
|
+
if (!exists(codexHome)) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
status: 'HOST_NOT_FOUND',
|
|
88
|
+
reason: 'codex_home_missing',
|
|
89
|
+
nextAction: 'Run Codex once on this machine so ~/.codex exists, then re-run `pd health --host codex`.',
|
|
90
|
+
payloadRoot: path.join(codexHome, 'plugins', 'cache'),
|
|
91
|
+
hostConfigPath: path.join(codexHome, 'plugins', 'cache'),
|
|
92
|
+
bindingPath: path.join(codexHome, 'plugins', 'cache'),
|
|
93
|
+
codexPinPath: null,
|
|
94
|
+
defaultRuntimeDir: path.join(codexHome, 'plugins', 'data', 'principles-disciple-principles', 'runtime'),
|
|
95
|
+
codexHome,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const cache = selectMarketplaceCacheVersionDir(codexHome);
|
|
99
|
+
const payloadRoot = cache === null
|
|
100
|
+
? path.join(codexHome, 'plugins', 'cache', 'principles', 'principles-disciple')
|
|
101
|
+
: path.join(codexHome, 'plugins', 'cache', 'principles', 'principles-disciple', cache.dirName);
|
|
102
|
+
if (cache === null) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 'NOT_INSTALLED',
|
|
106
|
+
reason: 'codex_plugin_payload_missing',
|
|
107
|
+
nextAction: 'Install the Codex plugin (`codex plugin add principles-disciple@principles`), then re-run $pd-control setup.',
|
|
108
|
+
payloadRoot,
|
|
109
|
+
hostConfigPath: path.join(payloadRoot, '.mcp.json'),
|
|
110
|
+
bindingPath: path.join(payloadRoot, 'mcp-binding.json'),
|
|
111
|
+
codexPinPath: null,
|
|
112
|
+
defaultRuntimeDir: path.join(codexHome, 'plugins', 'data', 'principles-disciple-principles', 'runtime'),
|
|
113
|
+
codexHome,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
payloadRoot,
|
|
119
|
+
hostConfigPath: path.join(payloadRoot, '.mcp.json'),
|
|
120
|
+
bindingPath: path.join(payloadRoot, 'mcp-binding.json'),
|
|
121
|
+
codexPinPath: path.join(payloadRoot, 'runtime-version.json'),
|
|
122
|
+
defaultRuntimeDir: path.join(codexHome, 'plugins', 'data', 'principles-disciple-principles', 'runtime'),
|
|
123
|
+
codexHome,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (host === 'openclaw') {
|
|
127
|
+
const payloadRoot = paths.openClawExtensionDir;
|
|
128
|
+
if (!exists(payloadRoot)) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
status: exists(paths.openClawDir) ? 'NOT_INSTALLED' : 'HOST_NOT_FOUND',
|
|
132
|
+
reason: exists(paths.openClawDir) ? 'openclaw_plugin_payload_missing' : 'openclaw_home_missing',
|
|
133
|
+
nextAction: 'Run `npx create-principles-disciple install --host openclaw` (or the OpenClaw setup once) so the PD plugin payload exists.',
|
|
134
|
+
payloadRoot,
|
|
135
|
+
hostConfigPath: path.join(payloadRoot, 'openclaw.plugin.json'),
|
|
136
|
+
bindingPath: path.join(payloadRoot, 'mcp-binding.json'),
|
|
137
|
+
codexPinPath: null,
|
|
138
|
+
defaultRuntimeDir: paths.runtimeDir,
|
|
139
|
+
codexHome: null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
payloadRoot,
|
|
145
|
+
hostConfigPath: path.join(payloadRoot, 'openclaw.plugin.json'),
|
|
146
|
+
bindingPath: path.join(payloadRoot, 'mcp-binding.json'),
|
|
147
|
+
codexPinPath: null,
|
|
148
|
+
defaultRuntimeDir: paths.runtimeDir,
|
|
149
|
+
codexHome: null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const payloadRoot = path.join(paths.pdDir, 'workbuddy-marketplace', 'principles-disciple');
|
|
153
|
+
if (!exists(payloadRoot)) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
status: 'NOT_INSTALLED',
|
|
157
|
+
reason: 'workbuddy_plugin_payload_missing',
|
|
158
|
+
nextAction: 'Run `npx create-principles-disciple install --host workbuddy` so the PD WorkBuddy marketplace payload exists.',
|
|
159
|
+
payloadRoot,
|
|
160
|
+
hostConfigPath: path.join(payloadRoot, '.mcp.json'),
|
|
161
|
+
bindingPath: path.join(payloadRoot, 'mcp-binding.json'),
|
|
162
|
+
codexPinPath: null,
|
|
163
|
+
defaultRuntimeDir: paths.runtimeDir,
|
|
164
|
+
codexHome: null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
payloadRoot,
|
|
170
|
+
hostConfigPath: path.join(payloadRoot, '.mcp.json'),
|
|
171
|
+
bindingPath: path.join(payloadRoot, 'mcp-binding.json'),
|
|
172
|
+
codexPinPath: null,
|
|
173
|
+
defaultRuntimeDir: paths.runtimeDir,
|
|
174
|
+
codexHome: null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function assessHostConfig(hostConfigPath) {
|
|
178
|
+
const record = readJsonRecord(hostConfigPath);
|
|
179
|
+
if (record === null)
|
|
180
|
+
return { path: hostConfigPath, registered: false, launcher: null };
|
|
181
|
+
const servers = record.mcpServers;
|
|
182
|
+
if (!isRecord(servers))
|
|
183
|
+
return { path: hostConfigPath, registered: false, launcher: null };
|
|
184
|
+
const entry = servers['pd-recall'];
|
|
185
|
+
if (!isRecord(entry))
|
|
186
|
+
return { path: hostConfigPath, registered: false, launcher: null };
|
|
187
|
+
const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === 'string') : [];
|
|
188
|
+
const launcher = args.find((arg) => path.basename(arg) === 'launcher.cjs') ?? null;
|
|
189
|
+
return { path: hostConfigPath, registered: launcher !== null, launcher };
|
|
190
|
+
}
|
|
191
|
+
function registrationConfigPath(host, homeDir, env) {
|
|
192
|
+
if (host === 'codex') {
|
|
193
|
+
const codexHome = env.CODEX_HOME !== undefined && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : path.join(homeDir, '.codex');
|
|
194
|
+
return path.join(codexHome, 'config.toml');
|
|
195
|
+
}
|
|
196
|
+
if (host === 'openclaw')
|
|
197
|
+
return path.join(homeDir, '.openclaw', 'openclaw.json');
|
|
198
|
+
return path.join(homeDir, '.workbuddy', 'mcp.json');
|
|
199
|
+
}
|
|
200
|
+
const PD_LAUNCHER_ID = 'pd-mcp-launcher.v1';
|
|
201
|
+
const PD_BINDING_SCHEMA_MARKER = 'pd.mcp-binding.v1';
|
|
202
|
+
const MAX_LAUNCHER_BYTES = 512 * 1024;
|
|
203
|
+
const HOST_MARKERS = {
|
|
204
|
+
codex: ['.codex-plugin', 'plugin.json'],
|
|
205
|
+
openclaw: ['openclaw.plugin.json'],
|
|
206
|
+
workbuddy: ['.codebuddy-plugin', 'plugin.json'],
|
|
207
|
+
};
|
|
208
|
+
function isPathWithin(child, root) {
|
|
209
|
+
const relative = path.relative(root, child);
|
|
210
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
211
|
+
}
|
|
212
|
+
/** Resolve BOTH sides before checking containment; lexical paths are not trust boundaries. */
|
|
213
|
+
function isInsideRoot(child, root) {
|
|
214
|
+
try {
|
|
215
|
+
const realRoot = fs.realpathSync(root);
|
|
216
|
+
const realChild = fs.realpathSync(child);
|
|
217
|
+
return isPathWithin(realChild, realRoot);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function pluginHostIdentity(pluginRoot, expectedHost) {
|
|
224
|
+
let realRoot;
|
|
225
|
+
try {
|
|
226
|
+
realRoot = fs.realpathSync(pluginRoot);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return 'unverifiable';
|
|
230
|
+
}
|
|
231
|
+
const detected = [];
|
|
232
|
+
for (const host of Object.keys(HOST_MARKERS)) {
|
|
233
|
+
const markerPath = path.join(realRoot, ...HOST_MARKERS[host]);
|
|
234
|
+
let markerStat;
|
|
235
|
+
try {
|
|
236
|
+
markerStat = fs.lstatSync(markerPath);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const code = isRecord(error) && typeof error.code === 'string' ? error.code : null;
|
|
240
|
+
if (code === 'ENOENT')
|
|
241
|
+
continue;
|
|
242
|
+
return 'unverifiable';
|
|
243
|
+
}
|
|
244
|
+
if (!markerStat.isFile() || markerStat.isSymbolicLink())
|
|
245
|
+
return 'unverifiable';
|
|
246
|
+
let realMarker;
|
|
247
|
+
try {
|
|
248
|
+
realMarker = fs.realpathSync(markerPath);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return 'unverifiable';
|
|
252
|
+
}
|
|
253
|
+
if (!isPathWithin(realMarker, realRoot))
|
|
254
|
+
return 'unverifiable';
|
|
255
|
+
let markerRecord;
|
|
256
|
+
try {
|
|
257
|
+
markerRecord = JSON.parse(fs.readFileSync(realMarker, 'utf8'));
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return 'unverifiable';
|
|
261
|
+
}
|
|
262
|
+
const markerKey = host === 'openclaw' ? 'id' : 'name';
|
|
263
|
+
if (!isRecord(markerRecord) || markerRecord[markerKey] !== 'principles-disciple')
|
|
264
|
+
return 'unverifiable';
|
|
265
|
+
detected.push(host);
|
|
266
|
+
}
|
|
267
|
+
if (detected.length !== 1)
|
|
268
|
+
return 'unverifiable';
|
|
269
|
+
return detected[0] === expectedHost ? 'match' : 'other';
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* SPEC §7.5/§11.3: prove launcher identity by content AND its unique host
|
|
273
|
+
* marker. The three launchers are byte-identical, so content stamps alone
|
|
274
|
+
* cannot establish which host payload owns the file.
|
|
275
|
+
*/
|
|
276
|
+
function launcherIdentity(file, pdRoots, expectedHost) {
|
|
277
|
+
if (file.includes('${') || !path.isAbsolute(file))
|
|
278
|
+
return 'unverifiable';
|
|
279
|
+
const resolved = path.resolve(file);
|
|
280
|
+
let real;
|
|
281
|
+
try {
|
|
282
|
+
real = fs.realpathSync(resolved);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return 'unverifiable';
|
|
286
|
+
}
|
|
287
|
+
if (path.basename(real).toLowerCase() !== 'launcher.cjs'
|
|
288
|
+
|| path.basename(path.dirname(real)).toLowerCase() !== 'mcp')
|
|
289
|
+
return 'conflict';
|
|
290
|
+
// A path under a known payload root must resolve inside its real directory;
|
|
291
|
+
// a junction/symlink cannot borrow the root's host marker.
|
|
292
|
+
for (const root of pdRoots) {
|
|
293
|
+
if (isPathWithin(resolved, path.resolve(root)) && !isInsideRoot(real, root))
|
|
294
|
+
return 'unverifiable';
|
|
295
|
+
}
|
|
296
|
+
// One open descriptor: fstat+read on the same handle removes the
|
|
297
|
+
// stat-then-read window (CodeQL js/file-system-race).
|
|
298
|
+
let fd;
|
|
299
|
+
try {
|
|
300
|
+
fd = fs.openSync(real, 'r');
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return 'unverifiable';
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const stat = fs.fstatSync(fd);
|
|
307
|
+
if (!stat.isFile())
|
|
308
|
+
return 'unverifiable';
|
|
309
|
+
if (stat.size > MAX_LAUNCHER_BYTES)
|
|
310
|
+
return 'conflict';
|
|
311
|
+
const buffer = Buffer.alloc(stat.size);
|
|
312
|
+
fs.readSync(fd, buffer, 0, stat.size, 0);
|
|
313
|
+
const content = buffer.toString('utf8');
|
|
314
|
+
if (!content.includes(PD_LAUNCHER_ID) || !content.includes(PD_BINDING_SCHEMA_MARKER))
|
|
315
|
+
return 'conflict';
|
|
316
|
+
const hostIdentity = pluginHostIdentity(path.dirname(path.dirname(real)), expectedHost);
|
|
317
|
+
return hostIdentity === 'match' ? 'pd_owned' : hostIdentity === 'other' ? 'conflict' : 'unverifiable';
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return 'unverifiable';
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
fs.closeSync(fd);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function launcherOwnsEntry(entry, pdRoots, expectedHost) {
|
|
327
|
+
const command = readString(entry, 'command');
|
|
328
|
+
const url = readString(entry, 'url');
|
|
329
|
+
if (command === null)
|
|
330
|
+
return url !== null ? 'conflict' : 'unverifiable';
|
|
331
|
+
const base = path.basename(command).toLowerCase();
|
|
332
|
+
if (base !== 'node' && base !== 'node.exe' && base !== path.basename(process.execPath).toLowerCase())
|
|
333
|
+
return 'conflict';
|
|
334
|
+
const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === 'string') : null;
|
|
335
|
+
if (args === null)
|
|
336
|
+
return 'unverifiable';
|
|
337
|
+
const launcherArg = args.find((arg) => path.basename(arg.replace(/\\/g, '/')) === 'launcher.cjs'
|
|
338
|
+
&& path.basename(path.dirname(arg.replace(/\\/g, '/'))) === 'mcp');
|
|
339
|
+
if (launcherArg === undefined)
|
|
340
|
+
return 'conflict';
|
|
341
|
+
return launcherIdentity(launcherArg, pdRoots, expectedHost);
|
|
342
|
+
}
|
|
343
|
+
function classifyJsonServers(servers, pdRoots, expectedHost) {
|
|
344
|
+
if (!isRecord(servers))
|
|
345
|
+
return 'unverifiable';
|
|
346
|
+
if (!Object.hasOwn(servers, 'pd-recall'))
|
|
347
|
+
return Object.keys(servers).length === 0 ? 'absent' : 'other_servers_only';
|
|
348
|
+
const entry = servers['pd-recall'];
|
|
349
|
+
return isRecord(entry) ? launcherOwnsEntry(entry, pdRoots, expectedHost) : 'unverifiable';
|
|
350
|
+
}
|
|
351
|
+
const TOML_PD_SECTION = /^\s*\[\s*mcp_servers\.(?:"pd-recall"|'pd-recall'|pd-recall)\s*\]\s*(?:#.*)?$/;
|
|
352
|
+
const TOML_PD_CHILD = /^\s*\[\s*mcp_servers\.(?:"pd-recall"|'pd-recall'|pd-recall)\./;
|
|
353
|
+
/**
|
|
354
|
+
* Minimal Codex `config.toml` reader for the `pd-recall` entry. Every
|
|
355
|
+
* `mcp_servers` construct this scan cannot model is `unverifiable`, never
|
|
356
|
+
* "absent" (rc-1: unknown ownership must not read as a free name).
|
|
357
|
+
*/
|
|
358
|
+
function classifyCodexToml(raw, pdRoots) {
|
|
359
|
+
let inPd = false;
|
|
360
|
+
let found = false;
|
|
361
|
+
let opaque = false;
|
|
362
|
+
let command = null;
|
|
363
|
+
let args = null;
|
|
364
|
+
let url = null;
|
|
365
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
366
|
+
const trimmed = line.trim();
|
|
367
|
+
if (trimmed.length === 0 || trimmed.startsWith('#'))
|
|
368
|
+
continue;
|
|
369
|
+
if (/^\s*\[/.test(line)) {
|
|
370
|
+
if (TOML_PD_SECTION.test(line)) {
|
|
371
|
+
inPd = true;
|
|
372
|
+
found = true;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
inPd = false;
|
|
376
|
+
if (TOML_PD_CHILD.test(line))
|
|
377
|
+
found = true;
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (/^\s*mcp_servers\s*=/.test(line)) {
|
|
382
|
+
// Inline table form — ownership unknowable.
|
|
383
|
+
found = true;
|
|
384
|
+
opaque = true;
|
|
385
|
+
inPd = false;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (!inPd)
|
|
389
|
+
continue;
|
|
390
|
+
const pair = /^\s*(command|args|url)\s*=\s*(.*)$/.exec(line);
|
|
391
|
+
if (!pair)
|
|
392
|
+
continue;
|
|
393
|
+
const [, key, value] = pair;
|
|
394
|
+
if (key === undefined || value === undefined)
|
|
395
|
+
continue;
|
|
396
|
+
if (key === 'command') {
|
|
397
|
+
command = /^"(.*)"$/.exec(value.trim())?.[1] ?? (/^'(.*)'$/.exec(value.trim())?.[1] ?? null);
|
|
398
|
+
if (command === null)
|
|
399
|
+
opaque = true;
|
|
400
|
+
}
|
|
401
|
+
else if (key === 'url') {
|
|
402
|
+
url = /^"(.*)"$/.exec(value.trim())?.[1] ?? (/^'(.*)'$/.exec(value.trim())?.[1] ?? null);
|
|
403
|
+
if (url === null)
|
|
404
|
+
opaque = true;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
try {
|
|
408
|
+
const parsed = JSON.parse(value.trim().replace(/'/g, '"'));
|
|
409
|
+
args = Array.isArray(parsed) ? parsed : null;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
args = null;
|
|
413
|
+
}
|
|
414
|
+
if (args === null)
|
|
415
|
+
opaque = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (!found)
|
|
419
|
+
return 'absent';
|
|
420
|
+
if (opaque)
|
|
421
|
+
return 'unverifiable';
|
|
422
|
+
const entry = {};
|
|
423
|
+
if (command !== null)
|
|
424
|
+
entry.command = command;
|
|
425
|
+
if (args !== null)
|
|
426
|
+
entry.args = args;
|
|
427
|
+
if (url !== null)
|
|
428
|
+
entry.url = url;
|
|
429
|
+
return launcherOwnsEntry(entry, pdRoots, 'codex');
|
|
430
|
+
}
|
|
431
|
+
function assessRegistrationConflict(input) {
|
|
432
|
+
const { host, homeDir, env, pdRoots } = input;
|
|
433
|
+
const configPath = registrationConfigPath(host, homeDir, env);
|
|
434
|
+
let raw;
|
|
435
|
+
try {
|
|
436
|
+
raw = fs.readFileSync(configPath, 'utf8');
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
const code = isRecord(error) && typeof error.code === 'string' ? error.code : null;
|
|
440
|
+
return code === 'ENOENT' || code === 'ENOTDIR' ? 'absent' : 'unverified';
|
|
441
|
+
}
|
|
442
|
+
if (host === 'codex')
|
|
443
|
+
return classifyCodexToml(raw, pdRoots);
|
|
444
|
+
let parsed;
|
|
445
|
+
try {
|
|
446
|
+
parsed = JSON.parse(raw);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return 'unverifiable';
|
|
450
|
+
}
|
|
451
|
+
if (!isRecord(parsed))
|
|
452
|
+
return 'unverifiable';
|
|
453
|
+
if (host === 'openclaw') {
|
|
454
|
+
if (parsed.mcp === undefined)
|
|
455
|
+
return 'absent';
|
|
456
|
+
if (!isRecord(parsed.mcp))
|
|
457
|
+
return 'unverifiable';
|
|
458
|
+
if (parsed.mcp.servers === undefined)
|
|
459
|
+
return 'absent';
|
|
460
|
+
return classifyJsonServers(parsed.mcp.servers, pdRoots, host);
|
|
461
|
+
}
|
|
462
|
+
if (parsed.mcpServers === undefined)
|
|
463
|
+
return 'absent';
|
|
464
|
+
return classifyJsonServers(parsed.mcpServers, pdRoots, host);
|
|
465
|
+
}
|
|
466
|
+
function baseSection({ host, layout, homeDir, env, status, reason, nextAction }) {
|
|
467
|
+
const entry = path.join(layout.defaultRuntimeDir, ...SERVER_ENTRY_SEGMENTS[host]);
|
|
468
|
+
return {
|
|
469
|
+
host,
|
|
470
|
+
status,
|
|
471
|
+
server: {
|
|
472
|
+
runtimeDir: layout.defaultRuntimeDir,
|
|
473
|
+
entry,
|
|
474
|
+
entryExists: exists(entry),
|
|
475
|
+
version: null,
|
|
476
|
+
pin: null,
|
|
477
|
+
pinSource: null,
|
|
478
|
+
versionMatchesPin: 'unverified',
|
|
479
|
+
},
|
|
480
|
+
binding: {
|
|
481
|
+
path: layout.bindingPath,
|
|
482
|
+
exists: exists(layout.bindingPath),
|
|
483
|
+
schemaVersion: null,
|
|
484
|
+
host: null,
|
|
485
|
+
workspaceDir: null,
|
|
486
|
+
runtimeDir: null,
|
|
487
|
+
serverEntry: null,
|
|
488
|
+
canonical: 'unverified',
|
|
489
|
+
insideRuntime: 'unverified',
|
|
490
|
+
versionMatchesInstalled: 'unverified',
|
|
491
|
+
},
|
|
492
|
+
hostConfig: assessHostConfig(layout.hostConfigPath),
|
|
493
|
+
workspace: { dir: null, exists: 'unverified' },
|
|
494
|
+
registrationConflict: {
|
|
495
|
+
configPath: registrationConfigPath(host, homeDir, env),
|
|
496
|
+
verdict: 'unverified',
|
|
497
|
+
},
|
|
498
|
+
restart: { required: 'unverified', reason: null },
|
|
499
|
+
probe: { performed: false, reason: 'health_is_read_only_and_does_not_spawn_the_server' },
|
|
500
|
+
reason,
|
|
501
|
+
nextAction,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
export function assessMcpHealth(options) {
|
|
505
|
+
const { host } = options;
|
|
506
|
+
const homeDir = path.resolve(options.homeDir ?? os.homedir());
|
|
507
|
+
const layout = resolveLayout(host, homeDir, options.env ?? {});
|
|
508
|
+
if (!layout.ok) {
|
|
509
|
+
return baseSection({
|
|
510
|
+
host,
|
|
511
|
+
layout,
|
|
512
|
+
homeDir,
|
|
513
|
+
env: options.env ?? {},
|
|
514
|
+
status: layout.status ?? 'NOT_INSTALLED',
|
|
515
|
+
reason: layout.reason ?? 'install_state_unverified',
|
|
516
|
+
nextAction: layout.nextAction ?? SETUP_NEXT_ACTION,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
// SPEC §7.5/§11.3: a same-name registration owned by another server (or an
|
|
520
|
+
// ownership that cannot be proven) precedes every other layer — PD must not
|
|
521
|
+
// register on top of it, so the host's MCP wiring is NOT healthy.
|
|
522
|
+
const conflictConfigPath = registrationConfigPath(host, homeDir, options.env ?? {});
|
|
523
|
+
const registrationConflict = assessRegistrationConflict({ host, homeDir, env: options.env ?? {}, pdRoots: [layout.payloadRoot] });
|
|
524
|
+
const bindingRecord = readJsonRecord(layout.bindingPath);
|
|
525
|
+
const bindingExists = exists(layout.bindingPath);
|
|
526
|
+
const bindingRuntimeDir = readString(bindingRecord, 'runtimeDir');
|
|
527
|
+
const runtimeDir = isAbsoluteString(bindingRuntimeDir) ? bindingRuntimeDir : layout.defaultRuntimeDir;
|
|
528
|
+
const entry = path.join(runtimeDir, ...SERVER_ENTRY_SEGMENTS[host]);
|
|
529
|
+
const entryExists = exists(entry);
|
|
530
|
+
const packageRoot = path.dirname(path.dirname(entry));
|
|
531
|
+
const installedVersion = readString(readJsonRecord(path.join(packageRoot, 'package.json')), 'version');
|
|
532
|
+
// Runtime Pin: Codex ships it in the plugin payload; OpenClaw/WorkBuddy
|
|
533
|
+
// record the same exact version in the binding they write at setup.
|
|
534
|
+
let pin = null;
|
|
535
|
+
let pinSource = null;
|
|
536
|
+
let pinProblem = null;
|
|
537
|
+
if (host === 'codex') {
|
|
538
|
+
const pinRecord = readJsonRecord(layout.codexPinPath);
|
|
539
|
+
const pinned = readString(pinRecord, 'mcpServer');
|
|
540
|
+
if (pinned === null)
|
|
541
|
+
pinProblem = 'runtime_pin_unreadable';
|
|
542
|
+
else {
|
|
543
|
+
pin = pinned;
|
|
544
|
+
pinSource = 'runtime-version.json';
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const bindingPin = readNestedString(bindingRecord, 'versions', 'mcpServer');
|
|
549
|
+
if (bindingPin === null)
|
|
550
|
+
pinProblem = 'binding_version_unreadable';
|
|
551
|
+
else {
|
|
552
|
+
pin = bindingPin;
|
|
553
|
+
pinSource = 'binding.versions.mcpServer';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const versionMatchesPin = installedVersion !== null && pin !== null ? installedVersion === pin : 'unverified';
|
|
557
|
+
const bindingVersion = readNestedString(bindingRecord, 'versions', 'mcpServer');
|
|
558
|
+
const bindingVersionMatchesInstalled = bindingVersion !== null && installedVersion !== null ? bindingVersion === installedVersion : 'unverified';
|
|
559
|
+
const bindingServerEntry = readString(bindingRecord, 'serverEntry');
|
|
560
|
+
const canonical = bindingServerEntry === null ? 'unverified'
|
|
561
|
+
: path.resolve(bindingServerEntry) === path.resolve(entry);
|
|
562
|
+
let insideRuntime = 'unverified';
|
|
563
|
+
if (entryExists) {
|
|
564
|
+
try {
|
|
565
|
+
const realRuntime = fs.realpathSync(runtimeDir);
|
|
566
|
+
const realEntry = fs.realpathSync(entry);
|
|
567
|
+
insideRuntime = realEntry === realRuntime || realEntry.startsWith(realRuntime + path.sep);
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
insideRuntime = 'unverified';
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const bindingWorkspaceDir = readString(bindingRecord, 'workspaceDir');
|
|
574
|
+
const workspace = {
|
|
575
|
+
dir: bindingWorkspaceDir,
|
|
576
|
+
exists: bindingWorkspaceDir === null || !path.isAbsolute(bindingWorkspaceDir)
|
|
577
|
+
? 'unverified'
|
|
578
|
+
: statIsDirectory(bindingWorkspaceDir),
|
|
579
|
+
};
|
|
580
|
+
let restart = { required: 'unverified', reason: 'host_process_start_not_observable' };
|
|
581
|
+
if (host === 'codex') {
|
|
582
|
+
const writtenAtRaw = readString(bindingRecord, 'writtenAt');
|
|
583
|
+
const writtenAt = writtenAtRaw === null ? Number.NaN : Date.parse(writtenAtRaw);
|
|
584
|
+
const newest = layout.codexHome === null ? null : findNewestRolloutFile(layout.codexHome);
|
|
585
|
+
if (newest !== null && newest.birthtimeMs > 0 && Number.isFinite(writtenAt)) {
|
|
586
|
+
restart = writtenAt > newest.birthtimeMs
|
|
587
|
+
? { required: true, reason: 'binding_written_after_newest_codex_session_started' }
|
|
588
|
+
: { required: false, reason: null };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const binding = {
|
|
592
|
+
path: layout.bindingPath,
|
|
593
|
+
exists: bindingExists,
|
|
594
|
+
schemaVersion: readString(bindingRecord, 'schemaVersion'),
|
|
595
|
+
host: readString(bindingRecord, 'host'),
|
|
596
|
+
workspaceDir: bindingWorkspaceDir,
|
|
597
|
+
runtimeDir: bindingRuntimeDir,
|
|
598
|
+
serverEntry: bindingServerEntry,
|
|
599
|
+
canonical,
|
|
600
|
+
insideRuntime,
|
|
601
|
+
versionMatchesInstalled: bindingVersionMatchesInstalled,
|
|
602
|
+
};
|
|
603
|
+
const hostConfig = assessHostConfig(layout.hostConfigPath);
|
|
604
|
+
const server = {
|
|
605
|
+
runtimeDir,
|
|
606
|
+
entry,
|
|
607
|
+
entryExists,
|
|
608
|
+
version: installedVersion,
|
|
609
|
+
pin,
|
|
610
|
+
pinSource,
|
|
611
|
+
versionMatchesPin,
|
|
612
|
+
};
|
|
613
|
+
// ── Derive the single status (first failing layer wins; SPEC §7.8) ────────
|
|
614
|
+
let status = 'HEALTHY';
|
|
615
|
+
let reason;
|
|
616
|
+
let nextAction;
|
|
617
|
+
const degrade = (next, why, action) => {
|
|
618
|
+
if (reason === undefined) {
|
|
619
|
+
status = next;
|
|
620
|
+
reason = why;
|
|
621
|
+
nextAction = action;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
if (registrationConflict === 'conflict') {
|
|
625
|
+
degrade('CONFIG_DRIFT', 'mcp_same_name_conflict', `The "${'pd-recall'}" launcher in ${conflictConfigPath} is not owned by ${host} (it may belong to a third party or another PD host). Point it at this host's own launcher or remove/rename the foreign entry, then re-install PD and restart the host.`);
|
|
626
|
+
}
|
|
627
|
+
else if (registrationConflict === 'unverifiable' || registrationConflict === 'unverified') {
|
|
628
|
+
degrade('CONFIG_DRIFT', 'mcp_registration_unverifiable', `The "${'pd-recall'}" ownership in ${conflictConfigPath} cannot be verified. Inspect it manually; PD never guesses whether an existing entry is its own.`);
|
|
629
|
+
}
|
|
630
|
+
if (!bindingExists) {
|
|
631
|
+
degrade('CONFIG_MISSING', 'mcp_binding_missing', SETUP_NEXT_ACTION);
|
|
632
|
+
}
|
|
633
|
+
else if (bindingRecord === null) {
|
|
634
|
+
degrade('CONFIG_DRIFT', 'mcp_binding_unreadable', SETUP_NEXT_ACTION);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
if (binding.schemaVersion !== MCP_BINDING_SCHEMA) {
|
|
638
|
+
degrade('CONFIG_DRIFT', 'binding_schema_unsupported', SETUP_NEXT_ACTION);
|
|
639
|
+
}
|
|
640
|
+
if (binding.host !== host) {
|
|
641
|
+
degrade('CONFIG_DRIFT', 'binding_host_mismatch', 'Re-run the PD host setup for THIS host; a binding written by another host payload is refused by the launcher.');
|
|
642
|
+
}
|
|
643
|
+
if (!isAbsoluteString(binding.runtimeDir) || !isAbsoluteString(binding.serverEntry)) {
|
|
644
|
+
degrade('CONFIG_DRIFT', 'binding_paths_not_absolute', SETUP_NEXT_ACTION);
|
|
645
|
+
}
|
|
646
|
+
if (bindingWorkspaceDir !== null && !path.isAbsolute(bindingWorkspaceDir)) {
|
|
647
|
+
degrade('CONFIG_DRIFT', 'binding_workspace_invalid', SETUP_NEXT_ACTION);
|
|
648
|
+
}
|
|
649
|
+
if (canonical === false) {
|
|
650
|
+
degrade('CONFIG_DRIFT', 'server_entry_not_canonical', SETUP_NEXT_ACTION);
|
|
651
|
+
}
|
|
652
|
+
if (!entryExists) {
|
|
653
|
+
degrade('HOST_RUNTIME_NOT_VERIFIED', 'server_entry_missing', 'Re-run the PD host setup so the pinned MCP server package is installed.');
|
|
654
|
+
}
|
|
655
|
+
if (insideRuntime === false) {
|
|
656
|
+
degrade('CONFIG_DRIFT', 'server_entry_escape', SETUP_NEXT_ACTION);
|
|
657
|
+
}
|
|
658
|
+
if (workspace.exists === false) {
|
|
659
|
+
degrade('CONFIG_DRIFT', 'binding_workspace_missing', 'The bound governance workspace no longer exists. Reinstall PD for this host or re-run setup with the correct workspace.');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (versionMatchesPin === false || bindingVersionMatchesInstalled === false) {
|
|
663
|
+
degrade('VERSION_MISMATCH', 'installed_mcp_server_version_mismatch', 'Re-run the PD host setup so the pinned MCP server package bytes are reinstalled.');
|
|
664
|
+
}
|
|
665
|
+
else if (pinProblem !== null) {
|
|
666
|
+
degrade('HOST_RUNTIME_NOT_VERIFIED', pinProblem, host === 'codex'
|
|
667
|
+
? 'Reinstall the Codex plugin so runtime-version.json is readable, then re-run $pd-control setup.'
|
|
668
|
+
: SETUP_NEXT_ACTION);
|
|
669
|
+
}
|
|
670
|
+
if (hostConfig.registered === false) {
|
|
671
|
+
degrade('CONFIG_DRIFT', 'host_mcp_config_not_registered', `Repair the host MCP config at ${layout.hostConfigPath} (add mcpServers.pd-recall with ./mcp/launcher.cjs) or reinstall the plugin.`);
|
|
672
|
+
}
|
|
673
|
+
if (restart.required === true && reason === undefined) {
|
|
674
|
+
status = 'RESTART_REQUIRED';
|
|
675
|
+
reason = restart.reason ?? 'restart_required';
|
|
676
|
+
nextAction = 'Restart the host so its MCP child process reloads the current mcp-binding.json.';
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
host,
|
|
680
|
+
status,
|
|
681
|
+
server,
|
|
682
|
+
binding,
|
|
683
|
+
hostConfig,
|
|
684
|
+
workspace,
|
|
685
|
+
registrationConflict: { configPath: conflictConfigPath, verdict: registrationConflict },
|
|
686
|
+
restart,
|
|
687
|
+
probe: { performed: false, reason: 'health_is_read_only_and_does_not_spawn_the_server' },
|
|
688
|
+
...(reason !== undefined ? { reason, nextAction } : {}),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/** Shared text rendering so every host health surface reports the same fields. */
|
|
692
|
+
export function formatMcpHealthLines(section) {
|
|
693
|
+
const tri = (value) => value === 'unverified' ? 'unverified' : String(value);
|
|
694
|
+
const lines = [
|
|
695
|
+
`mcp.status: ${section.status}`,
|
|
696
|
+
`mcp.server.runtimeDir: ${section.server.runtimeDir}`,
|
|
697
|
+
`mcp.server.entry: ${section.server.entry}`,
|
|
698
|
+
`mcp.server.entryExists: ${section.server.entryExists}`,
|
|
699
|
+
`mcp.server.version: ${section.server.version ?? 'unknown'}`,
|
|
700
|
+
`mcp.server.pin: ${section.server.pin ?? 'unverified'}${section.server.pinSource !== null ? ` (${section.server.pinSource})` : ''}`,
|
|
701
|
+
`mcp.server.versionMatchesPin: ${tri(section.server.versionMatchesPin)}`,
|
|
702
|
+
`mcp.binding.path: ${section.binding.path}`,
|
|
703
|
+
`mcp.binding.exists: ${section.binding.exists}`,
|
|
704
|
+
`mcp.binding.schemaVersion: ${section.binding.schemaVersion ?? 'missing'}`,
|
|
705
|
+
`mcp.binding.host: ${section.binding.host ?? 'missing'}`,
|
|
706
|
+
`mcp.binding.workspaceDir: ${section.binding.workspaceDir ?? 'missing'}`,
|
|
707
|
+
`mcp.binding.canonical: ${tri(section.binding.canonical)}`,
|
|
708
|
+
`mcp.binding.insideRuntime: ${tri(section.binding.insideRuntime)}`,
|
|
709
|
+
`mcp.binding.versionMatchesInstalled: ${tri(section.binding.versionMatchesInstalled)}`,
|
|
710
|
+
`mcp.hostConfig.path: ${section.hostConfig.path}`,
|
|
711
|
+
`mcp.hostConfig.registered: ${tri(section.hostConfig.registered)}`,
|
|
712
|
+
`mcp.registrationConflict.path: ${section.registrationConflict.configPath}`,
|
|
713
|
+
`mcp.registrationConflict.verdict: ${section.registrationConflict.verdict}`,
|
|
714
|
+
`mcp.workspace.exists: ${tri(section.workspace.exists)}`,
|
|
715
|
+
`mcp.restart.required: ${tri(section.restart.required)}${section.restart.reason !== null ? ` (${section.restart.reason})` : ''}`,
|
|
716
|
+
];
|
|
717
|
+
if (section.reason !== undefined) {
|
|
718
|
+
lines.push(`mcp.reason: ${section.reason}`);
|
|
719
|
+
lines.push(`mcp.nextAction: ${section.nextAction ?? ''}`);
|
|
720
|
+
}
|
|
721
|
+
return lines;
|
|
722
|
+
}
|
|
723
|
+
//# sourceMappingURL=health-mcp.js.map
|