create-principles-disciple 1.147.7 → 1.147.9

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.
Files changed (123) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/core/dist/host/host-installer.d.ts +15 -0
  3. package/core/dist/host/host-installer.d.ts.map +1 -1
  4. package/core/dist/host/host-installer.js.map +1 -1
  5. package/core/dist/runtime-v2/activation/adaptive-delivery-record.d.ts +37 -0
  6. package/core/dist/runtime-v2/activation/adaptive-delivery-record.d.ts.map +1 -0
  7. package/core/dist/runtime-v2/activation/adaptive-delivery-record.js +126 -0
  8. package/core/dist/runtime-v2/activation/adaptive-delivery-record.js.map +1 -0
  9. package/core/dist/runtime-v2/activation/adaptive-principle-intervention.d.ts +46 -0
  10. package/core/dist/runtime-v2/activation/adaptive-principle-intervention.d.ts.map +1 -0
  11. package/core/dist/runtime-v2/activation/adaptive-principle-intervention.js +91 -0
  12. package/core/dist/runtime-v2/activation/adaptive-principle-intervention.js.map +1 -0
  13. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +2 -0
  14. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
  15. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js +2 -0
  16. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
  17. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
  18. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  19. package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js +2 -2
  20. package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js.map +1 -1
  21. package/core/dist/runtime-v2/index.d.ts +3 -0
  22. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  23. package/core/dist/runtime-v2/index.js +2 -0
  24. package/core/dist/runtime-v2/index.js.map +1 -1
  25. package/core/dist/runtime-v2/pain-recall-capability.d.ts.map +1 -1
  26. package/core/dist/runtime-v2/pain-recall-capability.js +53 -2
  27. package/core/dist/runtime-v2/pain-recall-capability.js.map +1 -1
  28. package/core/dist/runtime-v2/pain-recall-read-model.d.ts.map +1 -1
  29. package/core/dist/runtime-v2/pain-recall-read-model.js +60 -5
  30. package/core/dist/runtime-v2/pain-recall-read-model.js.map +1 -1
  31. package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts.map +1 -1
  32. package/core/dist/runtime-v2/signal-collector/keyword-stage.js +35 -0
  33. package/core/dist/runtime-v2/signal-collector/keyword-stage.js.map +1 -1
  34. package/core/dist/runtime-v2/store/sqlite-connection.js +1 -1
  35. package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  36. package/core/dist/runtime-v2/types/event-types.d.ts +6 -3
  37. package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
  38. package/core/dist/runtime-v2/types/event-types.js +2 -0
  39. package/core/dist/runtime-v2/types/event-types.js.map +1 -1
  40. package/core/package.json +5 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +15 -29
  43. package/dist/index.js.map +1 -1
  44. package/dist/install-summary.d.ts +41 -0
  45. package/dist/install-summary.d.ts.map +1 -0
  46. package/dist/install-summary.js +123 -0
  47. package/dist/install-summary.js.map +1 -0
  48. package/dist/installer.d.ts +36 -0
  49. package/dist/installer.d.ts.map +1 -1
  50. package/dist/installer.js +120 -26
  51. package/dist/installer.js.map +1 -1
  52. package/dist/installers/workbuddy-host-installer.d.ts +70 -17
  53. package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
  54. package/dist/installers/workbuddy-host-installer.js +571 -186
  55. package/dist/installers/workbuddy-host-installer.js.map +1 -1
  56. package/dist/prompts.js +1 -1
  57. package/dist/prompts.js.map +1 -1
  58. package/host-runtime/dist/active-principle-prompt.d.ts +28 -0
  59. package/host-runtime/dist/active-principle-prompt.js +163 -50
  60. package/host-runtime/dist/adaptive-principle-revalidation.d.ts +8 -0
  61. package/host-runtime/dist/adaptive-principle-revalidation.js +52 -0
  62. package/host-runtime/dist/governance-signal-admission.d.ts +2 -2
  63. package/host-runtime/dist/governance-signal-admission.js +2 -6
  64. package/host-runtime/dist/index.d.ts +16 -0
  65. package/host-runtime/dist/index.js +34 -0
  66. package/host-runtime/dist/prompt-activation-authority.d.ts +7 -0
  67. package/host-runtime/dist/prompt-activation-authority.js +16 -0
  68. package/host-runtime/package.json +1 -1
  69. package/package.json +3 -3
  70. package/pd-cli/dist/commands/health-mcp.d.ts +19 -0
  71. package/pd-cli/dist/commands/health-mcp.d.ts.map +1 -1
  72. package/pd-cli/dist/commands/health-mcp.js +226 -0
  73. package/pd-cli/dist/commands/health-mcp.js.map +1 -1
  74. package/pd-cli/dist/commands/health.d.ts.map +1 -1
  75. package/pd-cli/dist/commands/health.js +18 -1
  76. package/pd-cli/dist/commands/health.js.map +1 -1
  77. package/pd-cli/dist/commands/mcp-logs-export.d.ts +7 -0
  78. package/pd-cli/dist/commands/mcp-logs-export.d.ts.map +1 -0
  79. package/pd-cli/dist/commands/mcp-logs-export.js +267 -0
  80. package/pd-cli/dist/commands/mcp-logs-export.js.map +1 -0
  81. package/pd-cli/dist/commands/workbuddy-worker.d.ts.map +1 -1
  82. package/pd-cli/dist/commands/workbuddy-worker.js +2 -1
  83. package/pd-cli/dist/commands/workbuddy-worker.js.map +1 -1
  84. package/pd-cli/dist/index.js +35 -0
  85. package/pd-cli/dist/index.js.map +1 -1
  86. package/pd-cli/dist/services/zip-writer.d.ts +6 -0
  87. package/pd-cli/dist/services/zip-writer.d.ts.map +1 -0
  88. package/pd-cli/dist/services/zip-writer.js +97 -0
  89. package/pd-cli/dist/services/zip-writer.js.map +1 -0
  90. package/pd-cli/package.json +1 -1
  91. package/pd-mcp-server/dist/index.d.ts +1 -0
  92. package/pd-mcp-server/dist/index.js +167 -10
  93. package/pd-mcp-server/dist/log-file.d.ts +26 -0
  94. package/pd-mcp-server/dist/log-file.js +312 -0
  95. package/pd-mcp-server/dist/logging.d.ts +2 -1
  96. package/pd-mcp-server/dist/logging.js +25 -14
  97. package/pd-mcp-server/dist/server.js +78 -24
  98. package/pd-mcp-server/dist/version.d.ts +1 -1
  99. package/pd-mcp-server/dist/version.js +1 -1
  100. package/pd-mcp-server/dist/workspace.d.ts +3 -0
  101. package/pd-mcp-server/dist/workspace.js +5 -0
  102. package/pd-mcp-server/package.json +1 -1
  103. package/plugin/dist/bundle.js +446 -445
  104. package/plugin/dist/core/event-log.d.ts +3 -0
  105. package/plugin/dist/governance-audit.js +3 -3
  106. package/plugin/dist/hooks/adaptive-activation-config.d.ts +8 -0
  107. package/plugin/dist/hooks/prompt-helpers.d.ts +5 -0
  108. package/plugin/dist/hooks/prompt-types.d.ts +1 -0
  109. package/plugin/dist/hooks/prompt.d.ts +8 -1
  110. package/plugin/dist/host-runtime/openclaw-host-runtime.d.ts +13 -0
  111. package/plugin/dist/openclaw-sdk.d.ts +4 -0
  112. package/plugin/dist/openclaw.plugin.json +33 -1
  113. package/plugin/mcp/launcher.cjs +561 -0
  114. package/plugin/openclaw.plugin.json +33 -1
  115. package/plugin/package.json +1 -1
  116. package/release-manager/dist/installer.js +120 -26
  117. package/release-manager/dist/installers/workbuddy-host-installer.js +571 -186
  118. package/release-manager/package.json +1 -1
  119. package/workbuddy-adapter/dist/pd-hook.js +3 -2
  120. package/workbuddy-adapter/package.json +1 -1
  121. package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +1 -1
  122. package/workbuddy-adapter/plugin/mcp/launcher.cjs +215 -1
  123. package/workbuddy-adapter/plugin/skills/pd-pain-signal/SKILL.md +4 -0
@@ -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.0",
5
+ "version": "2.6.1",
6
6
  "activation": {
7
7
  "onStartup": true,
8
8
  "onCapabilities": [
@@ -25,6 +25,38 @@
25
25
  "type": "object",
26
26
  "additionalProperties": false,
27
27
  "properties": {
28
+ "adaptiveActivation": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "description": "Optional expiring single-principle delivery-review mapping. Disabled by default; does not grant principle authorization.",
32
+ "properties": {
33
+ "enabled": {
34
+ "type": "boolean",
35
+ "default": false
36
+ },
37
+ "workspaceDir": {
38
+ "type": "string",
39
+ "minLength": 1
40
+ },
41
+ "artifactId": {
42
+ "type": "string",
43
+ "minLength": 1
44
+ },
45
+ "activationId": {
46
+ "type": "string",
47
+ "minLength": 1
48
+ },
49
+ "expiresAt": {
50
+ "type": "string",
51
+ "format": "date-time"
52
+ },
53
+ "softTimeoutMs": {
54
+ "type": "integer",
55
+ "minimum": 1,
56
+ "maximum": 15000
57
+ }
58
+ }
59
+ },
28
60
  "language": {
29
61
  "type": "string",
30
62
  "enum": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "2.6.0",
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",