@edgehero/pi-dispatch 2.0.0 → 3.0.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 +44 -7
- package/README.md +14 -6
- package/deploy/com.pi-dispatch.worker.plist +1 -1
- package/deploy/docker-compose.yml +12 -0
- package/deploy/egress-proxy.conf +28 -3
- package/deploy/pi-dispatch-egress-proxy.container +8 -2
- package/deploy/worker-env-wrapper.cmd +1 -1
- package/deploy/worker-env-wrapper.sh +3 -3
- package/package.json +9 -2
- package/src/allocation.mjs +731 -0
- package/src/backends.mjs +243 -0
- package/src/budget.mjs +40 -4
- package/src/cli.mjs +222 -11
- package/src/config.mjs +126 -5
- package/src/daemon-facts.mjs +3 -0
- package/src/deployment-venue.mjs +1 -0
- package/src/doctor.mjs +2316 -183
- package/src/dollar-budget.mjs +373 -0
- package/src/dollar-fingerprint.mjs +83 -0
- package/src/egress-cli.mjs +316 -0
- package/src/egress-proxy-state.mjs +35 -5
- package/src/egress.mjs +16 -3
- package/src/env-allowlist.mjs +142 -18
- package/src/env-file.mjs +194 -25
- package/src/envelope.mjs +413 -0
- package/src/exit-code.mjs +22 -0
- package/src/fleet-lease.mjs +85 -25
- package/src/get-token.mjs +16 -5
- package/src/git-dirty.mjs +67 -0
- package/src/github-app-setup.mjs +6 -3
- package/src/github-host.mjs +5 -3
- package/src/host-pi.mjs +19 -3
- package/src/identity.mjs +2 -1
- package/src/image-preflight.mjs +98 -24
- package/src/image-ref.mjs +37 -0
- package/src/import-pi.mjs +4 -2
- package/src/index.mjs +407 -62
- package/src/init.mjs +18 -0
- package/src/job-id.mjs +26 -3
- package/src/live-probes.mjs +24 -9
- package/src/model-catalog.mjs +297 -0
- package/src/model-endpoints.mjs +649 -0
- package/src/model-ref.mjs +151 -0
- package/src/models-json.mjs +262 -0
- package/src/money.mjs +144 -0
- package/src/octokit-log.mjs +65 -0
- package/src/outbox-plan.mjs +218 -0
- package/src/outbox.mjs +29 -9
- package/src/output-cap.mjs +157 -0
- package/src/packages.mjs +2 -2
- package/src/pause-windows.mjs +81 -2
- package/src/pi-model-loader.mjs +77 -0
- package/src/podman-stack.mjs +16 -3
- package/src/portfolio-snapshot.mjs +304 -0
- package/src/prepare-local.mjs +247 -12
- package/src/prepare.mjs +35 -3
- package/src/pricing.mjs +9 -5
- package/src/priorities.mjs +569 -0
- package/src/processor.mjs +603 -173
- package/src/project-id.mjs +17 -0
- package/src/projects.mjs +238 -0
- package/src/provider-key.mjs +32 -7
- package/src/provider-steering.mjs +214 -59
- package/src/queue.mjs +111 -6
- package/src/reserved-env.mjs +30 -0
- package/src/run-container.mjs +59 -5
- package/src/run-history.mjs +379 -24
- package/src/run-mirror.mjs +30 -0
- package/src/runtime-settings.mjs +104 -9
- package/src/schedules.mjs +33 -1
- package/src/scoped-limits.mjs +447 -27
- package/src/secrets.mjs +2 -1
- package/src/service.mjs +15 -4
- package/src/session-store.mjs +131 -6
- package/src/start.mjs +528 -40
- package/src/subscriptions.mjs +7 -3
- package/src/triggers-file.mjs +65 -4
- package/src/triggers.mjs +140 -9
- package/src/up.mjs +308 -34
- package/src/valkey-endpoint.mjs +3 -2
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The environment variables pi and
|
|
3
|
-
*
|
|
2
|
+
* The environment variables pi and the packages it runs read to CONFIGURE a provider or pi itself: where the
|
|
3
|
+
* request goes, which credentials it carries, and where pi reads its own configuration (issues #314, #511).
|
|
4
4
|
*
|
|
5
5
|
* WHY THIS IS A REFUSAL. `run.secrets` lets a trigger name an environment variable, and the existing gates
|
|
6
6
|
* refuse the names the worker writes and the ones pi reads the resolved provider's KEY from. They refuse
|
|
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,46 +21,97 @@
|
|
|
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
|
+
* six of pi's thirty-eight key variables (at the 0.99.1 pin) happen to be read by name 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.
|
|
30
32
|
*
|
|
31
33
|
* DERIVED, AND THE DERIVATION IS BOLTED IN BOTH DIRECTIONS by `worker/test/provider-steering.test.mjs`,
|
|
32
|
-
* from the pinned artifacts rather than from a second copy of them
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
34
|
+
* from the pinned artifacts rather than from a second copy of them. Every name the scan finds is in, with
|
|
35
|
+
* no judgement about which ones matter; the only subtractions are named and asserted there.
|
|
36
|
+
*
|
|
37
|
+
* TWO HOPS, THROUGH DECLARED DEPENDENCIES. Hop 1 is every package pi-ai's own dist imports, discovered
|
|
38
|
+
* from its import statements rather than listed, so a pi bump that adds an SDK fails the bolt. Hop 2 is,
|
|
39
|
+
* for each of those, the packages its sources import AND its package.json declares in `dependencies`,
|
|
40
|
+
* which is where google-auth-library, `@smithy/core` and the AWS credential chain are. The declared-
|
|
41
|
+
* dependency filter keeps out an optional peer that resolves only by an accident of layout (`openai`
|
|
42
|
+
* imports `undici` without declaring it).
|
|
43
|
+
*
|
|
44
|
+
* EVERY OCCURRENCE COUNTED, not a list of accessor spellings (a list kept missing the next one). After
|
|
45
|
+
* stripping comments, the bolt counts every occurrence that can reach the environment in every scanned
|
|
46
|
+
* file: each `env` token (so `process.env`, `ctx.env`, a parameter named `env`, `{ env: e } = process`),
|
|
47
|
+
* each `process["env"]`, each use of an alias of one (`const v = env()`, `const e = process.env`), and
|
|
48
|
+
* each call of a helper. Helpers are DERIVED: any named function one of whose own parameters is the key of
|
|
49
|
+
* an environment read (`resolveEnvConfigValue(name, env)`), so a new call of an existing helper counts. An occurrence either NAMES a variable (`.X`, `["X"]`, a key
|
|
50
|
+
* resolved through a string constant, a call `("X")`, `"X" in env`, `{ X } = env`, a selector argument
|
|
51
|
+
* beside its key) or it is a SITE, and the bolt pins every file's sites with a COUNT
|
|
52
|
+
* (`worker/test/fixtures/provider-steering-sites.json`). So a new occurrence anywhere either names
|
|
53
|
+
* something the equality sees or changes a count: a new helper reading `process.env[name]`, a key built
|
|
54
|
+
* at runtime, a new alias. A write is a site, not a read.
|
|
55
|
+
* The rule starts from an `env` token, so a form with none is NOT seen: `process["e" + "nv"]`,
|
|
56
|
+
* `const E = "env"; process[E]`, `{ ["env"]: e } = process`, `Reflect.get(process, "env")`,
|
|
57
|
+
* `require("process")["env"]`. None occurs in the pinned sources.
|
|
58
|
+
*
|
|
59
|
+
* BOTH COPIES. The hoisted pi-ai this workspace resolves, and the one the runner dispatches through,
|
|
60
|
+
* nested under pi-coding-agent with its own google-auth-library (10.6.2 there, 10.9.1 hoisted). The union
|
|
61
|
+
* of the two is reserved.
|
|
62
|
+
*
|
|
63
|
+
* LOWERCASE TWINS ARE LITERAL MEMBERS, and matching stays EXACT. google-auth-library reads
|
|
64
|
+
* `google_application_credentials`, `gcloud_project` and `google_cloud_project` beside the uppercase
|
|
65
|
+
* forms, so the scan finds them as reads and they are in. Folding case instead would refuse names no
|
|
66
|
+
* pinned source reads (`openai_base_url`, `anthropic_api_key`), which is the other half of the rule: an
|
|
67
|
+
* operator's own name stays bindable unless something pinned reads it.
|
|
68
|
+
*
|
|
69
|
+
* PI'S OWN NAMESPACE, in `PI_OWN_READS` below. Every `PI_*` name pi-coding-agent's dist and the pi
|
|
70
|
+
* packages it declares read (pi-tui runs in the same process). The agent and session directory keys are
|
|
71
|
+
* built at runtime from pi's app name, so the bolt imports pi's `config.js` to evaluate them. Two sets are
|
|
72
|
+
* subtracted: the names the worker writes into the container (`CONTAINER_ENV_NAMES`, reserved already),
|
|
73
|
+
* and the ones the runner assigns before pi runs, derived from `image/runner/src` (`PI_OFFLINE`,
|
|
74
|
+
* `PI_TELEMETRY`). `dist/bundle/`, the vendored single-file build, is not the code the runner loads and is
|
|
75
|
+
* not scanned.
|
|
76
|
+
*
|
|
77
|
+
* THE LIMITS, stated because a set like this is only worth what its boundary is honest about.
|
|
78
|
+
*
|
|
79
|
+
* Hop 3 is not followed, and these are read there and NOT reserved: in gcp-metadata (under
|
|
80
|
+
* google-auth-library) `GCE_METADATA_HOST`, `GCE_METADATA_IP`, `METADATA_SERVER_DETECTION` and `K_SERVICE`;
|
|
81
|
+
* in the AWS credential providers (under `@aws-sdk/credential-provider-node`) `AWS_ROLE_ARN` and
|
|
82
|
+
* `AWS_ROLE_SESSION_NAME` (web identity), `AWS_CONTAINER_AUTHORIZATION_TOKEN` and its `_FILE` form (http,
|
|
83
|
+
* useless without the reserved `AWS_CONTAINER_CREDENTIALS_FULL_URI`), and `AWS_ACCOUNT_ID`,
|
|
84
|
+
* `AWS_CREDENTIAL_SCOPE` and `AWS_CREDENTIAL_EXPIRATION` (env). The metadata host is the sharpest of them:
|
|
85
|
+
* it moves where google-auth-library asks for a token when no other credential is configured.
|
|
86
|
+
*
|
|
87
|
+
* pi-coding-agent's own third-party dependencies are not scanned either (undici, jiti, yaml, semver,
|
|
88
|
+
* cross-spawn, chalk and the rest). At the 0.99.1 pin they read the proxy variables (reserved by the egress
|
|
89
|
+
* policy or listed below) and tooling switches: jiti's `NODE_DEBUG` and Babel flags, chalk's
|
|
90
|
+
* `FORCE_COLOR`, yaml's `LOG_TOKENS`.
|
|
91
|
+
*
|
|
92
|
+
* Names pi reads outside its `PI_*` namespace are found and left out, and the bolt pins the list: the
|
|
93
|
+
* terminal, the OS, the editor, and the `llama.cpp` extension's `LLAMA_BASE_URL`, which pi reads both
|
|
94
|
+
* through `ctx.env` and through `process.env`. The worker cannot dispatch to that provider, and the bolt
|
|
95
|
+
* asserts it still cannot.
|
|
52
96
|
*
|
|
53
97
|
* `NODE_OPTIONS`, `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE` and `NODE_TLS_REJECT_UNAUTHORIZED` are NOT here.
|
|
54
98
|
* They subvert any provider call, but they are properties of the RUNTIME rather than of a provider, they
|
|
55
99
|
* predate this gate, and `NODE_OPTIONS` additionally needs a file the attacker can place. They belong to
|
|
56
100
|
* whatever closes the runtime-hijack question, not to a set derived from what pi reads about providers.
|
|
101
|
+
* The same holds for `HOME`, `PATH`, `APPDATA`, `USERPROFILE` and `XDG_CONFIG_HOME`, which the scan DOES
|
|
102
|
+
* reach: the Anthropic SDK reads the four directory variables only to locate its default config directory
|
|
103
|
+
* (`core/credentials`), and `PATH` inside its agent toolset, which pi does not use. `HOMEDRIVE` and
|
|
104
|
+
* `HOMEPATH` join them (issue #511): `@smithy/core` reads them in the same home-directory helper. The bolt subtracts them
|
|
105
|
+
* by name, with that reason, and asserts the scan still finds each, so a subtraction that stopped being
|
|
106
|
+
* needed is removed rather than carried. `HOME` is reserved anyway, by `reserved-env.mjs`, because the
|
|
107
|
+
* worker writes it (issue #341). The Anthropic-specific switch for the same directory,
|
|
108
|
+
* `ANTHROPIC_CONFIG_DIR`, IS in the set: it names a credential location outright.
|
|
57
109
|
*
|
|
58
|
-
* And a bound on the whole family
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
110
|
+
* And a bound on the whole family: a trigger author picks the variable NAME and a vault REFERENCE, never a
|
|
111
|
+
* value. Exploiting any of these needs the operator's own vault to hold a useful string at a reference the
|
|
112
|
+
* author is allowed to name. That is equally true of `AZURE_OPENAI_BASE_URL`, the variable #314 was filed
|
|
113
|
+
* about, so it bounds the severity of the whole set rather than distinguishing parts of it. An operator
|
|
114
|
+
* who needs one of these names in a job puts it on `PI_FORWARD_ENV`, the operator's own list.
|
|
63
115
|
*
|
|
64
116
|
* IMPORT-FREE, like `reserved-env.mjs` and `provider-key.mjs` beside it: `triggers.mjs` is the shared
|
|
65
117
|
* validator, the receiver loads it, and `admin/build.mjs` inlines it into the published console.
|
|
@@ -67,78 +119,181 @@
|
|
|
67
119
|
|
|
68
120
|
|
|
69
121
|
/**
|
|
70
|
-
*
|
|
122
|
+
* Provider KEY variables that stay reserved here although the bolt subtracts key variables in general.
|
|
123
|
+
*
|
|
124
|
+
* `ANTHROPIC_AUTH_TOKEN` was in this set from issue #314 on, because at 0.80.7 it was read only by the
|
|
125
|
+
* Anthropic SDK and was no key variable of pi's. At the 0.99.1 pin pi lists it FIRST among anthropic's key
|
|
126
|
+
* variables and sends it as `Authorization: Bearer` ahead of the API key (issue #509), which would move it
|
|
127
|
+
* to the per-provider pre-spend gate and make it bindable again for every non-anthropic job. Kept instead,
|
|
128
|
+
* because a version bump must not widen what a trigger may bind without an operator deciding it, and no
|
|
129
|
+
* deployment can be relying on binding a name that has been refused at load since #314. The bolt asserts it
|
|
130
|
+
* is still both a key variable pi reads and a name the scan finds, so the day either stops holding this
|
|
131
|
+
* list is revisited rather than carried.
|
|
132
|
+
*/
|
|
133
|
+
const RETAINED_KEY_VARIABLES = ["ANTHROPIC_AUTH_TOKEN"];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The steering variables the scan cannot reach.
|
|
71
137
|
*
|
|
72
138
|
* Named rather than quietly absent, because "derived, never curated" would otherwise be a claim the bolt
|
|
73
|
-
* cannot keep.
|
|
139
|
+
* cannot keep. Four of them, each read through a key built at runtime, and the bolt pins the key that
|
|
140
|
+
* builds it among the unresolved ones.
|
|
74
141
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* measured: `AWS_ENDPOINT_URL` redirects the call, and `AWS_SHARED_CREDENTIALS_FILE` replaces the
|
|
80
|
-
* credential it is signed with. `AWS_CONFIG_FILE` does both, and can also name a `credential_process`
|
|
81
|
-
* shell command.
|
|
142
|
+
* `AWS_ENDPOINT_URL_BEDROCK_RUNTIME` is `@smithy/core`'s `AWS_ENDPOINT_URL_<SERVICE>`. It matters because pi
|
|
143
|
+
* stops pinning the Bedrock endpoint itself as soon as `AWS_REGION` or `AWS_PROFILE` is present, which is
|
|
144
|
+
* the ordinary way to configure Bedrock. (`AWS_ENDPOINT_URL`, `AWS_CONFIG_FILE` and
|
|
145
|
+
* `AWS_SHARED_CREDENTIALS_FILE` were here until issue #511; the scan now names them.)
|
|
82
146
|
*
|
|
83
147
|
* The proxy spellings are read by pi's own `getProxyEnv`, which lowercases and uppercases the key it is
|
|
84
|
-
* given and asks for both
|
|
85
|
-
*
|
|
86
|
-
* `https_proxy` outranks the egress policy's own variable in
|
|
87
|
-
*
|
|
148
|
+
* given and asks for both, and builds `${protocol}_proxy`. `all_proxy` and `no_proxy` it names literally,
|
|
149
|
+
* so the scan finds those. `EGRESS_ENV_VARS` owns `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`; what is left
|
|
150
|
+
* is here. pi reads the LOWERCASE form FIRST, so `https_proxy` outranks the egress policy's own variable in
|
|
151
|
+
* pi's reader. Only the schemes a provider call can use are listed.
|
|
88
152
|
*/
|
|
89
|
-
const UNREACHABLE_BY_SCAN = [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
153
|
+
const UNREACHABLE_BY_SCAN = ["AWS_ENDPOINT_URL_BEDROCK_RUNTIME", "ALL_PROXY", "http_proxy", "https_proxy"];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* pi's own reads in its `PI_*` namespace (issue #511), minus what the worker and the runner write.
|
|
157
|
+
*
|
|
158
|
+
* `PI_CODING_AGENT_DIR` points pi at another agent directory, and with it another `auth.json`, settings
|
|
159
|
+
* and models file; `PI_RADIUS_GATEWAY`, `PI_SHARE_VIEWER_URL` and `PI_INSTALLER_API_BASE` are URLs pi
|
|
160
|
+
* calls. The rest are here because the scan finds them and the rule has no judgement in it: a name the
|
|
161
|
+
* pinned pi reads for its own configuration is not a name a trigger author picks.
|
|
162
|
+
*/
|
|
163
|
+
const PI_OWN_READS = [
|
|
164
|
+
"PI_CLEAR_ON_SHRINK",
|
|
165
|
+
"PI_CODING_AGENT_DIR",
|
|
166
|
+
"PI_CODING_AGENT_SESSION_DIR",
|
|
167
|
+
"PI_EXPERIMENTAL",
|
|
168
|
+
"PI_HARDWARE_CURSOR",
|
|
169
|
+
"PI_HYPERLINKS",
|
|
170
|
+
"PI_IMAGE_PROTOCOL",
|
|
171
|
+
"PI_INSTALLER_API_BASE",
|
|
172
|
+
"PI_MANAGED_INSTALL_ROOT",
|
|
173
|
+
"PI_PACKAGE_DIR",
|
|
174
|
+
"PI_RADIUS_GATEWAY",
|
|
175
|
+
"PI_SHARE_VIEWER_URL",
|
|
176
|
+
"PI_SKIP_VERSION_CHECK",
|
|
177
|
+
"PI_STARTUP_BENCHMARK",
|
|
178
|
+
"PI_TIMING",
|
|
179
|
+
"PI_TRUE_COLOR",
|
|
180
|
+
"PI_TUI_DEBUG",
|
|
181
|
+
"PI_TUI_DEBUG_REDRAW",
|
|
182
|
+
"PI_TUI_ESC_TIMEOUT",
|
|
183
|
+
"PI_TUI_WRITE_LOG",
|
|
99
184
|
];
|
|
100
185
|
|
|
101
186
|
export const PROVIDER_STEERING_VARS = new Set([
|
|
102
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
103
187
|
"ANTHROPIC_BASE_URL",
|
|
188
|
+
"ANTHROPIC_CONFIG_DIR",
|
|
189
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
190
|
+
"ANTHROPIC_ENVIRONMENT_ID",
|
|
191
|
+
"ANTHROPIC_ENVIRONMENT_KEY",
|
|
192
|
+
"ANTHROPIC_FEDERATION_RULE_ID",
|
|
193
|
+
"ANTHROPIC_IDENTITY_TOKEN",
|
|
194
|
+
"ANTHROPIC_IDENTITY_TOKEN_FILE",
|
|
104
195
|
"ANTHROPIC_LOG",
|
|
196
|
+
"ANTHROPIC_ORGANIZATION_ID",
|
|
197
|
+
"ANTHROPIC_PROFILE",
|
|
198
|
+
"ANTHROPIC_SCOPE",
|
|
199
|
+
"ANTHROPIC_SERVICE_ACCOUNT_ID",
|
|
200
|
+
"ANTHROPIC_SESSION_ID",
|
|
201
|
+
"ANTHROPIC_WEBHOOK_SIGNING_KEY",
|
|
202
|
+
"ANTHROPIC_WORKSPACE_ID",
|
|
203
|
+
"ANTHROPIC_WORK_ID",
|
|
204
|
+
"ANTHROPIC_WORK_SECRET",
|
|
105
205
|
"AWS_ACCESS_KEY_ID",
|
|
206
|
+
"AWS_ACCOUNT_ID_ENDPOINT_MODE",
|
|
207
|
+
"AWS_AUTH_SCHEME_PREFERENCE",
|
|
106
208
|
"AWS_BEARER_TOKEN_BEDROCK",
|
|
209
|
+
"AWS_BEDROCK_BASE_URL",
|
|
107
210
|
"AWS_BEDROCK_FORCE_CACHE",
|
|
108
211
|
"AWS_BEDROCK_FORCE_HTTP1",
|
|
109
212
|
"AWS_BEDROCK_SKIP_AUTH",
|
|
213
|
+
"AWS_CONFIG_FILE",
|
|
110
214
|
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
|
|
111
215
|
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
|
|
216
|
+
"AWS_DEFAULTS_MODE",
|
|
112
217
|
"AWS_DEFAULT_REGION",
|
|
218
|
+
"AWS_DISABLE_CLOCK_SKEW_CORRECTION",
|
|
219
|
+
"AWS_EC2_METADATA_DISABLED",
|
|
220
|
+
"AWS_EC2_METADATA_SERVICE_ENDPOINT",
|
|
221
|
+
"AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE",
|
|
222
|
+
"AWS_ENDPOINT_URL",
|
|
223
|
+
"AWS_EXECUTION_ENV",
|
|
224
|
+
"AWS_IGNORE_CONFIGURED_ENDPOINT_URLS",
|
|
225
|
+
"AWS_LAMBDA_FUNCTION_NAME",
|
|
226
|
+
"AWS_MAX_ATTEMPTS",
|
|
227
|
+
"AWS_NEW_RETRIES_2026",
|
|
113
228
|
"AWS_PROFILE",
|
|
114
229
|
"AWS_REGION",
|
|
230
|
+
"AWS_RETRY_MODE",
|
|
231
|
+
"AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED",
|
|
232
|
+
"AWS_SDK_UA_APP_ID",
|
|
115
233
|
"AWS_SECRET_ACCESS_KEY",
|
|
116
234
|
"AWS_SESSION_TOKEN",
|
|
235
|
+
"AWS_SHARED_CREDENTIALS_FILE",
|
|
236
|
+
"AWS_SIGV4A_SIGNING_REGION_SET",
|
|
237
|
+
"AWS_USE_DUALSTACK_ENDPOINT",
|
|
238
|
+
"AWS_USE_FIPS_ENDPOINT",
|
|
117
239
|
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
|
118
240
|
"AZURE_OPENAI_API_VERSION",
|
|
119
241
|
"AZURE_OPENAI_BASE_URL",
|
|
120
242
|
"AZURE_OPENAI_DEPLOYMENT_NAME_MAP",
|
|
121
243
|
"AZURE_OPENAI_ENDPOINT",
|
|
122
244
|
"AZURE_OPENAI_RESOURCE_NAME",
|
|
245
|
+
"CLOUDFLARE_ACCOUNT_ID",
|
|
246
|
+
"CLOUDFLARE_GATEWAY_ID",
|
|
247
|
+
"CLOUDSDK_CONFIG",
|
|
248
|
+
"CLOUD_RUN_JOB",
|
|
249
|
+
"DEBUG",
|
|
250
|
+
"FUNCTION_NAME",
|
|
251
|
+
"FUNCTION_TARGET",
|
|
252
|
+
"GAE_MODULE_NAME",
|
|
253
|
+
"GAE_SERVICE",
|
|
123
254
|
"GCLOUD_PROJECT",
|
|
124
|
-
"
|
|
125
|
-
"GEMINI_NEXT_GEN_API_LOG",
|
|
255
|
+
"GOOGLE_API_CERTIFICATE_CONFIG",
|
|
126
256
|
"GOOGLE_API_KEY",
|
|
127
257
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
128
258
|
"GOOGLE_CLOUD_LOCATION",
|
|
129
259
|
"GOOGLE_CLOUD_PROJECT",
|
|
260
|
+
"GOOGLE_CLOUD_QUOTA_PROJECT",
|
|
261
|
+
"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES",
|
|
130
262
|
"GOOGLE_GEMINI_BASE_URL",
|
|
263
|
+
"GOOGLE_GENAI_ACCESS_TOKEN",
|
|
264
|
+
"GOOGLE_GENAI_API_KEY",
|
|
265
|
+
"GOOGLE_GENAI_API_VERSION",
|
|
266
|
+
"GOOGLE_GENAI_DEBUG",
|
|
267
|
+
"GOOGLE_GENAI_USER_PROJECT",
|
|
131
268
|
"GOOGLE_GENAI_USE_ENTERPRISE",
|
|
132
269
|
"GOOGLE_GENAI_USE_VERTEXAI",
|
|
133
270
|
"GOOGLE_VERTEX_BASE_URL",
|
|
271
|
+
"HOSTNAME",
|
|
272
|
+
"KIMI_CODE_OAUTH_HOST",
|
|
273
|
+
"KIMI_OAUTH_HOST",
|
|
274
|
+
"K_CONFIGURATION",
|
|
275
|
+
"OPENAI_ADMIN_KEY",
|
|
134
276
|
"OPENAI_API_VERSION",
|
|
135
277
|
"OPENAI_BASE_URL",
|
|
278
|
+
"OPENAI_CUSTOM_HEADERS",
|
|
136
279
|
"OPENAI_LOG",
|
|
137
280
|
"OPENAI_ORG_ID",
|
|
138
281
|
"OPENAI_PROJECT_ID",
|
|
139
282
|
"OPENAI_WEBHOOK_SECRET",
|
|
140
283
|
"PI_CACHE_RETENTION",
|
|
141
|
-
"PI_GATEWAY",
|
|
142
284
|
"PI_OAUTH_CALLBACK_HOST",
|
|
285
|
+
"SMITHY_NEW_RETRIES_2026",
|
|
286
|
+
"WS_NO_BUFFER_UTIL",
|
|
287
|
+
"WS_NO_UTF_8_VALIDATE",
|
|
288
|
+
"_X_AMZN_TRACE_ID",
|
|
289
|
+
// The lowercase twins google-auth-library reads beside the uppercase forms, and the two proxy spellings
|
|
290
|
+
// pi's getProxyEnv names literally (issue #511).
|
|
291
|
+
"all_proxy",
|
|
292
|
+
"gcloud_project",
|
|
293
|
+
"google_application_credentials",
|
|
294
|
+
"google_cloud_project",
|
|
295
|
+
"no_proxy",
|
|
296
|
+
...PI_OWN_READS,
|
|
297
|
+
...RETAINED_KEY_VARIABLES,
|
|
143
298
|
...UNREACHABLE_BY_SCAN,
|
|
144
299
|
]);
|
package/src/queue.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { Queue } from "bullmq";
|
|
2
3
|
import { assertJudgedConnection, onValkeyError } from "./connection.mjs";
|
|
3
|
-
import { chainedJobId, localJobId, deliveryJobId, gitlabDeliveryJobId, forgeDeliveryJobId } from "./job-id.mjs";
|
|
4
|
+
import { chainedJobId, localJobId, deliveryJobId, gitlabDeliveryJobId, forgeDeliveryJobId, manualTriggerJobId } from "./job-id.mjs";
|
|
4
5
|
import { targetSeparator } from "./forges.mjs";
|
|
5
6
|
import { PR_CLOSE_ACTIONS } from "./triggers.mjs";
|
|
6
7
|
|
|
@@ -62,7 +63,7 @@ export function fleetQueueNames(hosts) {
|
|
|
62
63
|
/** The name charset, duplicated from `config.mjs` deliberately: this module imports nothing. */
|
|
63
64
|
const WORKER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
64
65
|
|
|
65
|
-
export { chainedJobId, localJobId, deliveryJobId, gitlabDeliveryJobId, forgeDeliveryJobId };
|
|
66
|
+
export { chainedJobId, localJobId, deliveryJobId, gitlabDeliveryJobId, forgeDeliveryJobId, manualTriggerJobId };
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
69
|
* A queue handle. `name` defaults to the shared queue, so every existing caller is unchanged and a
|
|
@@ -84,7 +85,98 @@ export function makeQueue(connection, { name = QUEUE } = {}) {
|
|
|
84
85
|
* removeOnComplete keeps the dedup window ~= the retention. Unlike webhooks, local jobs are not
|
|
85
86
|
* redelivered, so a modest window is enough.
|
|
86
87
|
*/
|
|
87
|
-
export async function enqueueLocalJob(queue,
|
|
88
|
+
export async function enqueueLocalJob(queue, fields) {
|
|
89
|
+
return (await addLocalJob(queue, fields)).id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `enqueueLocalJob` for a caller that must SAY whether anything was queued (issue #524): the operator's
|
|
94
|
+
* `pi-dispatch run`. Returns `{ id, existing }`, where `existing` is null for a job this call created, and
|
|
95
|
+
* `{ queuedAt, state }` when the one-minute dedup answered with a job an identical earlier call made.
|
|
96
|
+
*
|
|
97
|
+
* The CLI printed "queued <id>" either way, so a second `run` inside the same minute announced a job while
|
|
98
|
+
* running nothing, under the id of one that had already finished. The dedup itself is deliberate and stays.
|
|
99
|
+
*
|
|
100
|
+
* How a duplicate is told apart, and why not by asking first. BullMQ answers a jobId that already exists by
|
|
101
|
+
* returning the existing id inside a Job object it built LOCALLY for this call, so the return value says nothing
|
|
102
|
+
* about whether anything was stored. Each call therefore puts a random nonce of its own on the job's data
|
|
103
|
+
* (`enqueueNonce`) and reads the stored job back AFTER the add: the stored nonce is this call's exactly when
|
|
104
|
+
* this call's add is the one BullMQ kept. Exactly one of any number of concurrent identical calls finds its own.
|
|
105
|
+
* The first version compared the add's timestamp instead, and PR #528's review refuted it: calls in the same
|
|
106
|
+
* millisecond share a timestamp, and ten in parallel all reported "created" while one job existed. A lookup
|
|
107
|
+
* BEFORE the add has the same flaw by construction, since every concurrent caller can see nothing.
|
|
108
|
+
*
|
|
109
|
+
* The nonce is on job data, never inside `trigger`, which is the object copied into /job/event.json: it is the
|
|
110
|
+
* producer's bookkeeping and the agent has no use for it. Only the two reporting producers add it (this one and
|
|
111
|
+
* `enqueueTriggerRunReporting`), so a job from the outbox collector or a cron tick keeps exactly the data it had.
|
|
112
|
+
*
|
|
113
|
+
* A separate function rather than a change to `enqueueLocalJob`'s return, because the outbox collector consumes
|
|
114
|
+
* that as a bare id and prints nothing to a person. It also keeps its one Valkey round trip: only this function
|
|
115
|
+
* pays for the read back.
|
|
116
|
+
*
|
|
117
|
+
* `state` is BullMQ's own word for where the job is (`waiting`, `active`, `completed`, `failed`, `delayed`
|
|
118
|
+
* and the rest), or null when the state could not be read; the caller then says it is already queued or
|
|
119
|
+
* done rather than inventing one. A read that fails outright returns `existing: undefined`: whether this
|
|
120
|
+
* call queued anything is then unknown, and the caller says so instead of guessing either way.
|
|
121
|
+
*/
|
|
122
|
+
export async function enqueueLocalJobReporting(queue, fields) {
|
|
123
|
+
return addReporting(queue, (enqueueNonce) => addLocalJob(queue, { ...fields, enqueueNonce }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fire one cron trigger by hand (`pi-dispatch run --trigger <id>`, issue #505), reporting like
|
|
128
|
+
* `enqueueLocalJobReporting` and through the same nonce and read back, so a second call in the same minute says
|
|
129
|
+
* the first job was already queued instead of announcing one it did not make.
|
|
130
|
+
*
|
|
131
|
+
* `schedule` is the trigger's own scheduler entry from `loadSchedules`, and its `data` is passed through WHOLE, with
|
|
132
|
+
* only the nonce added: the folder, the flow or command, the task, `github`, `packages`, `resume`, the model fields,
|
|
133
|
+
* `trigger: { id, pattern }` and a job-level `portfolio`. Field by field through `addLocalJob` was the other way, and
|
|
134
|
+
* it is the one that drops a field the next issue adds to the schedule. The options are the schedule's too, so the
|
|
135
|
+
* job is not retried, as a scheduled tick is not (a scheduler job carries no `attempts`). The id is
|
|
136
|
+
* `manualTriggerJobId`'s, so `localEventContext` reports `source: "cron"` with `scheduledFor: null`.
|
|
137
|
+
*/
|
|
138
|
+
export async function enqueueTriggerRunReporting(queue, schedule, { now = new Date() } = {}) {
|
|
139
|
+
const jobId = manualTriggerJobId({ triggerId: schedule.schedulerId, now });
|
|
140
|
+
return addReporting(queue, async (enqueueNonce) => {
|
|
141
|
+
await queue.add(schedule.name, { ...schedule.data, enqueueNonce }, { ...schedule.opts, jobId });
|
|
142
|
+
return { id: jobId };
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The add, then the read back that says whether THIS call's add is the one stored (see `enqueueLocalJobReporting`). */
|
|
147
|
+
async function addReporting(queue, add) {
|
|
148
|
+
const enqueueNonce = randomUUID();
|
|
149
|
+
const { id } = await add(enqueueNonce);
|
|
150
|
+
let stored;
|
|
151
|
+
try {
|
|
152
|
+
stored = await queue.getJob(id);
|
|
153
|
+
} catch {
|
|
154
|
+
return { id, existing: undefined };
|
|
155
|
+
}
|
|
156
|
+
// No stored job: it was removed between the add and this read (a `cancel` racing it). Nothing says it was
|
|
157
|
+
// a duplicate, so it reads as created, which is what the add itself answered.
|
|
158
|
+
if (!stored || stored.data?.enqueueNonce === enqueueNonce) return { id, existing: null };
|
|
159
|
+
let state = null;
|
|
160
|
+
try {
|
|
161
|
+
const s = await stored.getState();
|
|
162
|
+
state = typeof s === "string" && s !== "unknown" ? s : null;
|
|
163
|
+
} catch {}
|
|
164
|
+
return { id, existing: { queuedAt: Number.isFinite(stored.timestamp) ? stored.timestamp : null, state } };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The one sentence for a swallowed local enqueue (issue #524), shared by `pi-dispatch run` and the admin's
|
|
169
|
+
* `/dispatch run` so the two producers an operator types cannot drift apart. `existing` is
|
|
170
|
+
* `enqueueLocalJobReporting`'s: the time is the first job's own, in this process's local time, and a state the
|
|
171
|
+
* queue could not give is said as "already queued or done" rather than guessed.
|
|
172
|
+
*/
|
|
173
|
+
export function swallowedRunSentence(jobId, existing) {
|
|
174
|
+
const at = Number.isFinite(existing?.queuedAt) ? new Date(existing.queuedAt) : null;
|
|
175
|
+
const hhmm = at ? ` at ${String(at.getHours()).padStart(2, "0")}:${String(at.getMinutes()).padStart(2, "0")}` : "";
|
|
176
|
+
return `an identical run was queued${hhmm} as ${jobId} (${existing?.state ?? "already queued or done"}); nothing new was queued.`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function addLocalJob(queue, { folder, flow, task, command, provider, model, maxTurns, models, maxCostUsd, image, backend, excludeTools, skillsDir, secrets, secretsProfile, chainDepth, parentJobId, jobId, enqueueNonce, now = new Date() }) {
|
|
88
180
|
const minute = now.toISOString().slice(0, 16); // YYYY-MM-DDTHH:MM -- the dedup window
|
|
89
181
|
// A caller-supplied jobId (the outbox collector's retry-idempotent chainedJobId) wins; otherwise the
|
|
90
182
|
// minute-windowed localJobId is the dedup key. A command job (issue #189) fills the flow slot with
|
|
@@ -93,7 +185,7 @@ export async function enqueueLocalJob(queue, { folder, flow, task, command, prov
|
|
|
93
185
|
// vanish silently -- and the `cmd:` prefix keeps a command named X from colliding with a flow named X
|
|
94
186
|
// (`:` is outside the skill-name charset, so no real flow can spell the prefixed form). A flow job's
|
|
95
187
|
// key is byte-identical to before the feature.
|
|
96
|
-
const id = jobId ?? localJobId({ folder, flow: command !== undefined ? `cmd:${command}` : flow, task, minute });
|
|
188
|
+
const id = jobId ?? localJobId({ folder, flow: command !== undefined ? `cmd:${command}` : flow, task, minute, provider, model, models });
|
|
97
189
|
// image/chainDepth/parentJobId land on `data` only when present, so a plain non-chained job's data is
|
|
98
190
|
// byte-identical. `image` is the container image this job runs in (INT-TRIGGERS-FILE-CONTRACT); absent
|
|
99
191
|
// resolves the deployment default at job start, never a value frozen here.
|
|
@@ -109,6 +201,11 @@ export async function enqueueLocalJob(queue, { folder, flow, task, command, prov
|
|
|
109
201
|
provider,
|
|
110
202
|
model,
|
|
111
203
|
maxTurns,
|
|
204
|
+
// Issue #502: the allowed-model list, conditional like `image`, so a job whose trigger named none keeps its
|
|
205
|
+
// data byte-identical. An outbox child carries its parent's (outbox.mjs), never one its request file names.
|
|
206
|
+
...(models !== undefined && { models }),
|
|
207
|
+
// Issue #501: the per-job dollar cap, passed through for the outbox child that inherits its parent's.
|
|
208
|
+
...(maxCostUsd !== undefined && { maxCostUsd }),
|
|
112
209
|
...(image !== undefined && { image }),
|
|
113
210
|
// #227. WHERE this job's container is built. Conditional like `image`, so a trigger that named no
|
|
114
211
|
// venue produces byte-identical job data -- and at JOB level, never inside `trigger`, for the reason
|
|
@@ -132,6 +229,8 @@ export async function enqueueLocalJob(queue, { folder, flow, task, command, prov
|
|
|
132
229
|
...(secretsProfile !== undefined && { secretsProfile }),
|
|
133
230
|
...(chainDepth !== undefined && { chainDepth }),
|
|
134
231
|
...(parentJobId !== undefined && { parentJobId }),
|
|
232
|
+
// Issue #524: `enqueueLocalJobReporting`'s own mark, so it can tell whether ITS add is the one stored.
|
|
233
|
+
...(enqueueNonce !== undefined && { enqueueNonce }),
|
|
135
234
|
};
|
|
136
235
|
await queue.add("local", data, {
|
|
137
236
|
jobId: id,
|
|
@@ -140,7 +239,7 @@ export async function enqueueLocalJob(queue, { folder, flow, task, command, prov
|
|
|
140
239
|
removeOnComplete: { age: 24 * 3600 },
|
|
141
240
|
removeOnFail: { age: 7 * 24 * 3600 },
|
|
142
241
|
});
|
|
143
|
-
return id;
|
|
242
|
+
return { id };
|
|
144
243
|
}
|
|
145
244
|
|
|
146
245
|
// Coalesces rapid re-label spam; the GUID jobId + 31d retention handle exact redelivery, so this
|
|
@@ -211,7 +310,7 @@ export async function enqueueGitLabJob(queue, fields) {
|
|
|
211
310
|
* window, replicas never coalesce against each other, and an unflagged job's dedup id is the same string it
|
|
212
311
|
* has always been.
|
|
213
312
|
*/
|
|
214
|
-
export async function enqueueForgeJob(queue, kind, { repo, projectId, azure, target, flow, command, trigger, provider, model, maxTurns, packages, image, backend, excludeTools, skillsDir, instructions, resume, secrets, secretsProfile, waitFor, replica, replicas }) {
|
|
313
|
+
export async function enqueueForgeJob(queue, kind, { repo, projectId, azure, target, flow, command, trigger, provider, model, maxTurns, models, maxCostUsd, packages, image, backend, excludeTools, skillsDir, instructions, resume, secrets, secretsProfile, waitFor, replica, replicas }) {
|
|
215
314
|
const jobId = forgeDeliveryJobId(kind, trigger?.deliveryId, replica);
|
|
216
315
|
// `packages` (whether to load the operator-staged pi packages) and `image` (which container image to run)
|
|
217
316
|
// come off the MATCHED trigger (INT-TRIGGERS-FILE-CONTRACT / REQ-GLOBAL-PI-OVERLAY) and land on `data`
|
|
@@ -237,6 +336,9 @@ export async function enqueueForgeJob(queue, kind, { repo, projectId, azure, tar
|
|
|
237
336
|
provider,
|
|
238
337
|
model,
|
|
239
338
|
maxTurns,
|
|
339
|
+
// Issue #502: the allowed-model list, conditional like `packages` below so an unflagged trigger's job data is
|
|
340
|
+
// byte-identical, and at JOB level, never inside `trigger` (copied verbatim into /job/event.json).
|
|
341
|
+
...(models !== undefined && { models }),
|
|
240
342
|
...(packages !== undefined && { packages }),
|
|
241
343
|
...(image !== undefined && { image }),
|
|
242
344
|
// #227. WHERE this job's container is built. Conditional like `image`, so a trigger that named no
|
|
@@ -249,6 +351,9 @@ export async function enqueueForgeJob(queue, kind, { repo, projectId, azure, tar
|
|
|
249
351
|
// same reason: `trigger` is copied verbatim into /job/event.json, and a permission boundary is the
|
|
250
352
|
// worker's business, never agent-visible input to reason about.
|
|
251
353
|
...(excludeTools !== undefined && { excludeTools }),
|
|
354
|
+
// #501. The trigger's per-job dollar cap, as written. Conditional like `excludeTools`, at JOB level for its
|
|
355
|
+
// reason, and a narrowing like it: the worker runs the job under the smaller of this and the deployment cap.
|
|
356
|
+
...(maxCostUsd !== undefined && { maxCostUsd }),
|
|
252
357
|
// The host directory of operator-authored skills this trigger injects (REQ-PER-TRIGGER-SKILLS).
|
|
253
358
|
// Conditional like `image`, so an unflagged job's data stays byte-identical, and at JOB level rather
|
|
254
359
|
// than inside `trigger` because a worker-host path is an execution knob, not a fact about the
|
package/src/reserved-env.mjs
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #503: the variable a keyless provider's `models.json` key names (`"apiKey": "$PI_DISPATCH_KEYLESS"`), so pi
|
|
3
|
+
* composes the provider. `buildContainerEnv` writes it, with the fixed, non-secret value `keyless`, only when the
|
|
4
|
+
* credential gate passed the job's provider keyless (env-allowlist.mjs, `resolveProviderCredential`): a provider pi does
|
|
5
|
+
* not know, served by keyless model endpoints alone. Reserved in the set below, so neither `run.secrets` nor
|
|
6
|
+
* `PI_FORWARD_ENV` can set it.
|
|
7
|
+
*/
|
|
8
|
+
export const KEYLESS_ENV_NAME = "PI_DISPATCH_KEYLESS";
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* The environment variable names `buildContainerEnv` writes itself, spelled once (issue #225).
|
|
3
12
|
*
|
|
@@ -34,6 +43,14 @@ export const CONTAINER_ENV_NAMES = new Set([
|
|
|
34
43
|
"PI_FLOW",
|
|
35
44
|
"PI_COMMAND",
|
|
36
45
|
"PI_EXCLUDE_TOOLS",
|
|
46
|
+
// Issues #501, #502: the per-job dollar cap (written by the worker since #501, from the trigger's and the
|
|
47
|
+
// deployment's caps) and the allowed-model list (reserved until #502 writes it). Neither can be bound through
|
|
48
|
+
// `run.secrets`: a trigger setting its own cap, or its own list, would be the policy choosing itself.
|
|
49
|
+
"PI_MAX_COST_MICROS",
|
|
50
|
+
"PI_ALLOWED_MODELS",
|
|
51
|
+
// Issue #545: tells the runner the exit line's key waits on stdin. A trigger setting it would block its own runner
|
|
52
|
+
// on a stdin no one writes, or turn the signed line off.
|
|
53
|
+
"PI_EXIT_AUTH",
|
|
37
54
|
"PI_OFFLINE",
|
|
38
55
|
"PLAYWRIGHT_BROWSERS_PATH",
|
|
39
56
|
"PLAYWRIGHT_MCP_BROWSER",
|
|
@@ -41,4 +58,17 @@ export const CONTAINER_ENV_NAMES = new Set([
|
|
|
41
58
|
// Issue #341: set beside `--user` so a uid with no passwd entry has a writable home. A new reservation, so a
|
|
42
59
|
// triggers file binding a secret named HOME is now refused at parse (worker, receiver and admin alike).
|
|
43
60
|
"HOME",
|
|
61
|
+
// Issue #503: written only on the keyless branch of the credential gate (see KEYLESS_ENV_NAME at the top).
|
|
62
|
+
KEYLESS_ENV_NAME,
|
|
44
63
|
]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Issue #500: the names the RUNNER sets in its own environment inside the container, for its descendants: the child
|
|
67
|
+
* ledger directory and the runner's pid (image/runner/src/usage-meter.mjs, openChildLedger). The worker never writes
|
|
68
|
+
* them, so they are not in the set above (which a test pins to what buildContainerEnv emits). They are reserved all the
|
|
69
|
+
* same: a trigger binding one through `run.secrets`, or a host value forwarded through `PI_FORWARD_ENV`, would arrive in
|
|
70
|
+
* the runner's environment before the runner sets its own, and a value the runner then failed to replace would point
|
|
71
|
+
* every pi child's ledger at a directory the agent chose. Refused at load in both lists, and deleted by
|
|
72
|
+
* buildContainerEnv after both loops as the backstop.
|
|
73
|
+
*/
|
|
74
|
+
export const RUNNER_ENV_NAMES = new Set(["PI_DISPATCH_CHILD_LEDGER", "PI_DISPATCH_RUNNER_PID"]);
|