specguard-mcp 0.1.12 → 0.1.14

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,69 @@ 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
- `full_name`, `name`, `registered_at` and `role`, ordered by `full_name` ascending unless `sort`
350
- asks otherwise. The first four are
351
- 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`,
370
+ `full_name`, `name`, `registered_at`, `role`, `delivery_health` and `latest_run`, ordered by
371
+ `full_name` ascending unless `sort` asks otherwise. The identity fields are deliberately the same
372
+ fields, under the same names, that `get_repository_overview` serves in its own `repository` block,
373
+ so a client that reads one reads the other.
374
+
375
+ `delivery_health` is the entry's delivery verdict `refusing` beside `last_rejection_at` served
376
+ on **every** entry, so this one call triages ingest-pipeline health across the whole reachable set
377
+ instead of paying one `get_repository_overview` call per repository. It is the coarse sibling of
378
+ the overview's fuller per-repository block. A quiet verdict is a finding, not a gap:
379
+ `refusing: false` means nothing was refused, never that delivery is untracked.
380
+
381
+ `latest_run` is the entry's newest run when CI last reported, on what branch and commit, how big
382
+ the suite is, how much of it SpecGuard can read, and the run-level cost scalars. `null` means CI
383
+ has **never reported** for that repository — never a run that found an empty suite.
384
+
385
+ `role` has one value per
386
+ credential kind: under a **user** key (`sgu_…`) it is `owner` or `member` — the list mixes
387
+ repositories this person owns with repositories somebody shared with them, and nothing else tells
388
+ them apart — while under an **agent** key (`sga_…`) every entry is `agent`, the value that says the
389
+ ownership question does not apply because the key speaks for nobody (branching on owner/member
390
+ correctly reads false for both). Read it before assuming a repository is one you may administer.
391
+
392
+ Under an **agent** key the top level also carries `credential.capabilities` — the calling key's own
393
+ grant, read through the server's policy, so an agent key learns what it was granted here instead of
394
+ discovering its permissions by hitting refusals; under a **user** key the block is absent, not
395
+ `null` — a person key has no mint-time permission set, and a null would assert one exists and is
396
+ empty.
397
+
398
+ An empty list means no access, not an error.
399
+
400
+ **It reads a different key from `get_repository_overview` — either of two, whichever is set.**
401
+ `SPECGUARD_AGENT_API_KEY` (`sga_…`) when it is set: the answer is then the repository set granted
402
+ onto that key at mint time, the same set every other agent-keyed call answers inside, so discovery
403
+ and reach always agree. `SPECGUARD_USER_API_KEY` (`sgu_…`) when no agent key is set: the answer is
404
+ what that person may open. With both set the **agent** key wins. Neither is ever a stand-in for
405
+ `SPECGUARD_API_KEY` (`sgk_…`) — SpecGuard refuses each credential kind in the other's place, the
406
+ prefix deciding which table is consulted before any of them is read — and every message this tool
407
+ produces names the variable *it* read, so a `401` here never sends you to check a key this tool
408
+ never used.
409
+
410
+ Registering a repository is `add_repository`, below — it reads `SPECGUARD_USER_API_KEY` (an
411
+ `sgu_…` key, which this tool also accepts but does not require) and takes the `full_name` this
412
+ tool reports. Removal and the key lifecycle (`remove_repository`,
365
413
  `create_repository_api_key`, `revoke_repository_api_key`) are below too, on the same key; a tool
366
414
  here is a promise the agent will act on, so each waits for the capability rather than the other way
367
415
  round.
@@ -405,8 +453,9 @@ non-blank string; SpecGuard validates the name and refuses an unusable one in it
405
453
  format rule on this side would be free to drift from the one that actually decides, and would surface
406
454
  as this bridge rejecting a name the platform would have accepted.
407
455
 
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.
456
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) a **person** key, which `list_repositories` also
457
+ accepts but does not require — and a different one from the `sgk_…` key `get_repository_overview`
458
+ uses.
410
459
 
411
460
  ### `registrable_repositories`
412
461
 
@@ -440,8 +489,9 @@ was one (first-time setup), a populated grant with `stale: true` means an existi
440
489
  parameters; which repositories are in the answer is decided by SpecGuard from the person the key
441
490
  speaks for.
442
491
 
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.
492
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
493
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
494
+ accepts that key but does not require it (its agent key wins when both are set).
445
495
 
446
496
  ### `near_duplicate_clusters`
447
497
 
@@ -457,10 +507,20 @@ and answers `near_duplicates: null` on the plain overview. Calling this tool **i
457
507
  `get_repository_overview` never sends it, so an agent reading the overview cannot pay the census by
458
508
  accident.
459
509
 
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.)
510
+ Nothing about the **census** is choosable the clusters are the repository's, computed over
511
+ every run; one call returns them all. (The server reads only that the `near_duplicates` key is
512
+ *present* — `=false` would open it too — so there is no value for the ask to carry.) Which
513
+ **repository** is censused is the one choice there is:
514
+
515
+ | argument | |
516
+ | --- | --- |
517
+ | `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 |
518
+
519
+ The credential changes with `repository`, exactly as on `get_repository_overview`: the plural
520
+ endpoint answers under the agent key, whose mint-time granted repository set is the boundary the id
521
+ is looked up inside, and the two credential kinds refuse each other's tokens — so a
522
+ `repository` ask without the agent key is refused *here*, naming the variable, before any request
523
+ is made.
464
524
 
465
525
  Read the response with its own rules in mind:
466
526
 
@@ -480,9 +540,13 @@ Read the response with its own rules in mind:
480
540
  three silences — nothing ingested, nothing embedded, nothing alike — are kept distinguishable by
481
541
  `recorded_count` / `identity_count` / the list itself.
482
542
 
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.
543
+ Without `repository`, same credential and endpoint as `get_repository_overview`'s default
544
+ (`sgk_…` repository key on `GET /api/v1/repository`); the response is that body with the
545
+ `near_duplicates` block opened, passed through unmodified. With `repository`, the plural endpoint
546
+ answers under the agent key, exactly as on `get_repository_overview` — and carries that path's
547
+ one deliberate omission: `api_key` is **absent** from the plural body rather than nulled, because
548
+ the block describes the credential that made the request and this request was not made with a
549
+ repository key (an absent key there is the surface's shape, never a dropped block).
486
550
 
487
551
  ### `remove_repository`
488
552
 
@@ -498,8 +562,9 @@ Authorization is the `repo.delete` capability at **either surface** — an owner
498
562
  `repo.delete`, may remove the repository. A member without it is refused `403` with SpecGuard's own
499
563
  sentence, verbatim. The repository's CI keys stop authenticating the moment it succeeds.
500
564
 
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.
565
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
566
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
567
+ accepts that key but does not require it (its agent key wins when both are set).
503
568
 
504
569
  ### `create_repository_api_key`
505
570
 
@@ -523,8 +588,9 @@ The body comes back as SpecGuard serves it: an `api_key` block (`name`, `token`,
523
588
  Authorization is the `keys_manage` capability; a member without it is refused `403` with SpecGuard's
524
589
  own sentence, verbatim.
525
590
 
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.
591
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
592
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
593
+ accepts that key but does not require it (its agent key wins when both are set).
528
594
 
529
595
  ### `revoke_repository_api_key`
530
596
 
@@ -545,8 +611,9 @@ replacement with `create_repository_api_key` and deploy it BEFORE revoking the o
545
611
  first and the repository's CI is locked out until a human mints a new key in a browser. A `204`
546
612
  means the key is revoked.
547
613
 
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.
614
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
615
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
616
+ accepts that key but does not require it (its agent key wins when both are set).
550
617
 
551
618
  ### `list_repository_agent_keys`
552
619
 
@@ -643,8 +710,9 @@ Authorization is the `members.manage` capability: a caller who is not a member i
643
710
  (the repository's existence stays hidden), and a member without `members.manage` is refused `403`
644
711
  with SpecGuard's own sentence, verbatim.
645
712
 
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.
713
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
714
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
715
+ accepts that key but does not require it (its agent key wins when both are set).
648
716
 
649
717
  ### `add_repository_member`
650
718
 
@@ -668,8 +736,9 @@ On success (`201`) the response carries a `member` block whose **`id` is the mem
668
736
  `members.manage` capability; a member without it is refused `403` with SpecGuard's own sentence,
669
737
  verbatim.
670
738
 
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.
739
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
740
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
741
+ accepts that key but does not require it (its agent key wins when both are set).
673
742
 
674
743
  ### `update_repository_member_permissions`
675
744
 
@@ -693,8 +762,9 @@ ids are scoped to the repository (a foreign id is refused `404`). Authorization
693
762
  `members.manage` capability; a member without it is refused `403` with SpecGuard's own sentence,
694
763
  verbatim.
695
764
 
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.
765
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
766
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
767
+ accepts that key but does not require it (its agent key wins when both are set).
698
768
 
699
769
  ### `remove_repository_member`
700
770
 
@@ -716,8 +786,9 @@ number on the wire, so pass it as a string, not as a number (see the edit tool a
716
786
  Authorization is the `members.manage` capability; a member without it is refused `403` with
717
787
  SpecGuard's own sentence, verbatim.
718
788
 
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.
789
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
790
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
791
+ accepts that key but does not require it (its agent key wins when both are set).
721
792
 
722
793
  ### `rename_repository`
723
794
 
@@ -737,8 +808,9 @@ the repository but may not rename it. The owner check also redeems a browser-iss
737
808
  (re-grant via the browser). A name another repository already holds is refused `400` — `taken`, not
738
809
  `409` — verbatim. The `200` body is `{repository: …}` in the same shape `list_repositories` serves.
739
810
 
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.
811
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`) the same **person** key `add_repository` reads, and
812
+ a different one from the `sgk_…` key `get_repository_overview` uses; `list_repositories` also
813
+ accepts that key but does not require it (its agent key wins when both are set).
742
814
 
743
815
  ## How it works
744
816
 
@@ -753,11 +825,13 @@ reshapes no response — every tool returns the shape of the capability it wraps
753
825
  upstream reaches the agent without a release here.
754
826
 
755
827
  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.
828
+ issue there — the same `sgk_…` keys CI uses to ingest runs, an `sgu_…` user key for the tools that
829
+ answer to a person rather than to a repository, and an `sga_…` agent key for the tools that answer
830
+ for an automated agent it speaks for nobody, and its reach is the repository set granted onto it
831
+ at mint time. Which of the three a request may carry is SpecGuard's decision and it is taken from
832
+ the token's prefix, before any credential is looked up, so the bridge cannot widen any key's reach:
833
+ it forwards the key the tool's own variable holds and reports what came back. It adds no
834
+ credentials of its own and stores nothing.
761
835
 
762
836
  No argument ever reaches a shell: subprocesses are spawned with an argument list, so a path from a
763
837
  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.