@wix/pathgrade 1.0.25 → 1.0.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.
Files changed (75) hide show
  1. package/README.md +42 -0
  2. package/dist/agents/claude/sdk-options.d.ts +1 -2
  3. package/dist/agents/claude.js +2 -1
  4. package/dist/agents/codex-app-server/agent.js +83 -46
  5. package/dist/agents/codex-app-server/scenario-mount.d.ts +40 -0
  6. package/dist/agents/codex-app-server/scenario-mount.js +238 -0
  7. package/dist/agents/cursor-scenario.d.ts +20 -0
  8. package/dist/agents/cursor-scenario.js +96 -0
  9. package/dist/agents/cursor.js +20 -0
  10. package/dist/agents/opencode/scenario.d.ts +25 -0
  11. package/dist/agents/opencode/scenario.js +195 -0
  12. package/dist/agents/opencode.js +50 -0
  13. package/dist/internal/direct-mcp-v2/acp-author-projector.d.ts +20 -0
  14. package/dist/internal/direct-mcp-v2/acp-author-projector.js +147 -0
  15. package/dist/internal/direct-mcp-v2/acp-author-session.d.ts +39 -0
  16. package/dist/internal/direct-mcp-v2/acp-author-session.js +201 -0
  17. package/dist/internal/direct-mcp-v2/acp-client.d.ts +70 -0
  18. package/dist/internal/direct-mcp-v2/acp-client.js +310 -0
  19. package/dist/internal/direct-mcp-v2/acp-direct-mcp.d.ts +38 -0
  20. package/dist/internal/direct-mcp-v2/acp-direct-mcp.js +326 -0
  21. package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +68 -0
  22. package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +456 -0
  23. package/dist/internal/direct-mcp-v2/claude-profile.d.ts +9 -0
  24. package/dist/internal/direct-mcp-v2/claude-profile.js +97 -0
  25. package/dist/internal/direct-mcp-v2/cleanup.d.ts +62 -0
  26. package/dist/internal/direct-mcp-v2/cleanup.js +182 -0
  27. package/dist/internal/direct-mcp-v2/codex-direct-mcp.d.ts +41 -0
  28. package/dist/internal/direct-mcp-v2/codex-direct-mcp.js +436 -0
  29. package/dist/internal/direct-mcp-v2/codex-profile.d.ts +12 -0
  30. package/dist/internal/direct-mcp-v2/codex-profile.js +81 -0
  31. package/dist/internal/direct-mcp-v2/compiler.d.ts +12 -0
  32. package/dist/internal/direct-mcp-v2/compiler.js +449 -0
  33. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.d.ts +14 -0
  34. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.js +60 -0
  35. package/dist/internal/direct-mcp-v2/cursor-profile.d.ts +38 -0
  36. package/dist/internal/direct-mcp-v2/cursor-profile.js +79 -0
  37. package/dist/internal/direct-mcp-v2/evidence.d.ts +50 -0
  38. package/dist/internal/direct-mcp-v2/evidence.js +147 -0
  39. package/dist/internal/direct-mcp-v2/host-reducer.d.ts +61 -0
  40. package/dist/internal/direct-mcp-v2/host-reducer.js +214 -0
  41. package/dist/internal/direct-mcp-v2/json.d.ts +9 -0
  42. package/dist/internal/direct-mcp-v2/json.js +220 -0
  43. package/dist/internal/direct-mcp-v2/materialize.d.ts +39 -0
  44. package/dist/internal/direct-mcp-v2/materialize.js +22 -0
  45. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.d.ts +14 -0
  46. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.js +71 -0
  47. package/dist/internal/direct-mcp-v2/opencode-profile.d.ts +8 -0
  48. package/dist/internal/direct-mcp-v2/opencode-profile.js +52 -0
  49. package/dist/internal/direct-mcp-v2/projection.d.ts +40 -0
  50. package/dist/internal/direct-mcp-v2/projection.js +46 -0
  51. package/dist/internal/direct-mcp-v2/public-scenario-runtime.d.ts +51 -0
  52. package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +353 -0
  53. package/dist/internal/direct-mcp-v2/runtime-lock.d.ts +51 -0
  54. package/dist/internal/direct-mcp-v2/runtime-lock.js +203 -0
  55. package/dist/internal/direct-mcp-v2/runtime-profile.d.ts +28 -0
  56. package/dist/internal/direct-mcp-v2/runtime-profile.js +163 -0
  57. package/dist/internal/direct-mcp-v2/scenario-http-host.d.ts +24 -0
  58. package/dist/internal/direct-mcp-v2/scenario-http-host.js +254 -0
  59. package/dist/internal/direct-mcp-v2/types.d.ts +233 -0
  60. package/dist/internal/direct-mcp-v2/types.js +1 -0
  61. package/dist/sdk/agent.js +49 -2
  62. package/dist/sdk/evaluate.js +6 -3
  63. package/dist/sdk/index.d.ts +4 -0
  64. package/dist/sdk/index.js +2 -0
  65. package/dist/sdk/managed-session.d.ts +6 -0
  66. package/dist/sdk/managed-session.js +157 -50
  67. package/dist/sdk/scenario-evidence.d.ts +27 -0
  68. package/dist/sdk/scenario-evidence.js +94 -0
  69. package/dist/sdk/scenario-machine-v2.d.ts +9 -0
  70. package/dist/sdk/scenario-machine-v2.js +37 -0
  71. package/dist/sdk/snapshots.d.ts +3 -0
  72. package/dist/sdk/snapshots.js +2 -0
  73. package/dist/sdk/types.d.ts +9 -1
  74. package/dist/types.d.ts +6 -0
  75. package/package.json +2 -2
@@ -0,0 +1,81 @@
1
+ import { access, realpath } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { delimiter, join } from 'node:path';
4
+ export const CODEX_DIRECT_MCP_NORMALIZED_HANDSHAKE = {
5
+ normalizerVersion: 1,
6
+ provider: 'codex',
7
+ controlPlane: 'app-server',
8
+ streamableHttp: true,
9
+ controlPlaneHeaders: true,
10
+ cancellation: 'turn-interrupt',
11
+ protocolMethods: ['mcpServer/tool/call', 'thread/start', 'turn/interrupt'],
12
+ };
13
+ /** Reviewed macOS arm64 pin captured by RFC 0006. Other platforms fail closed. */
14
+ export const CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE = {
15
+ format: 'pathgrade-direct-mcp-runtime-profile-v1',
16
+ provider: 'codex',
17
+ platform: 'darwin',
18
+ architecture: 'arm64',
19
+ controlPlane: 'app-server',
20
+ command: {
21
+ absolutePath: '/Applications/ChatGPT.app/Contents/Resources/codex',
22
+ args: [],
23
+ versionArgs: ['--version'],
24
+ },
25
+ expectedVersion: 'codex-cli 0.151.0-alpha.7.1',
26
+ artifacts: [{
27
+ label: 'codex-cli-0.151.0-alpha.7.1',
28
+ absolutePath: '/Applications/ChatGPT.app/Contents/Resources/codex',
29
+ sha256: 'a5976fd714dc0801a6f40e0e2b3051f64f1e0468f6c0f279b7d2a7afb7623f43',
30
+ }],
31
+ requiredCapabilities: {
32
+ streamableHttp: true,
33
+ controlPlaneHeaders: true,
34
+ cancellation: 'turn-interrupt',
35
+ },
36
+ profileFingerprint: '79ed563e36c65404b385e2a8ebd1c02e33e4c6f86dac589a94b32d4a0cf7f169',
37
+ runtimeCapabilityFingerprint: '2ea3ca12b39fb6de97464ab539be3986d5fe2f9fda51b718198128a55ba11f27',
38
+ };
39
+ /** Code-owned relocatable identity lock for the admitted public Codex slice. */
40
+ export const CODEX_DIRECT_MCP_DARWIN_ARM64_LOCK = {
41
+ format: 'pathgrade-direct-mcp-runtime-lock-v1',
42
+ provider: 'codex',
43
+ platform: 'darwin',
44
+ architecture: 'arm64',
45
+ controlPlane: 'app-server',
46
+ version: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.expectedVersion,
47
+ commandArtifact: 'codex-cli-0.151.0-alpha.7.1',
48
+ args: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.command.args,
49
+ versionArgs: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.command.versionArgs,
50
+ artifacts: [{
51
+ id: 'codex-cli-0.151.0-alpha.7.1',
52
+ relativePath: 'codex',
53
+ sha256: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.artifacts[0].sha256,
54
+ }],
55
+ capabilities: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.requiredCapabilities,
56
+ normalizedHandshakeSha256: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.runtimeCapabilityFingerprint,
57
+ };
58
+ /**
59
+ * Resolve one Codex candidate before admission. Selection is deterministic;
60
+ * identity failure never falls through to another process.
61
+ */
62
+ export async function resolveCodexScenarioExecutable(environment) {
63
+ const bundled = CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.command.absolutePath;
64
+ if (await isExecutable(bundled))
65
+ return realpath(bundled);
66
+ for (const directory of (environment.PATH ?? '').split(delimiter).filter(Boolean)) {
67
+ const candidate = join(directory, 'codex');
68
+ if (await isExecutable(candidate))
69
+ return realpath(candidate);
70
+ }
71
+ throw new Error('RUNTIME_NOT_ADMITTED: no Codex executable was discovered');
72
+ }
73
+ async function isExecutable(path) {
74
+ try {
75
+ await access(path, constants.X_OK);
76
+ return true;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
@@ -0,0 +1,12 @@
1
+ import type { JsonObject, MatchExpr, ScenarioArtifact, ScenarioDiagnostic } from './types.js';
2
+ export type CompileScenarioResult = {
3
+ ok: true;
4
+ artifact: ScenarioArtifact;
5
+ } | {
6
+ ok: false;
7
+ diagnostics: ScenarioDiagnostic[];
8
+ };
9
+ export declare function matchExpression(args: JsonObject, expression: MatchExpr): boolean;
10
+ export declare function compileScenario(source: unknown): CompileScenarioResult;
11
+ export declare function compileScenarioText(text: string): CompileScenarioResult;
12
+ export declare function isSha256(value: unknown): value is string;
@@ -0,0 +1,449 @@
1
+ /* oxlint-disable anti-slop/no-unknown-parameters, anti-slop/no-unsafe-dictionary-type, anti-slop/no-runtime-typeof -- This module is the strict I/O-boundary parser for untrusted scenario JSON. */
2
+ import { Ajv2020 } from 'ajv/dist/2020.js';
3
+ import { canonicalJson, MISSING, parsePointer, parseStrictJson, resolvePointer, sha256Canonical } from './json.js';
4
+ const ID = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
5
+ const SHA256 = /^[a-f0-9]{64}$/;
6
+ const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false });
7
+ function object(value) {
8
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
9
+ }
10
+ function diag(list, code, path, message) {
11
+ list.push({ code, path, message });
12
+ }
13
+ function exactKeys(value, allowed, path, diagnostics) {
14
+ for (const key of Object.keys(value)) {
15
+ if (!allowed.includes(key))
16
+ diag(diagnostics, 'UNKNOWN_FIELD', `${path}/${key}`, `unknown field ${key}`);
17
+ }
18
+ }
19
+ function validId(value, path, diagnostics) {
20
+ if (typeof value !== 'string' || !ID.test(value)) {
21
+ diag(diagnostics, 'INVALID_ID', path, 'expected an ID matching ^[A-Za-z][A-Za-z0-9_-]{0,63}$');
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+ function inspectSchema(schema, path, diagnostics) {
27
+ if (schema.type !== 'object')
28
+ diag(diagnostics, 'INVALID_SCHEMA_ROOT', path, 'inputSchema root type must be object');
29
+ const walk = (value, current) => {
30
+ if (Array.isArray(value))
31
+ return value.forEach((entry, index) => walk(entry, `${current}/${index}`));
32
+ if (!object(value))
33
+ return;
34
+ for (const [key, entry] of Object.entries(value)) {
35
+ const entryPath = `${current}/${key}`;
36
+ if (key === '$dynamicRef' || key === '$dynamicAnchor' || key === '$vocabulary' || key === 'format') {
37
+ diag(diagnostics, 'UNSUPPORTED_SCHEMA_KEYWORD', entryPath, `${key} is not admitted in v2`);
38
+ }
39
+ if (key === '$ref' && (typeof entry !== 'string' || !entry.startsWith('#/'))) {
40
+ diag(diagnostics, 'REMOTE_SCHEMA_REFERENCE', entryPath, 'only local $defs references are admitted');
41
+ }
42
+ walk(entry, entryPath);
43
+ }
44
+ };
45
+ walk(schema, path);
46
+ try {
47
+ if (!ajv.validateSchema(schema)) {
48
+ for (const error of ajv.errors ?? []) {
49
+ diag(diagnostics, 'INVALID_JSON_SCHEMA', `${path}${error.instancePath}`, `${error.keyword}: ${error.message ?? 'invalid schema'}`);
50
+ }
51
+ }
52
+ else {
53
+ ajv.compile(schema);
54
+ }
55
+ }
56
+ catch (error) {
57
+ diag(diagnostics, 'INVALID_JSON_SCHEMA', path, error instanceof Error ? error.message : String(error));
58
+ }
59
+ }
60
+ function validateOutcome(value, path, diagnostics) {
61
+ if (!object(value) || (value.kind !== 'success' && value.kind !== 'tool-error')) {
62
+ diag(diagnostics, 'INVALID_OUTCOME', path, 'expected success or tool-error outcome');
63
+ return false;
64
+ }
65
+ if (value.kind === 'success') {
66
+ exactKeys(value, ['kind', 'content', 'structuredContent'], path, diagnostics);
67
+ if (!Array.isArray(value.content) || value.content.some((entry) => !object(entry) || typeof entry.type !== 'string')) {
68
+ diag(diagnostics, 'INVALID_OUTCOME', `${path}/content`, 'success content must be MCP content objects');
69
+ return false;
70
+ }
71
+ if (value.structuredContent !== undefined && !object(value.structuredContent)) {
72
+ diag(diagnostics, 'INVALID_OUTCOME', `${path}/structuredContent`, 'structuredContent must be an object');
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+ exactKeys(value, ['kind', 'code', 'message', 'data'], path, diagnostics);
78
+ if (typeof value.code !== 'string' || typeof value.message !== 'string' || (value.data !== undefined && !object(value.data))) {
79
+ diag(diagnostics, 'INVALID_OUTCOME', path, 'tool-error requires string code/message and optional object data');
80
+ return false;
81
+ }
82
+ return true;
83
+ }
84
+ function validateMatch(value, path, diagnostics) {
85
+ if (!object(value)) {
86
+ diag(diagnostics, 'INVALID_MATCH', path, 'match expression must be an object');
87
+ return false;
88
+ }
89
+ exactKeys(value, ['exactArgs', 'all'], path, diagnostics);
90
+ if ('exactArgs' in value) {
91
+ if ('all' in value || !object(value.exactArgs)) {
92
+ diag(diagnostics, 'INVALID_MATCH', path, 'exactArgs must be the only match operator and must be an object');
93
+ return false;
94
+ }
95
+ return true;
96
+ }
97
+ if (!Array.isArray(value.all) || value.all.length === 0) {
98
+ diag(diagnostics, 'INVALID_MATCH', `${path}/all`, 'all must be a non-empty array');
99
+ return false;
100
+ }
101
+ const pointers = new Set();
102
+ let valid = true;
103
+ for (const [index, raw] of value.all.entries()) {
104
+ const predicatePath = `${path}/all/${index}`;
105
+ if (!object(raw) || typeof raw.pointer !== 'string') {
106
+ diag(diagnostics, 'INVALID_MATCH_PREDICATE', predicatePath, 'predicate requires a JSON Pointer');
107
+ valid = false;
108
+ continue;
109
+ }
110
+ try {
111
+ parsePointer(raw.pointer);
112
+ }
113
+ catch (error) {
114
+ diag(diagnostics, 'INVALID_JSON_POINTER', `${predicatePath}/pointer`, error instanceof Error ? error.message : String(error));
115
+ valid = false;
116
+ }
117
+ if (pointers.has(raw.pointer)) {
118
+ diag(diagnostics, 'DUPLICATE_MATCH_POINTER', `${predicatePath}/pointer`, 'all entries require unique pointers');
119
+ valid = false;
120
+ }
121
+ pointers.add(raw.pointer);
122
+ const operators = ['equals', 'present', 'absent'].filter((key) => key in raw);
123
+ if (operators.length !== 1 || (raw.present !== undefined && raw.present !== true) || (raw.absent !== undefined && raw.absent !== true)) {
124
+ diag(diagnostics, 'INVALID_MATCH_PREDICATE', predicatePath, 'predicate requires exactly one of equals, present:true, absent:true');
125
+ valid = false;
126
+ }
127
+ exactKeys(raw, ['pointer', ...operators], predicatePath, diagnostics);
128
+ }
129
+ if (valid) {
130
+ // SAFETY: every entry was validated above as an object with a valid pointer and exactly one admitted operator.
131
+ const predicates = value.all;
132
+ for (let left = 0; left < predicates.length; left += 1)
133
+ for (let right = left + 1; right < predicates.length; right += 1) {
134
+ if (predicatesContradict(predicates[left], predicates[right])) {
135
+ diag(diagnostics, 'CONTRADICTORY_MATCH', path, `predicates at indexes ${left} and ${right} contradict`);
136
+ valid = false;
137
+ }
138
+ }
139
+ }
140
+ return valid;
141
+ }
142
+ function predicateMatches(args, predicate) {
143
+ const actual = resolvePointer(args, predicate.pointer);
144
+ if ('present' in predicate)
145
+ return actual !== MISSING;
146
+ if ('absent' in predicate)
147
+ return actual === MISSING;
148
+ return actual !== MISSING && canonicalJson(actual) === canonicalJson(predicate.equals);
149
+ }
150
+ export function matchExpression(args, expression) {
151
+ if ('exactArgs' in expression)
152
+ return canonicalJson(args) === canonicalJson(expression.exactArgs);
153
+ return expression.all.every((predicate) => predicateMatches(args, predicate));
154
+ }
155
+ function pointerAncestor(left, right) {
156
+ return right.startsWith(`${left}/`);
157
+ }
158
+ function predicatesContradict(left, right) {
159
+ if (left.pointer === right.pointer) {
160
+ if (('absent' in left && !('absent' in right)) || ('absent' in right && !('absent' in left)))
161
+ return true;
162
+ return 'equals' in left && 'equals' in right && canonicalJson(left.equals) !== canonicalJson(right.equals);
163
+ }
164
+ const ancestor = pointerAncestor(left.pointer, right.pointer) ? left : pointerAncestor(right.pointer, left.pointer) ? right : undefined;
165
+ const descendant = ancestor === left ? right : ancestor === right ? left : undefined;
166
+ if (!ancestor || !descendant)
167
+ return false;
168
+ if ('absent' in ancestor)
169
+ return !('absent' in descendant);
170
+ if (!('equals' in ancestor))
171
+ return false;
172
+ const suffix = descendant.pointer.slice(ancestor.pointer.length);
173
+ const actual = resolvePointer(ancestor.equals, suffix);
174
+ if ('present' in descendant)
175
+ return actual === MISSING;
176
+ if ('absent' in descendant)
177
+ return actual !== MISSING;
178
+ return actual === MISSING || canonicalJson(actual) !== canonicalJson(descendant.equals);
179
+ }
180
+ function allPairDisjoint(left, right) {
181
+ for (const a of left)
182
+ for (const b of right) {
183
+ if (predicatesContradict(a, b))
184
+ return true;
185
+ }
186
+ return 'unknown';
187
+ }
188
+ function pairDisjoint(left, right) {
189
+ if ('exactArgs' in left)
190
+ return matchExpression(left.exactArgs, right) ? 'unknown' : true;
191
+ if ('exactArgs' in right)
192
+ return matchExpression(right.exactArgs, left) ? 'unknown' : true;
193
+ return allPairDisjoint(left.all, right.all);
194
+ }
195
+ function defaultRejection(qualifiedToolId) {
196
+ return { id: `NoMatch_${qualifiedToolId.replace('/', '_')}`, code: 'SCENARIO_NO_MATCH', message: `No scenario case matched ${qualifiedToolId}` };
197
+ }
198
+ function sortRecord(value) {
199
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, value[key]]));
200
+ }
201
+ function applyRules(rules, diagnostics) {
202
+ for (const rule of rules.filter((entry) => entry.invalid))
203
+ diag(diagnostics, rule.code, rule.path, rule.message);
204
+ }
205
+ function validateSource(source, diagnostics) {
206
+ exactKeys(source, ['version', 'scenarioId', 'initialState', 'servers', 'states', 'concurrency'], '$', diagnostics);
207
+ const servers = object(source.servers) ? source.servers : {};
208
+ const states = object(source.states) ? source.states : {};
209
+ applyRules([
210
+ { invalid: source.version !== 2, code: 'INVALID_VERSION', path: '$/version', message: 'version must be 2' },
211
+ { invalid: source.concurrency !== 'poison-effectful-overlap', code: 'INVALID_CONCURRENCY', path: '$/concurrency', message: 'unsupported concurrency policy' },
212
+ { invalid: !object(source.servers) || Object.keys(servers).length === 0, code: 'INVALID_SERVERS', path: '$/servers', message: 'servers must be a non-empty object' },
213
+ { invalid: !object(source.states) || Object.keys(states).length === 0, code: 'INVALID_STATES', path: '$/states', message: 'states must be a non-empty object' },
214
+ ], diagnostics);
215
+ validId(source.scenarioId, '$/scenarioId', diagnostics);
216
+ validId(source.initialState, '$/initialState', diagnostics);
217
+ return { servers, states };
218
+ }
219
+ function validateServers(servers, diagnostics) {
220
+ for (const [serverId, rawServer] of Object.entries(servers)) {
221
+ const serverPath = `$/servers/${serverId}`;
222
+ validId(serverId, `$/servers/${serverId}`, diagnostics);
223
+ if (!object(rawServer)) {
224
+ diag(diagnostics, 'INVALID_SERVER', serverPath, 'server must be an object');
225
+ continue;
226
+ }
227
+ exactKeys(rawServer, ['instructions', 'tools'], serverPath, diagnostics);
228
+ applyRules([{ invalid: rawServer.instructions !== undefined && typeof rawServer.instructions !== 'string', code: 'INVALID_SERVER', path: `${serverPath}/instructions`, message: 'instructions must be a string' }], diagnostics);
229
+ const tools = object(rawServer.tools) ? rawServer.tools : {};
230
+ if (!object(rawServer.tools) || Object.keys(tools).length === 0) {
231
+ diag(diagnostics, 'INVALID_TOOLS', `${serverPath}/tools`, 'tools must be a non-empty object');
232
+ continue;
233
+ }
234
+ validateTools(tools, serverPath, diagnostics);
235
+ }
236
+ }
237
+ function validateTools(tools, serverPath, diagnostics) {
238
+ for (const [toolId, rawTool] of Object.entries(tools)) {
239
+ const toolPath = `${serverPath}/tools/${toolId}`;
240
+ validId(toolId, toolPath, diagnostics);
241
+ if (!object(rawTool)) {
242
+ diag(diagnostics, 'INVALID_TOOL', toolPath, 'tool must be an object');
243
+ continue;
244
+ }
245
+ exactKeys(rawTool, ['description', 'inputSchema'], toolPath, diagnostics);
246
+ applyRules([{ invalid: typeof rawTool.description !== 'string', code: 'INVALID_TOOL', path: `${toolPath}/description`, message: 'description must be a string' }], diagnostics);
247
+ if (!object(rawTool.inputSchema))
248
+ diag(diagnostics, 'INVALID_JSON_SCHEMA', `${toolPath}/inputSchema`, 'inputSchema must be an object');
249
+ else {
250
+ // SAFETY: object() established the JSON object representation; canonicalJson already rejected non-JSON values.
251
+ inspectSchema(rawTool.inputSchema, `${toolPath}/inputSchema`, diagnostics);
252
+ }
253
+ }
254
+ }
255
+ function validateStates(parts, initialState, diagnostics) {
256
+ const transitions = new Map(Object.keys(parts.states).map((stateId) => [stateId, new Set()]));
257
+ const transitionTools = new Set();
258
+ const context = { ...parts, transitions, transitionTools, diagnostics };
259
+ for (const [stateId, rawState] of Object.entries(parts.states))
260
+ validateState(stateId, rawState, context);
261
+ return { ...parts, transitionTools, reachable: collectReachable(parts.states, transitions, initialState, diagnostics) };
262
+ }
263
+ function validateState(stateId, rawState, context) {
264
+ const statePath = `$/states/${stateId}`;
265
+ validId(stateId, statePath, context.diagnostics);
266
+ if (!object(rawState)) {
267
+ diag(context.diagnostics, 'INVALID_STATE', statePath, 'state must be an object');
268
+ return;
269
+ }
270
+ exactKeys(rawState, ['tools'], statePath, context.diagnostics);
271
+ if (!object(rawState.tools)) {
272
+ diag(context.diagnostics, 'INVALID_STATE_TOOLS', `${statePath}/tools`, 'tools must be an object');
273
+ return;
274
+ }
275
+ for (const [qualifiedToolId, rawDecision] of Object.entries(rawState.tools)) {
276
+ validateDecisionTable(stateId, qualifiedToolId, rawDecision, context);
277
+ }
278
+ }
279
+ function validateDecisionTable(stateId, qualifiedToolId, rawDecision, context) {
280
+ const path = `$/states/${stateId}/tools/${qualifiedToolId}`;
281
+ validateQualifiedTool(qualifiedToolId, path, context.servers, context.diagnostics);
282
+ if (!object(rawDecision)) {
283
+ diag(context.diagnostics, 'INVALID_DECISION_TABLE', path, 'decision table must be an object');
284
+ return;
285
+ }
286
+ exactKeys(rawDecision, ['cases', 'otherwise'], path, context.diagnostics);
287
+ if (!Array.isArray(rawDecision.cases)) {
288
+ diag(context.diagnostics, 'INVALID_CASES', `${path}/cases`, 'cases must be an array');
289
+ return;
290
+ }
291
+ const caseIds = new Set();
292
+ const validCases = rawDecision.cases.flatMap((rawCase, index) => {
293
+ const validCase = validateCase(rawCase, `${path}/cases/${index}`, stateId, qualifiedToolId, caseIds, context);
294
+ return validCase ? [validCase] : [];
295
+ });
296
+ validateCaseDisjointness(validCases, path, context.diagnostics);
297
+ validateOtherwise(rawDecision.otherwise, path, context.diagnostics);
298
+ }
299
+ function validateQualifiedTool(qualifiedToolId, path, servers, diagnostics) {
300
+ const segments = qualifiedToolId.split('/');
301
+ if (segments.length !== 2 || !ID.test(segments[0]) || !ID.test(segments[1])) {
302
+ diag(diagnostics, 'INVALID_QUALIFIED_TOOL_ID', path, 'expected <serverId>/<toolId>');
303
+ return;
304
+ }
305
+ const server = servers[segments[0]];
306
+ const tools = object(server) && object(server.tools) ? server.tools : {};
307
+ if (!object(tools[segments[1]]))
308
+ diag(diagnostics, 'UNKNOWN_TOOL_REFERENCE', path, `tool ${qualifiedToolId} is not declared`);
309
+ }
310
+ function validateCase(rawCase, casePath, stateId, qualifiedToolId, caseIds, context) {
311
+ if (!object(rawCase)) {
312
+ diag(context.diagnostics, 'INVALID_CASE', casePath, 'case must be an object');
313
+ return undefined;
314
+ }
315
+ exactKeys(rawCase, ['id', 'when', 'outcome', 'transition'], casePath, context.diagnostics);
316
+ validateCaseId(rawCase.id, casePath, caseIds, context.diagnostics);
317
+ const matchValid = validateMatch(rawCase.when, `${casePath}/when`, context.diagnostics);
318
+ const outcomeValid = validateOutcome(rawCase.outcome, `${casePath}/outcome`, context.diagnostics);
319
+ validateTransition(rawCase, casePath, stateId, qualifiedToolId, context);
320
+ if (!matchValid || !outcomeValid)
321
+ return undefined;
322
+ // SAFETY: ID, match, outcome, transition, and exact keys were validated above.
323
+ // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- Record validation establishes the complete ScenarioCase contract.
324
+ return rawCase;
325
+ }
326
+ function validateCaseId(value, casePath, caseIds, diagnostics) {
327
+ if (!validId(value, `${casePath}/id`, diagnostics))
328
+ return;
329
+ if (caseIds.has(value))
330
+ diag(diagnostics, 'DUPLICATE_CASE_ID', `${casePath}/id`, `duplicate case ID ${value}`);
331
+ caseIds.add(value);
332
+ }
333
+ function validateTransition(rawCase, casePath, stateId, qualifiedToolId, context) {
334
+ if (rawCase.transition === undefined)
335
+ return;
336
+ const transition = rawCase.transition;
337
+ if (!object(transition) || !validId(transition.to, `${casePath}/transition/to`, context.diagnostics)) {
338
+ diag(context.diagnostics, 'INVALID_TRANSITION', `${casePath}/transition`, 'transition requires a valid target state');
339
+ }
340
+ else if (!object(context.states[transition.to])) {
341
+ diag(context.diagnostics, 'UNKNOWN_TRANSITION_TARGET', `${casePath}/transition/to`, `unknown state ${transition.to}`);
342
+ }
343
+ else {
344
+ context.transitions.get(stateId)?.add(transition.to);
345
+ context.transitionTools.add(qualifiedToolId);
346
+ }
347
+ if (object(rawCase.outcome) && rawCase.outcome.kind === 'tool-error')
348
+ diag(context.diagnostics, 'ERROR_TRANSITION', casePath, 'tool-error cases cannot transition');
349
+ }
350
+ function validateCaseDisjointness(validCases, path, diagnostics) {
351
+ for (const [leftIndex, left] of validCases.entries()) {
352
+ for (const right of validCases.slice(leftIndex + 1)) {
353
+ if (pairDisjoint(left.when, right.when) !== true)
354
+ diag(diagnostics, 'CASE_OVERLAP_UNPROVEN', `${path}/cases`, `cases ${left.id} and ${right.id} are overlapping or not provably disjoint`);
355
+ }
356
+ }
357
+ }
358
+ function validateOtherwise(value, path, diagnostics) {
359
+ if (value === undefined)
360
+ return;
361
+ if (!object(value) || !validId(value.id, `${path}/otherwise/id`, diagnostics) || value.code !== 'SCENARIO_NO_MATCH' || typeof value.message !== 'string') {
362
+ diag(diagnostics, 'INVALID_OTHERWISE', `${path}/otherwise`, 'otherwise requires id, SCENARIO_NO_MATCH code, and message');
363
+ return;
364
+ }
365
+ exactKeys(value, ['id', 'code', 'message'], `${path}/otherwise`, diagnostics);
366
+ }
367
+ function collectReachable(states, transitions, initialState, diagnostics) {
368
+ const reachable = new Set();
369
+ const queue = initialState && object(states[initialState]) ? [initialState] : [];
370
+ while (queue.length > 0) {
371
+ const state = queue.shift();
372
+ if (reachable.has(state))
373
+ continue;
374
+ reachable.add(state);
375
+ queue.push(...(transitions.get(state) ?? []));
376
+ }
377
+ for (const stateId of Object.keys(states))
378
+ if (!reachable.has(stateId))
379
+ diag(diagnostics, 'UNREACHABLE_STATE', `$/states/${stateId}`, `state ${stateId} is unreachable`);
380
+ return reachable;
381
+ }
382
+ function validateInitialState(source, states, diagnostics) {
383
+ const initialState = typeof source.initialState === 'string' ? source.initialState : undefined;
384
+ if (initialState && !object(states[initialState]))
385
+ diag(diagnostics, 'UNKNOWN_INITIAL_STATE', '$/initialState', 'initialState is not declared');
386
+ return initialState;
387
+ }
388
+ function buildArtifact(source, graph) {
389
+ // SAFETY: buildArtifact runs only when every ScenarioMachineV2 field has passed the validators above.
390
+ // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- The parser intentionally narrows its fully validated input once at this boundary.
391
+ const typed = source;
392
+ const compiledStates = {};
393
+ for (const stateId of Object.keys(typed.states).sort()) {
394
+ const typedState = typed.states[stateId];
395
+ const tools = {};
396
+ for (const qualifiedToolId of Object.keys(typedState.tools).sort()) {
397
+ const decision = typedState.tools[qualifiedToolId];
398
+ const [serverId, toolId] = qualifiedToolId.split('/');
399
+ tools[qualifiedToolId] = {
400
+ qualifiedToolId,
401
+ serverId,
402
+ toolId,
403
+ cases: [...decision.cases].sort((a, b) => a.id.localeCompare(b.id, 'en')),
404
+ otherwise: decision.otherwise ?? defaultRejection(qualifiedToolId),
405
+ effectful: graph.transitionTools.has(qualifiedToolId),
406
+ };
407
+ }
408
+ compiledStates[stateId] = { tools };
409
+ }
410
+ const canonicalIr = {
411
+ version: 2,
412
+ scenarioId: typed.scenarioId,
413
+ initialState: typed.initialState,
414
+ servers: sortRecord(Object.fromEntries(Object.entries(typed.servers).map(([serverId, server]) => [serverId, { ...server, tools: sortRecord(server.tools) }]))),
415
+ states: compiledStates,
416
+ reachableStates: [...graph.reachable].sort(),
417
+ concurrency: typed.concurrency,
418
+ };
419
+ const format = 'pathgrade-scenario-machine-v2';
420
+ return { format, canonicalIr, artifactDigest: sha256Canonical({ format, canonicalIr }) };
421
+ }
422
+ export function compileScenario(source) {
423
+ try {
424
+ canonicalJson(source);
425
+ }
426
+ catch (error) {
427
+ return { ok: false, diagnostics: [{ code: 'INVALID_JSON_VALUE', path: '$', message: error instanceof Error ? error.message : String(error) }] };
428
+ }
429
+ if (!object(source))
430
+ return { ok: false, diagnostics: [{ code: 'INVALID_SOURCE', path: '$', message: 'scenario must be an object' }] };
431
+ const diagnostics = [];
432
+ const parts = validateSource(source, diagnostics);
433
+ validateServers(parts.servers, diagnostics);
434
+ const initialState = validateInitialState(source, parts.states, diagnostics);
435
+ const graph = validateStates(parts, initialState, diagnostics);
436
+ diagnostics.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b), 'en'));
437
+ return diagnostics.length > 0 ? { ok: false, diagnostics } : { ok: true, artifact: buildArtifact(source, graph) };
438
+ }
439
+ export function compileScenarioText(text) {
440
+ try {
441
+ return compileScenario(parseStrictJson(text));
442
+ }
443
+ catch (error) {
444
+ return { ok: false, diagnostics: [{ code: 'INVALID_JSON_TEXT', path: '$', message: error instanceof Error ? error.message : String(error) }] };
445
+ }
446
+ }
447
+ export function isSha256(value) {
448
+ return typeof value === 'string' && SHA256.test(value);
449
+ }
@@ -0,0 +1,14 @@
1
+ import { runAcpDirectMcpTrial, type AcpDirectMcpTrialResult } from './acp-direct-mcp.js';
2
+ import type { ScenarioMachineV2 } from './types.js';
3
+ export declare const CURSOR_DIRECT_MCP_FLAG = "PATHGRADE_INTERNAL_CURSOR_DIRECT_MCP_V2";
4
+ export declare function isCursorDirectMcpSelected(environment?: NodeJS.ProcessEnv): boolean;
5
+ export declare function cursorDirectMcpDisableReason(): string | undefined;
6
+ /** Test-only reset for proving the fail-closed process latch. */
7
+ export declare function __resetCursorDirectMcpDisablementForTesting(): void;
8
+ export declare function mapCursorProviderEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
9
+ export declare function runCursorDirectMcpTrial(options: {
10
+ scenario: ScenarioMachineV2;
11
+ environment?: NodeJS.ProcessEnv;
12
+ artifactDirectory?: string;
13
+ createClient?: Parameters<typeof runAcpDirectMcpTrial>[0]['createClient'];
14
+ }): Promise<AcpDirectMcpTrialResult>;
@@ -0,0 +1,60 @@
1
+ import { join } from 'node:path';
2
+ import { defaultPorts } from '../../providers/credentials.js';
3
+ import { curateAcpProcessEnvironment, runAcpDirectMcpTrial } from './acp-direct-mcp.js';
4
+ import { CURSOR_DIRECT_MCP_DARWIN_ARM64_PROFILE } from './cursor-profile.js';
5
+ export const CURSOR_DIRECT_MCP_FLAG = 'PATHGRADE_INTERNAL_CURSOR_DIRECT_MCP_V2';
6
+ const disabledProfiles = new Map();
7
+ export function isCursorDirectMcpSelected(environment = process.env) {
8
+ return environment[CURSOR_DIRECT_MCP_FLAG] === '1' && !disabledProfiles.has(CURSOR_DIRECT_MCP_DARWIN_ARM64_PROFILE.profileFingerprint);
9
+ }
10
+ export function cursorDirectMcpDisableReason() {
11
+ return disabledProfiles.get(CURSOR_DIRECT_MCP_DARWIN_ARM64_PROFILE.profileFingerprint);
12
+ }
13
+ /** Test-only reset for proving the fail-closed process latch. */
14
+ // oxlint-disable-next-line inhuman/no-empty-wrappers -- This test seam exposes Map.clear without exposing mutable production state.
15
+ export function __resetCursorDirectMcpDisablementForTesting() {
16
+ disabledProfiles.clear();
17
+ }
18
+ export function mapCursorProviderEnvironment(environment) {
19
+ const result = curateAcpProcessEnvironment(environment);
20
+ const appApiKey = environment.APP_CURSOR_API_KEY;
21
+ const appBaseUrl = environment.APP_CURSOR_API_BASE_URL;
22
+ const apiKey = environment.CURSOR_API_KEY ?? appApiKey;
23
+ const baseUrl = environment.CURSOR_API_BASE_URL ?? appBaseUrl;
24
+ if (apiKey)
25
+ result.CURSOR_API_KEY = apiKey;
26
+ if (baseUrl)
27
+ result.CURSOR_API_BASE_URL = baseUrl;
28
+ return result;
29
+ }
30
+ export async function runCursorDirectMcpTrial(options) {
31
+ const environment = options.environment ?? process.env;
32
+ if (!isCursorDirectMcpSelected(environment)) {
33
+ const reason = cursorDirectMcpDisableReason();
34
+ throw new Error(reason ? `Cursor direct MCP disabled for admitted profile: ${reason}` : `${CURSOR_DIRECT_MCP_FLAG} is disabled`);
35
+ }
36
+ try {
37
+ const runtimeEnvironment = mapCursorProviderEnvironment(environment);
38
+ const localHomeLinks = [];
39
+ if (!runtimeEnvironment.CURSOR_API_KEY) {
40
+ const ports = defaultPorts();
41
+ if (ports.platform !== 'darwin' || !await ports.keychainEntryExists('cursor-access-token', 'cursor-user')) {
42
+ throw new Error('RUNTIME_NOT_ADMITTED: Cursor direct MCP requires CURSOR_API_KEY, APP_CURSOR_API_KEY, or a local cursor-agent OAuth login');
43
+ }
44
+ localHomeLinks.push({ relativePath: 'Library/Keychains', hostPath: join(ports.homedir, 'Library/Keychains') });
45
+ }
46
+ return await runAcpDirectMcpTrial({
47
+ provider: 'cursor',
48
+ scenario: options.scenario,
49
+ environment: runtimeEnvironment,
50
+ profile: CURSOR_DIRECT_MCP_DARWIN_ARM64_PROFILE,
51
+ artifactDirectory: options.artifactDirectory,
52
+ createClient: options.createClient,
53
+ localHomeLinks,
54
+ });
55
+ }
56
+ catch (error) {
57
+ disabledProfiles.set(CURSOR_DIRECT_MCP_DARWIN_ARM64_PROFILE.profileFingerprint, (error instanceof Error ? error.message : String(error)).slice(0, 500));
58
+ throw error;
59
+ }
60
+ }
@@ -0,0 +1,38 @@
1
+ import type { AcpNormalizedHandshake } from './acp-client.js';
2
+ import type { RuntimeCapabilityProfile } from './types.js';
3
+ export declare const CURSOR_DIRECT_MCP_NORMALIZED_HANDSHAKE: AcpNormalizedHandshake;
4
+ /** Reviewed macOS arm64 pin captured by RFC 0006. Other platforms fail closed. */
5
+ export declare const CURSOR_DIRECT_MCP_DARWIN_ARM64_PROFILE: RuntimeCapabilityProfile;
6
+ /** Code-owned portable identity for the Cursor ACP author-session slice. */
7
+ export declare const CURSOR_ACP_AUTHOR_DARWIN_ARM64_LOCK: {
8
+ readonly format: "pathgrade-direct-mcp-runtime-lock-v1";
9
+ readonly provider: "cursor";
10
+ readonly platform: "darwin";
11
+ readonly architecture: "arm64";
12
+ readonly controlPlane: "acp-v1";
13
+ readonly version: "2026.06.26-7079533";
14
+ readonly commandArtifact: "cursor-wrapper";
15
+ readonly args: readonly ["acp"];
16
+ readonly versionArgs: readonly ["--version"];
17
+ readonly artifacts: readonly [{
18
+ readonly id: "cursor-wrapper";
19
+ readonly relativePath: "cursor-agent";
20
+ readonly sha256: "b7babf47d8b1eee28ac27a74affa02a559bb38103a6e71fbb1f120805d51fedf";
21
+ }, {
22
+ readonly id: "cursor-index";
23
+ readonly relativePath: "index.js";
24
+ readonly sha256: "80eca3cae34cd9af1ee63cdf962eaad42d2a720a2268866e6467aca1b5ab5de0";
25
+ }, {
26
+ readonly id: "cursor-node";
27
+ readonly relativePath: "node";
28
+ readonly sha256: "336b5b3ebc5deb86df842102b20b6e4761605b7a667823e68dda7761b91a161b";
29
+ }];
30
+ readonly capabilities: {
31
+ readonly streamableHttp: true;
32
+ readonly controlPlaneHeaders: true;
33
+ readonly cancellation: "session-cancel";
34
+ };
35
+ readonly normalizedHandshakeSha256: "75d455ba86260766996f533485c54657764095af306e72a4f4616b759eedf59b";
36
+ };
37
+ /** Resolve ordinary Cursor command discovery before exact lock admission. */
38
+ export declare function resolveCursorExecutable(environment: Readonly<Record<string, string | undefined>>, command?: string): Promise<string>;