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.
@@ -1,4 +1,4 @@
1
- import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
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, so the question
13
- * "what may I ask about" has no answer anywhere in this server. This is that
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
- * `requireUserApiConfig` rather than `requireApiConfig`: the two are the same
39
- * function over different variables, and the `Credential` each carries is what
40
- * makes a 401 or an unset variable name the one the OPERATOR of this tool has
41
- * 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
+ *
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 person the `sgu_` key
47
- * speaks for was the entire scope of the answer. That stopped being true on
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` — owned UNION shared-through-a-membership — is
62
- * still the platform's read-side boundary, and the controller chains every ask
63
- * onto the relation that boundary already admitted
64
- * (`narrow_repositories(authorized_repositories, …)`): a repository the person
65
- * neither owns nor is a member of never ENTERS the relation, so no argument
66
- * here widens the answer — it can only narrow, or re-order, what the
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-argument request below is
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 — `"owner"` or `"member"` — because the
90
- * list MIXES repositories the person owns with repositories somebody shared
91
- * with them and no other field separates the two. An agent that will later
92
- * register keys or change settings needs to know which of these it may expect
93
- * to administer, so the value is named in the description rather than left to be
94
- * 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.
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 the person behind this server's user API key may open — " +
108
- "the answer to \"what can I ask about\", which no other tool here can give, because every " +
109
- "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. " +
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` is `owner` or `member`: the list mixes repositories this person owns with " +
113
- "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 " +
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`), and `sort: \"stale\"` (repositories " +
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 this person may see — a repository they neither own nor were given " +
124
- "access to is absent rather than filtered, so an empty list means no access, never an error. " +
125
- "Needs SPECGUARD_USER_API_KEY (an sgu_… key), which is a DIFFERENT credential from the " +
126
- "sgk_… repository key get_repository_overview reads; SpecGuard refuses each in the other's " +
127
- "place.",
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\"` or `\"member\"`, but the ask is `owned`/`shared` — send `role: " +
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
- const api = requireUserApiConfig(context.config);
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":"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"}
1
+ {"version":3,"file":"list-repositories.js","sourceRoot":"","sources":["../../../src/tools/list-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0HG;AACH,MAAM,gBAAgB,GAAmB;IACvC,IAAI,EAAE,mBAAmB;IACzB,KAAK,EAAE,mBAAmB;IAC1B,WAAW,EACT,2FAA2F;QAC3F,6FAA6F;QAC7F,gCAAgC;QAChC,6FAA6F;QAC7F,wDAAwD;QACxD,0FAA0F;QAC1F,2FAA2F;QAC3F,wFAAwF;QACxF,2FAA2F;QAC3F,2FAA2F;QAC3F,uDAAuD;QACvD,yFAAyF;QACzF,2EAA2E;QAC3E,wFAAwF;QACxF,0FAA0F;QAC1F,wFAAwF;QACxF,kCAAkC;QAClC,mFAAmF;QACnF,wEAAwE;QACxE,yFAAyF;QACzF,cAAc;QACd,4FAA4F;QAC5F,0FAA0F;QAC1F,SAAS;QACT,0FAA0F;QAC1F,6FAA6F;QAC7F,6FAA6F;QAC7F,8FAA8F;QAC9F,2FAA2F;QAC3F,4BAA4B;QAC5B,wEAAwE;QACxE,6EAA6E;IAC/E,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,CAAC,EAAE;gBACD,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,iFAAiF;oBACjF,gFAAgF;oBAChF,sFAAsF;oBACtF,oFAAoF;oBACpF,8CAA8C;oBAC9C,uFAAuF;oBACvF,mFAAmF;oBACnF,oFAAoF;oBACpF,gBAAgB;oBAChB,gFAAgF;oBAChF,WAAW;aACd;YACD,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC;gBACzB,WAAW,EACT,mFAAmF;oBACnF,iFAAiF;oBACjF,uFAAuF;oBACvF,iFAAiF;oBACjF,uEAAuE;oBACvE,uFAAuF;oBACvF,yFAAyF;oBACzF,wEAAwE;oBACxE,oFAAoF;oBACpF,mFAAmF;oBACnF,sFAAsF;oBACtF,oFAAoF;oBACpF,oFAAoF;oBACpF,iCAAiC;oBACjC,qEAAqE;aACxE;YACD,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,CAAC,OAAO,CAAC;gBACf,WAAW,EACT,qFAAqF;oBACrF,mFAAmF;oBACnF,gFAAgF;oBAChF,sFAAsF;oBACtF,sFAAsF;oBACtF,eAAe;oBACf,iFAAiF;oBACjF,sFAAsF;oBACtF,iFAAiF;oBACjF,YAAY;oBACZ,uFAAuF;oBACvF,uFAAuF;oBACvF,WAAW;aACd;SACF;QACD,6EAA6E;QAC7E,2EAA2E;QAC3E,sEAAsE;QACtE,yEAAyE;QACzE,wEAAwE;QACxE,2EAA2E;QAC3E,2EAA2E;QAC3E,gCAAgC;QAChC,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,CAAC,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAClD,2EAA2E;QAC3E,mEAAmE;QACnE,uEAAuE;QACvE,MAAM,GAAG,GAAG,2BAA2B,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAExD,mEAAmE;QACnE,4EAA4E;QAC5E,0EAA0E;QAC1E,4EAA4E;QAC5E,wEAAwE;QACxE,2BAA2B;QAC3B,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,GAAG,EACH,sBAAsB,EACtB,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EACjB,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,gBAAgB,CAAC"}
@@ -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"}
@@ -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), the same credential `list_repositories` and `add_repository` read and a " +
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,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