@gajae-code/coding-agent 0.15.4 → 0.15.6

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.
Files changed (174) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  4. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  5. package/dist/types/cli/update-cli.d.ts +27 -5
  6. package/dist/types/commands/auth-gateway.d.ts +3 -0
  7. package/dist/types/commands/gc.d.ts +13 -0
  8. package/dist/types/commands/model-presets.d.ts +21 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +99 -0
  10. package/dist/types/config/file-lock.d.ts +49 -2
  11. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  12. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  13. package/dist/types/config/keybindings.d.ts +3 -2
  14. package/dist/types/config/model-preset-registry.d.ts +368 -0
  15. package/dist/types/config/model-profile-activation.d.ts +39 -0
  16. package/dist/types/config/model-profiles.d.ts +5 -3
  17. package/dist/types/config/model-registry.d.ts +11 -5
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  20. package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  22. package/dist/types/extensibility/shared-events.d.ts +2 -0
  23. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  25. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  26. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/main.d.ts +3 -1
  29. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  30. package/dist/types/modes/jobs-observer.d.ts +36 -0
  31. package/dist/types/sdk/broker/broker.d.ts +3 -0
  32. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  33. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  34. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  35. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  36. package/dist/types/sdk/client/discovery.d.ts +1 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  39. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  40. package/dist/types/sdk/host/session-runtime.d.ts +19 -1
  41. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  42. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  43. package/dist/types/sdk/router/session-router.d.ts +9 -0
  44. package/dist/types/session/account-inventory.d.ts +1 -0
  45. package/dist/types/session/agent-session.d.ts +19 -5
  46. package/dist/types/session/fold-coordinator.d.ts +201 -0
  47. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  48. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  49. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  50. package/dist/types/telemetry/control.d.ts +11 -0
  51. package/dist/types/telemetry/events.d.ts +20 -0
  52. package/dist/types/telemetry/index.d.ts +3 -0
  53. package/dist/types/telemetry/transport.d.ts +22 -0
  54. package/dist/types/tools/bash-interactive.d.ts +23 -0
  55. package/dist/types/tools/index.d.ts +6 -5
  56. package/dist/types/tools/path-utils.d.ts +5 -6
  57. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  58. package/dist/types/web/search/providers/base.d.ts +9 -4
  59. package/dist/types/web/search/providers/xai.d.ts +2 -1
  60. package/dist/types/web/search/types.d.ts +14 -0
  61. package/package.json +10 -7
  62. package/scripts/generate-hotkeys-docs.ts +4 -0
  63. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  64. package/src/async/job-manager.ts +671 -39
  65. package/src/cli/auth-broker-cli.ts +40 -12
  66. package/src/cli/auth-gateway-cli.ts +273 -97
  67. package/src/cli/config-cli.ts +2 -2
  68. package/src/cli/fast-help.ts +3 -0
  69. package/src/cli/model-presets-cli.ts +100 -0
  70. package/src/cli/update-cli.ts +142 -6
  71. package/src/cli.ts +6 -2
  72. package/src/commands/auth-gateway.ts +12 -8
  73. package/src/commands/gc.ts +19 -0
  74. package/src/commands/model-presets.ts +24 -0
  75. package/src/commands/sdk.ts +5 -0
  76. package/src/config/autorouting-tier-map.ts +33 -0
  77. package/src/config/file-lock-gc.ts +9 -4
  78. package/src/config/file-lock.ts +775 -59
  79. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  80. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  81. package/src/config/keybindings.ts +6 -2
  82. package/src/config/model-preset-registry.ts +2799 -0
  83. package/src/config/model-profile-activation.ts +115 -28
  84. package/src/config/model-profile-contract.ts +4 -1
  85. package/src/config/model-profiles.ts +56 -14
  86. package/src/config/model-registry.ts +1028 -123
  87. package/src/config/settings-schema.ts +11 -0
  88. package/src/config/settings.ts +6 -1
  89. package/src/coordinator-mcp/model-preset.ts +32 -17
  90. package/src/coordinator-mcp/policy.ts +1 -1
  91. package/src/coordinator-mcp/projection-scan.ts +571 -0
  92. package/src/coordinator-mcp/server.ts +19 -24
  93. package/src/coordinator-mcp/session-reaper.ts +9 -3
  94. package/src/extensibility/extensions/loader.ts +0 -1
  95. package/src/extensibility/extensions/types.ts +4 -2
  96. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  97. package/src/extensibility/shared-events.ts +2 -0
  98. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  99. package/src/gjc-runtime/gc-render.ts +29 -0
  100. package/src/gjc-runtime/gc-runtime.ts +174 -11
  101. package/src/gjc-runtime/launch-worktree.ts +44 -7
  102. package/src/gjc-runtime/ralplan-runtime.ts +14 -1
  103. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  104. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  105. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  106. package/src/index.ts +1 -0
  107. package/src/internal-urls/docs-index.generated.ts +14 -13
  108. package/src/lsp/config.ts +4 -3
  109. package/src/lsp/path-trust.ts +12 -6
  110. package/src/main.ts +9 -2
  111. package/src/modes/acp/acp-agent.ts +11 -5
  112. package/src/modes/components/jobs-overlay-model.ts +99 -16
  113. package/src/modes/components/model-selector.ts +158 -18
  114. package/src/modes/components/status-line/segments.ts +6 -1
  115. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  116. package/src/modes/controllers/input-controller.ts +23 -6
  117. package/src/modes/interactive-mode.ts +14 -1
  118. package/src/modes/jobs-observer.ts +149 -16
  119. package/src/modes/prompt-action-autocomplete.ts +26 -47
  120. package/src/modes/runtime-init.ts +1 -1
  121. package/src/modes/utils/injected-user-submission.ts +15 -1
  122. package/src/prompts/tools/bash.md +2 -2
  123. package/src/prompts/tools/fold-receipt.md +3 -0
  124. package/src/prompts/tools/job.md +1 -1
  125. package/src/sdk/acp/adapter.ts +17 -2
  126. package/src/sdk/broker/broker.ts +48 -21
  127. package/src/sdk/broker/lifecycle.ts +32 -6
  128. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  129. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  130. package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
  131. package/src/sdk/bus/prompt-reconciliation.ts +33 -6
  132. package/src/sdk/bus/slack-conversation.ts +5 -1
  133. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  134. package/src/sdk/cli/session-cli.ts +161 -5
  135. package/src/sdk/client/discovery.ts +3 -1
  136. package/src/sdk/host/control/dispatch.ts +3 -0
  137. package/src/sdk/host/host.ts +7 -0
  138. package/src/sdk/host/model-pin.ts +185 -25
  139. package/src/sdk/host/sdk-run-capability.ts +1 -0
  140. package/src/sdk/host/session-runtime.test.ts +1240 -107
  141. package/src/sdk/host/session-runtime.ts +1029 -115
  142. package/src/sdk/prompt-deadline-manager.ts +16 -1
  143. package/src/sdk/protocol/adapter-validation.ts +27 -0
  144. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  145. package/src/sdk/protocol/operation-registry.ts +4 -1
  146. package/src/sdk/router/session-router.ts +297 -111
  147. package/src/sdk/session.ts +83 -12
  148. package/src/session/account-inventory.ts +27 -11
  149. package/src/session/agent-session.ts +887 -95
  150. package/src/session/fold-coordinator.ts +433 -0
  151. package/src/session/internal/managed-session-storage.ts +64 -26
  152. package/src/session/sdk-run-capability-internal.ts +15 -0
  153. package/src/session/sdk-run-capability.ts +2 -0
  154. package/src/session/session-manager.ts +29 -10
  155. package/src/setup/provider-presets.json +1 -1
  156. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  157. package/src/task/executor.ts +19 -3
  158. package/src/telemetry/control.ts +26 -0
  159. package/src/telemetry/events.ts +142 -0
  160. package/src/telemetry/index.ts +3 -0
  161. package/src/telemetry/transport.ts +87 -0
  162. package/src/tools/bash-interactive.ts +182 -85
  163. package/src/tools/bash.ts +786 -67
  164. package/src/tools/index.ts +7 -5
  165. package/src/tools/path-utils.ts +77 -29
  166. package/src/tools/tmux-self-injection-guard.ts +359 -0
  167. package/src/tools/tool-catalog.generated.ts +1 -1
  168. package/src/web/search/provider.ts +120 -11
  169. package/src/web/search/providers/anthropic.ts +89 -17
  170. package/src/web/search/providers/base.ts +9 -4
  171. package/src/web/search/providers/gemini.ts +87 -15
  172. package/src/web/search/providers/openai-compatible.ts +23 -8
  173. package/src/web/search/providers/xai.ts +98 -12
  174. package/src/web/search/types.ts +16 -0
@@ -17,6 +17,7 @@ import * as crypto from "node:crypto";
17
17
  import * as fs from "node:fs/promises";
18
18
  import * as os from "node:os";
19
19
  import * as path from "node:path";
20
+ import * as readline from "node:readline";
20
21
  import { cleanReason } from "@gajae-code/ai/auth-broker/redact";
21
22
  import {
22
23
  AuthBrokerClient,
@@ -226,19 +227,46 @@ async function runLogin(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
226
227
  await runLocalLogin(providerArg as OAuthProvider);
227
228
  }
228
229
 
230
+ function promptForLogin(rl: readline.Interface, question: string): Promise<string> {
231
+ const { promise, resolve } = Promise.withResolvers<string>();
232
+ rl.question(question, answer => resolve(answer));
233
+ return promise;
234
+ }
235
+
229
236
  async function runLocalLogin(provider: OAuthProvider): Promise<void> {
230
- // Spawn the pi-ai CLI in-process — it handles the per-provider OAuth dance
231
- // and persists into the same SQLite store the broker uses.
232
- const piAiCli = Bun.fileURLToPath(import.meta.resolve("@gajae-code/ai/cli"));
233
- const proc = Bun.spawn({
234
- cmd: [process.execPath, piAiCli, "login", provider],
235
- stdin: "inherit",
236
- stdout: "inherit",
237
- stderr: "inherit",
238
- });
239
- const exitCode = await proc.exited;
240
- if (exitCode !== 0) {
241
- throw new Error(`pi-ai login exited with code ${exitCode}`);
237
+ // Drive AuthStorage.login() in-process against the local SQLite store the
238
+ // broker uses. Previously this spawned a child process resolved via
239
+ // `import.meta.resolve("@gajae-code/ai/cli")`, which requires an on-disk
240
+ // `node_modules` package resolution — present in a source/dev checkout but
241
+ // absent inside a compiled `bun build --compile` binary's `$bunfs`, so the
242
+ // standalone binary failed with a module-resolution error (issue #5064).
243
+ // `AuthStorage` is already a normal, statically-traceable import from
244
+ // `@gajae-code/ai/core` (see the top of this file), so it bundles cleanly
245
+ // into the compiled binary the same way every other `@gajae-code/ai/core`
246
+ // export used elsewhere in this file already does.
247
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
248
+ const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
249
+ const storage = new AuthStorage(store);
250
+ await storage.reload();
251
+ try {
252
+ await storage.login(provider, {
253
+ onAuth(info) {
254
+ const { url, instructions } = info;
255
+ process.stdout.write(`\nOpen this URL in your browser:\n${url}\n`);
256
+ if (instructions) process.stdout.write(`${instructions}\n`);
257
+ process.stdout.write("\n");
258
+ },
259
+ onProgress(message) {
260
+ process.stdout.write(`${message}\n`);
261
+ },
262
+ onPrompt(p) {
263
+ return promptForLogin(rl, `${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}: `);
264
+ },
265
+ });
266
+ process.stdout.write(`\nCredentials saved to ${getAgentDbPath()}\n`);
267
+ } finally {
268
+ store.close();
269
+ rl.close();
242
270
  }
243
271
  }
244
272
 
@@ -8,23 +8,28 @@
8
8
  * `GJC_AUTH_BROKER_URL` / `auth.broker.url` precedence used elsewhere).
9
9
  *
10
10
  * Sub-verbs:
11
- * - `serve [--bind=…]` — boots the gateway against the configured broker.
11
+ * - `serve --provider=<id> [--bind=…]` — boots a provider-scoped gateway against the configured broker.
12
12
  * - `token` / `token --regenerate` — manages the gateway bearer token file.
13
- * - `status` — prints the locally-stored gateway token and bind hint.
13
+ * - `status` — prints scoped gateway readiness without token material.
14
14
  */
15
15
  import * as crypto from "node:crypto";
16
16
  import * as path from "node:path";
17
17
  import { cleanReason } from "@gajae-code/ai/auth-broker/redact";
18
- import { startAuthGateway } from "@gajae-code/ai/auth-gateway/server";
19
18
  import {
20
- type Api,
19
+ createAuthGatewayModelCatalog,
20
+ isAuthGatewayModelBrokerConsumable,
21
+ isSafeProviderScope,
22
+ startAuthGateway,
23
+ } from "@gajae-code/ai/auth-gateway/server";
24
+ import {
21
25
  AuthBrokerClient,
26
+ type AuthCredentialSnapshot,
22
27
  AuthStorage,
28
+ type CredentialHealthResult,
23
29
  DEFAULT_AUTH_GATEWAY_BIND,
30
+ extractStructuredApiKeyToken,
24
31
  type GeneratedProvider,
25
32
  getBundledModels,
26
- getBundledProviders,
27
- type Model,
28
33
  RemoteAuthCredentialStore,
29
34
  type SnapshotResponse,
30
35
  } from "@gajae-code/ai/core";
@@ -44,6 +49,7 @@ export interface AuthGatewayCommandArgs {
44
49
  flags: {
45
50
  json?: boolean;
46
51
  bind?: string;
52
+ provider?: string;
47
53
  regenerate?: boolean;
48
54
  /**
49
55
  * Disable bearer-token auth on inbound requests. Useful when the gateway
@@ -77,12 +83,34 @@ function safeDiagnostic(value: unknown, fallback: string): string {
77
83
  return cleanReason(value) ?? fallback;
78
84
  }
79
85
 
86
+ export function resolveAuthGatewayReadiness(input: {
87
+ noAuth: boolean;
88
+ tokenPresent: boolean;
89
+ credentialCount: number;
90
+ modelCount: number;
91
+ }): { ready: boolean; reason: "token_missing" | "provider_credential_missing" | "provider_catalog_empty" | null } {
92
+ const authReady = input.noAuth || input.tokenPresent;
93
+ const ready = authReady && input.credentialCount > 0 && input.modelCount > 0;
94
+ const reason = !authReady
95
+ ? "token_missing"
96
+ : input.credentialCount === 0
97
+ ? "provider_credential_missing"
98
+ : input.modelCount === 0
99
+ ? "provider_catalog_empty"
100
+ : null;
101
+ return { ready, reason };
102
+ }
103
+
80
104
  function writeCommandFailure(action: AuthGatewayAction, flags: AuthGatewayCommandArgs["flags"], error: unknown): void {
81
105
  const stable = stableErrorForAction(action);
106
+ const scope = normalizeProviderScope(flags.provider);
107
+ const scopeLabel = flags.provider === undefined ? "(unscoped)" : (scope ?? "(invalid)");
82
108
  if (flags.json) {
83
- process.stdout.write(`${JSON.stringify({ ok: false, error: stable })}\n`);
109
+ process.stdout.write(`${JSON.stringify({ ok: false, error: stable, scope })}\n`);
84
110
  } else {
85
- process.stderr.write(`${chalk.red("FAILED")} ${safeDiagnostic(error, stable.message)}\n`);
111
+ process.stderr.write(`scope: ${scopeLabel}\n`);
112
+ const message = action === "check" ? stable.message : safeDiagnostic(error, stable.message);
113
+ process.stderr.write(`${chalk.red("FAILED")} ${message}\n`);
86
114
  }
87
115
  process.exitCode = 1;
88
116
  }
@@ -130,13 +158,85 @@ function createBrokerClient(brokerConfig: AuthBrokerClientConfig): AuthBrokerCli
130
158
  return new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
131
159
  }
132
160
 
161
+ export function normalizeProviderScope(provider: string | undefined): string | undefined {
162
+ return provider && isSafeProviderScope(provider) ? provider : undefined;
163
+ }
164
+
165
+ export function filterCredentialCheckResults(
166
+ results: readonly CredentialHealthResult[],
167
+ provider: string | undefined,
168
+ ): CredentialHealthResult[] {
169
+ return provider ? results.filter(row => row.provider === provider) : [...results];
170
+ }
171
+
172
+ function sanitizeCredentialCheckResult(row: CredentialHealthResult): Record<string, unknown> {
173
+ return {
174
+ id: row.id,
175
+ provider: row.provider,
176
+ type: row.type,
177
+ ok: row.ok,
178
+ ...(row.remoteRefresh ? { remoteRefresh: true } : {}),
179
+ ...(row.reason
180
+ ? { reason: row.ok === false ? "Credential check failed." : "Credential status unavailable." }
181
+ : {}),
182
+ };
183
+ }
184
+
185
+ function requireProviderScope(provider: string | undefined, action: "serve"): string {
186
+ const normalized = normalizeProviderScope(provider);
187
+ if (!normalized) {
188
+ throw new Error(
189
+ `gjc auth-gateway ${action} requires --provider=<id>; an unscoped gateway is disabled to prevent model-id ambiguity.`,
190
+ );
191
+ }
192
+ return normalized;
193
+ }
194
+
195
+ export function redactBrokerUrl(rawUrl: string): string {
196
+ try {
197
+ const url = new URL(rawUrl);
198
+ url.username = "";
199
+ url.password = "";
200
+ url.search = "";
201
+ url.hash = "";
202
+ return url.origin === "null" ? "<configured broker>" : url.origin;
203
+ } catch {
204
+ return "<configured broker>";
205
+ }
206
+ }
207
+
133
208
  async function fetchBrokerSnapshot(client: AuthBrokerClient): Promise<SnapshotResponse> {
134
209
  const result = await client.fetchSnapshot();
135
210
  if (result.status !== 200) throw new Error("Auth broker returned no initial snapshot");
136
211
  return result.snapshot;
137
212
  }
138
213
 
214
+ export function hasEnabledProviderCredential(
215
+ snapshot: Pick<AuthCredentialSnapshot, "credentials">,
216
+ provider: string,
217
+ ): boolean {
218
+ return snapshot.credentials.some(entry => entry.provider === provider);
219
+ }
220
+
221
+ export function matchesProviderCredential(
222
+ entry: AuthCredentialSnapshot["credentials"][number],
223
+ apiKey: string,
224
+ ): boolean {
225
+ if (entry.credential.type === "api_key") return entry.credential.key === apiKey;
226
+ return entry.credential.access === apiKey || extractStructuredApiKeyToken(apiKey) === entry.credential.access;
227
+ }
228
+
229
+ export function assertEnabledProviderCredential(
230
+ snapshot: Pick<AuthCredentialSnapshot, "credentials">,
231
+ provider: string,
232
+ ): void {
233
+ if (!hasEnabledProviderCredential(snapshot, provider)) {
234
+ throw new Error(`Auth gateway scope ${provider} has no enabled broker credential`);
235
+ }
236
+ }
237
+
139
238
  async function runServe(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
239
+ const provider = requireProviderScope(flags.provider, "serve");
140
240
  const brokerConfig = (await resolveStartupAuthConfig()).broker;
141
241
  if (!brokerConfig) {
142
242
  throw new Error(
@@ -144,89 +244,95 @@ async function runServe(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
144
244
  );
145
245
  }
146
246
  const bind = flags.bind ?? DEFAULT_AUTH_GATEWAY_BIND;
147
- const gatewayToken = flags.noAuth ? null : await ensureToken();
247
+ const models = getBundledModels(provider as GeneratedProvider);
248
+ if (!models.some(isAuthGatewayModelBrokerConsumable)) {
249
+ throw new Error(`Auth gateway scope ${provider} has no broker-consumable models`);
250
+ }
148
251
 
149
252
  // Build a broker-backed AuthStorage — same pattern as discoverAuthStorage()
150
253
  // in sdk/session.ts. The gateway never touches local SQLite.
151
254
  const client = createBrokerClient(brokerConfig);
152
255
  const initialSnapshot = await fetchBrokerSnapshot(client);
256
+ assertEnabledProviderCredential(initialSnapshot, provider);
153
257
  const store = new RemoteAuthCredentialStore({ client, initialSnapshot });
154
258
  // Refresh + usage both flow through the store's broker hooks automatically —
155
259
  // `RemoteAuthCredentialStore.refreshOAuthCredential` and `.fetchUsageReports`.
156
260
  // AuthStorage discovers them when no explicit option overrides them, so the
157
261
  // gateway only needs to construct the store and pass it in.
158
262
  const storage = new AuthStorage(store, {
159
- sourceLabel: `broker ${brokerConfig.url}`,
263
+ sourceLabel: `broker ${redactBrokerUrl(brokerConfig.url)}`,
160
264
  });
161
- await storage.reload();
162
-
163
- // Build the model resolver + catalog from pi-ai's bundled metadata, scoped
164
- // to providers we hold credentials for. Format handlers ask `resolveModel`
165
- // to translate a client-requested `model` field into a pi-ai `Model<Api>`
166
- // before dispatch; `listModels` powers `/v1/models`.
167
- const snapshot = storage.exportSnapshot();
168
- const providersWithCreds = new Set<string>();
169
- for (const entry of snapshot.credentials) providersWithCreds.add(entry.provider);
170
- const modelById = new Map<string, Model<Api>>();
171
- for (const provider of getBundledProviders()) {
172
- if (!providersWithCreds.has(provider)) continue;
173
- for (const model of getBundledModels(provider as GeneratedProvider)) {
174
- // First-write-wins so a canonical model id collisions across providers
175
- // stick to the provider listed first by getBundledProviders.
176
- if (!modelById.has(model.id)) modelById.set(model.id, model);
177
- }
178
- }
265
+ try {
266
+ await storage.reload();
267
+ assertEnabledProviderCredential(storage.exportSnapshot(), provider);
179
268
 
180
- const handle = startAuthGateway({
181
- storage,
182
- bind,
183
- bearerTokens: gatewayToken ? [gatewayToken] : [],
184
- version: VERSION,
185
- resolveModel: (id: string) => modelById.get(id),
186
- listModels: () => modelById.values(),
187
- });
188
- process.stdout.write(`auth-gateway listening on ${handle.url}\n`);
189
- if (gatewayToken) {
190
- process.stdout.write(`bearer token: ${getTokenFilePath()} (chmod 0600)\n`);
191
- } else {
192
- process.stdout.write(`auth: disabled (--no-auth) — any client can call this gateway\n`);
193
- }
194
- process.stdout.write(`upstream broker: ${brokerConfig.url}\n`);
195
-
196
- const stopped = Promise.withResolvers<void>();
197
- let shutdownStarted = false;
198
- const stop = async (signal: NodeJS.Signals): Promise<void> => {
199
- if (shutdownStarted) return;
200
- shutdownStarted = true;
201
- process.stdout.write(`\nReceived ${signal}, shutting down...\n`);
202
- let closeError: unknown;
203
- try {
204
- await handle.close();
205
- } catch (error) {
206
- closeError = error;
207
- } finally {
208
- storage.close();
269
+ const catalog = createAuthGatewayModelCatalog(provider, models);
270
+ if (catalog.models.length === 0) {
271
+ throw new Error(`Auth gateway scope ${provider} has no source-backed models`);
209
272
  }
210
- if (closeError) {
211
- stopped.reject(closeError);
273
+ const modelById = new Map(catalog.models.map(model => [model.id, model] as const));
274
+ const gatewayToken = flags.noAuth ? null : await ensureToken();
275
+ const handle = startAuthGateway({
276
+ storage,
277
+ hasProviderCredential: () => store.snapshot.credentials.some(entry => entry.provider === provider),
278
+ reloadProviderCredentials: async signal => {
279
+ await store.refreshSnapshot(signal);
280
+ },
281
+ validateProviderCredential: (candidateProvider, apiKey) =>
282
+ store.snapshot.credentials.some(
283
+ entry => entry.provider === candidateProvider && matchesProviderCredential(entry, apiKey),
284
+ ),
285
+ bind,
286
+ providerScope: { provider },
287
+ bearerTokens: gatewayToken ? [gatewayToken] : [],
288
+ version: VERSION,
289
+ resolveModel: (id: string) => modelById.get(id),
290
+ listModels: () => catalog.models,
291
+ });
292
+ process.stdout.write(`auth-gateway listening on ${handle.url}\n`);
293
+ process.stdout.write(`scope: ${provider}\n`);
294
+ if (gatewayToken) {
295
+ process.stdout.write(`bearer token: ${getTokenFilePath()} (chmod 0600)\n`);
212
296
  } else {
213
- stopped.resolve();
297
+ process.stdout.write(`auth: disabled (--no-auth) — non-browser local clients can call this gateway\n`);
214
298
  }
215
- };
216
- const onSigint = (): void => {
217
- void stop("SIGINT");
218
- };
219
- const onSigterm = (): void => {
220
- void stop("SIGTERM");
221
- };
222
- process.once("SIGINT", onSigint);
223
- process.once("SIGTERM", onSigterm);
299
+ process.stdout.write(`upstream broker: ${redactBrokerUrl(brokerConfig.url)}\n`);
224
300
 
225
- try {
226
- await stopped.promise;
301
+ const stopped = Promise.withResolvers<void>();
302
+ let shutdownStarted = false;
303
+ const stop = async (signal: NodeJS.Signals): Promise<void> => {
304
+ if (shutdownStarted) return;
305
+ shutdownStarted = true;
306
+ process.stdout.write(`\nReceived ${signal}, shutting down...\n`);
307
+ let closeError: unknown;
308
+ try {
309
+ await handle.close();
310
+ } catch (error) {
311
+ closeError = error;
312
+ }
313
+ if (closeError) {
314
+ stopped.reject(closeError);
315
+ } else {
316
+ stopped.resolve();
317
+ }
318
+ };
319
+ const onSigint = (): void => {
320
+ void stop("SIGINT");
321
+ };
322
+ const onSigterm = (): void => {
323
+ void stop("SIGTERM");
324
+ };
325
+ process.once("SIGINT", onSigint);
326
+ process.once("SIGTERM", onSigterm);
327
+
328
+ try {
329
+ await stopped.promise;
330
+ } finally {
331
+ process.off("SIGINT", onSigint);
332
+ process.off("SIGTERM", onSigterm);
333
+ }
227
334
  } finally {
228
- process.off("SIGINT", onSigint);
229
- process.off("SIGTERM", onSigterm);
335
+ storage.close();
230
336
  }
231
337
  }
232
338
 
@@ -253,20 +359,24 @@ async function runStatus(flags: AuthGatewayCommandArgs["flags"]): Promise<void>
253
359
  const token = await readToken();
254
360
  const brokerConfig = (await resolveStartupAuthConfig()).broker;
255
361
  const tokenFile = getTokenFilePath();
362
+ const provider = normalizeProviderScope(flags.provider);
256
363
  if (!brokerConfig) {
257
364
  const status = {
258
365
  ready: false,
259
366
  reason: "broker_not_configured",
260
367
  error: { code: "broker_not_configured", message: "Auth broker is not configured." },
368
+ scope: provider,
261
369
  tokenFile,
262
370
  tokenPresent: token !== null,
263
371
  broker: null,
264
372
  brokerConfigured: false,
265
373
  brokerAuthenticated: false,
374
+ credentialCount: null,
266
375
  };
267
376
  if (flags.json) {
268
377
  process.stdout.write(`${JSON.stringify(status)}\n`);
269
378
  } else {
379
+ process.stdout.write(`scope: ${provider ?? "(required: --provider=<id>)"}\n`);
270
380
  process.stdout.write(`${chalk.yellow("No broker configured.")} Set GJC_AUTH_BROKER_URL.\n`);
271
381
  process.stdout.write(
272
382
  `token: ${status.tokenPresent ? chalk.green("present") : chalk.red("missing")} at ${status.tokenFile}\n`,
@@ -275,54 +385,98 @@ async function runStatus(flags: AuthGatewayCommandArgs["flags"]): Promise<void>
275
385
  process.exitCode = 1;
276
386
  return;
277
387
  }
388
+ if (!provider) {
389
+ const status = {
390
+ ready: false,
391
+ reason: "provider_scope_required",
392
+ error: {
393
+ code: "provider_scope_required",
394
+ message: "A provider scope is required to report gateway readiness.",
395
+ },
396
+ scope: null,
397
+ tokenFile,
398
+ tokenPresent: token !== null,
399
+ broker: redactBrokerUrl(brokerConfig.url),
400
+ brokerConfigured: true,
401
+ brokerAuthenticated: false,
402
+ credentialCount: null,
403
+ };
404
+ if (flags.json) {
405
+ process.stdout.write(`${JSON.stringify(status)}\n`);
406
+ } else {
407
+ process.stdout.write(`scope: (required: --provider=<id>)\n`);
408
+ process.stdout.write(`${chalk.yellow("not ready")} provider scope is required\n`);
409
+ process.stdout.write(
410
+ `token: ${status.tokenPresent ? chalk.green("present") : chalk.red("missing")} at ${status.tokenFile}\n`,
411
+ );
412
+ }
413
+ process.exitCode = 1;
414
+ return;
415
+ }
278
416
 
279
417
  try {
280
418
  const snapshot = await fetchBrokerSnapshot(createBrokerClient(brokerConfig));
281
419
  const tokenPresent = token !== null;
420
+ const noAuth = flags.noAuth === true;
421
+ const credentialCount = snapshot.credentials.filter(entry => entry.provider === provider).length;
422
+ const catalog = createAuthGatewayModelCatalog(provider, getBundledModels(provider as GeneratedProvider));
423
+ const modelCount = catalog.models.length;
424
+ const { ready, reason } = resolveAuthGatewayReadiness({ noAuth, tokenPresent, credentialCount, modelCount });
282
425
  const status = {
283
- ready: tokenPresent,
284
- reason: tokenPresent ? null : "token_missing",
426
+ ready,
427
+ reason,
285
428
  tokenFile,
286
429
  tokenPresent,
287
- broker: brokerConfig.url,
430
+ broker: redactBrokerUrl(brokerConfig.url),
288
431
  brokerConfigured: true,
289
432
  brokerAuthenticated: true,
290
- credentialCount: snapshot.credentials.length,
433
+ scope: provider,
434
+ credentialCount,
435
+ modelCount,
291
436
  };
292
437
  if (flags.json) {
293
438
  process.stdout.write(`${JSON.stringify(status)}\n`);
294
439
  } else {
295
- const brokerLine = `upstream broker: ${brokerConfig.url} (${snapshot.credentials.length} credential${
296
- snapshot.credentials.length === 1 ? "" : "s"
440
+ const brokerLine = `upstream broker: ${redactBrokerUrl(brokerConfig.url)} (${credentialCount} scoped credential${
441
+ credentialCount === 1 ? "" : "s"
297
442
  })`;
298
- process.stdout.write(`${tokenPresent ? chalk.green("ready") : chalk.yellow("not ready")} ${brokerLine}\n`);
443
+ process.stdout.write(`scope: ${provider}\n`);
444
+ process.stdout.write(`${ready ? chalk.green("ready") : chalk.yellow("not ready")} ${brokerLine}\n`);
299
445
  process.stdout.write(
300
446
  `token: ${tokenPresent ? chalk.green("present") : chalk.red("missing")} at ${status.tokenFile}\n`,
301
447
  );
302
- if (!tokenPresent) {
448
+ if (!tokenPresent && !noAuth) {
303
449
  process.stdout.write(
304
450
  "Run `gjc auth-gateway token` or `gjc auth-gateway serve` to create a bearer token.\n",
305
451
  );
306
452
  }
453
+ if (credentialCount === 0) {
454
+ process.stdout.write(`No enabled broker credential is available for scope ${provider}.\n`);
455
+ }
456
+ if (modelCount === 0) {
457
+ process.stdout.write(`No source-backed models are available for scope ${provider}.\n`);
458
+ }
307
459
  }
308
- if (!tokenPresent) process.exitCode = 1;
460
+ if (!ready) process.exitCode = 1;
309
461
  } catch (error) {
310
462
  const status = {
311
463
  ready: false,
312
464
  reason: "broker_unavailable",
313
465
  tokenFile,
314
466
  tokenPresent: token !== null,
315
- broker: brokerConfig.url,
467
+ broker: redactBrokerUrl(brokerConfig.url),
316
468
  brokerConfigured: true,
317
469
  brokerAuthenticated: false,
470
+ scope: provider,
318
471
  error: { code: "broker_unavailable", message: "Auth broker is unavailable." },
319
472
  };
320
473
  if (flags.json) {
321
474
  process.stdout.write(`${JSON.stringify(status)}\n`);
322
475
  } else {
323
476
  process.stdout.write(
324
- `${chalk.red("FAILED")} upstream broker: ${brokerConfig.url}: ${safeDiagnostic(error, "Auth broker is unavailable.")}\n`,
477
+ `${chalk.red("FAILED")} upstream broker: ${redactBrokerUrl(brokerConfig.url)}: ${safeDiagnostic(error, "Auth broker is unavailable.")}\n`,
325
478
  );
479
+ process.stdout.write(`scope: ${provider}\n`);
326
480
  process.stdout.write(
327
481
  `token: ${status.tokenPresent ? chalk.green("present") : chalk.red("missing")} at ${status.tokenFile}\n`,
328
482
  );
@@ -333,6 +487,9 @@ async function runStatus(flags: AuthGatewayCommandArgs["flags"]): Promise<void>
333
487
 
334
488
  export async function runAuthGatewayCommand(cmd: AuthGatewayCommandArgs): Promise<void> {
335
489
  try {
490
+ if (cmd.flags.provider !== undefined && !normalizeProviderScope(cmd.flags.provider)) {
491
+ throw new Error("Invalid provider scope.");
492
+ }
336
493
  switch (cmd.action) {
337
494
  case "serve":
338
495
  await runServe(cmd.flags);
@@ -368,6 +525,7 @@ export async function runAuthGatewayCommand(cmd: AuthGatewayCommandArgs): Promis
368
525
  * dedicated diagnostic is the only way to see which credentials failed.
369
526
  */
370
527
  async function runCheck(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
528
+ const provider = normalizeProviderScope(flags.provider);
371
529
  const brokerConfig = (await resolveStartupAuthConfig()).broker;
372
530
  if (!brokerConfig) {
373
531
  throw new Error(
@@ -378,17 +536,32 @@ async function runCheck(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
378
536
  const client = createBrokerClient(brokerConfig);
379
537
  const initialSnapshot = await fetchBrokerSnapshot(client);
380
538
  const store = new RemoteAuthCredentialStore({ client, initialSnapshot });
381
- const storage = new AuthStorage(store, { sourceLabel: `broker ${brokerConfig.url}` });
539
+ const storage = new AuthStorage(store, { sourceLabel: `broker ${redactBrokerUrl(brokerConfig.url)}` });
382
540
  try {
383
541
  await storage.reload();
384
- const results = await storage.checkCredentials();
542
+ const results = filterCredentialCheckResults(
543
+ await storage.checkCredentials(provider ? { provider } : undefined),
544
+ provider,
545
+ );
546
+ const scopedCredentialCount = provider
547
+ ? store.snapshot.credentials.filter(entry => entry.provider === provider).length
548
+ : store.snapshot.credentials.length;
549
+ const failed = results.filter(row => row.ok === false).length;
385
550
 
386
551
  if (flags.json) {
387
- const credentials = results.map(row => ({
388
- ...row,
389
- ...(row.reason ? { reason: safeDiagnostic(row.reason, "Credential check failed.") } : {}),
390
- }));
391
- process.stdout.write(`${JSON.stringify({ broker: brokerConfig.url, credentials }, null, 2)}\n`);
552
+ const credentials = results.map(sanitizeCredentialCheckResult);
553
+ process.stdout.write(
554
+ `${JSON.stringify(
555
+ {
556
+ broker: redactBrokerUrl(brokerConfig.url),
557
+ scope: provider,
558
+ credentialCount: scopedCredentialCount,
559
+ credentials,
560
+ },
561
+ null,
562
+ 2,
563
+ )}\n`,
564
+ );
392
565
  } else {
393
566
  const grouped = new Map<string, typeof results>();
394
567
  for (const row of results) {
@@ -397,7 +570,11 @@ async function runCheck(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
397
570
  grouped.set(row.provider, list);
398
571
  }
399
572
  const providers = [...grouped.keys()].sort();
400
- process.stdout.write(`broker: ${brokerConfig.url}\n`);
573
+ process.stdout.write(`broker: ${redactBrokerUrl(brokerConfig.url)}\n`);
574
+ process.stdout.write(`scope: ${provider ?? "(all providers; diagnostic only)"}\n`);
575
+ if (provider && scopedCredentialCount === 0) {
576
+ process.stdout.write(`No enabled broker credential is available for scope ${provider}.\n`);
577
+ }
401
578
  for (const provider of providers) {
402
579
  const rows = grouped.get(provider) ?? [];
403
580
  process.stdout.write(`\n${chalk.bold(provider)} (${rows.length})\n`);
@@ -408,25 +585,24 @@ async function runCheck(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
408
585
  : row.ok === false
409
586
  ? chalk.red("FAIL ")
410
587
  : chalk.yellow("unknown ");
411
- const identity =
412
- row.email ?? row.accountId ?? (row.type === "api_key" ? "(api key)" : "(no identity on credential)");
588
+ const identity = row.type === "api_key" ? "(api key)" : "(identity hidden)";
413
589
  const remote = row.remoteRefresh ? chalk.dim(" [remote-refresh]") : "";
414
590
  const reason = row.reason
415
- ? chalk.dim(` — ${safeDiagnostic(row.reason, "Credential check failed.")}`)
591
+ ? chalk.dim(` — ${row.ok === false ? "Credential check failed." : "Credential status unavailable."}`)
416
592
  : "";
417
593
  process.stdout.write(
418
594
  ` ${status} id=${row.id.toString().padStart(3)} ${row.type.padEnd(7)} ${identity}${remote}${reason}\n`,
419
595
  );
420
596
  }
421
597
  }
422
- const failed = results.filter(row => row.ok === false).length;
423
598
  const unverifiable = results.filter(row => row.ok === null).length;
424
599
  const passing = results.filter(row => row.ok === true).length;
425
600
  process.stdout.write(
426
601
  `\n${chalk.green(`${passing} ok`)}, ${chalk.red(`${failed} failed`)}, ${chalk.yellow(`${unverifiable} unverifiable`)}, ${results.length} total\n`,
427
602
  );
428
- if (failed > 0) process.exitCode = 1;
429
603
  }
604
+ if (failed > 0) process.exitCode = 1;
605
+ if (provider && scopedCredentialCount === 0) process.exitCode = 1;
430
606
  } finally {
431
607
  storage.close();
432
608
  }
@@ -16,8 +16,8 @@ import {
16
16
  validateAutoroutingProvenance,
17
17
  validateAutoroutingSetup,
18
18
  } from "../config/autorouting-contract";
19
+ import { loadEffectiveModelProfiles } from "../config/model-preset-registry";
19
20
  import { resolveModelProfileName } from "../config/model-profile-contract";
20
- import { mergeModelProfiles } from "../config/model-profiles";
21
21
  import { ModelsConfigFile } from "../config/model-registry";
22
22
  import {
23
23
  getDefault,
@@ -548,7 +548,7 @@ function handlePath(): void {
548
548
  */
549
549
  export function collectConfigAdvisories(): string[] {
550
550
  const profileName = settings.get("modelProfile.default");
551
- const profiles = mergeModelProfiles(ModelsConfigFile.load()?.profiles);
551
+ const profiles = loadEffectiveModelProfiles(ModelsConfigFile.load()?.profiles);
552
552
  const resolvedProfileName = profileName ? resolveModelProfileName(profileName, profiles) : undefined;
553
553
  const delegation = resolveEagerTaskDelegation({
554
554
  settings,
@@ -9,6 +9,7 @@ export function getExtraHelpText(): string {
9
9
  ${APP_NAME} state - Inspect or manage persisted GJC state
10
10
  ${APP_NAME} auth-broker - Manage the auth-broker (credential vault)
11
11
  ${APP_NAME} accounts - List, check, pin, and remove stored OAuth accounts
12
+ ${APP_NAME} models presets - Inspect, refresh, pin, rollback, or disable the signed model preset registry
12
13
  ${APP_NAME} auth-gateway - Run an auth-gateway forward proxy
13
14
  ${APP_NAME} harness - Run harness control-plane commands
14
15
  ${APP_NAME} coordinator - Manage coordinator/runtime coordination helpers
@@ -82,6 +83,8 @@ Environment Variables:
82
83
  GJC_SMOL_MODEL - Override smol/fast model (see --smol)
83
84
  GJC_SLOW_MODEL - Override slow/reasoning model (see --slow)
84
85
  GJC_PLAN_MODEL - Override planning model (see --plan)
86
+ GJC_MODEL_PRESET_REGISTRY_URL - HTTPS latest.json override for the signed preset registry
87
+ GJC_MODEL_PRESET_REGISTRY_DISABLED - Disable registry fetch/use (1/true/yes/on)
85
88
  GJC_NO_PTY - Disable PTY-based interactive bash execution
86
89
  --tmux - Launch interactive startup inside a fresh tmux session
87
90
  gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions