specguard-mcp 0.1.4 → 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.
- package/README.md +150 -1
- package/dist/src/support/specguard-api.d.ts +19 -0
- package/dist/src/support/specguard-api.js +26 -0
- package/dist/src/support/specguard-api.js.map +1 -1
- package/dist/src/tools/add-repository-member.d.ts +31 -0
- package/dist/src/tools/add-repository-member.js +105 -0
- package/dist/src/tools/add-repository-member.js.map +1 -0
- package/dist/src/tools/args.d.ts +26 -0
- package/dist/src/tools/args.js +52 -0
- package/dist/src/tools/args.js.map +1 -1
- package/dist/src/tools/index.d.ts +53 -9
- package/dist/src/tools/index.js +65 -9
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-repository-members.d.ts +32 -0
- package/dist/src/tools/list-repository-members.js +76 -0
- package/dist/src/tools/list-repository-members.js.map +1 -0
- package/dist/src/tools/near-duplicate-clusters.d.ts +58 -0
- package/dist/src/tools/near-duplicate-clusters.js +116 -0
- package/dist/src/tools/near-duplicate-clusters.js.map +1 -0
- package/dist/src/tools/remove-repository-member.d.ts +30 -0
- package/dist/src/tools/remove-repository-member.js +93 -0
- package/dist/src/tools/remove-repository-member.js.map +1 -0
- package/dist/src/tools/rename-repository.d.ts +44 -0
- package/dist/src/tools/rename-repository.js +99 -0
- package/dist/src/tools/rename-repository.js.map +1 -0
- package/dist/src/tools/update-repository-member-permissions.d.ts +30 -0
- package/dist/src/tools/update-repository-member-permissions.js +97 -0
- package/dist/src/tools/update-repository-member-permissions.js.map +1 -0
- package/package.json +2 -1
package/dist/src/tools/index.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import addRepository from "./add-repository.js";
|
|
2
|
+
import addRepositoryMember from "./add-repository-member.js";
|
|
2
3
|
import createRepositoryApiKey from "./create-repository-api-key.js";
|
|
3
4
|
import lintIntentAnnotations from "./lint-intent-annotations.js";
|
|
4
5
|
import listRepositories from "./list-repositories.js";
|
|
6
|
+
import listRepositoryMembers from "./list-repository-members.js";
|
|
7
|
+
import nearDuplicateClusters from "./near-duplicate-clusters.js";
|
|
5
8
|
import getRepositoryOverview from "./repository-overview.js";
|
|
6
9
|
import registrableRepositories from "./registrable-repositories.js";
|
|
7
10
|
import removeRepository from "./remove-repository.js";
|
|
11
|
+
import removeRepositoryMember from "./remove-repository-member.js";
|
|
12
|
+
import renameRepository from "./rename-repository.js";
|
|
8
13
|
import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
|
|
14
|
+
import updateRepositoryMemberPermissions from "./update-repository-member-permissions.js";
|
|
9
15
|
/**
|
|
10
16
|
* THE REGISTRY — the one file that changes when the toolset grows.
|
|
11
17
|
*
|
|
@@ -41,12 +47,21 @@ import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
|
|
|
41
47
|
*
|
|
42
48
|
* == What is deliberately absent
|
|
43
49
|
*
|
|
44
|
-
* `/check-intent`
|
|
45
|
-
*
|
|
46
|
-
* advertised in `tools/list` is a
|
|
47
|
-
*
|
|
48
|
-
* and fails on use, which is worse than
|
|
49
|
-
* agent has already committed to a plan by
|
|
50
|
+
* `/check-intent` is NOT here and must not be added until its backing endpoint
|
|
51
|
+
* exists — `specguard/config/routes.rb:113` carries it as a comment only, which
|
|
52
|
+
* is the evidence this forbid rests on. A tool advertised in `tools/list` is a
|
|
53
|
+
* promise an agent will act on: wrapping an endpoint that does not exist would
|
|
54
|
+
* produce a server that discovers cleanly and fails on use, which is worse than
|
|
55
|
+
* not offering the tool, because the agent has already committed to a plan by
|
|
56
|
+
* the time it finds out.
|
|
57
|
+
*
|
|
58
|
+
* Duplicate clustering was once under this same forbid, on the same standing
|
|
59
|
+
* rule — no tool may wrap what has not shipped. That half retired when the
|
|
60
|
+
* platform moved: SPGD-703 (`specguard` `c43dc19`, 2026-08-28) shipped
|
|
61
|
+
* `GET /api/v1/repository?near_duplicates=`, serving
|
|
62
|
+
* `RepositoryOverview#serialized_near_duplicates` behind an opt-in ask, and
|
|
63
|
+
* `near_duplicate_clusters` below wraps it. What moved was the platform, not
|
|
64
|
+
* the bar.
|
|
50
65
|
*
|
|
51
66
|
* == The fourth: the first tool that WRITES
|
|
52
67
|
*
|
|
@@ -106,9 +121,44 @@ import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
|
|
|
106
121
|
* `requestJson`'s JSON parse.
|
|
107
122
|
*
|
|
108
123
|
* The standing rule itself is unchanged and still binding — which still keeps
|
|
109
|
-
* out `/check-intent
|
|
110
|
-
*
|
|
111
|
-
*
|
|
124
|
+
* out `/check-intent`: it has no backing endpoint (`routes.rb:113` mounts it
|
|
125
|
+
* as a comment only). A tool advertised in `tools/list` remains a promise an
|
|
126
|
+
* agent will act on.
|
|
127
|
+
*
|
|
128
|
+
* == The ninth through twelfth: member management
|
|
129
|
+
*
|
|
130
|
+
* - `list_repository_members`, `add_repository_member`,
|
|
131
|
+
* `update_repository_member_permissions` and `remove_repository_member`
|
|
132
|
+
* wrap the four `user_repository_members` endpoints (all shipped: SPGD-875,
|
|
133
|
+
* specguard@origin/main).
|
|
134
|
+
*
|
|
135
|
+
* They also forced the transport's fourth verb: `update` is a PATCH, which
|
|
136
|
+
* arrived with `patchJson` in this same slice — 200 with a JSON body, so it
|
|
137
|
+
* routes through `requestJson` like `postJson` rather than `deleteJson`'s
|
|
138
|
+
* raw-body handling.
|
|
139
|
+
*
|
|
140
|
+
* A known limitation rides with them and is stated in the edit/revoke tool
|
|
141
|
+
* descriptions rather than papered over: PATCH and DELETE name a membership by
|
|
142
|
+
* id, but no endpoint serves that id (the platform's `#serialize` deliberately
|
|
143
|
+
* omits it), so the id comes from the platform's web members page today. That
|
|
144
|
+
* is a platform-side follow-up, not a client-side workaround — there is no
|
|
145
|
+
* name-based lookup.
|
|
146
|
+
*
|
|
147
|
+
* == The thirteenth: rename
|
|
148
|
+
*
|
|
149
|
+
* - `rename_repository` wraps `PATCH /api/v1/repositories/:id` (shipped:
|
|
150
|
+
* SPGD-878, `specguard@origin/main` e026793, PR #266).
|
|
151
|
+
*
|
|
152
|
+
* This is the entry the forbid above once named as "the natural next slice" —
|
|
153
|
+
* the platform moved (SPGD-878 shipped the endpoint), so the forbid retired
|
|
154
|
+
* exactly as the duplicate-clustering one did. It reuses `patchJson` landed
|
|
155
|
+
* with the member-management slice rather than minting a transport of its own.
|
|
156
|
+
* The verb's one asymmetry is carried in the tool description rather than
|
|
157
|
+
* papered over: rename authorizes OWNER-ONLY, while removal authorizes the
|
|
158
|
+
* BROADER `repo.delete` capability — a member who may destroy the repository
|
|
159
|
+
* may not rename it. Before this tool the bridge's only rename path was
|
|
160
|
+
* remove-and-re-register, which destroys every key, run and intent; this one
|
|
161
|
+
* keeps them.
|
|
112
162
|
*/
|
|
113
163
|
export const TOOLS = [
|
|
114
164
|
lintIntentAnnotations,
|
|
@@ -119,5 +169,11 @@ export const TOOLS = [
|
|
|
119
169
|
removeRepository,
|
|
120
170
|
createRepositoryApiKey,
|
|
121
171
|
revokeRepositoryApiKey,
|
|
172
|
+
nearDuplicateClusters,
|
|
173
|
+
listRepositoryMembers,
|
|
174
|
+
addRepositoryMember,
|
|
175
|
+
updateRepositoryMemberPermissions,
|
|
176
|
+
removeRepositoryMember,
|
|
177
|
+
renameRepository,
|
|
122
178
|
];
|
|
123
179
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,uBAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,mBAAmB,MAAM,4BAA4B,CAAC;AAC7D,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,uBAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,+BAA+B,CAAC;AACnE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,iCAAiC,MAAM,2CAA2C,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmJG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;IACb,uBAAuB;IACvB,gBAAgB;IAChB,sBAAsB;IACtB,sBAAsB;IACtB,qBAAqB;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,iCAAiC;IACjC,sBAAsB;IACtB,gBAAgB;CACjB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `GET /api/v1/repositories/:repository_id/members` as a tool — shipped in the
|
|
4
|
+
* platform (`specguard/config/routes.rb`, `Api::V1::UserRepositoryMembersController#index`,
|
|
5
|
+
* SPGD-875).
|
|
6
|
+
*
|
|
7
|
+
* == Memberships only, never `keys_minted`
|
|
8
|
+
*
|
|
9
|
+
* The endpoint answers the same rows the web members page renders — `handle`,
|
|
10
|
+
* `permissions`, `granted_by`, `created_at`, ordered by handle — and
|
|
11
|
+
* deliberately NOTHING else. `keys_minted` is a `keys.manage` disclosure that
|
|
12
|
+
* page gates separately; this read answers memberships to a `members.manage`
|
|
13
|
+
* holder and no more. A tool that promised counts here would be promising
|
|
14
|
+
* something the endpoint refuses to say.
|
|
15
|
+
*
|
|
16
|
+
* == No membership id, by design
|
|
17
|
+
*
|
|
18
|
+
* The body carries no membership id: ids are not portable between
|
|
19
|
+
* repositories, and serving one would invite treating it as portable. PATCH
|
|
20
|
+
* and DELETE still name rows by that id — see
|
|
21
|
+
* `update_repository_member_permissions` and `remove_repository_member` for
|
|
22
|
+
* where the id comes from today.
|
|
23
|
+
*
|
|
24
|
+
* == No client-side capability probing
|
|
25
|
+
*
|
|
26
|
+
* The server gates at `current_repository(:members_manage)`: a non-member gets
|
|
27
|
+
* 404 (the repository's existence stays hidden), a member without the
|
|
28
|
+
* capability gets 403 with SpecGuard's own sentence. This tool predicts
|
|
29
|
+
* neither; it reports what came back.
|
|
30
|
+
*/
|
|
31
|
+
declare const listRepositoryMembers: ToolDefinition;
|
|
32
|
+
export default listRepositoryMembers;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
|
|
2
|
+
import { requireString } from "./args.js";
|
|
3
|
+
/**
|
|
4
|
+
* `GET /api/v1/repositories/:repository_id/members` as a tool — shipped in the
|
|
5
|
+
* platform (`specguard/config/routes.rb`, `Api::V1::UserRepositoryMembersController#index`,
|
|
6
|
+
* SPGD-875).
|
|
7
|
+
*
|
|
8
|
+
* == Memberships only, never `keys_minted`
|
|
9
|
+
*
|
|
10
|
+
* The endpoint answers the same rows the web members page renders — `handle`,
|
|
11
|
+
* `permissions`, `granted_by`, `created_at`, ordered by handle — and
|
|
12
|
+
* deliberately NOTHING else. `keys_minted` is a `keys.manage` disclosure that
|
|
13
|
+
* page gates separately; this read answers memberships to a `members.manage`
|
|
14
|
+
* holder and no more. A tool that promised counts here would be promising
|
|
15
|
+
* something the endpoint refuses to say.
|
|
16
|
+
*
|
|
17
|
+
* == No membership id, by design
|
|
18
|
+
*
|
|
19
|
+
* The body carries no membership id: ids are not portable between
|
|
20
|
+
* repositories, and serving one would invite treating it as portable. PATCH
|
|
21
|
+
* and DELETE still name rows by that id — see
|
|
22
|
+
* `update_repository_member_permissions` and `remove_repository_member` for
|
|
23
|
+
* where the id comes from today.
|
|
24
|
+
*
|
|
25
|
+
* == No client-side capability probing
|
|
26
|
+
*
|
|
27
|
+
* The server gates at `current_repository(:members_manage)`: a non-member gets
|
|
28
|
+
* 404 (the repository's existence stays hidden), a member without the
|
|
29
|
+
* capability gets 403 with SpecGuard's own sentence. This tool predicts
|
|
30
|
+
* neither; it reports what came back.
|
|
31
|
+
*/
|
|
32
|
+
const listRepositoryMembers = {
|
|
33
|
+
name: "list_repository_members",
|
|
34
|
+
title: "List repository members",
|
|
35
|
+
description: "Lists who has access to a SpecGuard repository: one row per member with their `handle`, " +
|
|
36
|
+
"`permissions`, `granted_by` (who last set them) and `created_at`, ordered by handle. " +
|
|
37
|
+
"The list answers MEMBERSHIPS only and never reports how many CI keys a member has minted " +
|
|
38
|
+
"(`keys_minted`) — that is a separate `keys.manage` disclosure this endpoint deliberately " +
|
|
39
|
+
"withholds, and the API-keys tools are the surface for it. " +
|
|
40
|
+
"Authorization is the `members.manage` capability: a caller who is not a member of the " +
|
|
41
|
+
"repository is refused 404 (the repository's existence stays hidden), and a member without " +
|
|
42
|
+
"`members.manage` is refused 403 in SpecGuard's own words. " +
|
|
43
|
+
"Takes `repository_id` — the numeric id `list_repositories` reports, not the `org/repo` " +
|
|
44
|
+
"handle. " +
|
|
45
|
+
"Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `list_repositories` " +
|
|
46
|
+
"reads and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
repository_id: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "The repository whose members to list — its numeric id, as `add_repository` " +
|
|
53
|
+
"returns and `list_repositories` reports, not the `org/repo` handle.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["repository_id"],
|
|
57
|
+
// Closed for the reason every tool here states: `server.ts` forwards
|
|
58
|
+
// `arguments` unvalidated, so an open schema would let a misspelled argument
|
|
59
|
+
// be dropped silently.
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
async run(args, context) {
|
|
63
|
+
const repositoryId = requireString(args["repository_id"], "repository_id");
|
|
64
|
+
const api = requireUserApiConfig(context.config);
|
|
65
|
+
const members = await getJsonObject(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/members`, {}, context.fetch);
|
|
66
|
+
// Passed through unreshaped, the standing rule (`types.ts`: "A thin client
|
|
67
|
+
// that reshapes its upstream is not thin") — the endpoint serves the same
|
|
68
|
+
// four fields the web members page renders, under the same names.
|
|
69
|
+
return {
|
|
70
|
+
text: JSON.stringify(members, null, 2),
|
|
71
|
+
structured: members,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
export default listRepositoryMembers;
|
|
76
|
+
//# sourceMappingURL=list-repository-members.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-repository-members.js","sourceRoot":"","sources":["../../../src/tools/list-repository-members.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,0FAA0F;QAC1F,uFAAuF;QACvF,2FAA2F;QAC3F,2FAA2F;QAC3F,4DAA4D;QAC5D,wFAAwF;QACxF,4FAA4F;QAC5F,4DAA4D;QAC5D,yFAAyF;QACzF,UAAU;QACV,uFAAuF;QACvF,yFAAyF;IAC3F,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,6EAA6E;oBAC7E,qEAAqE;aACxE;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,qEAAqE;QACrE,6EAA6E;QAC7E,uBAAuB;QACvB,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;QAE3E,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,UAAU,EAClE,EAAE,EACF,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,2EAA2E;QAC3E,0EAA0E;QAC1E,kEAAkE;QAClE,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,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `GET /api/v1/repository?near_duplicates=` as a tool — shipped in the
|
|
4
|
+
* platform by SPGD-703 (`specguard` `c43dc19`, 2026-08-28), which added the
|
|
5
|
+
* `near_duplicates` block to `RepositoryOverview` behind an opt-in ask.
|
|
6
|
+
*
|
|
7
|
+
* == What the block is, and why it is behind an ask at all
|
|
8
|
+
*
|
|
9
|
+
* It is the suite-wide near-duplicate census: which tests READ alike — same
|
|
10
|
+
* body text, not same file — clustered by the engine SPGD-369 shipped
|
|
11
|
+
* (`specguard` `f7d5352`). It is the first block on that endpoint whose grain
|
|
12
|
+
* is the REPOSITORY rather than a run or a window of runs, and the first one
|
|
13
|
+
* gated on COST rather than rows: `NearDuplicateClusters` is linear but
|
|
14
|
+
* measured in seconds (seven queries at every size; seconds at a few thousand
|
|
15
|
+
* identities, extrapolating to tens of seconds at the 20,000-identity design
|
|
16
|
+
* point). `?near_duplicates=` confines that cost to the client that named it —
|
|
17
|
+
* no ask, key present and `null`, not one query — which is why this block is a
|
|
18
|
+
* SEPARATE tool rather than a forwarded parameter on
|
|
19
|
+
* `get_repository_overview`: an agent calling the overview should never pay
|
|
20
|
+
* the census by accident, and an agent calling this tool has asked for nothing
|
|
21
|
+
* else. Splitting the tools splits the cost along exactly the line the server
|
|
22
|
+
* drew.
|
|
23
|
+
*
|
|
24
|
+
* == The ask is always sent, and always spelled `"true"`
|
|
25
|
+
*
|
|
26
|
+
* The server reads only whether the parameter is PRESENT
|
|
27
|
+
* (`RequestedNearDuplicatesParam`): `?near_duplicates=false` opens the block
|
|
28
|
+
* exactly as `=true` does, and a non-String shape is read as no ask at all.
|
|
29
|
+
* There is no "off" value for a client to send, so this tool has no arguments
|
|
30
|
+
* — nothing about the census is choosable from here, which is also why the
|
|
31
|
+
* schema is CLOSED rather than merely empty: `server.ts` forwards `arguments`
|
|
32
|
+
* unvalidated, and an open schema would let an invented argument ride through
|
|
33
|
+
* and be silently dropped (see `registrable-repositories.ts` for the same
|
|
34
|
+
* call). `near_duplicates: "true"` is built rather than stringified for the
|
|
35
|
+
* same reason `repository-overview.ts` builds its `unannotated_examples` key:
|
|
36
|
+
* `getJson` omits only `undefined`, so a conditional send is the only honest
|
|
37
|
+
* way to spell "always" here.
|
|
38
|
+
*
|
|
39
|
+
* == The response is passed through, not re-modelled
|
|
40
|
+
*
|
|
41
|
+
* Same rule as `repository-overview.ts`: every figure in the block is
|
|
42
|
+
* annotated in `RepositoryOverview#serialized_near_duplicates` with the reason
|
|
43
|
+
* for its shape, and several of those reasons are about honesty rather than
|
|
44
|
+
* convenience — `similarity_floor`/`similarity_basis` served FIRST because a
|
|
45
|
+
* count without the statement of what the similarity means is a vacuous
|
|
46
|
+
* figure; `member_count` (texts in the REPOSITORY, across every run) and
|
|
47
|
+
* `example_count` (examples in the ONE run `weighed_run_id` names) served side
|
|
48
|
+
* by side because a three-example table-driven loop is one member and three
|
|
49
|
+
* examples, and flattening them would erase the figure the ranking is built
|
|
50
|
+
* on; `unobserved_members` disclosing that the member list holds an identity
|
|
51
|
+
* the weighed run did not observe; `similarity_range` as the `[strongest,
|
|
52
|
+
* weakest]` pair because membership is transitive while similarity is not.
|
|
53
|
+
* Reshaping here would discard distinctions the serializer spent that care
|
|
54
|
+
* preserving, so the body goes back as it arrived — the whole body, which
|
|
55
|
+
* carries the repository/run context the clusters sit inside.
|
|
56
|
+
*/
|
|
57
|
+
declare const nearDuplicateClusters: ToolDefinition;
|
|
58
|
+
export default nearDuplicateClusters;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { requireApiConfig } from "../config.js";
|
|
2
|
+
import { getJsonObject } from "../support/specguard-api.js";
|
|
3
|
+
/**
|
|
4
|
+
* `GET /api/v1/repository?near_duplicates=` as a tool — shipped in the
|
|
5
|
+
* platform by SPGD-703 (`specguard` `c43dc19`, 2026-08-28), which added the
|
|
6
|
+
* `near_duplicates` block to `RepositoryOverview` behind an opt-in ask.
|
|
7
|
+
*
|
|
8
|
+
* == What the block is, and why it is behind an ask at all
|
|
9
|
+
*
|
|
10
|
+
* It is the suite-wide near-duplicate census: which tests READ alike — same
|
|
11
|
+
* body text, not same file — clustered by the engine SPGD-369 shipped
|
|
12
|
+
* (`specguard` `f7d5352`). It is the first block on that endpoint whose grain
|
|
13
|
+
* is the REPOSITORY rather than a run or a window of runs, and the first one
|
|
14
|
+
* gated on COST rather than rows: `NearDuplicateClusters` is linear but
|
|
15
|
+
* measured in seconds (seven queries at every size; seconds at a few thousand
|
|
16
|
+
* identities, extrapolating to tens of seconds at the 20,000-identity design
|
|
17
|
+
* point). `?near_duplicates=` confines that cost to the client that named it —
|
|
18
|
+
* no ask, key present and `null`, not one query — which is why this block is a
|
|
19
|
+
* SEPARATE tool rather than a forwarded parameter on
|
|
20
|
+
* `get_repository_overview`: an agent calling the overview should never pay
|
|
21
|
+
* the census by accident, and an agent calling this tool has asked for nothing
|
|
22
|
+
* else. Splitting the tools splits the cost along exactly the line the server
|
|
23
|
+
* drew.
|
|
24
|
+
*
|
|
25
|
+
* == The ask is always sent, and always spelled `"true"`
|
|
26
|
+
*
|
|
27
|
+
* The server reads only whether the parameter is PRESENT
|
|
28
|
+
* (`RequestedNearDuplicatesParam`): `?near_duplicates=false` opens the block
|
|
29
|
+
* exactly as `=true` does, and a non-String shape is read as no ask at all.
|
|
30
|
+
* There is no "off" value for a client to send, so this tool has no arguments
|
|
31
|
+
* — nothing about the census is choosable from here, which is also why the
|
|
32
|
+
* schema is CLOSED rather than merely empty: `server.ts` forwards `arguments`
|
|
33
|
+
* unvalidated, and an open schema would let an invented argument ride through
|
|
34
|
+
* and be silently dropped (see `registrable-repositories.ts` for the same
|
|
35
|
+
* call). `near_duplicates: "true"` is built rather than stringified for the
|
|
36
|
+
* same reason `repository-overview.ts` builds its `unannotated_examples` key:
|
|
37
|
+
* `getJson` omits only `undefined`, so a conditional send is the only honest
|
|
38
|
+
* way to spell "always" here.
|
|
39
|
+
*
|
|
40
|
+
* == The response is passed through, not re-modelled
|
|
41
|
+
*
|
|
42
|
+
* Same rule as `repository-overview.ts`: every figure in the block is
|
|
43
|
+
* annotated in `RepositoryOverview#serialized_near_duplicates` with the reason
|
|
44
|
+
* for its shape, and several of those reasons are about honesty rather than
|
|
45
|
+
* convenience — `similarity_floor`/`similarity_basis` served FIRST because a
|
|
46
|
+
* count without the statement of what the similarity means is a vacuous
|
|
47
|
+
* figure; `member_count` (texts in the REPOSITORY, across every run) and
|
|
48
|
+
* `example_count` (examples in the ONE run `weighed_run_id` names) served side
|
|
49
|
+
* by side because a three-example table-driven loop is one member and three
|
|
50
|
+
* examples, and flattening them would erase the figure the ranking is built
|
|
51
|
+
* on; `unobserved_members` disclosing that the member list holds an identity
|
|
52
|
+
* the weighed run did not observe; `similarity_range` as the `[strongest,
|
|
53
|
+
* weakest]` pair because membership is transitive while similarity is not.
|
|
54
|
+
* Reshaping here would discard distinctions the serializer spent that care
|
|
55
|
+
* preserving, so the body goes back as it arrived — the whole body, which
|
|
56
|
+
* carries the repository/run context the clusters sit inside.
|
|
57
|
+
*/
|
|
58
|
+
const nearDuplicateClusters = {
|
|
59
|
+
name: "near_duplicate_clusters",
|
|
60
|
+
title: "Near-duplicate clusters",
|
|
61
|
+
description: "Run SpecGuard's near-duplicate census over a repository's tests — which tests READ alike " +
|
|
62
|
+
"(same body text, whatever file they sit in), clustered by similarity. Answers the refactoring " +
|
|
63
|
+
"question the overview's per-run rankings cannot: where is the same test written twice, before " +
|
|
64
|
+
"you delete or merge anything. " +
|
|
65
|
+
"THIS IS THE EXPENSIVE READ ON THIS BRIDGE: the census is linear but measured in seconds — seven " +
|
|
66
|
+
"queries at every size, tens of seconds extrapolated at the 20,000-test design point — which is " +
|
|
67
|
+
"exactly why the server serves it only to a client that asks (`?near_duplicates=`) and answers " +
|
|
68
|
+
"`near_duplicates: null` on the plain overview. Calling this tool IS the ask; it takes no " +
|
|
69
|
+
"arguments because nothing about the census is choosable — the clusters are the repository's, " +
|
|
70
|
+
"computed over every run, and one call returns them all. " +
|
|
71
|
+
"READ THE DISCLOSURE KEYS BEFORE THE COUNT: `similarity_floor` and `similarity_basis` sit FIRST " +
|
|
72
|
+
"in the block and qualify every cluster below them — a cluster count without what 'similar' " +
|
|
73
|
+
"meant is a figure you cannot act on. `truncated: true` means the cluster list was cut at the " +
|
|
74
|
+
"cap while the counts above it (`cluster_count`, `identity_count`, `clustered_*`) describe the " +
|
|
75
|
+
"WHOLE census, so never fold `clusters` length as the total. " +
|
|
76
|
+
"READ EVERY CLUSTER'S FIGURES AT THEIR OWN GRAIN: `member_count` counts texts in the REPOSITORY " +
|
|
77
|
+
"across every run, `example_count` counts examples in the ONE run `weighed_run_id` names — a " +
|
|
78
|
+
"three-example table-driven loop is ONE member and THREE examples, and those two numbers beside " +
|
|
79
|
+
"each other are the whole point. `unobserved_members: true` on a cluster says a member identity " +
|
|
80
|
+
"the weighed run did not observe (deleted, renamed, deselected) is still listed — do not reconcile " +
|
|
81
|
+
"the member list against a run's examples and expect it to balance. `similarity_range` is " +
|
|
82
|
+
"[strongest, weakest]: membership is transitive, similarity is not, and the gap between the edges " +
|
|
83
|
+
"is the merge risk. `total_seconds` is raw and `null` where nothing was timed — never a zero " +
|
|
84
|
+
"that would read as free. " +
|
|
85
|
+
"A quiet answer is a FINDING, not a gap: `clusters: []` with a real `identity_count` is the " +
|
|
86
|
+
"success state (nothing reads alike), and the three silences — nothing ingested " +
|
|
87
|
+
"(`recorded_count: 0`), nothing embedded (`identity_count: 0`), nothing alike — are kept " +
|
|
88
|
+
"distinguishable by those counts rather than collapsed into one empty list. " +
|
|
89
|
+
"Same credential and endpoint as `get_repository_overview` (an `sgk_` repository key on " +
|
|
90
|
+
"`GET /api/v1/repository`); the response is that endpoint's full body with the `near_duplicates` " +
|
|
91
|
+
"block OPENED, passed through unmodified.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
// No properties, deliberately — see this file's header. Still CLOSED rather
|
|
95
|
+
// than merely empty, for the reason `registrable-repositories.ts` gives
|
|
96
|
+
// inline: `server.ts` forwards `arguments` unvalidated and `run` ignores
|
|
97
|
+
// them, so an open schema would have an invented argument silently dropped
|
|
98
|
+
// and the call answered as if it had been honoured.
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
},
|
|
101
|
+
async run(_args, context) {
|
|
102
|
+
const api = requireApiConfig(context.config);
|
|
103
|
+
const overview = await getJsonObject(api, "/api/v1/repository", {
|
|
104
|
+
// Always sent, always `"true"` — the server reads only that the key is
|
|
105
|
+
// present (`?near_duplicates=false` opens the block too), and this
|
|
106
|
+
// tool exists to open it. See this file's header.
|
|
107
|
+
near_duplicates: "true",
|
|
108
|
+
}, context.fetch);
|
|
109
|
+
return {
|
|
110
|
+
text: JSON.stringify(overview, null, 2),
|
|
111
|
+
structured: overview,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
export default nearDuplicateClusters;
|
|
116
|
+
//# sourceMappingURL=near-duplicate-clusters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"near-duplicate-clusters.js","sourceRoot":"","sources":["../../../src/tools/near-duplicate-clusters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,yBAAyB;IAE/B,KAAK,EAAE,yBAAyB;IAEhC,WAAW,EACT,2FAA2F;QAC3F,gGAAgG;QAChG,gGAAgG;QAChG,gCAAgC;QAChC,kGAAkG;QAClG,iGAAiG;QACjG,gGAAgG;QAChG,2FAA2F;QAC3F,+FAA+F;QAC/F,0DAA0D;QAC1D,iGAAiG;QACjG,6FAA6F;QAC7F,+FAA+F;QAC/F,gGAAgG;QAChG,8DAA8D;QAC9D,iGAAiG;QACjG,8FAA8F;QAC9F,iGAAiG;QACjG,iGAAiG;QACjG,oGAAoG;QACpG,2FAA2F;QAC3F,mGAAmG;QACnG,8FAA8F;QAC9F,2BAA2B;QAC3B,6FAA6F;QAC7F,iFAAiF;QACjF,0FAA0F;QAC1F,6EAA6E;QAC7E,yFAAyF;QACzF,kGAAkG;QAClG,0CAA0C;IAE5C,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,4EAA4E;QAC5E,wEAAwE;QACxE,yEAAyE;QACzE,2EAA2E;QAC3E,oDAAoD;QACpD,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO;QACtB,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAE7C,MAAM,QAAQ,GAAG,MAAM,aAAa,CAClC,GAAG,EACH,oBAAoB,EACpB;YACE,uEAAuE;YACvE,mEAAmE;YACnE,kDAAkD;YAClD,eAAe,EAAE,MAAM;SACxB,EACD,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,UAAU,EAAE,QAAQ;SACrB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `DELETE /api/v1/repositories/:repository_id/members/:id` as a tool — shipped
|
|
4
|
+
* in the platform (`specguard/config/routes.rb`,
|
|
5
|
+
* `Api::V1::UserRepositoryMembersController#destroy`, SPGD-875).
|
|
6
|
+
*
|
|
7
|
+
* == The two documented asymmetries, stated BEFORE the call
|
|
8
|
+
*
|
|
9
|
+
* Revoking a membership deliberately LEAVES that member's minted CI keys
|
|
10
|
+
* authenticating (`User has_many :created_api_keys, dependent: :nullify`) —
|
|
11
|
+
* the lever for those is the API-keys surface, not this one. And
|
|
12
|
+
* self-revocation is permitted: it is the caller's own access to give up, and
|
|
13
|
+
* after it the next request to these routes answers 404, not 403 (a former
|
|
14
|
+
* member is a non-member). Both belong in the description because they are
|
|
15
|
+
* the facts an agent must know before it commits to the call.
|
|
16
|
+
*
|
|
17
|
+
* == The owner row cannot arrive
|
|
18
|
+
*
|
|
19
|
+
* An owner membership is structurally impossible
|
|
20
|
+
* (`RepositoryMembership#user_is_not_the_owner`), so "cannot remove the owner"
|
|
21
|
+
* is a model invariant, not a guard this bridge re-checks.
|
|
22
|
+
*
|
|
23
|
+
* == 204 with no body
|
|
24
|
+
*
|
|
25
|
+
* The same empty-body success `remove_repository` and
|
|
26
|
+
* `revoke_repository_api_key` serve; the same reason `deleteJson` exists
|
|
27
|
+
* rather than routing a 204 through `requestJson`'s JSON parse.
|
|
28
|
+
*/
|
|
29
|
+
declare const removeRepositoryMember: ToolDefinition;
|
|
30
|
+
export default removeRepositoryMember;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { deleteJson, requireUserApiConfig } from "../support/specguard-api.js";
|
|
2
|
+
import { requireString } from "./args.js";
|
|
3
|
+
/**
|
|
4
|
+
* `DELETE /api/v1/repositories/:repository_id/members/:id` as a tool — shipped
|
|
5
|
+
* in the platform (`specguard/config/routes.rb`,
|
|
6
|
+
* `Api::V1::UserRepositoryMembersController#destroy`, SPGD-875).
|
|
7
|
+
*
|
|
8
|
+
* == The two documented asymmetries, stated BEFORE the call
|
|
9
|
+
*
|
|
10
|
+
* Revoking a membership deliberately LEAVES that member's minted CI keys
|
|
11
|
+
* authenticating (`User has_many :created_api_keys, dependent: :nullify`) —
|
|
12
|
+
* the lever for those is the API-keys surface, not this one. And
|
|
13
|
+
* self-revocation is permitted: it is the caller's own access to give up, and
|
|
14
|
+
* after it the next request to these routes answers 404, not 403 (a former
|
|
15
|
+
* member is a non-member). Both belong in the description because they are
|
|
16
|
+
* the facts an agent must know before it commits to the call.
|
|
17
|
+
*
|
|
18
|
+
* == The owner row cannot arrive
|
|
19
|
+
*
|
|
20
|
+
* An owner membership is structurally impossible
|
|
21
|
+
* (`RepositoryMembership#user_is_not_the_owner`), so "cannot remove the owner"
|
|
22
|
+
* is a model invariant, not a guard this bridge re-checks.
|
|
23
|
+
*
|
|
24
|
+
* == 204 with no body
|
|
25
|
+
*
|
|
26
|
+
* The same empty-body success `remove_repository` and
|
|
27
|
+
* `revoke_repository_api_key` serve; the same reason `deleteJson` exists
|
|
28
|
+
* rather than routing a 204 through `requestJson`'s JSON parse.
|
|
29
|
+
*/
|
|
30
|
+
const removeRepositoryMember = {
|
|
31
|
+
name: "remove_repository_member",
|
|
32
|
+
title: "Remove repository member",
|
|
33
|
+
description: "Revokes one person's access to a SpecGuard repository by removing their membership. " +
|
|
34
|
+
"The member is named by `member_id` — a MEMBERSHIP id, not a user id — scoped to " +
|
|
35
|
+
"`repository_id`: a membership id belonging to a different repository is refused 404, " +
|
|
36
|
+
"never a cross-repository revoke. " +
|
|
37
|
+
"⚠️ Revoking does NOT revoke that member's minted CI keys — any sgk_… keys they created " +
|
|
38
|
+
"on the repository keep authenticating by design; the lever for those is the API-keys " +
|
|
39
|
+
"surface (`revoke_repository_api_key`), not this one. " +
|
|
40
|
+
"Self-revocation is permitted: a caller may remove their own membership, and their next " +
|
|
41
|
+
"request to the member routes answers 404 — a former member is a non-member, so this tool " +
|
|
42
|
+
"cannot read the repository's members afterwards. The repository owner's membership cannot " +
|
|
43
|
+
"be removed at all (an owner holds everything by construction). " +
|
|
44
|
+
"KNOWN LIMITATION: no API endpoint serves the membership id — the member list and the " +
|
|
45
|
+
"add-member response both omit it by design — so the id must be obtained from the " +
|
|
46
|
+
"platform (today via the repository's web members page); there is no name-based lookup. " +
|
|
47
|
+
"Authorization is the `members.manage` capability — a member without it is refused 403 in " +
|
|
48
|
+
"SpecGuard's own words. A 204 means the membership is revoked. " +
|
|
49
|
+
"Takes `repository_id` — the numeric id `list_repositories` reports, not the `org/repo` " +
|
|
50
|
+
"handle. " +
|
|
51
|
+
"Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `add_repository` " +
|
|
52
|
+
"writes with and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
repository_id: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "The repository the membership belongs to — its numeric id, as `add_repository` " +
|
|
59
|
+
"returns and `list_repositories` reports, not the `org/repo` handle.",
|
|
60
|
+
},
|
|
61
|
+
member_id: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "The id of the MEMBERSHIP row to revoke — not a user id, and not the handle. " +
|
|
64
|
+
"Scoped to `repository_id`: a foreign membership id is refused 404. No API endpoint " +
|
|
65
|
+
"serves this id today; obtain it from the platform's web members page.",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ["repository_id", "member_id"],
|
|
69
|
+
// Closed for the reason every tool here states — and on a DESTRUCTIVE path,
|
|
70
|
+
// a silently dropped misspelled argument must not be able to leave the
|
|
71
|
+
// agent believing it revoked a membership the call never named.
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
async run(args, context) {
|
|
75
|
+
const repositoryId = requireString(args["repository_id"], "repository_id");
|
|
76
|
+
const memberId = requireString(args["member_id"], "member_id");
|
|
77
|
+
const api = requireUserApiConfig(context.config);
|
|
78
|
+
const body = await deleteJson(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/members/${encodeURIComponent(memberId)}`, context.fetch);
|
|
79
|
+
// 204 with no body — the tool's own words, because the deployment's whole
|
|
80
|
+
// answer was "no content". The keys asymmetry is restated here because the
|
|
81
|
+
// moment of success is the last moment the fact still matters.
|
|
82
|
+
return {
|
|
83
|
+
text: body === ""
|
|
84
|
+
? `Membership ${memberId} revoked (204). That person's minted CI keys on this ` +
|
|
85
|
+
"repository keep authenticating — revoke those separately with " +
|
|
86
|
+
"revoke_repository_api_key if that is the intent."
|
|
87
|
+
: body,
|
|
88
|
+
structured: { repository_id: repositoryId, member_id: memberId, revoked: true },
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
export default removeRepositoryMember;
|
|
93
|
+
//# sourceMappingURL=remove-repository-member.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remove-repository-member.js","sourceRoot":"","sources":["../../../src/tools/remove-repository-member.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,sBAAsB,GAAmB;IAC7C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,WAAW,EACT,sFAAsF;QACtF,kFAAkF;QAClF,uFAAuF;QACvF,mCAAmC;QACnC,yFAAyF;QACzF,uFAAuF;QACvF,uDAAuD;QACvD,yFAAyF;QACzF,2FAA2F;QAC3F,4FAA4F;QAC5F,iEAAiE;QACjE,uFAAuF;QACvF,mFAAmF;QACnF,yFAAyF;QACzF,2FAA2F;QAC3F,gEAAgE;QAChE,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,iFAAiF;oBACjF,qEAAqE;aACxE;YACD,SAAS,EAAE;gBACT,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,8EAA8E;oBAC9E,qFAAqF;oBACrF,uEAAuE;aAC1E;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,EAAE,WAAW,CAAC;QACxC,4EAA4E;QAC5E,uEAAuE;QACvE,gEAAgE;QAChE,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,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC;QAE/D,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,IAAI,GAAG,MAAM,UAAU,CAC3B,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,YAAY,kBAAkB,CAAC,QAAQ,CAAC,EAAE,EAClG,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,0EAA0E;QAC1E,2EAA2E;QAC3E,+DAA+D;QAC/D,OAAO;YACL,IAAI,EACF,IAAI,KAAK,EAAE;gBACT,CAAC,CAAC,cAAc,QAAQ,uDAAuD;oBAC7E,gEAAgE;oBAChE,kDAAkD;gBACpD,CAAC,CAAC,IAAI;YACV,UAAU,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE;SAChF,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `PATCH /api/v1/repositories/:id` as a tool — shipped in the platform
|
|
4
|
+
* (`specguard/config/routes.rb:186`, `Api::V1::UserRepositoriesController#update`,
|
|
5
|
+
* SPGD-878, PR #266).
|
|
6
|
+
*
|
|
7
|
+
* == Why this tool exists at all
|
|
8
|
+
*
|
|
9
|
+
* Before it, the only way to change a repository's `org/repo` name through the
|
|
10
|
+
* bridge was `remove_repository` plus `add_repository` — which destroys every
|
|
11
|
+
* API key, every run and every intent on the repository, because that is what
|
|
12
|
+
* remove does. The platform's rename endpoint keeps all of it; this tool is
|
|
13
|
+
* the bridge's one-line path to that.
|
|
14
|
+
*
|
|
15
|
+
* == Authorization is OWNER-ONLY — deliberately narrower than delete
|
|
16
|
+
*
|
|
17
|
+
* The controller gates through `current_repository(:owner)`, NOT the
|
|
18
|
+
* `repo.delete` capability fork `remove_repository` authorizes through — so a
|
|
19
|
+
* member granted `repo.delete` may DESTROY the repository but may NOT rename
|
|
20
|
+
* it. The asymmetry is the platform's, and the description carries it rather
|
|
21
|
+
* than papering over it.
|
|
22
|
+
*
|
|
23
|
+
* == The grant precondition
|
|
24
|
+
*
|
|
25
|
+
* The owner check redeems a browser-issued grant that is valid for seven days;
|
|
26
|
+
* a nil or stale one is a 403 whose own sentence names the fix (re-grant via
|
|
27
|
+
* the browser). The bridge surfaces that sentence verbatim and adds no
|
|
28
|
+
* client-side grant handling — the server is the gate.
|
|
29
|
+
*
|
|
30
|
+
* == The body is top-level `{github_full_name}` — NOT nested
|
|
31
|
+
*
|
|
32
|
+
* `update_params` permits `github_full_name` at the TOP level, not under a
|
|
33
|
+
* `repository` key; a nested body is silently empty to the server. And a taken
|
|
34
|
+
* name is a 400 (`render_bad_request`, the `taken` branch), not a 409 — this
|
|
35
|
+
* description says so because it is the one answer an agent is most likely to
|
|
36
|
+
* mispredict.
|
|
37
|
+
*
|
|
38
|
+
* == The 200 body is the list_repositories shape
|
|
39
|
+
*
|
|
40
|
+
* `{repository: serialize(...)}` — the same serializer `list_repositories`
|
|
41
|
+
* serves, so the response needs no reshaping and gets none.
|
|
42
|
+
*/
|
|
43
|
+
declare const renameRepository: ToolDefinition;
|
|
44
|
+
export default renameRepository;
|