@edgehero/pi-dispatch 2.0.0 → 2.1.0
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.
- package/.env.example +3 -2
- package/README.md +3 -1
- package/deploy/com.pi-dispatch.worker.plist +1 -1
- package/deploy/worker-env-wrapper.cmd +1 -1
- package/deploy/worker-env-wrapper.sh +3 -3
- package/package.json +2 -2
- package/src/doctor.mjs +81 -6
- package/src/egress.mjs +4 -3
- package/src/env-allowlist.mjs +35 -12
- package/src/host-pi.mjs +19 -3
- package/src/packages.mjs +2 -2
- package/src/pricing.mjs +9 -5
- package/src/processor.mjs +4 -3
- package/src/provider-key.mjs +32 -7
- package/src/provider-steering.mjs +50 -9
- package/src/secrets.mjs +2 -1
- package/src/subscriptions.mjs +7 -3
- package/src/triggers.mjs +5 -2
package/.env.example
CHANGED
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
# in two different places depending on which deployment reads it. Write the path out in full, not $HOME.
|
|
20
20
|
|
|
21
21
|
# --- Provider credential ---
|
|
22
|
-
# pi supports ~
|
|
22
|
+
# pi supports ~40 providers; set the key for the one you use, under the variable name pi expects.
|
|
23
23
|
# The worker forwards ONLY the configured provider's key into the job container -- nothing else.
|
|
24
|
-
# Anthropic: ANTHROPIC_API_KEY (
|
|
24
|
+
# Anthropic: ANTHROPIC_API_KEY (pi reads ANTHROPIC_AUTH_TOKEN, a bearer token, and ANTHROPIC_OAUTH_TOKEN
|
|
25
|
+
# BEFORE it, so leave both unset: either one set silently wins over the API key)
|
|
25
26
|
# OpenAI: OPENAI_API_KEY Google: GEMINI_API_KEY Groq: GROQ_API_KEY ... etc.
|
|
26
27
|
# You can LEAVE THIS BLANK if you are already logged into pi: when the env has no key, the worker reads the
|
|
27
28
|
# API key from ~/.pi/agent/auth.json (host-side) and env-injects it -- on by default, nothing to set.
|
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ from cron triggers, or from forge events that the
|
|
|
12
12
|
|
|
13
13
|
## Start
|
|
14
14
|
|
|
15
|
-
You need Docker or Podman
|
|
15
|
+
You need Docker or Podman and Node 22.19 or newer on one machine that stays on (a Linux server or VM, or
|
|
16
|
+
your own Mac or Windows machine with Docker Desktop), and an API key for a model provider that pi supports.
|
|
17
|
+
pi-dispatch itself needs no AI key: the key is for pi, inside each job.
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
20
|
mkdir my-dispatch && cd my-dispatch
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
(`pi-dispatch service render` composes exactly that argv with this host's real paths; the wrapper
|
|
16
16
|
refuses an empty argv rather than guessing what to run). NO secrets are inlined here: there is
|
|
17
17
|
deliberately no EnvironmentVariables dict, since that would commit credentials into this file. The
|
|
18
|
-
wrapper reads `.env` at runtime instead (note the ANTHROPIC_OAUTH_TOKEN over ANTHROPIC_API_KEY
|
|
18
|
+
wrapper reads `.env` at runtime instead (note the ANTHROPIC_AUTH_TOKEN/ANTHROPIC_OAUTH_TOKEN over ANTHROPIC_API_KEY
|
|
19
19
|
precedence trap documented in the wrapper).
|
|
20
20
|
|
|
21
21
|
Graceful shutdown needs NO macOS-specific code: `launchctl bootout` sends SIGTERM, which the wrapper
|
|
@@ -21,7 +21,7 @@ REM `pi-dispatch service install` passes them via nssm AppParameters. This w
|
|
|
21
21
|
REM decides WHAT to run -- only the env it runs in and what its exit code means -- so an empty
|
|
22
22
|
REM argument list is a configuration error, refused below.
|
|
23
23
|
REM
|
|
24
|
-
REM TRAP: inside pi, ANTHROPIC_OAUTH_TOKEN silently
|
|
24
|
+
REM TRAP: inside pi, ANTHROPIC_AUTH_TOKEN and ANTHROPIC_OAUTH_TOKEN silently take precedence over ANTHROPIC_API_KEY. Set exactly
|
|
25
25
|
REM one in `.env`.
|
|
26
26
|
REM
|
|
27
27
|
REM `.env` FORMAT for this loader: KEY=VALUE, one per line. Values MUST be UNQUOTED -- cmd's `set` keeps
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
# profile happens to export. Nothing here contains a credential -- the secrets live in `.env`, which is
|
|
25
25
|
# gitignored and read at runtime.
|
|
26
26
|
#
|
|
27
|
-
# TRAP: inside pi, `ANTHROPIC_OAUTH_TOKEN` silently
|
|
28
|
-
# one in `.env`; this wrapper only ADDS the `.env` vars on top of the
|
|
29
|
-
# clear a stray pre-existing one, so a leaked host
|
|
27
|
+
# TRAP: inside pi, `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_OAUTH_TOKEN` silently take precedence over
|
|
28
|
+
# `ANTHROPIC_API_KEY`. Set exactly one in `.env`; this wrapper only ADDS the `.env` vars on top of the
|
|
29
|
+
# current environment, it does not clear a stray pre-existing one, so a leaked host token would still win.
|
|
30
30
|
#
|
|
31
31
|
# One worker per host (DES-CONCURRENCY-3): parallelism is PI_CONCURRENCY inside the single process, not
|
|
32
32
|
# multiple daemons. Requires the AOF-enabled Valkey from deploy/docker-compose.yml.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgehero/pi-dispatch",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The pi-dispatch worker and CLI: runs the pi coding agent as a self-hosted service, one locked down Docker or Podman container per job, with spend caps checked before anything is spent, plus init, up, doctor and service install.",
|
|
6
6
|
"keywords": [
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"start": "node src/cli.mjs worker"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
|
-
"@earendil-works/pi-ai": "0.
|
|
101
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
102
102
|
"@octokit/auth-app": "8.2.0",
|
|
103
103
|
"@octokit/rest": "22.0.1",
|
|
104
104
|
"bullmq": "5.80.4",
|
package/src/doctor.mjs
CHANGED
|
@@ -93,7 +93,7 @@ import { BOOT_REFUSING_JOB_USER_CAUSES, DAEMON_FACTS_TIMEOUT_MS, JOB_USER_FIX, m
|
|
|
93
93
|
import { parseSecretProfiles } from "./secret-profiles.mjs";
|
|
94
94
|
// The OAuth-suffix rule and the variable it selects live in their own import-free module so the worker
|
|
95
95
|
// can share them: doctor NAMES a variable and env-allowlist WRITES one, and they must never differ.
|
|
96
|
-
import {
|
|
96
|
+
import { apiKeyVariable, nonApiKeyKind } from "./provider-key.mjs";
|
|
97
97
|
import { parseTriggers } from "./triggers.mjs";
|
|
98
98
|
import { cronPlacement } from "./schedules.mjs";
|
|
99
99
|
|
|
@@ -3468,9 +3468,10 @@ function providerKeyCheck({ provider, env, agentDir, oracle, nodeOk }) {
|
|
|
3468
3468
|
// still refused -- one step further down, against the variable pi actually reads, where it is a fact
|
|
3469
3469
|
// about THAT credential rather than a reason to pretend the variable is unset.
|
|
3470
3470
|
const set = candidates.filter((name) => (env[name] ?? "") !== "");
|
|
3471
|
-
// The variable to TELL an operator to set is never the OAuth token, and it is the
|
|
3472
|
-
// worker makes when it writes an `auth.json` key into a container (issue #311
|
|
3473
|
-
// module, so a doctor line cannot name a variable the job
|
|
3471
|
+
// The variable to TELL an operator to set is never the OAuth token or the bearer token, and it is the
|
|
3472
|
+
// SAME choice the worker makes when it writes an `auth.json` key into a container (issue #311; the
|
|
3473
|
+
// bearer half is issue #509). One function, one module, so a doctor line cannot name a variable the job
|
|
3474
|
+
// path does not use.
|
|
3474
3475
|
const apiKeyVar = apiKeyVariable(candidates);
|
|
3475
3476
|
|
|
3476
3477
|
if (set.length > 0) {
|
|
@@ -3489,19 +3490,57 @@ function providerKeyCheck({ provider, env, agentDir, oracle, nodeOk }) {
|
|
|
3489
3490
|
fix: `set a real value for ${using}, or unset it: pi reads it as present, so every job spends a container to fail auth`,
|
|
3490
3491
|
};
|
|
3491
3492
|
}
|
|
3492
|
-
|
|
3493
|
+
const kind = nonApiKeyKind(using);
|
|
3494
|
+
if (kind === null && isOAuthTokenValue(provider, env[using])) {
|
|
3495
|
+
// The variable is the API key's, the VALUE is a subscription login: pi decides by the value
|
|
3496
|
+
// (anthropic-messages.js `isOAuthToken`, a substring test for "sk-ant-oat"), not by the name, and
|
|
3497
|
+
// sends it as `Authorization: Bearer` with Claude Code's identity headers. So it is the OAuth
|
|
3498
|
+
// case under the API key's name, and it gets that warning rather than a green line.
|
|
3499
|
+
return {
|
|
3500
|
+
ok: false,
|
|
3501
|
+
warn: true,
|
|
3502
|
+
label: `Provider key set (${provider}: ${using}) -- but the value is an OAuth/subscription token, not an API key`,
|
|
3503
|
+
fix: `put a real API key in ${using}: pi recognises the value as a subscription login whatever variable holds it and sends it as an Authorization: Bearer header; it expires, and the container cannot refresh it`,
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
if (kind === null) return { ok: true, label: `Provider key set (${provider}: ${using})` };
|
|
3493
3507
|
// Warn, not fail, and the choice is deliberate: the worker forwards this variable and the job WILL
|
|
3494
3508
|
// run, so failing here would put doctor in disagreement with the worker -- the exact disease this
|
|
3495
3509
|
// issue is about. What doctor must stop doing is what it did before: pass in silence, as though a
|
|
3496
3510
|
// subscription login were a service credential.
|
|
3497
3511
|
const shadowed = set[1] ?? null;
|
|
3512
|
+
// The pi login this token silently displaces. The worker takes the ENVIRONMENT first and reads
|
|
3513
|
+
// auth.json only when the environment holds no candidate, while pi on this host takes a stored
|
|
3514
|
+
// api_key credential FIRST (pi-ai/dist/auth/helpers.js: "a stored credential key wins"). So with both
|
|
3515
|
+
// present the job spends the token and pi on the host spends the stored key, and without this line
|
|
3516
|
+
// nothing says so. Appended only to the fix lines for a token with no other env candidate beside it:
|
|
3517
|
+
// with one, that env key is the ignored credential and the line already names it.
|
|
3518
|
+
const ignoredLogin = env.PI_AUTH_FROM_PI !== "0" && usablePiLoginKey(agentDir, provider) !== null
|
|
3519
|
+
? `; the API key in pi auth.json is NOT used while ${using} is set (the worker reads the environment first, although pi on this host would use the stored key): unset ${using} to spend the pi login, or keep it only if this token is the credential you mean jobs to spend`
|
|
3520
|
+
: "";
|
|
3521
|
+
if (kind === "bearer") {
|
|
3522
|
+
// The bearer token (ANTHROPIC_AUTH_TOKEN at the pi 0.99.1 pin, issue #509) gets the OAuth token's
|
|
3523
|
+
// treatment, a forwarded variable and a warning, and a line of its own because the hazard is a
|
|
3524
|
+
// different one: it is often a real gateway credential rather than a login that expires, but pi
|
|
3525
|
+
// reads it BEFORE the API key and sends it as `Authorization: Bearer`, so while it is set the API
|
|
3526
|
+
// key is ignored. The API-key variable is named whether or not it is set, because an operator who
|
|
3527
|
+
// sets it later without unsetting this one gets no change at all.
|
|
3528
|
+
return {
|
|
3529
|
+
ok: false,
|
|
3530
|
+
warn: true,
|
|
3531
|
+
label: `Provider key set (${provider}: ${using}) -- a bearer token, not an API key`,
|
|
3532
|
+
fix: shadowed
|
|
3533
|
+
? `unset ${using}: pi reads it BEFORE ${shadowed} and sends it as an Authorization: Bearer header, so every job spends it and ${shadowed} is ignored`
|
|
3534
|
+
: `set ${apiKeyVar} and unset ${using}: pi reads ${using} BEFORE ${apiKeyVar} and sends it as an Authorization: Bearer header, so while it is set ${apiKeyVar} is ignored${ignoredLogin}`,
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3498
3537
|
return {
|
|
3499
3538
|
ok: false,
|
|
3500
3539
|
warn: true,
|
|
3501
3540
|
label: `Provider key set (${provider}: ${using}) -- an OAuth/subscription login, not an API key`,
|
|
3502
3541
|
fix: shadowed
|
|
3503
3542
|
? `unset ${using}: pi reads it BEFORE ${shadowed}, so every job spends the subscription login and your API key is ignored`
|
|
3504
|
-
: `set ${apiKeyVar} instead -- an OAuth/subscription token expires, the container cannot refresh it, and it is not the credential for an unattended service`,
|
|
3543
|
+
: `set ${apiKeyVar} instead -- an OAuth/subscription token expires, the container cannot refresh it, and it is not the credential for an unattended service${ignoredLogin}`,
|
|
3505
3544
|
};
|
|
3506
3545
|
}
|
|
3507
3546
|
|
|
@@ -3525,6 +3564,16 @@ function providerKeyCheck({ provider, env, agentDir, oracle, nodeOk }) {
|
|
|
3525
3564
|
// itself when IT reads auth.json, but this service forwards the value into a container where it is
|
|
3526
3565
|
// read raw, so a login stored that way is not a key this deployment can spend.
|
|
3527
3566
|
if (cred?.type === "api_key" && key && !key.startsWith("!") && !key.includes("$") && key.trim()) {
|
|
3567
|
+
if (isOAuthTokenValue(provider, key)) {
|
|
3568
|
+
// The env arm's value rule, for the same reason: the worker writes this under the API key's
|
|
3569
|
+
// name and pi sends it as a subscription login anyway.
|
|
3570
|
+
return {
|
|
3571
|
+
ok: false,
|
|
3572
|
+
warn: true,
|
|
3573
|
+
label: `Provider key set (${provider}) -- from pi auth.json, but the stored key is an OAuth/subscription token, not an API key`,
|
|
3574
|
+
fix: `run \`pi login\` with a real API key for ${provider}: pi recognises the stored value as a subscription login and sends it as an Authorization: Bearer header; it expires, and the container cannot refresh it`,
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3528
3577
|
return { ok: true, label: `Provider key set (${provider}) -- from pi auth.json` };
|
|
3529
3578
|
}
|
|
3530
3579
|
if (cred?.type === "api_key" && key && (key.startsWith("!") || key.includes("$")))
|
|
@@ -3542,6 +3591,32 @@ function providerKeyCheck({ provider, env, agentDir, oracle, nodeOk }) {
|
|
|
3542
3591
|
};
|
|
3543
3592
|
}
|
|
3544
3593
|
|
|
3594
|
+
/**
|
|
3595
|
+
* pi's own test for a subscription token in an API-key slot, restated: `isOAuthToken` in
|
|
3596
|
+
* pi-ai/dist/api/anthropic-messages.js is `apiKey.includes("sk-ant-oat")`, applied to the value whatever
|
|
3597
|
+
* variable carried it. Restricted to `anthropic`, the provider whose credential this is; the value itself is
|
|
3598
|
+
* never printed.
|
|
3599
|
+
*/
|
|
3600
|
+
function isOAuthTokenValue(provider, value) {
|
|
3601
|
+
return provider === "anthropic" && typeof value === "string" && value.includes("sk-ant-oat");
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
/**
|
|
3605
|
+
* The API key a pi login holds for `provider`, when it is one the worker would forward (a non-blank string
|
|
3606
|
+
* that is not pi's command or variable-reference form), else null. The same acceptance the auth.json arm of
|
|
3607
|
+
* providerKeyCheck applies; never throws, never printed.
|
|
3608
|
+
*/
|
|
3609
|
+
function usablePiLoginKey(agentDir, provider) {
|
|
3610
|
+
let cred;
|
|
3611
|
+
try {
|
|
3612
|
+
cred = JSON.parse(readFileSync(join(agentDir, "auth.json"), "utf8"))?.[provider];
|
|
3613
|
+
} catch {
|
|
3614
|
+
return null;
|
|
3615
|
+
}
|
|
3616
|
+
const key = cred?.type === "api_key" && typeof cred.key === "string" ? cred.key : null;
|
|
3617
|
+
return key && key.trim() && !key.startsWith("!") && !key.includes("$") ? key : null;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3545
3620
|
/**
|
|
3546
3621
|
* pi reads no API-key variable for this id, and the two reasons need different fixes -- which is exactly
|
|
3547
3622
|
* the distinction `findEnvKeys`'s single `undefined` cannot make, and the reason issue #286 needed a
|
package/src/egress.mjs
CHANGED
|
@@ -40,9 +40,10 @@ import { makeDetachGate } from "./netns-keeper.mjs";
|
|
|
40
40
|
* Hence a hostname allowlist and no address rule that allows anything, which is the mechanism OQ-004's
|
|
41
41
|
* close condition actually names (the proxy's one address rule only denies this host's loopback and
|
|
42
42
|
* link-local addresses, issue #428).
|
|
43
|
-
* BUT THAT WAS MEASURED WITHOUT PI LOADED (issue #427).
|
|
44
|
-
* 8.
|
|
45
|
-
*
|
|
43
|
+
* BUT THAT WAS MEASURED WITHOUT PI LOADED (issue #427). pi depends on npm `undici` (8.5.0 at the 0.80.7
|
|
44
|
+
* pin where this was measured, 8.10.2 at the 0.99.1 pin, where loading pi was re-measured to drop the
|
|
45
|
+
* env proxy the same way), whose load replaces the global dispatcher the flag installs with one that
|
|
46
|
+
* ignores the proxy variables, so in the runner the provider call went direct and every egress-armed job died at its first
|
|
46
47
|
* turn. The runner now re-installs an env-proxy dispatcher after loading pi
|
|
47
48
|
* (image/runner/src/env-proxy.mjs), and doctor's canary takes that same path instead of a plain fetch.
|
|
48
49
|
*/
|
package/src/env-allowlist.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build the EXACT environment a job container receives. Never a pass-through.
|
|
3
3
|
*
|
|
4
|
-
* `no-broad-env-into-container` is a BLOCKER, and for good reason: `ANTHROPIC_OAUTH_TOKEN`
|
|
5
|
-
* silently outranks `ANTHROPIC_API_KEY`, so one
|
|
6
|
-
* credential every job spends, with no error and no log line.
|
|
4
|
+
* `no-broad-env-into-container` is a BLOCKER, and for good reason: `ANTHROPIC_OAUTH_TOKEN` (and, from
|
|
5
|
+
* the pi 0.99.1 pin on, `ANTHROPIC_AUTH_TOKEN` ahead of it) silently outranks `ANTHROPIC_API_KEY`, so one
|
|
6
|
+
* stray host variable would redirect which credential every job spends, with no error and no log line.
|
|
7
|
+
* So we forward a closed set.
|
|
7
8
|
*
|
|
8
9
|
* The provider key variable is DERIVED from pi's own table, not hardcoded. Deriving it means any of
|
|
9
|
-
* pi's ~
|
|
10
|
+
* pi's ~40 providers works with no code change here, and the list cannot drift when pi adds one, as a
|
|
10
11
|
* hand-copied table would. `getApiKeyEnvVars` (that table) is intentionally NOT exported by pi, which
|
|
11
12
|
* is why `providerKeyCandidates` below has to recover it a different way.
|
|
12
13
|
*
|
|
@@ -89,10 +90,12 @@ export function providerKeyCandidates(provider) {
|
|
|
89
90
|
* `getProviders` from "/compat", which is a @deprecated alias for this exact function and reaching it
|
|
90
91
|
* means loading compat's module-scope provider registration.
|
|
91
92
|
*
|
|
92
|
-
* Only ever consulted AFTER `providerKeyCandidates` comes back empty
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
93
|
+
* Only ever consulted AFTER `providerKeyCandidates` comes back empty. At the 0.80.7 pin that order was
|
|
94
|
+
* load-bearing: `radius` was a purely dynamic provider with a real key variable and no catalog entry, so
|
|
95
|
+
* asking this first would have called a working configuration unknown. At the 0.99.1 pin (issue #509)
|
|
96
|
+
* radius is a builtin provider (key variable RADIUS_API_KEY) and the catalog covers every id pi reads a key
|
|
97
|
+
* for; env-allowlist.test.mjs pins that, and the order stays because it costs nothing and is the safe one
|
|
98
|
+
* the day the two diverge again.
|
|
96
99
|
*/
|
|
97
100
|
export function piProviders() {
|
|
98
101
|
return getBuiltinProviders();
|
|
@@ -129,6 +132,24 @@ export function resolveProviderCredential({ provider, hostEnv, authFromPi = fals
|
|
|
129
132
|
const held = providerKeyCandidates(provider)
|
|
130
133
|
.map((name) => [name, hostEnv[name]])
|
|
131
134
|
.filter(([, value]) => value);
|
|
135
|
+
// EVERY held candidate is forwarded, in pi's order, including the two that are not API keys: a host
|
|
136
|
+
// ANTHROPIC_OAUTH_TOKEN and (from the 0.99.1 pin, issue #509) a host ANTHROPIC_AUTH_TOKEN, which pi reads
|
|
137
|
+
// FIRST among the variables and sends as `Authorization: Bearer`. Forwarded rather than refused, and the
|
|
138
|
+
// choice is the same one for both: the operator put the variable in the worker's environment, a gateway
|
|
139
|
+
// bearer token is a legitimate service credential, and the job env should carry what the worker's own
|
|
140
|
+
// environment says rather than a filtered version of it.
|
|
141
|
+
//
|
|
142
|
+
// That does NOT make the job agree with pi on this host in every case, and the case where it does not
|
|
143
|
+
// is stated rather than implied: the ENVIRONMENT is this function's first source and auth.json only its
|
|
144
|
+
// fallback, while pi itself takes a stored api_key credential FIRST (pi-ai/dist/auth/helpers.js, "a
|
|
145
|
+
// stored credential key wins") and the environment only without one. So with an api_key login in
|
|
146
|
+
// auth.json AND a host token set, the job spends the token while `pi` on the host would spend the stored
|
|
147
|
+
// key. Kept that way because the env-first order is this function's documented contract (its header)
|
|
148
|
+
// and the token is the operator's explicit setting; `doctor` is where it is said out loud, a warning
|
|
149
|
+
// that names the API key the token shadows and, in this case, that the auth.json key is not used while
|
|
150
|
+
// the token is set.
|
|
151
|
+
// What is NEVER done is WRITING an auth.json key under either token name: that choice is
|
|
152
|
+
// `apiKeyVariable`'s, below.
|
|
132
153
|
if (held.length > 0) {
|
|
133
154
|
return Object.fromEntries(held);
|
|
134
155
|
}
|
|
@@ -215,8 +236,8 @@ function credentialFromPiAuth(provider, agentDir, readFile, { hostEnv = {}, forw
|
|
|
215
236
|
const name = resolveEnvName(provider);
|
|
216
237
|
if (!name) {
|
|
217
238
|
// Two different facts, and they need different fixes -- the same split `doctor` makes, in the same
|
|
218
|
-
// order (candidates first, catalog second: `radius`
|
|
219
|
-
// asking membership first would
|
|
239
|
+
// order (candidates first, catalog second: at 0.80.7 `radius` had a key variable and no catalog entry,
|
|
240
|
+
// so asking membership first would have called a working configuration unknown). The old message said "set it
|
|
220
241
|
// in the worker environment manually" for BOTH, which for the first is advice `doctor` correctly
|
|
221
242
|
// calls impossible: there is no variable to set.
|
|
222
243
|
if (piProviders().includes(provider)) {
|
|
@@ -239,7 +260,7 @@ function credentialFromPiAuth(provider, agentDir, readFile, { hostEnv = {}, forw
|
|
|
239
260
|
* plus a conventional `<PROVIDER>_API_KEY`/`_KEY`. That convention is the part that drifts, and it was
|
|
240
261
|
* wrong for 13 of the 34 provider ids that have a key variable at the pin -- `google` is
|
|
241
262
|
* `GEMINI_API_KEY`, `huggingface` is `HF_TOKEN`, `moonshotai` is `MOONSHOT_API_KEY`, `github-copilot` is
|
|
242
|
-
* `COPILOT_GITHUB_TOKEN`, `radius`
|
|
263
|
+
* `COPILOT_GITHUB_TOKEN`, `radius` was `PI_GATEWAY_API_KEY` (RADIUS_API_KEY at 0.99.1) -- so for those pi recognized nothing and a
|
|
243
264
|
* valid `pi login` refused every job. The convention happening to be right for the other 21 is what let
|
|
244
265
|
* this survive: `anthropic` and `openai` are both in that set.
|
|
245
266
|
*
|
|
@@ -251,7 +272,9 @@ function credentialFromPiAuth(provider, agentDir, readFile, { hostEnv = {}, forw
|
|
|
251
272
|
* environment where every name is present -- which also closes a hermeticity hole the synthetic object
|
|
252
273
|
* had: pi's `getProviderEnvValue` falls back to the real `process.env`, so on a host that exported
|
|
253
274
|
* `ANTHROPIC_OAUTH_TOKEN` the old code resolved to THAT name. `apiKeyVariable` then picks the same
|
|
254
|
-
* variable `doctor` names, from the same module, so the two cannot diverge again.
|
|
275
|
+
* variable `doctor` names, from the same module, so the two cannot diverge again. It skips both
|
|
276
|
+
* non-API-key kinds: the OAuth token, and (from the 0.99.1 pin, issue #509) the bearer token
|
|
277
|
+
* ANTHROPIC_AUTH_TOKEN, which pi lists first and sends as `Authorization: Bearer`.
|
|
255
278
|
*/
|
|
256
279
|
function resolveEnvName(provider) {
|
|
257
280
|
return apiKeyVariable(providerKeyCandidates(provider));
|
package/src/host-pi.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* What it must never become. Nothing on the worker's BOOT path may import this file. It reads host paths and
|
|
16
16
|
* may spawn a package manager, and neither belongs anywhere near `start.mjs`.
|
|
17
17
|
*
|
|
18
|
-
* Everything here MIRRORS a private detail of the pinned pi (0.
|
|
18
|
+
* Everything here MIRRORS a private detail of the pinned pi (0.99.1) rather than calling it: pi exports no
|
|
19
19
|
* public answer to "where is this package installed" or "is this resource enabled", and importing the whole
|
|
20
20
|
* coding-agent SDK to read two well-known paths is not worth the weight. That mirroring is a real risk --
|
|
21
21
|
* pi could change the grammar and we would silently start staging something the operator turned off -- so it
|
|
@@ -66,7 +66,12 @@ export const PINNED_PI_NEEDLES = {
|
|
|
66
66
|
"function isEnabledByOverrides(filePath, patterns, baseDir) {",
|
|
67
67
|
// The npm spec split that yields a name from `@scope/name@version`.
|
|
68
68
|
"const match = spec.match(/^(@?[^@]+(?:\\/[^@]+)?)(?:@(.+))?$/);",
|
|
69
|
+
// Built-in extensions (0.99.1, issue #509) are addressed in the SAME `extensions` list as files, as
|
|
70
|
+
// `builtin:<name>` paths, and resolved in a loop of their own. The mirror ignores those entries
|
|
71
|
+
// (see isOverridePattern); these two pin that they are still a separate namespace resolved apart.
|
|
72
|
+
"const path = `${BUILTIN_PATH_PREFIX}${name}`;",
|
|
69
73
|
],
|
|
74
|
+
"dist/core/source-info.js": ['export const BUILTIN_PATH_PREFIX = "builtin:";'],
|
|
70
75
|
"dist/core/settings-manager.d.ts": [
|
|
71
76
|
"export type PackageSource = string | {",
|
|
72
77
|
" autoload?: boolean;",
|
|
@@ -148,9 +153,20 @@ export function parsePackageSource(source) {
|
|
|
148
153
|
return { kind: "git", name, spec: null, requested: null, raw };
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
/**
|
|
156
|
+
/** pi's BUILTIN_PATH_PREFIX (source-info.js): a path naming a built-in extension, never a file. */
|
|
157
|
+
const BUILTIN_PATH_PREFIX = "builtin:";
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The override patterns that can address a FILE: pi's prefixes (getOverridePatterns), minus the ones whose
|
|
161
|
+
* body is a `builtin:<name>` path. From the 0.99.1 pin on (issue #509) the same `extensions` list holds
|
|
162
|
+
* `-builtin:mcp` and friends, which pi resolves against its built-in extensions only, never against a file
|
|
163
|
+
* on disk. Kept, they would change no verdict pi reaches, but they would still make a settings file with
|
|
164
|
+
* only a builtin entry look like one with overrides, and a `!builtin:*` glob would mark every extension
|
|
165
|
+
* "could not be evaluated", which is a note about nothing. Dropped here, before any verdict is computed.
|
|
166
|
+
*/
|
|
152
167
|
function isOverridePattern(pattern) {
|
|
153
|
-
|
|
168
|
+
if (typeof pattern !== "string" || !(pattern.startsWith("!") || pattern.startsWith("+") || pattern.startsWith("-"))) return false;
|
|
169
|
+
return !normalizeExact(pattern.slice(1)).startsWith(BUILTIN_PATH_PREFIX);
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
/** Any minimatch magic we decline to interpret. Deliberately over-broad: a false "unknown" only costs a note. */
|
package/src/packages.mjs
CHANGED
|
@@ -61,7 +61,7 @@ export const NPM_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
|
61
61
|
* would add an edge to the import-pi <-> packages cycle that only survives because both sides use their
|
|
62
62
|
* bindings at call time.
|
|
63
63
|
*
|
|
64
|
-
* This list is pi's, not ours: at the 0.80.7
|
|
64
|
+
* This list is pi's, not ours: at the 0.99.1 pin (as at 0.80.7) `collectPackageResources` falls through to exactly these
|
|
65
65
|
* four directory names when `readPiManifest` returns null, so a package with no `pi` key and a `skills/`
|
|
66
66
|
* dir IS a pi package. See host-pi.mjs's PINNED_PI_NEEDLES for the assertion that keeps that true.
|
|
67
67
|
*/
|
|
@@ -281,7 +281,7 @@ export function readStageManifest({ globalPiDir, readFile = readFileSync, fileEx
|
|
|
281
281
|
* readStageManifest's policy, because the consumers are advisory (doctor's per-trigger flow lines,
|
|
282
282
|
* and issue #188's topology) and a half-staged tree must degrade to "nothing visible", not a crash.
|
|
283
283
|
*
|
|
284
|
-
* The semantics mirror pi's collectPackageResources at the 0.
|
|
284
|
+
* The semantics mirror pi's collectPackageResources at the 0.99.1 pin EXACTLY (unchanged since 0.80.7), because an enumerator
|
|
285
285
|
* that agrees with pi by hand is how doctor comes to report a tier pi then ignores:
|
|
286
286
|
* - a `pi` manifest object means its `skills` entries are the ONLY sources -- a manifest WITHOUT a
|
|
287
287
|
* `skills` key contributes NO skills and gets NO convention fallback (readPiManifest short-circuits
|
package/src/pricing.mjs
CHANGED
|
@@ -29,8 +29,10 @@ import { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "@earendi
|
|
|
29
29
|
/**
|
|
30
30
|
* Every builtin pi-ai model, flattened to `{ provider, id, cost }`. `cost` is pi-ai's ModelCost
|
|
31
31
|
* object passed BY REFERENCE -- it is pi-ai's data, callers treat it read-only. Rates are USD per
|
|
32
|
-
* 1M tokens
|
|
33
|
-
*
|
|
32
|
+
* 1M tokens. An all-zero table is pi-ai's data, not missing data: at the 0.99.1 pin the all-zero
|
|
33
|
+
* providers include the qwen-token-plan family. It is NOT a subscription signal any more (issue #509):
|
|
34
|
+
* kimi-coding and zai-coding-cn, all-zero at 0.80.7, now carry implied API-equivalent rates on most
|
|
35
|
+
* models although their plans are prepaid, and plenty of pay-per-token providers list a free model.
|
|
34
36
|
*/
|
|
35
37
|
export function listPricedModels() {
|
|
36
38
|
const out = [];
|
|
@@ -58,9 +60,11 @@ export function getPricedModel(provider, id) {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
|
-
* True when all four base rates are zero
|
|
62
|
-
*
|
|
63
|
-
*
|
|
63
|
+
* True when all four base rates are zero: pi-ai rated the model at $0, which is a fact about the table
|
|
64
|
+
* and nothing more. It USED to be read as the signature of a subscription-backed provider; at the 0.99.1
|
|
65
|
+
* pin it is not (issue #509: kimi-coding and zai-coding-cn carry implied prices, and free models of
|
|
66
|
+
* metered providers are zero too), so a subscription is known only from subscriptions.json. Tiers are
|
|
67
|
+
* deliberately ignored: a zero-rate provider ships no tiers, and a priced provider
|
|
64
68
|
* with a zero base rate somewhere does not become "free" by it. Null/malformed input is false --
|
|
65
69
|
* "not zero-rated" is the safe answer for a thing that is not a model.
|
|
66
70
|
*/
|
package/src/processor.mjs
CHANGED
|
@@ -766,9 +766,10 @@ export async function runJob(job, deps) {
|
|
|
766
766
|
// - the worker WRITES the name: buildContainerEnv assigns the provider credential and
|
|
767
767
|
// PI_FORWARD_ENV before this feature's values, so the trigger's value replaces the operator's
|
|
768
768
|
// and every job of that trigger spends the trigger author's key;
|
|
769
|
-
// - the worker does NOT write the name but pi READS it first: the OAuth token variable
|
|
770
|
-
//
|
|
771
|
-
//
|
|
769
|
+
// - the worker does NOT write the name but pi READS it first: the OAuth token variable, and from
|
|
770
|
+
// the 0.99.1 pin the bearer ANTHROPIC_AUTH_TOKEN (issue #509), are deliberately never written
|
|
771
|
+
// (apiKeyVariable skips both), so a trigger binding one lands beside the operator's key and
|
|
772
|
+
// outranks it in pi's own precedence.
|
|
772
773
|
// The old message asserted the first for both, which is exactly backwards for the second.
|
|
773
774
|
await comment(job, `Refused: this trigger's \`run.secrets\` binds \`${resolved.reserved}\`, which is a variable this deployment already uses for the job's own credentials. Whichever of the two values reached the container, one of them would be silently ignored. Rename it in the triggers file. Not run.`);
|
|
774
775
|
// The variable NAME only. It is the operator's own choice of name, not payload, and naming it is what
|
package/src/provider-key.mjs
CHANGED
|
@@ -27,15 +27,40 @@
|
|
|
27
27
|
// worker/test/provider-key.test.mjs -- never against a second copy of a table.
|
|
28
28
|
export const OAUTH_KEY_RE = /_OAUTH_TOKEN$/;
|
|
29
29
|
|
|
30
|
+
// The second fact of the same kind, added with the pi 0.99.1 bump (issue #509): a variable pi reads for a
|
|
31
|
+
// provider that is NOT an API key, because pi sends it as a different header. At 0.99.1 pi lists
|
|
32
|
+
// ANTHROPIC_AUTH_TOKEN FIRST for `anthropic` (pi-ai/dist/env-api-keys.js getApiKeyEnvVars), and its
|
|
33
|
+
// resolver (pi-ai/dist/providers/anthropic.js) sends it as `Authorization: Bearer <value>` AHEAD of both
|
|
34
|
+
// ANTHROPIC_OAUTH_TOKEN and ANTHROPIC_API_KEY. Without this rule `apiKeyVariable` picked it: an auth.json
|
|
35
|
+
// API key would have been written into the container under the bearer name, so pi would send it as
|
|
36
|
+
// `Authorization: Bearer` rather than as the `x-api-key` an API key travels in, and doctor would have told
|
|
37
|
+
// an operator to set the bearer variable.
|
|
38
|
+
// pi's own `getEnvApiKey` skips it for the same reason, which is the evidence that the distinction is real
|
|
39
|
+
// rather than ours. A suffix rule, for OAUTH_KEY_RE's reason: a set naming one variable would silently
|
|
40
|
+
// bless the next provider's bearer variable. Pinned against pi in worker/test/provider-key.test.mjs.
|
|
41
|
+
export const BEARER_KEY_RE = /_AUTH_TOKEN$/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Why a variable pi reads is not an API key, or null when it is one. The two answers need different
|
|
45
|
+
* advice, so the caller is told which: an OAuth token is a subscription login that expires, a bearer token
|
|
46
|
+
* is a credential pi sends in another header and reads BEFORE the API key.
|
|
47
|
+
*/
|
|
48
|
+
export function nonApiKeyKind(name) {
|
|
49
|
+
if (OAUTH_KEY_RE.test(name)) return "oauth";
|
|
50
|
+
if (BEARER_KEY_RE.test(name)) return "bearer";
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
/**
|
|
31
55
|
* The api-key variable to write and to name, given pi's candidate list for a provider in pi's own
|
|
32
|
-
* precedence order. Never the OAuth token, whatever that precedence says: pi
|
|
33
|
-
* ANTHROPIC_OAUTH_TOKEN
|
|
34
|
-
* and an api-key credential written under
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
56
|
+
* precedence order. Never the OAuth token and never the bearer token, whatever that precedence says: pi
|
|
57
|
+
* returns ANTHROPIC_AUTH_TOKEN then ANTHROPIC_OAUTH_TOKEN before ANTHROPIC_API_KEY, "set your subscription
|
|
58
|
+
* login" is wrong advice for an unattended service, and an api-key credential written under either name
|
|
59
|
+
* would be a value whose variable lies about what it is (under the bearer name pi would also send it in
|
|
60
|
+
* the wrong header). Falls back to the first candidate only for a provider with no API-key variable at
|
|
61
|
+
* all, which is no provider pi has today; `null` for a provider pi reads no key variable for, which is the
|
|
62
|
+
* caller's cue to refuse rather than to guess.
|
|
38
63
|
*/
|
|
39
64
|
export function apiKeyVariable(candidates) {
|
|
40
|
-
return candidates.find((name) =>
|
|
65
|
+
return candidates.find((name) => nonApiKeyKind(name) === null) ?? candidates[0] ?? null;
|
|
41
66
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* nothing that says where the request goes, and that is the worse of the two: substitution spends the
|
|
8
8
|
* trigger author's money, redirection sends the OPERATOR'S credential to a host the trigger chose.
|
|
9
9
|
*
|
|
10
|
-
* Measured against the 0.80.7 pin with a stubbed fetch and no real key, three providers, three routes
|
|
10
|
+
* Measured against the 0.80.7 pin with a stubbed fetch and no real key, three providers, three routes (the
|
|
11
|
+
* derivation below was re-run at the 0.99.1 pin, issue #509):
|
|
11
12
|
* AZURE_OPENAI_BASE_URL -> the request goes to the named host carrying `api-key`
|
|
12
13
|
* GOOGLE_GEMINI_BASE_URL -> ... carrying `x-goog-api-key`
|
|
13
14
|
* AWS_ENDPOINT_URL -> ... carrying a SigV4 signature over the operator's Bedrock credential
|
|
@@ -20,10 +21,11 @@
|
|
|
20
21
|
* resolved provider, and the bound that gate keeps is deliberate and documented -- an `anthropic` job may
|
|
21
22
|
* bind `OPENAI_API_KEY` for a flow that talks to OpenAI itself, because refusing it would be this project
|
|
22
23
|
* claiming a namespace it does not own. Including them here would have broken that bound ARBITRARILY: only
|
|
23
|
-
*
|
|
24
|
-
* `OPENAI_API_KEY` would refuse while `GROQ_API_KEY` and `HF_TOKEN` stayed bindable, and the
|
|
25
|
-
* rule would be false for reasons no operator could predict. They are subtracted, and the bolt
|
|
26
|
-
* them the same way rather than by hand.
|
|
24
|
+
* five of pi's thirty-eight key variables (at the 0.99.1 pin) happen to appear as literals in a scanned
|
|
25
|
+
* artifact, so `OPENAI_API_KEY` would refuse while `GROQ_API_KEY` and `HF_TOKEN` stayed bindable, and the
|
|
26
|
+
* documented rule would be false for reasons no operator could predict. They are subtracted, and the bolt
|
|
27
|
+
* subtracts them the same way rather than by hand. ONE is kept, by name and for a stated reason, in
|
|
28
|
+
* `RETAINED_KEY_VARIABLES` below.
|
|
27
29
|
* The AWS credential variables STAY, and they are not an exception: pi's key table has no entry for
|
|
28
30
|
* `amazon-bedrock` at all (`providerKeyCandidates("amazon-bedrock")` is empty), so nothing else reserves
|
|
29
31
|
* them and they are read by the SDK as provider configuration, which is exactly what this set is.
|
|
@@ -54,6 +56,13 @@
|
|
|
54
56
|
* They subvert any provider call, but they are properties of the RUNTIME rather than of a provider, they
|
|
55
57
|
* predate this gate, and `NODE_OPTIONS` additionally needs a file the attacker can place. They belong to
|
|
56
58
|
* whatever closes the runtime-hijack question, not to a set derived from what pi reads about providers.
|
|
59
|
+
* The same holds for `HOME`, `PATH`, `APPDATA`, `USERPROFILE` and `XDG_CONFIG_HOME`, which the scan DOES
|
|
60
|
+
* reach from the 0.99.1 pin on (issue #509): the Anthropic SDK reads the four directory variables only to
|
|
61
|
+
* locate its default config directory (`core/credentials`), and `PATH` inside its agent toolset, which pi
|
|
62
|
+
* does not use. The bolt subtracts them by name, with that reason, and asserts the scan still finds each,
|
|
63
|
+
* so a subtraction that stopped being needed is removed rather than carried. `HOME` is reserved anyway,
|
|
64
|
+
* by `reserved-env.mjs`, because the worker writes it (issue #341). The Anthropic-specific switch for the
|
|
65
|
+
* same directory, `ANTHROPIC_CONFIG_DIR`, IS in the set: it names a credential location outright.
|
|
57
66
|
*
|
|
58
67
|
* And a bound on the whole family, which no document here stated before: a trigger author picks the
|
|
59
68
|
* variable NAME and a vault REFERENCE, never a value. Exploiting any of these needs the operator's own
|
|
@@ -86,6 +95,20 @@
|
|
|
86
95
|
* `https_proxy` outranks the egress policy's own variable in pi's reader. Only the schemes a provider call
|
|
87
96
|
* can use are listed: `ws_proxy` and the rest are reachable in `getProxyEnv` but not from an HTTPS request.
|
|
88
97
|
*/
|
|
98
|
+
/**
|
|
99
|
+
* Provider KEY variables that stay reserved here although the bolt subtracts key variables in general.
|
|
100
|
+
*
|
|
101
|
+
* `ANTHROPIC_AUTH_TOKEN` was in this set from issue #314 on, because at 0.80.7 it was read only by the
|
|
102
|
+
* Anthropic SDK and was no key variable of pi's. At the 0.99.1 pin pi lists it FIRST among anthropic's key
|
|
103
|
+
* variables and sends it as `Authorization: Bearer` ahead of the API key (issue #509), which would move it
|
|
104
|
+
* to the per-provider pre-spend gate and make it bindable again for every non-anthropic job. Kept instead,
|
|
105
|
+
* because a version bump must not widen what a trigger may bind without an operator deciding it, and no
|
|
106
|
+
* deployment can be relying on binding a name that has been refused at load since #314. The bolt asserts it
|
|
107
|
+
* is still both a key variable pi reads and a name the scan finds, so the day either stops holding this
|
|
108
|
+
* list is revisited rather than carried.
|
|
109
|
+
*/
|
|
110
|
+
const RETAINED_KEY_VARIABLES = ["ANTHROPIC_AUTH_TOKEN"];
|
|
111
|
+
|
|
89
112
|
const UNREACHABLE_BY_SCAN = [
|
|
90
113
|
"AWS_CONFIG_FILE",
|
|
91
114
|
"AWS_ENDPOINT_URL",
|
|
@@ -99,11 +122,27 @@ const UNREACHABLE_BY_SCAN = [
|
|
|
99
122
|
];
|
|
100
123
|
|
|
101
124
|
export const PROVIDER_STEERING_VARS = new Set([
|
|
102
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
103
125
|
"ANTHROPIC_BASE_URL",
|
|
126
|
+
"ANTHROPIC_CONFIG_DIR",
|
|
127
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
128
|
+
"ANTHROPIC_ENVIRONMENT_ID",
|
|
129
|
+
"ANTHROPIC_ENVIRONMENT_KEY",
|
|
130
|
+
"ANTHROPIC_FEDERATION_RULE_ID",
|
|
131
|
+
"ANTHROPIC_IDENTITY_TOKEN",
|
|
132
|
+
"ANTHROPIC_IDENTITY_TOKEN_FILE",
|
|
104
133
|
"ANTHROPIC_LOG",
|
|
134
|
+
"ANTHROPIC_ORGANIZATION_ID",
|
|
135
|
+
"ANTHROPIC_PROFILE",
|
|
136
|
+
"ANTHROPIC_SCOPE",
|
|
137
|
+
"ANTHROPIC_SERVICE_ACCOUNT_ID",
|
|
138
|
+
"ANTHROPIC_SESSION_ID",
|
|
139
|
+
"ANTHROPIC_WEBHOOK_SIGNING_KEY",
|
|
140
|
+
"ANTHROPIC_WORKSPACE_ID",
|
|
141
|
+
"ANTHROPIC_WORK_ID",
|
|
142
|
+
"ANTHROPIC_WORK_SECRET",
|
|
105
143
|
"AWS_ACCESS_KEY_ID",
|
|
106
144
|
"AWS_BEARER_TOKEN_BEDROCK",
|
|
145
|
+
"AWS_BEDROCK_BASE_URL",
|
|
107
146
|
"AWS_BEDROCK_FORCE_CACHE",
|
|
108
147
|
"AWS_BEDROCK_FORCE_HTTP1",
|
|
109
148
|
"AWS_BEDROCK_SKIP_AUTH",
|
|
@@ -121,8 +160,6 @@ export const PROVIDER_STEERING_VARS = new Set([
|
|
|
121
160
|
"AZURE_OPENAI_ENDPOINT",
|
|
122
161
|
"AZURE_OPENAI_RESOURCE_NAME",
|
|
123
162
|
"GCLOUD_PROJECT",
|
|
124
|
-
"GEMINI_NEXT_GEN_API_BASE_URL",
|
|
125
|
-
"GEMINI_NEXT_GEN_API_LOG",
|
|
126
163
|
"GOOGLE_API_KEY",
|
|
127
164
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
128
165
|
"GOOGLE_CLOUD_LOCATION",
|
|
@@ -131,14 +168,18 @@ export const PROVIDER_STEERING_VARS = new Set([
|
|
|
131
168
|
"GOOGLE_GENAI_USE_ENTERPRISE",
|
|
132
169
|
"GOOGLE_GENAI_USE_VERTEXAI",
|
|
133
170
|
"GOOGLE_VERTEX_BASE_URL",
|
|
171
|
+
"KIMI_CODE_OAUTH_HOST",
|
|
172
|
+
"KIMI_OAUTH_HOST",
|
|
173
|
+
"OPENAI_ADMIN_KEY",
|
|
134
174
|
"OPENAI_API_VERSION",
|
|
135
175
|
"OPENAI_BASE_URL",
|
|
176
|
+
"OPENAI_CUSTOM_HEADERS",
|
|
136
177
|
"OPENAI_LOG",
|
|
137
178
|
"OPENAI_ORG_ID",
|
|
138
179
|
"OPENAI_PROJECT_ID",
|
|
139
180
|
"OPENAI_WEBHOOK_SECRET",
|
|
140
181
|
"PI_CACHE_RETENTION",
|
|
141
|
-
"PI_GATEWAY",
|
|
142
182
|
"PI_OAUTH_CALLBACK_HOST",
|
|
183
|
+
...RETAINED_KEY_VARIABLES,
|
|
143
184
|
...UNREACHABLE_BY_SCAN,
|
|
144
185
|
]);
|
package/src/secrets.mjs
CHANGED
|
@@ -298,7 +298,8 @@ export function makeSecretsResolver({
|
|
|
298
298
|
// prevent. Same conflated `undefined` as issue #286, one module over.
|
|
299
299
|
//
|
|
300
300
|
// It also closes a second hole that never needed auth.json. The worker never WRITES the OAuth variable
|
|
301
|
-
// (
|
|
301
|
+
// or (from the 0.99.1 pin, issue #509) the bearer ANTHROPIC_AUTH_TOKEN (`apiKeyVariable` skips both
|
|
302
|
+
// deliberately) and pi reads both BEFORE `ANTHROPIC_API_KEY`, so a presence
|
|
302
303
|
// filter held it only on hosts that happened to export it, and a trigger binding it outranked the
|
|
303
304
|
// operator's own key on the pure-env path too.
|
|
304
305
|
//
|
package/src/subscriptions.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Operator-declared subscription plans (issue #53): the one place a flat-rate plan's real price can be
|
|
3
|
-
* stated.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* stated. pi-ai's rate table for a subscription-backed provider does not state the plan's price: at 0.80.7
|
|
4
|
+
* kimi-coding and zai-coding-cn shipped all-zero tables, so their runs recorded cost 0 and read as FREE
|
|
5
|
+
* when they were PREPAID; at the 0.99.1 pin most of their models carry an implied API-equivalent rate
|
|
6
|
+
* instead (issue #509), so the same prepaid runs now record a POSITIVE cost and read as METERED spend.
|
|
7
|
+
* Either way the table is the wrong price, and a zero rate is no longer even a hint that a plan exists
|
|
8
|
+
* (the qwen-token-plan family is all-zero, and so are free models of metered providers). And the env
|
|
9
|
+
* boundary REFUSES OAuth/subscription logins on purpose (env-allowlist.mjs: an expiring token cannot power an
|
|
6
10
|
* unattended service), so no credential ever reaches the worker that could name the plan. An operator-side
|
|
7
11
|
* declaration is therefore the only honest price source, and `subscriptions.json` is that declaration.
|
|
8
12
|
*
|
package/src/triggers.mjs
CHANGED
|
@@ -167,7 +167,10 @@ export const REVIEW_STATES = new Set(["approved", "changes_requested", "commente
|
|
|
167
167
|
const REVIEW_ACTION = "review_submitted";
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
* The pi tool names a trigger may exclude (issue #291) -- the built-in set of the PINNED pi, 0.
|
|
170
|
+
* The pi tool names a trigger may exclude (issue #291) -- the built-in set of the PINNED pi, 0.99.1, in pi's
|
|
171
|
+
* own `allToolNames` order. `powershell` joined it at the 0.99.1 bump (issue #509): pi registers it as a
|
|
172
|
+
* built-in but does not activate it by default (`DEFAULT_TOOL_NAMES` is read, bash, edit, write), so a
|
|
173
|
+
* trigger that means "no shell" names it beside `bash`.
|
|
171
174
|
* Hand-written because this validator is pure and pi-free (the worker does not depend on the agent
|
|
172
175
|
* package), and therefore BOLTED twice to the artifact it restates: `worker/test/exclude-tools.pinned.test.mjs`
|
|
173
176
|
* and `image/runner/test/pinned-api.test.mjs` both derive the set from the pinned package and fail with a
|
|
@@ -177,7 +180,7 @@ const REVIEW_ACTION = "review_submitted";
|
|
|
177
180
|
* validation exists to close -- pi ignores unknown names in `excludeTools` without a diagnostic.
|
|
178
181
|
*/
|
|
179
182
|
// EXPORTED for the two pinned-set bolts and for the admin, which states the vocabulary to an operator.
|
|
180
|
-
export const EXCLUDABLE_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
183
|
+
export const EXCLUDABLE_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
|
|
181
184
|
|
|
182
185
|
// A cron id flows into BullMQ's deterministic `repeat:<id>:<nextMillis>` jobId, so a `:` corrupts that
|
|
183
186
|
// parse; the charset also excludes `:` and the dedicated check names the reason.
|