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.
- package/README.md +255 -6
- package/dist/src/support/specguard-api.d.ts +36 -0
- package/dist/src/support/specguard-api.js +59 -4
- 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/create-repository-api-key.d.ts +26 -0
- package/dist/src/tools/create-repository-api-key.js +83 -0
- package/dist/src/tools/create-repository-api-key.js.map +1 -0
- package/dist/src/tools/index.d.ts +96 -11
- package/dist/src/tools/index.js +116 -11
- 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/registrable-repositories.d.ts +51 -0
- package/dist/src/tools/registrable-repositories.js +92 -0
- package/dist/src/tools/registrable-repositories.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/remove-repository.d.ts +33 -0
- package/dist/src/tools/remove-repository.js +81 -0
- package/dist/src/tools/remove-repository.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/revoke-repository-api-key.d.ts +29 -0
- package/dist/src/tools/revoke-repository-api-key.js +85 -0
- package/dist/src/tools/revoke-repository-api-key.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/args.js
CHANGED
|
@@ -93,4 +93,56 @@ export function optionalBoolean(value, field) {
|
|
|
93
93
|
throw new ArgumentError(`\`${field}\` must be a boolean.`);
|
|
94
94
|
return value;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* An optional array of non-blank strings, or nothing.
|
|
98
|
+
*
|
|
99
|
+
* The SHAPE-ONLY sibling of the linter's `optionalStringArray` (which stays in
|
|
100
|
+
* `lint-intent-annotations.ts` because its refusals — empty list, leading dash
|
|
101
|
+
* — are about the linter's own argument grammar and throw `CommandError`). This
|
|
102
|
+
* one answers only "is it an array of strings" and always throws
|
|
103
|
+
* `ArgumentError`, which is what a pass-through array of permission names
|
|
104
|
+
* needs: the platform is the authority on which strings are legal, and a
|
|
105
|
+
* client-side copy of that rule is a rule with no owner, free to drift from the
|
|
106
|
+
* one that actually decides.
|
|
107
|
+
*/
|
|
108
|
+
export function optionalStringArray(value, field) {
|
|
109
|
+
if (value === undefined || value === null)
|
|
110
|
+
return undefined;
|
|
111
|
+
if (!Array.isArray(value))
|
|
112
|
+
throw new ArgumentError(`\`${field}\` must be an array of strings.`);
|
|
113
|
+
const entries = value.map((entry) => {
|
|
114
|
+
if (typeof entry !== "string")
|
|
115
|
+
throw new ArgumentError(`\`${field}\` must contain only strings.`);
|
|
116
|
+
// Trimmed like its scalar siblings: a value an agent produced by
|
|
117
|
+
// concatenating strings arrives with whitespace that is not part of what it
|
|
118
|
+
// meant to send.
|
|
119
|
+
return entry.trim();
|
|
120
|
+
});
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A mandatory array of non-blank strings, and nothing else will do.
|
|
125
|
+
*
|
|
126
|
+
* The mandatory counterpart of `optionalStringArray`, added with
|
|
127
|
+
* `update_repository_member_permissions` (SPGD-885): the server column behind
|
|
128
|
+
* that call is `text[]`, and a scalar on the wire is silently dropped
|
|
129
|
+
* server-side — so the array shape is checked HERE, before a write, rather
|
|
130
|
+
* than persisted as a member holding nothing. Blank entries are dropped rather
|
|
131
|
+
* than refused: the server's own normalisation does the same, so refusing here
|
|
132
|
+
* would be a second vocabulary the caller must satisfy before the one that
|
|
133
|
+
* decides even sees the request.
|
|
134
|
+
*/
|
|
135
|
+
export function requireStringArray(value, field) {
|
|
136
|
+
if (value === undefined || value === null) {
|
|
137
|
+
throw new ArgumentError(`\`${field}\` is required.`);
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(value))
|
|
140
|
+
throw new ArgumentError(`\`${field}\` must be an array of strings.`);
|
|
141
|
+
const entries = value.map((entry) => {
|
|
142
|
+
if (typeof entry !== "string")
|
|
143
|
+
throw new ArgumentError(`\`${field}\` must contain only strings.`);
|
|
144
|
+
return entry.trim();
|
|
145
|
+
});
|
|
146
|
+
return entries.filter((entry) => entry !== "");
|
|
147
|
+
}
|
|
96
148
|
//# sourceMappingURL=args.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/tools/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,KAAa;IAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IACzF,8EAA8E;IAC9E,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,KAAa;IACzD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,iBAAiB,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IAEzF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,EAAE;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAE/E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,KAAa;IAC3D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/tools/args.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `POST /api/v1/repositories/:repository_id/api_keys` as a tool — shipped in
|
|
4
|
+
* the platform (`specguard/config/routes.rb:158`,
|
|
5
|
+
* `user_repository_api_keys_controller#create`, SPGD-754).
|
|
6
|
+
*
|
|
7
|
+
* == Reveal-once, again
|
|
8
|
+
*
|
|
9
|
+
* The 201 body carries `api_key.token` — the raw key, the only time it exists
|
|
10
|
+
* anywhere — exactly as `add_repository`'s does. The body is therefore passed
|
|
11
|
+
* through UNRESHAPED in both `text` and `structured` for the same reason that
|
|
12
|
+
* tool states: any reshaping on this hop is a value that cannot be recovered
|
|
13
|
+
* rather than a field that can be re-fetched. Recovery for a dropped token is
|
|
14
|
+
* minting another key — this same tool — because the platform ships no
|
|
15
|
+
* `#regenerate` and no re-serve.
|
|
16
|
+
*
|
|
17
|
+
* == The name is top-level and optional
|
|
18
|
+
*
|
|
19
|
+
* `params[:name]` defaults to `ApiKey::DEFAULT_NAME` server-side; `undefined`
|
|
20
|
+
* here means "let the server name it" and simply omits the key from the POST
|
|
21
|
+
* body. Not re-validated here for the reason `add_repository` states: a second
|
|
22
|
+
* format rule on this side is a rule with no owner, free to drift from the one
|
|
23
|
+
* that actually decides.
|
|
24
|
+
*/
|
|
25
|
+
declare const createRepositoryApiKey: ToolDefinition;
|
|
26
|
+
export default createRepositoryApiKey;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { postJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
|
|
2
|
+
import { optionalString, requireString } from "./args.js";
|
|
3
|
+
/**
|
|
4
|
+
* `POST /api/v1/repositories/:repository_id/api_keys` as a tool — shipped in
|
|
5
|
+
* the platform (`specguard/config/routes.rb:158`,
|
|
6
|
+
* `user_repository_api_keys_controller#create`, SPGD-754).
|
|
7
|
+
*
|
|
8
|
+
* == Reveal-once, again
|
|
9
|
+
*
|
|
10
|
+
* The 201 body carries `api_key.token` — the raw key, the only time it exists
|
|
11
|
+
* anywhere — exactly as `add_repository`'s does. The body is therefore passed
|
|
12
|
+
* through UNRESHAPED in both `text` and `structured` for the same reason that
|
|
13
|
+
* tool states: any reshaping on this hop is a value that cannot be recovered
|
|
14
|
+
* rather than a field that can be re-fetched. Recovery for a dropped token is
|
|
15
|
+
* minting another key — this same tool — because the platform ships no
|
|
16
|
+
* `#regenerate` and no re-serve.
|
|
17
|
+
*
|
|
18
|
+
* == The name is top-level and optional
|
|
19
|
+
*
|
|
20
|
+
* `params[:name]` defaults to `ApiKey::DEFAULT_NAME` server-side; `undefined`
|
|
21
|
+
* here means "let the server name it" and simply omits the key from the POST
|
|
22
|
+
* body. Not re-validated here for the reason `add_repository` states: a second
|
|
23
|
+
* format rule on this side is a rule with no owner, free to drift from the one
|
|
24
|
+
* that actually decides.
|
|
25
|
+
*/
|
|
26
|
+
const createRepositoryApiKey = {
|
|
27
|
+
name: "create_repository_api_key",
|
|
28
|
+
title: "Create repository API key",
|
|
29
|
+
description: "Mints a new CI API key (an sgk_… key) for a SpecGuard repository, and returns it " +
|
|
30
|
+
"alongside the repository's existing keys. " +
|
|
31
|
+
"⚠️ `api_key.token` is shown THIS ONCE AND NEVER AGAIN — nothing stores it and no " +
|
|
32
|
+
"endpoint can re-serve it, so hand it to the user in your reply rather than assuming " +
|
|
33
|
+
"it can be fetched later. If it is dropped, the recovery is minting another key with " +
|
|
34
|
+
"this same tool (the platform has no regenerate), then revoking the orphaned one. " +
|
|
35
|
+
"On success the response carries an `api_key` block (`name`, `token`, `hint`, " +
|
|
36
|
+
"`created_at`) — the same reveal-once shape `add_repository` serves. " +
|
|
37
|
+
"Minting does not disturb existing keys: each key on a repository authenticates " +
|
|
38
|
+
"independently until revoked. " +
|
|
39
|
+
"Takes `repository_id` (the numeric id `list_repositories` reports) and an optional " +
|
|
40
|
+
"`name`, which the server defaults when omitted. " +
|
|
41
|
+
"Authorization is the `keys_manage` capability — a member without it is refused 403 " +
|
|
42
|
+
"in SpecGuard's own words. " +
|
|
43
|
+
"Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `add_repository` " +
|
|
44
|
+
"writes with and a DIFFERENT one from the sgk_… repository key " +
|
|
45
|
+
"`get_repository_overview` uses.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
repository_id: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "The repository to mint the key for — its numeric id, as `add_repository` " +
|
|
52
|
+
"returns and `list_repositories` reports, not the `org/repo` handle.",
|
|
53
|
+
},
|
|
54
|
+
name: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "An optional label for the key. Omit it to let SpecGuard use its default name. " +
|
|
57
|
+
"The server validates the name and refuses an unusable one in its own words.",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
required: ["repository_id"],
|
|
61
|
+
// Closed for the reason every tool here states — and on a WRITE, a silently
|
|
62
|
+
// dropped misspelled argument still mints something, just possibly mislabeled.
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
},
|
|
65
|
+
async run(args, context) {
|
|
66
|
+
const repositoryId = requireString(args["repository_id"], "repository_id");
|
|
67
|
+
const name = optionalString(args["name"], "name");
|
|
68
|
+
const api = requireUserApiConfig(context.config);
|
|
69
|
+
// `name` omitted when absent rather than sent as null, so the server's own
|
|
70
|
+
// `ApiKey::DEFAULT_NAME` default applies — this bridge expresses "no
|
|
71
|
+
// preference", it does not choose on the server's behalf.
|
|
72
|
+
const created = await postJsonObject(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/api_keys`, name === undefined ? {} : { name }, context.fetch);
|
|
73
|
+
// Unreshaped, for the reason `add_repository` states at its own return:
|
|
74
|
+
// `api_key.token` exists nowhere else, so any reshaping on this hop is a
|
|
75
|
+
// value that cannot be recovered.
|
|
76
|
+
return {
|
|
77
|
+
text: JSON.stringify(created, null, 2),
|
|
78
|
+
structured: created,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
export default createRepositoryApiKey;
|
|
83
|
+
//# sourceMappingURL=create-repository-api-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-repository-api-key.js","sourceRoot":"","sources":["../../../src/tools/create-repository-api-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,sBAAsB,GAAmB;IAC7C,IAAI,EAAE,2BAA2B;IACjC,KAAK,EAAE,2BAA2B;IAClC,WAAW,EACT,mFAAmF;QACnF,4CAA4C;QAC5C,mFAAmF;QACnF,sFAAsF;QACtF,sFAAsF;QACtF,mFAAmF;QACnF,+EAA+E;QAC/E,sEAAsE;QACtE,iFAAiF;QACjF,+BAA+B;QAC/B,qFAAqF;QACrF,kDAAkD;QAClD,qFAAqF;QACrF,4BAA4B;QAC5B,oFAAoF;QACpF,gEAAgE;QAChE,iCAAiC;IACnC,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,2EAA2E;oBAC3E,qEAAqE;aACxE;YACD,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,gFAAgF;oBAChF,6EAA6E;aAChF;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,4EAA4E;QAC5E,+EAA+E;QAC/E,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,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAElD,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,2EAA2E;QAC3E,qEAAqE;QACrE,0DAA0D;QAC1D,MAAM,OAAO,GAAG,MAAM,cAAc,CAClC,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,WAAW,EACnE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAClC,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,wEAAwE;QACxE,yEAAyE;QACzE,kCAAkC;QAClC,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,sBAAsB,CAAC"}
|
|
@@ -34,12 +34,21 @@ import type { ToolDefinition } from "./types.js";
|
|
|
34
34
|
*
|
|
35
35
|
* == What is deliberately absent
|
|
36
36
|
*
|
|
37
|
-
* `/check-intent`
|
|
38
|
-
*
|
|
39
|
-
* advertised in `tools/list` is a
|
|
40
|
-
*
|
|
41
|
-
* and fails on use, which is worse than
|
|
42
|
-
* agent has already committed to a plan by
|
|
37
|
+
* `/check-intent` is NOT here and must not be added until its backing endpoint
|
|
38
|
+
* exists — `specguard/config/routes.rb:113` carries it as a comment only, which
|
|
39
|
+
* is the evidence this forbid rests on. A tool advertised in `tools/list` is a
|
|
40
|
+
* promise an agent will act on: wrapping an endpoint that does not exist would
|
|
41
|
+
* produce a server that discovers cleanly and fails on use, which is worse than
|
|
42
|
+
* not offering the tool, because the agent has already committed to a plan by
|
|
43
|
+
* the time it finds out.
|
|
44
|
+
*
|
|
45
|
+
* Duplicate clustering was once under this same forbid, on the same standing
|
|
46
|
+
* rule — no tool may wrap what has not shipped. That half retired when the
|
|
47
|
+
* platform moved: SPGD-703 (`specguard` `c43dc19`, 2026-08-28) shipped
|
|
48
|
+
* `GET /api/v1/repository?near_duplicates=`, serving
|
|
49
|
+
* `RepositoryOverview#serialized_near_duplicates` behind an opt-in ask, and
|
|
50
|
+
* `near_duplicate_clusters` below wraps it. What moved was the platform, not
|
|
51
|
+
* the bar.
|
|
43
52
|
*
|
|
44
53
|
* == The fourth: the first tool that WRITES
|
|
45
54
|
*
|
|
@@ -56,11 +65,87 @@ import type { ToolDefinition } from "./types.js";
|
|
|
56
65
|
* beside it, and `describeFailure` grew the `400` branch that surfaces
|
|
57
66
|
* SpecGuard's own refusal sentence — the modal answer this endpoint gives.
|
|
58
67
|
*
|
|
59
|
-
* The standing rule is unchanged and still binding
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* platform, not the bar.
|
|
68
|
+
* The standing rule is unchanged and still binding. What once kept `DELETE
|
|
69
|
+
* /api/v1/repositories/:id` and the API-key endpoints out under it — "not on
|
|
70
|
+
* `origin/main`, so they may not be wrapped" — stopped being true when SPGD-754
|
|
71
|
+
* shipped them, and the sixth-through-eighth section below records their
|
|
72
|
+
* wrapping. What moved was the platform, not the bar.
|
|
73
|
+
*
|
|
74
|
+
* == The fifth: the read half of the registration gate
|
|
75
|
+
*
|
|
76
|
+
* - `registrable_repositories` wraps `GET /api/v1/repositories/registrable`
|
|
77
|
+
* (shipped: `specguard/config/routes.rb:117`,
|
|
78
|
+
* `Api::V1::UserRepositoriesController#registrable`).
|
|
79
|
+
*
|
|
80
|
+
* `list_repositories` says what IS registered; this says what COULD be — the
|
|
81
|
+
* set the gate would consult, read out in advance, so an agent can pick a
|
|
82
|
+
* `full_name` for `add_repository` from a real answer. Landing it also
|
|
83
|
+
* generalised the 400 branch in `describeFailure` into a status-parameterised
|
|
84
|
+
* extractor, because this endpoint's modal first answer is a 403 (`not_granted`)
|
|
85
|
+
* carrying the same `{error, message}` contract — the identical defect, given
|
|
86
|
+
* the identical remedy.
|
|
87
|
+
*
|
|
88
|
+
* The user-scoped surface as it now stands is therefore three tools: read the
|
|
89
|
+
* list, read the gate's answer, write a registration. The standing rule is
|
|
90
|
+
* unchanged and still binding, which is what keeps the rest out:
|
|
91
|
+
* `DELETE /api/v1/repositories/:id` and the API-key endpoints (SPGD-754) are
|
|
92
|
+
* NOT on `origin/main`, so they may not be wrapped here however useful a tool
|
|
93
|
+
* for them would be. What moved was the platform, not the bar.
|
|
94
|
+
*
|
|
95
|
+
* == The sixth through eighth: removal and the key lifecycle
|
|
96
|
+
*
|
|
97
|
+
* - `remove_repository` wraps `DELETE /api/v1/repositories/:id`, and
|
|
98
|
+
* `create_repository_api_key` / `revoke_repository_api_key` wrap the two
|
|
99
|
+
* `api_keys` endpoints (all shipped: SPGD-754, `specguard@origin/main`).
|
|
100
|
+
*
|
|
101
|
+
* The closing fence the two paragraphs above share — "the DELETE and API-key
|
|
102
|
+
* endpoints are NOT on `origin/main`, so they may not be wrapped" — stopped
|
|
103
|
+
* being true when SPGD-754 landed, and these three entries are what became
|
|
104
|
+
* wrappable the moment it did. They also forced the transport's third verb:
|
|
105
|
+
* both DELETE endpoints answer `204` with NO body, the one response in the
|
|
106
|
+
* `sgu_` surface that is deliberately not JSON, which is why `deleteJson`
|
|
107
|
+
* returns the raw body text instead of routing an empty 204 through
|
|
108
|
+
* `requestJson`'s JSON parse.
|
|
109
|
+
*
|
|
110
|
+
* The standing rule itself is unchanged and still binding — which still keeps
|
|
111
|
+
* out `/check-intent`: it has no backing endpoint (`routes.rb:113` mounts it
|
|
112
|
+
* as a comment only). A tool advertised in `tools/list` remains a promise an
|
|
113
|
+
* agent will act on.
|
|
114
|
+
*
|
|
115
|
+
* == The ninth through twelfth: member management
|
|
116
|
+
*
|
|
117
|
+
* - `list_repository_members`, `add_repository_member`,
|
|
118
|
+
* `update_repository_member_permissions` and `remove_repository_member`
|
|
119
|
+
* wrap the four `user_repository_members` endpoints (all shipped: SPGD-875,
|
|
120
|
+
* specguard@origin/main).
|
|
121
|
+
*
|
|
122
|
+
* They also forced the transport's fourth verb: `update` is a PATCH, which
|
|
123
|
+
* arrived with `patchJson` in this same slice — 200 with a JSON body, so it
|
|
124
|
+
* routes through `requestJson` like `postJson` rather than `deleteJson`'s
|
|
125
|
+
* raw-body handling.
|
|
126
|
+
*
|
|
127
|
+
* A known limitation rides with them and is stated in the edit/revoke tool
|
|
128
|
+
* descriptions rather than papered over: PATCH and DELETE name a membership by
|
|
129
|
+
* id, but no endpoint serves that id (the platform's `#serialize` deliberately
|
|
130
|
+
* omits it), so the id comes from the platform's web members page today. That
|
|
131
|
+
* is a platform-side follow-up, not a client-side workaround — there is no
|
|
132
|
+
* name-based lookup.
|
|
133
|
+
*
|
|
134
|
+
* == The thirteenth: rename
|
|
135
|
+
*
|
|
136
|
+
* - `rename_repository` wraps `PATCH /api/v1/repositories/:id` (shipped:
|
|
137
|
+
* SPGD-878, `specguard@origin/main` e026793, PR #266).
|
|
138
|
+
*
|
|
139
|
+
* This is the entry the forbid above once named as "the natural next slice" —
|
|
140
|
+
* the platform moved (SPGD-878 shipped the endpoint), so the forbid retired
|
|
141
|
+
* exactly as the duplicate-clustering one did. It reuses `patchJson` landed
|
|
142
|
+
* with the member-management slice rather than minting a transport of its own.
|
|
143
|
+
* The verb's one asymmetry is carried in the tool description rather than
|
|
144
|
+
* papered over: rename authorizes OWNER-ONLY, while removal authorizes the
|
|
145
|
+
* BROADER `repo.delete` capability — a member who may destroy the repository
|
|
146
|
+
* may not rename it. Before this tool the bridge's only rename path was
|
|
147
|
+
* remove-and-re-register, which destroys every key, run and intent; this one
|
|
148
|
+
* keeps them.
|
|
64
149
|
*/
|
|
65
150
|
export declare const TOOLS: readonly ToolDefinition[];
|
|
66
151
|
export type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
|
package/dist/src/tools/index.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import addRepository from "./add-repository.js";
|
|
2
|
+
import addRepositoryMember from "./add-repository-member.js";
|
|
3
|
+
import createRepositoryApiKey from "./create-repository-api-key.js";
|
|
2
4
|
import lintIntentAnnotations from "./lint-intent-annotations.js";
|
|
3
5
|
import listRepositories from "./list-repositories.js";
|
|
6
|
+
import listRepositoryMembers from "./list-repository-members.js";
|
|
7
|
+
import nearDuplicateClusters from "./near-duplicate-clusters.js";
|
|
4
8
|
import getRepositoryOverview from "./repository-overview.js";
|
|
9
|
+
import registrableRepositories from "./registrable-repositories.js";
|
|
10
|
+
import removeRepository from "./remove-repository.js";
|
|
11
|
+
import removeRepositoryMember from "./remove-repository-member.js";
|
|
12
|
+
import renameRepository from "./rename-repository.js";
|
|
13
|
+
import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
|
|
14
|
+
import updateRepositoryMemberPermissions from "./update-repository-member-permissions.js";
|
|
5
15
|
/**
|
|
6
16
|
* THE REGISTRY — the one file that changes when the toolset grows.
|
|
7
17
|
*
|
|
@@ -37,12 +47,21 @@ import getRepositoryOverview from "./repository-overview.js";
|
|
|
37
47
|
*
|
|
38
48
|
* == What is deliberately absent
|
|
39
49
|
*
|
|
40
|
-
* `/check-intent`
|
|
41
|
-
*
|
|
42
|
-
* advertised in `tools/list` is a
|
|
43
|
-
*
|
|
44
|
-
* and fails on use, which is worse than
|
|
45
|
-
* 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.
|
|
46
65
|
*
|
|
47
66
|
* == The fourth: the first tool that WRITES
|
|
48
67
|
*
|
|
@@ -59,16 +78,102 @@ import getRepositoryOverview from "./repository-overview.js";
|
|
|
59
78
|
* beside it, and `describeFailure` grew the `400` branch that surfaces
|
|
60
79
|
* SpecGuard's own refusal sentence — the modal answer this endpoint gives.
|
|
61
80
|
*
|
|
62
|
-
* The standing rule is unchanged and still binding
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* platform, not the bar.
|
|
81
|
+
* The standing rule is unchanged and still binding. What once kept `DELETE
|
|
82
|
+
* /api/v1/repositories/:id` and the API-key endpoints out under it — "not on
|
|
83
|
+
* `origin/main`, so they may not be wrapped" — stopped being true when SPGD-754
|
|
84
|
+
* shipped them, and the sixth-through-eighth section below records their
|
|
85
|
+
* wrapping. What moved was the platform, not the bar.
|
|
86
|
+
*
|
|
87
|
+
* == The fifth: the read half of the registration gate
|
|
88
|
+
*
|
|
89
|
+
* - `registrable_repositories` wraps `GET /api/v1/repositories/registrable`
|
|
90
|
+
* (shipped: `specguard/config/routes.rb:117`,
|
|
91
|
+
* `Api::V1::UserRepositoriesController#registrable`).
|
|
92
|
+
*
|
|
93
|
+
* `list_repositories` says what IS registered; this says what COULD be — the
|
|
94
|
+
* set the gate would consult, read out in advance, so an agent can pick a
|
|
95
|
+
* `full_name` for `add_repository` from a real answer. Landing it also
|
|
96
|
+
* generalised the 400 branch in `describeFailure` into a status-parameterised
|
|
97
|
+
* extractor, because this endpoint's modal first answer is a 403 (`not_granted`)
|
|
98
|
+
* carrying the same `{error, message}` contract — the identical defect, given
|
|
99
|
+
* the identical remedy.
|
|
100
|
+
*
|
|
101
|
+
* The user-scoped surface as it now stands is therefore three tools: read the
|
|
102
|
+
* list, read the gate's answer, write a registration. The standing rule is
|
|
103
|
+
* unchanged and still binding, which is what keeps the rest out:
|
|
104
|
+
* `DELETE /api/v1/repositories/:id` and the API-key endpoints (SPGD-754) are
|
|
105
|
+
* NOT on `origin/main`, so they may not be wrapped here however useful a tool
|
|
106
|
+
* for them would be. What moved was the platform, not the bar.
|
|
107
|
+
*
|
|
108
|
+
* == The sixth through eighth: removal and the key lifecycle
|
|
109
|
+
*
|
|
110
|
+
* - `remove_repository` wraps `DELETE /api/v1/repositories/:id`, and
|
|
111
|
+
* `create_repository_api_key` / `revoke_repository_api_key` wrap the two
|
|
112
|
+
* `api_keys` endpoints (all shipped: SPGD-754, `specguard@origin/main`).
|
|
113
|
+
*
|
|
114
|
+
* The closing fence the two paragraphs above share — "the DELETE and API-key
|
|
115
|
+
* endpoints are NOT on `origin/main`, so they may not be wrapped" — stopped
|
|
116
|
+
* being true when SPGD-754 landed, and these three entries are what became
|
|
117
|
+
* wrappable the moment it did. They also forced the transport's third verb:
|
|
118
|
+
* both DELETE endpoints answer `204` with NO body, the one response in the
|
|
119
|
+
* `sgu_` surface that is deliberately not JSON, which is why `deleteJson`
|
|
120
|
+
* returns the raw body text instead of routing an empty 204 through
|
|
121
|
+
* `requestJson`'s JSON parse.
|
|
122
|
+
*
|
|
123
|
+
* The standing rule itself is unchanged and still binding — which still keeps
|
|
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.
|
|
67
162
|
*/
|
|
68
163
|
export const TOOLS = [
|
|
69
164
|
lintIntentAnnotations,
|
|
70
165
|
getRepositoryOverview,
|
|
71
166
|
listRepositories,
|
|
72
167
|
addRepository,
|
|
168
|
+
registrableRepositories,
|
|
169
|
+
removeRepository,
|
|
170
|
+
createRepositoryApiKey,
|
|
171
|
+
revokeRepositoryApiKey,
|
|
172
|
+
nearDuplicateClusters,
|
|
173
|
+
listRepositoryMembers,
|
|
174
|
+
addRepositoryMember,
|
|
175
|
+
updateRepositoryMemberPermissions,
|
|
176
|
+
removeRepositoryMember,
|
|
177
|
+
renameRepository,
|
|
73
178
|
];
|
|
74
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,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,0BAA0B,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;
|