create-principles-disciple 1.146.9 → 1.147.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_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/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/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/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/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-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/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/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,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PD MCP Server process entry — SPEC-PD-MCP-GATE4-001 §5.5.
|
|
3
|
+
*
|
|
4
|
+
* Startup: resolve the fixed workspace binding (fail-closed: a missing or
|
|
5
|
+
* invalid binding logs ONE sanitized reason to stderr and exits 1 BEFORE the
|
|
6
|
+
* MCP initialize — the server never serves with an unconfirmed workspace) →
|
|
7
|
+
* build the server → connect the stdio transport → wait for requests.
|
|
8
|
+
*
|
|
9
|
+
* Shutdown (host closes stdin / terminates): stop accepting new calls
|
|
10
|
+
* (in-flight ones drain bounded by the 10 s deadline; late results are
|
|
11
|
+
* suppressed), release resources, exit 0.
|
|
12
|
+
*
|
|
13
|
+
* stdout carries MCP protocol messages ONLY (§5.3). This module never
|
|
14
|
+
* console.log's; logs go through the sanitized stderr logger.
|
|
15
|
+
*/
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { pathToFileURL } from 'node:url';
|
|
20
|
+
import { createPdMcpServer } from './server.js';
|
|
21
|
+
import { resolveWorkspaceBinding } from './workspace.js';
|
|
22
|
+
import { stderrLog } from './logging.js';
|
|
23
|
+
export { createPdMcpServer, TOOL_NAME, toolInputSchema } from './server.js';
|
|
24
|
+
export { resolveWorkspaceBinding, WORKSPACE_DIR_ENV, HOST_ENV } from './workspace.js';
|
|
25
|
+
export { RESPONSE_BUDGET_BYTES, withinBudget, measureTextBytes } from './budget.js';
|
|
26
|
+
export { DEFAULT_DEADLINE_MS, MAX_TRACKED_ORPHANS, runWithDeadline, OrphanLimiter } from './deadline.js';
|
|
27
|
+
export { SERVER_NAME, SERVER_VERSION, MCP_SDK_PIN } from './version.js';
|
|
28
|
+
/**
|
|
29
|
+
* Run the stdio server against the real process env. Returns the intended
|
|
30
|
+
* process exit code: 0 after a clean shutdown, 1 for a fail-closed startup.
|
|
31
|
+
*/
|
|
32
|
+
export async function main(env = process.env) {
|
|
33
|
+
const binding = resolveWorkspaceBinding(env);
|
|
34
|
+
if (!binding.ok) {
|
|
35
|
+
// Fail-closed BEFORE initialize: one sanitized stderr line with the
|
|
36
|
+
// reason and the next action; nothing about values or paths.
|
|
37
|
+
stderrLog('error', 'startup_binding_failed', {
|
|
38
|
+
envVar: binding.envVar,
|
|
39
|
+
reasonCategory: 'invalid_binding',
|
|
40
|
+
reason: binding.reason,
|
|
41
|
+
nextAction: binding.nextAction,
|
|
42
|
+
});
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
// Workspace fingerprint (NOT the path): lets operators and acceptance runs
|
|
46
|
+
// prove WHICH workspace this process bound without leaking a directory
|
|
47
|
+
// (§10 forbids logging full paths). Multi-agent hosts must never see two
|
|
48
|
+
// different fingerprints for the same binding.
|
|
49
|
+
const workspaceFingerprint = createHash('sha256')
|
|
50
|
+
.update(path.resolve(binding.binding.workspaceDir))
|
|
51
|
+
.digest('hex')
|
|
52
|
+
.slice(0, 16);
|
|
53
|
+
stderrLog('info', 'startup', { host: binding.binding.host ?? 'unspecified', workspaceFingerprint });
|
|
54
|
+
const handle = createPdMcpServer({ workspaceDir: binding.binding.workspaceDir, host: binding.binding.host });
|
|
55
|
+
const transport = new StdioServerTransport();
|
|
56
|
+
await handle.mcpServer.connect(transport);
|
|
57
|
+
// Host-driven shutdown: stdin close → SDK closes the transport → we stop
|
|
58
|
+
// accepting new work, let in-flight calls drain (bounded by the deadline)
|
|
59
|
+
// and exit cleanly. The transport close callback is the single trigger.
|
|
60
|
+
transport.onclose = () => {
|
|
61
|
+
handle.beginShutdown();
|
|
62
|
+
stderrLog('info', 'transport_closed', { orphans: handle.orphans.size });
|
|
63
|
+
};
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
// Process-mode guard: only run when executed directly (not when the launcher
|
|
67
|
+
// or tests import this module). Windows-safe via pathToFileURL.
|
|
68
|
+
const { 1: invokedPath } = process.argv;
|
|
69
|
+
if (invokedPath !== undefined &&
|
|
70
|
+
import.meta.url === pathToFileURL(invokedPath).href) {
|
|
71
|
+
void main().then((code) => {
|
|
72
|
+
if (code !== 0)
|
|
73
|
+
process.exit(code);
|
|
74
|
+
// code 0: exit naturally once the event loop drains (timers unref'd).
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type LogField = string | number | boolean;
|
|
2
|
+
export type LogFields = Readonly<Record<string, LogField>>;
|
|
3
|
+
export type LogLevel = 'info' | 'warn' | 'error';
|
|
4
|
+
/** Bound logger: sink fixed at construction, ≤3 params per call. */
|
|
5
|
+
export type LogFn = (level: LogLevel, event: string, fields?: LogFields) => void;
|
|
6
|
+
declare function sanitizeErrorKind(err: unknown): string;
|
|
7
|
+
/** Default stderr sink. One JSON line per event, nothing else. */
|
|
8
|
+
export declare function stderrSink(line: string): void;
|
|
9
|
+
export declare function createLogger(sink: (line: string) => void, baseFields?: LogFields): LogFn;
|
|
10
|
+
/** Convenience logger bound to stderr (startup/shutdown paths). */
|
|
11
|
+
export declare const stderrLog: LogFn;
|
|
12
|
+
export { sanitizeErrorKind };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stderr-only structured logging — SPEC-PD-MCP-GATE4-001 §10.
|
|
3
|
+
*
|
|
4
|
+
* stdout carries MCP protocol messages ONLY; every log line goes to stderr
|
|
5
|
+
* as one JSON object. Redaction is by construction: callers may pass only
|
|
6
|
+
* scalar event/category/duration fields — never Pain text, evidence bodies,
|
|
7
|
+
* env values, raw tool results, absolute workspace/db paths or stack traces.
|
|
8
|
+
* The `errKind` field carries a sanitized exception category (the error's
|
|
9
|
+
* `name`), never `message` or `stack`.
|
|
10
|
+
*/
|
|
11
|
+
import { SERVER_NAME, SERVER_VERSION } from './version.js';
|
|
12
|
+
function sanitizeErrorKind(err) {
|
|
13
|
+
if (err !== null && typeof err === 'object' && Object.hasOwn(err, 'name') && typeof err.name === 'string') {
|
|
14
|
+
const { name } = err;
|
|
15
|
+
return name.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 60) || 'Error';
|
|
16
|
+
}
|
|
17
|
+
return 'Error';
|
|
18
|
+
}
|
|
19
|
+
/** Default stderr sink. One JSON line per event, nothing else. */
|
|
20
|
+
export function stderrSink(line) {
|
|
21
|
+
process.stderr.write(line + '\n');
|
|
22
|
+
}
|
|
23
|
+
export function createLogger(sink, baseFields) {
|
|
24
|
+
return (level, event, fields) => {
|
|
25
|
+
const entry = {
|
|
26
|
+
ts: new Date().toISOString(),
|
|
27
|
+
server: SERVER_NAME,
|
|
28
|
+
version: SERVER_VERSION,
|
|
29
|
+
level,
|
|
30
|
+
event,
|
|
31
|
+
};
|
|
32
|
+
const merged = { ...baseFields, ...fields };
|
|
33
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
34
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
|
|
35
|
+
entry[k] = v;
|
|
36
|
+
}
|
|
37
|
+
sink(JSON.stringify(entry));
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Convenience logger bound to stderr (startup/shutdown paths). */
|
|
41
|
+
export const stderrLog = createLogger(stderrSink);
|
|
42
|
+
export { sanitizeErrorKind };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PD MCP Server assembly — SPEC-PD-MCP-GATE4-001 §3/§4/§5 (Gate 4-B).
|
|
3
|
+
*
|
|
4
|
+
* One server implementation shared by all three hosts (Codex / OpenClaw /
|
|
5
|
+
* WorkBuddy each spawn their own process around this module). Business data
|
|
6
|
+
* flows EXCLUSIVELY through the Gate 3 Capability public subpath
|
|
7
|
+
* `@principles/core/runtime-v2/pain-recall-capability` — never the read
|
|
8
|
+
* model, never a CLI subprocess, never a second query implementation
|
|
9
|
+
* (§3.2). Core stays MCP-SDK-free (architecture guard test pins this).
|
|
10
|
+
*
|
|
11
|
+
* The single registered tool is the read-only `pd_recall_principle_pains`
|
|
12
|
+
* with an honest `readOnlyHint: true` (a host hint, not an OS boundary).
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer, type StandardSchemaWithJSON } from '@modelcontextprotocol/server';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { type PainRecallCapabilityResult } from '@principles/core/runtime-v2/pain-recall-capability';
|
|
17
|
+
import { OrphanLimiter, AdmissionLimiter } from './deadline.js';
|
|
18
|
+
export declare const TOOL_NAME: 'pd_recall_principle_pains';
|
|
19
|
+
export declare const TOOL_DESCRIPTION = "Recall a principle's verifiably-associated historical pain experience (availability-level disclosure only, OD-A v1). Read-only.";
|
|
20
|
+
/** §4.2 input contract: `principleId` (1..200, required) and `limit`
|
|
21
|
+
* (1..10, default 3); every other property is rejected at the protocol
|
|
22
|
+
* input-validation stage (`additionalProperties: false`) before the
|
|
23
|
+
* capability is called. No path, SQL, host/session/turn/pain id, or
|
|
24
|
+
* ordering parameters exist. The capability re-validates field-by-field
|
|
25
|
+
* (defense in depth). */
|
|
26
|
+
export declare const toolInputJsonSchema: {
|
|
27
|
+
readonly type: 'object';
|
|
28
|
+
readonly properties: {
|
|
29
|
+
readonly principleId: {
|
|
30
|
+
readonly type: 'string';
|
|
31
|
+
readonly minLength: 1;
|
|
32
|
+
readonly maxLength: 200;
|
|
33
|
+
readonly description: 'Principle identifier to recall pains for.';
|
|
34
|
+
};
|
|
35
|
+
readonly limit: {
|
|
36
|
+
readonly type: 'integer';
|
|
37
|
+
readonly minimum: 1;
|
|
38
|
+
readonly maximum: 10;
|
|
39
|
+
readonly default: 3;
|
|
40
|
+
readonly description: 'Maximum number of pain entries to return (1..10, default 3).';
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
readonly required: readonly ['principleId'];
|
|
44
|
+
readonly additionalProperties: false;
|
|
45
|
+
};
|
|
46
|
+
/** Protocol-level schema handed to the SDK (JSON-Schema-first: strict
|
|
47
|
+
* rejection of undeclared parameters at the input-validation stage). */
|
|
48
|
+
export declare const toolProtocolSchema: StandardSchemaWithJSON<unknown, unknown>;
|
|
49
|
+
/** In-handler typed re-validation (idempotent post-SDK parse; applies the
|
|
50
|
+
* `limit` default and yields the typed value). Mirrors the JSON schema
|
|
51
|
+
* above — kept in lockstep by the contract tests. */
|
|
52
|
+
export declare const toolInputSchema: z.ZodObject<{
|
|
53
|
+
principleId: z.ZodString;
|
|
54
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
55
|
+
}, z.core.$strict>;
|
|
56
|
+
export type ToolInput = z.infer<typeof toolInputSchema>;
|
|
57
|
+
/** The capability entry the server calls — injectable for adapter-contract
|
|
58
|
+
* tests; production always uses the real Gate 3 Facade. */
|
|
59
|
+
export type CapabilityCall = (input: {
|
|
60
|
+
readonly workspaceDir: string;
|
|
61
|
+
readonly principleId: string;
|
|
62
|
+
readonly limit: number;
|
|
63
|
+
}) => Promise<PainRecallCapabilityResult>;
|
|
64
|
+
export interface PdMcpServerOptions {
|
|
65
|
+
readonly workspaceDir: string;
|
|
66
|
+
readonly host?: string | null;
|
|
67
|
+
readonly deadlineMs?: number;
|
|
68
|
+
readonly callCapability?: CapabilityCall;
|
|
69
|
+
readonly logSink?: (line: string) => void;
|
|
70
|
+
}
|
|
71
|
+
export interface PdMcpServerHandle {
|
|
72
|
+
readonly mcpServer: McpServer;
|
|
73
|
+
/** Timeout stragglers tracked until their underlying work settles. */
|
|
74
|
+
readonly orphans: OrphanLimiter;
|
|
75
|
+
/** Executing-call capacity; admission is reserved before the capability is
|
|
76
|
+
* invoked and released only on real settlement (§4.7). */
|
|
77
|
+
readonly admissions: AdmissionLimiter;
|
|
78
|
+
/** Lifecycle flag: once closing, in-flight work drains and new calls get
|
|
79
|
+
* the shutting-down failure (§5.5). */
|
|
80
|
+
beginShutdown(): void;
|
|
81
|
+
isShuttingDown(): boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the PD MCP server (not yet connected to any transport).
|
|
85
|
+
*
|
|
86
|
+
* Handler flow per §4.7/§4.8: shutdown/saturation gates → deadline-wrapped
|
|
87
|
+
* capability call → envelope assembly (serialize → 32 KiB budget → emit) →
|
|
88
|
+
* explicit isError mapping. Nothing here writes to stdout.
|
|
89
|
+
*/
|
|
90
|
+
export declare function createPdMcpServer(options: PdMcpServerOptions): PdMcpServerHandle;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PD MCP Server assembly — SPEC-PD-MCP-GATE4-001 §3/§4/§5 (Gate 4-B).
|
|
3
|
+
*
|
|
4
|
+
* One server implementation shared by all three hosts (Codex / OpenClaw /
|
|
5
|
+
* WorkBuddy each spawn their own process around this module). Business data
|
|
6
|
+
* flows EXCLUSIVELY through the Gate 3 Capability public subpath
|
|
7
|
+
* `@principles/core/runtime-v2/pain-recall-capability` — never the read
|
|
8
|
+
* model, never a CLI subprocess, never a second query implementation
|
|
9
|
+
* (§3.2). Core stays MCP-SDK-free (architecture guard test pins this).
|
|
10
|
+
*
|
|
11
|
+
* The single registered tool is the read-only `pd_recall_principle_pains`
|
|
12
|
+
* with an honest `readOnlyHint: true` (a host hint, not an OS boundary).
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { recallPrinciplePainsCapability, } from '@principles/core/runtime-v2/pain-recall-capability';
|
|
17
|
+
import { runWithDeadline, OrphanLimiter, AdmissionLimiter, DEFAULT_DEADLINE_MS } from './deadline.js';
|
|
18
|
+
import { assembleResult, adapterFailureEnvelope, textResult } from './envelope.js';
|
|
19
|
+
import { RESPONSE_BUDGET_BYTES, withinBudget } from './budget.js';
|
|
20
|
+
import { createLogger, sanitizeErrorKind, stderrSink } from './logging.js';
|
|
21
|
+
import { SERVER_NAME, SERVER_VERSION } from './version.js';
|
|
22
|
+
export const TOOL_NAME = 'pd_recall_principle_pains';
|
|
23
|
+
export const TOOL_DESCRIPTION = 'Recall a principle\'s verifiably-associated historical pain experience (availability-level disclosure only, OD-A v1). Read-only.';
|
|
24
|
+
/** §4.2 input contract: `principleId` (1..200, required) and `limit`
|
|
25
|
+
* (1..10, default 3); every other property is rejected at the protocol
|
|
26
|
+
* input-validation stage (`additionalProperties: false`) before the
|
|
27
|
+
* capability is called. No path, SQL, host/session/turn/pain id, or
|
|
28
|
+
* ordering parameters exist. The capability re-validates field-by-field
|
|
29
|
+
* (defense in depth). */
|
|
30
|
+
export const toolInputJsonSchema = {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
principleId: { type: 'string', minLength: 1, maxLength: 200, description: 'Principle identifier to recall pains for.' },
|
|
34
|
+
limit: { type: 'integer', minimum: 1, maximum: 10, default: 3, description: 'Maximum number of pain entries to return (1..10, default 3).' },
|
|
35
|
+
},
|
|
36
|
+
required: ['principleId'],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
};
|
|
39
|
+
/** Protocol-level schema handed to the SDK (JSON-Schema-first: strict
|
|
40
|
+
* rejection of undeclared parameters at the input-validation stage). */
|
|
41
|
+
export const toolProtocolSchema = fromJsonSchema(toolInputJsonSchema);
|
|
42
|
+
/** In-handler typed re-validation (idempotent post-SDK parse; applies the
|
|
43
|
+
* `limit` default and yields the typed value). Mirrors the JSON schema
|
|
44
|
+
* above — kept in lockstep by the contract tests. */
|
|
45
|
+
export const toolInputSchema = z.strictObject({
|
|
46
|
+
principleId: z.string().min(1).max(200),
|
|
47
|
+
limit: z.number().int().min(1).max(10).optional().default(3),
|
|
48
|
+
});
|
|
49
|
+
/** §4.8 Adapter 执行失败 row (timeout / exception / saturated / shutting
|
|
50
|
+
* down) — one sanitized `tool_execution_failed` envelope, isError: true. */
|
|
51
|
+
function executionFailedResult(message) {
|
|
52
|
+
const envelope = adapterFailureEnvelope('tool_execution_failed', message);
|
|
53
|
+
return textResult(JSON.stringify(envelope), true);
|
|
54
|
+
}
|
|
55
|
+
/** §4.8 "Server 正在关闭" row: refused without touching the capability. */
|
|
56
|
+
function shuttingDownResult() {
|
|
57
|
+
return executionFailedResult('The server is shutting down; the call was not completed.');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the PD MCP server (not yet connected to any transport).
|
|
61
|
+
*
|
|
62
|
+
* Handler flow per §4.7/§4.8: shutdown/saturation gates → deadline-wrapped
|
|
63
|
+
* capability call → envelope assembly (serialize → 32 KiB budget → emit) →
|
|
64
|
+
* explicit isError mapping. Nothing here writes to stdout.
|
|
65
|
+
*/
|
|
66
|
+
export function createPdMcpServer(options) {
|
|
67
|
+
const { workspaceDir, host = null, deadlineMs = DEFAULT_DEADLINE_MS, callCapability = recallPrinciplePainsCapability, logSink = stderrSink, } = options;
|
|
68
|
+
const orphans = new OrphanLimiter();
|
|
69
|
+
const admissions = new AdmissionLimiter();
|
|
70
|
+
let shuttingDown = false;
|
|
71
|
+
const mcpServer = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
72
|
+
const log = createLogger(logSink, { host: host ?? 'unspecified' });
|
|
73
|
+
mcpServer.registerTool(TOOL_NAME, {
|
|
74
|
+
description: TOOL_DESCRIPTION,
|
|
75
|
+
// Static type-interop shim ONLY: our zod 4.6.7 type instantiation does
|
|
76
|
+
// not match the SDK's bundled zod-4.2-based generic constraint, so the
|
|
77
|
+
// overloads fall through to the raw-shape one. Runtime validation is
|
|
78
|
+
// unaffected — the SDK still validates every call against exactly this
|
|
79
|
+
// (fromJsonSchema-built) schema. NOT a validation bypass (rc-2 scope:
|
|
80
|
+
// untrusted-data validation stays fully runtime-checked).
|
|
81
|
+
inputSchema: toolProtocolSchema,
|
|
82
|
+
annotations: { readOnlyHint: true },
|
|
83
|
+
}, async (rawInput) => {
|
|
84
|
+
const startedAt = Date.now();
|
|
85
|
+
// §4.7 admission: reserve execution capacity SYNCHRONOUSLY at handler
|
|
86
|
+
// entry, before any await and before the capability is called. Without
|
|
87
|
+
// this, N concurrent requests all pass an orphan-count check while none
|
|
88
|
+
// has timed out yet and N > limit capability calls start.
|
|
89
|
+
if (shuttingDown) {
|
|
90
|
+
log('warn', 'tool_call_refused_shutting_down', { durationMs: Date.now() - startedAt });
|
|
91
|
+
return shuttingDownResult();
|
|
92
|
+
}
|
|
93
|
+
if (!admissions.tryAcquire()) {
|
|
94
|
+
log('warn', 'tool_call_refused_saturated', {
|
|
95
|
+
durationMs: Date.now() - startedAt,
|
|
96
|
+
active: admissions.active,
|
|
97
|
+
max: admissions.max,
|
|
98
|
+
});
|
|
99
|
+
return executionFailedResult('The server is already executing the maximum number of concurrent calls; retry shortly.');
|
|
100
|
+
}
|
|
101
|
+
// The window covers validation → capability → projection →
|
|
102
|
+
// serialization; validation of the parsed input is part of the race.
|
|
103
|
+
// The `finally` releases capacity when the UNDERLYING work actually
|
|
104
|
+
// settles — a deadline stops the wait, not the reservation. An orphan
|
|
105
|
+
// therefore keeps its slot until its promise resolves/rejects.
|
|
106
|
+
const outcome = await runWithDeadline(async () => {
|
|
107
|
+
try {
|
|
108
|
+
// The SDK already validated against the zod schema; parse again
|
|
109
|
+
// for the typed value (idempotent) and count it inside the window.
|
|
110
|
+
const parsed = toolInputSchema.parse(rawInput);
|
|
111
|
+
const envelope = await callCapability({
|
|
112
|
+
workspaceDir,
|
|
113
|
+
principleId: parsed.principleId,
|
|
114
|
+
limit: parsed.limit,
|
|
115
|
+
});
|
|
116
|
+
const assembled = assembleResult(envelope);
|
|
117
|
+
if (assembled.ok)
|
|
118
|
+
return { kind: 'envelope', result: assembled.result };
|
|
119
|
+
return { kind: 'overBudget', bytes: assembled.overBudgetBytes };
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
// §4.8 非预期异常: map to a sanitized Adapter failure here — the
|
|
123
|
+
// raw exception (message/stack may carry paths or internals)
|
|
124
|
+
// must NEVER reach the SDK's default handler-error path.
|
|
125
|
+
return { kind: 'exception', errorKind: sanitizeErrorKind(err) };
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
admissions.release();
|
|
129
|
+
}
|
|
130
|
+
}, deadlineMs, (orphaned) => orphans.register(orphaned));
|
|
131
|
+
const durationMs = Date.now() - startedAt;
|
|
132
|
+
if (outcome.ok) {
|
|
133
|
+
switch (outcome.value.kind) {
|
|
134
|
+
case 'envelope': {
|
|
135
|
+
log('info', 'tool_call_completed', { durationMs, resultKind: 'envelope' });
|
|
136
|
+
return outcome.value.result;
|
|
137
|
+
}
|
|
138
|
+
case 'overBudget': {
|
|
139
|
+
log('warn', 'tool_call_over_budget', { durationMs, bytes: outcome.value.bytes, budget: RESPONSE_BUDGET_BYTES });
|
|
140
|
+
const failure = adapterFailureEnvelope('response_budget_exceeded', 'The response exceeded the 32 KiB MCP budget and was withheld without truncation.');
|
|
141
|
+
const failureJson = JSON.stringify(failure);
|
|
142
|
+
if (!withinBudget(failureJson)) {
|
|
143
|
+
// Cannot happen (static shell), but fail loud over silent breakage.
|
|
144
|
+
log('error', 'failure_envelope_over_budget', {});
|
|
145
|
+
}
|
|
146
|
+
return textResult(failureJson, true);
|
|
147
|
+
}
|
|
148
|
+
case 'exception': {
|
|
149
|
+
// Sanitized category only — no message, no stack, no paths.
|
|
150
|
+
log('error', 'tool_call_unexpected_exception', { durationMs, errorKind: outcome.value.errorKind });
|
|
151
|
+
return executionFailedResult('The call failed unexpectedly; the error was logged server-side with a sanitized category only.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Deadline exceeded (§4.7): stop waiting, suppress the late result,
|
|
156
|
+
// report an Adapter execution failure. Not underlying cancellation:
|
|
157
|
+
// the capacity slot stays reserved until the orphan settles.
|
|
158
|
+
log('warn', 'tool_call_deadline_exceeded', {
|
|
159
|
+
durationMs,
|
|
160
|
+
deadlineMs,
|
|
161
|
+
active: admissions.active,
|
|
162
|
+
orphans: orphans.size,
|
|
163
|
+
});
|
|
164
|
+
return executionFailedResult(`The call exceeded the ${deadlineMs} ms deadline; the late result is suppressed and will not be delivered.`);
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
mcpServer,
|
|
168
|
+
orphans,
|
|
169
|
+
admissions,
|
|
170
|
+
beginShutdown() {
|
|
171
|
+
shuttingDown = true;
|
|
172
|
+
log('info', 'shutdown_begun', { orphans: orphans.size, active: admissions.active });
|
|
173
|
+
},
|
|
174
|
+
isShuttingDown() {
|
|
175
|
+
return shuttingDown;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server identity — SPEC-PD-MCP-GATE4-001 §9.3 release manifest anchor.
|
|
3
|
+
*
|
|
4
|
+
* SERVER_VERSION mirrors the package version through the release
|
|
5
|
+
* materialization step (`syncMcpServerVersionMirror` in
|
|
6
|
+
* scripts/release/lib/version-plan.mjs); changesets alone rewrites only
|
|
7
|
+
* package.json. A unit test pins it to package.json so the two cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SERVER_NAME: 'pd-mcp-server';
|
|
10
|
+
export declare const SERVER_VERSION: '0.2.0';
|
|
11
|
+
/** The only MCP SDK family/version this server is released against
|
|
12
|
+
* (Gate 4-A freeze, PD_MCP_GATE4A_REALITY_CHECK §3). Exact pin — no ranges. */
|
|
13
|
+
export declare const MCP_SDK_PIN: '@modelcontextprotocol/server@2.4.0';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server identity — SPEC-PD-MCP-GATE4-001 §9.3 release manifest anchor.
|
|
3
|
+
*
|
|
4
|
+
* SERVER_VERSION mirrors the package version through the release
|
|
5
|
+
* materialization step (`syncMcpServerVersionMirror` in
|
|
6
|
+
* scripts/release/lib/version-plan.mjs); changesets alone rewrites only
|
|
7
|
+
* package.json. A unit test pins it to package.json so the two cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
export const SERVER_NAME = 'pd-mcp-server';
|
|
10
|
+
export const SERVER_VERSION = '0.2.0';
|
|
11
|
+
/** The only MCP SDK family/version this server is released against
|
|
12
|
+
* (Gate 4-A freeze, PD_MCP_GATE4A_REALITY_CHECK §3). Exact pin — no ranges. */
|
|
13
|
+
export const MCP_SDK_PIN = '@modelcontextprotocol/server@2.4.0';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const WORKSPACE_DIR_ENV: 'PD_MCP_WORKSPACE_DIR';
|
|
2
|
+
export declare const HOST_ENV: 'PD_MCP_HOST';
|
|
3
|
+
export declare const KNOWN_HOSTS: readonly string[];
|
|
4
|
+
export interface WorkspaceBinding {
|
|
5
|
+
readonly workspaceDir: string;
|
|
6
|
+
readonly host: string | null;
|
|
7
|
+
}
|
|
8
|
+
export type WorkspaceBindingResolution = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly binding: WorkspaceBinding;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
readonly envVar: string;
|
|
15
|
+
readonly nextAction: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Resolve and validate the launch-time workspace binding from a process env
|
|
19
|
+
* (injected for testability; production passes `process.env`). Checks, in
|
|
20
|
+
* order: presence → non-empty string → absolute path → existing directory →
|
|
21
|
+
* (optional) host marker sanity. Any failure is terminal for the process.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveWorkspaceBinding(env: Record<string, unknown>): WorkspaceBindingResolution;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup workspace binding — SPEC-PD-MCP-GATE4-001 §3.5.
|
|
3
|
+
*
|
|
4
|
+
* The business `workspaceDir` is launch configuration, NEVER a model-supplied
|
|
5
|
+
* tool parameter. It arrives via `PD_MCP_WORKSPACE_DIR`, which the host
|
|
6
|
+
* plugin's PD-owned `mcp-binding.json` (through launcher.cjs) injects.
|
|
7
|
+
*
|
|
8
|
+
* Fail-closed everywhere: a missing/relative/nonexistent binding, an unknown
|
|
9
|
+
* host marker, or any attempt to smuggle a path through the tool input
|
|
10
|
+
* refuses to serve. There is NO `process.cwd()`, `HOME` or `USERPROFILE`
|
|
11
|
+
* fallback for business workspace resolution (§3.5: no implicit fallback).
|
|
12
|
+
*
|
|
13
|
+
* This module reads ONLY install/startup metadata (one env + one stat);
|
|
14
|
+
* business data stays behind the Gate 3 Capability.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
export const WORKSPACE_DIR_ENV = 'PD_MCP_WORKSPACE_DIR';
|
|
19
|
+
export const HOST_ENV = 'PD_MCP_HOST';
|
|
20
|
+
export const KNOWN_HOSTS = ['codex', 'openclaw', 'workbuddy'];
|
|
21
|
+
/**
|
|
22
|
+
* Resolve and validate the launch-time workspace binding from a process env
|
|
23
|
+
* (injected for testability; production passes `process.env`). Checks, in
|
|
24
|
+
* order: presence → non-empty string → absolute path → existing directory →
|
|
25
|
+
* (optional) host marker sanity. Any failure is terminal for the process.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveWorkspaceBinding(env) {
|
|
28
|
+
const raw = env[WORKSPACE_DIR_ENV];
|
|
29
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: 'workspace binding env is missing or empty',
|
|
33
|
+
envVar: WORKSPACE_DIR_ENV,
|
|
34
|
+
nextAction: `Re-run the PD host setup (this writes the PD-owned mcp-binding.json and launches via launcher.cjs). Do not set ${WORKSPACE_DIR_ENV} by hand from agent context.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!path.isAbsolute(raw)) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
reason: 'workspace binding is not an absolute path',
|
|
41
|
+
envVar: WORKSPACE_DIR_ENV,
|
|
42
|
+
nextAction: 'Re-run the PD host setup so the binding records the absolute governance workspace directory.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let stat;
|
|
46
|
+
try {
|
|
47
|
+
stat = fs.statSync(raw);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: 'workspace binding directory does not exist or is not readable',
|
|
53
|
+
envVar: WORKSPACE_DIR_ENV,
|
|
54
|
+
nextAction: 'Re-run the PD host setup; if the workspace was moved, re-install PD for this host so the binding is regenerated.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (!stat.isDirectory()) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
reason: 'workspace binding is not a directory',
|
|
61
|
+
envVar: WORKSPACE_DIR_ENV,
|
|
62
|
+
nextAction: 'Re-run the PD host setup so the binding points at the governance workspace directory.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const host = env[HOST_ENV];
|
|
66
|
+
if (host !== undefined) {
|
|
67
|
+
if (typeof host !== 'string' || !KNOWN_HOSTS.includes(host)) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: 'host marker is not one of the known PD hosts',
|
|
71
|
+
envVar: HOST_ENV,
|
|
72
|
+
nextAction: 'Re-run the PD host setup; the host marker is written by the PD installer and must not be edited.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, binding: { workspaceDir: path.resolve(raw), host: typeof host === 'string' ? host : null } };
|
|
77
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@principles/pd-mcp-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Principles Disciple MCP Server — the single shared MCP server implementation for the three PD hosts (Codex, OpenClaw, WorkBuddy). Read-only pain recall through the Gate 3 Capability (SPEC-PD-MCP-GATE4-001).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"NOTICE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"clean": "node ../../scripts/build/clean-dist.mjs",
|
|
23
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && node ../../scripts/build/check-dist-hygiene.mjs",
|
|
24
|
+
"test": "npm run build && vitest run",
|
|
25
|
+
"test:coverage": "vitest run --coverage",
|
|
26
|
+
"lint": "eslint \"src/**/*.ts\""
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/server": "2.4.0",
|
|
30
|
+
"@principles/core": "file:../core",
|
|
31
|
+
"zod": "4.6.7"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@modelcontextprotocol/client": "2.4.0",
|
|
35
|
+
"@types/node": "^26.4.1",
|
|
36
|
+
"typescript": "^7.0.2",
|
|
37
|
+
"vite": "^8.3.4",
|
|
38
|
+
"vitest": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"license": "Apache-2.0",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/csuzngjh/principles.git",
|
|
44
|
+
"directory": "packages/pd-mcp-server"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|