specguard-mcp 0.1.1 → 0.1.3
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 +129 -22
- package/dist/bin/specguard-mcp.js +5 -0
- package/dist/bin/specguard-mcp.js.map +1 -1
- package/dist/src/config.d.ts +104 -14
- package/dist/src/config.js +78 -7
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +6 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/support/run-command.d.ts +42 -0
- package/dist/src/support/run-command.js +124 -8
- package/dist/src/support/run-command.js.map +1 -1
- package/dist/src/support/specguard-api.d.ts +53 -2
- package/dist/src/support/specguard-api.js +153 -8
- package/dist/src/support/specguard-api.js.map +1 -1
- package/dist/src/support/teardown.d.ts +33 -0
- package/dist/src/support/teardown.js +56 -0
- package/dist/src/support/teardown.js.map +1 -0
- package/dist/src/tools/add-repository.d.ts +55 -0
- package/dist/src/tools/add-repository.js +114 -0
- package/dist/src/tools/add-repository.js.map +1 -0
- package/dist/src/tools/args.d.ts +20 -0
- package/dist/src/tools/args.js +30 -0
- package/dist/src/tools/args.js.map +1 -1
- package/dist/src/tools/index.d.ts +33 -0
- package/dist/src/tools/index.js +41 -1
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-repositories.d.ts +75 -0
- package/dist/src/tools/list-repositories.js +112 -0
- package/dist/src/tools/list-repositories.js.map +1 -0
- package/dist/src/tools/repository-overview.js +35 -15
- package/dist/src/tools/repository-overview.js.map +1 -1
- package/package.json +1 -1
|
@@ -68,6 +68,48 @@ export declare const DEFAULT_COMMAND_TIMEOUT_MS = 120000;
|
|
|
68
68
|
* descriptor costs a truncated tail instead of a call that never returns.
|
|
69
69
|
*/
|
|
70
70
|
export declare const EXIT_CLOSE_GRACE_MS = 1000;
|
|
71
|
+
/**
|
|
72
|
+
* The pids of every run currently spawned and not yet reaped.
|
|
73
|
+
*
|
|
74
|
+
* Exported for the teardown handler's diagnostics. A PROJECTION, not the set:
|
|
75
|
+
* it drops entries whose spawn never produced a process, because a `undefined`
|
|
76
|
+
* pid is not something a diagnostic can name or a caller can signal.
|
|
77
|
+
*
|
|
78
|
+
* That filter makes it the WRONG observer for asserting the set does not leak —
|
|
79
|
+
* see `outstandingRunCount`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function outstandingRunPids(): readonly number[];
|
|
82
|
+
/**
|
|
83
|
+
* How many runs are registered, counting those whose spawn produced no pid.
|
|
84
|
+
*
|
|
85
|
+
* The unfiltered companion to `outstandingRunPids`, and the one a leak test must
|
|
86
|
+
* use. The distinction is not pedantic: the spawn-failure path registers a child
|
|
87
|
+
* whose `pid` is `undefined`, so it is invisible to `outstandingRunPids` BY
|
|
88
|
+
* EXACTLY THE PROPERTY THAT MAKES IT A LEAK. A test asserting that path through
|
|
89
|
+
* the pid projection holds whether or not the deregistration happens, and would
|
|
90
|
+
* stay green if a refactor dropped it.
|
|
91
|
+
*
|
|
92
|
+
* The leak is memory-only — `killRun` returns `false` for an undefined pid, so a
|
|
93
|
+
* stranded entry is skipped by the drain rather than mis-signalled — but it is
|
|
94
|
+
* one entry per failed spawn for the life of the server, and it is only ever
|
|
95
|
+
* observable from outside. Hence a reader of the set itself.
|
|
96
|
+
*/
|
|
97
|
+
export declare function outstandingRunCount(): number;
|
|
98
|
+
/**
|
|
99
|
+
* Kills every run still in flight, and answers how many it signalled.
|
|
100
|
+
*
|
|
101
|
+
* The teardown path. SYNCHRONOUS AND UNBOUNDED BY NOTHING — it sends signals and
|
|
102
|
+
* returns, it never waits for a child to die. That is deliberate: this runs from
|
|
103
|
+
* a SIGINT/SIGTERM handler, where anything that waits is something that can hang
|
|
104
|
+
* the shutdown it was supposed to perform. SIGKILL is not refusable, so there is
|
|
105
|
+
* no acknowledgement worth waiting for.
|
|
106
|
+
*
|
|
107
|
+
* It reuses `killRun` rather than re-deriving the kill, so the group-vs-child
|
|
108
|
+
* fallback and the spawn-failure guard have exactly one implementation. The set
|
|
109
|
+
* is snapshotted before iterating because `killRun` can drive an `exit` that
|
|
110
|
+
* mutates it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function killOutstandingRuns(): number;
|
|
71
113
|
/**
|
|
72
114
|
* Runs a program with an argument LIST, never through a shell.
|
|
73
115
|
*
|
|
@@ -15,6 +15,95 @@ export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
|
15
15
|
* descriptor costs a truncated tail instead of a call that never returns.
|
|
16
16
|
*/
|
|
17
17
|
export const EXIT_CLOSE_GRACE_MS = 1_000;
|
|
18
|
+
/**
|
|
19
|
+
* The runs that are spawned and not yet reaped.
|
|
20
|
+
*
|
|
21
|
+
* This exists because `detached: true` below buys the reach of the timeout kill
|
|
22
|
+
* and PAYS for it: a detached child is in a new session, outside this server's
|
|
23
|
+
* controlling terminal, so a signal aimed at OUR group — an interactive Ctrl-C,
|
|
24
|
+
* a supervisor's `kill -- -PGID` — no longer reaches a lint run in flight. The
|
|
25
|
+
* run is then orphaned, and orphaned WITHOUT A DEADLINE: the 120s ceiling is a
|
|
26
|
+
* parent-side `setTimeout`, so killing the parent destroys the only thing that
|
|
27
|
+
* was going to stop it. On a 20k-example suite that is a full lint's worth of
|
|
28
|
+
* CPU held by a process attached to nothing. `killOutstandingRuns` is the
|
|
29
|
+
* teardown path that closes it, and this set is what tells it whom to signal.
|
|
30
|
+
*
|
|
31
|
+
* MEMBERSHIP MEANS "NOT YET REAPED", and that is load-bearing rather than
|
|
32
|
+
* descriptive: `killRun` signals a raw negated pid, which has no liveness check
|
|
33
|
+
* of its own, so a stale entry is a SIGKILL aimed at whatever recycled that pid.
|
|
34
|
+
* Entries are therefore removed on the child's own `exit` — the reap — and NOT
|
|
35
|
+
* at settle, which on the grace-backstop path happens up to EXIT_CLOSE_GRACE_MS
|
|
36
|
+
* later. Deleting at settle would leave exactly the window in which a drain
|
|
37
|
+
* would signal a freed pid.
|
|
38
|
+
*
|
|
39
|
+
* The accepted consequence is the one this file already takes at `killRun`: a
|
|
40
|
+
* straggler outliving an already-exited child is not killed at teardown. That
|
|
41
|
+
* trade is deliberate and is not widened here.
|
|
42
|
+
*/
|
|
43
|
+
const liveChildren = new Set();
|
|
44
|
+
/**
|
|
45
|
+
* The pids of every run currently spawned and not yet reaped.
|
|
46
|
+
*
|
|
47
|
+
* Exported for the teardown handler's diagnostics. A PROJECTION, not the set:
|
|
48
|
+
* it drops entries whose spawn never produced a process, because a `undefined`
|
|
49
|
+
* pid is not something a diagnostic can name or a caller can signal.
|
|
50
|
+
*
|
|
51
|
+
* That filter makes it the WRONG observer for asserting the set does not leak —
|
|
52
|
+
* see `outstandingRunCount`.
|
|
53
|
+
*/
|
|
54
|
+
export function outstandingRunPids() {
|
|
55
|
+
const pids = [];
|
|
56
|
+
for (const child of liveChildren) {
|
|
57
|
+
if (child.pid !== undefined)
|
|
58
|
+
pids.push(child.pid);
|
|
59
|
+
}
|
|
60
|
+
return pids;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* How many runs are registered, counting those whose spawn produced no pid.
|
|
64
|
+
*
|
|
65
|
+
* The unfiltered companion to `outstandingRunPids`, and the one a leak test must
|
|
66
|
+
* use. The distinction is not pedantic: the spawn-failure path registers a child
|
|
67
|
+
* whose `pid` is `undefined`, so it is invisible to `outstandingRunPids` BY
|
|
68
|
+
* EXACTLY THE PROPERTY THAT MAKES IT A LEAK. A test asserting that path through
|
|
69
|
+
* the pid projection holds whether or not the deregistration happens, and would
|
|
70
|
+
* stay green if a refactor dropped it.
|
|
71
|
+
*
|
|
72
|
+
* The leak is memory-only — `killRun` returns `false` for an undefined pid, so a
|
|
73
|
+
* stranded entry is skipped by the drain rather than mis-signalled — but it is
|
|
74
|
+
* one entry per failed spawn for the life of the server, and it is only ever
|
|
75
|
+
* observable from outside. Hence a reader of the set itself.
|
|
76
|
+
*/
|
|
77
|
+
export function outstandingRunCount() {
|
|
78
|
+
return liveChildren.size;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Kills every run still in flight, and answers how many it signalled.
|
|
82
|
+
*
|
|
83
|
+
* The teardown path. SYNCHRONOUS AND UNBOUNDED BY NOTHING — it sends signals and
|
|
84
|
+
* returns, it never waits for a child to die. That is deliberate: this runs from
|
|
85
|
+
* a SIGINT/SIGTERM handler, where anything that waits is something that can hang
|
|
86
|
+
* the shutdown it was supposed to perform. SIGKILL is not refusable, so there is
|
|
87
|
+
* no acknowledgement worth waiting for.
|
|
88
|
+
*
|
|
89
|
+
* It reuses `killRun` rather than re-deriving the kill, so the group-vs-child
|
|
90
|
+
* fallback and the spawn-failure guard have exactly one implementation. The set
|
|
91
|
+
* is snapshotted before iterating because `killRun` can drive an `exit` that
|
|
92
|
+
* mutates it.
|
|
93
|
+
*/
|
|
94
|
+
export function killOutstandingRuns() {
|
|
95
|
+
const children = [...liveChildren];
|
|
96
|
+
let signalled = 0;
|
|
97
|
+
for (const child of children) {
|
|
98
|
+
// Removed FIRST, so the entry is gone even if the kill throws. A registry
|
|
99
|
+
// that kept a child it had already tried to kill would hand a second drain
|
|
100
|
+
// the same stale pid.
|
|
101
|
+
liveChildren.delete(child);
|
|
102
|
+
if (killRun(child))
|
|
103
|
+
signalled += 1;
|
|
104
|
+
}
|
|
105
|
+
return signalled;
|
|
106
|
+
}
|
|
18
107
|
/**
|
|
19
108
|
* Runs a program with an argument LIST, never through a shell.
|
|
20
109
|
*
|
|
@@ -128,6 +217,11 @@ export const runCommand = (argv, options = {}) => {
|
|
|
128
217
|
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
129
218
|
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
130
219
|
child.on("error", (error) => {
|
|
220
|
+
// The spawn-failure path: there is no process, and `child.pid` is
|
|
221
|
+
// `undefined`, so this entry can never be a legitimate kill target. Dropped
|
|
222
|
+
// here because `exit` does not always follow an `error` — leaving it would
|
|
223
|
+
// strand an unkillable entry in the registry for the life of the server.
|
|
224
|
+
liveChildren.delete(child);
|
|
131
225
|
finish(() => reject(new CommandError(describeSpawnFailure(program, error, options))));
|
|
132
226
|
});
|
|
133
227
|
/**
|
|
@@ -153,6 +247,20 @@ export const runCommand = (argv, options = {}) => {
|
|
|
153
247
|
// on we hold a real `code`/`signal`, so whatever the timer may still find
|
|
154
248
|
// alive in the process group, this is not a run that produced no verdict.
|
|
155
249
|
exited = true;
|
|
250
|
+
// Deregistered HERE — at the reap, alongside `exited`, and deliberately not
|
|
251
|
+
// at the settle below. The two are not the same instant: on the grace
|
|
252
|
+
// backstop `exit` fires and `settleWith` follows up to EXIT_CLOSE_GRACE_MS
|
|
253
|
+
// later, so a registry keyed on settle would hold a child whose pid the
|
|
254
|
+
// kernel has already freed, and a teardown drain landing in that window
|
|
255
|
+
// would fire `process.kill(-pid)` at whatever now owns that number. That
|
|
256
|
+
// is the unrecoverable, aimed-at-a-stranger hazard `killRun` documents and
|
|
257
|
+
// refuses to pay; membership must mean "not yet reaped" so its precondition
|
|
258
|
+
// holds by construction.
|
|
259
|
+
//
|
|
260
|
+
// Placed BEFORE the `settled` early-return for the same reason: the
|
|
261
|
+
// already-settled path is a reap too, and returning first would leak the
|
|
262
|
+
// entry.
|
|
263
|
+
liveChildren.delete(child);
|
|
156
264
|
if (settled)
|
|
157
265
|
return;
|
|
158
266
|
graceTimer = setTimeout(() => {
|
|
@@ -224,7 +332,7 @@ function killRun(child) {
|
|
|
224
332
|
* promise losing the child's type.
|
|
225
333
|
*/
|
|
226
334
|
function spawnChild(program, args, options) {
|
|
227
|
-
|
|
335
|
+
const child = spawn(program, [...args], {
|
|
228
336
|
cwd: options.cwd,
|
|
229
337
|
stdio: ["ignore", "pipe", "pipe"],
|
|
230
338
|
// Explicitly off. Stated rather than defaulted, because this is the line
|
|
@@ -240,15 +348,23 @@ function spawnChild(program, args, options) {
|
|
|
240
348
|
// that detaching buys the reach of the kill and PAYS for it here: a new
|
|
241
349
|
// group is also a new session, outside this server's controlling terminal,
|
|
242
350
|
// so a signal aimed at OUR group — an interactive Ctrl-C, a supervisor's
|
|
243
|
-
// `kill -- -PGID` — no longer reaches a lint run in flight.
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
351
|
+
// `kill -- -PGID` — no longer reaches a lint run in flight. Such a run would
|
|
352
|
+
// be orphaned where it would previously have died alongside us, and orphaned
|
|
353
|
+
// without a deadline, since the ceiling above is a parent-side timer.
|
|
354
|
+
//
|
|
355
|
+
// That is the debt this line used to carry, and it is now PAID rather than
|
|
356
|
+
// merely disclosed: the run is registered in `liveChildren` below, and
|
|
357
|
+
// `bin/specguard-mcp.ts` installs SIGINT/SIGTERM handlers that call
|
|
358
|
+
// `killOutstandingRuns` before exiting. The trade the kill's reach was
|
|
359
|
+
// bought with is closed; do not remove either half without restoring the
|
|
360
|
+
// other.
|
|
250
361
|
detached: true,
|
|
251
362
|
});
|
|
363
|
+
// Registered at the single spawn call site, so a run cannot enter the world
|
|
364
|
+
// unregistered. Removed again on the child's own `exit` — see `liveChildren`
|
|
365
|
+
// for why the reap, and not the settle, is the moment that matters.
|
|
366
|
+
liveChildren.add(child);
|
|
367
|
+
return child;
|
|
252
368
|
}
|
|
253
369
|
/**
|
|
254
370
|
* Which thing failed to run — asked rather than assumed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-command.js","sourceRoot":"","sources":["../../../src/support/run-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAgE5C,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,0BAA0B,GAAG,OAAO,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"run-command.js","sourceRoot":"","sources":["../../../src/support/run-command.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { requireApiConfig, type ApiConfig } from "../config.js";
|
|
1
|
+
import { requireApiConfig, requireUserApiConfig, type ApiConfig } from "../config.js";
|
|
2
2
|
/**
|
|
3
3
|
* The SpecGuard HTTP client — a Bearer key and a path, and nothing else.
|
|
4
4
|
*
|
|
@@ -8,4 +8,55 @@ import { requireApiConfig, type ApiConfig } from "../config.js";
|
|
|
8
8
|
* second place for the permission model to be got wrong.
|
|
9
9
|
*/
|
|
10
10
|
export declare function getJson(api: ApiConfig, path: string, query: Record<string, string | undefined>, fetchImpl: typeof globalThis.fetch): Promise<unknown>;
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* `POST` with a JSON body — the write half of the transport, and deliberately
|
|
13
|
+
* the SAME function underneath.
|
|
14
|
+
*
|
|
15
|
+
* It shares `fetchWithTimeout` rather than standing beside it. The one-total-
|
|
16
|
+
* budget deadline, the explicit race, the `unref`'d timer, the abort and the
|
|
17
|
+
* "reached and stopped" vs "could not reach" split are the expensive part of
|
|
18
|
+
* this module and every argument for them is written above them — none of it is
|
|
19
|
+
* about the verb. A second transport re-deriving them is how the two come to
|
|
20
|
+
* disagree about what `SPECGUARD_TIMEOUT_MS` bounds, and the write path is the
|
|
21
|
+
* one where a call that never returns costs the most: the agent has already
|
|
22
|
+
* committed to a registration by the time it hangs.
|
|
23
|
+
*
|
|
24
|
+
* The body is serialized HERE rather than taken as a string, so no caller can
|
|
25
|
+
* send a body whose `Content-Type` says JSON and whose bytes are not.
|
|
26
|
+
*/
|
|
27
|
+
export declare function postJson(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* `postJson`, narrowed exactly as `getJsonObject` narrows `getJson`.
|
|
30
|
+
*
|
|
31
|
+
* The write path needs the same guard for the same reason, and the reason is not
|
|
32
|
+
* about reading: `ToolResult.structured` is a `Record<string, unknown>`, so a
|
|
33
|
+
* body that is an array or a bare scalar is not something a tool can pass
|
|
34
|
+
* through whichever verb fetched it. Shipping only the raw `postJson` would
|
|
35
|
+
* leave the first write tool to re-type the three-clause check and its sentence
|
|
36
|
+
* — which is precisely the duplication `getJsonObject`'s header says no tool
|
|
37
|
+
* should have to repeat.
|
|
38
|
+
*
|
|
39
|
+
* The pair is mirrored rather than collapsed for the reason the read pair is:
|
|
40
|
+
* `postJson` stays exported un-narrowed for an endpoint that legitimately
|
|
41
|
+
* answers with an array.
|
|
42
|
+
*/
|
|
43
|
+
export declare function postJsonObject(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<Record<string, unknown>>;
|
|
44
|
+
/**
|
|
45
|
+
* `getJson`, narrowed to the object every tool here actually asks it for.
|
|
46
|
+
*
|
|
47
|
+
* MCP hands a tool result back as an object, so an array or a bare JSON scalar
|
|
48
|
+
* is not something a tool can pass through: it surfaces as a protocol error
|
|
49
|
+
* rather than as something the agent can read. Every HTTP tool therefore
|
|
50
|
+
* needed the same three-clause guard, and the same sentence, immediately after
|
|
51
|
+
* its own `getJson` call — which made both the check and its wording the one
|
|
52
|
+
* thing each new tool had to remember to write for itself, and get identical.
|
|
53
|
+
*
|
|
54
|
+
* It belongs here for the reason `requireApiConfig` parses the endpoint rather
|
|
55
|
+
* than leaving that to callers: every HTTP-backed tool added later comes
|
|
56
|
+
* through this function and inherits the check, the same way it inherits the
|
|
57
|
+
* URL check and the 401 wording. `getJson` stays exported un-narrowed for an
|
|
58
|
+
* endpoint that legitimately serves an array — the point is not that objects
|
|
59
|
+
* are the only legal body, it is that no tool re-types this guard.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getJsonObject(api: ApiConfig, path: string, query: Record<string, string | undefined>, fetchImpl: typeof globalThis.fetch): Promise<Record<string, unknown>>;
|
|
62
|
+
export { requireApiConfig, requireUserApiConfig };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { requireApiConfig } from "../config.js";
|
|
1
|
+
import { requireApiConfig, requireUserApiConfig } from "../config.js";
|
|
2
2
|
import { ApiError } from "../errors.js";
|
|
3
3
|
/**
|
|
4
4
|
* The SpecGuard HTTP client — a Bearer key and a path, and nothing else.
|
|
@@ -14,7 +14,59 @@ export async function getJson(api, path, query, fetchImpl) {
|
|
|
14
14
|
if (value !== undefined)
|
|
15
15
|
url.searchParams.set(key, value);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
return requestJson(url, api, fetchImpl, { method: "GET" });
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `POST` with a JSON body — the write half of the transport, and deliberately
|
|
21
|
+
* the SAME function underneath.
|
|
22
|
+
*
|
|
23
|
+
* It shares `fetchWithTimeout` rather than standing beside it. The one-total-
|
|
24
|
+
* budget deadline, the explicit race, the `unref`'d timer, the abort and the
|
|
25
|
+
* "reached and stopped" vs "could not reach" split are the expensive part of
|
|
26
|
+
* this module and every argument for them is written above them — none of it is
|
|
27
|
+
* about the verb. A second transport re-deriving them is how the two come to
|
|
28
|
+
* disagree about what `SPECGUARD_TIMEOUT_MS` bounds, and the write path is the
|
|
29
|
+
* one where a call that never returns costs the most: the agent has already
|
|
30
|
+
* committed to a registration by the time it hangs.
|
|
31
|
+
*
|
|
32
|
+
* The body is serialized HERE rather than taken as a string, so no caller can
|
|
33
|
+
* send a body whose `Content-Type` says JSON and whose bytes are not.
|
|
34
|
+
*/
|
|
35
|
+
export async function postJson(api, path, body, fetchImpl) {
|
|
36
|
+
return requestJson(new URL(`${api.endpoint}${path}`), api, fetchImpl, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
body: JSON.stringify(body),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `postJson`, narrowed exactly as `getJsonObject` narrows `getJson`.
|
|
43
|
+
*
|
|
44
|
+
* The write path needs the same guard for the same reason, and the reason is not
|
|
45
|
+
* about reading: `ToolResult.structured` is a `Record<string, unknown>`, so a
|
|
46
|
+
* body that is an array or a bare scalar is not something a tool can pass
|
|
47
|
+
* through whichever verb fetched it. Shipping only the raw `postJson` would
|
|
48
|
+
* leave the first write tool to re-type the three-clause check and its sentence
|
|
49
|
+
* — which is precisely the duplication `getJsonObject`'s header says no tool
|
|
50
|
+
* should have to repeat.
|
|
51
|
+
*
|
|
52
|
+
* The pair is mirrored rather than collapsed for the reason the read pair is:
|
|
53
|
+
* `postJson` stays exported un-narrowed for an endpoint that legitimately
|
|
54
|
+
* answers with an array.
|
|
55
|
+
*/
|
|
56
|
+
export async function postJsonObject(api, path, body, fetchImpl) {
|
|
57
|
+
return asJsonObject(await postJson(api, path, body, fetchImpl));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Everything both verbs do with a response, in one place.
|
|
61
|
+
*
|
|
62
|
+
* Extracted when the write path landed rather than copied into it: the status
|
|
63
|
+
* check, the "reached and refused" hand-off to `describeFailure` and the
|
|
64
|
+
* not-JSON sentence are identical for a `GET` and a `POST`, and the not-JSON
|
|
65
|
+
* sentence in particular is a diagnosis an operator acts on — a second copy is a
|
|
66
|
+
* second wording waiting to drift from this one.
|
|
67
|
+
*/
|
|
68
|
+
async function requestJson(url, api, fetchImpl, request) {
|
|
69
|
+
const { response, body } = await fetchWithTimeout(url, api, fetchImpl, request);
|
|
18
70
|
if (!response.ok)
|
|
19
71
|
throw describeFailure(response.status, body, api);
|
|
20
72
|
try {
|
|
@@ -26,6 +78,33 @@ export async function getJson(api, path, query, fetchImpl) {
|
|
|
26
78
|
"or login page.", response.status);
|
|
27
79
|
}
|
|
28
80
|
}
|
|
81
|
+
/** The three-clause guard both `*JsonObject` narrowings share. */
|
|
82
|
+
function asJsonObject(body) {
|
|
83
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
84
|
+
throw new ApiError("SpecGuard returned a JSON value that was not an object.");
|
|
85
|
+
}
|
|
86
|
+
return body;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `getJson`, narrowed to the object every tool here actually asks it for.
|
|
90
|
+
*
|
|
91
|
+
* MCP hands a tool result back as an object, so an array or a bare JSON scalar
|
|
92
|
+
* is not something a tool can pass through: it surfaces as a protocol error
|
|
93
|
+
* rather than as something the agent can read. Every HTTP tool therefore
|
|
94
|
+
* needed the same three-clause guard, and the same sentence, immediately after
|
|
95
|
+
* its own `getJson` call — which made both the check and its wording the one
|
|
96
|
+
* thing each new tool had to remember to write for itself, and get identical.
|
|
97
|
+
*
|
|
98
|
+
* It belongs here for the reason `requireApiConfig` parses the endpoint rather
|
|
99
|
+
* than leaving that to callers: every HTTP-backed tool added later comes
|
|
100
|
+
* through this function and inherits the check, the same way it inherits the
|
|
101
|
+
* URL check and the 401 wording. `getJson` stays exported un-narrowed for an
|
|
102
|
+
* endpoint that legitimately serves an array — the point is not that objects
|
|
103
|
+
* are the only legal body, it is that no tool re-types this guard.
|
|
104
|
+
*/
|
|
105
|
+
export async function getJsonObject(api, path, query, fetchImpl) {
|
|
106
|
+
return asJsonObject(await getJson(api, path, query, fetchImpl));
|
|
107
|
+
}
|
|
29
108
|
/**
|
|
30
109
|
* Tells "the deadline won the race" apart from any value a phase could produce.
|
|
31
110
|
*
|
|
@@ -65,7 +144,7 @@ const TIMED_OUT = Symbol("specguard-api deadline");
|
|
|
65
144
|
* frame later, so there is no window in which the read is awaiting somewhere the
|
|
66
145
|
* timer does not reach.
|
|
67
146
|
*/
|
|
68
|
-
async function fetchWithTimeout(url, api, fetchImpl) {
|
|
147
|
+
async function fetchWithTimeout(url, api, fetchImpl, request) {
|
|
69
148
|
const controller = new AbortController();
|
|
70
149
|
let timer;
|
|
71
150
|
const deadline = new Promise((resolve) => {
|
|
@@ -80,12 +159,17 @@ async function fetchWithTimeout(url, api, fetchImpl) {
|
|
|
80
159
|
try {
|
|
81
160
|
const response = await Promise.race([
|
|
82
161
|
fetchImpl(url, {
|
|
83
|
-
method:
|
|
162
|
+
method: request.method,
|
|
84
163
|
headers: {
|
|
85
164
|
Authorization: `Bearer ${api.apiKey}`,
|
|
86
165
|
Accept: "application/json",
|
|
87
166
|
"User-Agent": "specguard-mcp",
|
|
167
|
+
// Sent only when there IS a body. A `Content-Type` on a GET announces
|
|
168
|
+
// a payload that is not there, and some deployments and proxies treat
|
|
169
|
+
// that as a malformed request rather than as a harmless header.
|
|
170
|
+
...(request.body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
88
171
|
},
|
|
172
|
+
...(request.body === undefined ? {} : { body: request.body }),
|
|
89
173
|
signal: controller.signal,
|
|
90
174
|
}),
|
|
91
175
|
deadline,
|
|
@@ -140,18 +224,79 @@ function timedOut(api) {
|
|
|
140
224
|
* hit, and because SpecGuard answers it deliberately flat — "a valid Bearer API
|
|
141
225
|
* key is required", with no detail about why — so the useful half of the
|
|
142
226
|
* diagnosis has to be supplied from this side.
|
|
227
|
+
*
|
|
228
|
+
* WHICH VARIABLE AND WHICH PREFIX ARE READ OFF `api.credential`, never spelled
|
|
229
|
+
* out here. SpecGuard has two credential kinds that refuse each other's tokens
|
|
230
|
+
* before any table is read, so this one branch is reached by tools reading two
|
|
231
|
+
* different variables — and the sentence it used to hardcode ("SPECGUARD_API_KEY
|
|
232
|
+
* must be an sgk_… key … keys are per-repository") is false in all three of its
|
|
233
|
+
* claims for a user-scoped tool, naming a variable its operator may never have
|
|
234
|
+
* touched. That is the same defect `endpointVariable` fixes one branch down, and
|
|
235
|
+
* it gets the same remedy rather than a second hardcoded string: a tool added
|
|
236
|
+
* later inherits correct naming from the `require*` helper it already calls.
|
|
143
237
|
*/
|
|
144
238
|
function describeFailure(status, body, api) {
|
|
145
239
|
if (status === 401) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
240
|
+
const { variable, prefix, rejection } = api.credential;
|
|
241
|
+
return new ApiError(`SpecGuard rejected the API key (401). ${variable} must be an ${prefix}… key issued by ` +
|
|
242
|
+
`${api.endpoint} ${rejection}.`, status);
|
|
149
243
|
}
|
|
150
244
|
if (status === 404) {
|
|
151
245
|
return new ApiError(`${api.endpoint} has no such endpoint (404). Check that ${api.endpointVariable} is the ` +
|
|
152
246
|
"deployment's root URL, without a path.", status);
|
|
153
247
|
}
|
|
248
|
+
if (status === 400) {
|
|
249
|
+
const message = badRequestMessage(body);
|
|
250
|
+
if (message !== undefined)
|
|
251
|
+
return new ApiError(message, status);
|
|
252
|
+
}
|
|
154
253
|
return new ApiError(`SpecGuard answered ${status}${body.trim() === "" ? "" : `: ${body.trim().slice(0, 500)}`}`, status);
|
|
155
254
|
}
|
|
156
|
-
|
|
255
|
+
/**
|
|
256
|
+
* The sentence SpecGuard already wrote, or nothing.
|
|
257
|
+
*
|
|
258
|
+
* `Api::BaseController#render_bad_request` is a CONTRACT, not an ad-hoc body:
|
|
259
|
+
* `{error:, message:, details:}`, where `details` carries every validation
|
|
260
|
+
* failure and `message` repeats the first "so a client that reads only the two
|
|
261
|
+
* conventional keys still learns which spec is at fault". Both callers of it on
|
|
262
|
+
* `origin/main` route here, so this branch serves the API surface rather than
|
|
263
|
+
* one tool.
|
|
264
|
+
*
|
|
265
|
+
* SURFACING IT IS THE OPPOSITE OF RESHAPING IT. The generic branch below turns
|
|
266
|
+
* the most useful sentence in this direction —
|
|
267
|
+
*
|
|
268
|
+
* "cannot be registered from an API key — SpecGuard has no current record of
|
|
269
|
+
* your GitHub permissions. Sign in to SpecGuard in a browser and reconnect
|
|
270
|
+
* GitHub, then try again."
|
|
271
|
+
*
|
|
272
|
+
* — into a JSON blob glued to "SpecGuard answered 400" and truncated at 500
|
|
273
|
+
* characters. That sentence names the operator's exact next move, and it is the
|
|
274
|
+
* MODAL first answer this endpoint gives: `GrantVerifier` fails closed on a
|
|
275
|
+
* missing or stale grant, which is every person who has not opened SpecGuard in
|
|
276
|
+
* a browser since the feature shipped. `:not_administered`, `:not_in_installation`
|
|
277
|
+
* and "has already been taken" arrive the same way. This branch does not author
|
|
278
|
+
* a sentence the way the 401 and 404 branches must — it stops DISCARDING one.
|
|
279
|
+
*
|
|
280
|
+
* Returns `undefined` rather than a fallback string, so the decision about what
|
|
281
|
+
* to say when the body is not that shape stays in one place. A 400 from
|
|
282
|
+
* somewhere that is not this contract — a proxy's HTML, a bare string, JSON
|
|
283
|
+
* whose `message` is absent or is not a string — still gets the generic
|
|
284
|
+
* sentence, which at least shows the operator what actually came back.
|
|
285
|
+
*/
|
|
286
|
+
function badRequestMessage(body) {
|
|
287
|
+
let parsed;
|
|
288
|
+
try {
|
|
289
|
+
parsed = JSON.parse(body);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
295
|
+
return undefined;
|
|
296
|
+
const message = parsed["message"];
|
|
297
|
+
if (typeof message !== "string" || message.trim() === "")
|
|
298
|
+
return undefined;
|
|
299
|
+
return `SpecGuard refused the request (400): ${message.trim()}`;
|
|
300
|
+
}
|
|
301
|
+
export { requireApiConfig, requireUserApiConfig };
|
|
157
302
|
//# sourceMappingURL=specguard-api.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specguard-api.js","sourceRoot":"","sources":["../../../src/support/specguard-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAkB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"specguard-api.js","sourceRoot":"","sources":["../../../src/support/specguard-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kill the runs we started before going away.
|
|
3
|
+
*
|
|
4
|
+
* `run-command.ts` spawns every run `detached`, which is what lets a timeout
|
|
5
|
+
* signal the whole process tree rather than only the process we forked. The
|
|
6
|
+
* cost is that a detached child is in its own session, outside this server's
|
|
7
|
+
* controlling terminal, so a signal aimed at OUR group — an interactive Ctrl-C,
|
|
8
|
+
* a supervisor's `kill -- -PGID` — does not reach a lint run in flight. Without
|
|
9
|
+
* the handlers below such a run is not merely orphaned but UNBOUNDED: the
|
|
10
|
+
* `DEFAULT_COMMAND_TIMEOUT_MS` ceiling is a parent-side timer, so killing the
|
|
11
|
+
* parent destroys the only thing that was ever going to stop it, and a lint of a
|
|
12
|
+
* 20k-example suite goes on burning CPU with nobody waiting for its answer.
|
|
13
|
+
*
|
|
14
|
+
* == Why this is not written inline in `bin/specguard-mcp.ts`
|
|
15
|
+
*
|
|
16
|
+
* It lives here so a test can install the REAL handler. `bin/` runs `main()` on
|
|
17
|
+
* import, so a test that imported it would connect a transport rather than
|
|
18
|
+
* exercise a teardown, and the alternative — retyping the handler inside a test
|
|
19
|
+
* fixture — would assert that a COPY of the logic works while the shipped one
|
|
20
|
+
* went unread. `bin/` is left as the one place the policy is applied, which is
|
|
21
|
+
* the same split it already makes for the transport.
|
|
22
|
+
*
|
|
23
|
+
* DIAGNOSTICS GO TO STDERR, without exception. On stdio, stdout IS the JSON-RPC
|
|
24
|
+
* protocol channel: a line written there is framed as a message on the way out
|
|
25
|
+
* and corrupts the stream the client is still reading, surfacing as an
|
|
26
|
+
* unexplained disconnect rather than as the shutdown it actually was.
|
|
27
|
+
*
|
|
28
|
+
* NOTHING HERE WAITS. `killOutstandingRuns` sends SIGKILL and returns; SIGKILL
|
|
29
|
+
* cannot be refused, so there is no acknowledgement worth blocking a shutdown
|
|
30
|
+
* for. A teardown path that waits is a teardown path that can hang, which is the
|
|
31
|
+
* failure this handler exists to prevent rather than one to introduce.
|
|
32
|
+
*/
|
|
33
|
+
export declare function installTeardown(): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { killOutstandingRuns } from "./run-command.js";
|
|
2
|
+
/**
|
|
3
|
+
* Kill the runs we started before going away.
|
|
4
|
+
*
|
|
5
|
+
* `run-command.ts` spawns every run `detached`, which is what lets a timeout
|
|
6
|
+
* signal the whole process tree rather than only the process we forked. The
|
|
7
|
+
* cost is that a detached child is in its own session, outside this server's
|
|
8
|
+
* controlling terminal, so a signal aimed at OUR group — an interactive Ctrl-C,
|
|
9
|
+
* a supervisor's `kill -- -PGID` — does not reach a lint run in flight. Without
|
|
10
|
+
* the handlers below such a run is not merely orphaned but UNBOUNDED: the
|
|
11
|
+
* `DEFAULT_COMMAND_TIMEOUT_MS` ceiling is a parent-side timer, so killing the
|
|
12
|
+
* parent destroys the only thing that was ever going to stop it, and a lint of a
|
|
13
|
+
* 20k-example suite goes on burning CPU with nobody waiting for its answer.
|
|
14
|
+
*
|
|
15
|
+
* == Why this is not written inline in `bin/specguard-mcp.ts`
|
|
16
|
+
*
|
|
17
|
+
* It lives here so a test can install the REAL handler. `bin/` runs `main()` on
|
|
18
|
+
* import, so a test that imported it would connect a transport rather than
|
|
19
|
+
* exercise a teardown, and the alternative — retyping the handler inside a test
|
|
20
|
+
* fixture — would assert that a COPY of the logic works while the shipped one
|
|
21
|
+
* went unread. `bin/` is left as the one place the policy is applied, which is
|
|
22
|
+
* the same split it already makes for the transport.
|
|
23
|
+
*
|
|
24
|
+
* DIAGNOSTICS GO TO STDERR, without exception. On stdio, stdout IS the JSON-RPC
|
|
25
|
+
* protocol channel: a line written there is framed as a message on the way out
|
|
26
|
+
* and corrupts the stream the client is still reading, surfacing as an
|
|
27
|
+
* unexplained disconnect rather than as the shutdown it actually was.
|
|
28
|
+
*
|
|
29
|
+
* NOTHING HERE WAITS. `killOutstandingRuns` sends SIGKILL and returns; SIGKILL
|
|
30
|
+
* cannot be refused, so there is no acknowledgement worth blocking a shutdown
|
|
31
|
+
* for. A teardown path that waits is a teardown path that can hang, which is the
|
|
32
|
+
* failure this handler exists to prevent rather than one to introduce.
|
|
33
|
+
*/
|
|
34
|
+
export function installTeardown() {
|
|
35
|
+
let tearingDown = false;
|
|
36
|
+
const teardown = (signal, status) => {
|
|
37
|
+
// A second Ctrl-C while the first is still unwinding must not re-enter the
|
|
38
|
+
// drain — the registry is already empty and the pids in it already spent.
|
|
39
|
+
if (tearingDown)
|
|
40
|
+
return;
|
|
41
|
+
tearingDown = true;
|
|
42
|
+
const killed = killOutstandingRuns();
|
|
43
|
+
if (killed > 0) {
|
|
44
|
+
process.stderr.write(`specguard-mcp: ${signal} received, killed ${killed} run${killed === 1 ? "" : "s"} still in flight\n`);
|
|
45
|
+
}
|
|
46
|
+
// The conventional 128 + signo, so a supervisor reads "died on SIGINT"
|
|
47
|
+
// rather than an ordinary failure. Exiting explicitly rather than restoring
|
|
48
|
+
// the default disposition and re-signalling ourselves: we hold no other
|
|
49
|
+
// teardown obligation, and an explicit status cannot be lost to a handler
|
|
50
|
+
// installed elsewhere.
|
|
51
|
+
process.exit(status);
|
|
52
|
+
};
|
|
53
|
+
process.on("SIGINT", () => teardown("SIGINT", 130));
|
|
54
|
+
process.on("SIGTERM", () => teardown("SIGTERM", 143));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=teardown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown.js","sourceRoot":"","sources":["../../../src/support/teardown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,WAAW,GAAG,KAAK,CAAC;IAExB,MAAM,QAAQ,GAAG,CAAC,MAA4B,EAAE,MAAc,EAAE,EAAE;QAChE,2EAA2E;QAC3E,0EAA0E;QAC1E,IAAI,WAAW;YAAE,OAAO;QACxB,WAAW,GAAG,IAAI,CAAC;QAEnB,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAC;QAErC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;YACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kBAAkB,MAAM,qBAAqB,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,oBAAoB,CACtG,CAAC;QACJ,CAAC;QAED,uEAAuE;QACvE,4EAA4E;QAC5E,wEAAwE;QACxE,0EAA0E;QAC1E,uBAAuB;QACvB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC"}
|