specguard-mcp 0.1.2 → 0.1.4
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 +183 -17
- package/dist/bin/specguard-mcp.js +5 -0
- package/dist/bin/specguard-mcp.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 +50 -0
- package/dist/src/support/specguard-api.js +148 -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/create-repository-api-key.d.ts +26 -0
- package/dist/src/tools/create-repository-api-key.js +83 -0
- package/dist/src/tools/create-repository-api-key.js.map +1 -0
- package/dist/src/tools/index.d.ts +61 -7
- package/dist/src/tools/index.js +71 -7
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-repositories.d.ts +14 -7
- package/dist/src/tools/list-repositories.js +14 -7
- package/dist/src/tools/list-repositories.js.map +1 -1
- package/dist/src/tools/registrable-repositories.d.ts +51 -0
- package/dist/src/tools/registrable-repositories.js +92 -0
- package/dist/src/tools/registrable-repositories.js.map +1 -0
- package/dist/src/tools/remove-repository.d.ts +33 -0
- package/dist/src/tools/remove-repository.js +81 -0
- package/dist/src/tools/remove-repository.js.map +1 -0
- package/dist/src/tools/repository-overview.js +33 -8
- package/dist/src/tools/repository-overview.js.map +1 -1
- package/dist/src/tools/revoke-repository-api-key.d.ts +29 -0
- package/dist/src/tools/revoke-repository-api-key.js +85 -0
- package/dist/src/tools/revoke-repository-api-key.js.map +1 -0
- package/package.json +1 -1
|
@@ -14,7 +14,81 @@ 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
|
+
* `DELETE` — the destructive half of the transport, and deliberately the SAME
|
|
43
|
+
* function underneath `postJson` rather than beside it, for the reason
|
|
44
|
+
* `postJson`'s header states: everything expensive about this module is about
|
|
45
|
+
* the deadline, not the verb.
|
|
46
|
+
*
|
|
47
|
+
* Returns the RAW BODY TEXT rather than a parsed value, because the endpoints
|
|
48
|
+
* this serves answer `204` with NO body at all — the one response in the `sgu_`
|
|
49
|
+
* surface that is deliberately not JSON. `requestJson` JSON-parses every 2xx it
|
|
50
|
+
* sees, so routing a `204` through it would turn a successful delete into
|
|
51
|
+
* "answered 204 but the body was not JSON" — the trap this verb specifically
|
|
52
|
+
* introduces, and the reason the DELETE path has its own success handling
|
|
53
|
+
* instead of sharing `requestJson`'s. The status check and the
|
|
54
|
+
* "reached and refused" hand-off to `describeFailure` are still shared
|
|
55
|
+
* verbatim: only what happens to a SUCCESS body differs.
|
|
56
|
+
*/
|
|
57
|
+
export async function deleteJson(api, path, fetchImpl) {
|
|
58
|
+
const { response, body } = await fetchWithTimeout(new URL(`${api.endpoint}${path}`), api, fetchImpl, { method: "DELETE" });
|
|
59
|
+
if (!response.ok)
|
|
60
|
+
throw describeFailure(response.status, body, api);
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `postJson`, narrowed exactly as `getJsonObject` narrows `getJson`.
|
|
65
|
+
*
|
|
66
|
+
* The write path needs the same guard for the same reason, and the reason is not
|
|
67
|
+
* about reading: `ToolResult.structured` is a `Record<string, unknown>`, so a
|
|
68
|
+
* body that is an array or a bare scalar is not something a tool can pass
|
|
69
|
+
* through whichever verb fetched it. Shipping only the raw `postJson` would
|
|
70
|
+
* leave the first write tool to re-type the three-clause check and its sentence
|
|
71
|
+
* — which is precisely the duplication `getJsonObject`'s header says no tool
|
|
72
|
+
* should have to repeat.
|
|
73
|
+
*
|
|
74
|
+
* The pair is mirrored rather than collapsed for the reason the read pair is:
|
|
75
|
+
* `postJson` stays exported un-narrowed for an endpoint that legitimately
|
|
76
|
+
* answers with an array.
|
|
77
|
+
*/
|
|
78
|
+
export async function postJsonObject(api, path, body, fetchImpl) {
|
|
79
|
+
return asJsonObject(await postJson(api, path, body, fetchImpl));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Everything both verbs do with a response, in one place.
|
|
83
|
+
*
|
|
84
|
+
* Extracted when the write path landed rather than copied into it: the status
|
|
85
|
+
* check, the "reached and refused" hand-off to `describeFailure` and the
|
|
86
|
+
* not-JSON sentence are identical for a `GET` and a `POST`, and the not-JSON
|
|
87
|
+
* sentence in particular is a diagnosis an operator acts on — a second copy is a
|
|
88
|
+
* second wording waiting to drift from this one.
|
|
89
|
+
*/
|
|
90
|
+
async function requestJson(url, api, fetchImpl, request) {
|
|
91
|
+
const { response, body } = await fetchWithTimeout(url, api, fetchImpl, request);
|
|
18
92
|
if (!response.ok)
|
|
19
93
|
throw describeFailure(response.status, body, api);
|
|
20
94
|
try {
|
|
@@ -26,6 +100,13 @@ export async function getJson(api, path, query, fetchImpl) {
|
|
|
26
100
|
"or login page.", response.status);
|
|
27
101
|
}
|
|
28
102
|
}
|
|
103
|
+
/** The three-clause guard both `*JsonObject` narrowings share. */
|
|
104
|
+
function asJsonObject(body) {
|
|
105
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
106
|
+
throw new ApiError("SpecGuard returned a JSON value that was not an object.");
|
|
107
|
+
}
|
|
108
|
+
return body;
|
|
109
|
+
}
|
|
29
110
|
/**
|
|
30
111
|
* `getJson`, narrowed to the object every tool here actually asks it for.
|
|
31
112
|
*
|
|
@@ -44,11 +125,7 @@ export async function getJson(api, path, query, fetchImpl) {
|
|
|
44
125
|
* are the only legal body, it is that no tool re-types this guard.
|
|
45
126
|
*/
|
|
46
127
|
export async function getJsonObject(api, path, query, fetchImpl) {
|
|
47
|
-
|
|
48
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
49
|
-
throw new ApiError("SpecGuard returned a JSON value that was not an object.");
|
|
50
|
-
}
|
|
51
|
-
return body;
|
|
128
|
+
return asJsonObject(await getJson(api, path, query, fetchImpl));
|
|
52
129
|
}
|
|
53
130
|
/**
|
|
54
131
|
* Tells "the deadline won the race" apart from any value a phase could produce.
|
|
@@ -89,7 +166,7 @@ const TIMED_OUT = Symbol("specguard-api deadline");
|
|
|
89
166
|
* frame later, so there is no window in which the read is awaiting somewhere the
|
|
90
167
|
* timer does not reach.
|
|
91
168
|
*/
|
|
92
|
-
async function fetchWithTimeout(url, api, fetchImpl) {
|
|
169
|
+
async function fetchWithTimeout(url, api, fetchImpl, request) {
|
|
93
170
|
const controller = new AbortController();
|
|
94
171
|
let timer;
|
|
95
172
|
const deadline = new Promise((resolve) => {
|
|
@@ -104,12 +181,17 @@ async function fetchWithTimeout(url, api, fetchImpl) {
|
|
|
104
181
|
try {
|
|
105
182
|
const response = await Promise.race([
|
|
106
183
|
fetchImpl(url, {
|
|
107
|
-
method:
|
|
184
|
+
method: request.method,
|
|
108
185
|
headers: {
|
|
109
186
|
Authorization: `Bearer ${api.apiKey}`,
|
|
110
187
|
Accept: "application/json",
|
|
111
188
|
"User-Agent": "specguard-mcp",
|
|
189
|
+
// Sent only when there IS a body. A `Content-Type` on a GET announces
|
|
190
|
+
// a payload that is not there, and some deployments and proxies treat
|
|
191
|
+
// that as a malformed request rather than as a harmless header.
|
|
192
|
+
...(request.body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
112
193
|
},
|
|
194
|
+
...(request.body === undefined ? {} : { body: request.body }),
|
|
113
195
|
signal: controller.signal,
|
|
114
196
|
}),
|
|
115
197
|
deadline,
|
|
@@ -185,7 +267,65 @@ function describeFailure(status, body, api) {
|
|
|
185
267
|
return new ApiError(`${api.endpoint} has no such endpoint (404). Check that ${api.endpointVariable} is the ` +
|
|
186
268
|
"deployment's root URL, without a path.", status);
|
|
187
269
|
}
|
|
270
|
+
if (status === 400 || status === 403) {
|
|
271
|
+
const message = refusalMessage(body, status);
|
|
272
|
+
if (message !== undefined)
|
|
273
|
+
return new ApiError(message, status);
|
|
274
|
+
}
|
|
188
275
|
return new ApiError(`SpecGuard answered ${status}${body.trim() === "" ? "" : `: ${body.trim().slice(0, 500)}`}`, status);
|
|
189
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* The sentence SpecGuard already wrote, or nothing.
|
|
279
|
+
*
|
|
280
|
+
* `Api::BaseController#render_bad_request` is a CONTRACT, not an ad-hoc body:
|
|
281
|
+
* `{error:, message:, details:}`, where `details` carries every validation
|
|
282
|
+
* failure and `message` repeats the first "so a client that reads only the two
|
|
283
|
+
* conventional keys still learns which spec is at fault". Both callers of it on
|
|
284
|
+
* `origin/main` route here, so this branch serves the API surface rather than
|
|
285
|
+
* one tool.
|
|
286
|
+
*
|
|
287
|
+
* The 403 is the same shape under another status. `UserRepositoriesController#
|
|
288
|
+
* render_not_granted` renders `{error: "not_granted", message:, grant:}` — the
|
|
289
|
+
* `grant` block is simply ignored by the extractor, exactly as `details` is.
|
|
290
|
+
* Same defect (the generic branch truncating the one sentence that names the
|
|
291
|
+
* fix), same remedy — which is why the helper is ONE function parameterised on
|
|
292
|
+
* the status rather than two copies beside each other.
|
|
293
|
+
*
|
|
294
|
+
* SURFACING IT IS THE OPPOSITE OF RESHAPING IT. The generic branch below turns
|
|
295
|
+
* the most useful sentence in this direction —
|
|
296
|
+
*
|
|
297
|
+
* "cannot be registered from an API key — SpecGuard has no current record of
|
|
298
|
+
* your GitHub permissions. Sign in to SpecGuard in a browser and reconnect
|
|
299
|
+
* GitHub, then try again."
|
|
300
|
+
*
|
|
301
|
+
* — into a JSON blob glued to "SpecGuard answered 400" and truncated at 500
|
|
302
|
+
* characters. That sentence names the operator's exact next move, and it is the
|
|
303
|
+
* MODAL first answer this endpoint gives: `GrantVerifier` fails closed on a
|
|
304
|
+
* missing or stale grant, which is every person who has not opened SpecGuard in
|
|
305
|
+
* a browser since the feature shipped. `:not_administered`, `:not_in_installation`
|
|
306
|
+
* and "has already been taken" arrive the same way. This branch does not author
|
|
307
|
+
* a sentence the way the 401 and 404 branches must — it stops DISCARDING one.
|
|
308
|
+
*
|
|
309
|
+
* Returns `undefined` rather than a fallback string, so the decision about what
|
|
310
|
+
* to say when the body is not that shape stays in one place. A 400 from
|
|
311
|
+
* somewhere that is not this contract — a proxy's HTML, a bare string, JSON
|
|
312
|
+
* whose `message` is absent or is not a string — still gets the generic
|
|
313
|
+
* sentence, which at least shows the operator what actually came back.
|
|
314
|
+
*/
|
|
315
|
+
function refusalMessage(body, status) {
|
|
316
|
+
let parsed;
|
|
317
|
+
try {
|
|
318
|
+
parsed = JSON.parse(body);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
324
|
+
return undefined;
|
|
325
|
+
const message = parsed["message"];
|
|
326
|
+
if (typeof message !== "string" || message.trim() === "")
|
|
327
|
+
return undefined;
|
|
328
|
+
return `SpecGuard refused the request (${status}): ${message.trim()}`;
|
|
329
|
+
}
|
|
190
330
|
export { requireApiConfig, requireUserApiConfig };
|
|
191
331
|
//# 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,EAAE,oBAAoB,EAAkB,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,GAAc,EACd,IAAY,EACZ,KAAyC,EACzC,SAAkC;IAElC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC5D,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"specguard-api.js","sourceRoot":"","sources":["../../../src/support/specguard-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAkB,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,GAAc,EACd,IAAY,EACZ,KAAyC,EACzC,SAAkC;IAElC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,GAAc,EACd,IAAY,EACZ,IAA6B,EAC7B,SAAkC;IAElC,OAAO,WAAW,CAAC,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE;QACpE,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,GAAc,EACd,IAAY,EACZ,SAAkC;IAElC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,gBAAgB,CAC/C,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,EACjC,GAAG,EACH,SAAS,EACT,EAAE,MAAM,EAAE,QAAQ,EAAE,CACrB,CAAC;IAEF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAEpE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAc,EACd,IAAY,EACZ,IAA6B,EAC7B,SAAkC;IAElC,OAAO,YAAY,CAAC,MAAM,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,WAAW,CACxB,GAAQ,EACR,GAAc,EACd,SAAkC,EAClC,OAAoB;IAEpB,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAEhF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAEpE,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,QAAQ,CAChB,GAAG,GAAG,CAAC,QAAQ,aAAa,QAAQ,CAAC,MAAM,8BAA8B;YACvE,cAAc,GAAG,CAAC,gBAAgB,0DAA0D;YAC5F,gBAAgB,EAClB,QAAQ,CAAC,MAAM,CAChB,CAAC;IACJ,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,SAAS,YAAY,CAAC,IAAa;IACjC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,QAAQ,CAAC,yDAAyD,CAAC,CAAC;IAChF,CAAC;IAED,OAAO,IAA+B,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAc,EACd,IAAY,EACZ,KAAyC,EACzC,SAAkC;IAElC,OAAO,YAAY,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;AAClE,CAAC;AAQD;;;;;;;GAOG;AACH,MAAM,SAAS,GAAG,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAgBnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,KAAK,UAAU,gBAAgB,CAC7B,GAAQ,EACR,GAAc,EACd,SAAkC,EAClC,OAAoB;IAEpB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,IAAI,KAAgD,CAAC;IAErD,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAmB,CAAC,OAAO,EAAE,EAAE;QACzD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,UAAU,CAAC,KAAK,EAAE,CAAC;YACnB,OAAO,CAAC,SAAS,CAAC,CAAC;QACrB,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACzB,6EAA6E;QAC7E,6EAA6E;QAC7E,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAClC,SAAS,CAAC,GAAG,EAAE;gBACb,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE;oBACrC,MAAM,EAAE,kBAAkB;oBAC1B,YAAY,EAAE,eAAe;oBAC7B,sEAAsE;oBACtE,sEAAsE;oBACtE,gEAAgE;oBAChE,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;iBAC9E;gBACD,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;gBAC7D,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC;YACF,QAAQ;SACT,CAAC,CAAC;QACH,IAAI,QAAQ,KAAK,SAAS;YAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QAEhD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC7D,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QAE5C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,6EAA6E;QAC7E,0EAA0E;QAC1E,4EAA4E;QAC5E,6EAA6E;QAC7E,IAAI,KAAK,YAAY,QAAQ;YAAE,MAAM,KAAK,CAAC;QAE3C,0EAA0E;QAC1E,0EAA0E;QAC1E,6EAA6E;QAC7E,uEAAuE;QACvE,8DAA8D;QAC9D,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QAEnD,MAAM,IAAI,QAAQ,CAChB,mBAAmB,GAAG,CAAC,QAAQ,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI;YAC5F,SAAS,GAAG,CAAC,gBAAgB,0DAA0D,CAC1F,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,uEAAuE;QACvE,wEAAwE;QACxE,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,QAAQ,CAAC,GAAc;IAC9B,OAAO,IAAI,QAAQ,CAAC,GAAG,GAAG,CAAC,QAAQ,2BAA2B,GAAG,CAAC,gBAAgB,KAAK,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,eAAe,CAAC,MAAc,EAAE,IAAY,EAAE,GAAc;IACnE,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;QAEvD,OAAO,IAAI,QAAQ,CACjB,yCAAyC,QAAQ,eAAe,MAAM,kBAAkB;YACtF,GAAG,GAAG,CAAC,QAAQ,IAAI,SAAS,GAAG,EACjC,MAAM,CACP,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,OAAO,IAAI,QAAQ,CACjB,GAAG,GAAG,CAAC,QAAQ,2CAA2C,GAAG,CAAC,gBAAgB,UAAU;YACtF,wCAAwC,EAC1C,MAAM,CACP,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7C,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,IAAI,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,IAAI,QAAQ,CACjB,sBAAsB,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,EAC3F,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,SAAS,cAAc,CAAC,IAAY,EAAE,MAAc;IAClD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAE7F,MAAM,OAAO,GAAI,MAAkC,CAAC,SAAS,CAAC,CAAC;IAC/D,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAE3E,OAAO,kCAAkC,MAAM,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;AACxE,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `POST /api/v1/repositories` as a tool — shipped today in the platform
|
|
4
|
+
* (`specguard/config/routes.rb`, `Api::V1::UserRepositoriesController#create`).
|
|
5
|
+
*
|
|
6
|
+
* == The first tool here that WRITES, and what that changed
|
|
7
|
+
*
|
|
8
|
+
* Everything before this read. The registry's standing rule is that a tool in
|
|
9
|
+
* `tools/list` is a promise an agent will act on, and this endpoint has existed
|
|
10
|
+
* on the platform for some time — what was missing was on THIS side: `getJson`
|
|
11
|
+
* hardcoded `method: "GET"` and took no body. `tools/index.ts` and
|
|
12
|
+
* `list-repositories.ts` both said so, and said the transport should land with
|
|
13
|
+
* the first write tool so it could be designed against a real request body and a
|
|
14
|
+
* real 4xx surface rather than invented for a caller that did not exist. This is
|
|
15
|
+
* that tool, and `postJson`/`postJsonObject` are that transport.
|
|
16
|
+
*
|
|
17
|
+
* == The request body is top-level, because the caller is an agent
|
|
18
|
+
*
|
|
19
|
+
* `{"github_full_name": "org/repo"}`, not `{"repository": {…}}`. The controller
|
|
20
|
+
* permits it that way and says why: this is a JSON API being driven by an agent,
|
|
21
|
+
* not a Rails form being submitted by a browser, and the top-level shape is what
|
|
22
|
+
* a caller writing curl by hand will send.
|
|
23
|
+
*
|
|
24
|
+
* == The format is NOT re-validated here, deliberately
|
|
25
|
+
*
|
|
26
|
+
* `Repository` validates `org/repo` itself, and a refusal now arrives through
|
|
27
|
+
* the 400 branch in `describeFailure` in SpecGuard's own words. A second format
|
|
28
|
+
* rule on this side is exactly what "a thin client that reshapes its upstream is
|
|
29
|
+
* not thin" forbids — it would be a rule with no owner, free to drift from the
|
|
30
|
+
* one that actually decides, and its divergence would surface as this bridge
|
|
31
|
+
* refusing a name the platform would have accepted. Checking that `full_name` is
|
|
32
|
+
* a present, non-blank string is the whole of the bridge's business: that is a
|
|
33
|
+
* shape check, which is why it is `requireString` from `args.ts` and not a
|
|
34
|
+
* hand-rolled one here.
|
|
35
|
+
*
|
|
36
|
+
* == The MODAL first answer is a 400, and it is the useful one
|
|
37
|
+
*
|
|
38
|
+
* `RepositoryRegistration::GrantVerifier` fails closed on a grant that is
|
|
39
|
+
* missing or stale, which is every person who has not opened SpecGuard in a
|
|
40
|
+
* browser since this shipped — the controller records that this is "an ordinary
|
|
41
|
+
* state and not an error". The sentence that comes back names the operator's
|
|
42
|
+
* exact next move (sign in, reconnect GitHub, retry), and reaching the agent
|
|
43
|
+
* intact is what the 400 branch in `specguard-api.ts` is for.
|
|
44
|
+
*
|
|
45
|
+
* == Why the description carries a hazard paragraph
|
|
46
|
+
*
|
|
47
|
+
* `types.ts` calls the description "prompt material, not documentation … the
|
|
48
|
+
* entire basis on which a model decides whether to call the tool". This tool is
|
|
49
|
+
* NOT idempotent and its 201 carries a reveal-once token, so an agent that
|
|
50
|
+
* learns those facts by hitting them has already lost the token. They are stated
|
|
51
|
+
* where they are read BEFORE the call is committed to, rather than left to be
|
|
52
|
+
* discovered from a failure.
|
|
53
|
+
*/
|
|
54
|
+
declare const addRepository: ToolDefinition;
|
|
55
|
+
export default addRepository;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { postJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
|
|
2
|
+
import { requireString } from "./args.js";
|
|
3
|
+
/**
|
|
4
|
+
* `POST /api/v1/repositories` as a tool — shipped today in the platform
|
|
5
|
+
* (`specguard/config/routes.rb`, `Api::V1::UserRepositoriesController#create`).
|
|
6
|
+
*
|
|
7
|
+
* == The first tool here that WRITES, and what that changed
|
|
8
|
+
*
|
|
9
|
+
* Everything before this read. The registry's standing rule is that a tool in
|
|
10
|
+
* `tools/list` is a promise an agent will act on, and this endpoint has existed
|
|
11
|
+
* on the platform for some time — what was missing was on THIS side: `getJson`
|
|
12
|
+
* hardcoded `method: "GET"` and took no body. `tools/index.ts` and
|
|
13
|
+
* `list-repositories.ts` both said so, and said the transport should land with
|
|
14
|
+
* the first write tool so it could be designed against a real request body and a
|
|
15
|
+
* real 4xx surface rather than invented for a caller that did not exist. This is
|
|
16
|
+
* that tool, and `postJson`/`postJsonObject` are that transport.
|
|
17
|
+
*
|
|
18
|
+
* == The request body is top-level, because the caller is an agent
|
|
19
|
+
*
|
|
20
|
+
* `{"github_full_name": "org/repo"}`, not `{"repository": {…}}`. The controller
|
|
21
|
+
* permits it that way and says why: this is a JSON API being driven by an agent,
|
|
22
|
+
* not a Rails form being submitted by a browser, and the top-level shape is what
|
|
23
|
+
* a caller writing curl by hand will send.
|
|
24
|
+
*
|
|
25
|
+
* == The format is NOT re-validated here, deliberately
|
|
26
|
+
*
|
|
27
|
+
* `Repository` validates `org/repo` itself, and a refusal now arrives through
|
|
28
|
+
* the 400 branch in `describeFailure` in SpecGuard's own words. A second format
|
|
29
|
+
* rule on this side is exactly what "a thin client that reshapes its upstream is
|
|
30
|
+
* not thin" forbids — it would be a rule with no owner, free to drift from the
|
|
31
|
+
* one that actually decides, and its divergence would surface as this bridge
|
|
32
|
+
* refusing a name the platform would have accepted. Checking that `full_name` is
|
|
33
|
+
* a present, non-blank string is the whole of the bridge's business: that is a
|
|
34
|
+
* shape check, which is why it is `requireString` from `args.ts` and not a
|
|
35
|
+
* hand-rolled one here.
|
|
36
|
+
*
|
|
37
|
+
* == The MODAL first answer is a 400, and it is the useful one
|
|
38
|
+
*
|
|
39
|
+
* `RepositoryRegistration::GrantVerifier` fails closed on a grant that is
|
|
40
|
+
* missing or stale, which is every person who has not opened SpecGuard in a
|
|
41
|
+
* browser since this shipped — the controller records that this is "an ordinary
|
|
42
|
+
* state and not an error". The sentence that comes back names the operator's
|
|
43
|
+
* exact next move (sign in, reconnect GitHub, retry), and reaching the agent
|
|
44
|
+
* intact is what the 400 branch in `specguard-api.ts` is for.
|
|
45
|
+
*
|
|
46
|
+
* == Why the description carries a hazard paragraph
|
|
47
|
+
*
|
|
48
|
+
* `types.ts` calls the description "prompt material, not documentation … the
|
|
49
|
+
* entire basis on which a model decides whether to call the tool". This tool is
|
|
50
|
+
* NOT idempotent and its 201 carries a reveal-once token, so an agent that
|
|
51
|
+
* learns those facts by hitting them has already lost the token. They are stated
|
|
52
|
+
* where they are read BEFORE the call is committed to, rather than left to be
|
|
53
|
+
* discovered from a failure.
|
|
54
|
+
*/
|
|
55
|
+
const addRepository = {
|
|
56
|
+
name: "add_repository",
|
|
57
|
+
title: "Add repository",
|
|
58
|
+
description: "Registers a GitHub repository with SpecGuard for the person behind this server's user API " +
|
|
59
|
+
"key, and returns the repository along with its first CI API key. " +
|
|
60
|
+
"Takes `full_name` as `org/repo` — the same handle `list_repositories` reports and every other " +
|
|
61
|
+
"SpecGuard surface names a repository by. " +
|
|
62
|
+
"On success the response carries a `repository` block (`id`, `full_name`, `name`, " +
|
|
63
|
+
"`registered_at`) and an `api_key` block (`name`, `token`, `hint`, `created_at`). " +
|
|
64
|
+
"⚠️ `api_key.token` is shown THIS ONCE AND NEVER AGAIN — nothing stores it and no endpoint can " +
|
|
65
|
+
"re-serve it, so hand it to the user in your reply rather than assuming it can be fetched " +
|
|
66
|
+
"later. " +
|
|
67
|
+
"⚠️ This tool is NOT idempotent and it WRITES. If the call times out (SPECGUARD_TIMEOUT_MS) " +
|
|
68
|
+
"the registration may still have succeeded on the server, taking its one-time token with it; " +
|
|
69
|
+
"retrying then fails with `has already been taken`, which is the honest answer, and the " +
|
|
70
|
+
"recovery is SpecGuard's API-keys page in a browser. Do not retry a timeout blindly. " +
|
|
71
|
+
"Requires a CURRENT record of the caller's GitHub permissions, which only a browser session " +
|
|
72
|
+
"creates: a person who has not signed in to SpecGuard and connected GitHub recently is refused " +
|
|
73
|
+
"with a message saying exactly that, and the fix is theirs to perform in a browser — no " +
|
|
74
|
+
"argument to this tool can substitute for it. The repository must also be one the SpecGuard " +
|
|
75
|
+
"GitHub App is installed on and that this person administers. " +
|
|
76
|
+
"Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `list_repositories` reads " +
|
|
77
|
+
"and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
full_name: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "The repository to register, as `org/repo` (for example `acme/billing`) — the same " +
|
|
84
|
+
"handle `list_repositories` reports. Not a URL and not a bare repository name. " +
|
|
85
|
+
"SpecGuard validates the format and refuses an unusable one in its own words.",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
required: ["full_name"],
|
|
89
|
+
// Closed for the reason `list_repositories` states: `server.ts` forwards
|
|
90
|
+
// `arguments` unvalidated, so an open schema would let a misspelled argument
|
|
91
|
+
// be dropped silently and the call answered as though it had been honoured.
|
|
92
|
+
// On a WRITE that is worse than on a read — the call still registers
|
|
93
|
+
// something, just not what the agent believed it was asking for.
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
},
|
|
96
|
+
async run(args, context) {
|
|
97
|
+
// Argument shape FIRST, before the config is resolved and before anything is
|
|
98
|
+
// sent: a malformed call is the one failure the agent can fix unaided, and
|
|
99
|
+
// it must not cost a write attempt to discover.
|
|
100
|
+
const fullName = requireString(args["full_name"], "full_name");
|
|
101
|
+
const api = requireUserApiConfig(context.config);
|
|
102
|
+
const registration = await postJsonObject(api, "/api/v1/repositories", { github_full_name: fullName }, context.fetch);
|
|
103
|
+
// Passed through exactly as `list_repositories` passes its listing through.
|
|
104
|
+
// It matters more here: `api_key.token` exists nowhere else, so any reshaping
|
|
105
|
+
// on this hop is a value that cannot be recovered rather than a field that
|
|
106
|
+
// can be re-fetched.
|
|
107
|
+
return {
|
|
108
|
+
text: JSON.stringify(registration, null, 2),
|
|
109
|
+
structured: registration,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
export default addRepository;
|
|
114
|
+
//# sourceMappingURL=add-repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-repository.js","sourceRoot":"","sources":["../../../src/tools/add-repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,aAAa,GAAmB;IACpC,IAAI,EAAE,gBAAgB;IACtB,KAAK,EAAE,gBAAgB;IACvB,WAAW,EACT,4FAA4F;QAC5F,mEAAmE;QACnE,gGAAgG;QAChG,2CAA2C;QAC3C,mFAAmF;QACnF,mFAAmF;QACnF,gGAAgG;QAChG,2FAA2F;QAC3F,SAAS;QACT,6FAA6F;QAC7F,8FAA8F;QAC9F,yFAAyF;QACzF,sFAAsF;QACtF,6FAA6F;QAC7F,gGAAgG;QAChG,yFAAyF;QACzF,6FAA6F;QAC7F,+DAA+D;QAC/D,6FAA6F;QAC7F,mFAAmF;IACrF,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,SAAS,EAAE;gBACT,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,oFAAoF;oBACpF,gFAAgF;oBAChF,8EAA8E;aACjF;SACF;QACD,QAAQ,EAAE,CAAC,WAAW,CAAC;QACvB,yEAAyE;QACzE,6EAA6E;QAC7E,4EAA4E;QAC5E,qEAAqE;QACrE,iEAAiE;QACjE,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,6EAA6E;QAC7E,2EAA2E;QAC3E,gDAAgD;QAChD,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC;QAE/D,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,YAAY,GAAG,MAAM,cAAc,CACvC,GAAG,EACH,sBAAsB,EACtB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAC9B,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,4EAA4E;QAC5E,8EAA8E;QAC9E,2EAA2E;QAC3E,qBAAqB;QACrB,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3C,UAAU,EAAE,YAAY;SACzB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
package/dist/src/tools/args.d.ts
CHANGED
|
@@ -45,4 +45,24 @@
|
|
|
45
45
|
* A non-blank string, or nothing.
|
|
46
46
|
*/
|
|
47
47
|
export declare function optionalString(value: unknown, field: string): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* A non-blank string, and nothing else will do.
|
|
50
|
+
*
|
|
51
|
+
* The mandatory counterpart of `optionalString`, and it belongs here for the
|
|
52
|
+
* reason stated above rather than in the first tool that needs one: "is it
|
|
53
|
+
* present and a non-blank string" is a check about the SHAPE of a value, whose
|
|
54
|
+
* failure is always `ArgumentError`. Hand-rolling it inside a tool is the exact
|
|
55
|
+
* copy-paste this file was created to end — and the copy would have to re-pick
|
|
56
|
+
* the error class, which is the one thing the two `optionalString` copies got
|
|
57
|
+
* wrong.
|
|
58
|
+
*
|
|
59
|
+
* Trimmed like its optional sibling, and for the same reason: a value an agent
|
|
60
|
+
* produced by concatenating strings arrives with whitespace that is not part of
|
|
61
|
+
* what it meant to send.
|
|
62
|
+
*
|
|
63
|
+
* The two refusals are separate sentences on purpose. "You sent a number" and
|
|
64
|
+
* "you sent nothing" are different mistakes with different fixes, and a single
|
|
65
|
+
* message covering both would leave the agent to work out which it made.
|
|
66
|
+
*/
|
|
67
|
+
export declare function requireString(value: unknown, field: string): string;
|
|
48
68
|
export declare function optionalBoolean(value: unknown, field: string): boolean | undefined;
|
package/dist/src/tools/args.js
CHANGED
|
@@ -56,6 +56,36 @@ export function optionalString(value, field) {
|
|
|
56
56
|
const trimmed = value.trim();
|
|
57
57
|
return trimmed === "" ? undefined : trimmed;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A non-blank string, and nothing else will do.
|
|
61
|
+
*
|
|
62
|
+
* The mandatory counterpart of `optionalString`, and it belongs here for the
|
|
63
|
+
* reason stated above rather than in the first tool that needs one: "is it
|
|
64
|
+
* present and a non-blank string" is a check about the SHAPE of a value, whose
|
|
65
|
+
* failure is always `ArgumentError`. Hand-rolling it inside a tool is the exact
|
|
66
|
+
* copy-paste this file was created to end — and the copy would have to re-pick
|
|
67
|
+
* the error class, which is the one thing the two `optionalString` copies got
|
|
68
|
+
* wrong.
|
|
69
|
+
*
|
|
70
|
+
* Trimmed like its optional sibling, and for the same reason: a value an agent
|
|
71
|
+
* produced by concatenating strings arrives with whitespace that is not part of
|
|
72
|
+
* what it meant to send.
|
|
73
|
+
*
|
|
74
|
+
* The two refusals are separate sentences on purpose. "You sent a number" and
|
|
75
|
+
* "you sent nothing" are different mistakes with different fixes, and a single
|
|
76
|
+
* message covering both would leave the agent to work out which it made.
|
|
77
|
+
*/
|
|
78
|
+
export function requireString(value, field) {
|
|
79
|
+
if (value === undefined || value === null) {
|
|
80
|
+
throw new ArgumentError(`\`${field}\` is required.`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof value !== "string")
|
|
83
|
+
throw new ArgumentError(`\`${field}\` must be a string.`);
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (trimmed === "")
|
|
86
|
+
throw new ArgumentError(`\`${field}\` must not be blank.`);
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
59
89
|
export function optionalBoolean(value, field) {
|
|
60
90
|
if (value === undefined || value === null)
|
|
61
91
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/tools/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,KAAa;IAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IACzF,8EAA8E;IAC9E,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,KAAa;IAC3D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/tools/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,KAAa;IAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IACzF,8EAA8E;IAC9E,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,KAAa;IACzD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,iBAAiB,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IAEzF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,EAAE;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAE/E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,KAAa;IAC3D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `POST /api/v1/repositories/:repository_id/api_keys` as a tool — shipped in
|
|
4
|
+
* the platform (`specguard/config/routes.rb:158`,
|
|
5
|
+
* `user_repository_api_keys_controller#create`, SPGD-754).
|
|
6
|
+
*
|
|
7
|
+
* == Reveal-once, again
|
|
8
|
+
*
|
|
9
|
+
* The 201 body carries `api_key.token` — the raw key, the only time it exists
|
|
10
|
+
* anywhere — exactly as `add_repository`'s does. The body is therefore passed
|
|
11
|
+
* through UNRESHAPED in both `text` and `structured` for the same reason that
|
|
12
|
+
* tool states: any reshaping on this hop is a value that cannot be recovered
|
|
13
|
+
* rather than a field that can be re-fetched. Recovery for a dropped token is
|
|
14
|
+
* minting another key — this same tool — because the platform ships no
|
|
15
|
+
* `#regenerate` and no re-serve.
|
|
16
|
+
*
|
|
17
|
+
* == The name is top-level and optional
|
|
18
|
+
*
|
|
19
|
+
* `params[:name]` defaults to `ApiKey::DEFAULT_NAME` server-side; `undefined`
|
|
20
|
+
* here means "let the server name it" and simply omits the key from the POST
|
|
21
|
+
* body. Not re-validated here for the reason `add_repository` states: a second
|
|
22
|
+
* format rule on this side is a rule with no owner, free to drift from the one
|
|
23
|
+
* that actually decides.
|
|
24
|
+
*/
|
|
25
|
+
declare const createRepositoryApiKey: ToolDefinition;
|
|
26
|
+
export default createRepositoryApiKey;
|