create-principles-disciple 1.147.7 → 1.147.8
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/core/dist/host/host-installer.d.ts +15 -0
- package/core/dist/host/host-installer.d.ts.map +1 -1
- package/core/dist/host/host-installer.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js +2 -2
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js.map +1 -1
- package/core/dist/runtime-v2/pain-recall-capability.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-recall-capability.js +53 -2
- package/core/dist/runtime-v2/pain-recall-capability.js.map +1 -1
- package/core/dist/runtime-v2/pain-recall-read-model.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-recall-read-model.js +60 -5
- package/core/dist/runtime-v2/pain-recall-read-model.js.map +1 -1
- package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts.map +1 -1
- package/core/dist/runtime-v2/signal-collector/keyword-stage.js +35 -0
- package/core/dist/runtime-v2/signal-collector/keyword-stage.js.map +1 -1
- package/core/package.json +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -29
- package/dist/index.js.map +1 -1
- package/dist/install-summary.d.ts +38 -0
- package/dist/install-summary.d.ts.map +1 -0
- package/dist/install-summary.js +115 -0
- package/dist/install-summary.js.map +1 -0
- package/dist/installer.d.ts +36 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +120 -26
- package/dist/installer.js.map +1 -1
- package/dist/installers/workbuddy-host-installer.d.ts +51 -17
- package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/dist/installers/workbuddy-host-installer.js +510 -186
- package/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/dist/prompts.js +1 -1
- package/dist/prompts.js.map +1 -1
- package/host-runtime/dist/governance-signal-admission.d.ts +2 -2
- package/host-runtime/dist/governance-signal-admission.js +2 -6
- package/host-runtime/package.json +1 -1
- package/package.json +3 -3
- package/pd-cli/dist/commands/health-mcp.d.ts +19 -0
- package/pd-cli/dist/commands/health-mcp.d.ts.map +1 -1
- package/pd-cli/dist/commands/health-mcp.js +226 -0
- package/pd-cli/dist/commands/health-mcp.js.map +1 -1
- package/pd-cli/dist/commands/health.d.ts.map +1 -1
- package/pd-cli/dist/commands/health.js +18 -1
- package/pd-cli/dist/commands/health.js.map +1 -1
- package/pd-cli/dist/commands/mcp-logs-export.d.ts +7 -0
- package/pd-cli/dist/commands/mcp-logs-export.d.ts.map +1 -0
- package/pd-cli/dist/commands/mcp-logs-export.js +267 -0
- package/pd-cli/dist/commands/mcp-logs-export.js.map +1 -0
- package/pd-cli/dist/commands/workbuddy-worker.d.ts.map +1 -1
- package/pd-cli/dist/commands/workbuddy-worker.js +2 -1
- package/pd-cli/dist/commands/workbuddy-worker.js.map +1 -1
- package/pd-cli/dist/index.js +35 -0
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/dist/services/zip-writer.d.ts +6 -0
- package/pd-cli/dist/services/zip-writer.d.ts.map +1 -0
- package/pd-cli/dist/services/zip-writer.js +97 -0
- package/pd-cli/dist/services/zip-writer.js.map +1 -0
- package/pd-mcp-server/dist/index.d.ts +1 -0
- package/pd-mcp-server/dist/index.js +167 -10
- package/pd-mcp-server/dist/log-file.d.ts +26 -0
- package/pd-mcp-server/dist/log-file.js +312 -0
- package/pd-mcp-server/dist/logging.d.ts +2 -1
- package/pd-mcp-server/dist/logging.js +25 -14
- package/pd-mcp-server/dist/server.js +78 -24
- package/pd-mcp-server/dist/version.d.ts +1 -1
- package/pd-mcp-server/dist/version.js +1 -1
- package/pd-mcp-server/dist/workspace.d.ts +3 -0
- package/pd-mcp-server/dist/workspace.js +5 -0
- package/pd-mcp-server/package.json +1 -1
- package/plugin/dist/bundle.js +274 -273
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/mcp/launcher.cjs +561 -0
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/dist/installer.js +120 -26
- package/release-manager/dist/installers/workbuddy-host-installer.js +510 -186
- package/release-manager/package.json +1 -1
- package/workbuddy-adapter/dist/pd-hook.js +3 -2
- package/workbuddy-adapter/package.json +1 -1
- package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +1 -1
- package/workbuddy-adapter/plugin/mcp/launcher.cjs +215 -1
- package/workbuddy-adapter/plugin/skills/pd-pain-signal/SKILL.md +4 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Principles Disciple is an AI Agent Governance System. Stop correcting the same AI behavior across sessions. Turn repeated Agent corrections into Owner-approved, observable, reversible behavior principles.",
|
|
5
|
-
"version": "2.6.
|
|
5
|
+
"version": "2.6.1",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PD MCP launcher — SPEC-PD-MCP-GATE4-001 §3.3/§3.5 (Gate 4-A freeze).
|
|
4
|
+
*
|
|
5
|
+
* This is the CANONICAL copy. The WorkBuddy plugin payload
|
|
6
|
+
* (packages/workbuddy-adapter/plugin/mcp/launcher.cjs) and the OpenClaw
|
|
7
|
+
* plugin payload (packages/openclaw-plugin/mcp/launcher.cjs) carry
|
|
8
|
+
* byte-identical copies — pinned by the pd-mcp-server architecture guard
|
|
9
|
+
* test G4-7. Change it here first, then propagate.
|
|
10
|
+
*
|
|
11
|
+
* The host starts this launcher via the plugin's PD-owned MCP config
|
|
12
|
+
* (Codex: .mcp.json with cwd = installed plugin root; WorkBuddy:
|
|
13
|
+
* ${CODEBUDDY_PLUGIN_ROOT} placeholders; OpenClaw: plugin-root-relative
|
|
14
|
+
* args). It reads the PD-owned machine-local `mcp-binding.json` beside the
|
|
15
|
+
* plugin payload root, validates it strictly against the payload's own host
|
|
16
|
+
* marker and the installed package bytes, injects the trusted env, and runs
|
|
17
|
+
* the shared server entry IN-PROCESS — one process per host, and the host
|
|
18
|
+
* owns this PID's lifecycle (no daemon, no extra children).
|
|
19
|
+
*
|
|
20
|
+
* Binding security (Gate 4 P2): the binding is untrusted input. The host is
|
|
21
|
+
* derived from the payload marker beside this launcher (never from the
|
|
22
|
+
* binding itself), the entry must equal the canonical path for that host's
|
|
23
|
+
* runtime layout, symlinks may not escape the recorded runtime directory, and
|
|
24
|
+
* the installed package identity/version must agree with the binding and the
|
|
25
|
+
* plugin's runtime pin. A binding copied from another host fails closed.
|
|
26
|
+
*
|
|
27
|
+
* Fail-closed: a missing/invalid binding, host mismatch, workspace, or server
|
|
28
|
+
* entry exits
|
|
29
|
+
* 1 with ONE sanitized stderr JSON line BEFORE any MCP traffic — the host
|
|
30
|
+
* reports the server as failed to start (honest, recoverable state). No
|
|
31
|
+
* cwd/HOME fallback exists by design (SPEC §3.5).
|
|
32
|
+
*/
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const os = require('os');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
const { pathToFileURL } = require('url');
|
|
39
|
+
const crypto = require('crypto');
|
|
40
|
+
|
|
41
|
+
const BINDING_SCHEMA = 'pd.mcp-binding.v1';
|
|
42
|
+
// Ownership stamp (SPEC §7.5/§11.3): installers and `pd health` prove that a
|
|
43
|
+
// registered `mcp/launcher.cjs` really is PD's launcher by finding the stamp
|
|
44
|
+
// `pd-mcp-launcher.v1` in this file's bytes plus the binding schema above —
|
|
45
|
+
// never by the file name alone. Keep the stamp literally present.
|
|
46
|
+
const KNOWN_HOSTS = ['codex', 'openclaw', 'workbuddy'];
|
|
47
|
+
// The payload marker that identifies which host owns THIS launcher copy.
|
|
48
|
+
const HOST_MARKERS = [
|
|
49
|
+
['codex', path.join('.codex-plugin', 'plugin.json')],
|
|
50
|
+
['workbuddy', path.join('.codebuddy-plugin', 'plugin.json')],
|
|
51
|
+
['openclaw', 'openclaw.plugin.json'],
|
|
52
|
+
];
|
|
53
|
+
// Canonical server entry per host, relative to the binding's runtimeDir:
|
|
54
|
+
// Codex installs into its plugin-data npm runtime (node_modules layout);
|
|
55
|
+
// OpenClaw/WorkBuddy bind to the shared ~/.pd/runtime component layout.
|
|
56
|
+
const SERVER_ENTRY_SEGMENTS = {
|
|
57
|
+
codex: ['node_modules', '@principles', 'pd-mcp-server', 'dist', 'index.js'],
|
|
58
|
+
openclaw: ['pd-mcp-server', 'dist', 'index.js'],
|
|
59
|
+
workbuddy: ['pd-mcp-server', 'dist', 'index.js'],
|
|
60
|
+
};
|
|
61
|
+
const SERVER_PACKAGE_NAME = '@principles/pd-mcp-server';
|
|
62
|
+
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
63
|
+
|
|
64
|
+
// ── Durable observability log (default ON, PD_MCP_LOG=0 disables) ──────────
|
|
65
|
+
// Same file contract as the server: ~/.pd/logs/mcp, one JSONL file per
|
|
66
|
+
// process, 2 MiB/file, 64 MiB/dir, 14 days retention. The launcher writes
|
|
67
|
+
// `launcher_start` / `startup_failure` and then hands the SAME file to the
|
|
68
|
+
// server through PD_MCP_LOG_FILE (basename only) so a process keeps one file.
|
|
69
|
+
// Everything here is best-effort: a logging failure never changes the
|
|
70
|
+
// fail-closed decision or the stderr line the host parses.
|
|
71
|
+
const LAUNCHER_VERSION = 'pd-mcp-launcher.v1';
|
|
72
|
+
const LOG_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
73
|
+
const LOG_MAX_DIR_BYTES = 64 * 1024 * 1024;
|
|
74
|
+
const LOG_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
75
|
+
const LOG_MAX_SHARDS = 3;
|
|
76
|
+
const LOG_DISABLED = ['0', 'false', 'off'].includes(String(process.env.PD_MCP_LOG || '').trim().toLowerCase());
|
|
77
|
+
const INITIAL_ENV_PRESENCE = Object.freeze({
|
|
78
|
+
PD_MCP_WORKSPACE_DIR: typeof process.env.PD_MCP_WORKSPACE_DIR === 'string' && process.env.PD_MCP_WORKSPACE_DIR.length > 0,
|
|
79
|
+
PD_MCP_HOST: typeof process.env.PD_MCP_HOST === 'string' && process.env.PD_MCP_HOST.length > 0,
|
|
80
|
+
PD_WORKSPACE_DIR: typeof process.env.PD_WORKSPACE_DIR === 'string' && process.env.PD_WORKSPACE_DIR.length > 0,
|
|
81
|
+
OPENCLAW_WORKSPACE: typeof process.env.OPENCLAW_WORKSPACE === 'string' && process.env.OPENCLAW_WORKSPACE.length > 0,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const logState = {
|
|
85
|
+
host: 'unknown',
|
|
86
|
+
bindingPathExists: false,
|
|
87
|
+
workspaceResolution: 'not_checked',
|
|
88
|
+
seq: 0,
|
|
89
|
+
runId: null,
|
|
90
|
+
fileName: null,
|
|
91
|
+
currentPath: null,
|
|
92
|
+
bytes: 0,
|
|
93
|
+
shard: 1,
|
|
94
|
+
exhausted: false,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function pad(value, width) {
|
|
98
|
+
return String(value).padStart(width || 2, '0');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function localTimestamp(date) {
|
|
102
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
103
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
104
|
+
const abs = Math.abs(offsetMinutes);
|
|
105
|
+
return {
|
|
106
|
+
ts:
|
|
107
|
+
`${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
|
108
|
+
`T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}` +
|
|
109
|
+
`${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`,
|
|
110
|
+
tzOffsetMinutes: offsetMinutes,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function logDir() {
|
|
115
|
+
const override = process.env.PD_MCP_LOG_DIR;
|
|
116
|
+
if (typeof override === 'string' && override.trim().length > 0) return path.resolve(override);
|
|
117
|
+
return path.join(os.homedir(), '.pd', 'logs', 'mcp');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sanitizeSegment(value) {
|
|
121
|
+
const cleaned = String(value).replace(/[^A-Za-z0-9_-]/g, '').slice(0, 40);
|
|
122
|
+
return cleaned.length > 0 ? cleaned : 'unknown';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function listOwnLogFiles(dir) {
|
|
126
|
+
try {
|
|
127
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
128
|
+
.filter((entry) => entry.isFile() && /^mcp-.*\.jsonl$/.test(entry.name))
|
|
129
|
+
.map((entry) => {
|
|
130
|
+
const fullPath = path.join(dir, entry.name);
|
|
131
|
+
try {
|
|
132
|
+
const stat = fs.statSync(fullPath);
|
|
133
|
+
return { fullPath, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
.filter((file) => file !== null);
|
|
139
|
+
} catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function maintainLogDir(dir, nowMs, keepPath) {
|
|
145
|
+
try {
|
|
146
|
+
const survivors = [];
|
|
147
|
+
for (const file of listOwnLogFiles(dir)) {
|
|
148
|
+
if (nowMs - file.mtimeMs > LOG_RETENTION_MS && file.fullPath !== keepPath) {
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(file.fullPath);
|
|
151
|
+
continue;
|
|
152
|
+
} catch {
|
|
153
|
+
// Keep it.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
survivors.push(file);
|
|
157
|
+
}
|
|
158
|
+
let total = survivors.reduce((sum, file) => sum + file.size, 0);
|
|
159
|
+
if (total <= LOG_MAX_DIR_BYTES) return;
|
|
160
|
+
for (const file of survivors.filter((f) => f.fullPath !== keepPath).sort((a, b) => a.mtimeMs - b.mtimeMs)) {
|
|
161
|
+
if (total <= LOG_MAX_DIR_BYTES) break;
|
|
162
|
+
try {
|
|
163
|
+
fs.unlinkSync(file.fullPath);
|
|
164
|
+
total -= file.size;
|
|
165
|
+
} catch {
|
|
166
|
+
// Leave it.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Maintenance is advisory only.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ensureLogFile() {
|
|
175
|
+
if (LOG_DISABLED) return null;
|
|
176
|
+
if (logState.currentPath !== null) return logState.currentPath;
|
|
177
|
+
try {
|
|
178
|
+
const dir = logDir();
|
|
179
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
180
|
+
if (!fs.statSync(dir).isDirectory()) return null;
|
|
181
|
+
maintainLogDir(dir, Date.now(), null);
|
|
182
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, 'Z');
|
|
183
|
+
logState.fileName = `mcp-${sanitizeSegment(logState.host)}-${process.pid}-${stamp}.jsonl`;
|
|
184
|
+
logState.runId = crypto.randomBytes(4).toString('hex');
|
|
185
|
+
logState.currentPath = path.join(dir, logState.fileName);
|
|
186
|
+
try {
|
|
187
|
+
logState.bytes = fs.statSync(logState.currentPath).size;
|
|
188
|
+
} catch {
|
|
189
|
+
logState.bytes = 0;
|
|
190
|
+
}
|
|
191
|
+
return logState.currentPath;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function writeLogEvent(level, event, fields) {
|
|
198
|
+
try {
|
|
199
|
+
const file = ensureLogFile();
|
|
200
|
+
if (file === null) return;
|
|
201
|
+
const dir = path.dirname(file);
|
|
202
|
+
const stamp = localTimestamp(new Date());
|
|
203
|
+
const entry = Object.assign(
|
|
204
|
+
{
|
|
205
|
+
ts: stamp.ts,
|
|
206
|
+
tzOffsetMinutes: stamp.tzOffsetMinutes,
|
|
207
|
+
runId: logState.runId,
|
|
208
|
+
seq: (logState.seq += 1),
|
|
209
|
+
server: 'pd-mcp-launcher',
|
|
210
|
+
version: LAUNCHER_VERSION,
|
|
211
|
+
level,
|
|
212
|
+
event,
|
|
213
|
+
host: logState.host,
|
|
214
|
+
pid: process.pid,
|
|
215
|
+
},
|
|
216
|
+
fields
|
|
217
|
+
);
|
|
218
|
+
const line = JSON.stringify(entry) + '\n';
|
|
219
|
+
const bytes = Buffer.byteLength(line, 'utf8');
|
|
220
|
+
if (logState.exhausted || logState.bytes + bytes > LOG_MAX_FILE_BYTES) {
|
|
221
|
+
if (logState.shard >= LOG_MAX_SHARDS) {
|
|
222
|
+
logState.exhausted = true;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
logState.shard += 1;
|
|
226
|
+
logState.currentPath = path.join(dir, logState.fileName.replace(/\.jsonl$/, `-${logState.shard}.jsonl`));
|
|
227
|
+
logState.bytes = 0;
|
|
228
|
+
maintainLogDir(dir, Date.now(), logState.currentPath);
|
|
229
|
+
}
|
|
230
|
+
fs.appendFileSync(logState.currentPath, line, { mode: 0o600 });
|
|
231
|
+
logState.bytes += bytes;
|
|
232
|
+
} catch {
|
|
233
|
+
// Logging must never affect launch behaviour.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Hand the same file/runId/seq to the server so one process keeps one file. */
|
|
238
|
+
function shareLogFileWithServer() {
|
|
239
|
+
try {
|
|
240
|
+
if (logState.fileName === null) ensureLogFile();
|
|
241
|
+
if (logState.fileName === null) return;
|
|
242
|
+
process.env.PD_MCP_LOG_FILE = path.basename(logState.fileName);
|
|
243
|
+
process.env.PD_MCP_LOG_RUN_ID = logState.runId;
|
|
244
|
+
process.env.PD_MCP_LOG_SEQ = String(logState.seq);
|
|
245
|
+
} catch {
|
|
246
|
+
// The server then opens its own file — still bounded, still silent.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function fail(reason, nextAction) {
|
|
251
|
+
writeLogEvent('error', 'startup_failure', {
|
|
252
|
+
stage: 'launcher',
|
|
253
|
+
reason: String(reason).replace(/[^A-Za-z0-9_.:-]/g, '').slice(0, 120),
|
|
254
|
+
bindingPathExists: logState.bindingPathExists,
|
|
255
|
+
workspaceResolution: logState.workspaceResolution,
|
|
256
|
+
envPresence: INITIAL_ENV_PRESENCE,
|
|
257
|
+
});
|
|
258
|
+
process.stderr.write(JSON.stringify({ launcher: 'pd-mcp', status: 'failed', reason, nextAction }) + '\n');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isRecord(value) {
|
|
263
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function readJson(file) {
|
|
267
|
+
try {
|
|
268
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
269
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
270
|
+
} catch {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function samePath(left, right) {
|
|
276
|
+
const a = path.resolve(left);
|
|
277
|
+
const b = path.resolve(right);
|
|
278
|
+
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isInside(parent, child) {
|
|
282
|
+
const relative = path.relative(parent, child);
|
|
283
|
+
return relative !== '' && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const PD_CONFIG_FILENAME = 'principles-disciple.json';
|
|
287
|
+
|
|
288
|
+
/** A declared workspace must be one fixed, real directory — never HOME or a drive root. */
|
|
289
|
+
function isUsableWorkspaceDir(candidate) {
|
|
290
|
+
if (typeof candidate !== 'string' || candidate.length === 0 || !path.isAbsolute(candidate)) return false;
|
|
291
|
+
const resolved = path.resolve(candidate);
|
|
292
|
+
if (samePath(resolved, os.homedir())) return false;
|
|
293
|
+
if (samePath(resolved, path.parse(resolved).root)) return false;
|
|
294
|
+
try {
|
|
295
|
+
return fs.statSync(resolved).isDirectory();
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** First trustworthy `workspace` pointer among the PD config files (rg-1: untrusted files). */
|
|
302
|
+
function readConfigWorkspace(files) {
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
let raw;
|
|
305
|
+
try {
|
|
306
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
307
|
+
} catch {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
let parsed;
|
|
311
|
+
try {
|
|
312
|
+
parsed = JSON.parse(raw);
|
|
313
|
+
} catch {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (!isRecord(parsed) || !Object.hasOwn(parsed, 'workspace')) continue;
|
|
317
|
+
const value = parsed.workspace;
|
|
318
|
+
if (typeof value !== 'string' || value.trim().length === 0) continue;
|
|
319
|
+
return value.trim();
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Re-derive the OWNER-DECLARED workspace exactly as the plugin's MCP binding
|
|
326
|
+
* writer does (env `PD_WORKSPACE_DIR` → `OPENCLAW_WORKSPACE` → PD config files
|
|
327
|
+
* under HOME). Deliberately NO `cwd`, HOME-as-workspace, agent, or host-default
|
|
328
|
+
* inference: those describe where a session runs, not which state tree owns
|
|
329
|
+
* governance for every agent of the host (SPEC §3.5).
|
|
330
|
+
*/
|
|
331
|
+
function resolveDeclaredWorkspaceDir() {
|
|
332
|
+
const home = os.homedir();
|
|
333
|
+
const readers = [
|
|
334
|
+
() => process.env.PD_WORKSPACE_DIR,
|
|
335
|
+
() => process.env.OPENCLAW_WORKSPACE,
|
|
336
|
+
() => readConfigWorkspace([
|
|
337
|
+
path.join(home, '.openclaw', PD_CONFIG_FILENAME),
|
|
338
|
+
path.join(home, '.principles', PD_CONFIG_FILENAME),
|
|
339
|
+
]),
|
|
340
|
+
];
|
|
341
|
+
let sawDeclaration = false;
|
|
342
|
+
for (const read of readers) {
|
|
343
|
+
let value;
|
|
344
|
+
try {
|
|
345
|
+
value = read();
|
|
346
|
+
} catch {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (typeof value !== 'string' || value.trim().length === 0) continue;
|
|
350
|
+
sawDeclaration = true;
|
|
351
|
+
if (isUsableWorkspaceDir(value.trim())) return { workspaceDir: path.resolve(value.trim()) };
|
|
352
|
+
}
|
|
353
|
+
return { reason: sawDeclaration ? 'workspace_declaration_invalid' : 'workspace_declaration_missing' };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const pluginRoot = path.join(__dirname, '..');
|
|
357
|
+
|
|
358
|
+
// rc-1/rc-3: the payload marker is the host authority. Exactly one marker
|
|
359
|
+
// must exist; ambiguity (tampered/copied payload) is never resolved by guess.
|
|
360
|
+
const detectedHosts = HOST_MARKERS
|
|
361
|
+
.filter(([, marker]) => {
|
|
362
|
+
try {
|
|
363
|
+
return fs.statSync(path.join(pluginRoot, marker)).isFile();
|
|
364
|
+
} catch {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
.map(([markerHost]) => markerHost);
|
|
369
|
+
if (detectedHosts.length !== 1) {
|
|
370
|
+
fail(
|
|
371
|
+
detectedHosts.length === 0 ? 'host_marker_missing' : `host_marker_ambiguous:${detectedHosts.join(',')}`,
|
|
372
|
+
'Reinstall the PD plugin from its marketplace source; the host payload marker is missing or ambiguous.',
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const host = detectedHosts[0];
|
|
376
|
+
logState.host = host;
|
|
377
|
+
|
|
378
|
+
// rc-1/rc-3/rc-5: the binding file is read as untrusted; every field is
|
|
379
|
+
// validated before use and unknown content is never coerced.
|
|
380
|
+
const bindingPath = path.join(pluginRoot, 'mcp-binding.json');
|
|
381
|
+
try {
|
|
382
|
+
logState.bindingPathExists = fs.statSync(bindingPath).isFile();
|
|
383
|
+
} catch {
|
|
384
|
+
logState.bindingPathExists = false;
|
|
385
|
+
}
|
|
386
|
+
let raw;
|
|
387
|
+
try {
|
|
388
|
+
raw = fs.readFileSync(bindingPath, 'utf8');
|
|
389
|
+
} catch {
|
|
390
|
+
fail('binding_missing', 'Re-run the PD host setup (Codex: $pd-control setup) so mcp-binding.json is written into the plugin directory.');
|
|
391
|
+
}
|
|
392
|
+
let binding;
|
|
393
|
+
try {
|
|
394
|
+
binding = JSON.parse(raw);
|
|
395
|
+
} catch {
|
|
396
|
+
fail('binding_unreadable', 'Re-run the PD host setup to regenerate a valid mcp-binding.json.');
|
|
397
|
+
}
|
|
398
|
+
if (!isRecord(binding)) {
|
|
399
|
+
fail('binding_invalid', 'Re-run the PD host setup to regenerate a valid mcp-binding.json.');
|
|
400
|
+
}
|
|
401
|
+
if (binding.schemaVersion !== BINDING_SCHEMA) {
|
|
402
|
+
// rc-8/rc-9: this value is untrusted and the reason lands in host logs —
|
|
403
|
+
// report a bounded identifier, never the raw string.
|
|
404
|
+
const schemaKind = typeof binding.schemaVersion === 'string'
|
|
405
|
+
? binding.schemaVersion.replace(/[^A-Za-z0-9._-]/g, '').slice(0, 60)
|
|
406
|
+
: 'non-string';
|
|
407
|
+
fail(`binding_schema_unsupported:${schemaKind}`, 'The plugin and the PD installation come from different generations. Re-run the PD host setup after updating PD.');
|
|
408
|
+
}
|
|
409
|
+
if (!KNOWN_HOSTS.includes(binding.host)) {
|
|
410
|
+
fail('binding_host_invalid', 'Re-run the PD host setup; the host marker is written by the PD installer and must not be edited.');
|
|
411
|
+
}
|
|
412
|
+
// A binding copied from another host's payload would make this launcher pass
|
|
413
|
+
// the wrong workspace and entry to the server — fail closed instead.
|
|
414
|
+
if (binding.host !== host) {
|
|
415
|
+
fail(`binding_host_mismatch:${binding.host}!=${host}`, 'This mcp-binding.json belongs to a different host payload. Re-run the PD host setup for THIS host.');
|
|
416
|
+
}
|
|
417
|
+
if (typeof binding.workspaceDir !== 'string' || !path.isAbsolute(binding.workspaceDir)) {
|
|
418
|
+
fail('binding_workspace_invalid', 'Re-run the PD host setup so the binding records the absolute governance workspace directory.');
|
|
419
|
+
}
|
|
420
|
+
if (typeof binding.runtimeDir !== 'string' || !path.isAbsolute(binding.runtimeDir)) {
|
|
421
|
+
fail('binding_runtime_dir_invalid', 'Re-run the PD host setup so the binding records the absolute pinned-runtime directory.');
|
|
422
|
+
}
|
|
423
|
+
if (typeof binding.serverEntry !== 'string' || !path.isAbsolute(binding.serverEntry)) {
|
|
424
|
+
fail('binding_server_entry_invalid', 'Re-run the PD host setup so the binding records the absolute MCP server entry path.');
|
|
425
|
+
}
|
|
426
|
+
if (!isRecord(binding.versions) || typeof binding.versions.mcpServer !== 'string' || !SEMVER.test(binding.versions.mcpServer)) {
|
|
427
|
+
fail('binding_versions_invalid', 'Re-run the PD host setup to regenerate the binding version record.');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Canonical path: the entry must be exactly the documented server entry for
|
|
431
|
+
// this host's runtime layout — not merely an existing JavaScript file.
|
|
432
|
+
const expectedEntry = path.join(binding.runtimeDir, ...SERVER_ENTRY_SEGMENTS[host]);
|
|
433
|
+
if (!samePath(binding.serverEntry, expectedEntry)) {
|
|
434
|
+
fail(`server_entry_not_canonical:${host}`, 'The binding points outside the documented MCP server entry. Re-run the PD host setup; do not edit mcp-binding.json by hand.');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let realRuntimeDir;
|
|
438
|
+
try {
|
|
439
|
+
realRuntimeDir = fs.realpathSync(binding.runtimeDir);
|
|
440
|
+
} catch {
|
|
441
|
+
fail('runtime_dir_missing', 'The pinned PD runtime is not installed. Re-run the PD host setup to install it.');
|
|
442
|
+
}
|
|
443
|
+
if (!fs.statSync(realRuntimeDir).isDirectory()) {
|
|
444
|
+
fail('runtime_dir_missing', 'The pinned PD runtime directory is not a directory. Re-run the PD host setup to repair it.');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let realEntry;
|
|
448
|
+
try {
|
|
449
|
+
realEntry = fs.realpathSync(expectedEntry);
|
|
450
|
+
} catch {
|
|
451
|
+
fail('server_entry_missing', 'The pinned PD MCP server is not installed. Re-run the PD host setup to install the pinned runtime.');
|
|
452
|
+
}
|
|
453
|
+
// Symlink/junction escape defence: an entry that resolves outside the
|
|
454
|
+
// recorded runtime tree can never be the installed component.
|
|
455
|
+
if (!isInside(realRuntimeDir, realEntry)) {
|
|
456
|
+
fail('server_entry_escape', 'The MCP server entry resolves outside the pinned runtime directory. Re-run the PD host setup; if it repeats, reinstall PD.');
|
|
457
|
+
}
|
|
458
|
+
if (!fs.statSync(realEntry).isFile()) {
|
|
459
|
+
fail('server_entry_missing', 'The pinned PD MCP server entry is not a file. Re-run the PD host setup to install the pinned runtime.');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Package identity/version: the entry must belong to the installed package,
|
|
463
|
+
// and both the binding and (Codex) the plugin runtime pin must agree with the
|
|
464
|
+
// bytes on disk. A stale or forged binding cannot silently launch another
|
|
465
|
+
// version of the server.
|
|
466
|
+
const packageManifest = readJson(path.join(path.dirname(path.dirname(realEntry)), 'package.json'));
|
|
467
|
+
if (packageManifest === undefined || packageManifest.name !== SERVER_PACKAGE_NAME) {
|
|
468
|
+
fail('server_package_identity_mismatch', 'The MCP server entry does not belong to @principles/pd-mcp-server. Re-run the PD host setup to reinstall the pinned component.');
|
|
469
|
+
}
|
|
470
|
+
if (typeof packageManifest.version !== 'string' || !SEMVER.test(packageManifest.version)) {
|
|
471
|
+
fail('server_package_version_invalid', 'The installed MCP server package version is unreadable. Re-run the PD host setup to reinstall the pinned component.');
|
|
472
|
+
}
|
|
473
|
+
if (binding.versions.mcpServer !== packageManifest.version) {
|
|
474
|
+
fail(`binding_version_mismatch:${binding.versions.mcpServer}!=${packageManifest.version}`, 'The binding and the installed MCP server disagree. Re-run the PD host setup so both match the runtime pin.');
|
|
475
|
+
}
|
|
476
|
+
if (host === 'codex') {
|
|
477
|
+
const pin = readJson(path.join(pluginRoot, 'runtime-version.json'));
|
|
478
|
+
if (pin === undefined || typeof pin.mcpServer !== 'string' || !SEMVER.test(pin.mcpServer) || typeof pin.core !== 'string') {
|
|
479
|
+
fail('runtime_pin_invalid', 'The plugin runtime pin is missing or malformed. Reinstall the PD plugin, then re-run $pd-control setup.');
|
|
480
|
+
}
|
|
481
|
+
if (pin.mcpServer !== packageManifest.version) {
|
|
482
|
+
fail(`runtime_pin_mismatch:mcpServer:${pin.mcpServer}!=${packageManifest.version}`, 'The installed MCP server no longer matches the plugin runtime pin. Re-run $pd-control setup to realign the runtime.');
|
|
483
|
+
}
|
|
484
|
+
if (typeof binding.versions.core === 'string' && binding.versions.core !== pin.core) {
|
|
485
|
+
fail(`runtime_pin_mismatch:core:${binding.versions.core}!=${pin.core}`, 'The binding core version no longer matches the plugin runtime pin. Re-run $pd-control setup to realign the runtime.');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let workspaceStat;
|
|
490
|
+
try {
|
|
491
|
+
workspaceStat = fs.statSync(binding.workspaceDir);
|
|
492
|
+
} catch {
|
|
493
|
+
fail('binding_workspace_missing', 'The bound governance workspace no longer exists. Re-install PD for this host or re-run setup with the correct workspace.');
|
|
494
|
+
}
|
|
495
|
+
if (!workspaceStat.isDirectory()) {
|
|
496
|
+
fail('binding_workspace_not_directory', 'Re-run the PD host setup so the binding points at the governance workspace directory.');
|
|
497
|
+
}
|
|
498
|
+
logState.workspaceResolution = 'ok';
|
|
499
|
+
|
|
500
|
+
// ── OpenClaw workspace-declaration gate (SPEC §3.5, final safety closure) ──
|
|
501
|
+
// The OpenClaw binding is HOST-WIDE while agents have per-agent workspaces, so
|
|
502
|
+
// a binding may only be trusted while it still matches the workspace the Owner
|
|
503
|
+
// EXPLICITLY declared. The plugin's startup health check cannot be the last
|
|
504
|
+
// line of defence: it runs after registration and may fail, skip, or lag a
|
|
505
|
+
// workspace switch. Re-derive the declaration HERE, at launch, and refuse
|
|
506
|
+
// otherwise — a stale binding must never serve the previous workspace's
|
|
507
|
+
// evidence to another agent. Codex/WorkBuddy have a single user-level workspace
|
|
508
|
+
// and their binding writer is the authority; the gate is OpenClaw-only.
|
|
509
|
+
if (host === 'openclaw') {
|
|
510
|
+
const declared = resolveDeclaredWorkspaceDir();
|
|
511
|
+
if (declared.workspaceDir === undefined) {
|
|
512
|
+
fail(
|
|
513
|
+
declared.reason,
|
|
514
|
+
'pd-recall refuses to start because the PD governance workspace is not explicitly declared. ' +
|
|
515
|
+
'Declare ONE PD workspace (set PD_WORKSPACE_DIR or OPENCLAW_WORKSPACE, or add ' +
|
|
516
|
+
'{"workspace":"<absolute dir>"} to ~/.openclaw/principles-disciple.json), then restart the OpenClaw gateway.',
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (!samePath(declared.workspaceDir, binding.workspaceDir)) {
|
|
520
|
+
fail(
|
|
521
|
+
'binding_workspace_not_declared',
|
|
522
|
+
'mcp-binding.json does not match the explicitly declared PD workspace (a workspace switch was never committed). ' +
|
|
523
|
+
'Restart the OpenClaw gateway so the PD plugin rewrites the binding for the declared workspace.',
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// The binding is the ONLY authority for the business workspace (SPEC §3.5):
|
|
529
|
+
// it overrides any inherited value and the server validates it fail-closed.
|
|
530
|
+
process.env.PD_MCP_WORKSPACE_DIR = binding.workspaceDir;
|
|
531
|
+
process.env.PD_MCP_HOST = host;
|
|
532
|
+
|
|
533
|
+
// Validation is complete: record the launch attempt (version, binding
|
|
534
|
+
// presence, workspace resolution and env PRESENCE — never values) and then
|
|
535
|
+
// share this file with the server.
|
|
536
|
+
writeLogEvent('info', 'launcher_start', {
|
|
537
|
+
launcherVersion: LAUNCHER_VERSION,
|
|
538
|
+
mcpServerVersion: packageManifest.version,
|
|
539
|
+
bindingPathExists: logState.bindingPathExists,
|
|
540
|
+
workspaceResolution: logState.workspaceResolution,
|
|
541
|
+
envPresence: INITIAL_ENV_PRESENCE,
|
|
542
|
+
});
|
|
543
|
+
shareLogFileWithServer();
|
|
544
|
+
|
|
545
|
+
// Run the shared server entry in-process (single process per host). Nothing
|
|
546
|
+
// above imports the server, so a rejected binding never emits MCP traffic.
|
|
547
|
+
import(pathToFileURL(realEntry).href)
|
|
548
|
+
.then((mod) => {
|
|
549
|
+
if (mod === null || typeof mod !== 'object' || typeof mod.main !== 'function') {
|
|
550
|
+
fail('server_entry_invalid', 'The pinned MCP server entry does not export main(). Re-run the PD host setup.');
|
|
551
|
+
}
|
|
552
|
+
return mod.main(process.env).then((code) => {
|
|
553
|
+
if (code !== 0) process.exit(code);
|
|
554
|
+
// code 0: the server owns the event loop from here (stdio transport).
|
|
555
|
+
});
|
|
556
|
+
})
|
|
557
|
+
.catch((error) => {
|
|
558
|
+
// Sanitized: category only — no message/stack/path leak to host logs.
|
|
559
|
+
const kind = error !== null && typeof error === 'object' && typeof error.name === 'string' ? error.name : 'Error';
|
|
560
|
+
fail(`server_startup_failed:${kind.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 60)}`, 'Re-run the PD host setup; if it repeats, run the PD host health check (pd health).');
|
|
561
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Principles Disciple is an AI Agent Governance System. Stop correcting the same AI behavior across sessions. Turn repeated Agent corrections into Owner-approved, observable, reversible behavior principles.",
|
|
5
|
-
"version": "2.6.
|
|
5
|
+
"version": "2.6.1",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/plugin/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "principles-disciple",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Principles Disciple is an AI Agent Governance System. Stop correcting the same AI behavior across sessions. Turn repeated Agent corrections into Owner-approved, observable, reversible behavior principles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|