pullfrog 0.1.53 → 0.1.55

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.
@@ -3,7 +3,7 @@
3
3
  * Re-exports shared types, values, and utilities needed by the Next.js app.
4
4
  */
5
5
  export type { AuthorPermission, AutoTier, EffortPosition, ModelAlias, ModelProvider, Payload, PayloadEvent, ProviderConfig, PushPermission, ShellPermission, ToolPermission, WriteablePayload, XrepoConfig, } from "../external.ts";
6
- export { AUTO_EFFICIENT, AUTO_INTELLIGENT, DEFAULT_EFFORT_POSITION, DEFAULT_PROXY_MODEL, defaultAutoTier, EFFORT_ALIASES, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoTier, isCardGatedModel, isEffortPosition, isOssAllowedModel, modelAliases, modelHasStoredAuth, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, offeredRungs, parseEffortPosition, parseModel, providers, pullfrogMcpName, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, resolveRung, rungLabel, rungPosition, } from "../external.ts";
6
+ export { AUTO_EFFICIENT, AUTO_INTELLIGENT, DEFAULT_EFFORT_POSITION, DEFAULT_PROXY_MODEL, defaultAutoTier, EFFORT_ALIASES, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoTier, isCardGatedModel, isEffortPosition, isOssAllowedModel, modelAliases, modelHasStoredAuth, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, offeredRungs, ossFundedModelNames, parseEffortPosition, parseModel, providers, pullfrogMcpName, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, resolveRung, rungLabel, rungPosition, } from "../external.ts";
7
7
  export type { Mode } from "../modes.ts";
8
8
  export { modes } from "../modes.ts";
9
9
  export { commercialPaywallBody } from "../utils/billingErrors.ts";
@@ -11,6 +11,8 @@ export type { BuildPullfrogFooterParams, WorkflowRunFooterInfo, } from "../utils
11
11
  export { buildPullfrogFooter, PULLFROG_DIVIDER, stripExistingFooter, } from "../utils/buildPullfrogFooter.ts";
12
12
  export type { CodexAuthBody } from "../utils/codexOAuth.ts";
13
13
  export { decodeJwtExpMs, OAuthInvalidGrantError, parseCodexAuthBody, refreshCodexAuthBody, stringifyCodexAuthBody, } from "../utils/codexOAuth.ts";
14
+ export type { CredentialVerdict } from "../utils/credentialCheck.ts";
15
+ export { isCredentialProbeable, verifyCredential } from "../utils/credentialCheck.ts";
14
16
  export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
15
17
  export { isPullfrog } from "../utils/isPullfrog.ts";
16
18
  export { isLeapingIntoActionCommentBody, LEAPING_INTO_ACTION_PREFIX, } from "../utils/leapingComment.ts";
package/dist/internal.js CHANGED
@@ -321,7 +321,12 @@ var providers = {
321
321
  displayName: "Big Pickle",
322
322
  resolve: "opencode/big-pickle",
323
323
  preferred: true,
324
- envVars: [],
324
+ // free to RUN, but Zen still refuses it without a key — a keyless run
325
+ // dies at session start with `No provider available`. the `envVars: []`
326
+ // override that used to sit here shadowed the provider's own
327
+ // `OPENCODE_API_KEY` and made every static gate wave it through (#1077).
328
+ // `isFree` stays: it is what skips the proxy mint in run-context, and
329
+ // the model does cost nothing.
325
330
  isFree: true
326
331
  },
327
332
  "claude-opus": {
@@ -443,14 +448,16 @@ var providers = {
443
448
  "mimo-v2-pro-free": {
444
449
  displayName: "MiMo V2 Pro",
445
450
  resolve: "opencode/mimo-v2-pro-free",
446
- envVars: [],
451
+ // free to run, still gated on the provider's own OPENCODE_API_KEY —
452
+ // see the big-pickle note above (#1077).
447
453
  isFree: true,
448
454
  fallback: "opencode/big-pickle"
449
455
  },
450
456
  "minimax-m2.5-free": {
451
457
  displayName: "MiniMax M2",
452
458
  resolve: "opencode/minimax-m2.5-free",
453
- envVars: [],
459
+ // free to run, still gated on the provider's own OPENCODE_API_KEY —
460
+ // see the big-pickle note above (#1077).
454
461
  isFree: true,
455
462
  fallback: "opencode/big-pickle",
456
463
  hidden: true
@@ -646,7 +653,11 @@ var providers = {
646
653
  "gemini-flash": {
647
654
  displayName: "Gemini Flash",
648
655
  resolve: "openrouter/~google/gemini-flash-latest",
649
- effort: ["minimal", "low", "medium", "high"],
656
+ // no `minimal` here, unlike the google/ and opencode/ entries for the
657
+ // same model: OpenRouter's floating `~google/gemini-flash-latest` stopped
658
+ // publishing that rung. mirroring it is not cosmetic — claude-code
659
+ // hard-errors on an out-of-range `--effort` before it calls the API.
660
+ effort: ["low", "medium", "high"],
650
661
  openRouterResolve: "openrouter/~google/gemini-flash-latest"
651
662
  },
652
663
  grok: {
@@ -822,10 +833,12 @@ var OSS_MODEL_ALLOWLIST = [
822
833
  "deepseek/deepseek-flash",
823
834
  "deepseek/deepseek-pro",
824
835
  "openai/gpt-luna",
825
- "openrouter/minimax-m2.5",
826
- "moonshotai/kimi-k2"
836
+ "openrouter/minimax-m2.5"
827
837
  ];
828
838
  var OSS_RECOMMENDED_MODEL = AUTO_TIER_TARGET[AUTO_EFFICIENT];
839
+ function ossFundedModelNames() {
840
+ return OSS_MODEL_ALLOWLIST.map((slug) => resolveDisplayAlias(slug)?.displayName ?? slug);
841
+ }
829
842
  var ossAllowedTargets = new Set(
830
843
  OSS_MODEL_ALLOWLIST.map((slug) => {
831
844
  const target = resolveOpenRouterModel(slug);
@@ -1295,7 +1308,7 @@ function formatModelLabel(params) {
1295
1308
  }
1296
1309
  const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
1297
1310
  if (params.fallbackFrom) {
1298
- return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} not configured)`;
1311
+ return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} were rejected by the provider)`;
1299
1312
  }
1300
1313
  if (params.clamped) {
1301
1314
  const target = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
@@ -1423,6 +1436,7 @@ function parseCodexAuthBody(raw) {
1423
1436
  if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
1424
1437
  return {
1425
1438
  auth_mode: "chatgpt",
1439
+ ...typeof v.refresh_rejected_at === "string" ? { refresh_rejected_at: v.refresh_rejected_at } : {},
1426
1440
  tokens: {
1427
1441
  access_token: t.access_token,
1428
1442
  refresh_token: t.refresh_token,
@@ -1437,6 +1451,109 @@ function stringifyCodexAuthBody(body) {
1437
1451
  `;
1438
1452
  }
1439
1453
 
1454
+ // utils/claudeSubscription.ts
1455
+ var CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
1456
+ var fallbackResolve = resolveModelSlug("anthropic/claude-haiku");
1457
+ if (!fallbackResolve) {
1458
+ throw new Error("claudeSubscription preflight: anthropic/claude-haiku missing from registry");
1459
+ }
1460
+ var FALLBACK_PROBE_MODEL = fallbackResolve.slice(fallbackResolve.indexOf("/") + 1);
1461
+ async function preflightClaudeSubscription(params) {
1462
+ let res;
1463
+ try {
1464
+ res = await fetch("https://api.anthropic.com/v1/messages", {
1465
+ method: "POST",
1466
+ headers: {
1467
+ authorization: `Bearer ${params.token}`,
1468
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
1469
+ "anthropic-version": "2023-06-01",
1470
+ "content-type": "application/json",
1471
+ "x-app": "cli"
1472
+ },
1473
+ body: JSON.stringify({
1474
+ model: params.model ?? FALLBACK_PROBE_MODEL,
1475
+ max_tokens: 1,
1476
+ system: CLAUDE_CODE_IDENTITY,
1477
+ messages: [{ role: "user", content: "ok" }]
1478
+ }),
1479
+ signal: AbortSignal.timeout(1e4)
1480
+ });
1481
+ } catch {
1482
+ return { usable: true, status: void 0 };
1483
+ }
1484
+ if (res.status !== 401 && res.status !== 403 && res.status !== 429)
1485
+ return { usable: true, status: res.status };
1486
+ const body = await res.text().catch(() => "");
1487
+ return {
1488
+ usable: false,
1489
+ status: res.status,
1490
+ reason: `${res.status}: ${extractApiErrorMessage(body)}`
1491
+ };
1492
+ }
1493
+ function extractApiErrorMessage(body) {
1494
+ try {
1495
+ const parsed = JSON.parse(body);
1496
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed && typeof parsed.error === "object" && parsed.error !== null && "message" in parsed.error && typeof parsed.error.message === "string") {
1497
+ return parsed.error.message;
1498
+ }
1499
+ } catch {
1500
+ }
1501
+ return body.slice(0, 200);
1502
+ }
1503
+
1504
+ // utils/credentialCheck.ts
1505
+ var PROBES = {
1506
+ ANTHROPIC_API_KEY: {
1507
+ request: (value) => ({
1508
+ url: "https://api.anthropic.com/v1/models",
1509
+ headers: { "x-api-key": value, "anthropic-version": "2023-06-01" }
1510
+ })
1511
+ },
1512
+ OPENROUTER_API_KEY: {
1513
+ request: (value) => ({
1514
+ url: "https://openrouter.ai/api/v1/key",
1515
+ headers: { authorization: `Bearer ${value}` }
1516
+ })
1517
+ },
1518
+ GEMINI_API_KEY: {
1519
+ request: (value) => ({
1520
+ url: `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(value)}`,
1521
+ headers: {}
1522
+ }),
1523
+ deadWhen: (status, body) => status === 400 && body.includes("API_KEY_INVALID")
1524
+ }
1525
+ };
1526
+ PROBES.GOOGLE_GENERATIVE_AI_API_KEY = PROBES.GEMINI_API_KEY;
1527
+ async function verifyClaudeSubscription(value) {
1528
+ const preflight = await preflightClaudeSubscription({ token: value, model: void 0 });
1529
+ if (preflight.status === void 0) return "unknown";
1530
+ return preflight.status === 401 ? "dead" : "alive";
1531
+ }
1532
+ function isCredentialProbeable(envVar) {
1533
+ return envVar === "CLAUDE_CODE_OAUTH_TOKEN" || envVar in PROBES;
1534
+ }
1535
+ async function verifyCredential(params) {
1536
+ const value = params.value.trim();
1537
+ if (value.length === 0) return "dead";
1538
+ if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") return verifyClaudeSubscription(value);
1539
+ const probe = PROBES[params.envVar];
1540
+ if (!probe) return "unknown";
1541
+ const request = probe.request(value);
1542
+ try {
1543
+ const response = await fetch(request.url, {
1544
+ headers: request.headers,
1545
+ signal: AbortSignal.timeout(5e3)
1546
+ });
1547
+ if (response.ok) return "alive";
1548
+ if (probe.deadWhen) {
1549
+ return probe.deadWhen(response.status, await response.text()) ? "dead" : "unknown";
1550
+ }
1551
+ return response.status === 401 ? "dead" : "unknown";
1552
+ } catch {
1553
+ return "unknown";
1554
+ }
1555
+ }
1556
+
1440
1557
  // utils/isPullfrog.ts
1441
1558
  function isPullfrog(actor) {
1442
1559
  actor = actor?.toLowerCase().replace("[bot]", "");
@@ -1562,6 +1679,7 @@ async function createLeapingProgressComment(ctx, target, body) {
1562
1679
 
1563
1680
  // utils/runStatusCheck.ts
1564
1681
  var RUN_STATUS_CHECK_NAME = "pullfrog";
1682
+ var GITHUB_ACTIONS_APP_SLUG = "github-actions";
1565
1683
  var APPROVAL_CHECK_NAME = "pullfrog-approval";
1566
1684
  var IN_PROGRESS_OUTPUT = {
1567
1685
  title: "Pullfrog is running",
@@ -1601,10 +1719,6 @@ var TERMINAL_OUTPUT = {
1601
1719
  skipped: {
1602
1720
  title: "Pullfrog run skipped",
1603
1721
  summary: "This run was superseded by another Pullfrog run."
1604
- },
1605
- stale: {
1606
- title: "Pullfrog run didn't finish",
1607
- summary: "Pullfrog never received a completion signal for this run. See the run logs for details."
1608
1722
  }
1609
1723
  };
1610
1724
  function terminalOutput(params) {
@@ -1623,7 +1737,9 @@ async function createRunStatusCheck(params) {
1623
1737
  check_name: RUN_STATUS_CHECK_NAME,
1624
1738
  status: "in_progress"
1625
1739
  }).catch(() => void 0);
1626
- const reusable = existing?.data.check_runs[0];
1740
+ const reusable = existing?.data.check_runs.find(
1741
+ (run) => run.app?.slug !== GITHUB_ACTIONS_APP_SLUG
1742
+ );
1627
1743
  if (reusable) return reusable.id;
1628
1744
  const createParams = {
1629
1745
  owner: params.owner,
@@ -1713,6 +1829,7 @@ export {
1713
1829
  getProviderDisplayName,
1714
1830
  isAutoTier,
1715
1831
  isCardGatedModel,
1832
+ isCredentialProbeable,
1716
1833
  isEffortPosition,
1717
1834
  isLeapingIntoActionCommentBody,
1718
1835
  isOssAllowedModel,
@@ -1722,6 +1839,7 @@ export {
1722
1839
  modelHasStoredAuth,
1723
1840
  modes,
1724
1841
  offeredRungs,
1842
+ ossFundedModelNames,
1725
1843
  parseCodexAuthBody,
1726
1844
  parseEffortPosition,
1727
1845
  parseModel,
@@ -1742,5 +1860,6 @@ export {
1742
1860
  stringifyCodexAuthBody,
1743
1861
  stripExistingFooter,
1744
1862
  truncateAtLineBoundary,
1745
- updateProgressComment
1863
+ updateProgressComment,
1864
+ verifyCredential
1746
1865
  };
package/dist/mcp/git.d.ts CHANGED
@@ -32,6 +32,33 @@ export declare function CommitChangesTool(ctx: ToolContext): import("fastmcp").T
32
32
  export declare const AUTH_REQUIRED_REDIRECT: Record<string, string>;
33
33
  export declare const NOSHELL_BLOCKED_SUBCOMMANDS: Record<string, string>;
34
34
  export declare const NOSHELL_BLOCKED_ARGS: string[];
35
+ /**
36
+ * Two malformed-but-unambiguous shapes the models keep emitting: `args[0]`
37
+ * repeating the subcommand, and the whole cmdline pushed into `args` behind
38
+ * `command: "git"`. Both were rejected (or, for the second, run as `git git
39
+ * log …`) and the models re-emitted the byte-identical call rather than
40
+ * adapting — 368 dead round trips over 190 runs in 24h (#1199). The intent is
41
+ * knowable, so normalize rather than reject.
42
+ *
43
+ * The promotion out of `command: "git"` re-clears `subcommandPattern`: without
44
+ * that, `args: ["-c", "alias.x=!evil", "x"]` would move a global git option
45
+ * into the subcommand slot that pattern exists to guard.
46
+ */
47
+ export declare function normalizeGitInvocation(input: {
48
+ command: string;
49
+ args: string[];
50
+ }): {
51
+ command: string;
52
+ args: string[];
53
+ };
54
+ /**
55
+ * git subcommands whose exit 1 is a documented result rather than a failure.
56
+ * The status code IS the answer and both streams are empty by design, so
57
+ * anything that reads "non-zero means broken" reports a successful negative as
58
+ * an error. See #1152; `merge-base --is-ancestor` keeps its own branch because
59
+ * it returns a boolean rather than a note.
60
+ */
61
+ export declare function describeEmptyResult(command: string, args: string[]): string | null;
35
62
  export declare function GitTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
36
63
  command: string;
37
64
  args?: string[];
@@ -189,8 +189,8 @@ export declare const CreatePullRequestReview: import("arktype/internal/variants/
189
189
  line: number;
190
190
  side?: "LEFT" | "RIGHT";
191
191
  body?: string;
192
- suggestion?: string;
193
- start_line?: number;
192
+ suggestion?: string | null;
193
+ start_line?: number | null;
194
194
  }[];
195
195
  }, {}>;
196
196
  export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
@@ -204,8 +204,8 @@ export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("f
204
204
  line: number;
205
205
  side?: "LEFT" | "RIGHT";
206
206
  body?: string;
207
- suggestion?: string;
208
- start_line?: number;
207
+ suggestion?: string | null;
208
+ start_line?: number | null;
209
209
  }[];
210
210
  }, {
211
211
  pull_number: number;
@@ -218,8 +218,8 @@ export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("f
218
218
  line: number;
219
219
  side?: "LEFT" | "RIGHT";
220
220
  body?: string;
221
- suggestion?: string;
222
- start_line?: number;
221
+ suggestion?: string | null;
222
+ start_line?: number | null;
223
223
  }[];
224
224
  }>> & {
225
225
  mutates?: boolean;
@@ -1,6 +1,6 @@
1
1
  import type { Octokit } from "@octokit/rest";
2
2
  import type { ToolContext } from "./server.ts";
3
- export declare const REVIEW_THREADS_QUERY = "\nquery ($owner: String!, $name: String!, $prNumber: Int!) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $prNumber) {\n reviewThreads(first: 100) {\n nodes {\n id\n path\n line\n startLine\n diffSide\n isResolved\n isOutdated\n comments(first: 50) {\n nodes {\n fullDatabaseId\n body\n bodyHTML\n createdAt\n diffHunk\n line\n startLine\n originalLine\n originalStartLine\n author { login }\n pullRequestReview {\n databaseId\n author { login }\n }\n reactionGroups {\n content\n reactors(first: 10) {\n nodes {\n ... on Actor { login }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n";
3
+ export declare const REVIEW_THREADS_QUERY = "\nquery ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $prNumber) {\n reviewThreads(first: 100, after: $cursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n path\n line\n startLine\n diffSide\n isResolved\n isOutdated\n comments(first: 50) {\n nodes {\n fullDatabaseId\n body\n bodyHTML\n createdAt\n diffHunk\n line\n startLine\n originalLine\n originalStartLine\n author { login }\n pullRequestReview {\n databaseId\n author { login }\n }\n reactionGroups {\n content\n reactors(first: 10) {\n nodes {\n ... on Actor { login }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n";
4
4
  export type ReviewThreadComment = {
5
5
  fullDatabaseId: string | null;
6
6
  body: string;
@@ -45,6 +45,10 @@ export type ReviewThreadsQueryResponse = {
45
45
  repository: {
46
46
  pullRequest: {
47
47
  reviewThreads: {
48
+ pageInfo: {
49
+ hasNextPage: boolean;
50
+ endCursor: string | null;
51
+ };
48
52
  nodes: (ReviewThread | null)[] | null;
49
53
  } | null;
50
54
  } | null;
package/dist/models.d.ts CHANGED
@@ -38,7 +38,10 @@ export interface ModelAlias {
38
38
  openRouterResolve: string | undefined;
39
39
  /** top-tier pick for this provider — preferred during auto-select */
40
40
  preferred: boolean;
41
- /** whether this alias is free and requires no API key */
41
+ /** whether this alias costs nothing to run. NOT the same as needing no
42
+ * credential: Zen's free models still require `OPENCODE_API_KEY`, and reading
43
+ * this as "keyless" is what let a keyless run boot onto one and die with
44
+ * `No provider available` (#1077). used to skip the proxy mint. */
42
45
  isFree: boolean;
43
46
  /** slug of a replacement model to resolve through. presence means this alias
44
47
  * never runs as-is — resolution redirects to the replacement and it's hidden
@@ -122,6 +125,8 @@ export declare function parseModel(slug: string): {
122
125
  };
123
126
  export declare function getModelProvider(slug: string): string;
124
127
  export declare function getProviderDisplayName(slug: string): string | undefined;
128
+ /** provider-prefixed specifier → bare model id (`anthropic/claude-opus-5` → `claude-opus-5`). */
129
+ export declare function stripProviderPrefix(specifier: string): string;
125
130
  export declare function getModelEnvVars(slug: string): string[];
126
131
  /** managed credentials are authored only via `pullfrog auth <provider>` — they
127
132
  * count as "configured" for hasAnyKey-style UI checks but are never offered as
@@ -245,14 +250,27 @@ export declare const DEFAULT_PROXY_MODEL: string;
245
250
  * DB toggle — changing it is a deploy, which is the right friction. costs below
246
251
  * are modelled on the measured OSS token mix, where cache reads are ~87.5% of
247
252
  * input and list price therefore ranks models WRONG: Luna 0.39x, Flash 0.45x,
248
- * Pro 1.00x, MiniMax M2 1.30x, Kimi 3.88x modelled — but 7.25x as actually
249
- * measured over the pre-#1063 Kimi era, which is the number to trust.
253
+ * Pro 1.00x, MiniMax M2 1.30x.
254
+ *
255
+ * Kimi K2 was funded and then dropped: 13.2x the funded default as MEASURED
256
+ * over 2,203 real runs ($0.4782 vs $0.0362), almost entirely on its cache-read
257
+ * RATE — 8.5x Flash's. it also fails 2.9% of runs against Flash's 0.0%. best
258
+ * achievable was 10.9x (cheapest OpenRouter endpoint), so no routing fixes it.
259
+ * see wiki/oss-model-allowlist.md.
250
260
  */
251
261
  export declare const OSS_MODEL_ALLOWLIST: readonly string[];
252
262
  /** the pick the console badges. DERIVED from the efficient tier rather than
253
263
  * restated, so the badge cannot drift from the model that actually runs — a
254
264
  * second literal here would be an invariant nothing enforces. */
255
265
  export declare const OSS_RECOMMENDED_MODEL: string;
266
+ /**
267
+ * display names of the funded set, in allowlist order. the single place every
268
+ * surface that NAMES the menu in prose reads it from — the application page,
269
+ * the acceptance email, the model-access error — so none of them can promise a
270
+ * model the program stopped funding. callers format the list themselves; this
271
+ * is a catalog, not copy.
272
+ */
273
+ export declare function ossFundedModelNames(): string[];
256
274
  /**
257
275
  * whether an OSS repo's stored pick is one the program funds. resolves through
258
276
  * the fallback chain first, so `auto/*` sentinels and deprecated slugs are
@@ -148,6 +148,13 @@ export interface ToolState {
148
148
  oss?: boolean | undefined;
149
149
  todoTracker?: TodoTracker | undefined;
150
150
  agentDiagnostic?: AgentDiagnostic | undefined;
151
+ /**
152
+ * a `PATCH /api/workflow-run/:id` came back 404 — the reservation was never
153
+ * claimed, which is permanent for the life of the run (the claim happens at
154
+ * setup, strictly before any PATCH). latched so the remaining calls stop
155
+ * firing: one CI run spent 28 doomed round trips on it. see #1153.
156
+ */
157
+ workflowRunUnclaimed?: boolean | undefined;
151
158
  }
152
159
  interface InitToolStateParams {
153
160
  progressComment: {
@@ -5,6 +5,28 @@
5
5
  * on our side is the wrong CTA — their key is configured and still stored.
6
6
  */
7
7
  export declare const SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
8
+ /**
9
+ * marker for the "your Router wallet is empty AND you have no provider key"
10
+ * body. its own marker rather than the missing-key one because
11
+ * `formatApiKeyErrorSummary` rebuilds any body carrying `MISSING_KEY_MARKER`
12
+ * that doesn't start with it, which would silently discard this copy.
13
+ */
14
+ export declare const ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
15
+ /**
16
+ * marker for a credential the provider explicitly REJECTED before the agent
17
+ * started. needs a verbatim guard for the same reason as its three siblings,
18
+ * and more sharply: the body quotes the provider's own wording, which is
19
+ * exactly what `isApiKeyAuthError` sniffs — so an Anthropic error whose JSON
20
+ * carries `authentication_error` would be rebuilt into the generic
21
+ * "rotate the key, update the GitHub Actions secret" copy. That is #782
22
+ * verbatim, on the code path built to stop it.
23
+ *
24
+ * Phrased distinctively for the same reason as its siblings: `runErrorRenderer`
25
+ * short-circuits the whole rendering pipeline on a SUBSTRING match against
26
+ * arbitrary agent output, so a marker short enough to occur by accident would
27
+ * hand that agent the ability to suppress every other error body.
28
+ */
29
+ export declare const CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
8
30
  /**
9
31
  * Validate that the resolved model can actually be served by the chosen
10
32
  * agent. For routing slugs (Bedrock / Vertex) the auth shape is multi-var
@@ -26,6 +48,7 @@ export declare function validateAgentApiKey(params: {
26
48
  /** run-context couldn't hand over Pullfrog-stored secrets, so a missing key
27
49
  * says nothing about what the user actually configured. */
28
50
  secretsUnavailable?: boolean | undefined;
51
+ routerUnfunded?: boolean | undefined;
29
52
  }): void;
30
53
  /**
31
54
  * Detect agent-runtime auth failures that should be reformatted as an actionable
@@ -68,6 +91,14 @@ export declare function isApiKeyAuthError(text: string): boolean;
68
91
  * expiry can't be misread as an LLM credential problem.
69
92
  */
70
93
  export declare function isOAuthCredentialExpiredError(text: string): boolean;
94
+ /**
95
+ * The key itself is unsendable, not wrong: a line-wrapped paste leaves an
96
+ * interior newline, and the provider SDK refuses to serialize the header
97
+ * (`API Error: Header '14' has invalid value`). The only member of the
98
+ * credential-error family whose remedy is NOT "rotate the key" — re-saving the
99
+ * same value on one line fixes it. See #1162.
100
+ */
101
+ export declare function isMalformedKeyError(text: string): boolean;
71
102
  /**
72
103
  * Anthropic entitlement denial for Claude Pro/Max subscription auth (#1072):
73
104
  * an org admin turned off Claude Code subscription access. no credential the
@@ -75,6 +106,20 @@ export declare function isOAuthCredentialExpiredError(text: string): boolean;
75
106
  */
76
107
  export declare function isClaudeSubscriptionDisabledError(text: string): boolean;
77
108
  export declare function isClaudeSessionLimitError(text: string): boolean;
109
+ /**
110
+ * The provider rejected a credential we checked before the agent started, and
111
+ * nothing else on the account can serve this run. Names the credential and the
112
+ * place it actually lives, rather than guessing from the provider's wording.
113
+ */
114
+ export declare function buildRejectedCredentialError(params: {
115
+ credential: string;
116
+ /** the provider's own message, when it gave one worth quoting. */
117
+ reason: string | undefined;
118
+ owner: string;
119
+ name: string;
120
+ /** stored in Pullfrog (console / `pullfrog auth`) rather than a GitHub Actions secret. */
121
+ inPullfrogStore: boolean;
122
+ }): string;
78
123
  /**
79
124
  * Friendly Markdown summary for both the missing-key and invalid-key cases.
80
125
  * Used in the catch / result-failure paths in `main.ts` to overwrite the raw
@@ -18,10 +18,10 @@ export interface BuildPullfrogFooterParams {
18
18
  /** model slug from payload (e.g., "anthropic/claude-opus"). shown in footer as "Using `Model Name`" */
19
19
  model?: string | undefined;
20
20
  /**
21
- * When the action engaged the BYOK fallback, this is the slug the user
22
- * had configured (e.g. "anthropic/claude-opus") — the footer renders
23
- * `Using <free model> (credentials for <configured> not configured)`
24
- * so the substitution is visible in PR comments + reviews.
21
+ * When a credential was rejected and the run moved to another model, this is
22
+ * the slug the user had configured (e.g. "anthropic/claude-opus") — the footer
23
+ * renders `Using <model> (credentials for <configured> were rejected by the
24
+ * provider)` so the substitution is visible in PR comments + reviews.
25
25
  */
26
26
  fallbackFrom?: string | undefined;
27
27
  /**
@@ -1,7 +1,16 @@
1
+ /**
2
+ * `status` is the HTTP status Anthropic answered with, or `undefined` when we
3
+ * never got an answer at all (network failure / timeout). Callers that need to
4
+ * tell "the credential is dead" from "we couldn't ask" — the paste-time check
5
+ * in `credentialCheck.ts` — read it; `usable` alone conflates the two, because
6
+ * fail-open makes both `usable: true`.
7
+ */
1
8
  export type SubscriptionPreflight = {
2
9
  usable: true;
10
+ status: number | undefined;
3
11
  } | {
4
12
  usable: false;
13
+ status: number;
5
14
  reason: string;
6
15
  };
7
16
  /**
@@ -18,6 +18,17 @@ export interface CodexAuthBody {
18
18
  account_id?: string;
19
19
  };
20
20
  last_refresh?: string;
21
+ /**
22
+ * ISO timestamp of an `invalid_grant` rejection. OpenAI rotates the refresh
23
+ * token on every use, so a rejection is PERMANENT — without a latch the
24
+ * server re-issued the identical doomed refresh on every run (455 futile
25
+ * round trips in 7 days, one per run, each holding a Postgres row lock across
26
+ * a 10s external call). Cleared implicitly: `pullfrog auth codex` and
27
+ * `PUT /api/runtime/secret` write a fresh blob without it, which re-arms
28
+ * rotation. Never reaches `auth.json` — `installCodexAuth` builds that file
29
+ * from explicit fields. See [#1101](https://github.com/pullfrog/app/issues/1101).
30
+ */
31
+ refresh_rejected_at?: string;
21
32
  }
22
33
  /** OAuth client id Codex CLI and OpenCode both use against `auth.openai.com`.
23
34
  * Same chain — a refresh token minted via `codex login --device-auth` can be
@@ -0,0 +1,21 @@
1
+ /**
2
+ * What the provider itself says about a credential. `unknown` is the safe
3
+ * default and covers every outcome that isn't the provider explicitly
4
+ * rejecting it — no probe for this env var, a network failure, a timeout, a
5
+ * 404, a 5xx. Callers must treat `unknown` as "carry on as before": only
6
+ * `dead` is positive evidence, and acting on anything weaker would let a
7
+ * provider outage rewrite a working account's configuration.
8
+ */
9
+ export type CredentialVerdict = "alive" | "dead" | "unknown";
10
+ /** whether asking the provider about this credential would tell us anything. */
11
+ export declare function isCredentialProbeable(envVar: string): boolean;
12
+ /**
13
+ * Ask the provider whether it still accepts this credential. Used at the two
14
+ * points that were each getting it wrong on their own: the run-time fallback
15
+ * decision, and the console/CLI paste flow that previously stored a dead token
16
+ * without comment.
17
+ */
18
+ export declare function verifyCredential(params: {
19
+ envVar: string;
20
+ value: string;
21
+ }): Promise<CredentialVerdict>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `ok` covers "the credential works", "we couldn't tell", and "something else
3
+ * on this model still works" — the run proceeds as it did before this check
4
+ * existed. The other two are only reached when every credential the configured
5
+ * model could use has been explicitly rejected by its own provider.
6
+ *
7
+ * `replacement` is a concrete model, never a "just auto-select something":
8
+ * deciding an alternative exists and picking it have to be the SAME decision,
9
+ * or the picker can land back on the provider that just failed.
10
+ */
11
+ export type CredentialOutcome = {
12
+ kind: "ok";
13
+ } | {
14
+ kind: "fellBack";
15
+ credential: string;
16
+ reason: string | undefined;
17
+ replacement: string;
18
+ } | {
19
+ kind: "dead";
20
+ credential: string;
21
+ reason: string | undefined;
22
+ };
23
+ /**
24
+ * Ask the providers whether the credentials behind the configured model still
25
+ * work, BEFORE the agent spawns — so a rejected one becomes either a run on a
26
+ * model that does work or an accurate error, instead of the agent 401ing three
27
+ * seconds in against a "rotate your GitHub Actions secret" CTA naming a place
28
+ * the user never put a key.
29
+ *
30
+ * Failed credentials are deleted from `process.env` only when this run can
31
+ * still proceed without them. Nothing downstream infers anything from that
32
+ * deletion — `claude.ts` runs its own preflight regardless, because this check
33
+ * cannot promise it probed any particular token (see its comment for the three
34
+ * paths that reach it unprobed).
35
+ *
36
+ * Only providers in the `credentialCheck` probe table can be found dead; every
37
+ * other credential is left exactly as it was.
38
+ */
39
+ export declare function checkConfiguredCredentials(params: {
40
+ model: string | undefined;
41
+ authorized: Set<string>;
42
+ }): Promise<CredentialOutcome>;
@@ -1,5 +1,6 @@
1
1
  import { throttling } from "@octokit/plugin-throttling";
2
2
  import { Octokit } from "@octokit/rest";
3
+ import * as yes from "../yes/index.ts";
3
4
  /** GitHub Actions OIDC request credentials, stashed before env wipes */
4
5
  export interface OidcCredentials {
5
6
  requestUrl: string;
@@ -57,6 +58,18 @@ type AcquireTokenOptions = {
57
58
  * as transient.
58
59
  */
59
60
  export declare function fetchIdTokenFromStash(creds: OidcCredentials): Promise<string>;
61
+ /**
62
+ * One ID token per run for our single audience. `acquireTokenViaOIDC` runs two
63
+ * to four times per run (git token, MCP token, xrepo read token) on top of
64
+ * `resolveRunContextData`'s own mint, and each call used to hit the runner's
65
+ * OIDC endpoint again — back-to-back requests are exactly what provoked the
66
+ * Envoy load-shed (`reset reason: overflow`) that killed 19 runs in 24h, all of
67
+ * them AFTER setup had already succeeded (#1182). The token is valid for
68
+ * minutes, so the TTL costs nothing; failures are never cached, so a caller's
69
+ * own retry still re-mints. Mid-run refreshes take the stashed-credentials path
70
+ * (`opts.oidc`) and deliberately bypass this.
71
+ */
72
+ export declare const mintIdToken: yes.OpFunction<() => Promise<string>>;
60
73
  /**
61
74
  * ensure a GitHub token is available in the environment.
62
75
  *
@@ -16,6 +16,8 @@
16
16
  * value surfaces as a clear "missing key" downstream rather than silently
17
17
  * mutating to the empty string.
18
18
  */
19
+ /** C0 controls + DEL — the bytes an HTTP header value cannot carry. */
20
+ export declare function hasControlCharacter(value: string): boolean;
19
21
  export declare function sanitizeSecret(key: string, value: string): string | null;
20
22
  /**
21
23
  * Normalize environment variables to uppercase.