specguard-mcp 0.1.12 → 0.1.13
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 +113 -58
- package/dist/src/config.d.ts +100 -18
- package/dist/src/config.js +125 -5
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +7 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/support/specguard-api.d.ts +2 -2
- package/dist/src/support/specguard-api.js +11 -10
- package/dist/src/support/specguard-api.js.map +1 -1
- package/dist/src/tools/add-repository.js +3 -2
- package/dist/src/tools/add-repository.js.map +1 -1
- package/dist/src/tools/index.d.ts +4 -3
- package/dist/src/tools/index.js +4 -3
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-repositories.d.ts +47 -25
- package/dist/src/tools/list-repositories.js +79 -40
- package/dist/src/tools/list-repositories.js.map +1 -1
- package/dist/src/tools/list-repository-members.js +3 -2
- package/dist/src/tools/list-repository-members.js.map +1 -1
- package/dist/src/tools/near-duplicate-clusters.d.ts +20 -8
- package/dist/src/tools/near-duplicate-clusters.js +70 -23
- package/dist/src/tools/near-duplicate-clusters.js.map +1 -1
- package/dist/src/tools/registrable-repositories.js +2 -2
- package/dist/src/tools/registrable-repositories.js.map +1 -1
- package/dist/src/tools/repository-overview.d.ts +41 -1
- package/dist/src/tools/repository-overview.js +107 -6
- package/dist/src/tools/repository-overview.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getJsonObject,
|
|
1
|
+
import { getJsonObject, requireUserOrAgentApiConfig } from "../support/specguard-api.js";
|
|
2
2
|
import { optionalString } from "./args.js";
|
|
3
3
|
/**
|
|
4
4
|
* `GET /api/v1/repositories` as a tool — shipped today in the platform
|
|
@@ -9,9 +9,9 @@ import { optionalString } from "./args.js";
|
|
|
9
9
|
* Every other tool here answers about ONE repository the caller has already
|
|
10
10
|
* named — and a bridge that can only answer about a repository you can already
|
|
11
11
|
* name cannot tell an agent which repositories there ARE. `get_repository_overview`
|
|
12
|
-
* does not take a repository: its `sgk_` key IS the repository,
|
|
13
|
-
* "what may I ask about" has no answer anywhere in this server. This
|
|
14
|
-
* answer, and it is the whole of what this tool does.
|
|
12
|
+
* without `repository` does not take a repository: its `sgk_` key IS the repository,
|
|
13
|
+
* so the question "what may I ask about" has no answer anywhere in this server. This
|
|
14
|
+
* is that answer, and it is the whole of what this tool does.
|
|
15
15
|
*
|
|
16
16
|
* It is also the tool that proves the second credential slot works end to end,
|
|
17
17
|
* which is why it shipped alone. It no longer IS alone: `add_repository`
|
|
@@ -34,17 +34,24 @@ import { optionalString } from "./args.js";
|
|
|
34
34
|
* `Api::BaseController` decides which credential table to consult from the
|
|
35
35
|
* token's PREFIX, before any table is read, and answers 401 on a mismatch
|
|
36
36
|
* without a lookup — so this endpoint refuses the `sgk_` key
|
|
37
|
-
* `get_repository_overview` uses, and that one refuses this key. Hence
|
|
38
|
-
* `
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* `get_repository_overview` uses, and that one refuses this key. Hence a
|
|
38
|
+
* `require*` helper rather than a raw key: the `Credential` carried on the
|
|
39
|
+
* resolved config is what makes a 401 or an unset variable name the one the
|
|
40
|
+
* OPERATOR of this tool has to go and fix. See `config.ts`.
|
|
41
|
+
*
|
|
42
|
+
* Since SPGD-953 it is `requireUserOrAgentApiConfig`, because SPGD-952 made the
|
|
43
|
+
* endpoint answer to BOTH key kinds — the person's `accessible_by` set for an
|
|
44
|
+
* `sgu_` key, the agent key's own granted set for an `sga_` one — and an agent
|
|
45
|
+
* holding only the agent credential must still be able to ask "what may I ask
|
|
46
|
+
* about". When both variables are set the agent key wins, so the listing names
|
|
47
|
+
* the same set every other agent-keyed tool answers inside; the precedence and
|
|
48
|
+
* its reasoning live on the helper.
|
|
42
49
|
*
|
|
43
50
|
* == The credential is the SCOPE, and the arguments narrow WITHIN it
|
|
44
51
|
*
|
|
45
52
|
* This file first shipped argument-less on a premise that has since rotted: it
|
|
46
|
-
* said "the endpoint takes no parameters", because the
|
|
47
|
-
*
|
|
53
|
+
* said "the endpoint takes no parameters", because the credential behind the
|
|
54
|
+
* key was the entire scope of the answer. That stopped being true on
|
|
48
55
|
* 2026-09-05, when specguard `ef6236d` (SPGD-940, #941) grew
|
|
49
56
|
* `GET /api/v1/repositories` three narrowing asks through the shared
|
|
50
57
|
* `RepositoryNarrowing` concern — `?q=` (case-insensitive substring on
|
|
@@ -57,23 +64,29 @@ import { optionalString } from "./args.js";
|
|
|
57
64
|
* a machine" — and this bridge IS that machine, so they are forwarded here
|
|
58
65
|
* rather than re-invented.
|
|
59
66
|
*
|
|
60
|
-
* What has NOT changed is which side of the boundary the asks sit on.
|
|
61
|
-
* `Repository.accessible_by`
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
+
* What has NOT changed is which side of the boundary the asks sit on. WHICH
|
|
68
|
+
* boundary answered follows the credential — `Repository.accessible_by` (owned
|
|
69
|
+
* UNION shared-through-a-membership) for an `sgu_` key, `AgentApiKey#repositories`
|
|
70
|
+
* (the mint-time granted set) for an `sga_` one — and either way it is the
|
|
71
|
+
* platform's read-side rule, not a filter this bridge could widen or narrow:
|
|
72
|
+
* the controller chains every ask onto the relation that boundary already
|
|
73
|
+
* admitted (`narrow_repositories(authorized_repositories, …)`), so a
|
|
74
|
+
* repository the credential does not admit never ENTERS the relation and no
|
|
75
|
+
* argument here widens the answer — it can only narrow, or re-order, what the
|
|
67
76
|
* credential already admits. Out-of-vocabulary values are the server's to
|
|
68
77
|
* clamp, not ours: an unknown `role` or `sort` settles to the no-ask, never a
|
|
69
78
|
* 400, which is also why a value that reaches the wire is passed through
|
|
70
|
-
* verbatim rather than validated against a second vocabulary here.
|
|
79
|
+
* verbatim rather than validated against a second vocabulary here. Under the
|
|
80
|
+
* agent key the `?role=` ask clamps to the no-ask outright — ownership is a
|
|
81
|
+
* person fact and the key speaks for nobody, so there is no owned/shared line
|
|
82
|
+
* to draw (`UserRepositoriesController#requested_role` is where the server
|
|
83
|
+
* writes that rule, beside `#credential_role`'s `role: "agent"`).
|
|
71
84
|
*
|
|
72
85
|
* Blank is no ask, and so is undefined: `optionalString` returns `undefined`
|
|
73
86
|
* for a blank value and `getJson` omits an `undefined` query entry, so
|
|
74
87
|
* declining an ask and omitting the argument are the same wire request — the
|
|
75
88
|
* established spelling in this codebase (`repository-overview.ts` states it
|
|
76
|
-
* as build-don't-stringify), and the reason the no-
|
|
89
|
+
* as build-don't-stringify), and the reason the no-ask request below is
|
|
77
90
|
* byte-identical to the one this tool made before it had arguments at all.
|
|
78
91
|
*
|
|
79
92
|
* == The response is passed through, not re-modelled
|
|
@@ -86,12 +99,21 @@ import { optionalString } from "./args.js";
|
|
|
86
99
|
* `repository` block, so a client that has read one knows how to read the other.
|
|
87
100
|
* Renaming or flattening anything here would spend that parity on the last hop.
|
|
88
101
|
*
|
|
89
|
-
* `role` is the field this surface adds
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* register keys or change settings needs to know which of these
|
|
93
|
-
*
|
|
94
|
-
*
|
|
102
|
+
* `role` is the field this surface adds, and its value depends on WHICH
|
|
103
|
+
* credential answered — three values, one per credential kind, and all three
|
|
104
|
+
* are named rather than left to be discovered from the data, because an agent
|
|
105
|
+
* that will later register keys or change settings needs to know which of these
|
|
106
|
+
* entries it may expect to administer. Under the `sgu_` PERSON key it is
|
|
107
|
+
* `"owner"` or `"member"`: the list MIXES repositories the person owns with
|
|
108
|
+
* repositories somebody shared with them, and no other field separates the two.
|
|
109
|
+
* Under the `sga_` AGENT key every entry is `"agent"` — the key is NOBODY, so
|
|
110
|
+
* the owner/member question does not apply, and a client branching on
|
|
111
|
+
* owner/member reads false for both, which is the correct reading rather than a
|
|
112
|
+
* gap (`Api::V1::UserRepositoriesController#credential_role` is where the
|
|
113
|
+
* server writes that rule down, beside the `?role=` clamp that exists for the
|
|
114
|
+
* same reason). One value per kind also means the field identifies WHICH
|
|
115
|
+
* credential served the list — worth knowing when both variables are set and
|
|
116
|
+
* the agent key wins.
|
|
95
117
|
*
|
|
96
118
|
* The order is `full_name` ascending, which the controller picks as the only
|
|
97
119
|
* column a client can page or diff against without SpecGuard promising an id
|
|
@@ -104,27 +126,38 @@ import { optionalString } from "./args.js";
|
|
|
104
126
|
const listRepositories = {
|
|
105
127
|
name: "list_repositories",
|
|
106
128
|
title: "List repositories",
|
|
107
|
-
description: "Lists the SpecGuard repositories
|
|
108
|
-
"
|
|
109
|
-
"
|
|
129
|
+
description: "Lists the SpecGuard repositories this server's key may open — the answer to \"what can I " +
|
|
130
|
+
"ask about\", which no other tool here can give, because every other tool is already scoped " +
|
|
131
|
+
"to one repository by its key. " +
|
|
110
132
|
"Each entry carries `id`, `full_name` (`org/repo`, and the handle every other surface names " +
|
|
111
133
|
"a repository by), `name`, `registered_at` and `role`. " +
|
|
112
|
-
"`role`
|
|
113
|
-
"
|
|
134
|
+
"`role` has one value per credential kind. Under a PERSON key (`sgu_…`) it is `owner` or " +
|
|
135
|
+
"`member`: the list mixes repositories this person owns with repositories somebody shared " +
|
|
136
|
+
"with them, and nothing else distinguishes the two. Under an AGENT key (`sga_…`) every " +
|
|
137
|
+
"entry is `agent` — the value that says the ownership question does not apply because the " +
|
|
138
|
+
"key speaks for nobody; branching on owner/member correctly reads false for both. Read it " +
|
|
114
139
|
"before assuming a repository is yours to administer. " +
|
|
115
140
|
"Three optional asks narrow WITHIN that set — none of them can widen it — all optional, " +
|
|
116
141
|
"composable on one call: `q` (case-insensitive substring on `full_name`), " +
|
|
117
142
|
"`role: \"owned\"` or `\"shared\"` (one half of the owned/shared mix — note the ask is " +
|
|
118
|
-
"spelled `owned`, not the response field's `owner
|
|
143
|
+
"spelled `owned`, not the response field's `owner`; under the AGENT key this ask settles " +
|
|
144
|
+
"to the no-ask, because ownership is a person fact and the key speaks for nobody), and " +
|
|
145
|
+
"`sort: \"stale\"` (repositories " +
|
|
119
146
|
"CI has never ingested a run for first, then least-recently-ingested, `full_name` " +
|
|
120
147
|
"breaking ties). Omit them all and the request is the plain full list. " +
|
|
121
148
|
"Ordered by `full_name` ascending unless `sort` asks otherwise, and stable across calls " +
|
|
122
149
|
"either way. " +
|
|
123
|
-
"The set is exactly what
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
150
|
+
"The set is exactly what the key behind it may see — a repository outside the credential's " +
|
|
151
|
+
"own boundary is absent rather than filtered, so an empty list means no access, never an " +
|
|
152
|
+
"error. " +
|
|
153
|
+
"It authenticates with EITHER of this server's two list-scoped credentials, whichever is " +
|
|
154
|
+
"set: SPECGUARD_AGENT_API_KEY (an sga_… key — the answer is the repository set granted onto " +
|
|
155
|
+
"that key at mint time, the same set every other agent-keyed tool here answers inside) and, " +
|
|
156
|
+
"when that is not set, SPECGUARD_USER_API_KEY (an sgu_… key — the answer is what that person " +
|
|
157
|
+
"may open). With both set the agent key wins, so discovery stays inside the set the other " +
|
|
158
|
+
"tools can actually reach. " +
|
|
159
|
+
"Either way it is a DIFFERENT credential from the sgk_… repository key " +
|
|
160
|
+
"get_repository_overview reads; SpecGuard refuses each in the other's place.",
|
|
128
161
|
inputSchema: {
|
|
129
162
|
type: "object",
|
|
130
163
|
properties: {
|
|
@@ -151,11 +184,14 @@ const listRepositories = {
|
|
|
151
184
|
"so nothing is lost by asking for one half — and both chain onto the credential " +
|
|
152
185
|
"boundary, so neither half can be wider than the plain call's answer. " +
|
|
153
186
|
"THE ASK VALUES ARE SPELLED DIFFERENTLY FROM THE RESPONSE FIELD: an entry's `role` is " +
|
|
154
|
-
"`\"owner\"
|
|
187
|
+
"`\"owner\"`/`\"member\"` under the PERSON key and `\"agent\"` under the AGENT one (the " +
|
|
188
|
+
"key speaks for nobody), but the ask is `owned`/`shared` — send `role: " +
|
|
155
189
|
"\"owner\"` and you are not asking for anything. A client that honours this schema " +
|
|
156
190
|
"cannot send it (the enum refuses it before the call); a client that bypasses the " +
|
|
157
191
|
"schema gets the server's clamp, where any value outside `owned`/`shared` settles to " +
|
|
158
|
-
"the no-ask and the full list is served — never an error
|
|
192
|
+
"the no-ask and the full list is served — never an error — and under the AGENT key " +
|
|
193
|
+
"the ask settles to the no-ask for EVERY value, because ownership is a person fact " +
|
|
194
|
+
"and the key speaks for nobody. " +
|
|
159
195
|
"Blank (or null) is no ask: byte-identical to omitting the argument.",
|
|
160
196
|
},
|
|
161
197
|
sort: {
|
|
@@ -190,7 +226,10 @@ const listRepositories = {
|
|
|
190
226
|
const q = optionalString(args["q"], "q");
|
|
191
227
|
const role = optionalString(args["role"], "role");
|
|
192
228
|
const sort = optionalString(args["sort"], "sort");
|
|
193
|
-
|
|
229
|
+
// EITHER credential the endpoint serves — agent key preferred, user key as
|
|
230
|
+
// the fallback, both named in the refusal when neither is set. The
|
|
231
|
+
// precedence and its scope argument live on the helper in `config.ts`.
|
|
232
|
+
const api = requireUserOrAgentApiConfig(context.config);
|
|
194
233
|
// Built, not stringified — the established rule for a query object
|
|
195
234
|
// (`repository-overview.ts`): `getJson` omits an `undefined` entry and sets
|
|
196
235
|
// everything else verbatim, so an ask declined (blank), omitted, or never
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-repositories.js","sourceRoot":"","sources":["../../../src/tools/list-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"list-repositories.js","sourceRoot":"","sources":["../../../src/tools/list-repositories.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,8 +46,9 @@ const listRepositoryMembers = {
|
|
|
46
46
|
"`members.manage` is refused 403 in SpecGuard's own words. " +
|
|
47
47
|
"Takes `repository_id` — the numeric id `list_repositories` reports, not the `org/repo` " +
|
|
48
48
|
"handle. " +
|
|
49
|
-
"Needs SPECGUARD_USER_API_KEY (an sgu_… key)
|
|
50
|
-
"
|
|
49
|
+
"Needs SPECGUARD_USER_API_KEY (an sgu_… key) — a PERSON key, which `list_repositories` " +
|
|
50
|
+
"also accepts but does not require — and a DIFFERENT one from the sgk_… repository key " +
|
|
51
|
+
"`get_repository_overview` uses.",
|
|
51
52
|
inputSchema: {
|
|
52
53
|
type: "object",
|
|
53
54
|
properties: {
|
|
@@ -1 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,0FAA0F;QAC1F,6FAA6F;QAC7F,iGAAiG;QACjG,2FAA2F;QAC3F,2FAA2F;QAC3F,4DAA4D;QAC5D,wFAAwF;QACxF,4FAA4F;QAC5F,4DAA4D;QAC5D,yFAAyF;QACzF,UAAU;QACV,
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,0FAA0F;QAC1F,6FAA6F;QAC7F,iGAAiG;QACjG,2FAA2F;QAC3F,2FAA2F;QAC3F,4DAA4D;QAC5D,wFAAwF;QACxF,4FAA4F;QAC5F,4DAA4D;QAC5D,yFAAyF;QACzF,UAAU;QACV,wFAAwF;QACxF,wFAAwF;QACxF,iCAAiC;IACnC,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,wEAAwE;QACxE,oDAAoD;QACpD,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"}
|
|
@@ -26,15 +26,27 @@ import type { ToolDefinition } from "./types.js";
|
|
|
26
26
|
* The server reads only whether the parameter is PRESENT
|
|
27
27
|
* (`RequestedNearDuplicatesParam`): `?near_duplicates=false` opens the block
|
|
28
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
* There is no "off" value for a client to send, so nothing about the CENSUS is
|
|
30
|
+
* choosable from here — the clusters are the repository's, computed over every
|
|
31
|
+
* run, and one call returns them all. What IS choosable, since SPGD-953, is
|
|
32
|
+
* WHICH REPOSITORY is censused: an optional `repository` argument (a numeric id
|
|
33
|
+
* from `list_repositories`) moves the call to the plural endpoint
|
|
34
|
+
* `GET /api/v1/repositories/:id` under the AGENT key, same body (minus the
|
|
35
|
+
* `api_key` block, which is ABSENT on that surface rather than nulled — see
|
|
36
|
+
* `repository-overview.ts` for why the omission is the server's, deliberately),
|
|
37
|
+
* same ask, same cost gate. Without it the request is byte-for-byte the singular,
|
|
38
|
+
* `sgk_`-bound one this tool has always made — and the cost argument above is
|
|
39
|
+
* exactly why the argument is OPTIONAL rather than required: an agent that has
|
|
40
|
+
* only ever had one reachable repository should not be asked to learn a second
|
|
41
|
+
* credential to keep reading it.
|
|
42
|
+
*
|
|
43
|
+
* `near_duplicates: "true"` is built rather than stringified for the same
|
|
44
|
+
* reason `repository-overview.ts` builds its `unannotated_examples` key:
|
|
36
45
|
* `getJson` omits only `undefined`, so a conditional send is the only honest
|
|
37
|
-
* way to spell "always" here.
|
|
46
|
+
* way to spell "always" here. The schema stays CLOSED around the one argument:
|
|
47
|
+
* `server.ts` forwards `arguments` unvalidated, and an open schema would let an
|
|
48
|
+
* invented argument ride through and be silently dropped (see
|
|
49
|
+
* `registrable-repositories.ts` for the same call).
|
|
38
50
|
*
|
|
39
51
|
* == The response is passed through, not re-modelled
|
|
40
52
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { requireApiConfig } from "../config.js";
|
|
1
|
+
import { requireAgentApiConfig, requireApiConfig } from "../config.js";
|
|
2
2
|
import { getJsonObject } from "../support/specguard-api.js";
|
|
3
|
+
import { optionalString } from "./args.js";
|
|
3
4
|
/**
|
|
4
5
|
* `GET /api/v1/repository?near_duplicates=` as a tool — shipped in the
|
|
5
6
|
* platform by SPGD-703 (`specguard` `c43dc19`, 2026-08-28), which added the
|
|
@@ -27,15 +28,27 @@ import { getJsonObject } from "../support/specguard-api.js";
|
|
|
27
28
|
* The server reads only whether the parameter is PRESENT
|
|
28
29
|
* (`RequestedNearDuplicatesParam`): `?near_duplicates=false` opens the block
|
|
29
30
|
* 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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* There is no "off" value for a client to send, so nothing about the CENSUS is
|
|
32
|
+
* choosable from here — the clusters are the repository's, computed over every
|
|
33
|
+
* run, and one call returns them all. What IS choosable, since SPGD-953, is
|
|
34
|
+
* WHICH REPOSITORY is censused: an optional `repository` argument (a numeric id
|
|
35
|
+
* from `list_repositories`) moves the call to the plural endpoint
|
|
36
|
+
* `GET /api/v1/repositories/:id` under the AGENT key, same body (minus the
|
|
37
|
+
* `api_key` block, which is ABSENT on that surface rather than nulled — see
|
|
38
|
+
* `repository-overview.ts` for why the omission is the server's, deliberately),
|
|
39
|
+
* same ask, same cost gate. Without it the request is byte-for-byte the singular,
|
|
40
|
+
* `sgk_`-bound one this tool has always made — and the cost argument above is
|
|
41
|
+
* exactly why the argument is OPTIONAL rather than required: an agent that has
|
|
42
|
+
* only ever had one reachable repository should not be asked to learn a second
|
|
43
|
+
* credential to keep reading it.
|
|
44
|
+
*
|
|
45
|
+
* `near_duplicates: "true"` is built rather than stringified for the same
|
|
46
|
+
* reason `repository-overview.ts` builds its `unannotated_examples` key:
|
|
37
47
|
* `getJson` omits only `undefined`, so a conditional send is the only honest
|
|
38
|
-
* way to spell "always" here.
|
|
48
|
+
* way to spell "always" here. The schema stays CLOSED around the one argument:
|
|
49
|
+
* `server.ts` forwards `arguments` unvalidated, and an open schema would let an
|
|
50
|
+
* invented argument ride through and be silently dropped (see
|
|
51
|
+
* `registrable-repositories.ts` for the same call).
|
|
39
52
|
*
|
|
40
53
|
* == The response is passed through, not re-modelled
|
|
41
54
|
*
|
|
@@ -65,9 +78,12 @@ const nearDuplicateClusters = {
|
|
|
65
78
|
"THIS IS THE EXPENSIVE READ ON THIS BRIDGE: the census is linear but measured in seconds — seven " +
|
|
66
79
|
"queries at every size, tens of seconds extrapolated at the 20,000-test design point — which is " +
|
|
67
80
|
"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;
|
|
69
|
-
"
|
|
70
|
-
"
|
|
81
|
+
"`near_duplicates: null` on the plain overview. Calling this tool IS the ask; nothing about the " +
|
|
82
|
+
"census is choosable — the clusters are the repository's, computed over every run, and one call " +
|
|
83
|
+
"returns them all. WHICH repository is censused is the one choice there is: pass `repository` " +
|
|
84
|
+
"(a numeric id from `list_repositories`) to census that named repository under the agent key, " +
|
|
85
|
+
"or omit it to census the repository the configured sgk_… key resolves to, exactly as before " +
|
|
86
|
+
"the argument existed. " +
|
|
71
87
|
"READ THE DISCLOSURE KEYS BEFORE THE COUNT: `similarity_floor` and `similarity_basis` sit FIRST " +
|
|
72
88
|
"in the block and qualify every cluster below them — a cluster count without what 'similar' " +
|
|
73
89
|
"meant is a figure you cannot act on. `truncated: true` means the cluster list was cut at the " +
|
|
@@ -86,21 +102,52 @@ const nearDuplicateClusters = {
|
|
|
86
102
|
"success state (nothing reads alike), and the three silences — nothing ingested " +
|
|
87
103
|
"(`recorded_count: 0`), nothing embedded (`identity_count: 0`), nothing alike — are kept " +
|
|
88
104
|
"distinguishable by those counts rather than collapsed into one empty list. " +
|
|
89
|
-
"Same
|
|
90
|
-
"`GET /api/v1/repository
|
|
91
|
-
"
|
|
105
|
+
"Same endpoints and credentials as `get_repository_overview`: without `repository`, an " +
|
|
106
|
+
"`sgk_` repository key on `GET /api/v1/repository`; with `repository`, an `sga_` agent key " +
|
|
107
|
+
"(SPECGUARD_AGENT_API_KEY) on `GET /api/v1/repositories/:id`, whose answer the key's own " +
|
|
108
|
+
"granted repository set bounds. SpecGuard refuses each credential in the other's place, so " +
|
|
109
|
+
"a `repository` ask without the agent key set is refused HERE, by name, before any request " +
|
|
110
|
+
"is made. " +
|
|
111
|
+
"The response is the endpoint's full body with the `near_duplicates` block OPENED, passed " +
|
|
112
|
+
"through unmodified — with the one surface difference `get_repository_overview` documents " +
|
|
113
|
+
"for its own `repository` ask: on that plural path the `api_key` block is ABSENT from the " +
|
|
114
|
+
"body rather than nulled (it describes the credential that made the request, and an agent " +
|
|
115
|
+
"key is not a repository key), so an absent `api_key` there is the surface's shape, never " +
|
|
116
|
+
"a dropped block.",
|
|
92
117
|
inputSchema: {
|
|
93
118
|
type: "object",
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
119
|
+
properties: {
|
|
120
|
+
repository: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Census THIS repository instead of the one the configured sgk_… key resolves to. The " +
|
|
123
|
+
"value is the repository's NUMERIC ID, exactly as served in `list_repositories` " +
|
|
124
|
+
"entries' `id` — not the `org/repo` handle. " +
|
|
125
|
+
"The credential changes with it, because SpecGuard refuses each key kind in the " +
|
|
126
|
+
"other's place: the call authenticates with SPECGUARD_AGENT_API_KEY (an sga_… agent " +
|
|
127
|
+
"key) instead of SPECGUARD_API_KEY, and the key's granted repository set is the " +
|
|
128
|
+
"boundary the id is resolved inside — a repository outside the set answers 404, " +
|
|
129
|
+
"indistinguishable from one that does not exist. The census itself is identical on " +
|
|
130
|
+
"either path: same block, same caps, same disclosure keys. " +
|
|
131
|
+
"Omit it — or pass a blank — and the call is byte-for-byte the singular one under " +
|
|
132
|
+
"SPECGUARD_API_KEY, exactly as before this argument existed.",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
99
135
|
additionalProperties: false,
|
|
100
136
|
},
|
|
101
|
-
async run(
|
|
102
|
-
const
|
|
103
|
-
|
|
137
|
+
async run(args, context) {
|
|
138
|
+
const repository = optionalString(args["repository"], "repository");
|
|
139
|
+
// WHICH ENDPOINT AND WHICH CREDENTIAL is one branch point over the one ask,
|
|
140
|
+
// for the reason `repository-overview.ts` states at its own call site: the
|
|
141
|
+
// pair (path, credential) must not be mixable, because either mixed pairing
|
|
142
|
+
// is a 401 at the deployment by design and both are refused here, legibly,
|
|
143
|
+
// instead.
|
|
144
|
+
const api = repository === undefined
|
|
145
|
+
? requireApiConfig(context.config)
|
|
146
|
+
: requireAgentApiConfig(context.config);
|
|
147
|
+
const path = repository === undefined
|
|
148
|
+
? "/api/v1/repository"
|
|
149
|
+
: `/api/v1/repositories/${encodeURIComponent(repository)}`;
|
|
150
|
+
const overview = await getJsonObject(api, path, {
|
|
104
151
|
// Always sent, always `"true"` — the server reads only that the key is
|
|
105
152
|
// present (`?near_duplicates=false` opens the block too), and this
|
|
106
153
|
// tool exists to open it. See this file's header.
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"near-duplicate-clusters.js","sourceRoot":"","sources":["../../../src/tools/near-duplicate-clusters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;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,iGAAiG;QACjG,iGAAiG;QACjG,+FAA+F;QAC/F,+FAA+F;QAC/F,8FAA8F;QAC9F,wBAAwB;QACxB,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,wFAAwF;QACxF,4FAA4F;QAC5F,0FAA0F;QAC1F,4FAA4F;QAC5F,4FAA4F;QAC5F,WAAW;QACX,2FAA2F;QAC3F,2FAA2F;QAC3F,2FAA2F;QAC3F,2FAA2F;QAC3F,2FAA2F;QAC3F,kBAAkB;IAEpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,UAAU,EAAE;gBACV,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,sFAAsF;oBACtF,iFAAiF;oBACjF,6CAA6C;oBAC7C,iFAAiF;oBACjF,qFAAqF;oBACrF,iFAAiF;oBACjF,iFAAiF;oBACjF,oFAAoF;oBACpF,4DAA4D;oBAC5D,mFAAmF;oBACnF,6DAA6D;aAChE;SACF;QACD,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC,CAAC;QACpE,4EAA4E;QAC5E,2EAA2E;QAC3E,4EAA4E;QAC5E,2EAA2E;QAC3E,WAAW;QACX,MAAM,GAAG,GACP,UAAU,KAAK,SAAS;YACtB,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC;YAClC,CAAC,CAAC,qBAAqB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,IAAI,GACR,UAAU,KAAK,SAAS;YACtB,CAAC,CAAC,oBAAoB;YACtB,CAAC,CAAC,wBAAwB,kBAAkB,CAAC,UAAU,CAAC,EAAE,CAAC;QAE/D,MAAM,QAAQ,GAAG,MAAM,aAAa,CAClC,GAAG,EACH,IAAI,EACJ;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"}
|
|
@@ -73,8 +73,8 @@ const registrableRepositories = {
|
|
|
73
73
|
"telling the person what to do. A name appearing in the list is not a promise the write " +
|
|
74
74
|
"will succeed: someone may register it between this read and the POST. Ordered by " +
|
|
75
75
|
"`full_name` ascending, which is stable across calls. Needs SPECGUARD_USER_API_KEY (an " +
|
|
76
|
-
"sgu_… key)
|
|
77
|
-
"DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
|
|
76
|
+
"sgu_… key) — a PERSON key, which `list_repositories` also accepts but does not require — " +
|
|
77
|
+
"and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
|
|
78
78
|
inputSchema: {
|
|
79
79
|
type: "object",
|
|
80
80
|
// No properties, deliberately — see this file's header. Still CLOSED rather
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registrable-repositories.js","sourceRoot":"","sources":["../../../src/tools/registrable-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGlF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,MAAM,uBAAuB,GAAmB;IAC9C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,WAAW,EACT,4FAA4F;QAC5F,2FAA2F;QAC3F,sFAAsF;QACtF,6FAA6F;QAC7F,qFAAqF;QACrF,6FAA6F;QAC7F,yFAAyF;QACzF,mFAAmF;QACnF,8FAA8F;QAC9F,yFAAyF;QACzF,8FAA8F;QAC9F,4FAA4F;QAC5F,6FAA6F;QAC7F,wFAAwF;QACxF,4FAA4F;QAC5F,yFAAyF;QACzF,mFAAmF;QACnF,wFAAwF;QACxF,
|
|
1
|
+
{"version":3,"file":"registrable-repositories.js","sourceRoot":"","sources":["../../../src/tools/registrable-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGlF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,MAAM,uBAAuB,GAAmB;IAC9C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,WAAW,EACT,4FAA4F;QAC5F,2FAA2F;QAC3F,sFAAsF;QACtF,6FAA6F;QAC7F,qFAAqF;QACrF,6FAA6F;QAC7F,yFAAyF;QACzF,mFAAmF;QACnF,8FAA8F;QAC9F,yFAAyF;QACzF,8FAA8F;QAC9F,4FAA4F;QAC5F,6FAA6F;QAC7F,wFAAwF;QACxF,4FAA4F;QAC5F,yFAAyF;QACzF,mFAAmF;QACnF,wFAAwF;QACxF,2FAA2F;QAC3F,mFAAmF;IACrF,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,4EAA4E;QAC5E,yEAAyE;QACzE,0EAA0E;QAC1E,0EAA0E;QAC1E,4CAA4C;QAC5C,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO;QACtB,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,GAAG,EACH,kCAAkC,EAClC,EAAE,EACF,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,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,uBAAuB,CAAC"}
|
|
@@ -18,6 +18,41 @@ import type { ToolDefinition } from "./types.js";
|
|
|
18
18
|
* has grown — so the tool is described in those terms rather than as "get
|
|
19
19
|
* repository", which is not a question anybody asks.
|
|
20
20
|
*
|
|
21
|
+
* == `repository` chooses WHICH REPOSITORY, and the credential changes with it
|
|
22
|
+
*
|
|
23
|
+
* Everything above answers about the one repository the configured `sgk_` key
|
|
24
|
+
* RESOLVES TO — with five repositories in play, four of them are invisible to
|
|
25
|
+
* every agent holding a single-repository key. `repository` is the ask that
|
|
26
|
+
* fixes that: name one by its numeric id (the id `list_repositories` reports,
|
|
27
|
+
* not the `org/repo` handle — the plural endpoint looks the id up inside the
|
|
28
|
+
* calling credential's own read boundary) and the tool calls the PLURAL
|
|
29
|
+
* endpoint, `GET /api/v1/repositories/:id`, which serves the same
|
|
30
|
+
* `RepositoryOverview` body with every parameter below honoured exactly as the
|
|
31
|
+
* singular one serves it — with ONE DELIBERATE OMISSION this branch owns:
|
|
32
|
+
* `api_key` is ABSENT from the plural body, not null. That block describes the
|
|
33
|
+
* credential that made the REQUEST, and the singular controller is the only
|
|
34
|
+
* caller that has one to describe (`RepositoriesController#show` passes
|
|
35
|
+
* `api_key_block: serialized_api_key`; `UserRepositoriesController#show`
|
|
36
|
+
* passes none and says why at length: a block of nulls would be a sentence
|
|
37
|
+
* about a credential that does not exist, because this request was made with
|
|
38
|
+
* an agent or person key, and the repository's own `sgk_` keys are not the
|
|
39
|
+
* caller's to describe). So the `api_key.last_used_at` paragraph below is
|
|
40
|
+
* about the SINGULAR response; under `repository` the key is simply not there,
|
|
41
|
+
* and its absence is the plural surface's shape — never this bridge dropping
|
|
42
|
+
* a key. The two health blocks come from `RepositoryOverview` itself and DO
|
|
43
|
+
* travel. The ladder moves; the subject moves; `api_key` does not.
|
|
44
|
+
*
|
|
45
|
+
* THE CREDENTIAL MOVES WITH IT, because the endpoints refuse each other's
|
|
46
|
+
* tokens before any table is read: the singular path still authenticates with
|
|
47
|
+
* `SPECGUARD_API_KEY` (`sgk_`, one repository), the plural path with
|
|
48
|
+
* `SPECGUARD_AGENT_API_KEY` (`sga_`, the agent credential minted for exactly
|
|
49
|
+
* this — one key, many repositories, each read bounded by the key's own
|
|
50
|
+
* granted set server-side). Omitting `repository` is byte-for-byte today's
|
|
51
|
+
* request, and an operator who never sets the agent variable never sees this
|
|
52
|
+
* half of the tool exist — the blank-is-no-ask rule every argument here follows
|
|
53
|
+
* covers `repository` too, so a blank value falls back to the singular path
|
|
54
|
+
* rather than sending a guaranteed-404 id.
|
|
55
|
+
*
|
|
21
56
|
* == The response is passed through, not re-modelled
|
|
22
57
|
*
|
|
23
58
|
* Every figure in that body is annotated in the controller with the reason for
|
|
@@ -359,7 +394,12 @@ import type { ToolDefinition } from "./types.js";
|
|
|
359
394
|
* timestamp beside its stalest run, and the controller answers that with
|
|
360
395
|
* `acceptance_reported_by` / `rotation_reported_by` naming the keys that answer
|
|
361
396
|
* what it cannot. Naming the correction and not the claim would have been half a
|
|
362
|
-
* sentence.
|
|
397
|
+
* sentence. (The claim itself is served by the SINGULAR body only — the plural
|
|
398
|
+
* one under `repository` omits the whole `api_key` block, the deliberate
|
|
399
|
+
* omission documented at the branch above — so the correction paragraph in the
|
|
400
|
+
* description says which surface the instruction applies to rather than sending
|
|
401
|
+
* a plural-path reader hunting for a key that is not there.) And the truncation
|
|
402
|
+
* contract, which is NOT the uniform family it looks like from the
|
|
363
403
|
* key names: only eight lists have a `*_window` sibling at all, MOST lists under `latest_run`
|
|
364
404
|
* carry an inline `limit` beside `rows` instead, four of those windows serve no bound of their
|
|
365
405
|
* own, `rejections_window` serves a bound and no order, and the lists this census found carrying
|