pullfrog 0.1.52 → 0.1.54

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,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.
@@ -7,6 +7,7 @@ export declare const JsonPayload: import("arktype/internal/variants/object.ts").
7
7
  model?: string | undefined;
8
8
  modelExplicit?: boolean | undefined;
9
9
  effort?: string | number | undefined;
10
+ debug?: boolean | undefined;
10
11
  triggerer?: string | undefined;
11
12
  baseInstructions?: string | undefined;
12
13
  eventInstructions?: string;
@@ -33,6 +34,7 @@ export declare const Inputs: import("arktype/internal/variants/object.ts").Objec
33
34
  prompt_file?: string | undefined;
34
35
  model?: string | undefined;
35
36
  effort?: string | undefined;
37
+ debug?: "disabled" | "enabled" | undefined;
36
38
  timeout?: string | undefined;
37
39
  push?: "disabled" | "enabled" | "restricted" | undefined;
38
40
  shell?: "disabled" | "enabled" | "restricted" | undefined;
@@ -50,6 +52,7 @@ export declare function resolvePayload(resolvedPromptInput: ResolvedPromptInput,
50
52
  model: string | undefined;
51
53
  modelExplicit: boolean;
52
54
  effort: number | undefined;
55
+ debug: true | undefined;
53
56
  prompt: string;
54
57
  triggerer: string | undefined;
55
58
  baseInstructions: string | undefined;
@@ -1,5 +1,10 @@
1
1
  /** Stable label for the BYOK provider-billing-exhausted classification. */
2
2
  export declare const PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
3
+ /** Stable label for "OpenRouter can route this model nowhere you permit". */
4
+ export declare const PROVIDER_NO_ENDPOINTS_LABEL = "provider no routable endpoints";
5
+ export declare function findAnthropicSpendCap(text: string): {
6
+ regainAt: string | null;
7
+ } | null;
3
8
  /**
4
9
  * Result of a provider-error scan: the classification label plus a
5
10
  * human-readable excerpt centered on the matched line. The excerpt is what
@@ -22,6 +27,44 @@ export declare function isRouterKeylimitExhaustedError(text: string): boolean;
22
27
  * is the user's own provider account.
23
28
  */
24
29
  export declare function isProviderBillingExhausted(text: string): boolean;
30
+ /**
31
+ * OpenRouter accepted the request and found nowhere to send it: the account's
32
+ * data policy or guardrail settings exclude every provider serving the picked
33
+ * model. Nothing is billed and no credential is at fault — the only fix is the
34
+ * privacy settings page or a different model. See #1164.
35
+ */
36
+ export declare function isProviderNoRoutableEndpoints(text: string): boolean;
37
+ /**
38
+ * OpenRouter's per-key ceiling, not an empty wallet. Topping up credits alone
39
+ * will not clear it, so it needs a different headline and a second lever than
40
+ * the generic billing-exhausted copy. Both shapes classify as
41
+ * `PROVIDER_BILLING_EXHAUSTED_LABEL`; this narrows within that class.
42
+ *
43
+ * TWO wire forms, and the obvious one is the rarer one. #1071's `Key limit
44
+ * exceeded (total limit)` names itself; #1164's is the `requires more credits,
45
+ * or fewer max_tokens` form, which is indistinguishable from a drained wallet
46
+ * except for the remedy the provider appends — a link to that key's own page.
47
+ * Anchor on the `/keys/<id>` URL rather than the "adjust the key's total limit"
48
+ * sentence, because the apostrophe in it arrives from provider JSON and may be
49
+ * typographic. A genuinely empty wallet (`Insufficient credits. Add more using
50
+ * …/settings/credits`) carries no `/keys/` path and correctly stays out.
51
+ */
52
+ export declare function isOpenRouterKeyLimitExceeded(text: string): boolean;
53
+ /**
54
+ * The upstream is having a moment: Anthropic's `API Error: 529 Overloaded`,
55
+ * OpenRouter's `provider_unavailable` / `timeout`, OpenCode Zen's `Streaming
56
+ * response failed: [5xx]`. Three transports, one condition — nothing the user
57
+ * configured is wrong and the next run will probably work, which is exactly
58
+ * what the contentless `Run failed.` comment failed to say across 28 runs that
59
+ * had already done up to 38 tool calls of real work (#1173).
60
+ *
61
+ * `error_type` is matched as the machine-readable FIELD it is rather than by
62
+ * substring-matching the human prose around it, which varies per upstream.
63
+ * Deliberately narrow on 5xx so it cannot reach a 401/402 that
64
+ * `isApiKeyAuthError` / `isProviderBillingExhausted` own — and `renderRunError`
65
+ * orders it after both regardless.
66
+ */
67
+ export declare function isTransientUpstreamError(text: string): boolean;
25
68
  /**
26
69
  * Extract `providerID=foo` from agent error logs (OpenCode emits this on
27
70
  * `provider error detected (...)` lines). Returns the lowercase provider
@@ -15,7 +15,7 @@
15
15
  * exist yet at this point in the pipeline.
16
16
  *
17
17
  * - 402 → `BillingError` (card declined, balance empty, 3DS, etc.)
18
- * - 503 → `TransientError` (transient sync issueretry next dispatch)
18
+ * - 5xx → `TransientError` (the mint is down retried in-run first)
19
19
  * - 404 → `TransientError` (stale repo↔account link — re-homes on next webhook)
20
20
  */
21
21
  import type { ToolState } from "../toolState.ts";
@@ -76,6 +76,16 @@ export interface RunContext {
76
76
  * distinction a transient failure renders as "you have no API key".
77
77
  */
78
78
  secretsUnavailable?: boolean | undefined;
79
+ /**
80
+ * the Router was this account's funding path and its wallet is empty, so the
81
+ * server declined the mint rather than 402ing — the run falls through to
82
+ * BYOK, which is the documented affordance for a router-mode account whose
83
+ * key lives in workflow `env:`. only meaningful when the key search then
84
+ * comes up dry, where it turns "go add an API key" into copy that also names
85
+ * topping up. defaults false: an unreachable server must not assert a
86
+ * funding state. see wiki/billing.md.
87
+ */
88
+ routerUnfunded?: boolean | undefined;
79
89
  }
80
90
  /**
81
91
  * fetch run context from Pullfrog API
@@ -17,6 +17,9 @@ export interface RunContextData {
17
17
  /** stored secrets couldn't be materialized for this run — not the same as
18
18
  * the user having none. see `RunContext.secretsUnavailable`. */
19
19
  secretsUnavailable?: boolean | undefined;
20
+ /** the Router was declined because the wallet is empty. see
21
+ * `RunContext.routerUnfunded`. */
22
+ routerUnfunded?: boolean | undefined;
20
23
  }
21
24
  interface ResolveRunContextDataParams {
22
25
  octokit: OctokitWithPlugins;
@@ -35,6 +35,12 @@
35
35
  * `maximum context length is N tokens`. Actionable, so it renders on
36
36
  * both surfaces rather than collapsing to the one-line comment.
37
37
  *
38
+ * 4c. Transient upstream failure (#1173) — Anthropic `529 Overloaded`,
39
+ * OpenRouter `provider_unavailable` / `timeout`, Zen `Streaming response
40
+ * failed: [5xx]`. Last of the classified branches so every more specific
41
+ * one wins first. Renders on both surfaces: the remedy is re-triggering,
42
+ * which the user can only know if we say so.
43
+ *
38
44
  * 5. Activity-timeout hang — `errorMessage` starts with
39
45
  * `"activity timeout"` or `"agent still pending"` AND none of the
40
46
  * above matched. The harness keeps structured diagnostic state on
@@ -52,8 +58,8 @@
52
58
  * the hang case, since the raw internal string helps nobody on the PR.
53
59
  *
54
60
  * Net: the actionable classifications (billing, API-key, model-not-found,
55
- * no-provider-available, context-overflow) render identical bodies on both
56
- * surfaces; the non-actionable ones (hang,
61
+ * no-provider-available, context-overflow, transient-upstream) render identical
62
+ * bodies on both surfaces; the non-actionable ones (hang,
57
63
  * generic) keep the forensics in the Actions job summary and show a calm
58
64
  * one-liner in the PR comment, whose footer already carries Pullfrog
59
65
  * branding + rerun links.
@@ -35,11 +35,18 @@ export declare const RUN_STATUS_CHECK_NAME = "pullfrog";
35
35
  /** the review-verdict check. opt-in, terminal-only, and deliberately separate from the above. */
36
36
  export declare const APPROVAL_CHECK_NAME = "pullfrog-approval";
37
37
  /**
38
- * The terminal states we report. Exactly GitHub's check-run `conclusion` enum, which
39
- * happens to be `WorkflowRunStatus` minus `running` — so the server can map a finished
40
- * run's status straight across (`checkConclusionFromStatus` in utils/workflowRunStatus.ts).
38
+ * The terminal states we report: exactly GitHub's check-run `conclusion` enum.
39
+ *
40
+ * NOT `WorkflowRunStatus`. `stale` is a valid `workflow_run.conclusion` and is
41
+ * NOT a member of the Checks API enum, which GitHub's own 422 enumerates as
42
+ * `["success", "failure", "neutral", "cancelled", "timed_out",
43
+ * "action_required", "skipped"]`. Admitting it here let
44
+ * `checkConclusionFromStatus` type-check while producing a value the wire
45
+ * always rejects, so 100% of reaper-expired rows failed close-out — and since
46
+ * the failure left `checkRunId` set, the hourly sweep re-sent the same illegal
47
+ * conclusion forever. See [#1178](https://github.com/pullfrog/app/issues/1178).
41
48
  */
42
- export type RunStatusCheckConclusion = "success" | "failure" | "cancelled" | "timed_out" | "action_required" | "neutral" | "skipped" | "stale";
49
+ export type RunStatusCheckConclusion = "success" | "failure" | "cancelled" | "timed_out" | "action_required" | "neutral" | "skipped";
43
50
  /**
44
51
  * Parse the on-the-wire `{ id: string }` shape (the form carried in `JsonPayload`) into
45
52
  * a check-run id. Mirrors `parseProgressComment` — returns undefined when the id isn't a
@@ -101,6 +108,9 @@ export interface RunStatusCheckOctokit {
101
108
  data: {
102
109
  check_runs: {
103
110
  id: number;
111
+ app?: {
112
+ slug?: string | null;
113
+ } | null;
104
114
  }[];
105
115
  };
106
116
  }>;