specguard-mcp 0.1.11 → 0.1.13

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
@@ -33,8 +33,9 @@ refuses to boot and takes the tools that needed no configuration down with it.
33
33
  | Variable | Needed by | Default | What it is |
34
34
  | --- | --- | --- | --- |
35
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
- | `SPECGUARD_API_KEY` | `get_repository_overview` | — | an agent/CI API key (`sgk_…`) issued by that deployment |
37
- | `SPECGUARD_USER_API_KEY` | `list_repositories`, `add_repository`, `registrable_repositories`, `remove_repository`, `create_repository_api_key`, `revoke_repository_api_key`, `list_repository_agent_keys`, `revoke_repository_agent_key`, `list_repository_agent_keys_presented_revoked`, `list_repository_members`, `add_repository_member`, `update_repository_member_permissions`, `remove_repository_member`, `rename_repository` | — | 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 |
36
+ | `SPECGUARD_API_KEY` | `get_repository_overview`, `near_duplicate_clusters` (default calls) | — | an agent/CI API key (`sgk_…`) issued by that deployment — a **per-repository** key, which is the single repository those tools answer about by default |
37
+ | `SPECGUARD_USER_API_KEY` | `list_repositories` (fallback), `add_repository`, `registrable_repositories`, `remove_repository`, `create_repository_api_key`, `revoke_repository_api_key`, `list_repository_agent_keys`, `revoke_repository_agent_key`, `list_repository_agent_keys_presented_revoked`, `list_repository_members`, `add_repository_member`, `update_repository_member_permissions`, `remove_repository_member`, `rename_repository` | — | 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
+ | `SPECGUARD_AGENT_API_KEY` | `list_repositories` (preferred), `get_repository_overview` / `near_duplicate_clusters` **with** `repository` | — | an **agent** API key (`sga_…`), minted from that deployment's account page (Agent keys panel) with an explicit set of repositories and permissions. It speaks for nobody: its reach is exactly the set granted onto it, fixed at mint time, and every read is bounded by that set server-side. This is the credential to give an automated agent — one key, many repositories, none of a person's rights. When it and `SPECGUARD_USER_API_KEY` are both set, `list_repositories` uses **this** one, so discovery stays inside the set the other tools can reach |
38
39
  | `SPECGUARD_LINT_COMMAND` | `lint_intent_annotations` | `specguard-lint` | the command that runs the linter. Most Ruby projects need `bundle exec specguard-lint` |
39
40
  | `SPECGUARD_TIMEOUT_MS` | HTTP tools | `30000` | how long a call to SpecGuard may take |
40
41
 
@@ -56,6 +57,7 @@ Register it with your MCP client — for Claude Code:
56
57
  "SPECGUARD_ENDPOINT": "https://specguard.example.com",
57
58
  "SPECGUARD_API_KEY": "sgk_…",
58
59
  "SPECGUARD_USER_API_KEY": "sgu_…",
60
+ "SPECGUARD_AGENT_API_KEY": "sga_…",
59
61
  "SPECGUARD_LINT_COMMAND": "bundle exec specguard-lint"
60
62
  }
61
63
  }
@@ -101,8 +103,27 @@ history, and the branches that have runs. Pass `branch` for two more: which test
101
103
  rather than consistently (the cross-run flakiness ranking) and how the areas moved across the whole
102
104
  branch window rather than between the last two runs.
103
105
 
106
+ Everything above is answered about **one repository**, and which one has two spellings. By default
107
+ it is the repository the configured `sgk_…` key resolves to — the key *is* the subject. Pass
108
+ `repository` (the numeric id `list_repositories` reports) and the call goes to the **plural**
109
+ endpoint, `GET /api/v1/repositories/:id`, under the **agent key** (`SPECGUARD_AGENT_API_KEY`,
110
+ `sga_…`): same overview body, same ladder, every parameter below honoured identically — with one
111
+ deliberate omission this path owns: `api_key` is **absent** from the plural body rather than nulled,
112
+ because that block describes the credential that made the request and this request was not made
113
+ with a repository key (an absent key there is the surface's shape, never a dropped block). Other
114
+ than that, only the subject moves. That is what makes the *other* repositories visible: an `sgk_`
115
+ key is one repository
116
+ by design, so an agent holding only that could never ask about a second one, while the agent key's
117
+ own granted repository set is exactly the boundary the plural endpoint looks ids up inside (a
118
+ repository outside the set answers 404, indistinguishable from one that does not exist —
119
+ `list_repositories` under the same key lists the set that is reachable). SpecGuard refuses each
120
+ credential kind in the other's place before it reads anything, so the pairing is not mixable: the
121
+ default call wants the `sgk_` key, the `repository` call wants the agent key, and a call made with
122
+ the wrong one is refused *here*, naming the variable to set, before any request is made.
123
+
104
124
  | argument | |
105
125
  | --- | --- |
126
+ | `repository` | ask about THIS repository (its numeric id from `list_repositories`) under the **agent key**, instead of the one the `sgk_…` key resolves to — omit it for the default, `sgk_`-keyed call |
106
127
  | `branch` | narrow the run **history** to one branch, for a real growth series — and unlock `unstable_tests` and `directory_growth`, which read the same window |
107
128
  | `spec_directory` | open ONE of the heaviest directories and list the spec files inside it |
108
129
  | `spec_file` | open ONE of the heaviest spec files and list the individual examples inside it |
@@ -326,33 +347,50 @@ because a zero would read as a measurement that was taken.
326
347
 
327
348
  ### `list_repositories`
328
349
 
329
- Lists the SpecGuard repositories the person behind `SPECGUARD_USER_API_KEY` may open — *what can I
330
- ask about*, which is the one question no other tool here can answer. `get_repository_overview` takes
331
- no repository because its `sgk_…` key **is** the repository, so without this an agent can only report
350
+ Lists the SpecGuard repositories this server's key may open — *what can I ask about*, which is the
351
+ one question no other tool here can answer. `get_repository_overview` without `repository` takes no
352
+ repository because its `sgk_…` key **is** the repository, so without this an agent can only report
332
353
  on a repository somebody already named for it.
333
354
 
334
- **This tool takes no arguments** and not as an omission. The credential is the whole of the scope:
335
- the endpoint takes no parameters, and which repositories are in the answer is decided by SpecGuard
336
- from the person the key speaks for (owned, plus shared with them through a membership). A repository
337
- they neither own nor were given access to never enters the response, so it cannot be filtered *in*
338
- from this side either.
355
+ The credential decides which repositories are *in* the answer a person's own-plus-shared set
356
+ under an `sgu_…` key, the agent key's mint-time granted set under an `sga_…` one; the three
357
+ optional asks below narrow **within** whichever set answered never around it. The endpoint has
358
+ served them since SpecGuard's SPGD-940 (`ef6236d`), composed through the same `RepositoryNarrowing`
359
+ concern the web grid reads, and this tool forwards them rather than re-deriving them. A repository
360
+ the credential does not admit never enters the response, so no ask can filter it *in* either. A
361
+ blank value is no ask: passing an empty string and omitting the argument make the identical request.
362
+
363
+ | argument | |
364
+ | --- | --- |
365
+ | `q` | keep only repositories whose `full_name` (`org/repo`) contains this substring, case-insensitively. A plain substring, not a pattern — the LIKE wildcards `%`/`_` are escaped server-side, so `org/my_repo` matches itself. A match-less ask is an empty list, not an error |
366
+ | `role` | `"owned"` or `"shared"` — one half of the list's mix: the repositories this person owns, or the ones shared with them. Mind the spelling: the *ask* values are `owned`/`shared`, while each entry's `role` field reads `owner`/`member` — `role: "owner"` is not a valid ask and settles to no ask (full list, no error). Under the `sga_…` agent key the ask settles to no ask for every value: ownership is a person fact and the key speaks for nobody, so the full granted list is served |
367
+ | `sort` | `"stale"` — re-order stalest-first: repositories CI has never ingested a run for first, then least-recently-ingested, with `full_name` breaking ties so two calls agree element for element. The same entries, a different order; the default order stays `full_name` ascending |
339
368
 
340
369
  The body comes back as SpecGuard serves it — `{"repositories": […]}`, each entry carrying `id`,
341
- `full_name`, `name`, `registered_at` and `role`, ordered by `full_name` ascending. The first four are
370
+ `full_name`, `name`, `registered_at` and `role`, ordered by `full_name` ascending unless `sort`
371
+ asks otherwise. The first four are
342
372
  deliberately the same four fields, under the same names, that `get_repository_overview` serves in its
343
- own `repository` block, so a client that reads one reads the other. `role` is `owner` or `member`:
344
- the list mixes repositories this person owns with repositories somebody shared with them, and nothing
345
- else tells them apart read it before assuming a repository is one you may administer. An empty list
346
- means no access, not an error.
347
-
348
- **It reads a different key from `get_repository_overview`.** `SPECGUARD_USER_API_KEY` (`sgu_…`), not
349
- `SPECGUARD_API_KEY` (`sgk_…`). SpecGuard refuses each credential in the other's place — the prefix
350
- decides which table is consulted before any of them is read — so the two are not interchangeable and
351
- setting one does not stand in for the other. Every message this tool produces names the variable
352
- *it* reads, so a `401` here never sends you to check the key `get_repository_overview` uses.
353
-
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`,
373
+ own `repository` block, so a client that reads one reads the other. `role` has one value per
374
+ credential kind: under a **user** key (`sgu_…`) it is `owner` or `member` the list mixes
375
+ repositories this person owns with repositories somebody shared with them, and nothing else tells
376
+ them apart while under an **agent** key (`sga_…`) every entry is `agent`, the value that says the
377
+ ownership question does not apply because the key speaks for nobody (branching on owner/member
378
+ correctly reads false for both). Read it before assuming a repository is one you may administer. An
379
+ empty list means no access, not an error.
380
+
381
+ **It reads a different key from `get_repository_overview` either of two, whichever is set.**
382
+ `SPECGUARD_AGENT_API_KEY` (`sga_…`) when it is set: the answer is then the repository set granted
383
+ onto that key at mint time, the same set every other agent-keyed call answers inside, so discovery
384
+ and reach always agree. `SPECGUARD_USER_API_KEY` (`sgu_…`) when no agent key is set: the answer is
385
+ what that person may open. With both set the **agent** key wins. Neither is ever a stand-in for
386
+ `SPECGUARD_API_KEY` (`sgk_…`) — SpecGuard refuses each credential kind in the other's place, the
387
+ prefix deciding which table is consulted before any of them is read — and every message this tool
388
+ produces names the variable *it* read, so a `401` here never sends you to check a key this tool
389
+ never used.
390
+
391
+ Registering a repository is `add_repository`, below — it reads `SPECGUARD_USER_API_KEY` (an
392
+ `sgu_…` key, which this tool also accepts but does not require) and takes the `full_name` this
393
+ tool reports. Removal and the key lifecycle (`remove_repository`,
356
394
  `create_repository_api_key`, `revoke_repository_api_key`) are below too, on the same key; a tool
357
395
  here is a promise the agent will act on, so each waits for the capability rather than the other way
358
396
  round.
@@ -396,8 +434,9 @@ non-blank string; SpecGuard validates the name and refuses an unusable one in it
396
434
  format rule on this side would be free to drift from the one that actually decides, and would surface
397
435
  as this bridge rejecting a name the platform would have accepted.
398
436
 
399
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and a
400
- different one from the `sgk_…` key `get_repository_overview` uses.
437
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) a **person** key, which `list_repositories` also
438
+ accepts but does not require — and a different one from the `sgk_…` key `get_repository_overview`
439
+ uses.
401
440
 
402
441
  ### `registrable_repositories`
403
442
 
@@ -431,8 +470,9 @@ was one (first-time setup), a populated grant with `stale: true` means an existi
431
470
  parameters; which repositories are in the answer is decided by SpecGuard from the person the key
432
471
  speaks for.
433
472
 
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.
473
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
474
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
475
+ accepts that key but does not require it (its agent key wins when both are set).
436
476
 
437
477
  ### `near_duplicate_clusters`
438
478
 
@@ -448,10 +488,20 @@ and answers `near_duplicates: null` on the plain overview. Calling this tool **i
448
488
  `get_repository_overview` never sends it, so an agent reading the overview cannot pay the census by
449
489
  accident.
450
490
 
451
- **This tool takes no arguments** — nothing about the census is choosable. The clusters are the
452
- repository's, computed over every run; one call returns them all. (The server reads only that the
453
- `near_duplicates` key is *present* — `=false` would open it too — so there is no value for an
454
- argument to carry.)
491
+ Nothing about the **census** is choosable the clusters are the repository's, computed over
492
+ every run; one call returns them all. (The server reads only that the `near_duplicates` key is
493
+ *present* — `=false` would open it too — so there is no value for the ask to carry.) Which
494
+ **repository** is censused is the one choice there is:
495
+
496
+ | argument | |
497
+ | --- | --- |
498
+ | `repository` | census THIS repository (its numeric id from `list_repositories`) under the **agent key** (`SPECGUARD_AGENT_API_KEY`, `sga_…`), instead of the one the `sgk_…` key resolves to — omit it for the default, `sgk_`-keyed census |
499
+
500
+ The credential changes with `repository`, exactly as on `get_repository_overview`: the plural
501
+ endpoint answers under the agent key, whose mint-time granted repository set is the boundary the id
502
+ is looked up inside, and the two credential kinds refuse each other's tokens — so a
503
+ `repository` ask without the agent key is refused *here*, naming the variable, before any request
504
+ is made.
455
505
 
456
506
  Read the response with its own rules in mind:
457
507
 
@@ -471,9 +521,13 @@ Read the response with its own rules in mind:
471
521
  three silences — nothing ingested, nothing embedded, nothing alike — are kept distinguishable by
472
522
  `recorded_count` / `identity_count` / the list itself.
473
523
 
474
- Same credential and endpoint as `get_repository_overview` (`sgk_…` repository key on
475
- `GET /api/v1/repository`); the response is that endpoint's full body with the `near_duplicates`
476
- block opened, passed through unmodified.
524
+ Without `repository`, same credential and endpoint as `get_repository_overview`'s default
525
+ (`sgk_…` repository key on `GET /api/v1/repository`); the response is that body with the
526
+ `near_duplicates` block opened, passed through unmodified. With `repository`, the plural endpoint
527
+ answers under the agent key, exactly as on `get_repository_overview` — and carries that path's
528
+ one deliberate omission: `api_key` is **absent** from the plural body rather than nulled, because
529
+ the block describes the credential that made the request and this request was not made with a
530
+ repository key (an absent key there is the surface's shape, never a dropped block).
477
531
 
478
532
  ### `remove_repository`
479
533
 
@@ -489,8 +543,9 @@ Authorization is the `repo.delete` capability at **either surface** — an owner
489
543
  `repo.delete`, may remove the repository. A member without it is refused `403` with SpecGuard's own
490
544
  sentence, verbatim. The repository's CI keys stop authenticating the moment it succeeds.
491
545
 
492
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
493
- `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
546
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
547
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
548
+ accepts that key but does not require it (its agent key wins when both are set).
494
549
 
495
550
  ### `create_repository_api_key`
496
551
 
@@ -514,8 +569,9 @@ The body comes back as SpecGuard serves it: an `api_key` block (`name`, `token`,
514
569
  Authorization is the `keys_manage` capability; a member without it is refused `403` with SpecGuard's
515
570
  own sentence, verbatim.
516
571
 
517
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
518
- `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
572
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
573
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
574
+ accepts that key but does not require it (its agent key wins when both are set).
519
575
 
520
576
  ### `revoke_repository_api_key`
521
577
 
@@ -536,8 +592,9 @@ replacement with `create_repository_api_key` and deploy it BEFORE revoking the o
536
592
  first and the repository's CI is locked out until a human mints a new key in a browser. A `204`
537
593
  means the key is revoked.
538
594
 
539
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
540
- `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
595
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
596
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
597
+ accepts that key but does not require it (its agent key wins when both are set).
541
598
 
542
599
  ### `list_repository_agent_keys`
543
600
 
@@ -634,8 +691,9 @@ Authorization is the `members.manage` capability: a caller who is not a member i
634
691
  (the repository's existence stays hidden), and a member without `members.manage` is refused `403`
635
692
  with SpecGuard's own sentence, verbatim.
636
693
 
637
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
638
- `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
694
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
695
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
696
+ accepts that key but does not require it (its agent key wins when both are set).
639
697
 
640
698
  ### `add_repository_member`
641
699
 
@@ -659,8 +717,9 @@ On success (`201`) the response carries a `member` block whose **`id` is the mem
659
717
  `members.manage` capability; a member without it is refused `403` with SpecGuard's own sentence,
660
718
  verbatim.
661
719
 
662
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
663
- `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
720
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
721
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
722
+ accepts that key but does not require it (its agent key wins when both are set).
664
723
 
665
724
  ### `update_repository_member_permissions`
666
725
 
@@ -684,8 +743,9 @@ ids are scoped to the repository (a foreign id is refused `404`). Authorization
684
743
  `members.manage` capability; a member without it is refused `403` with SpecGuard's own sentence,
685
744
  verbatim.
686
745
 
687
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
688
- `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
746
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
747
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
748
+ accepts that key but does not require it (its agent key wins when both are set).
689
749
 
690
750
  ### `remove_repository_member`
691
751
 
@@ -707,8 +767,9 @@ number on the wire, so pass it as a string, not as a number (see the edit tool a
707
767
  Authorization is the `members.manage` capability; a member without it is refused `403` with
708
768
  SpecGuard's own sentence, verbatim.
709
769
 
710
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
711
- `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
770
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
771
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
772
+ accepts that key but does not require it (its agent key wins when both are set).
712
773
 
713
774
  ### `rename_repository`
714
775
 
@@ -728,8 +789,9 @@ the repository but may not rename it. The owner check also redeems a browser-iss
728
789
  (re-grant via the browser). A name another repository already holds is refused `400` — `taken`, not
729
790
  `409` — verbatim. The `200` body is `{repository: …}` in the same shape `list_repositories` serves.
730
791
 
731
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
732
- `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
792
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
793
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
794
+ accepts that key but does not require it (its agent key wins when both are set).
733
795
 
734
796
  ## How it works
735
797
 
@@ -744,11 +806,13 @@ reshapes no response — every tool returns the shape of the capability it wraps
744
806
  upstream reaches the agent without a release here.
745
807
 
746
808
  Authorization and project scoping are enforced by SpecGuard, never by this bridge, using keys you
747
- issue there — the same `sgk_…` keys CI uses to ingest runs, and, for the tools that answer to a
748
- person rather than to a repository, an `sgu_…` user key. Which of the two a request may carry is
749
- SpecGuard's decision and it is taken from the token's prefix, before any credential is looked up, so
750
- the bridge cannot widen either one's reach: it forwards the key the tool's own variable holds and
751
- reports what came back. It adds no credentials of its own and stores nothing.
809
+ issue there — the same `sgk_…` keys CI uses to ingest runs, an `sgu_…` user key for the tools that
810
+ answer to a person rather than to a repository, and an `sga_…` agent key for the tools that answer
811
+ for an automated agent it speaks for nobody, and its reach is the repository set granted onto it
812
+ at mint time. Which of the three a request may carry is SpecGuard's decision and it is taken from
813
+ the token's prefix, before any credential is looked up, so the bridge cannot widen any key's reach:
814
+ it forwards the key the tool's own variable holds and reports what came back. It adds no
815
+ credentials of its own and stores nothing.
752
816
 
753
817
  No argument ever reaches a shell: subprocesses are spawned with an argument list, so a path from a
754
818
  model is a path that does not exist rather than a command.
@@ -27,22 +27,26 @@
27
27
  * `SPECGUARD_ENDPOINT` wins when both are set and disagree, because it is the
28
28
  * one the rest of the toolchain is already reading.
29
29
  *
30
- * == TWO KEY VARIABLES, because SpecGuard has two credentials that refuse each
31
- * == other
30
+ * == THREE KEY VARIABLES, because SpecGuard has three credentials that refuse
31
+ * == each other
32
32
  *
33
33
  * `Api::BaseController` discriminates on the token's PREFIX *before any table is
34
34
  * read*, and answers 401 on a mismatch without a lookup: `sgk_` names ONE
35
35
  * repository (`GET /api/v1/repository`, `POST /api/v1/ingest`), `sgu_` names a
36
- * PERSON (`GET /api/v1/repositories`). `UserApiKey::TOKEN_PREFIX` says the two
37
- * prefixes are deliberately the same length so neither can be a prefix of the
38
- * other the mutual refusal is designed, not incidental.
36
+ * PERSON (`GET /api/v1/repositories` and its writes), and SPGD-952 — `sga_`
37
+ * names AN AGENT: nobody's account and no single repository, but an explicit
38
+ * set of repositories with a permission set, both fixed at mint time
39
+ * (`AgentApiKey::TOKEN_PREFIX` says all three prefixes are deliberately the same
40
+ * length so neither can be a prefix of another — the mutual refusal is designed,
41
+ * not incidental).
39
42
  *
40
- * One variable therefore cannot serve both: whichever kind it holds, the tools
41
- * needing the other kind 401. So there are two, read independently, and neither
42
- * is required — an operator who only ever calls the repository tool sets only
43
- * `SPECGUARD_API_KEY`, exactly as before this existed. Prefix-dispatching over a
44
- * single variable was the alternative and it cannot work: an operator wanting
45
- * both kinds of tool needs both keys present at once.
43
+ * One variable therefore cannot serve two kinds, let alone three: whichever
44
+ * kind it holds, the tools needing another kind 401. So there are three, read
45
+ * independently, and none is required — an operator who only ever calls the
46
+ * repository tool sets only `SPECGUARD_API_KEY`, exactly as before any of this
47
+ * existed. Prefix-dispatching over a single variable was the alternative and it
48
+ * cannot work: an operator wanting several kinds of tool needs several keys
49
+ * present at once.
46
50
  *
47
51
  * Which variable a tool reads is then carried onto `ApiConfig` alongside the
48
52
  * value — see `Credential` — for the same reason `endpointVariable` is: a
@@ -74,6 +78,18 @@ export interface Config {
74
78
  * accept the other's token, so the two values live in two places here too.
75
79
  */
76
80
  readonly userApiKey: string | undefined;
81
+ /**
82
+ * An `sga_…` agent API key, from `SPECGUARD_AGENT_API_KEY`. `undefined` when unset.
83
+ *
84
+ * A THIRD slot, and the same argument as the second one — see the note at the
85
+ * top of this file. It is minted from the account page's Agent keys panel
86
+ * (`/account`) and speaks for NOBODY: its reach is the repository set and
87
+ * permission set granted onto it at mint time, which is what makes it the
88
+ * credential an automated agent can hold without borrowing a person's rights.
89
+ * Neither deployment endpoint will accept the other token's kind, so the
90
+ * three values live in three places here too.
91
+ */
92
+ readonly agentApiKey: string | undefined;
77
93
  /**
78
94
  * The command that runs the `@intent` linter, already tokenised.
79
95
  *
@@ -126,23 +142,23 @@ export interface ApiConfig {
126
142
  readonly requestTimeoutMs: number;
127
143
  }
128
144
  export type EndpointVariable = "SPECGUARD_ENDPOINT" | "SPECGUARD_URL";
129
- export type ApiKeyVariable = "SPECGUARD_API_KEY" | "SPECGUARD_USER_API_KEY";
145
+ export type ApiKeyVariable = "SPECGUARD_API_KEY" | "SPECGUARD_USER_API_KEY" | "SPECGUARD_AGENT_API_KEY";
130
146
  /**
131
- * One of SpecGuard's two credential kinds, described well enough that a message
147
+ * One of SpecGuard's three credential kinds, described well enough that a message
132
148
  * about it can be written without knowing which one it is.
133
149
  *
134
150
  * The two prose fields are sentence FRAGMENTS rather than whole messages on
135
151
  * purpose: the surrounding wording — "is not set in the MCP server's
136
- * environment", "SpecGuard rejected the API key (401)" — is the same for both
137
- * kinds and is written once, at the site that knows the situation. Only the
138
- * parts that genuinely differ between an `sgk_` key and an `sgu_` key live
139
- * here.
152
+ * environment", "SpecGuard rejected the API key (401)" — is the same for every
153
+ * kind and is written once, at the site that knows the situation. Only the
154
+ * parts that genuinely differ between an `sgk_` key, an `sgu_` key and an
155
+ * `sga_` key live here.
140
156
  */
141
157
  export interface Credential {
142
158
  /** The environment variable this kind of key is read from. */
143
159
  readonly variable: ApiKeyVariable;
144
160
  /** The prefix SpecGuard requires of it, checked before any table is read. */
145
- readonly prefix: "sgk_" | "sgu_";
161
+ readonly prefix: "sgk_" | "sgu_" | "sga_";
146
162
  /** Completes "… issued from ___" in a message about the variable being unset. */
147
163
  readonly issuedFrom: string;
148
164
  /** Completes "… issued by <deployment> ___" in a message about a 401. */
@@ -165,6 +181,27 @@ export declare const REPOSITORY_CREDENTIAL: Credential;
165
181
  * than assuming it is lying around.
166
182
  */
167
183
  export declare const USER_CREDENTIAL: Credential;
184
+ /**
185
+ * The `sga_` key: an AGENT, and a credential that speaks for nobody.
186
+ *
187
+ * Like the `sgu_` key it is minted from the account page, but the two are not
188
+ * the same gesture and the message must not blur them: a person key speaks with
189
+ * the person's whole grantable surface, while an agent key reaches exactly the
190
+ * repository set and permission set granted onto it at mint time — which is the
191
+ * whole reason an automated agent holds this one rather than a person's. The
192
+ * `issuedFrom` fragment names the panel, because an operator sent to "the
193
+ * account page" for an `sga_` key lands on the person-key panel one scroll
194
+ * earlier and mints the wrong kind.
195
+ *
196
+ * The `rejection` fragment names the two refusals specific to this kind: the
197
+ * mutual prefix refusal (both other key kinds are refused here without a
198
+ * lookup), and the fact that a key whose grant was too narrow does NOT read as
199
+ * a 401 — a repository outside the key's set answers 404, deliberately
200
+ * indistinguishable from a nonexistent one, so a 401 here is about the KEY
201
+ * itself (wrong kind, revoked, or the owner archived) and never about which
202
+ * repositories it names.
203
+ */
204
+ export declare const AGENT_CREDENTIAL: Credential;
168
205
  /**
169
206
  * What a tool needing the `sgk_` REPOSITORY key requires — the endpoint and
170
207
  * `SPECGUARD_API_KEY`.
@@ -187,6 +224,51 @@ export declare function requireApiConfig(config: Config): ApiConfig;
187
224
  * situation differently.
188
225
  */
189
226
  export declare function requireUserApiConfig(config: Config): ApiConfig;
227
+ /**
228
+ * What a tool needing the `sga_` AGENT key requires — the endpoint and
229
+ * `SPECGUARD_AGENT_API_KEY`.
230
+ *
231
+ * The third sibling over the one shared body, for the reason the file's own
232
+ * header anticipated: a tool asks for what IT needs, startup still validates
233
+ * nothing, and a fix to the diagnostics reaches all three because they cannot
234
+ * drift into describing the same situation differently. A repository-scoped
235
+ * tool asked about a repository its `sgk_` slot does not name calls this, and
236
+ * reads the plural endpoints the agent credential is served by.
237
+ */
238
+ export declare function requireAgentApiConfig(config: Config): ApiConfig;
239
+ /**
240
+ * What a tool EITHER credential serves requires — the endpoint and at least one
241
+ * of `SPECGUARD_AGENT_API_KEY` / `SPECGUARD_USER_API_KEY`.
242
+ *
243
+ * The plural endpoints accept both key kinds and bound the answer by whichever
244
+ * was presented — the person's `accessible_by` set for an `sgu_` key, the key's
245
+ * own granted set for an `sga_` one — so the tool's question ("which
246
+ * repositories may I ask about") has a true answer under either credential, and
247
+ * refusing until the operator picks one would be inventing a requirement the
248
+ * deployment does not have.
249
+ *
250
+ * When BOTH are set, the agent key wins, and the reason is consistency of
251
+ * scope rather than preference: every other agent-keyed tool answers inside the
252
+ * key's granted set, so a listing served from the person's wider set would
253
+ * advertise repositories the agent then cannot open — discovery promising more
254
+ * than the tools that follow it can deliver. The agent set is a subset of its
255
+ * owner's by mint-time validation, so this never widens what the answer names,
256
+ * only keeps it to what the agent can actually act on. The variable is new, so
257
+ * no existing operator's behavior changes: today nobody sets it, and setting it
258
+ * is a deliberate act this precedence honours.
259
+ *
260
+ * When NEITHER is set, the one-message rule applies at double width: both
261
+ * variables are named, because telling an operator who is expected to choose
262
+ * between two spellings about only one of them would have them fix a variable,
263
+ * re-call, and be told about the other — the round-trip waste the shared body's
264
+ * note above exists to prevent. That sentence is NOT written here: the helper
265
+ * falls through to the shared body with the credentials it should name, so the
266
+ * wording is produced once, by the same code the other two entry points use,
267
+ * and the one invariant this file asserts about its diagnostics — a fix reaches
268
+ * every caller because no caller describes the situation itself — holds for
269
+ * this helper too. The endpoint joins the same sentence when it is missing too.
270
+ */
271
+ export declare function requireUserOrAgentApiConfig(config: Config): ApiConfig;
190
272
  /**
191
273
  * Splits a configured command into argv WITHOUT a shell, honouring single and
192
274
  * double quotes so a path with a space survives.