@haystackeditor/cli 0.15.21 → 0.15.23
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 +39 -2
- package/dist/commands/verify-core.d.ts +26 -1
- package/dist/commands/verify-core.js +59 -23
- package/dist/commands/verify-hosted-mcp.d.ts +12 -0
- package/dist/commands/verify-hosted-mcp.js +234 -0
- package/dist/commands/verify-hosted.d.ts +60 -0
- package/dist/commands/verify-hosted.js +342 -0
- package/dist/commands/verify-mcp.js +59 -0
- package/dist/commands/verify-ops.js +1 -1
- package/dist/commands/verify-reproducibility.d.ts +85 -0
- package/dist/commands/verify-reproducibility.js +494 -0
- package/dist/commands/verify.d.ts +11 -0
- package/dist/commands/verify.js +55 -22
- package/dist/index.js +91 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +1 -0
- package/dist/utils/haystack-api.d.ts +1 -1
- package/dist/utils/haystack-api.js +2 -2
- package/package.json +1 -1
- package/schemas/cloud-verifier.v1.json +96 -0
package/README.md
CHANGED
|
@@ -33,8 +33,8 @@ Most consumers of this CLI are coding agents. These invariants hold everywhere:
|
|
|
33
33
|
- **Every JSON payload is versioned and schema'd.** Payloads carry
|
|
34
34
|
`schema_version`; print the contract with `haystack schema <name>`
|
|
35
35
|
(`haystack schema` lists all: `triage`, `pr`, `pr-status`, `inbox`, `ask`,
|
|
36
|
-
`traces`, `submit`, `action`, `error`, `setup`). Schemas
|
|
37
|
-
2020-12 and CI-guarded against drift.
|
|
36
|
+
`traces`, `submit`, `action`, `cloud-verifier`, `error`, `setup`). Schemas
|
|
37
|
+
are JSON-Schema 2020-12 and CI-guarded against drift.
|
|
38
38
|
- **One state vocabulary.** All state tokens are snake_case across every
|
|
39
39
|
command: verdicts are `good_to_merge` / `needs_review` / `needs_input`;
|
|
40
40
|
feed buckets are `analyzing`, `good_to_merge`, `issues`, `needs_assignment`,
|
|
@@ -79,6 +79,43 @@ haystack verify refork latest 3 attempt:fix-2
|
|
|
79
79
|
haystack verify reopen archil:<disk-id>:branch:<returned-child>
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
**`haystack verify hosted`** is the authenticated production entry point for
|
|
83
|
+
exact GitHub commit pairs. It uses the repository-default saved account,
|
|
84
|
+
submits an idempotent run, and waits up to 35 minutes by default:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
haystack verify hosted start owner/repo \
|
|
88
|
+
--base <40-character-commit-sha> \
|
|
89
|
+
--head <40-character-commit-sha> \
|
|
90
|
+
--json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Use `--no-wait` to return as soon as the workflow is queued. The result includes
|
|
94
|
+
an account-bound `next_command`; use it verbatim on machines with multiple
|
|
95
|
+
saved GitHub accounts. Repeating the same repository/base/head tuple reuses the
|
|
96
|
+
same production run by default. Pass a new explicit `--idempotency-key` only
|
|
97
|
+
when a fresh execution is intentional. A bounded wait that expires still emits
|
|
98
|
+
the `cloud-verifier` schema with `timed_out: true`, the last observed status,
|
|
99
|
+
and `next_command`, then exits 2. Read or resume a known run with:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
haystack verify hosted status cv_<48-lowercase-hex-characters> \
|
|
103
|
+
--account <login> \
|
|
104
|
+
--wait \
|
|
105
|
+
--json
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Agents can use the same production lifecycle over stdio MCP:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
haystack verify hosted mcp --account <login>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The hosted server exposes only `verify_start`, `verify_status`, and
|
|
115
|
+
`verify_wait`. Tool results use the same `cloud-verifier` schema as the CLI;
|
|
116
|
+
credentials, repository source, provider metadata, and sandbox mutation are
|
|
117
|
+
not exposed.
|
|
118
|
+
|
|
82
119
|
**`haystack setup --json`** speaks NDJSON: events (`question`, `permission`,
|
|
83
120
|
`progress`, `result`) on stdout, replies on stdin keyed by
|
|
84
121
|
`requestID`/`permissionID`. `haystack schema setup` documents the full
|
|
@@ -41,15 +41,31 @@ export interface UniverseRef {
|
|
|
41
41
|
}
|
|
42
42
|
export interface RiskCell {
|
|
43
43
|
riskCellId: string;
|
|
44
|
+
riskSourceId?: string;
|
|
44
45
|
ordinal: number;
|
|
45
46
|
title: string;
|
|
46
47
|
rationale?: string;
|
|
47
48
|
observableConsequence?: string;
|
|
48
49
|
activationConditions?: string[];
|
|
49
50
|
causalPath?: string[];
|
|
51
|
+
capabilityId?: string;
|
|
50
52
|
status: string;
|
|
51
53
|
severity?: string;
|
|
52
54
|
confidence?: number;
|
|
55
|
+
sourceEvidence?: Array<{
|
|
56
|
+
evidenceId: string;
|
|
57
|
+
file: string;
|
|
58
|
+
line?: number;
|
|
59
|
+
excerpt?: string;
|
|
60
|
+
relationship: string;
|
|
61
|
+
}>;
|
|
62
|
+
oracles?: Array<{
|
|
63
|
+
oracleId: string;
|
|
64
|
+
kind: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
measurement: string;
|
|
67
|
+
threshold?: string;
|
|
68
|
+
}>;
|
|
53
69
|
durationMs?: number;
|
|
54
70
|
base: UniverseRef;
|
|
55
71
|
head: UniverseRef;
|
|
@@ -114,6 +130,7 @@ export interface VerificationRun {
|
|
|
114
130
|
command: string;
|
|
115
131
|
createdAt: string;
|
|
116
132
|
updatedAt: string;
|
|
133
|
+
planSourceRunId?: string;
|
|
117
134
|
chapters: VerificationChapter[];
|
|
118
135
|
events: VerificationRunEvent[];
|
|
119
136
|
summary: {
|
|
@@ -139,9 +156,15 @@ export declare class VerifyUnauthorizedError extends Error {
|
|
|
139
156
|
export declare function discoverVerifyRoot(explicit?: string): string | null;
|
|
140
157
|
export declare function runsRoot(repoRoot: string): string;
|
|
141
158
|
export declare function runDir(repoRoot: string, runId: string): string;
|
|
159
|
+
/**
|
|
160
|
+
* Read when a run began from the record itself. updatedAt and mtime both move
|
|
161
|
+
* when cleanup or a migration rewrites old runs and therefore cannot define
|
|
162
|
+
* "latest".
|
|
163
|
+
*/
|
|
164
|
+
export declare function runStartedAtMs(runJsonPath: string): number | null;
|
|
142
165
|
export declare function listRunDirs(repoRoot: string): Array<{
|
|
143
166
|
runId: string;
|
|
144
|
-
|
|
167
|
+
startedAt: number;
|
|
145
168
|
}>;
|
|
146
169
|
/** Same alias rules as the control plane: run id verbatim, `latest`, or a chapter id. */
|
|
147
170
|
export declare function resolveRunIdOnDisk(repoRoot: string, requested: string): string;
|
|
@@ -179,6 +202,7 @@ export interface RunListRow {
|
|
|
179
202
|
chapters: string[];
|
|
180
203
|
status: string;
|
|
181
204
|
age: string | null;
|
|
205
|
+
created_at?: string;
|
|
182
206
|
updated_at: string;
|
|
183
207
|
tests: number;
|
|
184
208
|
passed: number;
|
|
@@ -400,6 +424,7 @@ export interface WatchCallbacks {
|
|
|
400
424
|
export interface WatchResult {
|
|
401
425
|
loaded: LoadedRun;
|
|
402
426
|
timedOut: boolean;
|
|
427
|
+
endReason: 'terminal' | 'stale' | 'timeout';
|
|
403
428
|
}
|
|
404
429
|
/**
|
|
405
430
|
* Poll until the run reaches a terminal state. The terminal predicate is the
|
|
@@ -72,6 +72,36 @@ export function runsRoot(repoRoot) {
|
|
|
72
72
|
export function runDir(repoRoot, runId) {
|
|
73
73
|
return path.join(runsRoot(repoRoot), runId);
|
|
74
74
|
}
|
|
75
|
+
function isMissingFileError(error) {
|
|
76
|
+
return typeof error === 'object'
|
|
77
|
+
&& error !== null
|
|
78
|
+
&& 'code' in error
|
|
79
|
+
&& error.code === 'ENOENT';
|
|
80
|
+
}
|
|
81
|
+
function runRecordReadError(runJsonPath, error) {
|
|
82
|
+
return new Error(`Could not read verifier run record ${runJsonPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read when a run began from the record itself. updatedAt and mtime both move
|
|
86
|
+
* when cleanup or a migration rewrites old runs and therefore cannot define
|
|
87
|
+
* "latest".
|
|
88
|
+
*/
|
|
89
|
+
export function runStartedAtMs(runJsonPath) {
|
|
90
|
+
try {
|
|
91
|
+
const run = JSON.parse(fs.readFileSync(runJsonPath, 'utf8'));
|
|
92
|
+
if (typeof run.createdAt === 'string') {
|
|
93
|
+
const recorded = Date.parse(run.createdAt);
|
|
94
|
+
if (Number.isFinite(recorded))
|
|
95
|
+
return recorded;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (isMissingFileError(error))
|
|
100
|
+
return null;
|
|
101
|
+
throw runRecordReadError(runJsonPath, error);
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
75
105
|
export function listRunDirs(repoRoot) {
|
|
76
106
|
let entries;
|
|
77
107
|
try {
|
|
@@ -82,16 +112,12 @@ export function listRunDirs(repoRoot) {
|
|
|
82
112
|
}
|
|
83
113
|
return entries
|
|
84
114
|
.filter(name => name.startsWith('run-'))
|
|
85
|
-
.map(name => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
.filter(entry => entry.changedAt > 0)
|
|
94
|
-
.sort((a, b) => b.changedAt - a.changedAt);
|
|
115
|
+
.map(name => ({
|
|
116
|
+
runId: name,
|
|
117
|
+
startedAt: runStartedAtMs(path.join(runsRoot(repoRoot), name, 'run.json')),
|
|
118
|
+
}))
|
|
119
|
+
.filter((entry) => (entry.startedAt !== null))
|
|
120
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
95
121
|
}
|
|
96
122
|
/** Same alias rules as the control plane: run id verbatim, `latest`, or a chapter id. */
|
|
97
123
|
export function resolveRunIdOnDisk(repoRoot, requested) {
|
|
@@ -102,11 +128,14 @@ export function resolveRunIdOnDisk(repoRoot, requested) {
|
|
|
102
128
|
return match?.runId ?? requested;
|
|
103
129
|
}
|
|
104
130
|
export function loadRunFromDisk(repoRoot, runId) {
|
|
131
|
+
const runJsonPath = path.join(runDir(repoRoot, runId), 'run.json');
|
|
105
132
|
try {
|
|
106
|
-
return JSON.parse(fs.readFileSync(
|
|
133
|
+
return JSON.parse(fs.readFileSync(runJsonPath, 'utf8'));
|
|
107
134
|
}
|
|
108
|
-
catch {
|
|
109
|
-
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (isMissingFileError(error))
|
|
137
|
+
return null;
|
|
138
|
+
throw runRecordReadError(runJsonPath, error);
|
|
110
139
|
}
|
|
111
140
|
}
|
|
112
141
|
export async function fetchRunFromServer(server, idOrAlias) {
|
|
@@ -158,12 +187,13 @@ function rowOfRun(run) {
|
|
|
158
187
|
run_id: run.runId,
|
|
159
188
|
chapters: run.chapters.map(chapter => chapter.chapterId),
|
|
160
189
|
status: run.status,
|
|
161
|
-
age: formatAge(run.
|
|
190
|
+
age: formatAge(run.createdAt),
|
|
191
|
+
created_at: run.createdAt,
|
|
162
192
|
updated_at: run.updatedAt,
|
|
163
193
|
tests: run.summary.total,
|
|
164
194
|
passed: run.summary.passed,
|
|
165
195
|
failed: run.summary.failed,
|
|
166
|
-
could_not_run: run.summary.infraErrors,
|
|
196
|
+
could_not_run: run.summary.infraErrors + (run.summary.inconclusive ?? 0),
|
|
167
197
|
culprit_sha: bisect?.culpritSha ?? null,
|
|
168
198
|
};
|
|
169
199
|
}
|
|
@@ -184,7 +214,10 @@ export async function listRunRows(options) {
|
|
|
184
214
|
const body = (await response.json());
|
|
185
215
|
if (Array.isArray(body.runs)) {
|
|
186
216
|
// Recompute ages locally so they do not drift with server response caching.
|
|
187
|
-
const rows = body.runs.map(row => ({
|
|
217
|
+
const rows = body.runs.map(row => ({
|
|
218
|
+
...row,
|
|
219
|
+
age: formatAge(row.created_at || row.updated_at) ?? row.age,
|
|
220
|
+
}));
|
|
188
221
|
return { rows, source: 'server', total: body.total ?? rows.length };
|
|
189
222
|
}
|
|
190
223
|
}
|
|
@@ -504,7 +537,7 @@ export function summarizeRun(loaded, server) {
|
|
|
504
537
|
source: loaded.source,
|
|
505
538
|
created_at: run.createdAt,
|
|
506
539
|
updated_at: run.updatedAt,
|
|
507
|
-
age: formatAge(run.
|
|
540
|
+
age: formatAge(run.createdAt),
|
|
508
541
|
counts: {
|
|
509
542
|
total: run.summary.total,
|
|
510
543
|
completed: run.summary.completed,
|
|
@@ -729,8 +762,9 @@ export async function waitForTerminal(options) {
|
|
|
729
762
|
}
|
|
730
763
|
first = false;
|
|
731
764
|
const staleMarked = loaded.stale || loaded.run.events.some(event => event.eventId.startsWith('evt-stale-'));
|
|
732
|
-
if (isTerminal(loaded.run) && !staleMarked)
|
|
733
|
-
return { loaded, timedOut: false };
|
|
765
|
+
if (isTerminal(loaded.run) && !staleMarked) {
|
|
766
|
+
return { loaded, timedOut: false, endReason: 'terminal' };
|
|
767
|
+
}
|
|
734
768
|
const progressed = loaded.run.updatedAt !== lastUpdatedAt;
|
|
735
769
|
lastUpdatedAt = loaded.run.updatedAt;
|
|
736
770
|
if (staleMarked) {
|
|
@@ -739,15 +773,17 @@ export async function waitForTerminal(options) {
|
|
|
739
773
|
}
|
|
740
774
|
else {
|
|
741
775
|
noProgressSince = noProgressSince ?? Date.now();
|
|
742
|
-
if (Date.now() - noProgressSince > staleGraceMs)
|
|
743
|
-
return { loaded, timedOut: false };
|
|
776
|
+
if (Date.now() - noProgressSince > staleGraceMs) {
|
|
777
|
+
return { loaded, timedOut: false, endReason: 'stale' };
|
|
778
|
+
}
|
|
744
779
|
}
|
|
745
780
|
}
|
|
746
781
|
else {
|
|
747
782
|
noProgressSince = null;
|
|
748
783
|
}
|
|
749
|
-
if (deadline && Date.now() > deadline)
|
|
750
|
-
return { loaded, timedOut: true };
|
|
784
|
+
if (deadline && Date.now() > deadline) {
|
|
785
|
+
return { loaded, timedOut: true, endReason: 'timeout' };
|
|
786
|
+
}
|
|
751
787
|
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
752
788
|
}
|
|
753
789
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export declare const HOSTED_VERIFY_TOOLS: Tool[];
|
|
3
|
+
export interface HostedVerifyMcpOptions {
|
|
4
|
+
account?: string;
|
|
5
|
+
}
|
|
6
|
+
interface HostedToolContext {
|
|
7
|
+
token: string;
|
|
8
|
+
account?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function executeHostedVerifyTool(name: string, input: Record<string, unknown>, context: HostedToolContext): Promise<Record<string, unknown>>;
|
|
11
|
+
export declare function runHostedVerifyMcpServer(options: HostedVerifyMcpOptions): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Production Cloud Verifier MCP server: `haystack verify hosted mcp`.
|
|
2
|
+
//
|
|
3
|
+
// This intentionally exposes only the durable hosted run lifecycle. The local
|
|
4
|
+
// specimen MCP server has artifact and retained-sandbox tools whose data model
|
|
5
|
+
// does not exist in the source-free production result contract.
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { resolveAuthContext } from '../utils/auth.js';
|
|
10
|
+
import { buildHostedJson, getHostedVerification, hostedResumeCommand, parseHostedRepository, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
|
|
11
|
+
const TERMINAL_STATUSES = new Set([
|
|
12
|
+
'complete',
|
|
13
|
+
'errored',
|
|
14
|
+
'expired',
|
|
15
|
+
'terminated',
|
|
16
|
+
]);
|
|
17
|
+
const DEFAULT_TIMEOUT_SECONDS = 30 * 60;
|
|
18
|
+
const MAX_TIMEOUT_SECONDS = 60 * 60;
|
|
19
|
+
const accountInput = {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'Saved Haystack account login. Omit to use the server default or active account.',
|
|
22
|
+
};
|
|
23
|
+
const runIdInput = {
|
|
24
|
+
type: 'string',
|
|
25
|
+
pattern: '^cv_[0-9a-f]{48}$',
|
|
26
|
+
description: 'Owned hosted run id.',
|
|
27
|
+
};
|
|
28
|
+
export const HOSTED_VERIFY_TOOLS = [
|
|
29
|
+
{
|
|
30
|
+
name: 'verify_start',
|
|
31
|
+
description: 'Start an authenticated production verification for an exact GitHub commit pair. Returns immediately; call verify_wait next.',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
properties: {
|
|
36
|
+
repository: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
pattern: '^[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}$',
|
|
39
|
+
description: 'Exact GitHub owner/repository name.',
|
|
40
|
+
},
|
|
41
|
+
base_commit: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
pattern: '^[0-9a-f]{40}$',
|
|
44
|
+
description: 'Exact 40-character lowercase base commit SHA.',
|
|
45
|
+
},
|
|
46
|
+
head_commit: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
pattern: '^[0-9a-f]{40}$',
|
|
49
|
+
description: 'Exact 40-character lowercase head commit SHA.',
|
|
50
|
+
},
|
|
51
|
+
idempotency_key: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$',
|
|
54
|
+
description: 'Optional stable retry key. Omit to derive it from the exact repository and commits.',
|
|
55
|
+
},
|
|
56
|
+
account: accountInput,
|
|
57
|
+
},
|
|
58
|
+
required: ['repository', 'base_commit', 'head_commit'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'verify_status',
|
|
63
|
+
description: 'Read the current source-free production receipt for an owned hosted run.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
run_id: runIdInput,
|
|
69
|
+
account: accountInput,
|
|
70
|
+
},
|
|
71
|
+
required: ['run_id'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'verify_wait',
|
|
76
|
+
description: 'Wait for an owned production run to finish. A timeout returns the last observed status with timed_out=true; the run keeps going.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
properties: {
|
|
81
|
+
run_id: runIdInput,
|
|
82
|
+
timeout_seconds: {
|
|
83
|
+
type: 'integer',
|
|
84
|
+
minimum: 10,
|
|
85
|
+
maximum: MAX_TIMEOUT_SECONDS,
|
|
86
|
+
description: 'Bounded wait in seconds (default 1800, maximum 3600).',
|
|
87
|
+
},
|
|
88
|
+
account: accountInput,
|
|
89
|
+
},
|
|
90
|
+
required: ['run_id'],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
function requireInputString(input, field) {
|
|
95
|
+
const value = input[field];
|
|
96
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
97
|
+
throw new Error(`${field} required`);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
function optionalInputString(input, field) {
|
|
102
|
+
const value = input[field];
|
|
103
|
+
if (value === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
106
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function assertExactInputFields(input, allowed) {
|
|
111
|
+
const allowedFields = new Set(allowed);
|
|
112
|
+
const unsupported = Object.keys(input).find(field => !allowedFields.has(field));
|
|
113
|
+
if (unsupported)
|
|
114
|
+
throw new Error(`unsupported input field: ${unsupported}`);
|
|
115
|
+
}
|
|
116
|
+
function assertToolInputFields(name, input) {
|
|
117
|
+
if (name === 'verify_start') {
|
|
118
|
+
assertExactInputFields(input, [
|
|
119
|
+
'account',
|
|
120
|
+
'base_commit',
|
|
121
|
+
'head_commit',
|
|
122
|
+
'idempotency_key',
|
|
123
|
+
'repository',
|
|
124
|
+
]);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (name === 'verify_status') {
|
|
128
|
+
assertExactInputFields(input, ['account', 'run_id']);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (name === 'verify_wait') {
|
|
132
|
+
assertExactInputFields(input, ['account', 'run_id', 'timeout_seconds']);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`unknown tool: ${name}`);
|
|
136
|
+
}
|
|
137
|
+
function nextCommand(state, account) {
|
|
138
|
+
return TERMINAL_STATUSES.has(state.status)
|
|
139
|
+
? undefined
|
|
140
|
+
: hostedResumeCommand(state.runId, account);
|
|
141
|
+
}
|
|
142
|
+
export async function executeHostedVerifyTool(name, input, context) {
|
|
143
|
+
assertToolInputFields(name, input);
|
|
144
|
+
if (name === 'verify_start') {
|
|
145
|
+
const repository = requireInputString(input, 'repository');
|
|
146
|
+
const started = await startHostedVerification(repository, {
|
|
147
|
+
base: requireInputString(input, 'base_commit'),
|
|
148
|
+
head: requireInputString(input, 'head_commit'),
|
|
149
|
+
idempotencyKey: optionalInputString(input, 'idempotency_key'),
|
|
150
|
+
wait: false,
|
|
151
|
+
}, context.token);
|
|
152
|
+
return buildHostedJson('start', started.state, {
|
|
153
|
+
account: context.account,
|
|
154
|
+
idempotencyKey: started.idempotencyKey,
|
|
155
|
+
nextCommand: nextCommand(started.state, context.account),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (name === 'verify_status') {
|
|
159
|
+
const state = await getHostedVerification(requireInputString(input, 'run_id'), context.token);
|
|
160
|
+
return buildHostedJson('status', state, {
|
|
161
|
+
account: context.account,
|
|
162
|
+
nextCommand: nextCommand(state, context.account),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (name === 'verify_wait') {
|
|
166
|
+
const runId = requireInputString(input, 'run_id');
|
|
167
|
+
const rawTimeout = input.timeout_seconds;
|
|
168
|
+
const timeoutSeconds = rawTimeout === undefined
|
|
169
|
+
? DEFAULT_TIMEOUT_SECONDS
|
|
170
|
+
: rawTimeout;
|
|
171
|
+
if (typeof timeoutSeconds !== 'number'
|
|
172
|
+
|| !Number.isInteger(timeoutSeconds)
|
|
173
|
+
|| timeoutSeconds < 10
|
|
174
|
+
|| timeoutSeconds > MAX_TIMEOUT_SECONDS) {
|
|
175
|
+
throw new Error('timeout_seconds must be an integer from 10 to 3600');
|
|
176
|
+
}
|
|
177
|
+
const waited = await waitForHostedVerification(runId, context.token, {
|
|
178
|
+
intervalMs: 3_000,
|
|
179
|
+
timeoutMs: timeoutSeconds * 1_000,
|
|
180
|
+
});
|
|
181
|
+
return buildHostedJson('status', waited.state, {
|
|
182
|
+
account: context.account,
|
|
183
|
+
nextCommand: nextCommand(waited.state, context.account),
|
|
184
|
+
timedOut: waited.timedOut,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`unknown tool: ${name}`);
|
|
188
|
+
}
|
|
189
|
+
async function resolveToolContext(name, input, options) {
|
|
190
|
+
const requestedAccount = optionalInputString(input, 'account') ?? options.account;
|
|
191
|
+
const repository = name === 'verify_start'
|
|
192
|
+
? parseHostedRepository(requireInputString(input, 'repository'))
|
|
193
|
+
: undefined;
|
|
194
|
+
const auth = await resolveAuthContext({
|
|
195
|
+
preferredLogin: requestedAccount,
|
|
196
|
+
owner: repository?.owner,
|
|
197
|
+
repo: repository?.repository,
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
token: auth.token,
|
|
201
|
+
account: auth.token.startsWith('hsk_live_') ? undefined : auth.login,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export async function runHostedVerifyMcpServer(options) {
|
|
205
|
+
const mcp = new Server({ name: 'haystack-verify-hosted', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
206
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: HOSTED_VERIFY_TOOLS }));
|
|
207
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
208
|
+
const { name, arguments: args } = request.params;
|
|
209
|
+
const input = (args ?? {});
|
|
210
|
+
try {
|
|
211
|
+
// Reject undeclared fields before account resolution so callers cannot
|
|
212
|
+
// use an invalid request to trigger credential or repository lookup.
|
|
213
|
+
assertToolInputFields(name, input);
|
|
214
|
+
const context = await resolveToolContext(name, input, options);
|
|
215
|
+
const result = await executeHostedVerifyTool(name, input, context);
|
|
216
|
+
return {
|
|
217
|
+
content: [{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: JSON.stringify(result, null, 2),
|
|
220
|
+
}],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
return {
|
|
225
|
+
isError: true,
|
|
226
|
+
content: [{
|
|
227
|
+
type: 'text',
|
|
228
|
+
text: error instanceof Error ? error.message : String(error),
|
|
229
|
+
}],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
await mcp.connect(new StdioServerTransport());
|
|
234
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface HostedStartOptions {
|
|
2
|
+
base: string;
|
|
3
|
+
head: string;
|
|
4
|
+
idempotencyKey?: string;
|
|
5
|
+
account?: string;
|
|
6
|
+
wait?: boolean;
|
|
7
|
+
interval?: string;
|
|
8
|
+
timeout?: string;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface HostedStatusOptions {
|
|
12
|
+
account?: string;
|
|
13
|
+
wait?: boolean;
|
|
14
|
+
interval?: string;
|
|
15
|
+
timeout?: string;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
}
|
|
18
|
+
interface HostedRunState {
|
|
19
|
+
runId: string;
|
|
20
|
+
status: string;
|
|
21
|
+
repository?: string;
|
|
22
|
+
baseCommit?: string;
|
|
23
|
+
headCommit?: string;
|
|
24
|
+
createdAt?: string;
|
|
25
|
+
completedAt?: string;
|
|
26
|
+
output?: Record<string, unknown>;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
interface HostedPollOptions {
|
|
30
|
+
intervalMs: number;
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
onStatus?: (state: HostedRunState) => void;
|
|
33
|
+
}
|
|
34
|
+
interface HostedWaitResult {
|
|
35
|
+
state: HostedRunState;
|
|
36
|
+
timedOut: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function parseHostedRunState(value: unknown, expectedRunId?: string): HostedRunState;
|
|
39
|
+
export declare function parseHostedRepository(value: string): {
|
|
40
|
+
owner: string;
|
|
41
|
+
repository: string;
|
|
42
|
+
fullName: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function defaultHostedIdempotencyKey(repository: string, baseCommit: string, headCommit: string): string;
|
|
45
|
+
export declare function startHostedVerification(repositoryArg: string, options: HostedStartOptions, token: string): Promise<{
|
|
46
|
+
state: HostedRunState;
|
|
47
|
+
idempotencyKey: string;
|
|
48
|
+
}>;
|
|
49
|
+
export declare function getHostedVerification(runId: string, token: string): Promise<HostedRunState>;
|
|
50
|
+
export declare function waitForHostedVerification(runId: string, token: string, options: HostedPollOptions): Promise<HostedWaitResult>;
|
|
51
|
+
export declare function buildHostedJson(operation: 'start' | 'status', state: HostedRunState, extras?: {
|
|
52
|
+
account?: string;
|
|
53
|
+
idempotencyKey?: string;
|
|
54
|
+
nextCommand?: string;
|
|
55
|
+
timedOut?: boolean;
|
|
56
|
+
}): Record<string, unknown>;
|
|
57
|
+
export declare function hostedResumeCommand(runId: string, account?: string): string;
|
|
58
|
+
export declare function verifyHostedStartCommand(repositoryArg: string, options: HostedStartOptions): Promise<void>;
|
|
59
|
+
export declare function verifyHostedStatusCommand(runId: string, options: HostedStatusOptions): Promise<void>;
|
|
60
|
+
export {};
|