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 +135 -61
- package/dist/src/config.d.ts +100 -18
- package/dist/src/config.js +125 -5
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +7 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/support/specguard-api.d.ts +2 -2
- package/dist/src/support/specguard-api.js +11 -10
- package/dist/src/support/specguard-api.js.map +1 -1
- package/dist/src/tools/add-repository.js +3 -2
- package/dist/src/tools/add-repository.js.map +1 -1
- package/dist/src/tools/index.d.ts +23 -19
- package/dist/src/tools/index.js +23 -19
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-repositories.d.ts +93 -31
- package/dist/src/tools/list-repositories.js +140 -47
- package/dist/src/tools/list-repositories.js.map +1 -1
- package/dist/src/tools/list-repository-members.js +3 -2
- package/dist/src/tools/list-repository-members.js.map +1 -1
- package/dist/src/tools/near-duplicate-clusters.d.ts +20 -8
- package/dist/src/tools/near-duplicate-clusters.js +70 -23
- package/dist/src/tools/near-duplicate-clusters.js.map +1 -1
- package/dist/src/tools/registrable-repositories.js +2 -2
- package/dist/src/tools/registrable-repositories.js.map +1 -1
- package/dist/src/tools/repository-overview.d.ts +41 -1
- package/dist/src/tools/repository-overview.js +107 -6
- package/dist/src/tools/repository-overview.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
response, so no ask can filter it *in* either. A
|
|
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 `
|
|
350
|
-
asks otherwise. The
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
`
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
`
|
|
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_…`)
|
|
409
|
-
different one from the `sgk_…` key `get_repository_overview`
|
|
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_…`)
|
|
444
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
484
|
-
`GET /api/v1/repository`); the response is that
|
|
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_…`)
|
|
502
|
-
|
|
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_…`)
|
|
527
|
-
|
|
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_…`)
|
|
549
|
-
|
|
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_…`)
|
|
647
|
-
|
|
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_…`)
|
|
672
|
-
|
|
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_…`)
|
|
697
|
-
|
|
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_…`)
|
|
720
|
-
|
|
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_…`)
|
|
741
|
-
|
|
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,
|
|
757
|
-
person rather than to a repository, an `
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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.
|
package/dist/src/config.d.ts
CHANGED
|
@@ -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
|
-
* ==
|
|
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`)
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
41
|
-
*
|
|
42
|
-
* is required — an operator who only ever calls the
|
|
43
|
-
* `SPECGUARD_API_KEY`, exactly as before
|
|
44
|
-
* single variable was the alternative and it
|
|
45
|
-
*
|
|
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
|
|
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
|
|
137
|
-
*
|
|
138
|
-
* parts that genuinely differ between an `sgk_` key
|
|
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.
|