specguard-mcp 0.1.3 → 0.1.5

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.
Files changed (41) hide show
  1. package/README.md +255 -6
  2. package/dist/src/support/specguard-api.d.ts +36 -0
  3. package/dist/src/support/specguard-api.js +59 -4
  4. package/dist/src/support/specguard-api.js.map +1 -1
  5. package/dist/src/tools/add-repository-member.d.ts +31 -0
  6. package/dist/src/tools/add-repository-member.js +105 -0
  7. package/dist/src/tools/add-repository-member.js.map +1 -0
  8. package/dist/src/tools/args.d.ts +26 -0
  9. package/dist/src/tools/args.js +52 -0
  10. package/dist/src/tools/args.js.map +1 -1
  11. package/dist/src/tools/create-repository-api-key.d.ts +26 -0
  12. package/dist/src/tools/create-repository-api-key.js +83 -0
  13. package/dist/src/tools/create-repository-api-key.js.map +1 -0
  14. package/dist/src/tools/index.d.ts +96 -11
  15. package/dist/src/tools/index.js +116 -11
  16. package/dist/src/tools/index.js.map +1 -1
  17. package/dist/src/tools/list-repository-members.d.ts +32 -0
  18. package/dist/src/tools/list-repository-members.js +76 -0
  19. package/dist/src/tools/list-repository-members.js.map +1 -0
  20. package/dist/src/tools/near-duplicate-clusters.d.ts +58 -0
  21. package/dist/src/tools/near-duplicate-clusters.js +116 -0
  22. package/dist/src/tools/near-duplicate-clusters.js.map +1 -0
  23. package/dist/src/tools/registrable-repositories.d.ts +51 -0
  24. package/dist/src/tools/registrable-repositories.js +92 -0
  25. package/dist/src/tools/registrable-repositories.js.map +1 -0
  26. package/dist/src/tools/remove-repository-member.d.ts +30 -0
  27. package/dist/src/tools/remove-repository-member.js +93 -0
  28. package/dist/src/tools/remove-repository-member.js.map +1 -0
  29. package/dist/src/tools/remove-repository.d.ts +33 -0
  30. package/dist/src/tools/remove-repository.js +81 -0
  31. package/dist/src/tools/remove-repository.js.map +1 -0
  32. package/dist/src/tools/rename-repository.d.ts +44 -0
  33. package/dist/src/tools/rename-repository.js +99 -0
  34. package/dist/src/tools/rename-repository.js.map +1 -0
  35. package/dist/src/tools/revoke-repository-api-key.d.ts +29 -0
  36. package/dist/src/tools/revoke-repository-api-key.js +85 -0
  37. package/dist/src/tools/revoke-repository-api-key.js.map +1 -0
  38. package/dist/src/tools/update-repository-member-permissions.d.ts +30 -0
  39. package/dist/src/tools/update-repository-member-permissions.js +97 -0
  40. package/dist/src/tools/update-repository-member-permissions.js.map +1 -0
  41. package/package.json +2 -1
package/README.md CHANGED
@@ -32,9 +32,9 @@ refuses to boot and takes the tools that needed no configuration down with it.
32
32
 
33
33
  | Variable | Needed by | Default | What it is |
34
34
  | --- | --- | --- | --- |
35
- | `SPECGUARD_ENDPOINT` | `get_repository_overview`, `list_repositories` | — | your SpecGuard instance's root URL, **including the scheme** — e.g. `https://specguard.example.com`, or `http://localhost:3000`. A value with no scheme is refused by name (`SPECGUARD_ENDPOINT is not a usable URL: "sg.example.com"`) rather than surfacing later as an opaque failure. `SPECGUARD_URL` is accepted as an alias, and is the name every message uses when it is the one you set. A blank value counts as unset, so leaving `SPECGUARD_ENDPOINT` empty in a templated config falls through to `SPECGUARD_URL` instead of suppressing it |
35
+ | `SPECGUARD_ENDPOINT` | `get_repository_overview`, `list_repositories`, `add_repository`, `registrable_repositories` | — | your SpecGuard instance's root URL, **including the scheme** — e.g. `https://specguard.example.com`, or `http://localhost:3000`. A value with no scheme is refused by name (`SPECGUARD_ENDPOINT is not a usable URL: "sg.example.com"`) rather than surfacing later as an opaque failure. `SPECGUARD_URL` is accepted as an alias, and is the name every message uses when it is the one you set. A blank value counts as unset, so leaving `SPECGUARD_ENDPOINT` empty in a templated config falls through to `SPECGUARD_URL` instead of suppressing it |
36
36
  | `SPECGUARD_API_KEY` | `get_repository_overview` | — | an agent/CI API key (`sgk_…`) issued by that deployment |
37
- | `SPECGUARD_USER_API_KEY` | `list_repositories` | — | a **user** API key (`sgu_…`), minted from that deployment's account page. A different credential from the one above, not a second place to put the same value: SpecGuard decides which of them a request may use from the token's prefix, before it reads anything, and answers `401` for the other one. Set whichever your tools need — both, if you use both |
37
+ | `SPECGUARD_USER_API_KEY` | `list_repositories`, `add_repository`, `registrable_repositories`, `remove_repository`, `create_repository_api_key`, `revoke_repository_api_key`, `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
38
  | `SPECGUARD_LINT_COMMAND` | `lint_intent_annotations` | `specguard-lint` | the command that runs the linter. Most Ruby projects need `bundle exec specguard-lint` |
39
39
  | `SPECGUARD_TIMEOUT_MS` | HTTP tools | `30000` | how long a call to SpecGuard may take |
40
40
 
@@ -351,10 +351,11 @@ decides which table is consulted before any of them is read — so the two are n
351
351
  setting one does not stand in for the other. Every message this tool produces names the variable
352
352
  *it* reads, so a `401` here never sends you to check the key `get_repository_overview` uses.
353
353
 
354
- Registering a repository is now `add_repository`, below — it reads the same `sgu_…` key and takes the
355
- `full_name` this tool reports. Revoking keys and the rest of the user-scoped surface are still absent,
356
- because their endpoints have not shipped: a tool here is a promise the agent will act on, so it waits
357
- for the capability rather than the other way round.
354
+ Registering a repository is `add_repository`, below — it reads the same `sgu_…` key and takes the
355
+ `full_name` this tool reports. Removal and the key lifecycle (`remove_repository`,
356
+ `create_repository_api_key`, `revoke_repository_api_key`) are below too, on the same key; a tool
357
+ here is a promise the agent will act on, so each waits for the capability rather than the other way
358
+ round.
358
359
 
359
360
  ### `add_repository`
360
361
 
@@ -398,6 +399,254 @@ as this bridge rejecting a name the platform would have accepted.
398
399
  It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and a
399
400
  different one from the `sgk_…` key `get_repository_overview` uses.
400
401
 
402
+ ### `registrable_repositories`
403
+
404
+ Lists the GitHub repositories the person behind `SPECGUARD_USER_API_KEY` **could** register with
405
+ SpecGuard — the set the registration gate would consult, read out loud in advance, so an agent can
406
+ pick a `full_name` for `add_repository` from a real answer rather than by guessing. `list_repositories`
407
+ reports what *is* registered; this reports what could be, and the two answer different questions.
408
+
409
+ The body comes back as SpecGuard serves it: `{"repositories": […]}` with each entry carrying
410
+ `full_name` and `registered`, ordered by `full_name` ascending, plus a `grant` block (`captured_at`,
411
+ `expires_at`, `stale`) describing the stored record of this person's GitHub permissions.
412
+
413
+ **`registered` is asked globally, not just of your own repositories.** An entry marked
414
+ `registered: true` was registered by *somebody* — possibly someone else — and a POST naming it will
415
+ be refused with `has already been taken`. That is exactly why entries are *marked* rather than
416
+ excluded: a reading scoped to what you can open would send you at a name nobody can register.
417
+
418
+ **A name appearing here is not a promise the write will succeed.** This is the set the gate would
419
+ consult at the moment of the read; the repository may be registered by someone else between this
420
+ call and your POST.
421
+
422
+ **A missing or stale grant is not an error — it is the modal first answer.** SpecGuard fails closed
423
+ when it has no current record of your GitHub permissions, which is every person who has not opened
424
+ SpecGuard in a browser recently. The call then answers `403` with SpecGuard's own sentence naming
425
+ the fix: *sign in to SpecGuard in a browser and reconnect GitHub, then try again*. On that refusal
426
+ the body still carries `grant`, and it distinguishes the two cases: `grant: null` means there never
427
+ was one (first-time setup), a populated grant with `stale: true` means an existing connection lapsed
428
+ — same remedy, very different urgency. Read it before telling the person what to do.
429
+
430
+ **This tool takes no arguments** — the credential is the whole of the scope. The endpoint takes no
431
+ parameters; which repositories are in the answer is decided by SpecGuard from the person the key
432
+ speaks for.
433
+
434
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
435
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
436
+
437
+ ### `near_duplicate_clusters`
438
+
439
+ Runs SpecGuard's near-duplicate census over a repository's tests — which tests *read alike* (same
440
+ body text, whatever file they sit in), clustered by similarity. Answers the refactoring question
441
+ the overview's per-run rankings cannot: where the same test is written twice, before you delete or
442
+ merge anything.
443
+
444
+ **This is the expensive read in this toolset.** The census is linear but measured in seconds —
445
+ seven queries at every size, tens of seconds extrapolated at the 20,000-test design point — which
446
+ is why the platform serves it only to a client that asks (`?near_duplicates=`, shipped by SPGD-703)
447
+ and answers `near_duplicates: null` on the plain overview. Calling this tool **is** the ask;
448
+ `get_repository_overview` never sends it, so an agent reading the overview cannot pay the census by
449
+ accident.
450
+
451
+ **This tool takes no arguments** — nothing about the census is choosable. The clusters are the
452
+ repository's, computed over every run; one call returns them all. (The server reads only that the
453
+ `near_duplicates` key is *present* — `=false` would open it too — so there is no value for an
454
+ argument to carry.)
455
+
456
+ Read the response with its own rules in mind:
457
+
458
+ - `similarity_floor` and `similarity_basis` sit **first** in the block and qualify every figure
459
+ below them — a cluster count without what "similar" meant is a count you cannot act on.
460
+ - `member_count` (texts in the repository, across every run) and `example_count` (examples in the
461
+ one run `weighed_run_id` names) are **different grains**: a three-example table-driven loop is
462
+ one member and three examples. Never fold them.
463
+ - `truncated: true` means the `clusters` list was cut at the cap while the counts above it
464
+ (`cluster_count`, `identity_count`, `clustered_*`) describe the whole census.
465
+ - `unobserved_members: true` on a cluster says a member identity the weighed run did not observe
466
+ (deleted, renamed, deselected) is still listed — do not reconcile the member list against that
467
+ run's examples.
468
+ - `similarity_range` is `[strongest, weakest]`: membership is transitive, similarity is not.
469
+ - `total_seconds` is `null` where nothing was timed — never a zero that would read as free.
470
+ - `clusters: []` with a real `identity_count` is the **success** state (nothing reads alike); the
471
+ three silences — nothing ingested, nothing embedded, nothing alike — are kept distinguishable by
472
+ `recorded_count` / `identity_count` / the list itself.
473
+
474
+ Same credential and endpoint as `get_repository_overview` (`sgk_…` repository key on
475
+ `GET /api/v1/repository`); the response is that endpoint's full body with the `near_duplicates`
476
+ block opened, passed through unmodified.
477
+
478
+ ### `remove_repository`
479
+
480
+ Removes a repository from SpecGuard — and with it **every key, run and intent on it**. This is the
481
+ destructive gesture in this toolset: irreversible, no undo, and a `204` means the repository and its
482
+ history are gone short of re-registering from scratch. Confirm with the user before calling.
483
+
484
+ | argument | |
485
+ | --- | --- |
486
+ | `repository_id` | the repository to remove — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
487
+
488
+ Authorization is the `repo.delete` capability at **either surface** — an owner, or a member granted
489
+ `repo.delete`, may remove the repository. A member without it is refused `403` with SpecGuard's own
490
+ sentence, verbatim. The repository's CI keys stop authenticating the moment it succeeds.
491
+
492
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
493
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
494
+
495
+ ### `create_repository_api_key`
496
+
497
+ Mints a new CI API key (`sgk_…`) for a SpecGuard repository and returns it alongside the
498
+ repository's existing keys. Minting does not disturb existing keys — each key on a repository
499
+ authenticates independently until revoked.
500
+
501
+ | argument | |
502
+ | --- | --- |
503
+ | `repository_id` | the repository to mint the key for — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
504
+ | `name` | an optional label for the key; omit it to let SpecGuard use its default name |
505
+
506
+ The body comes back as SpecGuard serves it: an `api_key` block (`name`, `token`, `hint`,
507
+ `created_at`) — the same shape `add_repository` serves.
508
+
509
+ > ⚠️ **`api_key.token` is shown once and never again.** Nothing stores it and no endpoint can
510
+ > re-serve it. Hand it to the person you are working for in your reply. If it is dropped, the
511
+ > recovery is minting another key with this same tool — the platform has no regenerate — then
512
+ > revoking the orphaned one with `revoke_repository_api_key`.
513
+
514
+ Authorization is the `keys_manage` capability; a member without it is refused `403` with SpecGuard's
515
+ own sentence, verbatim.
516
+
517
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
518
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
519
+
520
+ ### `revoke_repository_api_key`
521
+
522
+ Revokes one CI API key on a SpecGuard repository. The key stops authenticating immediately; every
523
+ **other** key on the repository keeps working, so CI keeps ingesting if it holds a surviving key.
524
+
525
+ | argument | |
526
+ | --- | --- |
527
+ | `repository_id` | the repository the key belongs to — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
528
+ | `key_id` | the id of the key to revoke, as served in the `api_key` block of `add_repository` or `create_repository_api_key` |
529
+
530
+ The `key_id` is scoped to `repository_id`: a key id belonging to a different repository is refused
531
+ `404`, never a cross-repository delete. Authorization is the `keys_manage` capability; a member
532
+ without it is refused `403` with SpecGuard's own sentence, verbatim.
533
+
534
+ **Key rotation is mint-then-revoke, in that order.** The platform has no regenerate, so mint a
535
+ replacement with `create_repository_api_key` and deploy it BEFORE revoking the old one — revoke
536
+ first and the repository's CI is locked out until a human mints a new key in a browser. A `204`
537
+ means the key is revoked.
538
+
539
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
540
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
541
+
542
+ ### `list_repository_members`
543
+
544
+ Lists who has access to a SpecGuard repository: one row per member with their `handle`,
545
+ `permissions`, `granted_by` (who last set them) and `created_at`, ordered by handle.
546
+
547
+ | argument | |
548
+ | --- | --- |
549
+ | `repository_id` | the repository — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
550
+
551
+ The list answers **memberships only** and never reports how many CI keys a member has minted
552
+ (`keys_minted`) — that is a separate `keys.manage` disclosure this endpoint deliberately withholds;
553
+ the API-keys tools are the surface for it. The response carries **no membership id**, by design.
554
+
555
+ Authorization is the `members.manage` capability: a caller who is not a member is refused `404`
556
+ (the repository's existence stays hidden), and a member without `members.manage` is refused `403`
557
+ with SpecGuard's own sentence, verbatim.
558
+
559
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
560
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
561
+
562
+ ### `add_repository_member`
563
+
564
+ Grants a person access to a SpecGuard repository by their GitHub handle, with an optional list of
565
+ permissions.
566
+
567
+ | argument | |
568
+ | --- | --- |
569
+ | `repository_id` | the repository to grant access to — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
570
+ | `handle` | the person's GitHub **login** — `octocat`, not a profile URL and not a display name; SpecGuard refuses both with its own sentence |
571
+ | `permissions` | an optional list of permission strings (`view`, `keys.manage`, `members.manage`, `repo.delete`); omit it to grant access with no additional permissions |
572
+
573
+ Each resolution failure — nobody has signed in as that handle yet, the account is archived, the
574
+ handle is ambiguous, the handle is not a login — arrives as a **distinguishable** 400 message
575
+ naming the exact next move. The grantor recorded on the membership is always the person behind
576
+ `SPECGUARD_USER_API_KEY`; no argument can name a different one.
577
+
578
+ On success (`201`) the response carries a `member` block (`handle`, `permissions`, `granted_by`,
579
+ `created_at`) — and **no membership id**, by design. Authorization is the `members.manage`
580
+ capability; a member without it is refused `403` with SpecGuard's own sentence, verbatim.
581
+
582
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
583
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
584
+
585
+ ### `update_repository_member_permissions`
586
+
587
+ Replaces one member's permission set on a SpecGuard repository.
588
+
589
+ | argument | |
590
+ | --- | --- |
591
+ | `repository_id` | the repository the membership belongs to — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
592
+ | `member_id` | the id of the **membership** row to edit — not a user id, not the handle. Scoped to `repository_id`: a foreign membership id is refused `404` |
593
+ | `permissions` | the member's **complete new** permission set (`view`, `keys.manage`, `members.manage`, `repo.delete`) — it replaces what they hold today, it is not merged |
594
+
595
+ **`permissions` replaces the whole set** — name every permission the member should end with.
596
+ Values are validated by SpecGuard; an unknown value is refused with its own sentence, verbatim.
597
+
598
+ **Known limitation:** no API endpoint serves the membership id — the member list and the
599
+ add-member response both omit it by design — so the id must be obtained from the platform (today
600
+ via the repository's web members page). There is no name-based lookup. Authorization is the
601
+ `members.manage` capability; a member without it is refused `403` with SpecGuard's own sentence,
602
+ verbatim.
603
+
604
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
605
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
606
+
607
+ ### `remove_repository_member`
608
+
609
+ Revokes one person's access to a SpecGuard repository by removing their membership. A `204` means
610
+ it is revoked.
611
+
612
+ | argument | |
613
+ | --- | --- |
614
+ | `repository_id` | the repository the membership belongs to — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
615
+ | `member_id` | the id of the **membership** row to revoke — not a user id, not the handle. Scoped to `repository_id`: a foreign membership id is refused `404` |
616
+
617
+ **Revoking does NOT revoke that member's minted CI keys.** Any `sgk_…` keys they created on the
618
+ repository keep authenticating by design; the lever for those is the API-keys surface
619
+ (`revoke_repository_api_key`), not this one. Self-revocation is permitted — after it, the caller's
620
+ next request to the member routes answers `404`, not `403`. The repository owner's membership
621
+ cannot be removed at all. The same known limitation as the edit tool applies to `member_id`: no
622
+ endpoint serves it, so it comes from the platform's web members page today. Authorization is the
623
+ `members.manage` capability; a member without it is refused `403` with SpecGuard's own sentence,
624
+ verbatim.
625
+
626
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
627
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
628
+
629
+ ### `rename_repository`
630
+
631
+ Renames a SpecGuard repository — changes the `org/repo` GitHub full name it is registered under —
632
+ keeping every API key, run and intent on it.
633
+
634
+ | argument | |
635
+ | --- | --- |
636
+ | `repository_id` | the id of the repository to rename — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
637
+ | `github_full_name` | the new GitHub full name, `org/repo` — sent top-level in the body, not nested under `repository` |
638
+
639
+ This is the alternative to the remove-and-re-register path: that one **deletes every API key, run
640
+ and intent** on the repository (`remove_repository`), while this keeps all of them. **Authorization
641
+ is owner-only** — deliberately narrower than removal: a member granted `repo.delete` may destroy
642
+ the repository but may not rename it. The owner check also redeems a browser-issued grant valid for
643
+ 7 days; a nil or stale one is refused `403` with SpecGuard's own sentence naming the fix
644
+ (re-grant via the browser). A name another repository already holds is refused `400` — `taken`, not
645
+ `409` — verbatim. The `200` body is `{repository: …}` in the same shape `list_repositories` serves.
646
+
647
+ It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
648
+ `add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
649
+
401
650
  ## How it works
402
651
 
403
652
  ```
@@ -25,6 +25,35 @@ export declare function getJson(api: ApiConfig, path: string, query: Record<stri
25
25
  * send a body whose `Content-Type` says JSON and whose bytes are not.
26
26
  */
27
27
  export declare function postJson(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<unknown>;
28
+ /**
29
+ * `PATCH` with a JSON body — the mutating-without-replacing half of the
30
+ * transport, and deliberately the SAME function underneath `postJson`, for the
31
+ * reason that header states: everything expensive about this module is about
32
+ * the deadline, not the verb.
33
+ *
34
+ * Routed through `requestJson` rather than `deleteJson`'s raw-body handling,
35
+ * because every PATCH this surface serves answers `200` WITH a JSON body —
36
+ * `requestJson`'s parse is correct here, and re-deriving success handling would
37
+ * be a third copy of the one status check.
38
+ */
39
+ export declare function patchJson(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<unknown>;
40
+ /**
41
+ * `DELETE` — the destructive half of the transport, and deliberately the SAME
42
+ * function underneath `postJson` rather than beside it, for the reason
43
+ * `postJson`'s header states: everything expensive about this module is about
44
+ * the deadline, not the verb.
45
+ *
46
+ * Returns the RAW BODY TEXT rather than a parsed value, because the endpoints
47
+ * this serves answer `204` with NO body at all — the one response in the `sgu_`
48
+ * surface that is deliberately not JSON. `requestJson` JSON-parses every 2xx it
49
+ * sees, so routing a `204` through it would turn a successful delete into
50
+ * "answered 204 but the body was not JSON" — the trap this verb specifically
51
+ * introduces, and the reason the DELETE path has its own success handling
52
+ * instead of sharing `requestJson`'s. The status check and the
53
+ * "reached and refused" hand-off to `describeFailure` are still shared
54
+ * verbatim: only what happens to a SUCCESS body differs.
55
+ */
56
+ export declare function deleteJson(api: ApiConfig, path: string, fetchImpl: typeof globalThis.fetch): Promise<string>;
28
57
  /**
29
58
  * `postJson`, narrowed exactly as `getJsonObject` narrows `getJson`.
30
59
  *
@@ -41,6 +70,13 @@ export declare function postJson(api: ApiConfig, path: string, body: Record<stri
41
70
  * answers with an array.
42
71
  */
43
72
  export declare function postJsonObject(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<Record<string, unknown>>;
73
+ /**
74
+ * `patchJson`, narrowed exactly as `postJsonObject` narrows `postJson` — same
75
+ * guard, same sentence, same reason: `ToolResult.structured` is a
76
+ * `Record<string, unknown>`, so no tool should re-type the three-clause check
77
+ * on this verb either.
78
+ */
79
+ export declare function patchJsonObject(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<Record<string, unknown>>;
44
80
  /**
45
81
  * `getJson`, narrowed to the object every tool here actually asks it for.
46
82
  *
@@ -38,6 +38,45 @@ export async function postJson(api, path, body, fetchImpl) {
38
38
  body: JSON.stringify(body),
39
39
  });
40
40
  }
41
+ /**
42
+ * `PATCH` with a JSON body — the mutating-without-replacing half of the
43
+ * transport, and deliberately the SAME function underneath `postJson`, for the
44
+ * reason that header states: everything expensive about this module is about
45
+ * the deadline, not the verb.
46
+ *
47
+ * Routed through `requestJson` rather than `deleteJson`'s raw-body handling,
48
+ * because every PATCH this surface serves answers `200` WITH a JSON body —
49
+ * `requestJson`'s parse is correct here, and re-deriving success handling would
50
+ * be a third copy of the one status check.
51
+ */
52
+ export async function patchJson(api, path, body, fetchImpl) {
53
+ return requestJson(new URL(`${api.endpoint}${path}`), api, fetchImpl, {
54
+ method: "PATCH",
55
+ body: JSON.stringify(body),
56
+ });
57
+ }
58
+ /**
59
+ * `DELETE` — the destructive half of the transport, and deliberately the SAME
60
+ * function underneath `postJson` rather than beside it, for the reason
61
+ * `postJson`'s header states: everything expensive about this module is about
62
+ * the deadline, not the verb.
63
+ *
64
+ * Returns the RAW BODY TEXT rather than a parsed value, because the endpoints
65
+ * this serves answer `204` with NO body at all — the one response in the `sgu_`
66
+ * surface that is deliberately not JSON. `requestJson` JSON-parses every 2xx it
67
+ * sees, so routing a `204` through it would turn a successful delete into
68
+ * "answered 204 but the body was not JSON" — the trap this verb specifically
69
+ * introduces, and the reason the DELETE path has its own success handling
70
+ * instead of sharing `requestJson`'s. The status check and the
71
+ * "reached and refused" hand-off to `describeFailure` are still shared
72
+ * verbatim: only what happens to a SUCCESS body differs.
73
+ */
74
+ export async function deleteJson(api, path, fetchImpl) {
75
+ const { response, body } = await fetchWithTimeout(new URL(`${api.endpoint}${path}`), api, fetchImpl, { method: "DELETE" });
76
+ if (!response.ok)
77
+ throw describeFailure(response.status, body, api);
78
+ return body;
79
+ }
41
80
  /**
42
81
  * `postJson`, narrowed exactly as `getJsonObject` narrows `getJson`.
43
82
  *
@@ -56,6 +95,15 @@ export async function postJson(api, path, body, fetchImpl) {
56
95
  export async function postJsonObject(api, path, body, fetchImpl) {
57
96
  return asJsonObject(await postJson(api, path, body, fetchImpl));
58
97
  }
98
+ /**
99
+ * `patchJson`, narrowed exactly as `postJsonObject` narrows `postJson` — same
100
+ * guard, same sentence, same reason: `ToolResult.structured` is a
101
+ * `Record<string, unknown>`, so no tool should re-type the three-clause check
102
+ * on this verb either.
103
+ */
104
+ export async function patchJsonObject(api, path, body, fetchImpl) {
105
+ return asJsonObject(await patchJson(api, path, body, fetchImpl));
106
+ }
59
107
  /**
60
108
  * Everything both verbs do with a response, in one place.
61
109
  *
@@ -245,8 +293,8 @@ function describeFailure(status, body, api) {
245
293
  return new ApiError(`${api.endpoint} has no such endpoint (404). Check that ${api.endpointVariable} is the ` +
246
294
  "deployment's root URL, without a path.", status);
247
295
  }
248
- if (status === 400) {
249
- const message = badRequestMessage(body);
296
+ if (status === 400 || status === 403) {
297
+ const message = refusalMessage(body, status);
250
298
  if (message !== undefined)
251
299
  return new ApiError(message, status);
252
300
  }
@@ -262,6 +310,13 @@ function describeFailure(status, body, api) {
262
310
  * `origin/main` route here, so this branch serves the API surface rather than
263
311
  * one tool.
264
312
  *
313
+ * The 403 is the same shape under another status. `UserRepositoriesController#
314
+ * render_not_granted` renders `{error: "not_granted", message:, grant:}` — the
315
+ * `grant` block is simply ignored by the extractor, exactly as `details` is.
316
+ * Same defect (the generic branch truncating the one sentence that names the
317
+ * fix), same remedy — which is why the helper is ONE function parameterised on
318
+ * the status rather than two copies beside each other.
319
+ *
265
320
  * SURFACING IT IS THE OPPOSITE OF RESHAPING IT. The generic branch below turns
266
321
  * the most useful sentence in this direction —
267
322
  *
@@ -283,7 +338,7 @@ function describeFailure(status, body, api) {
283
338
  * whose `message` is absent or is not a string — still gets the generic
284
339
  * sentence, which at least shows the operator what actually came back.
285
340
  */
286
- function badRequestMessage(body) {
341
+ function refusalMessage(body, status) {
287
342
  let parsed;
288
343
  try {
289
344
  parsed = JSON.parse(body);
@@ -296,7 +351,7 @@ function badRequestMessage(body) {
296
351
  const message = parsed["message"];
297
352
  if (typeof message !== "string" || message.trim() === "")
298
353
  return undefined;
299
- return `SpecGuard refused the request (400): ${message.trim()}`;
354
+ return `SpecGuard refused the request (${status}): ${message.trim()}`;
300
355
  }
301
356
  export { requireApiConfig, requireUserApiConfig };
302
357
  //# sourceMappingURL=specguard-api.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"specguard-api.js","sourceRoot":"","sources":["../../../src/support/specguard-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"specguard-api.js","sourceRoot":"","sources":["../../../src/support/specguard-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAkB,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,GAAc,EACd,IAAY,EACZ,KAAyC,EACzC,SAAkC;IAElC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,GAAc,EACd,IAAY,EACZ,IAA6B,EAC7B,SAAkC;IAElC,OAAO,WAAW,CAAC,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE;QACpE,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAc,EACd,IAAY,EACZ,IAA6B,EAC7B,SAAkC;IAElC,OAAO,WAAW,CAAC,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE;QACpE,MAAM,EAAE,OAAO;QACf,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,GAAc,EACd,IAAY,EACZ,SAAkC;IAElC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,gBAAgB,CAC/C,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,EACjC,GAAG,EACH,SAAS,EACT,EAAE,MAAM,EAAE,QAAQ,EAAE,CACrB,CAAC;IAEF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAEpE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAc,EACd,IAAY,EACZ,IAA6B,EAC7B,SAAkC;IAElC,OAAO,YAAY,CAAC,MAAM,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAc,EACd,IAAY,EACZ,IAA6B,EAC7B,SAAkC;IAElC,OAAO,YAAY,CAAC,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,WAAW,CACxB,GAAQ,EACR,GAAc,EACd,SAAkC,EAClC,OAAoB;IAEpB,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAEhF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAEpE,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,QAAQ,CAChB,GAAG,GAAG,CAAC,QAAQ,aAAa,QAAQ,CAAC,MAAM,8BAA8B;YACvE,cAAc,GAAG,CAAC,gBAAgB,0DAA0D;YAC5F,gBAAgB,EAClB,QAAQ,CAAC,MAAM,CAChB,CAAC;IACJ,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,SAAS,YAAY,CAAC,IAAa;IACjC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,QAAQ,CAAC,yDAAyD,CAAC,CAAC;IAChF,CAAC;IAED,OAAO,IAA+B,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAc,EACd,IAAY,EACZ,KAAyC,EACzC,SAAkC;IAElC,OAAO,YAAY,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;AAClE,CAAC;AAQD;;;;;;;GAOG;AACH,MAAM,SAAS,GAAG,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAgBnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,KAAK,UAAU,gBAAgB,CAC7B,GAAQ,EACR,GAAc,EACd,SAAkC,EAClC,OAAoB;IAEpB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,IAAI,KAAgD,CAAC;IAErD,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAmB,CAAC,OAAO,EAAE,EAAE;QACzD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,UAAU,CAAC,KAAK,EAAE,CAAC;YACnB,OAAO,CAAC,SAAS,CAAC,CAAC;QACrB,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACzB,6EAA6E;QAC7E,6EAA6E;QAC7E,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAClC,SAAS,CAAC,GAAG,EAAE;gBACb,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE;oBACrC,MAAM,EAAE,kBAAkB;oBAC1B,YAAY,EAAE,eAAe;oBAC7B,sEAAsE;oBACtE,sEAAsE;oBACtE,gEAAgE;oBAChE,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;iBAC9E;gBACD,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;gBAC7D,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC;YACF,QAAQ;SACT,CAAC,CAAC;QACH,IAAI,QAAQ,KAAK,SAAS;YAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QAEhD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC7D,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QAE5C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,6EAA6E;QAC7E,0EAA0E;QAC1E,4EAA4E;QAC5E,6EAA6E;QAC7E,IAAI,KAAK,YAAY,QAAQ;YAAE,MAAM,KAAK,CAAC;QAE3C,0EAA0E;QAC1E,0EAA0E;QAC1E,6EAA6E;QAC7E,uEAAuE;QACvE,8DAA8D;QAC9D,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QAEnD,MAAM,IAAI,QAAQ,CAChB,mBAAmB,GAAG,CAAC,QAAQ,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI;YAC5F,SAAS,GAAG,CAAC,gBAAgB,0DAA0D,CAC1F,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,uEAAuE;QACvE,wEAAwE;QACxE,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,QAAQ,CAAC,GAAc;IAC9B,OAAO,IAAI,QAAQ,CAAC,GAAG,GAAG,CAAC,QAAQ,2BAA2B,GAAG,CAAC,gBAAgB,KAAK,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,eAAe,CAAC,MAAc,EAAE,IAAY,EAAE,GAAc;IACnE,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;QAEvD,OAAO,IAAI,QAAQ,CACjB,yCAAyC,QAAQ,eAAe,MAAM,kBAAkB;YACtF,GAAG,GAAG,CAAC,QAAQ,IAAI,SAAS,GAAG,EACjC,MAAM,CACP,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,OAAO,IAAI,QAAQ,CACjB,GAAG,GAAG,CAAC,QAAQ,2CAA2C,GAAG,CAAC,gBAAgB,UAAU;YACtF,wCAAwC,EAC1C,MAAM,CACP,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7C,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,IAAI,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,IAAI,QAAQ,CACjB,sBAAsB,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,EAC3F,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,SAAS,cAAc,CAAC,IAAY,EAAE,MAAc;IAClD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAE7F,MAAM,OAAO,GAAI,MAAkC,CAAC,SAAS,CAAC,CAAC;IAC/D,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAE3E,OAAO,kCAAkC,MAAM,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;AACxE,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `POST /api/v1/repositories/:repository_id/members` as a tool — shipped in the
4
+ * platform (`specguard/config/routes.rb`,
5
+ * `Api::V1::UserRepositoryMembersController#create`, SPGD-875).
6
+ *
7
+ * == The handle is the LOGIN itself
8
+ *
9
+ * `User.resolve_by_handle` refuses a profile URL and a display name with its
10
+ * own sentence ("Send the login itself — octocat, not a profile URL or a
11
+ * display name"), and each non-`:found` resolution — never-signed-in,
12
+ * archived, ambiguous, malformed — arrives as a distinguishable 400 sentence
13
+ * through the landed `refusalMessage` branch. This bridge does not re-derive
14
+ * that vocabulary: a client-side copy is a rule with no owner, free to drift
15
+ * from the one that actually decides.
16
+ *
17
+ * == The grantor is always the authenticated principal
18
+ *
19
+ * `granted_by_user` is stamped server-side from the `sgu_` credential and is
20
+ * deliberately absent from the permitted params. The tool therefore takes no
21
+ * grantor argument, and no caller can name one.
22
+ *
23
+ * == The 201 body omits the membership id, by server design
24
+ *
25
+ * `#serialize` serves `handle`, `permissions`, `granted_by`, `created_at` —
26
+ * and no id, because ids are not portable between repositories and serving
27
+ * one invites treating it as portable. See
28
+ * `update_repository_member_permissions` for where the id comes from today.
29
+ */
30
+ declare const addRepositoryMember: ToolDefinition;
31
+ export default addRepositoryMember;
@@ -0,0 +1,105 @@
1
+ import { postJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
2
+ import { optionalStringArray, requireString } from "./args.js";
3
+ /**
4
+ * `POST /api/v1/repositories/:repository_id/members` as a tool — shipped in the
5
+ * platform (`specguard/config/routes.rb`,
6
+ * `Api::V1::UserRepositoryMembersController#create`, SPGD-875).
7
+ *
8
+ * == The handle is the LOGIN itself
9
+ *
10
+ * `User.resolve_by_handle` refuses a profile URL and a display name with its
11
+ * own sentence ("Send the login itself — octocat, not a profile URL or a
12
+ * display name"), and each non-`:found` resolution — never-signed-in,
13
+ * archived, ambiguous, malformed — arrives as a distinguishable 400 sentence
14
+ * through the landed `refusalMessage` branch. This bridge does not re-derive
15
+ * that vocabulary: a client-side copy is a rule with no owner, free to drift
16
+ * from the one that actually decides.
17
+ *
18
+ * == The grantor is always the authenticated principal
19
+ *
20
+ * `granted_by_user` is stamped server-side from the `sgu_` credential and is
21
+ * deliberately absent from the permitted params. The tool therefore takes no
22
+ * grantor argument, and no caller can name one.
23
+ *
24
+ * == The 201 body omits the membership id, by server design
25
+ *
26
+ * `#serialize` serves `handle`, `permissions`, `granted_by`, `created_at` —
27
+ * and no id, because ids are not portable between repositories and serving
28
+ * one invites treating it as portable. See
29
+ * `update_repository_member_permissions` for where the id comes from today.
30
+ */
31
+ const addRepositoryMember = {
32
+ name: "add_repository_member",
33
+ title: "Add repository member",
34
+ description: "Grants a person access to a SpecGuard repository by their GitHub handle, with an optional " +
35
+ "list of permissions. " +
36
+ "The `handle` is the LOGIN itself — `octocat`, not a profile URL and not a display name: " +
37
+ "SpecGuard refuses each of those with its own sentence, and every other resolution failure " +
38
+ "(nobody has signed in as that handle yet, the account is archived, the handle is ambiguous) " +
39
+ "arrives as a distinguishable 400 message naming the exact next move. " +
40
+ "The grantor recorded on the membership is always the person behind this server's user API " +
41
+ "key — the server stamps it, and no argument can name a different one. " +
42
+ "On success (201) the response carries a `member` block (`handle`, `permissions`, " +
43
+ "`granted_by`, `created_at`). It carries NO membership id, by design: ids are not portable " +
44
+ "between repositories, and editing or revoking a membership names a membership id obtained " +
45
+ "from the platform (today only via the web members page) — see the edit/revoke tools. " +
46
+ "Permissions are strings from SpecGuard's own set (`view`, `keys.manage`, `members.manage`, " +
47
+ "`repo.delete`); an unknown value is refused in SpecGuard's own words, and omitting the " +
48
+ "list grants access with no additional permissions. The repository owner cannot be added " +
49
+ "(they hold everything by construction). " +
50
+ "Authorization is the `members.manage` capability — a member without it is refused 403 in " +
51
+ "SpecGuard's own words. " +
52
+ "Takes `repository_id` — the numeric id `list_repositories` reports, not the `org/repo` " +
53
+ "handle. " +
54
+ "Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `add_repository` " +
55
+ "writes with and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ repository_id: {
60
+ type: "string",
61
+ description: "The repository to grant access to — its numeric id, as `add_repository` " +
62
+ "returns and `list_repositories` reports, not the `org/repo` handle.",
63
+ },
64
+ handle: {
65
+ type: "string",
66
+ description: "The GitHub login of the person to add, e.g. `octocat` — not a profile URL and not a " +
67
+ "display name; SpecGuard refuses both with its own sentence. The person must have " +
68
+ "signed in to SpecGuard at least once.",
69
+ },
70
+ permissions: {
71
+ type: "array",
72
+ items: { type: "string" },
73
+ description: "An optional list of permission strings from SpecGuard's own set (`view`, " +
74
+ "`keys.manage`, `members.manage`, `repo.delete`). Omit it to grant access with no " +
75
+ "additional permissions. SpecGuard validates the values and refuses an unknown one " +
76
+ "in its own words.",
77
+ },
78
+ },
79
+ required: ["repository_id", "handle"],
80
+ // Closed for the reason every tool here states — and on a WRITE, a silently
81
+ // dropped misspelled argument still grants something, just not what the
82
+ // agent believed it was asking for.
83
+ additionalProperties: false,
84
+ },
85
+ async run(args, context) {
86
+ const repositoryId = requireString(args["repository_id"], "repository_id");
87
+ const handle = requireString(args["handle"], "handle");
88
+ const permissions = optionalStringArray(args["permissions"], "permissions");
89
+ const api = requireUserApiConfig(context.config);
90
+ // Top-level `{handle, permissions}` — the shape the controller permits,
91
+ // matching `add_repository`'s stated rule: this is JSON an agent writes,
92
+ // not a Rails form. `permissions` omitted when absent so the server's own
93
+ // default applies; passed through as an ARRAY when present (the column is
94
+ // `text[]`, and a scalar on the wire is silently dropped server-side).
95
+ const created = await postJsonObject(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/members`, permissions === undefined ? { handle } : { handle, permissions }, context.fetch);
96
+ // Unreshaped, the standing rule — and the 201 body is the only answer this
97
+ // verb gives; nothing here exists to re-fetch elsewhere.
98
+ return {
99
+ text: JSON.stringify(created, null, 2),
100
+ structured: created,
101
+ };
102
+ },
103
+ };
104
+ export default addRepositoryMember;
105
+ //# sourceMappingURL=add-repository-member.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add-repository-member.js","sourceRoot":"","sources":["../../../src/tools/add-repository-member.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG/D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,mBAAmB,GAAmB;IAC1C,IAAI,EAAE,uBAAuB;IAC7B,KAAK,EAAE,uBAAuB;IAC9B,WAAW,EACT,4FAA4F;QAC5F,uBAAuB;QACvB,0FAA0F;QAC1F,4FAA4F;QAC5F,8FAA8F;QAC9F,uEAAuE;QACvE,4FAA4F;QAC5F,wEAAwE;QACxE,mFAAmF;QACnF,4FAA4F;QAC5F,4FAA4F;QAC5F,uFAAuF;QACvF,6FAA6F;QAC7F,yFAAyF;QACzF,0FAA0F;QAC1F,0CAA0C;QAC1C,2FAA2F;QAC3F,yBAAyB;QACzB,yFAAyF;QACzF,UAAU;QACV,oFAAoF;QACpF,+FAA+F;IACjG,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,0EAA0E;oBAC1E,qEAAqE;aACxE;YACD,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,sFAAsF;oBACtF,mFAAmF;oBACnF,uCAAuC;aAC1C;YACD,WAAW,EAAE;gBACX,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EACT,2EAA2E;oBAC3E,mFAAmF;oBACnF,oFAAoF;oBACpF,mBAAmB;aACtB;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,EAAE,QAAQ,CAAC;QACrC,4EAA4E;QAC5E,wEAAwE;QACxE,oCAAoC;QACpC,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAC3E,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;QACvD,MAAM,WAAW,GAAG,mBAAmB,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,CAAC;QAE5E,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,0EAA0E;QAC1E,uEAAuE;QACvE,MAAM,OAAO,GAAG,MAAM,cAAc,CAClC,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,UAAU,EAClE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAChE,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,2EAA2E;QAC3E,yDAAyD;QACzD,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,UAAU,EAAE,OAAO;SACpB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -66,3 +66,29 @@ export declare function optionalString(value: unknown, field: string): string |
66
66
  */
67
67
  export declare function requireString(value: unknown, field: string): string;
68
68
  export declare function optionalBoolean(value: unknown, field: string): boolean | undefined;
69
+ /**
70
+ * An optional array of non-blank strings, or nothing.
71
+ *
72
+ * The SHAPE-ONLY sibling of the linter's `optionalStringArray` (which stays in
73
+ * `lint-intent-annotations.ts` because its refusals — empty list, leading dash
74
+ * — are about the linter's own argument grammar and throw `CommandError`). This
75
+ * one answers only "is it an array of strings" and always throws
76
+ * `ArgumentError`, which is what a pass-through array of permission names
77
+ * needs: the platform is the authority on which strings are legal, and a
78
+ * client-side copy of that rule is a rule with no owner, free to drift from the
79
+ * one that actually decides.
80
+ */
81
+ export declare function optionalStringArray(value: unknown, field: string): string[] | undefined;
82
+ /**
83
+ * A mandatory array of non-blank strings, and nothing else will do.
84
+ *
85
+ * The mandatory counterpart of `optionalStringArray`, added with
86
+ * `update_repository_member_permissions` (SPGD-885): the server column behind
87
+ * that call is `text[]`, and a scalar on the wire is silently dropped
88
+ * server-side — so the array shape is checked HERE, before a write, rather
89
+ * than persisted as a member holding nothing. Blank entries are dropped rather
90
+ * than refused: the server's own normalisation does the same, so refusing here
91
+ * would be a second vocabulary the caller must satisfy before the one that
92
+ * decides even sees the request.
93
+ */
94
+ export declare function requireStringArray(value: unknown, field: string): string[];