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.
@@ -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
- return spawn(program, [...args], {
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. Nothing in
244
- // `bin/specguard-mcp.ts` installs a SIGINT/SIGTERM handler to kill
245
- // outstanding children at teardown, so such a run is now orphaned where it
246
- // would previously have died alongside us. Worth it, because the failure
247
- // being traded away is an agent that never returns rather than a stray
248
- // process — but it is a trade, and a teardown handler is what would close
249
- // it.
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":"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"}
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
- export { requireApiConfig };
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
- const { response, body } = await fetchWithTimeout(url, api, fetchImpl);
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: "GET",
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
- return new ApiError("SpecGuard rejected the API key (401). SPECGUARD_API_KEY must be an sgk_… key issued by " +
147
- `${api.endpoint} for the repository you are asking about — keys are per-repository, and a ` +
148
- "revoked key reads the same as a wrong one.", status);
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
- export { requireApiConfig };
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":"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"}
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"}