@martinloop/mcp 0.2.0 → 0.2.7
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/README.md +118 -182
- package/dist/discovery-metadata.d.ts +21 -0
- package/dist/discovery-metadata.js +152 -0
- package/dist/discovery-support.d.ts +62 -0
- package/dist/discovery-support.js +224 -0
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +3 -0
- package/dist/prompts.d.ts +13 -3
- package/dist/prompts.js +537 -74
- package/dist/resources.d.ts +35 -5
- package/dist/resources.js +788 -71
- package/dist/server-validation.d.ts +2 -3
- package/dist/server-validation.js +375 -119
- package/dist/server.d.ts +76 -7
- package/dist/server.js +1478 -394
- package/dist/tools/doctor.d.ts +2 -0
- package/dist/tools/doctor.js +18 -6
- package/dist/tools/eval.d.ts +24 -0
- package/dist/tools/eval.js +65 -0
- package/dist/tools/get-attempt.d.ts +13 -6
- package/dist/tools/get-attempt.js +14 -5
- package/dist/tools/get-run.d.ts +19 -12
- package/dist/tools/get-run.js +20 -11
- package/dist/tools/get-status.d.ts +19 -0
- package/dist/tools/get-status.js +30 -2
- package/dist/tools/get-verification-results.d.ts +10 -7
- package/dist/tools/get-verification-results.js +11 -6
- package/dist/tools/inspect-loop.d.ts +9 -0
- package/dist/tools/inspect-loop.js +11 -2
- package/dist/tools/list-runs.d.ts +25 -5
- package/dist/tools/list-runs.js +21 -4
- package/dist/tools/logs.d.ts +25 -0
- package/dist/tools/logs.js +49 -0
- package/dist/tools/plan.d.ts +20 -0
- package/dist/tools/plan.js +10 -0
- package/dist/tools/pr-tools.d.ts +31 -0
- package/dist/tools/pr-tools.js +111 -0
- package/dist/tools/preflight.d.ts +10 -0
- package/dist/tools/preflight.js +18 -4
- package/dist/tools/run-controls.d.ts +36 -0
- package/dist/tools/run-controls.js +88 -0
- package/dist/tools/run-dossier.d.ts +51 -4
- package/dist/tools/run-dossier.js +100 -5
- package/dist/tools/run-loop.d.ts +19 -0
- package/dist/tools/run-loop.js +61 -4
- package/dist/tools/run-store.d.ts +57 -3
- package/dist/tools/run-store.js +404 -53
- package/dist/tools/tool-errors.d.ts +37 -0
- package/dist/tools/tool-errors.js +170 -0
- package/dist/tools/tool-response.d.ts +16 -0
- package/dist/tools/tool-response.js +34 -0
- package/dist/tools/tool-support.d.ts +92 -2
- package/dist/tools/tool-support.js +385 -63
- package/dist/tools/triage-runs.d.ts +33 -0
- package/dist/tools/triage-runs.js +138 -0
- package/dist/tools/workflow-governance.d.ts +133 -0
- package/dist/tools/workflow-governance.js +581 -0
- package/dist/vendor/adapters/claude-cli.js +0 -1
- package/dist/vendor/adapters/cli-bridge.d.ts +5 -0
- package/dist/vendor/adapters/cli-bridge.js +16 -9
- package/dist/vendor/adapters/direct-provider.js +0 -1
- package/dist/vendor/adapters/index.d.ts +2 -1
- package/dist/vendor/adapters/index.js +2 -1
- package/dist/vendor/adapters/openai-compatible.d.ts +47 -0
- package/dist/vendor/adapters/openai-compatible.js +242 -0
- package/dist/vendor/adapters/runtime-support.js +0 -1
- package/dist/vendor/adapters/stub-agent-cli.js +0 -1
- package/dist/vendor/adapters/stub-direct-provider.js +0 -1
- package/dist/vendor/adapters/verifier-only.js +0 -1
- package/dist/vendor/contracts/governance.js +0 -1
- package/dist/vendor/contracts/index.d.ts +2 -0
- package/dist/vendor/contracts/index.js +1 -1
- package/dist/vendor/contracts/operator.d.ts +19 -0
- package/dist/vendor/contracts/operator.js +11 -0
- package/dist/vendor/core/compiler.js +0 -1
- package/dist/vendor/core/context-integrity.js +0 -1
- package/dist/vendor/core/grounding.js +0 -1
- package/dist/vendor/core/index.js +1 -2
- package/dist/vendor/core/leash.js +19 -12
- package/dist/vendor/core/persistence/compiler.js +0 -1
- package/dist/vendor/core/persistence/index.js +0 -1
- package/dist/vendor/core/persistence/ledger.js +0 -1
- package/dist/vendor/core/persistence/runs-reader.js +0 -1
- package/dist/vendor/core/persistence/store.js +0 -1
- package/dist/vendor/core/policy.js +0 -1
- package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
- package/dist/vendor/core/red-blue/red-phase.js +135 -0
- package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
- package/dist/vendor/core/red-blue/risk-tiers.js +32 -0
- package/dist/vendor/core/rollback.js +2 -3
- package/dist/workflow-state.d.ts +25 -0
- package/dist/workflow-state.js +102 -0
- package/package.json +12 -7
- package/server.json +2 -2
- package/dist/tools/cockpit-support.d.ts +0 -69
- package/dist/tools/cockpit-support.js +0 -108
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
2
3
|
import { resolveRunsRoot } from "./vendor/core/index.js";
|
|
4
|
+
import { invalidArgumentsError, invalidPathError, invalidSelectorError } from "./tools/tool-errors.js";
|
|
5
|
+
export { sanitizeToolErrorMessage } from "./tools/tool-errors.js";
|
|
3
6
|
export function validateToolInput(name, args) {
|
|
4
7
|
switch (name) {
|
|
5
|
-
case "martin_doctor":
|
|
6
|
-
return validateDoctorInput(args);
|
|
7
|
-
case "martin_preflight":
|
|
8
|
-
return validatePreflightInput(args);
|
|
9
8
|
case "martin_run":
|
|
10
9
|
return validateRunInput(args);
|
|
11
10
|
case "martin_inspect":
|
|
12
11
|
return validateInspectInput(args);
|
|
13
12
|
case "martin_status":
|
|
14
13
|
return validateStatusInput(args);
|
|
14
|
+
case "martin_doctor":
|
|
15
|
+
return validateDoctorInput(args);
|
|
16
|
+
case "martin_plan":
|
|
17
|
+
return validatePlanInput(args);
|
|
18
|
+
case "martin_preflight":
|
|
19
|
+
return validatePreflightInput(args);
|
|
20
|
+
case "martin_logs":
|
|
21
|
+
return validateLogsInput(args);
|
|
22
|
+
case "martin_cancel":
|
|
23
|
+
case "martin_pause":
|
|
24
|
+
case "martin_continue":
|
|
25
|
+
return validateRunControlInput(args);
|
|
15
26
|
case "martin_list_runs":
|
|
16
27
|
return validateListRunsInput(args);
|
|
28
|
+
case "martin_triage_runs":
|
|
29
|
+
return validateTriageRunsInput(args);
|
|
17
30
|
case "martin_get_run":
|
|
18
31
|
return validateGetRunInput(args);
|
|
19
32
|
case "martin_get_attempt":
|
|
@@ -21,97 +34,79 @@ export function validateToolInput(name, args) {
|
|
|
21
34
|
case "martin_get_verification_results":
|
|
22
35
|
return validateGetVerificationResultsInput(args);
|
|
23
36
|
case "martin_run_dossier":
|
|
37
|
+
case "martin_dossier":
|
|
38
|
+
return validateRunDossierInput(args);
|
|
39
|
+
case "martin_eval":
|
|
40
|
+
return validateEvalInput(args);
|
|
41
|
+
case "martin_pr_summary":
|
|
24
42
|
return validateRunDossierInput(args);
|
|
43
|
+
case "martin_create_pr":
|
|
44
|
+
return validateCreatePrInput(args);
|
|
45
|
+
case "martin_review_pr":
|
|
46
|
+
return validateReviewPrInput(args);
|
|
25
47
|
default:
|
|
26
|
-
throw
|
|
48
|
+
throw invalidArgumentsError(`Unknown tool: ${name}`, "Refresh the Martin tool manifest and retry.");
|
|
27
49
|
}
|
|
28
50
|
}
|
|
29
|
-
export function sanitizeToolErrorMessage(error) {
|
|
30
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
-
return /([A-Za-z]:\\|\/|policy\.rego|policy\.wasm|\.pem|\.env)/u.test(message)
|
|
32
|
-
? "Tool execution failed."
|
|
33
|
-
: message;
|
|
34
|
-
}
|
|
35
|
-
function validateDoctorInput(args) {
|
|
36
|
-
const record = requireObject(args);
|
|
37
|
-
assertAllowedKeys(record, ["workingDirectory", "runsDir", "engine"]);
|
|
38
|
-
const engine = optionalEnum(record.engine, "engine", ["claude", "codex"]);
|
|
39
|
-
return {
|
|
40
|
-
...(record.workingDirectory !== undefined
|
|
41
|
-
? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
|
|
42
|
-
: {}),
|
|
43
|
-
...(record.runsDir !== undefined
|
|
44
|
-
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
45
|
-
: {}),
|
|
46
|
-
...(engine ? { engine } : {})
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function validatePreflightInput(args) {
|
|
50
|
-
const record = requireObject(args);
|
|
51
|
-
assertAllowedKeys(record, [
|
|
52
|
-
"objective",
|
|
53
|
-
"workingDirectory",
|
|
54
|
-
"engine",
|
|
55
|
-
"model",
|
|
56
|
-
"maxUsd",
|
|
57
|
-
"maxIterations",
|
|
58
|
-
"maxTokens",
|
|
59
|
-
"verificationPlan",
|
|
60
|
-
"allowedPaths",
|
|
61
|
-
"deniedPaths",
|
|
62
|
-
"workspaceId",
|
|
63
|
-
"projectId"
|
|
64
|
-
]);
|
|
65
|
-
const engine = optionalEnum(record.engine, "engine", ["claude", "codex"]);
|
|
66
|
-
return {
|
|
67
|
-
objective: requireString(record.objective, "objective"),
|
|
68
|
-
...(record.workingDirectory !== undefined
|
|
69
|
-
? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
|
|
70
|
-
: {}),
|
|
71
|
-
...(engine ? { engine } : {}),
|
|
72
|
-
...optionalString(record.model, "model"),
|
|
73
|
-
...optionalPositiveNumber(record.maxUsd, "maxUsd"),
|
|
74
|
-
...optionalPositiveInteger(record.maxIterations, "maxIterations"),
|
|
75
|
-
...optionalPositiveInteger(record.maxTokens, "maxTokens"),
|
|
76
|
-
...optionalStringArrayAsObject(record.verificationPlan, "verificationPlan"),
|
|
77
|
-
...optionalPathPatternArrayAsObject(record.allowedPaths, "allowedPaths"),
|
|
78
|
-
...optionalPathPatternArrayAsObject(record.deniedPaths, "deniedPaths"),
|
|
79
|
-
...optionalString(record.workspaceId, "workspaceId"),
|
|
80
|
-
...optionalString(record.projectId, "projectId")
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
51
|
export function resolveSafeRepoRoot(repoRoot, workspaceRoot = process.env.MARTIN_MCP_WORKSPACE_ROOT ?? process.cwd()) {
|
|
84
52
|
const baseRoot = resolve(workspaceRoot);
|
|
85
53
|
const candidate = repoRoot ? resolve(baseRoot, repoRoot) : baseRoot;
|
|
86
|
-
assertPathWithinRoot(candidate, baseRoot, "workingDirectory"
|
|
54
|
+
assertPathWithinRoot(candidate, baseRoot, "workingDirectory", {
|
|
55
|
+
requireExistingCandidate: true,
|
|
56
|
+
requireExistingRoot: true
|
|
57
|
+
});
|
|
87
58
|
return candidate;
|
|
88
59
|
}
|
|
89
60
|
export function resolveSafeRunsJsonPath(file, runsRoot = resolveRunsRoot(process.env)) {
|
|
90
61
|
const baseRoot = resolve(runsRoot);
|
|
91
62
|
const candidate = resolve(baseRoot, file);
|
|
92
|
-
assertPathWithinRoot(candidate, baseRoot, "file"
|
|
63
|
+
assertPathWithinRoot(candidate, baseRoot, "file", {
|
|
64
|
+
requireExistingCandidate: true,
|
|
65
|
+
requireExistingRoot: true
|
|
66
|
+
});
|
|
93
67
|
const extension = extname(candidate).toLowerCase();
|
|
94
68
|
if (extension !== ".json" && extension !== ".jsonl") {
|
|
95
|
-
throw
|
|
69
|
+
throw invalidPathError("Invalid file.", "Point file at a loop-record.json, a legacy .jsonl file, or a run directory under the runs root.");
|
|
96
70
|
}
|
|
97
71
|
return candidate;
|
|
98
72
|
}
|
|
99
73
|
export function resolveSafeRunsPath(file, runsRoot = resolveRunsRoot(process.env)) {
|
|
100
74
|
const baseRoot = resolve(runsRoot);
|
|
101
75
|
const candidate = resolve(baseRoot, file);
|
|
102
|
-
assertPathWithinRoot(candidate, baseRoot, "file"
|
|
76
|
+
assertPathWithinRoot(candidate, baseRoot, "file", {
|
|
77
|
+
requireExistingCandidate: true,
|
|
78
|
+
requireExistingRoot: true
|
|
79
|
+
});
|
|
103
80
|
const extension = extname(candidate).toLowerCase();
|
|
104
81
|
if (extension && extension !== ".json" && extension !== ".jsonl") {
|
|
105
|
-
throw
|
|
82
|
+
throw invalidPathError("Invalid file.", "Point file at a loop-record.json, a legacy .jsonl file, or a run directory under the runs root.");
|
|
106
83
|
}
|
|
107
84
|
return candidate;
|
|
108
85
|
}
|
|
109
86
|
export function resolveSafeRunsRootPath(runsRoot, fallbackRunsRoot = resolveRunsRoot(process.env)) {
|
|
110
87
|
const baseRoot = resolve(fallbackRunsRoot);
|
|
111
88
|
const candidate = runsRoot ? resolve(baseRoot, runsRoot) : baseRoot;
|
|
112
|
-
|
|
89
|
+
if (runsRoot && !existsSync(candidate)) {
|
|
90
|
+
if (!isAbsolute(runsRoot)) {
|
|
91
|
+
assertRawPathWithinRoot(candidate, baseRoot, "runsDir");
|
|
92
|
+
}
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
assertPathWithinRoot(candidate, baseRoot, "runsDir", {
|
|
96
|
+
requireExistingCandidate: false,
|
|
97
|
+
requireExistingRoot: false
|
|
98
|
+
});
|
|
113
99
|
return candidate;
|
|
114
100
|
}
|
|
101
|
+
function assertRawPathWithinRoot(candidatePath, rootPath, name) {
|
|
102
|
+
const relativePath = relative(resolve(rootPath), resolve(candidatePath));
|
|
103
|
+
if (relativePath === "" || relativePath === ".") {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
107
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
115
110
|
export function resolveSafeLoopRecordPath(loopId, runsRoot = resolveRunsRoot(process.env)) {
|
|
116
111
|
const normalizedLoopId = requireLoopId(loopId, "loopId");
|
|
117
112
|
return resolveSafeRunsJsonPath(`${normalizedLoopId}/loop-record.json`, runsRoot);
|
|
@@ -127,7 +122,7 @@ export function normalizeSafePathPatterns(value, name) {
|
|
|
127
122
|
normalized.startsWith("/") ||
|
|
128
123
|
/^[A-Za-z]:\//u.test(normalized) ||
|
|
129
124
|
normalized.split("/").includes("..")) {
|
|
130
|
-
throw
|
|
125
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
131
126
|
}
|
|
132
127
|
return normalized;
|
|
133
128
|
});
|
|
@@ -139,9 +134,14 @@ function validateRunInput(args) {
|
|
|
139
134
|
"workingDirectory",
|
|
140
135
|
"engine",
|
|
141
136
|
"model",
|
|
137
|
+
"context",
|
|
138
|
+
"policyPack",
|
|
142
139
|
"maxUsd",
|
|
143
140
|
"maxIterations",
|
|
144
141
|
"maxTokens",
|
|
142
|
+
"maxMinutes",
|
|
143
|
+
"maxFilesChanged",
|
|
144
|
+
"maxCommands",
|
|
145
145
|
"verificationPlan",
|
|
146
146
|
"allowedPaths",
|
|
147
147
|
"deniedPaths",
|
|
@@ -156,9 +156,20 @@ function validateRunInput(args) {
|
|
|
156
156
|
: {}),
|
|
157
157
|
...(engine ? { engine } : {}),
|
|
158
158
|
...optionalString(record.model, "model"),
|
|
159
|
+
...optionalString(record.context, "context"),
|
|
160
|
+
...optionalEnumAsObject(record.policyPack, "policyPack", [
|
|
161
|
+
"solo-founder",
|
|
162
|
+
"startup-team",
|
|
163
|
+
"enterprise-strict",
|
|
164
|
+
"oss-maintainer",
|
|
165
|
+
"security-sensitive"
|
|
166
|
+
]),
|
|
159
167
|
...optionalPositiveNumber(record.maxUsd, "maxUsd"),
|
|
160
168
|
...optionalPositiveInteger(record.maxIterations, "maxIterations"),
|
|
161
169
|
...optionalPositiveInteger(record.maxTokens, "maxTokens"),
|
|
170
|
+
...optionalPositiveInteger(record.maxMinutes, "maxMinutes"),
|
|
171
|
+
...optionalPositiveInteger(record.maxFilesChanged, "maxFilesChanged"),
|
|
172
|
+
...optionalPositiveInteger(record.maxCommands, "maxCommands"),
|
|
162
173
|
...optionalStringArrayAsObject(record.verificationPlan, "verificationPlan"),
|
|
163
174
|
...optionalPathPatternArrayAsObject(record.allowedPaths, "allowedPaths"),
|
|
164
175
|
...optionalPathPatternArrayAsObject(record.deniedPaths, "deniedPaths"),
|
|
@@ -169,18 +180,24 @@ function validateRunInput(args) {
|
|
|
169
180
|
function validateInspectInput(args) {
|
|
170
181
|
const record = requireObject(args);
|
|
171
182
|
assertAllowedKeys(record, ["file", "runsDir"]);
|
|
183
|
+
const resolvedRunsDir = record.runsDir !== undefined
|
|
184
|
+
? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
|
|
185
|
+
: undefined;
|
|
172
186
|
return {
|
|
173
187
|
...(record.file !== undefined
|
|
174
|
-
? {
|
|
188
|
+
? {
|
|
189
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
190
|
+
}
|
|
175
191
|
: {}),
|
|
176
|
-
...(
|
|
177
|
-
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
178
|
-
: {})
|
|
192
|
+
...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {})
|
|
179
193
|
};
|
|
180
194
|
}
|
|
181
195
|
function validateStatusInput(args) {
|
|
182
196
|
const record = requireObject(args);
|
|
183
197
|
assertAllowedKeys(record, ["loopJson", "file", "loopId", "runsDir", "latest"]);
|
|
198
|
+
const resolvedRunsDir = record.runsDir !== undefined
|
|
199
|
+
? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
|
|
200
|
+
: undefined;
|
|
184
201
|
const selectors = [
|
|
185
202
|
record.loopJson !== undefined ? "loopJson" : null,
|
|
186
203
|
record.file !== undefined ? "file" : null,
|
|
@@ -188,113 +205,334 @@ function validateStatusInput(args) {
|
|
|
188
205
|
record.latest !== undefined ? "latest" : null
|
|
189
206
|
].filter((value) => value !== null);
|
|
190
207
|
if (selectors.length !== 1) {
|
|
191
|
-
throw
|
|
208
|
+
throw invalidSelectorError("Provide exactly one of loopJson, file, loopId, or latest.", "Choose exactly one status selector per call.");
|
|
192
209
|
}
|
|
193
210
|
if (record.latest !== undefined && record.latest !== true) {
|
|
194
|
-
throw
|
|
211
|
+
throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
|
|
195
212
|
}
|
|
196
213
|
return {
|
|
197
214
|
...(record.loopJson !== undefined
|
|
198
215
|
? { loopJson: requireString(record.loopJson, "loopJson") }
|
|
199
216
|
: {}),
|
|
200
217
|
...(record.file !== undefined
|
|
201
|
-
? {
|
|
218
|
+
? {
|
|
219
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
220
|
+
}
|
|
202
221
|
: {}),
|
|
203
222
|
...(record.loopId !== undefined
|
|
204
|
-
? {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
223
|
+
? {
|
|
224
|
+
loopId: requireLoopId(record.loopId, "loopId")
|
|
225
|
+
}
|
|
208
226
|
: {}),
|
|
227
|
+
...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
|
|
209
228
|
...(record.latest === true ? { latest: true } : {})
|
|
210
229
|
};
|
|
211
230
|
}
|
|
212
|
-
function
|
|
231
|
+
function validateDoctorInput(args) {
|
|
213
232
|
const record = requireObject(args);
|
|
214
|
-
assertAllowedKeys(record, ["runsDir", "
|
|
233
|
+
assertAllowedKeys(record, ["workingDirectory", "runsDir", "engine"]);
|
|
215
234
|
return {
|
|
235
|
+
...(record.workingDirectory !== undefined
|
|
236
|
+
? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
|
|
237
|
+
: {}),
|
|
216
238
|
...(record.runsDir !== undefined
|
|
217
239
|
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
218
240
|
: {}),
|
|
219
|
-
...(record.
|
|
241
|
+
...optionalEnumAsObject(record.engine, "engine", ["claude", "codex"])
|
|
220
242
|
};
|
|
221
243
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
|
|
225
|
-
return validateRunSelector(record);
|
|
244
|
+
function validatePreflightInput(args) {
|
|
245
|
+
return validateRunInput(args);
|
|
226
246
|
}
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
|
|
230
|
-
return validateRunSelector(record);
|
|
247
|
+
function validatePlanInput(args) {
|
|
248
|
+
return validateRunInput(args);
|
|
231
249
|
}
|
|
232
|
-
function
|
|
250
|
+
function validateLogsInput(args) {
|
|
233
251
|
const record = requireObject(args);
|
|
234
|
-
assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
|
|
235
|
-
|
|
252
|
+
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "limit"]);
|
|
253
|
+
const resolvedRunsDir = record.runsDir !== undefined
|
|
254
|
+
? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
|
|
255
|
+
: undefined;
|
|
256
|
+
const selectors = [
|
|
257
|
+
record.file !== undefined ? "file" : null,
|
|
258
|
+
record.loopId !== undefined ? "loopId" : null,
|
|
259
|
+
record.latest !== undefined ? "latest" : null
|
|
260
|
+
].filter((value) => value !== null);
|
|
261
|
+
if (selectors.length !== 1) {
|
|
262
|
+
throw invalidSelectorError("Provide exactly one of file, loopId, or latest.", "Choose exactly one run selector per call.");
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
...(record.file !== undefined
|
|
266
|
+
? {
|
|
267
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
268
|
+
}
|
|
269
|
+
: {}),
|
|
270
|
+
...(record.loopId !== undefined ? { loopId: requireLoopId(record.loopId, "loopId") } : {}),
|
|
271
|
+
...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
|
|
272
|
+
...(record.latest === true ? { latest: true } : {}),
|
|
273
|
+
...optionalPositiveInteger(record.limit, "limit")
|
|
274
|
+
};
|
|
236
275
|
}
|
|
237
|
-
function
|
|
276
|
+
function validateListRunsInput(args) {
|
|
238
277
|
const record = requireObject(args);
|
|
239
|
-
assertAllowedKeys(record, [
|
|
278
|
+
assertAllowedKeys(record, [
|
|
279
|
+
"runsDir",
|
|
280
|
+
"limit",
|
|
281
|
+
"status",
|
|
282
|
+
"lifecycleState",
|
|
283
|
+
"adapterId",
|
|
284
|
+
"model",
|
|
285
|
+
"updatedAfter"
|
|
286
|
+
]);
|
|
240
287
|
return {
|
|
241
|
-
loopId: requireLoopId(record.loopId, "loopId"),
|
|
242
|
-
attemptIndex: requirePositiveInteger(record.attemptIndex, "attemptIndex"),
|
|
243
288
|
...(record.runsDir !== undefined
|
|
244
289
|
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
245
|
-
: {})
|
|
290
|
+
: {}),
|
|
291
|
+
...optionalPositiveInteger(record.limit, "limit"),
|
|
292
|
+
...optionalString(record.status, "status"),
|
|
293
|
+
...optionalString(record.lifecycleState, "lifecycleState"),
|
|
294
|
+
...optionalString(record.adapterId, "adapterId"),
|
|
295
|
+
...optionalString(record.model, "model"),
|
|
296
|
+
...optionalString(record.updatedAfter, "updatedAfter")
|
|
246
297
|
};
|
|
247
298
|
}
|
|
248
|
-
function
|
|
299
|
+
function validateGetRunInput(args) {
|
|
300
|
+
const record = requireObject(args);
|
|
301
|
+
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest"]);
|
|
302
|
+
const resolvedRunsDir = record.runsDir !== undefined
|
|
303
|
+
? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
|
|
304
|
+
: undefined;
|
|
249
305
|
const selectors = [
|
|
306
|
+
record.file !== undefined ? "file" : null,
|
|
250
307
|
record.loopId !== undefined ? "loopId" : null,
|
|
251
308
|
record.latest !== undefined ? "latest" : null
|
|
252
309
|
].filter((value) => value !== null);
|
|
253
310
|
if (selectors.length !== 1) {
|
|
254
|
-
throw
|
|
311
|
+
throw invalidSelectorError("Provide exactly one of file, loopId, or latest.", "Choose exactly one run selector per call.");
|
|
255
312
|
}
|
|
256
313
|
if (record.latest !== undefined && record.latest !== true) {
|
|
257
|
-
throw
|
|
314
|
+
throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
|
|
258
315
|
}
|
|
259
316
|
return {
|
|
260
|
-
...(record.
|
|
261
|
-
|
|
317
|
+
...(record.file !== undefined
|
|
318
|
+
? {
|
|
319
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
320
|
+
}
|
|
321
|
+
: {}),
|
|
322
|
+
...(record.loopId !== undefined
|
|
323
|
+
? { loopId: requireLoopId(record.loopId, "loopId") }
|
|
324
|
+
: {}),
|
|
325
|
+
...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
|
|
326
|
+
...(record.latest === true ? { latest: true } : {})
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function validateTriageRunsInput(args) {
|
|
330
|
+
const record = requireObject(args);
|
|
331
|
+
assertAllowedKeys(record, [
|
|
332
|
+
"runsDir",
|
|
333
|
+
"limit",
|
|
334
|
+
"status",
|
|
335
|
+
"lifecycleState",
|
|
336
|
+
"adapterId",
|
|
337
|
+
"model",
|
|
338
|
+
"updatedAfter",
|
|
339
|
+
"includeHealthy"
|
|
340
|
+
]);
|
|
341
|
+
return {
|
|
262
342
|
...(record.runsDir !== undefined
|
|
263
343
|
? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
|
|
264
|
-
: {})
|
|
344
|
+
: {}),
|
|
345
|
+
...optionalPositiveInteger(record.limit, "limit"),
|
|
346
|
+
...optionalString(record.status, "status"),
|
|
347
|
+
...optionalString(record.lifecycleState, "lifecycleState"),
|
|
348
|
+
...optionalString(record.adapterId, "adapterId"),
|
|
349
|
+
...optionalString(record.model, "model"),
|
|
350
|
+
...optionalString(record.updatedAfter, "updatedAfter"),
|
|
351
|
+
...optionalBoolean(record.includeHealthy, "includeHealthy")
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function validateGetAttemptInput(args) {
|
|
355
|
+
const record = requireObject(args);
|
|
356
|
+
assertAllowedKeys(record, ["file", "loopId", "runsDir", "attemptIndex"]);
|
|
357
|
+
const resolvedRunsDir = record.runsDir !== undefined
|
|
358
|
+
? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
|
|
359
|
+
: undefined;
|
|
360
|
+
const selectors = [
|
|
361
|
+
record.file !== undefined ? "file" : null,
|
|
362
|
+
record.loopId !== undefined ? "loopId" : null
|
|
363
|
+
].filter((value) => value !== null);
|
|
364
|
+
if (selectors.length !== 1) {
|
|
365
|
+
throw invalidSelectorError("Provide exactly one of file or loopId.", "Choose exactly one run selector per call.");
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
...(record.file !== undefined
|
|
369
|
+
? {
|
|
370
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
371
|
+
}
|
|
372
|
+
: {}),
|
|
373
|
+
...(record.loopId !== undefined
|
|
374
|
+
? { loopId: requireLoopId(record.loopId, "loopId") }
|
|
375
|
+
: {}),
|
|
376
|
+
...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
|
|
377
|
+
...optionalPositiveInteger(record.attemptIndex, "attemptIndex")
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function validateGetVerificationResultsInput(args) {
|
|
381
|
+
const record = requireObject(args);
|
|
382
|
+
assertAllowedKeys(record, ["file", "loopId", "runsDir"]);
|
|
383
|
+
const resolvedRunsDir = record.runsDir !== undefined
|
|
384
|
+
? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
|
|
385
|
+
: undefined;
|
|
386
|
+
const selectors = [
|
|
387
|
+
record.file !== undefined ? "file" : null,
|
|
388
|
+
record.loopId !== undefined ? "loopId" : null
|
|
389
|
+
].filter((value) => value !== null);
|
|
390
|
+
if (selectors.length !== 1) {
|
|
391
|
+
throw invalidSelectorError("Provide exactly one of file or loopId.", "Choose exactly one run selector per call.");
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
...(record.file !== undefined
|
|
395
|
+
? {
|
|
396
|
+
file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
|
|
397
|
+
}
|
|
398
|
+
: {}),
|
|
399
|
+
...(record.loopId !== undefined
|
|
400
|
+
? { loopId: requireLoopId(record.loopId, "loopId") }
|
|
401
|
+
: {}),
|
|
402
|
+
...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {})
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function validateRunDossierInput(args) {
|
|
406
|
+
const record = requireObject(args);
|
|
407
|
+
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format"]);
|
|
408
|
+
const base = validateGetRunInput(args);
|
|
409
|
+
return {
|
|
410
|
+
...base,
|
|
411
|
+
...optionalEnumAsObject(record.format, "format", ["json", "md", "github-pr"])
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function validateEvalInput(args) {
|
|
415
|
+
return validateGetRunInput(args);
|
|
416
|
+
}
|
|
417
|
+
function validateRunControlInput(args) {
|
|
418
|
+
const record = requireObject(args);
|
|
419
|
+
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "reason", "requestedBy"]);
|
|
420
|
+
const base = validateGetRunInput(args);
|
|
421
|
+
return {
|
|
422
|
+
...base,
|
|
423
|
+
...optionalString(record.reason, "reason"),
|
|
424
|
+
...optionalString(record.requestedBy, "requestedBy")
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function validateCreatePrInput(args) {
|
|
428
|
+
const record = requireObject(args);
|
|
429
|
+
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format", "title", "base", "execute"]);
|
|
430
|
+
const base = validateRunDossierInput(args);
|
|
431
|
+
return {
|
|
432
|
+
...base,
|
|
433
|
+
...optionalString(record.title, "title"),
|
|
434
|
+
...optionalString(record.base, "base"),
|
|
435
|
+
...optionalBoolean(record.execute, "execute")
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function validateReviewPrInput(args) {
|
|
439
|
+
const record = requireObject(args);
|
|
440
|
+
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format", "prBody"]);
|
|
441
|
+
const base = validateRunDossierInput(args);
|
|
442
|
+
return {
|
|
443
|
+
...base,
|
|
444
|
+
...optionalString(record.prBody, "prBody")
|
|
265
445
|
};
|
|
266
446
|
}
|
|
267
447
|
function requireObject(value) {
|
|
268
448
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
269
|
-
throw
|
|
449
|
+
throw invalidArgumentsError("Tool arguments must be an object.");
|
|
270
450
|
}
|
|
271
451
|
return value;
|
|
272
452
|
}
|
|
273
453
|
function assertAllowedKeys(record, allowed) {
|
|
274
454
|
const unknownKeys = Object.keys(record).filter((key) => !allowed.includes(key));
|
|
275
455
|
if (unknownKeys.length > 0) {
|
|
276
|
-
throw
|
|
456
|
+
throw invalidArgumentsError(`Unknown arguments: ${unknownKeys.join(", ")}`);
|
|
277
457
|
}
|
|
278
458
|
}
|
|
279
|
-
function assertPathWithinRoot(candidatePath, rootPath, name) {
|
|
280
|
-
|
|
459
|
+
function assertPathWithinRoot(candidatePath, rootPath, name, options = {}) {
|
|
460
|
+
assertNoSymbolicLinkSegments(candidatePath, name, rootPath);
|
|
461
|
+
const canonicalRoot = canonicalizePath(rootPath, name, options.requireExistingRoot ?? false);
|
|
462
|
+
const canonicalCandidate = canonicalizePath(candidatePath, name, options.requireExistingCandidate ?? false);
|
|
463
|
+
const relativePath = relative(canonicalRoot, canonicalCandidate);
|
|
281
464
|
if (relativePath === "" || relativePath === ".") {
|
|
282
465
|
return;
|
|
283
466
|
}
|
|
284
467
|
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
285
|
-
throw
|
|
468
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function assertNoSymbolicLinkSegments(pathValue, name, stopAtPath) {
|
|
472
|
+
const stopAt = stopAtPath ? resolve(stopAtPath) : undefined;
|
|
473
|
+
let current = resolve(pathValue);
|
|
474
|
+
while (true) {
|
|
475
|
+
if (existsSync(current)) {
|
|
476
|
+
try {
|
|
477
|
+
const stats = lstatSync(current);
|
|
478
|
+
if (stats.isSymbolicLink()) {
|
|
479
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
if (error instanceof Error) {
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (stopAt && relative(stopAt, current) === "") {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
const parent = dirname(current);
|
|
493
|
+
if (parent === current) {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
current = parent;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function canonicalizePath(pathValue, name, requireExisting) {
|
|
500
|
+
const resolvedPath = resolve(pathValue);
|
|
501
|
+
if (!existsSync(resolvedPath)) {
|
|
502
|
+
if (requireExisting) {
|
|
503
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
504
|
+
}
|
|
505
|
+
return resolvedPath;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const stats = lstatSync(resolvedPath);
|
|
509
|
+
if (stats.isSymbolicLink()) {
|
|
510
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
if (error instanceof Error) {
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
return realpathSync.native(resolvedPath);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
throw invalidPathError(`Invalid ${name}.`);
|
|
286
524
|
}
|
|
287
525
|
}
|
|
288
526
|
function requireString(value, name) {
|
|
289
527
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
290
|
-
throw
|
|
528
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
291
529
|
}
|
|
292
530
|
return value.trim();
|
|
293
531
|
}
|
|
294
532
|
function requireLoopId(value, name) {
|
|
295
533
|
const loopId = requireString(value, name);
|
|
296
534
|
if (!/^[A-Za-z0-9._-]+$/u.test(loopId)) {
|
|
297
|
-
throw
|
|
535
|
+
throw invalidPathError(`Invalid ${name}.`, "loopId may only include letters, numbers, dots, underscores, and hyphens.");
|
|
298
536
|
}
|
|
299
537
|
return loopId;
|
|
300
538
|
}
|
|
@@ -309,7 +547,7 @@ function optionalPositiveNumber(value, name) {
|
|
|
309
547
|
return {};
|
|
310
548
|
}
|
|
311
549
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
312
|
-
throw
|
|
550
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
313
551
|
}
|
|
314
552
|
return { [name]: value };
|
|
315
553
|
}
|
|
@@ -317,20 +555,35 @@ function optionalPositiveInteger(value, name) {
|
|
|
317
555
|
if (value === undefined) {
|
|
318
556
|
return {};
|
|
319
557
|
}
|
|
320
|
-
return { [name]: requirePositiveInteger(value, name) };
|
|
321
|
-
}
|
|
322
|
-
function requirePositiveInteger(value, name) {
|
|
323
558
|
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
324
|
-
throw
|
|
559
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
325
560
|
}
|
|
326
|
-
return value;
|
|
561
|
+
return { [name]: value };
|
|
562
|
+
}
|
|
563
|
+
function optionalNonNegativeInteger(value, name) {
|
|
564
|
+
if (value === undefined) {
|
|
565
|
+
return {};
|
|
566
|
+
}
|
|
567
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
568
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
569
|
+
}
|
|
570
|
+
return { [name]: value };
|
|
571
|
+
}
|
|
572
|
+
function optionalBoolean(value, name) {
|
|
573
|
+
if (value === undefined) {
|
|
574
|
+
return {};
|
|
575
|
+
}
|
|
576
|
+
if (typeof value !== "boolean") {
|
|
577
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
578
|
+
}
|
|
579
|
+
return { [name]: value };
|
|
327
580
|
}
|
|
328
581
|
function optionalStringArray(value, name) {
|
|
329
582
|
if (value === undefined) {
|
|
330
583
|
return undefined;
|
|
331
584
|
}
|
|
332
585
|
if (!Array.isArray(value)) {
|
|
333
|
-
throw
|
|
586
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
334
587
|
}
|
|
335
588
|
return value.map((item) => requireString(item, name));
|
|
336
589
|
}
|
|
@@ -347,8 +600,11 @@ function optionalEnum(value, name, allowed) {
|
|
|
347
600
|
return undefined;
|
|
348
601
|
}
|
|
349
602
|
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
350
|
-
throw
|
|
603
|
+
throw invalidArgumentsError(`Invalid ${name}.`);
|
|
351
604
|
}
|
|
352
605
|
return value;
|
|
353
606
|
}
|
|
354
|
-
|
|
607
|
+
function optionalEnumAsObject(value, name, allowed) {
|
|
608
|
+
const enumValue = optionalEnum(value, name, allowed);
|
|
609
|
+
return enumValue ? { [name]: enumValue } : {};
|
|
610
|
+
}
|