specguard-mcp 0.1.11 → 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.
@@ -1,4 +1,5 @@
1
- import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
1
+ import { getJsonObject, requireUserOrAgentApiConfig } from "../support/specguard-api.js";
2
+ import { optionalString } from "./args.js";
2
3
  /**
3
4
  * `GET /api/v1/repositories` as a tool — shipped today in the platform
4
5
  * (`specguard/config/routes.rb`, `Api::V1::UserRepositoriesController#index`).
@@ -8,9 +9,9 @@ import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js
8
9
  * Every other tool here answers about ONE repository the caller has already
9
10
  * named — and a bridge that can only answer about a repository you can already
10
11
  * name cannot tell an agent which repositories there ARE. `get_repository_overview`
11
- * does not take a repository: its `sgk_` key IS the repository, so the question
12
- * "what may I ask about" has no answer anywhere in this server. This is that
13
- * 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.
14
15
  *
15
16
  * It is also the tool that proves the second credential slot works end to end,
16
17
  * which is why it shipped alone. It no longer IS alone: `add_repository`
@@ -33,20 +34,60 @@ import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js
33
34
  * `Api::BaseController` decides which credential table to consult from the
34
35
  * token's PREFIX, before any table is read, and answers 401 on a mismatch
35
36
  * without a lookup — so this endpoint refuses the `sgk_` key
36
- * `get_repository_overview` uses, and that one refuses this key. Hence
37
- * `requireUserApiConfig` rather than `requireApiConfig`: the two are the same
38
- * function over different variables, and the `Credential` each carries is what
39
- * makes a 401 or an unset variable name the one the OPERATOR of this tool has
40
- * to go and fix. See `config.ts`.
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
41
  *
42
- * == No arguments, because the credential is the whole question
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.
43
49
  *
44
- * The endpoint takes no parameters: the person the `sgu_` key speaks for is the
45
- * entire scope of the answer, and `Repository.accessible_by` — owned UNION
46
- * shared-through-a-membership — is the platform's read-side boundary rather
47
- * than a filter this bridge could widen or narrow. A repository the person
48
- * neither owns nor is a member of never enters the response, so there is
49
- * nothing here for a parameter to select and nothing an argument could reach.
50
+ * == The credential is the SCOPE, and the arguments narrow WITHIN it
51
+ *
52
+ * This file first shipped argument-less on a premise that has since rotted: it
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
55
+ * 2026-09-05, when specguard `ef6236d` (SPGD-940, #941) grew
56
+ * `GET /api/v1/repositories` three narrowing asks through the shared
57
+ * `RepositoryNarrowing` concern — `?q=` (case-insensitive substring on
58
+ * `github_full_name`, with the LIKE wildcards escaped server-side, so a
59
+ * literal `org/my_repo` does not widen into a pattern match), `?role=owned`
60
+ * and `?role=shared` (the exact partition of the accessible set into owned and
61
+ * shared halves), and `?sort=stale` (never-ingested repositories first, then
62
+ * least-recently-ingested, `github_full_name` breaking ties so the order is
63
+ * deterministic). The concern's own comment says the endpoint reads them "for
64
+ * a machine" — and this bridge IS that machine, so they are forwarded here
65
+ * rather than re-invented.
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
76
+ * credential already admits. Out-of-vocabulary values are the server's to
77
+ * clamp, not ours: an unknown `role` or `sort` settles to the no-ask, never a
78
+ * 400, which is also why a value that reaches the wire is passed through
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"`).
84
+ *
85
+ * Blank is no ask, and so is undefined: `optionalString` returns `undefined`
86
+ * for a blank value and `getJson` omits an `undefined` query entry, so
87
+ * declining an ask and omitting the argument are the same wire request — the
88
+ * established spelling in this codebase (`repository-overview.ts` states it
89
+ * as build-don't-stringify), and the reason the no-ask request below is
90
+ * byte-identical to the one this tool made before it had arguments at all.
50
91
  *
51
92
  * == The response is passed through, not re-modelled
52
93
  *
@@ -58,50 +99,144 @@ import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js
58
99
  * `repository` block, so a client that has read one knows how to read the other.
59
100
  * Renaming or flattening anything here would spend that parity on the last hop.
60
101
  *
61
- * `role` is the field this surface adds — `"owner"` or `"member"` — because the
62
- * list MIXES repositories the person owns with repositories somebody shared
63
- * with them and no other field separates the two. An agent that will later
64
- * register keys or change settings needs to know which of these it may expect
65
- * to administer, so the value is named in the description rather than left to be
66
- * discovered from the data.
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.
67
117
  *
68
118
  * The order is `full_name` ascending, which the controller picks as the only
69
119
  * column a client can page or diff against without SpecGuard promising an id
70
- * ordering it has not designed. It is stated here for the same reason the other
71
- * tool states its orders: a list whose order is a coincidence and a list whose
72
- * order is a contract look identical in a response body.
120
+ * ordering it has not designed — and `?sort=stale` re-sequences exactly that
121
+ * loaded set rather than issuing a different query, so the entries never
122
+ * change, only their order does. It is stated here for the same reason the
123
+ * other tool states its orders: a list whose order is a coincidence and a list
124
+ * whose order is a contract look identical in a response body.
73
125
  */
74
126
  const listRepositories = {
75
127
  name: "list_repositories",
76
128
  title: "List repositories",
77
- description: "Lists the SpecGuard repositories the person behind this server's user API key may open — " +
78
- "the answer to \"what can I ask about\", which no other tool here can give, because every " +
79
- "other tool is already scoped to one repository by its key. " +
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. " +
80
132
  "Each entry carries `id`, `full_name` (`org/repo`, and the handle every other surface names " +
81
133
  "a repository by), `name`, `registered_at` and `role`. " +
82
- "`role` is `owner` or `member`: the list mixes repositories this person owns with " +
83
- "repositories somebody shared with them, and nothing else distinguishes the two — read it " +
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 " +
84
139
  "before assuming a repository is yours to administer. " +
85
- "Ordered by `full_name` ascending, which is stable across calls. " +
86
- "The set is exactly what this person may see — a repository they neither own nor were given " +
87
- "access to is absent rather than filtered, so an empty list means no access, never an error. " +
88
- "Needs SPECGUARD_USER_API_KEY (an sgu_… key), which is a DIFFERENT credential from the " +
89
- "sgk_… repository key get_repository_overview reads; SpecGuard refuses each in the other's " +
90
- "place.",
140
+ "Three optional asks narrow WITHIN that set — none of them can widen it — all optional, " +
141
+ "composable on one call: `q` (case-insensitive substring on `full_name`), " +
142
+ "`role: \"owned\"` or `\"shared\"` (one half of the owned/shared mix — note the ask is " +
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 " +
146
+ "CI has never ingested a run for first, then least-recently-ingested, `full_name` " +
147
+ "breaking ties). Omit them all and the request is the plain full list. " +
148
+ "Ordered by `full_name` ascending unless `sort` asks otherwise, and stable across calls " +
149
+ "either way. " +
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.",
91
161
  inputSchema: {
92
162
  type: "object",
93
- // No properties, deliberately — see this file's header. Still CLOSED rather
94
- // than merely empty: `additionalProperties: false` is advertised in
95
- // `tools/list`, so a client that honours the schema REJECTS an invented
163
+ properties: {
164
+ q: {
165
+ type: "string",
166
+ description: "Keep only the repositories whose `github_full_name` (`org/repo`) CONTAINS this " +
167
+ "substring, case-insensitively — the same ask the deployment's web grid reads. " +
168
+ "A plain substring, not a pattern: the server matches with `ILIKE '%…%'` and escapes " +
169
+ "the LIKE wildcards (`%`, `_`, `\\`) first, so `org/my_repo` matches itself rather " +
170
+ "than widening into `my-repo` and `myxrepo`. " +
171
+ "It narrows the SAME set the plain call serves — a repository the credential does not " +
172
+ "admit never enters the response, so no `q` can make one appear — and an ask that " +
173
+ "matches nothing is an empty list with a 200, the same answer as having no access, " +
174
+ "not an error. " +
175
+ "Blank (or null) is no ask: the request is then byte-identical to omitting the " +
176
+ "argument.",
177
+ },
178
+ role: {
179
+ type: "string",
180
+ enum: ["owned", "shared"],
181
+ description: "Keep only ONE half of the list's mix: `owned` keeps the repositories this person " +
182
+ "owns, `shared` keeps the ones somebody shared with them. The two partition the " +
183
+ "accessible set exactly — a repository owned AND shared cannot exist by construction, " +
184
+ "so nothing is lost by asking for one half — and both chain onto the credential " +
185
+ "boundary, so neither half can be wider than the plain call's answer. " +
186
+ "THE ASK VALUES ARE SPELLED DIFFERENTLY FROM THE RESPONSE FIELD: an entry's `role` is " +
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: " +
189
+ "\"owner\"` and you are not asking for anything. A client that honours this schema " +
190
+ "cannot send it (the enum refuses it before the call); a client that bypasses the " +
191
+ "schema gets the server's clamp, where any value outside `owned`/`shared` settles to " +
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. " +
195
+ "Blank (or null) is no ask: byte-identical to omitting the argument.",
196
+ },
197
+ sort: {
198
+ type: "string",
199
+ enum: ["stale"],
200
+ description: "Re-order the list stalest-first. The default order is `full_name` ascending, which " +
201
+ "is stable across calls but says nothing about what needs attention; `stale` puts " +
202
+ "the repositories CI has NEVER ingested a run for FIRST (never-ingested is the " +
203
+ "stalest state on this list, not a zero), then least-recently-ingested, newest last, " +
204
+ "with `github_full_name` breaking ties so two calls with the same data agree element " +
205
+ "for element. " +
206
+ "It re-sequences the loaded set rather than issuing a different query: the same " +
207
+ "entries, a different order. `stale` is the only ordering the endpoint names — there " +
208
+ "is deliberately no word for the default, because omitting the argument already " +
209
+ "means it. " +
210
+ "Any other value a schema-bypassing client sends settles to the server's no-ask clamp " +
211
+ "(default order, no error); blank (or null) is no ask, byte-identical to omitting the " +
212
+ "argument.",
213
+ },
214
+ },
215
+ // Still CLOSED rather than open: `additionalProperties: false` is advertised
216
+ // in `tools/list`, so a client that honours the schema REJECTS an invented
96
217
  // argument before the call is made. Nothing on this side refuses it —
97
- // `server.ts` forwards `arguments` unvalidated and `run` ignores them — so
98
- // an open schema would have the argument silently dropped and the call
99
- // answered as if it had been honoured.
218
+ // `server.ts` forwards `arguments` unvalidated — so an open schema would
219
+ // have the argument silently dropped and the call answered as if it had
220
+ // been honoured. The enum vocabularies above are the server's own accepted
221
+ // values (`RepositoryNarrowing`'s ROLES and SORTS), not a second list this
222
+ // bridge keeps in step by hand.
100
223
  additionalProperties: false,
101
224
  },
102
- async run(_args, context) {
103
- const api = requireUserApiConfig(context.config);
104
- const listing = await getJsonObject(api, "/api/v1/repositories", {}, context.fetch);
225
+ async run(args, context) {
226
+ const q = optionalString(args["q"], "q");
227
+ const role = optionalString(args["role"], "role");
228
+ const sort = optionalString(args["sort"], "sort");
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);
233
+ // Built, not stringified — the established rule for a query object
234
+ // (`repository-overview.ts`): `getJson` omits an `undefined` entry and sets
235
+ // everything else verbatim, so an ask declined (blank), omitted, or never
236
+ // defined all compose to the ONE request the caller meant, and with no asks
237
+ // at all the object is `{}` — the byte-identical request this tool made
238
+ // before it had arguments.
239
+ const listing = await getJsonObject(api, "/api/v1/repositories", { q, role, sort }, context.fetch);
105
240
  return {
106
241
  text: JSON.stringify(listing, null, 2),
107
242
  structured: listing,
@@ -1 +1 @@
1
- {"version":3,"file":"list-repositories.js","sourceRoot":"","sources":["../../../src/tools/list-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGlF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AACH,MAAM,gBAAgB,GAAmB;IACvC,IAAI,EAAE,mBAAmB;IACzB,KAAK,EAAE,mBAAmB;IAC1B,WAAW,EACT,2FAA2F;QAC3F,2FAA2F;QAC3F,6DAA6D;QAC7D,6FAA6F;QAC7F,wDAAwD;QACxD,mFAAmF;QACnF,2FAA2F;QAC3F,uDAAuD;QACvD,kEAAkE;QAClE,6FAA6F;QAC7F,8FAA8F;QAC9F,wFAAwF;QACxF,4FAA4F;QAC5F,QAAQ;IACV,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,4EAA4E;QAC5E,oEAAoE;QACpE,wEAAwE;QACxE,sEAAsE;QACtE,2EAA2E;QAC3E,uEAAuE;QACvE,uCAAuC;QACvC,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,CAAC,GAAG,EAAE,sBAAsB,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QAEpF,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,gBAAgB,CAAC"}
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), the same credential `list_repositories` " +
50
- "reads and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
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,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,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"}
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 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:
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 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:
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; 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. " +
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 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.",
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
- // 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.
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(_args, context) {
102
- const api = requireApiConfig(context.config);
103
- const overview = await getJsonObject(api, "/api/v1/repository", {
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;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"}
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"}
@@ -42,10 +42,15 @@ import type { ToolDefinition } from "./types.js";
42
42
  * ago" is a different fact from "you never had one", and the tool description
43
43
  * is where an agent learns to branch on it.
44
44
  *
45
- * == No arguments, for the same reason `list_repositories` has none
45
+ * == No arguments, for the same reason `list_repositories` used to have none
46
46
  *
47
47
  * The credential is the whole of the scope. The endpoint takes no parameters,
48
- * and nothing an argument could select reaches this answer.
48
+ * and nothing an argument could select reaches this answer. (`list_repositories`
49
+ * argued the same way and argued it the same amount — until its endpoint grew
50
+ * the SPGD-940 narrowing asks and the tool grew the arguments that forward
51
+ * them. This endpoint has grown no asks, so the argument-less shape here is
52
+ * still the honest one, and the sentence above keeps saying so rather than
53
+ * inheriting a reason that moved.)
49
54
  */
50
55
  declare const registrableRepositories: ToolDefinition;
51
56
  export default registrableRepositories;
@@ -42,10 +42,15 @@ import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js
42
42
  * ago" is a different fact from "you never had one", and the tool description
43
43
  * is where an agent learns to branch on it.
44
44
  *
45
- * == No arguments, for the same reason `list_repositories` has none
45
+ * == No arguments, for the same reason `list_repositories` used to have none
46
46
  *
47
47
  * The credential is the whole of the scope. The endpoint takes no parameters,
48
- * and nothing an argument could select reaches this answer.
48
+ * and nothing an argument could select reaches this answer. (`list_repositories`
49
+ * argued the same way and argued it the same amount — until its endpoint grew
50
+ * the SPGD-940 narrowing asks and the tool grew the arguments that forward
51
+ * them. This endpoint has grown no asks, so the argument-less shape here is
52
+ * still the honest one, and the sentence above keeps saying so rather than
53
+ * inheriting a reason that moved.)
49
54
  */
50
55
  const registrableRepositories = {
51
56
  name: "registrable_repositories",
@@ -68,8 +73,8 @@ const registrableRepositories = {
68
73
  "telling the person what to do. A name appearing in the list is not a promise the write " +
69
74
  "will succeed: someone may register it between this read and the POST. Ordered by " +
70
75
  "`full_name` ascending, which is stable across calls. Needs SPECGUARD_USER_API_KEY (an " +
71
- "sgu_… key), the same credential `list_repositories` and `add_repository` read and a " +
72
- "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.",
73
78
  inputSchema: {
74
79
  type: "object",
75
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;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,sFAAsF;QACtF,6EAA6E;IAC/E,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"}
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. And the truncation contract, which is NOT the uniform family it looks like from the
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