@unbrained/pm-cli 2026.8.24 → 2026.8.26
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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +46 -0
- package/dist/cli-bundle/bundle-manifest.json +149 -149
- package/dist/cli-bundle/chunks/chunk-E2GCFJSU.js +35 -0
- package/dist/cli-bundle/chunks/{chunk-TEQDR5KI.js → chunk-ES25LX3D.js} +62 -62
- package/dist/cli-bundle/chunks/{chunk-I5Q7QSZX.js → chunk-FRDWWB6R.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-X7WWQDYQ.js → chunk-ICQ3RVIY.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-YOLLRCLY.js → chunk-IV64RJVE.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-L22NP5MA.js → chunk-KBFP3E4E.js} +86 -86
- package/dist/cli-bundle/chunks/{chunk-AGUL3HUT.js → chunk-MVYLQ67M.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-DJNJBWAQ.js → chunk-S4U76VZF.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-IW72J5FW.js → chunk-UKBCRPA2.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-WRHJ3MB6.js +8 -0
- package/dist/cli-bundle/chunks/{chunk-ZAVCS2GH.js → chunk-ZNRLJ54C.js} +60 -60
- package/dist/cli-bundle/chunks/{register-list-query-37A4P6O4.js → register-list-query-EMCPMICY.js} +2 -2
- package/dist/cli-bundle/chunks/{register-mutation-TBJDHYIT.js → register-mutation-OJ67ABCB.js} +3 -3
- package/dist/cli-bundle/chunks/{register-operations-WRZNEBUM.js → register-operations-H2GLP7LT.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-H4PDLACD.js → register-setup-GLZAHLVI.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-IMWTYKII.js → chunk-4K2II4TV.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-4XNH2HM7.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-GJR6YEMR.js → chunk-57XY346D.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-4WP7LNE3.js → chunk-66VGB23P.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-72T6JGAE.js +50 -0
- package/dist/cli-bundle/focused-chunks/{chunk-BFA6KKCI.js → chunk-7I23XGWO.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-6Z24ZMU3.js → chunk-7YCDTCBC.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-O4ZXNJMT.js → chunk-A644DUFQ.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-FXDLT6FL.js +16 -0
- package/dist/cli-bundle/focused-chunks/{chunk-RASVBJX5.js → chunk-IBHXMFE7.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-LMKG3DFE.js +5 -0
- package/dist/cli-bundle/focused-chunks/{chunk-6MUTDXMP.js → chunk-LV5N3LK5.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-CWCN5RLO.js → chunk-MMXUPDDJ.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-WZLSSGEU.js → chunk-OHIHZ7HS.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-P2E6LDAE.js +19 -0
- package/dist/cli-bundle/focused-chunks/{chunk-IZNRRCU3.js → chunk-TMJDFHVD.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-QBP5FSBX.js → chunk-UYBA57GY.js} +4 -4
- package/dist/cli-bundle/focused-chunks/{chunk-MNPECIRL.js → chunk-YO3ZF3FI.js} +2 -2
- package/dist/cli-bundle/main.js +2 -2
- package/dist/cli-bundle/sdk-authoring.js +1 -1
- package/dist/cli-bundle/sdk-contracts.js +2 -2
- package/dist/cli-bundle/sdk-core.js +41 -41
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-graph.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +32 -33
- package/dist/cli-bundle/sdk-query.js +1 -1
- package/dist/cli-bundle/sdk-runtime.js +1 -1
- package/dist/cli-bundle/sdk-testing.js +1 -1
- package/dist/cli-bundle/sdk.js +7 -6
- package/dist/core/diagnostics/remediation.js +17 -2
- package/dist/core/extensions/manifest-schema.d.ts +20 -0
- package/dist/core/extensions/manifest-schema.js +28 -10
- package/dist/mcp/http-server.d.ts +60 -0
- package/dist/mcp/http-server.js +451 -0
- package/dist/mcp/legacy-adapter.d.ts +50 -0
- package/dist/mcp/legacy-adapter.js +64 -0
- package/dist/mcp/server.d.ts +42 -9
- package/dist/mcp/server.js +457 -58
- package/dist/sdk/agent/refusal-closure-census.d.ts +6 -2
- package/dist/sdk/agent/refusal-closure-census.js +16 -8
- package/dist/sdk/cli-contracts/runtime-contracts.d.ts +10 -0
- package/dist/sdk/cli-contracts/runtime-contracts.js +20 -3
- package/dist/sdk/compose.d.ts +4 -1
- package/dist/sdk/compose.js +23 -36
- package/dist/sdk/extension/author-manifest.d.ts +22 -0
- package/dist/sdk/extension/author-manifest.js +93 -0
- package/dist/sdk/extension.js +6 -3
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +67 -28
- package/dist/sdk/generated/generated-error-code-catalog-part-2.js +30 -3
- package/dist/sdk/governance/health.js +103 -11
- package/dist/sdk/governance/validate.js +15 -9
- package/dist/sdk/history-repair.d.ts +24 -0
- package/dist/sdk/history-repair.js +145 -4
- package/dist/sdk/index.d.ts +6 -0
- package/dist/sdk/index.js +8 -2
- package/dist/sdk/mcp/authorization.d.ts +134 -0
- package/dist/sdk/mcp/authorization.js +405 -0
- package/dist/sdk/mcp/interactions.d.ts +118 -0
- package/dist/sdk/mcp/interactions.js +337 -0
- package/dist/sdk/mcp/protocol.d.ts +142 -0
- package/dist/sdk/mcp/protocol.js +174 -0
- package/dist/sdk/mcp/subscriptions.d.ts +65 -0
- package/dist/sdk/mcp/subscriptions.js +212 -0
- package/dist/sdk/mcp/tasks.d.ts +107 -0
- package/dist/sdk/mcp/tasks.js +431 -0
- package/dist/sdk/mcp/transport.d.ts +30 -0
- package/dist/sdk/mcp/transport.js +261 -0
- package/dist/sdk/merge/driver.js +25 -4
- package/dist/sdk/merge/receipt-file-boundary.d.ts +21 -0
- package/dist/sdk/merge/receipt-file-boundary.js +47 -0
- package/dist/sdk/merge/receipts.d.ts +40 -1
- package/dist/sdk/merge/receipts.js +343 -36
- package/dist/sdk/merge/reconcile.js +117 -47
- package/dist/sdk/read-output/continuation.d.ts +15 -2
- package/dist/sdk/read-output/continuation.js +86 -6
- package/dist/sdk/read-output-contracts.d.ts +4 -1
- package/dist/sdk/read-output-contracts.js +9 -6
- package/dist/sdk/runtime-primitives.d.ts +1 -1
- package/dist/sdk/runtime-primitives.js +3 -3
- package/dist/sdk/test/execution.d.ts +6 -0
- package/dist/sdk/test/execution.js +32 -3
- package/docs/AGENT_PROVENANCE_ADR.md +6 -4
- package/docs/AGENT_RUNTIME_PRIMITIVES.md +6 -5
- package/docs/CLAUDE_CODE_PLUGIN.md +12 -5
- package/docs/DIAGNOSTIC_OUTPUT_CONTRACTS.md +8 -0
- package/docs/EXTENSIONS.md +3 -4
- package/docs/MCP_2026_07_28.md +138 -0
- package/docs/MCP_2026_07_28_CONFORMANCE.md +30 -0
- package/docs/MCP_REMOTE_TRANSPORT_SECURITY.md +180 -0
- package/docs/MERGE_SAFETY.md +34 -5
- package/docs/OUTPUT_PROJECTION_CONTRACTS.md +13 -1
- package/docs/README.md +4 -0
- package/docs/READ_OUTPUT_CONTRACTS.md +25 -2
- package/docs/RELEASING.md +3 -2
- package/docs/SDK.md +22 -1
- package/docs/SDK_AGENT_SESSION_CONTEXT.md +18 -13
- package/docs/SDK_MCP_INTERACTIONS.md +227 -0
- package/docs/TESTING.md +4 -0
- package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +13 -8
- package/marketplace.json +2 -2
- package/package.json +7 -5
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/scripts/finalize-build.mjs +1 -0
- package/sdk/public-surface.json +762 -15
- package/dist/cli-bundle/chunks/chunk-MKE6MD2K.js +0 -36
- package/dist/cli-bundle/chunks/chunk-NXBFO63A.js +0 -8
- package/dist/cli-bundle/focused-chunks/chunk-FCTT4CZU.js +0 -5
- package/dist/cli-bundle/focused-chunks/chunk-Q63PSLO4.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-QV5UP27O.js +0 -51
- package/dist/cli-bundle/focused-chunks/chunk-RPRLENVA.js +0 -16
- package/dist/cli-bundle/focused-chunks/chunk-TOFVXBWA.js +0 -19
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module sdk/mcp/transport
|
|
3
|
+
*
|
|
4
|
+
* Implements MCP 2026-07-28 Streamable HTTP header encoding, schema-derived
|
|
5
|
+
* custom header extraction, and body/header parity validation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5ffe7128-0fbb-59af-89ef-74d5d799a00d")}catch(e){}}();
|
|
9
|
+
import { PM_MCP_ERROR_CODES, PM_MCP_PROTOCOL_VERSION, PmMcpProtocolError, isMcpRecord, resolveMcpRequestContext, } from "./protocol.js";
|
|
10
|
+
const BASE64_SENTINEL_PATTERN = /^=\?base64\?([A-Za-z0-9+/]*={0,2})\?=$/u;
|
|
11
|
+
const HEADER_NAME_TOKEN_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u;
|
|
12
|
+
const PLAIN_HEADER_VALUE_PATTERN = /^[\t\x20-\x7E]*$/u;
|
|
13
|
+
const REQUIRED_NAME_METHODS = new Set([
|
|
14
|
+
"prompts/get",
|
|
15
|
+
"resources/read",
|
|
16
|
+
"tools/call",
|
|
17
|
+
]);
|
|
18
|
+
function normalizedHeaders(headers) {
|
|
19
|
+
const normalized = new Map();
|
|
20
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
continue;
|
|
23
|
+
const entry = typeof value === "string" ? value : value.join(", ");
|
|
24
|
+
normalized.set(name.toLowerCase(), entry);
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
function failHeader(message, header) {
|
|
29
|
+
throw new PmMcpProtocolError(message, PM_MCP_ERROR_CODES.headerMismatch, {
|
|
30
|
+
header,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Encode a name or primitive parameter into the protocol's safe header form. */
|
|
34
|
+
export function encodeMcpHttpHeaderValue(value) {
|
|
35
|
+
const serialized = typeof value === "boolean" ? String(value).toLowerCase() : String(value);
|
|
36
|
+
if (serialized.length > 0 &&
|
|
37
|
+
serialized.trim() === serialized &&
|
|
38
|
+
PLAIN_HEADER_VALUE_PATTERN.test(serialized) &&
|
|
39
|
+
!BASE64_SENTINEL_PATTERN.test(serialized)) {
|
|
40
|
+
return serialized;
|
|
41
|
+
}
|
|
42
|
+
return `=?base64?${Buffer.from(serialized, "utf8").toString("base64")}?=`;
|
|
43
|
+
}
|
|
44
|
+
/** Decode and validate a mirrored MCP header value. */
|
|
45
|
+
export function decodeMcpHttpHeaderValue(value, header) {
|
|
46
|
+
const sentinel = BASE64_SENTINEL_PATTERN.exec(value);
|
|
47
|
+
if (sentinel) {
|
|
48
|
+
const encoded = sentinel[1];
|
|
49
|
+
const decoded = Buffer.from(encoded, "base64");
|
|
50
|
+
if (decoded.toString("base64") !== encoded || decoded.length === 0) {
|
|
51
|
+
return failHeader("Malformed MCP Base64 header value", header);
|
|
52
|
+
}
|
|
53
|
+
return decoded.toString("utf8");
|
|
54
|
+
}
|
|
55
|
+
if (value.length === 0 ||
|
|
56
|
+
value.trim() !== value ||
|
|
57
|
+
!PLAIN_HEADER_VALUE_PATTERN.test(value)) {
|
|
58
|
+
return failHeader("Malformed MCP header value", header);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function containsHeaderAnnotation(value) {
|
|
63
|
+
if (Array.isArray(value))
|
|
64
|
+
return value.some(containsHeaderAnnotation);
|
|
65
|
+
if (!isMcpRecord(value))
|
|
66
|
+
return false;
|
|
67
|
+
return (Object.prototype.hasOwnProperty.call(value, "x-mcp-header") ||
|
|
68
|
+
Object.values(value).some(containsHeaderAnnotation));
|
|
69
|
+
}
|
|
70
|
+
/** Validate and normalize one schema-derived custom-header annotation. */
|
|
71
|
+
function parseMcpHeaderAnnotation(propertySchema, propertyPath, names) {
|
|
72
|
+
const annotation = propertySchema["x-mcp-header"];
|
|
73
|
+
if (annotation === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const type = propertySchema.type;
|
|
76
|
+
if (typeof annotation !== "string" ||
|
|
77
|
+
!HEADER_NAME_TOKEN_PATTERN.test(annotation) ||
|
|
78
|
+
!["boolean", "integer", "string"].includes(String(type)) ||
|
|
79
|
+
names.has(annotation.toLowerCase())) {
|
|
80
|
+
throw new PmMcpProtocolError("Invalid MCP x-mcp-header annotation", PM_MCP_ERROR_CODES.invalidParams, { field: propertyPath.join("."), annotation });
|
|
81
|
+
}
|
|
82
|
+
names.add(annotation.toLowerCase());
|
|
83
|
+
return {
|
|
84
|
+
name: annotation,
|
|
85
|
+
path: propertyPath,
|
|
86
|
+
type: type,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Parse valid statically reachable custom-header annotations from a tool schema. */
|
|
90
|
+
export function collectMcpHeaderAnnotations(schema) {
|
|
91
|
+
if (!isMcpRecord(schema)) {
|
|
92
|
+
throw new PmMcpProtocolError("Invalid MCP tool schema", PM_MCP_ERROR_CODES.invalidParams, { field: "inputSchema", expected: "object" });
|
|
93
|
+
}
|
|
94
|
+
const annotations = [];
|
|
95
|
+
const names = new Set();
|
|
96
|
+
const visitProperties = (node, path) => {
|
|
97
|
+
const properties = node.properties;
|
|
98
|
+
if (properties === undefined)
|
|
99
|
+
return;
|
|
100
|
+
if (!isMcpRecord(properties)) {
|
|
101
|
+
throw new PmMcpProtocolError("Invalid MCP tool schema properties", PM_MCP_ERROR_CODES.invalidParams, { field: "inputSchema.properties" });
|
|
102
|
+
}
|
|
103
|
+
for (const [property, propertySchema] of Object.entries(properties)) {
|
|
104
|
+
if (!isMcpRecord(propertySchema))
|
|
105
|
+
continue;
|
|
106
|
+
const propertyPath = [...path, property];
|
|
107
|
+
const annotation = parseMcpHeaderAnnotation(propertySchema, propertyPath, names);
|
|
108
|
+
if (annotation)
|
|
109
|
+
annotations.push(annotation);
|
|
110
|
+
for (const [keyword, nested] of Object.entries(propertySchema)) {
|
|
111
|
+
if (keyword !== "properties" &&
|
|
112
|
+
keyword !== "x-mcp-header" &&
|
|
113
|
+
containsHeaderAnnotation(nested)) {
|
|
114
|
+
throw new PmMcpProtocolError("MCP x-mcp-header is not statically reachable", PM_MCP_ERROR_CODES.invalidParams, { field: propertyPath.join("."), keyword });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
visitProperties(propertySchema, propertyPath);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
for (const [keyword, nested] of Object.entries(schema)) {
|
|
121
|
+
if (keyword !== "properties" && containsHeaderAnnotation(nested)) {
|
|
122
|
+
throw new PmMcpProtocolError("MCP x-mcp-header is not statically reachable", PM_MCP_ERROR_CODES.invalidParams, { field: "inputSchema", keyword });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
visitProperties(schema, []);
|
|
126
|
+
return annotations;
|
|
127
|
+
}
|
|
128
|
+
function valueAtPath(value, path) {
|
|
129
|
+
let current = value;
|
|
130
|
+
for (const part of path) {
|
|
131
|
+
if (!isMcpRecord(current))
|
|
132
|
+
return undefined;
|
|
133
|
+
current = current[part];
|
|
134
|
+
}
|
|
135
|
+
return current;
|
|
136
|
+
}
|
|
137
|
+
function mirroredName(method, params) {
|
|
138
|
+
const value = method === "resources/read" ? params.uri : params.name;
|
|
139
|
+
return typeof value === "string" ? value : undefined;
|
|
140
|
+
}
|
|
141
|
+
function buildMcpCustomRequestHeaders(params, toolSchema) {
|
|
142
|
+
const argumentsValue = isMcpRecord(params.arguments) ? params.arguments : {};
|
|
143
|
+
const headers = {};
|
|
144
|
+
for (const annotation of collectMcpHeaderAnnotations(toolSchema)) {
|
|
145
|
+
const value = valueAtPath(argumentsValue, annotation.path);
|
|
146
|
+
if (value === undefined || value === null)
|
|
147
|
+
continue;
|
|
148
|
+
const valid = (annotation.type === "integer" &&
|
|
149
|
+
typeof value === "number" &&
|
|
150
|
+
Number.isSafeInteger(value)) ||
|
|
151
|
+
(annotation.type === "boolean" && typeof value === "boolean") ||
|
|
152
|
+
(annotation.type === "string" && typeof value === "string");
|
|
153
|
+
if (!valid) {
|
|
154
|
+
throw new PmMcpProtocolError("Invalid MCP custom header argument", PM_MCP_ERROR_CODES.invalidParams, { field: annotation.path.join("."), expected: annotation.type });
|
|
155
|
+
}
|
|
156
|
+
headers[`Mcp-Param-${annotation.name}`] = encodeMcpHttpHeaderValue(value);
|
|
157
|
+
}
|
|
158
|
+
return headers;
|
|
159
|
+
}
|
|
160
|
+
/** Build required Streamable HTTP headers for one modern client request. */
|
|
161
|
+
export function buildMcpHttpRequestHeaders(input) {
|
|
162
|
+
const params = isMcpRecord(input.request.params)
|
|
163
|
+
? input.request.params
|
|
164
|
+
: undefined;
|
|
165
|
+
const context = resolveMcpRequestContext(params);
|
|
166
|
+
const method = input.request.method;
|
|
167
|
+
if (typeof method !== "string" || method.length === 0) {
|
|
168
|
+
return failHeader("Missing MCP request method", "Mcp-Method");
|
|
169
|
+
}
|
|
170
|
+
const headers = {
|
|
171
|
+
Accept: "application/json, text/event-stream",
|
|
172
|
+
"Content-Type": "application/json",
|
|
173
|
+
"MCP-Protocol-Version": context.protocolVersion,
|
|
174
|
+
"Mcp-Method": method,
|
|
175
|
+
};
|
|
176
|
+
const name = mirroredName(method, params);
|
|
177
|
+
if (REQUIRED_NAME_METHODS.has(method)) {
|
|
178
|
+
if (name === undefined)
|
|
179
|
+
return failHeader("Missing MCP request name", "Mcp-Name");
|
|
180
|
+
headers["Mcp-Name"] = encodeMcpHttpHeaderValue(name);
|
|
181
|
+
}
|
|
182
|
+
if (method === "tools/call" && input.toolSchema !== undefined) {
|
|
183
|
+
Object.assign(headers, buildMcpCustomRequestHeaders(params, input.toolSchema));
|
|
184
|
+
}
|
|
185
|
+
return headers;
|
|
186
|
+
}
|
|
187
|
+
function validateMcpRequiredRequestHeaders(headers, request) {
|
|
188
|
+
const method = request.method;
|
|
189
|
+
if (typeof method !== "string" || method.length === 0) {
|
|
190
|
+
failHeader("Missing MCP request method", "Mcp-Method");
|
|
191
|
+
}
|
|
192
|
+
const params = isMcpRecord(request.params) ? request.params : undefined;
|
|
193
|
+
const expectedProtocol = resolveMcpRequestContext(params).protocolVersion;
|
|
194
|
+
if (headers.get("mcp-protocol-version") !== expectedProtocol) {
|
|
195
|
+
failHeader("MCP protocol header does not match request metadata", "MCP-Protocol-Version");
|
|
196
|
+
}
|
|
197
|
+
if (headers.get("mcp-method") !== method) {
|
|
198
|
+
failHeader("MCP method header does not match request body", "Mcp-Method");
|
|
199
|
+
}
|
|
200
|
+
if (REQUIRED_NAME_METHODS.has(method)) {
|
|
201
|
+
const headerName = headers.get("mcp-name");
|
|
202
|
+
const name = mirroredName(method, params);
|
|
203
|
+
if (headerName === undefined ||
|
|
204
|
+
name === undefined ||
|
|
205
|
+
decodeMcpHttpHeaderValue(headerName, "Mcp-Name") !== name) {
|
|
206
|
+
failHeader("MCP name header does not match request body", "Mcp-Name");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (headers.has("last-event-id")) {
|
|
210
|
+
// The preceding match is an mcp-deprecation-negative-control: no resume/redelivery.
|
|
211
|
+
failHeader("MCP SSE resumability is not supported", "Last-Event-ID");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function customHeaderMatches(annotation, value, decoded) {
|
|
215
|
+
if (annotation.type === "integer") {
|
|
216
|
+
return (typeof value === "number" &&
|
|
217
|
+
Number.isSafeInteger(value) &&
|
|
218
|
+
Number(decoded) === value);
|
|
219
|
+
}
|
|
220
|
+
if (annotation.type === "boolean") {
|
|
221
|
+
return typeof value === "boolean" && decoded === String(value);
|
|
222
|
+
}
|
|
223
|
+
return typeof value === "string" && decoded === value;
|
|
224
|
+
}
|
|
225
|
+
function validateMcpCustomRequestHeaders(input) {
|
|
226
|
+
const argumentsValue = isMcpRecord(input.params.arguments)
|
|
227
|
+
? input.params.arguments
|
|
228
|
+
: {};
|
|
229
|
+
for (const annotation of collectMcpHeaderAnnotations(input.toolSchema)) {
|
|
230
|
+
const value = valueAtPath(argumentsValue, annotation.path);
|
|
231
|
+
const header = `Mcp-Param-${annotation.name}`;
|
|
232
|
+
const mirrored = input.headers.get(header.toLowerCase());
|
|
233
|
+
if (value === undefined || value === null) {
|
|
234
|
+
if (mirrored !== undefined)
|
|
235
|
+
failHeader("Unexpected MCP parameter header", header);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (mirrored === undefined)
|
|
239
|
+
failHeader("Missing MCP parameter header", header);
|
|
240
|
+
if (!customHeaderMatches(annotation, value, decodeMcpHttpHeaderValue(mirrored, header))) {
|
|
241
|
+
failHeader("MCP parameter header does not match request body", header);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Validate required Streamable HTTP headers against the JSON-RPC body. */
|
|
246
|
+
export function validateMcpHttpRequestHeaders(input) {
|
|
247
|
+
const headers = normalizedHeaders(input.headers);
|
|
248
|
+
const method = typeof input.request.method === "string" ? input.request.method : "";
|
|
249
|
+
validateMcpRequiredRequestHeaders(headers, input.request);
|
|
250
|
+
if (method !== "tools/call" || input.toolSchema === undefined)
|
|
251
|
+
return;
|
|
252
|
+
validateMcpCustomRequestHeaders({
|
|
253
|
+
headers,
|
|
254
|
+
params: input.request.params,
|
|
255
|
+
toolSchema: input.toolSchema,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/** Required protocol version exposed for clients constructing headers manually. */
|
|
259
|
+
export const PM_MCP_HTTP_PROTOCOL_VERSION = PM_MCP_PROTOCOL_VERSION;
|
|
260
|
+
//# sourceMappingURL=transport.js.map
|
|
261
|
+
//# debugId=5ffe7128-0fbb-59af-89ef-74d5d799a00d
|
package/dist/sdk/merge/driver.js
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* merge never corrupts tracker storage the way raw conflict markers do.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
12
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2c8046a5-06aa-58f8-b347-e757086082d1")}catch(e){}}();
|
|
13
13
|
import { readFile, writeFile } from "node:fs/promises";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { getActiveExtensionRegistrations } from "../../core/extensions/index.js";
|
|
16
16
|
import { collectRegisteredItemFieldNames } from "../../core/extensions/item-fields.js";
|
|
17
17
|
import { pathExists } from "../../core/fs/fs-utils.js";
|
|
18
|
+
import { parseItemDocument } from "../../core/item/item-format.js";
|
|
18
19
|
import { EXIT_CODE } from "../../core/shared/constants.js";
|
|
19
20
|
import { PmCliError } from "../../core/shared/errors.js";
|
|
21
|
+
import { sha256Hex, stableStringify } from "../../core/shared/serialization.js";
|
|
20
22
|
import { nowIso } from "../../core/shared/time.js";
|
|
21
23
|
import { getSettingsPath, resolvePmRoot } from "../../core/store/paths.js";
|
|
22
24
|
import { readSettings } from "../../core/store/settings.js";
|
|
@@ -122,10 +124,12 @@ export async function runMergeDriver(options, global) {
|
|
|
122
124
|
}
|
|
123
125
|
else if (artifact === "item") {
|
|
124
126
|
const settings = await loadOptionalSettings(global);
|
|
127
|
+
const itemFormat = itemFormatForPath(options.itemPath ?? outputPath, settings);
|
|
128
|
+
const extensionFieldNames = collectRegisteredItemFieldNames(getActiveExtensionRegistrations());
|
|
125
129
|
const itemMerge = mergeItemDocuments(baseRaw, oursRaw, theirsRaw, {
|
|
126
|
-
format:
|
|
130
|
+
format: itemFormat,
|
|
127
131
|
schema: settings?.schema,
|
|
128
|
-
extensionFieldNames
|
|
132
|
+
extensionFieldNames,
|
|
129
133
|
preferred,
|
|
130
134
|
conflictResolution: "stable_value_order",
|
|
131
135
|
});
|
|
@@ -137,6 +141,19 @@ export async function runMergeDriver(options, global) {
|
|
|
137
141
|
conflict_resolution: "stable_value_order",
|
|
138
142
|
};
|
|
139
143
|
if (options.itemPath !== undefined) {
|
|
144
|
+
const mergedDocument = parseItemDocument(merged, {
|
|
145
|
+
format: itemFormat,
|
|
146
|
+
schema: settings?.schema,
|
|
147
|
+
extensionFieldNames,
|
|
148
|
+
});
|
|
149
|
+
const mergedMetadata = mergedDocument.metadata;
|
|
150
|
+
const receiptFields = [
|
|
151
|
+
...new Set([
|
|
152
|
+
...itemMerge.fields_from_theirs,
|
|
153
|
+
...itemMerge.union_fields,
|
|
154
|
+
...itemMerge.conflict_decisions.map((decision) => decision.field),
|
|
155
|
+
]),
|
|
156
|
+
];
|
|
140
157
|
receiptInput = {
|
|
141
158
|
cwd: process.cwd(),
|
|
142
159
|
itemPath: options.itemPath,
|
|
@@ -144,6 +161,10 @@ export async function runMergeDriver(options, global) {
|
|
|
144
161
|
conflictResolution: "stable_value_order",
|
|
145
162
|
fieldsFromTheirs: itemMerge.fields_from_theirs,
|
|
146
163
|
unionFields: itemMerge.union_fields,
|
|
164
|
+
mergedFieldHashes: Object.fromEntries(receiptFields.map((field) => [
|
|
165
|
+
field,
|
|
166
|
+
sha256Hex(stableStringify(field === "body" ? mergedDocument.body : mergedMetadata[field])),
|
|
167
|
+
])),
|
|
147
168
|
decisions: itemMerge.conflict_decisions,
|
|
148
169
|
};
|
|
149
170
|
}
|
|
@@ -187,4 +208,4 @@ export async function runMergeDriver(options, global) {
|
|
|
187
208
|
};
|
|
188
209
|
}
|
|
189
210
|
//# sourceMappingURL=driver.js.map
|
|
190
|
-
//# debugId=
|
|
211
|
+
//# debugId=2c8046a5-06aa-58f8-b347-e757086082d1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module sdk/merge/receipt-file-boundary
|
|
3
|
+
*
|
|
4
|
+
* Reads untrusted receipt candidates through a bounded, no-follow file
|
|
5
|
+
* boundary whose race and short-read behavior can be exercised directly.
|
|
6
|
+
*/
|
|
7
|
+
import { type Stats } from "node:fs";
|
|
8
|
+
import { type FileHandle } from "node:fs/promises";
|
|
9
|
+
/** Minimal filesystem operations required by the receipt reader. */
|
|
10
|
+
export interface ReceiptFileBoundary {
|
|
11
|
+
/** Inspect the path without following its final symbolic link. */
|
|
12
|
+
lstat(receiptPath: string): Promise<Stats>;
|
|
13
|
+
/** Open the candidate and return the handle used for verified reads. */
|
|
14
|
+
open(receiptPath: string, flags: number): Promise<FileHandle>;
|
|
15
|
+
}
|
|
16
|
+
/** Resolve the platform no-follow flag, falling back only where Node omits it. */
|
|
17
|
+
export declare function resolveReceiptNoFollowFlag(constants: {
|
|
18
|
+
O_NOFOLLOW?: number;
|
|
19
|
+
}): number;
|
|
20
|
+
/** Read one regular file only when its opened identity remains bounded and stable. */
|
|
21
|
+
export declare function readBoundedRegularFile(receiptPath: string, maximumBytes: number, boundary?: ReceiptFileBoundary): Promise<string | null>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module sdk/merge/receipt-file-boundary
|
|
3
|
+
*
|
|
4
|
+
* Reads untrusted receipt candidates through a bounded, no-follow file
|
|
5
|
+
* boundary whose race and short-read behavior can be exercised directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6ece0e15-aff5-5e2d-b18a-16ffaa0fe2ea")}catch(e){}}();
|
|
9
|
+
import { constants as fsConstants } from "node:fs";
|
|
10
|
+
import { lstat, open } from "node:fs/promises";
|
|
11
|
+
const NODE_RECEIPT_FILE_BOUNDARY = { lstat, open };
|
|
12
|
+
/** Resolve the platform no-follow flag, falling back only where Node omits it. */
|
|
13
|
+
export function resolveReceiptNoFollowFlag(constants) {
|
|
14
|
+
return constants.O_NOFOLLOW ?? 0;
|
|
15
|
+
}
|
|
16
|
+
/** Read one regular file only when its opened identity remains bounded and stable. */
|
|
17
|
+
export async function readBoundedRegularFile(receiptPath, maximumBytes, boundary = NODE_RECEIPT_FILE_BOUNDARY) {
|
|
18
|
+
const pathStats = await boundary.lstat(receiptPath);
|
|
19
|
+
if (pathStats.isSymbolicLink() ||
|
|
20
|
+
!pathStats.isFile() ||
|
|
21
|
+
pathStats.size > maximumBytes) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const handle = await boundary.open(receiptPath, fsConstants.O_RDONLY | resolveReceiptNoFollowFlag(fsConstants));
|
|
25
|
+
try {
|
|
26
|
+
const openedStats = await handle.stat();
|
|
27
|
+
if (!openedStats.isFile() || openedStats.size > maximumBytes)
|
|
28
|
+
return null;
|
|
29
|
+
const buffer = Buffer.alloc(openedStats.size);
|
|
30
|
+
let offset = 0;
|
|
31
|
+
while (offset < buffer.length) {
|
|
32
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
33
|
+
if (bytesRead === 0)
|
|
34
|
+
break;
|
|
35
|
+
offset += bytesRead;
|
|
36
|
+
}
|
|
37
|
+
const finalStats = await handle.stat();
|
|
38
|
+
return finalStats.size === openedStats.size && offset === openedStats.size
|
|
39
|
+
? buffer.toString("utf8", 0, offset)
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await handle.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=receipt-file-boundary.js.map
|
|
47
|
+
//# debugId=6ece0e15-aff5-5e2d-b18a-16ffaa0fe2ea
|
|
@@ -19,6 +19,8 @@ export interface MergeDecisionReceipt {
|
|
|
19
19
|
fields_from_theirs: string[];
|
|
20
20
|
/** Collections combined from both branches. */
|
|
21
21
|
union_fields: string[];
|
|
22
|
+
/** SHA-256 hashes of the merged values for every field represented by this receipt. Absent on legacy receipts, which cannot prove drift attribution. */
|
|
23
|
+
merged_field_hashes?: Record<string, string>;
|
|
22
24
|
/** Full recoverable scalar decisions, retained only in the clone. */
|
|
23
25
|
decisions: ItemMergeConflictDecision[];
|
|
24
26
|
/** Whether a merge reconciliation history event consumed this receipt. */
|
|
@@ -29,6 +31,8 @@ export interface MergeDecisionReceipt {
|
|
|
29
31
|
reconciled_at?: string;
|
|
30
32
|
/** Whether decision values are recoverable locally or represented by hashes only. */
|
|
31
33
|
value_availability?: "clone_local" | "hash_only";
|
|
34
|
+
/** Runtime-only provenance assigned by the reader; serialized values are ignored. */
|
|
35
|
+
evidence_source?: "clone_local" | "durable";
|
|
32
36
|
}
|
|
33
37
|
/** Privacy-safe receipt summary suitable for committed history context. */
|
|
34
38
|
export interface MergeDecisionReceiptSummary {
|
|
@@ -66,6 +70,24 @@ export interface MergeReceiptReport {
|
|
|
66
70
|
/** ISO timestamp for the report. */
|
|
67
71
|
generated_at: string;
|
|
68
72
|
}
|
|
73
|
+
/** Internal integrity scan result used by health without exposing malformed receipt contents. */
|
|
74
|
+
export interface MergeReceiptEvidenceScan {
|
|
75
|
+
/** Valid receipts that passed bounded-file, schema, and identity validation. */
|
|
76
|
+
receipts: MergeDecisionReceipt[];
|
|
77
|
+
/** Number of JSON receipt candidates that could not be validated safely. */
|
|
78
|
+
invalid_evidence_count: number;
|
|
79
|
+
}
|
|
80
|
+
declare function prepareReceiptSettlement(params: {
|
|
81
|
+
receiptPath: string;
|
|
82
|
+
receiptId: string;
|
|
83
|
+
evidenceSource: "clone_local" | "durable";
|
|
84
|
+
reconciledAt: string;
|
|
85
|
+
readReceipt?: (receiptPath: string) => Promise<string | null>;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
path: string;
|
|
88
|
+
content: string;
|
|
89
|
+
fingerprint: string;
|
|
90
|
+
} | null>;
|
|
69
91
|
/** Convert a raw receipt into privacy-safe history context. */
|
|
70
92
|
export declare function summarizeMergeReceipt(receipt: MergeDecisionReceipt): MergeDecisionReceiptSummary;
|
|
71
93
|
/** Persist one item-driver outcome in the clone-local Git directory. */
|
|
@@ -76,8 +98,18 @@ export declare function writeMergeReceipt(params: {
|
|
|
76
98
|
conflictResolution?: "preferred_side" | "stable_value_order";
|
|
77
99
|
fieldsFromTheirs: string[];
|
|
78
100
|
unionFields: string[];
|
|
101
|
+
mergedFieldHashes?: Record<string, string>;
|
|
79
102
|
decisions: ItemMergeConflictDecision[];
|
|
80
103
|
}): Promise<MergeDecisionReceipt | null>;
|
|
104
|
+
/** Inspect valid receipts and count invalid evidence without returning untrusted file contents. */
|
|
105
|
+
export declare function inspectMergeReceiptEvidence(cwd: string, options?: {
|
|
106
|
+
/** Include receipts whose reconciliation state is already settled. */
|
|
107
|
+
includeReconciled?: boolean;
|
|
108
|
+
/** Include receipts that contain no discarded scalar values. */
|
|
109
|
+
includeLossless?: boolean;
|
|
110
|
+
/** Explicit tracker root used to locate durable receipt evidence. */
|
|
111
|
+
pmRoot?: string;
|
|
112
|
+
}): Promise<MergeReceiptEvidenceScan>;
|
|
81
113
|
/** Read clone-local receipts with explicit reconciled/lossless classification controls. */
|
|
82
114
|
export declare function listMergeReceipts(cwd: string, options?: {
|
|
83
115
|
includeReconciled?: boolean;
|
|
@@ -92,7 +124,9 @@ export declare function partitionMergeReceipts(receipts: MergeDecisionReceipt[])
|
|
|
92
124
|
lossless: MergeDecisionReceipt[];
|
|
93
125
|
};
|
|
94
126
|
/** Mark a receipt as represented by a committed merge history event. */
|
|
95
|
-
export declare function markMergeReceiptReconciled(cwd: string, receipt: MergeDecisionReceipt
|
|
127
|
+
export declare function markMergeReceiptReconciled(cwd: string, receipt: MergeDecisionReceipt, options?: {
|
|
128
|
+
requireExisting?: boolean;
|
|
129
|
+
}): Promise<void>;
|
|
96
130
|
/** Report pending or historical clone-local merge decisions. */
|
|
97
131
|
export declare function runMergeReceiptReport(options: {
|
|
98
132
|
/** Include receipts already represented by merge history events. */
|
|
@@ -100,3 +134,8 @@ export declare function runMergeReceiptReport(options: {
|
|
|
100
134
|
/** Repository directory to inspect; defaults to the process working directory. */
|
|
101
135
|
cwd?: string;
|
|
102
136
|
}): Promise<MergeReceiptReport>;
|
|
137
|
+
/** Test-only seams for deterministic receipt-boundary fault coverage. */
|
|
138
|
+
export declare const _testOnlyMergeReceipts: {
|
|
139
|
+
prepareReceiptSettlement: typeof prepareReceiptSettlement;
|
|
140
|
+
};
|
|
141
|
+
export {};
|