specguard-mcp 0.1.12 → 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,42 +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
- The credential decides which repositories are *in* the answer (owned, plus shared with them
335
- through a membership); the three optional asks below narrow **within** that set never around it.
336
- The endpoint has served them since SpecGuard's SPGD-940 (`ef6236d`), composed through the same
337
- `RepositoryNarrowing` concern the web grid reads, and this tool forwards them rather than
338
- re-deriving them. A repository the person neither owns nor was given access to never enters the
339
- response, so no ask can filter it *in* either. A blank value is no ask: passing an empty string
340
- and omitting the argument make the identical request.
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.
341
362
 
342
363
  | argument | |
343
364
  | --- | --- |
344
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 |
345
- | `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) |
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 |
346
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 |
347
368
 
348
369
  The body comes back as SpecGuard serves it — `{"repositories": […]}`, each entry carrying `id`,
349
370
  `full_name`, `name`, `registered_at` and `role`, ordered by `full_name` ascending unless `sort`
350
371
  asks otherwise. The first four are
351
372
  deliberately the same four fields, under the same names, that `get_repository_overview` serves in its
352
- own `repository` block, so a client that reads one reads the other. `role` is `owner` or `member`:
353
- the list mixes repositories this person owns with repositories somebody shared with them, and nothing
354
- else tells them apart read it before assuming a repository is one you may administer. An empty list
355
- means no access, not an error.
356
-
357
- **It reads a different key from `get_repository_overview`.** `SPECGUARD_USER_API_KEY` (`sgu_…`), not
358
- `SPECGUARD_API_KEY` (`sgk_…`). SpecGuard refuses each credential in the other's place — the prefix
359
- decides which table is consulted before any of them is read — so the two are not interchangeable and
360
- setting one does not stand in for the other. Every message this tool produces names the variable
361
- *it* reads, so a `401` here never sends you to check the key `get_repository_overview` uses.
362
-
363
- Registering a repository is `add_repository`, below it reads the same `sgu_…` key and takes the
364
- `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`,
365
394
  `create_repository_api_key`, `revoke_repository_api_key`) are below too, on the same key; a tool
366
395
  here is a promise the agent will act on, so each waits for the capability rather than the other way
367
396
  round.
@@ -405,8 +434,9 @@ non-blank string; SpecGuard validates the name and refuses an unusable one in it
405
434
  format rule on this side would be free to drift from the one that actually decides, and would surface
406
435
  as this bridge rejecting a name the platform would have accepted.
407
436
 
408
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and a
409
- 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.
410
440
 
411
441
  ### `registrable_repositories`
412
442
 
@@ -440,8 +470,9 @@ was one (first-time setup), a populated grant with `stale: true` means an existi
440
470
  parameters; which repositories are in the answer is decided by SpecGuard from the person the key
441
471
  speaks for.
442
472
 
443
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
444
- `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).
445
476
 
446
477
  ### `near_duplicate_clusters`
447
478
 
@@ -457,10 +488,20 @@ and answers `near_duplicates: null` on the plain overview. Calling this tool **i
457
488
  `get_repository_overview` never sends it, so an agent reading the overview cannot pay the census by
458
489
  accident.
459
490
 
460
- **This tool takes no arguments** — nothing about the census is choosable. The clusters are the
461
- repository's, computed over every run; one call returns them all. (The server reads only that the
462
- `near_duplicates` key is *present* — `=false` would open it too — so there is no value for an
463
- 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.
464
505
 
465
506
  Read the response with its own rules in mind:
466
507
 
@@ -480,9 +521,13 @@ Read the response with its own rules in mind:
480
521
  three silences — nothing ingested, nothing embedded, nothing alike — are kept distinguishable by
481
522
  `recorded_count` / `identity_count` / the list itself.
482
523
 
483
- Same credential and endpoint as `get_repository_overview` (`sgk_…` repository key on
484
- `GET /api/v1/repository`); the response is that endpoint's full body with the `near_duplicates`
485
- 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).
486
531
 
487
532
  ### `remove_repository`
488
533
 
@@ -498,8 +543,9 @@ Authorization is the `repo.delete` capability at **either surface** — an owner
498
543
  `repo.delete`, may remove the repository. A member without it is refused `403` with SpecGuard's own
499
544
  sentence, verbatim. The repository's CI keys stop authenticating the moment it succeeds.
500
545
 
501
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
502
- `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).
503
549
 
504
550
  ### `create_repository_api_key`
505
551
 
@@ -523,8 +569,9 @@ The body comes back as SpecGuard serves it: an `api_key` block (`name`, `token`,
523
569
  Authorization is the `keys_manage` capability; a member without it is refused `403` with SpecGuard's
524
570
  own sentence, verbatim.
525
571
 
526
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
527
- `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).
528
575
 
529
576
  ### `revoke_repository_api_key`
530
577
 
@@ -545,8 +592,9 @@ replacement with `create_repository_api_key` and deploy it BEFORE revoking the o
545
592
  first and the repository's CI is locked out until a human mints a new key in a browser. A `204`
546
593
  means the key is revoked.
547
594
 
548
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
549
- `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).
550
598
 
551
599
  ### `list_repository_agent_keys`
552
600
 
@@ -643,8 +691,9 @@ Authorization is the `members.manage` capability: a caller who is not a member i
643
691
  (the repository's existence stays hidden), and a member without `members.manage` is refused `403`
644
692
  with SpecGuard's own sentence, verbatim.
645
693
 
646
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
647
- `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).
648
697
 
649
698
  ### `add_repository_member`
650
699
 
@@ -668,8 +717,9 @@ On success (`201`) the response carries a `member` block whose **`id` is the mem
668
717
  `members.manage` capability; a member without it is refused `403` with SpecGuard's own sentence,
669
718
  verbatim.
670
719
 
671
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
672
- `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).
673
723
 
674
724
  ### `update_repository_member_permissions`
675
725
 
@@ -693,8 +743,9 @@ ids are scoped to the repository (a foreign id is refused `404`). Authorization
693
743
  `members.manage` capability; a member without it is refused `403` with SpecGuard's own sentence,
694
744
  verbatim.
695
745
 
696
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
697
- `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).
698
749
 
699
750
  ### `remove_repository_member`
700
751
 
@@ -716,8 +767,9 @@ number on the wire, so pass it as a string, not as a number (see the edit tool a
716
767
  Authorization is the `members.manage` capability; a member without it is refused `403` with
717
768
  SpecGuard's own sentence, verbatim.
718
769
 
719
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
720
- `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).
721
773
 
722
774
  ### `rename_repository`
723
775
 
@@ -737,8 +789,9 @@ the repository but may not rename it. The owner check also redeems a browser-iss
737
789
  (re-grant via the browser). A name another repository already holds is refused `400` — `taken`, not
738
790
  `409` — verbatim. The `200` body is `{repository: …}` in the same shape `list_repositories` serves.
739
791
 
740
- It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
741
- `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).
742
795
 
743
796
  ## How it works
744
797
 
@@ -753,11 +806,13 @@ reshapes no response — every tool returns the shape of the capability it wraps
753
806
  upstream reaches the agent without a release here.
754
807
 
755
808
  Authorization and project scoping are enforced by SpecGuard, never by this bridge, using keys you
756
- issue there — the same `sgk_…` keys CI uses to ingest runs, and, for the tools that answer to a
757
- person rather than to a repository, an `sgu_…` user key. Which of the two a request may carry is
758
- SpecGuard's decision and it is taken from the token's prefix, before any credential is looked up, so
759
- the bridge cannot widen either one's reach: it forwards the key the tool's own variable holds and
760
- 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.
761
816
 
762
817
  No argument ever reaches a shell: subprocesses are spawned with an argument list, so a path from a
763
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.