specguard-mcp 0.1.3 → 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 CHANGED
@@ -32,9 +32,9 @@ refuses to boot and takes the tools that needed no configuration down with it.
32
32
 
33
33
  | Variable | Needed by | Default | What it is |
34
34
  | --- | --- | --- | --- |
35
- | `SPECGUARD_ENDPOINT` | `get_repository_overview`, `list_repositories` | — | your SpecGuard instance's root URL, **including the scheme** — e.g. `https://specguard.example.com`, or `http://localhost:3000`. A value with no scheme is refused by name (`SPECGUARD_ENDPOINT is not a usable URL: "sg.example.com"`) rather than surfacing later as an opaque failure. `SPECGUARD_URL` is accepted as an alias, and is the name every message uses when it is the one you set. A blank value counts as unset, so leaving `SPECGUARD_ENDPOINT` empty in a templated config falls through to `SPECGUARD_URL` instead of suppressing it |
35
+ | `SPECGUARD_ENDPOINT` | `get_repository_overview`, `list_repositories`, `add_repository`, `registrable_repositories` | — | your SpecGuard instance's root URL, **including the scheme** — e.g. `https://specguard.example.com`, or `http://localhost:3000`. A value with no scheme is refused by name (`SPECGUARD_ENDPOINT is not a usable URL: "sg.example.com"`) rather than surfacing later as an opaque failure. `SPECGUARD_URL` is accepted as an alias, and is the name every message uses when it is the one you set. A blank value counts as unset, so leaving `SPECGUARD_ENDPOINT` empty in a templated config falls through to `SPECGUARD_URL` instead of suppressing it |
36
36
  | `SPECGUARD_API_KEY` | `get_repository_overview` | — | an agent/CI API key (`sgk_…`) issued by that deployment |
37
- | `SPECGUARD_USER_API_KEY` | `list_repositories` | — | a **user** API key (`sgu_…`), minted from that deployment's account page. A different credential from the one above, not a second place to put the same value: SpecGuard decides which of them a request may use from the token's prefix, before it reads anything, and answers `401` for the other one. Set whichever your tools need — both, if you use both |
37
+ | `SPECGUARD_USER_API_KEY` | `list_repositories`, `add_repository`, `registrable_repositories`, `remove_repository`, `create_repository_api_key`, `revoke_repository_api_key` | — | a **user** API key (`sgu_…`), minted from that deployment's account page. A different credential from the one above, not a second place to put the same value: SpecGuard decides which of them a request may use from the token's prefix, before it reads anything, and answers `401` for the other one. Set whichever your tools need — both, if you use both |
38
38
  | `SPECGUARD_LINT_COMMAND` | `lint_intent_annotations` | `specguard-lint` | the command that runs the linter. Most Ruby projects need `bundle exec specguard-lint` |
39
39
  | `SPECGUARD_TIMEOUT_MS` | HTTP tools | `30000` | how long a call to SpecGuard may take |
40
40
 
@@ -351,10 +351,11 @@ decides which table is consulted before any of them is read — so the two are n
351
351
  setting one does not stand in for the other. Every message this tool produces names the variable
352
352
  *it* reads, so a `401` here never sends you to check the key `get_repository_overview` uses.
353
353
 
354
- Registering a repository is now `add_repository`, below — it reads the same `sgu_…` key and takes the
355
- `full_name` this tool reports. Revoking keys and the rest of the user-scoped surface are still absent,
356
- because their endpoints have not shipped: a tool here is a promise the agent will act on, so it waits
357
- for the capability rather than the other way round.
354
+ Registering a repository is `add_repository`, below — it reads the same `sgu_…` key and takes the
355
+ `full_name` this tool reports. Removal and the key lifecycle (`remove_repository`,
356
+ `create_repository_api_key`, `revoke_repository_api_key`) are below too, on the same key; a tool
357
+ here is a promise the agent will act on, so each waits for the capability rather than the other way
358
+ round.
358
359
 
359
360
  ### `add_repository`
360
361
 
@@ -398,6 +399,105 @@ as this bridge rejecting a name the platform would have accepted.
398
399
  It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and a
399
400
  different one from the `sgk_…` key `get_repository_overview` uses.
400
401
 
402
+ ### `registrable_repositories`
403
+
404
+ Lists the GitHub repositories the person behind `SPECGUARD_USER_API_KEY` **could** register with
405
+ SpecGuard — the set the registration gate would consult, read out loud in advance, so an agent can
406
+ pick a `full_name` for `add_repository` from a real answer rather than by guessing. `list_repositories`
407
+ reports what *is* registered; this reports what could be, and the two answer different questions.
408
+
409
+ The body comes back as SpecGuard serves it: `{"repositories": […]}` with each entry carrying
410
+ `full_name` and `registered`, ordered by `full_name` ascending, plus a `grant` block (`captured_at`,
411
+ `expires_at`, `stale`) describing the stored record of this person's GitHub permissions.
412
+
413
+ **`registered` is asked globally, not just of your own repositories.** An entry marked
414
+ `registered: true` was registered by *somebody* — possibly someone else — and a POST naming it will
415
+ be refused with `has already been taken`. That is exactly why entries are *marked* rather than
416
+ excluded: a reading scoped to what you can open would send you at a name nobody can register.
417
+
418
+ **A name appearing here is not a promise the write will succeed.** This is the set the gate would
419
+ consult at the moment of the read; the repository may be registered by someone else between this
420
+ call and your POST.
421
+
422
+ **A missing or stale grant is not an error — it is the modal first answer.** SpecGuard fails closed
423
+ when it has no current record of your GitHub permissions, which is every person who has not opened
424
+ SpecGuard in a browser recently. The call then answers `403` with SpecGuard's own sentence naming
425
+ the fix: *sign in to SpecGuard in a browser and reconnect GitHub, then try again*. On that refusal
426
+ the body still carries `grant`, and it distinguishes the two cases: `grant: null` means there never
427
+ was one (first-time setup), a populated grant with `stale: true` means an existing connection lapsed
428
+ — same remedy, very different urgency. Read it before telling the person what to do.
429
+
430
+ **This tool takes no arguments** — the credential is the whole of the scope. The endpoint takes no
431
+ parameters; which repositories are in the answer is decided by SpecGuard from the person the key
432
+ speaks for.
433
+
434
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
435
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
436
+
437
+ ### `remove_repository`
438
+
439
+ Removes a repository from SpecGuard — and with it **every key, run and intent on it**. This is the
440
+ destructive gesture in this toolset: irreversible, no undo, and a `204` means the repository and its
441
+ history are gone short of re-registering from scratch. Confirm with the user before calling.
442
+
443
+ | argument | |
444
+ | --- | --- |
445
+ | `repository_id` | the repository to remove — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
446
+
447
+ Authorization is the `repo.delete` capability at **either surface** — an owner, or a member granted
448
+ `repo.delete`, may remove the repository. A member without it is refused `403` with SpecGuard's own
449
+ sentence, verbatim. The repository's CI keys stop authenticating the moment it succeeds.
450
+
451
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
452
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
453
+
454
+ ### `create_repository_api_key`
455
+
456
+ Mints a new CI API key (`sgk_…`) for a SpecGuard repository and returns it alongside the
457
+ repository's existing keys. Minting does not disturb existing keys — each key on a repository
458
+ authenticates independently until revoked.
459
+
460
+ | argument | |
461
+ | --- | --- |
462
+ | `repository_id` | the repository to mint the key for — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
463
+ | `name` | an optional label for the key; omit it to let SpecGuard use its default name |
464
+
465
+ The body comes back as SpecGuard serves it: an `api_key` block (`name`, `token`, `hint`,
466
+ `created_at`) — the same shape `add_repository` serves.
467
+
468
+ > ⚠️ **`api_key.token` is shown once and never again.** Nothing stores it and no endpoint can
469
+ > re-serve it. Hand it to the person you are working for in your reply. If it is dropped, the
470
+ > recovery is minting another key with this same tool — the platform has no regenerate — then
471
+ > revoking the orphaned one with `revoke_repository_api_key`.
472
+
473
+ Authorization is the `keys_manage` capability; a member without it is refused `403` with SpecGuard's
474
+ own sentence, verbatim.
475
+
476
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
477
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
478
+
479
+ ### `revoke_repository_api_key`
480
+
481
+ Revokes one CI API key on a SpecGuard repository. The key stops authenticating immediately; every
482
+ **other** key on the repository keeps working, so CI keeps ingesting if it holds a surviving key.
483
+
484
+ | argument | |
485
+ | --- | --- |
486
+ | `repository_id` | the repository the key belongs to — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
487
+ | `key_id` | the id of the key to revoke, as served in the `api_key` block of `add_repository` or `create_repository_api_key` |
488
+
489
+ The `key_id` is scoped to `repository_id`: a key id belonging to a different repository is refused
490
+ `404`, never a cross-repository delete. Authorization is the `keys_manage` capability; a member
491
+ without it is refused `403` with SpecGuard's own sentence, verbatim.
492
+
493
+ **Key rotation is mint-then-revoke, in that order.** The platform has no regenerate, so mint a
494
+ replacement with `create_repository_api_key` and deploy it BEFORE revoking the old one — revoke
495
+ first and the repository's CI is locked out until a human mints a new key in a browser. A `204`
496
+ means the key is revoked.
497
+
498
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
499
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
500
+
401
501
  ## How it works
402
502
 
403
503
  ```
@@ -25,6 +25,23 @@ export declare function getJson(api: ApiConfig, path: string, query: Record<stri
25
25
  * send a body whose `Content-Type` says JSON and whose bytes are not.
26
26
  */
27
27
  export declare function postJson(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<unknown>;
28
+ /**
29
+ * `DELETE` — the destructive half of the transport, and deliberately the SAME
30
+ * function underneath `postJson` rather than beside it, for the reason
31
+ * `postJson`'s header states: everything expensive about this module is about
32
+ * the deadline, not the verb.
33
+ *
34
+ * Returns the RAW BODY TEXT rather than a parsed value, because the endpoints
35
+ * this serves answer `204` with NO body at all — the one response in the `sgu_`
36
+ * surface that is deliberately not JSON. `requestJson` JSON-parses every 2xx it
37
+ * sees, so routing a `204` through it would turn a successful delete into
38
+ * "answered 204 but the body was not JSON" — the trap this verb specifically
39
+ * introduces, and the reason the DELETE path has its own success handling
40
+ * instead of sharing `requestJson`'s. The status check and the
41
+ * "reached and refused" hand-off to `describeFailure` are still shared
42
+ * verbatim: only what happens to a SUCCESS body differs.
43
+ */
44
+ export declare function deleteJson(api: ApiConfig, path: string, fetchImpl: typeof globalThis.fetch): Promise<string>;
28
45
  /**
29
46
  * `postJson`, narrowed exactly as `getJsonObject` narrows `getJson`.
30
47
  *
@@ -38,6 +38,28 @@ export async function postJson(api, path, body, fetchImpl) {
38
38
  body: JSON.stringify(body),
39
39
  });
40
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
+ }
41
63
  /**
42
64
  * `postJson`, narrowed exactly as `getJsonObject` narrows `getJson`.
43
65
  *
@@ -245,8 +267,8 @@ function describeFailure(status, body, api) {
245
267
  return new ApiError(`${api.endpoint} has no such endpoint (404). Check that ${api.endpointVariable} is the ` +
246
268
  "deployment's root URL, without a path.", status);
247
269
  }
248
- if (status === 400) {
249
- const message = badRequestMessage(body);
270
+ if (status === 400 || status === 403) {
271
+ const message = refusalMessage(body, status);
250
272
  if (message !== undefined)
251
273
  return new ApiError(message, status);
252
274
  }
@@ -262,6 +284,13 @@ function describeFailure(status, body, api) {
262
284
  * `origin/main` route here, so this branch serves the API surface rather than
263
285
  * one tool.
264
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
+ *
265
294
  * SURFACING IT IS THE OPPOSITE OF RESHAPING IT. The generic branch below turns
266
295
  * the most useful sentence in this direction —
267
296
  *
@@ -283,7 +312,7 @@ function describeFailure(status, body, api) {
283
312
  * whose `message` is absent or is not a string — still gets the generic
284
313
  * sentence, which at least shows the operator what actually came back.
285
314
  */
286
- function badRequestMessage(body) {
315
+ function refusalMessage(body, status) {
287
316
  let parsed;
288
317
  try {
289
318
  parsed = JSON.parse(body);
@@ -296,7 +325,7 @@ function badRequestMessage(body) {
296
325
  const message = parsed["message"];
297
326
  if (typeof message !== "string" || message.trim() === "")
298
327
  return undefined;
299
- return `SpecGuard refused the request (400): ${message.trim()}`;
328
+ return `SpecGuard refused the request (${status}): ${message.trim()}`;
300
329
  }
301
330
  export { requireApiConfig, requireUserApiConfig };
302
331
  //# 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,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;
@@ -0,0 +1,83 @@
1
+ import { postJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
2
+ import { optionalString, requireString } from "./args.js";
3
+ /**
4
+ * `POST /api/v1/repositories/:repository_id/api_keys` as a tool — shipped in
5
+ * the platform (`specguard/config/routes.rb:158`,
6
+ * `user_repository_api_keys_controller#create`, SPGD-754).
7
+ *
8
+ * == Reveal-once, again
9
+ *
10
+ * The 201 body carries `api_key.token` — the raw key, the only time it exists
11
+ * anywhere — exactly as `add_repository`'s does. The body is therefore passed
12
+ * through UNRESHAPED in both `text` and `structured` for the same reason that
13
+ * tool states: any reshaping on this hop is a value that cannot be recovered
14
+ * rather than a field that can be re-fetched. Recovery for a dropped token is
15
+ * minting another key — this same tool — because the platform ships no
16
+ * `#regenerate` and no re-serve.
17
+ *
18
+ * == The name is top-level and optional
19
+ *
20
+ * `params[:name]` defaults to `ApiKey::DEFAULT_NAME` server-side; `undefined`
21
+ * here means "let the server name it" and simply omits the key from the POST
22
+ * body. Not re-validated here for the reason `add_repository` states: a second
23
+ * format rule on this side is a rule with no owner, free to drift from the one
24
+ * that actually decides.
25
+ */
26
+ const createRepositoryApiKey = {
27
+ name: "create_repository_api_key",
28
+ title: "Create repository API key",
29
+ description: "Mints a new CI API key (an sgk_… key) for a SpecGuard repository, and returns it " +
30
+ "alongside the repository's existing keys. " +
31
+ "⚠️ `api_key.token` is shown THIS ONCE AND NEVER AGAIN — nothing stores it and no " +
32
+ "endpoint can re-serve it, so hand it to the user in your reply rather than assuming " +
33
+ "it can be fetched later. If it is dropped, the recovery is minting another key with " +
34
+ "this same tool (the platform has no regenerate), then revoking the orphaned one. " +
35
+ "On success the response carries an `api_key` block (`name`, `token`, `hint`, " +
36
+ "`created_at`) — the same reveal-once shape `add_repository` serves. " +
37
+ "Minting does not disturb existing keys: each key on a repository authenticates " +
38
+ "independently until revoked. " +
39
+ "Takes `repository_id` (the numeric id `list_repositories` reports) and an optional " +
40
+ "`name`, which the server defaults when omitted. " +
41
+ "Authorization is the `keys_manage` capability — a member without it is refused 403 " +
42
+ "in SpecGuard's own words. " +
43
+ "Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `add_repository` " +
44
+ "writes with and a DIFFERENT one from the sgk_… repository key " +
45
+ "`get_repository_overview` uses.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ repository_id: {
50
+ type: "string",
51
+ description: "The repository to mint the key for — its numeric id, as `add_repository` " +
52
+ "returns and `list_repositories` reports, not the `org/repo` handle.",
53
+ },
54
+ name: {
55
+ type: "string",
56
+ description: "An optional label for the key. Omit it to let SpecGuard use its default name. " +
57
+ "The server validates the name and refuses an unusable one in its own words.",
58
+ },
59
+ },
60
+ required: ["repository_id"],
61
+ // Closed for the reason every tool here states — and on a WRITE, a silently
62
+ // dropped misspelled argument still mints something, just possibly mislabeled.
63
+ additionalProperties: false,
64
+ },
65
+ async run(args, context) {
66
+ const repositoryId = requireString(args["repository_id"], "repository_id");
67
+ const name = optionalString(args["name"], "name");
68
+ const api = requireUserApiConfig(context.config);
69
+ // `name` omitted when absent rather than sent as null, so the server's own
70
+ // `ApiKey::DEFAULT_NAME` default applies — this bridge expresses "no
71
+ // preference", it does not choose on the server's behalf.
72
+ const created = await postJsonObject(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/api_keys`, name === undefined ? {} : { name }, context.fetch);
73
+ // Unreshaped, for the reason `add_repository` states at its own return:
74
+ // `api_key.token` exists nowhere else, so any reshaping on this hop is a
75
+ // value that cannot be recovered.
76
+ return {
77
+ text: JSON.stringify(created, null, 2),
78
+ structured: created,
79
+ };
80
+ },
81
+ };
82
+ export default createRepositoryApiKey;
83
+ //# sourceMappingURL=create-repository-api-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-repository-api-key.js","sourceRoot":"","sources":["../../../src/tools/create-repository-api-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,sBAAsB,GAAmB;IAC7C,IAAI,EAAE,2BAA2B;IACjC,KAAK,EAAE,2BAA2B;IAClC,WAAW,EACT,mFAAmF;QACnF,4CAA4C;QAC5C,mFAAmF;QACnF,sFAAsF;QACtF,sFAAsF;QACtF,mFAAmF;QACnF,+EAA+E;QAC/E,sEAAsE;QACtE,iFAAiF;QACjF,+BAA+B;QAC/B,qFAAqF;QACrF,kDAAkD;QAClD,qFAAqF;QACrF,4BAA4B;QAC5B,oFAAoF;QACpF,gEAAgE;QAChE,iCAAiC;IACnC,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,2EAA2E;oBAC3E,qEAAqE;aACxE;YACD,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,gFAAgF;oBAChF,6EAA6E;aAChF;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,4EAA4E;QAC5E,+EAA+E;QAC/E,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAElD,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,2EAA2E;QAC3E,qEAAqE;QACrE,0DAA0D;QAC1D,MAAM,OAAO,GAAG,MAAM,cAAc,CAClC,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,WAAW,EACnE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAClC,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,wEAAwE;QACxE,yEAAyE;QACzE,kCAAkC;QAClC,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,UAAU,EAAE,OAAO;SACpB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
@@ -56,11 +56,52 @@ import type { ToolDefinition } from "./types.js";
56
56
  * beside it, and `describeFailure` grew the `400` branch that surfaces
57
57
  * SpecGuard's own refusal sentence — the modal answer this endpoint gives.
58
58
  *
59
- * The standing rule is unchanged and still binding, which is what keeps the rest
60
- * of the user-scoped write surface out: `DELETE /api/v1/repositories/:id` and
61
- * the API-key endpoints (SPGD-754) are NOT on `origin/main`, so they may not be
62
- * wrapped here however useful a tool for them would be. What moved was the
63
- * platform, not the bar.
59
+ * The standing rule is unchanged and still binding. What once kept `DELETE
60
+ * /api/v1/repositories/:id` and the API-key endpoints out under it — "not on
61
+ * `origin/main`, so they may not be wrapped" — stopped being true when SPGD-754
62
+ * shipped them, and the sixth-through-eighth section below records their
63
+ * wrapping. What moved was the platform, not the bar.
64
+ *
65
+ * == The fifth: the read half of the registration gate
66
+ *
67
+ * - `registrable_repositories` wraps `GET /api/v1/repositories/registrable`
68
+ * (shipped: `specguard/config/routes.rb:117`,
69
+ * `Api::V1::UserRepositoriesController#registrable`).
70
+ *
71
+ * `list_repositories` says what IS registered; this says what COULD be — the
72
+ * set the gate would consult, read out in advance, so an agent can pick a
73
+ * `full_name` for `add_repository` from a real answer. Landing it also
74
+ * generalised the 400 branch in `describeFailure` into a status-parameterised
75
+ * extractor, because this endpoint's modal first answer is a 403 (`not_granted`)
76
+ * carrying the same `{error, message}` contract — the identical defect, given
77
+ * the identical remedy.
78
+ *
79
+ * The user-scoped surface as it now stands is therefore three tools: read the
80
+ * list, read the gate's answer, write a registration. The standing rule is
81
+ * unchanged and still binding, which is what keeps the rest out:
82
+ * `DELETE /api/v1/repositories/:id` and the API-key endpoints (SPGD-754) are
83
+ * NOT on `origin/main`, so they may not be wrapped here however useful a tool
84
+ * for them would be. What moved was the platform, not the bar.
85
+ *
86
+ * == The sixth through eighth: removal and the key lifecycle
87
+ *
88
+ * - `remove_repository` wraps `DELETE /api/v1/repositories/:id`, and
89
+ * `create_repository_api_key` / `revoke_repository_api_key` wrap the two
90
+ * `api_keys` endpoints (all shipped: SPGD-754, `specguard@origin/main`).
91
+ *
92
+ * The closing fence the two paragraphs above share — "the DELETE and API-key
93
+ * endpoints are NOT on `origin/main`, so they may not be wrapped" — stopped
94
+ * being true when SPGD-754 landed, and these three entries are what became
95
+ * wrappable the moment it did. They also forced the transport's third verb:
96
+ * both DELETE endpoints answer `204` with NO body, the one response in the
97
+ * `sgu_` surface that is deliberately not JSON, which is why `deleteJson`
98
+ * returns the raw body text instead of routing an empty 204 through
99
+ * `requestJson`'s JSON parse.
100
+ *
101
+ * The standing rule itself is unchanged and still binding — which still keeps
102
+ * out `/check-intent`, duplicate clustering, member management and rename: none
103
+ * of their backing endpoints has shipped, and a tool advertised in `tools/list`
104
+ * remains a promise an agent will act on.
64
105
  */
65
106
  export declare const TOOLS: readonly ToolDefinition[];
66
107
  export type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
@@ -1,7 +1,11 @@
1
1
  import addRepository from "./add-repository.js";
2
+ import createRepositoryApiKey from "./create-repository-api-key.js";
2
3
  import lintIntentAnnotations from "./lint-intent-annotations.js";
3
4
  import listRepositories from "./list-repositories.js";
4
5
  import getRepositoryOverview from "./repository-overview.js";
6
+ import registrableRepositories from "./registrable-repositories.js";
7
+ import removeRepository from "./remove-repository.js";
8
+ import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
5
9
  /**
6
10
  * THE REGISTRY — the one file that changes when the toolset grows.
7
11
  *
@@ -59,16 +63,61 @@ import getRepositoryOverview from "./repository-overview.js";
59
63
  * beside it, and `describeFailure` grew the `400` branch that surfaces
60
64
  * SpecGuard's own refusal sentence — the modal answer this endpoint gives.
61
65
  *
62
- * The standing rule is unchanged and still binding, which is what keeps the rest
63
- * of the user-scoped write surface out: `DELETE /api/v1/repositories/:id` and
64
- * the API-key endpoints (SPGD-754) are NOT on `origin/main`, so they may not be
65
- * wrapped here however useful a tool for them would be. What moved was the
66
- * platform, not the bar.
66
+ * The standing rule is unchanged and still binding. What once kept `DELETE
67
+ * /api/v1/repositories/:id` and the API-key endpoints out under it — "not on
68
+ * `origin/main`, so they may not be wrapped" — stopped being true when SPGD-754
69
+ * shipped them, and the sixth-through-eighth section below records their
70
+ * wrapping. What moved was the platform, not the bar.
71
+ *
72
+ * == The fifth: the read half of the registration gate
73
+ *
74
+ * - `registrable_repositories` wraps `GET /api/v1/repositories/registrable`
75
+ * (shipped: `specguard/config/routes.rb:117`,
76
+ * `Api::V1::UserRepositoriesController#registrable`).
77
+ *
78
+ * `list_repositories` says what IS registered; this says what COULD be — the
79
+ * set the gate would consult, read out in advance, so an agent can pick a
80
+ * `full_name` for `add_repository` from a real answer. Landing it also
81
+ * generalised the 400 branch in `describeFailure` into a status-parameterised
82
+ * extractor, because this endpoint's modal first answer is a 403 (`not_granted`)
83
+ * carrying the same `{error, message}` contract — the identical defect, given
84
+ * the identical remedy.
85
+ *
86
+ * The user-scoped surface as it now stands is therefore three tools: read the
87
+ * list, read the gate's answer, write a registration. The standing rule is
88
+ * unchanged and still binding, which is what keeps the rest out:
89
+ * `DELETE /api/v1/repositories/:id` and the API-key endpoints (SPGD-754) are
90
+ * NOT on `origin/main`, so they may not be wrapped here however useful a tool
91
+ * for them would be. What moved was the platform, not the bar.
92
+ *
93
+ * == The sixth through eighth: removal and the key lifecycle
94
+ *
95
+ * - `remove_repository` wraps `DELETE /api/v1/repositories/:id`, and
96
+ * `create_repository_api_key` / `revoke_repository_api_key` wrap the two
97
+ * `api_keys` endpoints (all shipped: SPGD-754, `specguard@origin/main`).
98
+ *
99
+ * The closing fence the two paragraphs above share — "the DELETE and API-key
100
+ * endpoints are NOT on `origin/main`, so they may not be wrapped" — stopped
101
+ * being true when SPGD-754 landed, and these three entries are what became
102
+ * wrappable the moment it did. They also forced the transport's third verb:
103
+ * both DELETE endpoints answer `204` with NO body, the one response in the
104
+ * `sgu_` surface that is deliberately not JSON, which is why `deleteJson`
105
+ * returns the raw body text instead of routing an empty 204 through
106
+ * `requestJson`'s JSON parse.
107
+ *
108
+ * The standing rule itself is unchanged and still binding — which still keeps
109
+ * out `/check-intent`, duplicate clustering, member management and rename: none
110
+ * of their backing endpoints has shipped, and a tool advertised in `tools/list`
111
+ * remains a promise an agent will act on.
67
112
  */
68
113
  export const TOOLS = [
69
114
  lintIntentAnnotations,
70
115
  getRepositoryOverview,
71
116
  listRepositories,
72
117
  addRepository,
118
+ registrableRepositories,
119
+ removeRepository,
120
+ createRepositoryApiKey,
121
+ revokeRepositoryApiKey,
73
122
  ];
74
123
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAG7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;CACd,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,uBAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AAGpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuGG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;IACb,uBAAuB;IACvB,gBAAgB;IAChB,sBAAsB;IACtB,sBAAsB;CACvB,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `GET /api/v1/repositories/registrable` as a tool — shipped today in the
4
+ * platform (`specguard/config/routes.rb:117`, served by
5
+ * `Api::V1::UserRepositoriesController#registrable`).
6
+ *
7
+ * == What it answers, and why `list_repositories` does not
8
+ *
9
+ * `list_repositories` reports what is already registered; this reports what
10
+ * COULD be — the repositories the registration gate would consult, read out
11
+ * loud in advance. It exists so an agent can pick a `full_name` for
12
+ * `add_repository` from a real answer rather than by guessing, and so a
13
+ * `has already been taken` refusal can be explained rather than merely
14
+ * retried.
15
+ *
16
+ * == Two controller decisions the description carries, because both are
17
+ * counter-intuitive and both are stated in the controller's own comments
18
+ *
19
+ * `registered` is asked of `Repository` GLOBALLY, not of what this person can
20
+ * open. The controller says why: a repository somebody ELSE registered still
21
+ * refuses this person's POST with `has already been taken`, so a reading
22
+ * scoped to what they can see would mark it `registered: false` and send them
23
+ * at a name that cannot be registered by anyone. An entry is therefore
24
+ * MARKED, not excluded — `registered: true` is the answer to "why did my POST
25
+ * say has already been taken".
26
+ *
27
+ * And a name appearing here is NOT a promise the write will succeed. This is
28
+ * the set the gate would consult at the moment of the read; the repository may
29
+ * be registered by someone else between the two calls.
30
+ *
31
+ * == The modal first answer is a 403, and that is why `describeFailure` grew
32
+ *
33
+ * `#registrable` fails closed on the two states `GrantVerifier` refuses on and
34
+ * renders `status: :forbidden` — NOT the 400 path the other refusals use. A
35
+ * nil grant is, in the controller's own words, "an ordinary state and not an
36
+ * error: it is every person who has not opened SpecGuard in a browser since
37
+ * this shipped" — so the 403 is the modal first answer this tool gives, and
38
+ * the sentence it carries (sign in, reconnect GitHub, retry) reaches the agent
39
+ * through the 403 branch in `specguard-api.ts`, which this tool is the reason
40
+ * for. On refusal the body still carries `grant`: `null` when there never was
41
+ * one, populated with `stale: true` when it lapsed — "yours lapsed four days
42
+ * ago" is a different fact from "you never had one", and the tool description
43
+ * is where an agent learns to branch on it.
44
+ *
45
+ * == No arguments, for the same reason `list_repositories` has none
46
+ *
47
+ * The credential is the whole of the scope. The endpoint takes no parameters,
48
+ * and nothing an argument could select reaches this answer.
49
+ */
50
+ declare const registrableRepositories: ToolDefinition;
51
+ export default registrableRepositories;
@@ -0,0 +1,92 @@
1
+ import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
2
+ /**
3
+ * `GET /api/v1/repositories/registrable` as a tool — shipped today in the
4
+ * platform (`specguard/config/routes.rb:117`, served by
5
+ * `Api::V1::UserRepositoriesController#registrable`).
6
+ *
7
+ * == What it answers, and why `list_repositories` does not
8
+ *
9
+ * `list_repositories` reports what is already registered; this reports what
10
+ * COULD be — the repositories the registration gate would consult, read out
11
+ * loud in advance. It exists so an agent can pick a `full_name` for
12
+ * `add_repository` from a real answer rather than by guessing, and so a
13
+ * `has already been taken` refusal can be explained rather than merely
14
+ * retried.
15
+ *
16
+ * == Two controller decisions the description carries, because both are
17
+ * counter-intuitive and both are stated in the controller's own comments
18
+ *
19
+ * `registered` is asked of `Repository` GLOBALLY, not of what this person can
20
+ * open. The controller says why: a repository somebody ELSE registered still
21
+ * refuses this person's POST with `has already been taken`, so a reading
22
+ * scoped to what they can see would mark it `registered: false` and send them
23
+ * at a name that cannot be registered by anyone. An entry is therefore
24
+ * MARKED, not excluded — `registered: true` is the answer to "why did my POST
25
+ * say has already been taken".
26
+ *
27
+ * And a name appearing here is NOT a promise the write will succeed. This is
28
+ * the set the gate would consult at the moment of the read; the repository may
29
+ * be registered by someone else between the two calls.
30
+ *
31
+ * == The modal first answer is a 403, and that is why `describeFailure` grew
32
+ *
33
+ * `#registrable` fails closed on the two states `GrantVerifier` refuses on and
34
+ * renders `status: :forbidden` — NOT the 400 path the other refusals use. A
35
+ * nil grant is, in the controller's own words, "an ordinary state and not an
36
+ * error: it is every person who has not opened SpecGuard in a browser since
37
+ * this shipped" — so the 403 is the modal first answer this tool gives, and
38
+ * the sentence it carries (sign in, reconnect GitHub, retry) reaches the agent
39
+ * through the 403 branch in `specguard-api.ts`, which this tool is the reason
40
+ * for. On refusal the body still carries `grant`: `null` when there never was
41
+ * one, populated with `stale: true` when it lapsed — "yours lapsed four days
42
+ * ago" is a different fact from "you never had one", and the tool description
43
+ * is where an agent learns to branch on it.
44
+ *
45
+ * == No arguments, for the same reason `list_repositories` has none
46
+ *
47
+ * The credential is the whole of the scope. The endpoint takes no parameters,
48
+ * and nothing an argument could select reaches this answer.
49
+ */
50
+ const registrableRepositories = {
51
+ name: "registrable_repositories",
52
+ title: "Registrable repositories",
53
+ description: "Lists the GitHub repositories the person behind this server's user API key could register " +
54
+ "with SpecGuard — the set the registration gate would consult, read out in advance, so an " +
55
+ "agent can pick a `full_name` for `add_repository` from a real answer rather than by " +
56
+ "guessing. Each entry carries `full_name` and `registered`. `registered` is asked GLOBALLY, " +
57
+ "not just of this person's own repositories: an entry marked `registered: true` was " +
58
+ "registered by SOMEBODY — possibly someone else — and a POST naming it will be refused with " +
59
+ "`has already been taken`, which is exactly the question this flag answers. Entries are " +
60
+ "marked, not excluded, for that reason. The response also carries a `grant` block " +
61
+ "(`captured_at`, `expires_at`, `stale`) describing the stored record of this person's GitHub " +
62
+ "permissions. A MISSING or STALE grant is not an error — it is every person who has not " +
63
+ "opened SpecGuard in a browser recently — and the call then answers 403 with SpecGuard's own " +
64
+ "sentence naming the fix: sign in to SpecGuard in a browser and reconnect GitHub, then try " +
65
+ "again. On that refusal the body's `grant` distinguishes the two cases: `grant: null` means " +
66
+ "there never was one (first-time setup), a populated grant with `stale: true` means an " +
67
+ "existing connection lapsed (same remedy, likely faster to complete) — branch on it before " +
68
+ "telling the person what to do. A name appearing in the list is not a promise the write " +
69
+ "will succeed: someone may register it between this read and the POST. Ordered by " +
70
+ "`full_name` ascending, which is stable across calls. Needs SPECGUARD_USER_API_KEY (an " +
71
+ "sgu_… key), the same credential `list_repositories` and `add_repository` read and a " +
72
+ "DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
73
+ inputSchema: {
74
+ type: "object",
75
+ // No properties, deliberately — see this file's header. Still CLOSED rather
76
+ // than merely empty, for the reason `list-repositories.ts` gives inline:
77
+ // `server.ts` forwards `arguments` unvalidated and `run` ignores them, so
78
+ // an open schema would have an invented argument silently dropped and the
79
+ // call answered as if it had been honoured.
80
+ additionalProperties: false,
81
+ },
82
+ async run(_args, context) {
83
+ const api = requireUserApiConfig(context.config);
84
+ const listing = await getJsonObject(api, "/api/v1/repositories/registrable", {}, context.fetch);
85
+ return {
86
+ text: JSON.stringify(listing, null, 2),
87
+ structured: listing,
88
+ };
89
+ },
90
+ };
91
+ export default registrableRepositories;
92
+ //# sourceMappingURL=registrable-repositories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registrable-repositories.js","sourceRoot":"","sources":["../../../src/tools/registrable-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGlF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,uBAAuB,GAAmB;IAC9C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,WAAW,EACT,4FAA4F;QAC5F,2FAA2F;QAC3F,sFAAsF;QACtF,6FAA6F;QAC7F,qFAAqF;QACrF,6FAA6F;QAC7F,yFAAyF;QACzF,mFAAmF;QACnF,8FAA8F;QAC9F,yFAAyF;QACzF,8FAA8F;QAC9F,4FAA4F;QAC5F,6FAA6F;QAC7F,wFAAwF;QACxF,4FAA4F;QAC5F,yFAAyF;QACzF,mFAAmF;QACnF,wFAAwF;QACxF,sFAAsF;QACtF,6EAA6E;IAC/E,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,4EAA4E;QAC5E,yEAAyE;QACzE,0EAA0E;QAC1E,0EAA0E;QAC1E,4CAA4C;QAC5C,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO;QACtB,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,GAAG,EACH,kCAAkC,EAClC,EAAE,EACF,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,UAAU,EAAE,OAAO;SACpB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `DELETE /api/v1/repositories/:id` as a tool — shipped in the platform
4
+ * (`specguard/config/routes.rb:152`, `Api::V1::UserRepositoriesController#destroy`,
5
+ * SPGD-754).
6
+ *
7
+ * == The destructive gesture in the whole surface
8
+ *
9
+ * Everything else here reads, registers, or mints. This one removes a
10
+ * repository AND every key, run and intent on it (`memberships_helper.rb`:
11
+ * "Delete the repository, and every key, run and intent on it"), and there is no
12
+ * undo. The description therefore carries the hazard BEFORE the call: it is
13
+ * prompt material, read when the agent is deciding whether to act, not a
14
+ * footnote discovered after.
15
+ *
16
+ * == Authorization is `repo.delete` at either surface
17
+ *
18
+ * The controller authorizes through `RepositoryAuthorization`'s `:repo_delete`
19
+ * fork — deliberately NOT `:owner` — so a member granted `repo.delete` may
20
+ * remove the repository from either surface. The tool does not probe for the
21
+ * capability client-side: the server is the gate, and a member without it
22
+ * receives a 403 whose sentence arrives verbatim through the landed
23
+ * `refusalMessage` branch in `describeFailure`.
24
+ *
25
+ * == 204 with no body
26
+ *
27
+ * This is the one response in the `sgu_` surface that is deliberately not a
28
+ * JSON body — which is why `deleteJson` exists: routing an empty-body 204
29
+ * through `requestJson`'s JSON parse would turn a successful delete into
30
+ * "the body was not JSON". Here, no body IS the success.
31
+ */
32
+ declare const removeRepository: ToolDefinition;
33
+ export default removeRepository;
@@ -0,0 +1,81 @@
1
+ import { deleteJson, requireUserApiConfig } from "../support/specguard-api.js";
2
+ import { requireString } from "./args.js";
3
+ /**
4
+ * `DELETE /api/v1/repositories/:id` as a tool — shipped in the platform
5
+ * (`specguard/config/routes.rb:152`, `Api::V1::UserRepositoriesController#destroy`,
6
+ * SPGD-754).
7
+ *
8
+ * == The destructive gesture in the whole surface
9
+ *
10
+ * Everything else here reads, registers, or mints. This one removes a
11
+ * repository AND every key, run and intent on it (`memberships_helper.rb`:
12
+ * "Delete the repository, and every key, run and intent on it"), and there is no
13
+ * undo. The description therefore carries the hazard BEFORE the call: it is
14
+ * prompt material, read when the agent is deciding whether to act, not a
15
+ * footnote discovered after.
16
+ *
17
+ * == Authorization is `repo.delete` at either surface
18
+ *
19
+ * The controller authorizes through `RepositoryAuthorization`'s `:repo_delete`
20
+ * fork — deliberately NOT `:owner` — so a member granted `repo.delete` may
21
+ * remove the repository from either surface. The tool does not probe for the
22
+ * capability client-side: the server is the gate, and a member without it
23
+ * receives a 403 whose sentence arrives verbatim through the landed
24
+ * `refusalMessage` branch in `describeFailure`.
25
+ *
26
+ * == 204 with no body
27
+ *
28
+ * This is the one response in the `sgu_` surface that is deliberately not a
29
+ * JSON body — which is why `deleteJson` exists: routing an empty-body 204
30
+ * through `requestJson`'s JSON parse would turn a successful delete into
31
+ * "the body was not JSON". Here, no body IS the success.
32
+ */
33
+ const removeRepository = {
34
+ name: "remove_repository",
35
+ title: "Remove repository",
36
+ description: "Removes a repository from SpecGuard, DELETING every key, run and intent on it. " +
37
+ "This is IRREVERSIBLE: the repository's CI keys stop authenticating and its recorded " +
38
+ "runs and intents are gone, with no undo and no way to recover them short of " +
39
+ "re-registering from scratch. A 204 means it is deleted. " +
40
+ "Authorization is the `repo.delete` capability at either surface — an owner, or a " +
41
+ "member granted it, may remove the repository; a member without it is refused 403 in " +
42
+ "SpecGuard's own words. Confirm with the user before calling: this is the destructive " +
43
+ "gesture in this toolset, and once it returns 204 the repository and its history " +
44
+ "cannot be brought back. " +
45
+ "Takes `repository_id` — the numeric id `add_repository` and `list_repositories` " +
46
+ "report, not the `org/repo` handle. " +
47
+ "Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `add_repository` " +
48
+ "writes with and a DIFFERENT one from the sgk_… repository key " +
49
+ "`get_repository_overview` uses.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ repository_id: {
54
+ type: "string",
55
+ description: "The id of the repository to remove — the numeric id `add_repository` returns " +
56
+ "and `list_repositories` reports, not the `org/repo` handle. SpecGuard scopes " +
57
+ "the lookup to repositories you may act on and answers 404 otherwise.",
58
+ },
59
+ },
60
+ required: ["repository_id"],
61
+ // Closed for the reason every tool here states: `server.ts` forwards
62
+ // `arguments` unvalidated, and on the DESTRUCTIVE path a silently-dropped
63
+ // argument is the worst case of the write-path argument — the call still
64
+ // deletes something, just not what the agent believed it named.
65
+ additionalProperties: false,
66
+ },
67
+ async run(args, context) {
68
+ const repositoryId = requireString(args["repository_id"], "repository_id");
69
+ const api = requireUserApiConfig(context.config);
70
+ const body = await deleteJson(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}`, context.fetch);
71
+ // 204 with no body. Nothing upstream to pass through, so the result says
72
+ // what the verb did — in the tool's own words, because the deployment's
73
+ // whole answer was "no content".
74
+ return {
75
+ text: body === "" ? "Repository removed (204). Every key, run and intent on it is deleted." : body,
76
+ structured: { repository_id: repositoryId, deleted: true },
77
+ };
78
+ },
79
+ };
80
+ export default removeRepository;
81
+ //# sourceMappingURL=remove-repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove-repository.js","sourceRoot":"","sources":["../../../src/tools/remove-repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,gBAAgB,GAAmB;IACvC,IAAI,EAAE,mBAAmB;IACzB,KAAK,EAAE,mBAAmB;IAC1B,WAAW,EACT,iFAAiF;QACjF,sFAAsF;QACtF,8EAA8E;QAC9E,0DAA0D;QAC1D,mFAAmF;QACnF,sFAAsF;QACtF,uFAAuF;QACvF,kFAAkF;QAClF,0BAA0B;QAC1B,kFAAkF;QAClF,qCAAqC;QACrC,oFAAoF;QACpF,gEAAgE;QAChE,iCAAiC;IACnC,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,+EAA+E;oBAC/E,+EAA+E;oBAC/E,sEAAsE;aACzE;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,qEAAqE;QACrE,0EAA0E;QAC1E,yEAAyE;QACzE,gEAAgE;QAChE,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAE3E,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,IAAI,GAAG,MAAM,UAAU,CAC3B,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,EAAE,EAC1D,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,yEAAyE;QACzE,wEAAwE;QACxE,iCAAiC;QACjC,OAAO;YACL,IAAI,EAAE,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,uEAAuE,CAAC,CAAC,CAAC,IAAI;YAClG,UAAU,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE;SAC3D,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `DELETE /api/v1/repositories/:repository_id/api_keys/:id` as a tool —
4
+ * shipped in the platform (`specguard/config/routes.rb:159`,
5
+ * `user_repository_api_keys_controller#destroy`, SPGD-754).
6
+ *
7
+ * == Replacement-mint-then-revoke is the rotation model
8
+ *
9
+ * The platform ships no `#regenerate`. Rotating a compromised key is therefore
10
+ * two calls with tools this slice lands together: mint a replacement
11
+ * (`create_repository_api_key`), deploy it, then revoke the old one here. That
12
+ * ordering is stated in the description because it is the one thing an agent
13
+ * must know before it revokes — revoke-first locks the repository's CI out
14
+ * until a human intervenes.
15
+ *
16
+ * == The id is scoped to the repository
17
+ *
18
+ * `repository.api_keys.find(params[:id])` — a key id belonging to a DIFFERENT
19
+ * repository is a 404, not a cross-repository delete. The bridge relies on that
20
+ * scoping entirely and adds no client-side check of its own, for the reason
21
+ * every tool here states: the server is the gate.
22
+ *
23
+ * == 204 with no body
24
+ *
25
+ * The same empty-body success `remove_repository` serves; the same reason
26
+ * `deleteJson` exists rather than routing a 204 through `requestJson`.
27
+ */
28
+ declare const revokeRepositoryApiKey: ToolDefinition;
29
+ export default revokeRepositoryApiKey;
@@ -0,0 +1,85 @@
1
+ import { deleteJson, requireUserApiConfig } from "../support/specguard-api.js";
2
+ import { requireString } from "./args.js";
3
+ /**
4
+ * `DELETE /api/v1/repositories/:repository_id/api_keys/:id` as a tool —
5
+ * shipped in the platform (`specguard/config/routes.rb:159`,
6
+ * `user_repository_api_keys_controller#destroy`, SPGD-754).
7
+ *
8
+ * == Replacement-mint-then-revoke is the rotation model
9
+ *
10
+ * The platform ships no `#regenerate`. Rotating a compromised key is therefore
11
+ * two calls with tools this slice lands together: mint a replacement
12
+ * (`create_repository_api_key`), deploy it, then revoke the old one here. That
13
+ * ordering is stated in the description because it is the one thing an agent
14
+ * must know before it revokes — revoke-first locks the repository's CI out
15
+ * until a human intervenes.
16
+ *
17
+ * == The id is scoped to the repository
18
+ *
19
+ * `repository.api_keys.find(params[:id])` — a key id belonging to a DIFFERENT
20
+ * repository is a 404, not a cross-repository delete. The bridge relies on that
21
+ * scoping entirely and adds no client-side check of its own, for the reason
22
+ * every tool here states: the server is the gate.
23
+ *
24
+ * == 204 with no body
25
+ *
26
+ * The same empty-body success `remove_repository` serves; the same reason
27
+ * `deleteJson` exists rather than routing a 204 through `requestJson`.
28
+ */
29
+ const revokeRepositoryApiKey = {
30
+ name: "revoke_repository_api_key",
31
+ title: "Revoke repository API key",
32
+ description: "Revokes one CI API key on a SpecGuard repository. The key stops authenticating " +
33
+ "immediately; every OTHER key on the repository keeps working, so CI keeps " +
34
+ "ingesting if it holds a surviving key. " +
35
+ "The `key_id` is scoped to `repository_id`: a key id belonging to a different " +
36
+ "repository is refused 404, never a cross-repository delete. " +
37
+ "Key rotation is mint-then-revoke, in that order: the platform has no regenerate, " +
38
+ "so mint a replacement with `create_repository_api_key` and deploy it BEFORE " +
39
+ "revoking the old one — revoke first and the repository's CI is locked out until " +
40
+ "a human mints a new key in a browser. " +
41
+ "Authorization is the `keys_manage` capability — a member without it is refused " +
42
+ "403 in SpecGuard's own words. A 204 means the key is revoked. " +
43
+ "Takes `repository_id` (the numeric id `list_repositories` reports) and `key_id` " +
44
+ "(the id from `add_repository`'s or `create_repository_api_key`'s response). " +
45
+ "Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential " +
46
+ "`add_repository` writes with and a DIFFERENT one from the sgk_… repository key " +
47
+ "`get_repository_overview` uses.",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ repository_id: {
52
+ type: "string",
53
+ description: "The repository the key belongs to — its numeric id, as `add_repository` " +
54
+ "returns and `list_repositories` reports, not the `org/repo` handle.",
55
+ },
56
+ key_id: {
57
+ type: "string",
58
+ description: "The id of the key to revoke, as served in the `api_key` block of " +
59
+ "`add_repository` or `create_repository_api_key` (or the repository's " +
60
+ "API-keys page). Scoped to `repository_id`: a foreign key id is refused 404.",
61
+ },
62
+ },
63
+ required: ["repository_id", "key_id"],
64
+ // Closed for the reason every tool here states — and on a DESTRUCTIVE path,
65
+ // a silently dropped misspelled argument must not be able to leave the
66
+ // agent believing it revoked a key the call never named.
67
+ additionalProperties: false,
68
+ },
69
+ async run(args, context) {
70
+ const repositoryId = requireString(args["repository_id"], "repository_id");
71
+ const keyId = requireString(args["key_id"], "key_id");
72
+ const api = requireUserApiConfig(context.config);
73
+ const body = await deleteJson(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/api_keys/${encodeURIComponent(keyId)}`, context.fetch);
74
+ // 204 with no body — the tool's own words, because the deployment's whole
75
+ // answer was "no content".
76
+ return {
77
+ text: body === ""
78
+ ? `Key ${keyId} revoked (204). Other keys on the repository keep authenticating.`
79
+ : body,
80
+ structured: { repository_id: repositoryId, key_id: keyId, revoked: true },
81
+ };
82
+ },
83
+ };
84
+ export default revokeRepositoryApiKey;
85
+ //# sourceMappingURL=revoke-repository-api-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revoke-repository-api-key.js","sourceRoot":"","sources":["../../../src/tools/revoke-repository-api-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,sBAAsB,GAAmB;IAC7C,IAAI,EAAE,2BAA2B;IACjC,KAAK,EAAE,2BAA2B;IAClC,WAAW,EACT,iFAAiF;QACjF,4EAA4E;QAC5E,yCAAyC;QACzC,+EAA+E;QAC/E,8DAA8D;QAC9D,mFAAmF;QACnF,8EAA8E;QAC9E,kFAAkF;QAClF,wCAAwC;QACxC,iFAAiF;QACjF,gEAAgE;QAChE,kFAAkF;QAClF,8EAA8E;QAC9E,mEAAmE;QACnE,iFAAiF;QACjF,iCAAiC;IACnC,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,0EAA0E;oBAC1E,qEAAqE;aACxE;YACD,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,mEAAmE;oBACnE,uEAAuE;oBACvE,6EAA6E;aAChF;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,EAAE,QAAQ,CAAC;QACrC,4EAA4E;QAC5E,uEAAuE;QACvE,yDAAyD;QACzD,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAC3E,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;QAEtD,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,IAAI,GAAG,MAAM,UAAU,CAC3B,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,aAAa,kBAAkB,CAAC,KAAK,CAAC,EAAE,EAChG,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,0EAA0E;QAC1E,2BAA2B;QAC3B,OAAO;YACL,IAAI,EACF,IAAI,KAAK,EAAE;gBACT,CAAC,CAAC,OAAO,KAAK,mEAAmE;gBACjF,CAAC,CAAC,IAAI;YACV,UAAU,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;SAC1E,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specguard-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "MCP server exposing SpecGuard suite intelligence to AI coding agents",
5
5
  "license": "ISC",
6
6
  "type": "module",