razzou-harness 0.4.0 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,55 @@
3
3
  The notable changes in each release of razzou-harness. Before 1.0, a new minor version
4
4
  may change how a project is set up, and its notes say how.
5
5
 
6
+ ## 0.4.2 (2026-11-08)
7
+
8
+ **Upgrading from 0.4.1:** upgrade OpenShell to 0.1.7 first, from its Debian package
9
+ (`sudo dpkg -i openshell_0.1.7-1_amd64.deb`); the gateway keeps its providers and
10
+ profiles. Then install the new version, and rebuild the agents' image with `harness
11
+ doctor --build-images`: its tag follows the harness's version. A project's AWS services
12
+ stay as its `harness/config.json` lists them; for every one, set `"services": ["all"]`
13
+ under `aws`.
14
+
15
+ - **OpenShell 0.1.7.** The harness is pinned to it, and tested with it: the probe passes
16
+ with every bundle and every AWS service, and the Architect's terminal and the shell
17
+ in a sandbox work. The doctor's and setup's fix for a stopped gateway now starts its
18
+ systemd user service: OpenShell's CLI has no command for it.
19
+ - **Every AWS service by default.** `"all"` in `aws.services` is every service the AWS
20
+ SDK knows, about 420 endpoints in a region with the global ones, each signed with the
21
+ view role under its own signing name, as OpenShell requires. It is the default, and
22
+ `harness setup` no longer asks which services the project uses: the view role alone
23
+ decides what agents read. The table comes from the AWS SDK's own data
24
+ (`scripts/aws-services.ts` writes it).
25
+ - **AWS reads with `GET` work.** OpenShell's automatic `sigv4` mode signs a request
26
+ with neither a body nor `x-amz-content-sha256` as `UNSIGNED-PAYLOAD`, which only S3
27
+ accepts ([NVIDIA/OpenShell#5375](https://github.com/NVIDIA/OpenShell/issues/5375)):
28
+ every service that reads with `GET`, such as Lambda, API Gateway, EKS, Route 53,
29
+ CloudFront and Bedrock, answered `SignatureDoesNotMatch` in the sandboxes, and a
30
+ `terraform plan` there failed on their resources. Every AWS endpoint but S3's is now
31
+ signed with `sigv4:body`, the hash of the request's body, empty for a `GET`: the AWS
32
+ CLI's calls to those services and a `terraform plan` reading them work.
33
+
34
+ ## 0.4.1 (2026-11-08)
35
+
36
+ **Upgrading a project from 0.4.0:** install the new version, then rebuild the agents'
37
+ image with `harness doctor --build-images`: its tag follows the harness's version. A
38
+ project's network stays as its `harness/config.json` lists it; for every bundle, set
39
+ `"bundles": ["all"]`.
40
+
41
+ - **Every network bundle by default.** `"all"` in `network.bundles` stands for every
42
+ bundle, those later versions add included, and it is the default: `harness init`
43
+ writes it, and `harness setup` no longer asks which bundles the stack needs. The
44
+ sandboxes of every role reach every bundle's package registries and documentation,
45
+ read-only. A project narrows it by listing bundles. An unknown bundle now fails when
46
+ the config is read, not when a sandbox is made.
47
+ - **Third-party APIs**, in `network.apis`: the hosts the code may call with any method,
48
+ like a payment provider's test API. With `"key": "STRIPE_API_KEY"`, `harness doctor`
49
+ asks for the API's key at a hidden prompt and stores it in an OpenShell provider of
50
+ its own; every sandbox gets it, and holds a placeholder in that variable, which
51
+ OpenShell replaces with the key on requests to that host alone, in headers and URLs,
52
+ not in request bodies. While a key isn't stored on the machine, no new attempt
53
+ starts: the pipeline pauses and says so.
54
+
6
55
  ## 0.4.0 (2026-11-08)
7
56
 
8
57
  **Upgrading a project from 0.3.x:** install the new version, then rebuild the agents'
package/README.md CHANGED
@@ -106,7 +106,7 @@ without pull requests.
106
106
 
107
107
  - Linux, Node 24 or later, git, Docker, and `gh` logged in as an admin of your
108
108
  project's repository.
109
- - OpenShell 0.1.2 with a running gateway. The harness is tested with this version only,
109
+ - OpenShell 0.1.7 with a running gateway. The harness is tested with this version only,
110
110
  and `harness doctor` refuses others.
111
111
  - Claude Code on this machine, with a Claude subscription: its binary goes into the
112
112
  sandbox image, and `claude setup-token` gives the agents' token. Access with an API
@@ -134,9 +134,9 @@ such as the Harness.io CLI, comes first on the PATH.
134
134
 
135
135
  `harness setup` does every step it can and asks only for what is yours: a `gh` login,
136
136
  the token from `claude setup-token` at a hidden prompt, two clicks on GitHub for the
137
- App, since GitHub creates and installs Apps only in the browser, three questions (your
138
- stack's network bundles, AWS or not, the hours of your time), and, with AWS, the role
139
- ARNs from your bootstrap and the gateway key. In order, it logs you in to `gh`; creates
137
+ App, since GitHub creates and installs Apps only in the browser, two questions (AWS or
138
+ not, the hours of your time), and, with AWS, the role ARNs from your bootstrap and the
139
+ gateway key. In order, it logs you in to `gh`; creates
140
140
  the repository on GitHub if the project has no remote; creates the App from its
141
141
  manifest when this machine has none, and waits for you to install it; writes the
142
142
  harness's files; asks the questions and writes `harness/config.json`, with the
@@ -169,9 +169,10 @@ from a sandbox like an agent's and from outside, everything that must be refused
169
169
  writes and secret reads, other projects' state, `git push`, unlisted hosts and the
170
170
  metadata address, a workflow pushed by the App, a merge without your approval.
171
171
 
172
- `harness init` lets the sandboxes reach only the `core` network bundle. Add your
173
- stack's bundles, such as `node` for npm, in `harness/config.json` (see
174
- [Configuration](#configuration)).
172
+ `harness init` lets the sandboxes reach every network bundle: the package registries
173
+ and documentation of each stack, read-only. To narrow it, list fewer bundles in
174
+ `harness/config.json`; to let the code call a third-party API, add it under
175
+ `network.apis` (see [Configuration](#configuration)).
175
176
 
176
177
  ## Your first module
177
178
 
@@ -432,14 +433,14 @@ through an `Arch:` pull request, or a commit of your own on `main`.
432
433
 
433
434
  ```json
434
435
  {
435
- "harnessVersion": "0.4.0",
436
+ "harnessVersion": "0.4.2",
436
437
  "project": { "name": "shop", "repo": "<org>/shop", "bot": "<org>-harness[bot]" },
437
438
  "roles": {
438
439
  "architect": { "model": "claude-opus-5-5", "effort": "max" },
439
440
  "manager": { "model": "claude-opus-5-5", "effort": "high" },
440
441
  "developer": { "model": "claude-opus-5-5", "effort": "xhigh", "attemptTimeoutMinutes": 90, "maxAttempts": 2 }
441
442
  },
442
- "network": { "bundles": ["core", "node"], "extraHosts": [] },
443
+ "network": { "bundles": ["all"], "apis": [{ "host": "api.stripe.com", "key": "STRIPE_API_KEY" }] },
443
444
  "checks": [{ "id": "unit", "run": "npm test", "phase": "pre-merge" }]
444
445
  }
445
446
  ```
@@ -452,8 +453,9 @@ through an `Arch:` pull request, or a commit of your own on `main`.
452
453
  | `roles.<role>.effort` | `max`, `high`, `xhigh` | Claude Code's `--effort`; `null` for a model without effort levels, such as Haiku |
453
454
  | `roles.developer.attemptTimeoutMinutes` | 120 | An attempt's deadline |
454
455
  | `roles.developer.maxAttempts` | 2 | Attempts per feature before it is escalated to you |
455
- | `network.bundles` | `["core"]` | The hosts sandboxes reach, by bundle: `core`, `node`, `python`, `aws`, `terraform`, `go`, `rust`, `jvm`, `opentofu`, `browsers`. The Architect gets them all. |
456
+ | `network.bundles` | `["all"]` | The hosts sandboxes reach directly, read-only, by bundle: `core`, `node`, `python`, `aws`, `terraform`, `go`, `rust`, `jvm`, `opentofu`, `browsers`, or `all` for every one, those later versions add included. The Architect gets them all. |
456
457
  | `network.extraHosts` | `[]` | More hosts, read-only |
458
+ | `network.apis` | `[]` | Third-party APIs the code may call with any method, like a payment provider's test API: `{ "host": "api.stripe.com" }`, with `"key": "STRIPE_API_KEY"` for one that needs a key. `harness doctor` asks for the key at a hidden prompt and stores it in OpenShell; in a sandbox the variable holds a placeholder, which OpenShell replaces with the key on requests to that host alone, in headers and URLs, not in request bodies. CI gets no key. |
457
459
  | `checks` | `[]` | Checks that every feature must keep passing, such as `npm run lint` |
458
460
  | `protect.files`, `protect.jsonFields` | none | More protected files, as paths or globs (`scripts/*.sh`, `docs/`, `**/Makefile`), and protected JSON fields by file, like `{"tsconfig.json": ["/compilerOptions/strict"]}` |
459
461
  | `sandbox.image` | `razzou-harness/agent:<version>` | The agents' image, tagged with the harness's version, which `harness doctor --build-images` builds |
@@ -556,13 +558,18 @@ and Terraform applies only in CI, after you merge.
556
558
  1. In `harness/config.json`, add `aws` and `terraform`:
557
559
 
558
560
  ```json
559
- "aws": { "region": "us-east-1", "provider": "aws-view-<org>-shop", "services": ["sts", "s3", "iam", "sqs"] },
561
+ "aws": { "region": "us-east-1", "provider": "aws-view-<org>-shop", "services": ["all"] },
560
562
  "terraform": { "dir": "iac", "backendConfig": "backend-configs/staging.hcl", "varFile": "env/staging.tfvars" }
561
563
  ```
562
564
 
563
- `services` lists the AWS services the project uses: their endpoints become reachable,
564
- and signed. Keep the `terraform` paths as they are: the CI workflow uses the same
565
- ones. `harness github-setup` then requires `tf-plan` as well.
565
+ `services` lists the AWS services whose endpoints the sandboxes reach, signed with
566
+ the view role. `all`, the default, is every service the AWS SDK knows, so the view
567
+ role alone decides what agents read; a list of names narrows it. Every service but S3
568
+ is signed with the hash of the request's body, empty for a `GET`: OpenShell's
569
+ automatic mode signs a bodiless `GET` in a way only S3 accepts
570
+ ([NVIDIA/OpenShell#5375](https://github.com/NVIDIA/OpenShell/issues/5375)). Keep the `terraform` paths
571
+ as they are: the CI workflow uses the same ones. `harness github-setup` then requires
572
+ `tf-plan` as well.
566
573
  2. Set the repository variables the workflow reads: `AWS_REGION_STAGING`,
567
574
  `AWS_ROLE_PLAN_ARN_STAGING` and `AWS_ROLE_APPLY_ARN_STAGING`.
568
575
  3. `harness doctor` creates the OpenShell AWS provider: it asks for the view role's ARN,
@@ -581,7 +588,8 @@ RDS, `manage_master_user_password`); anything else uses write-only arguments.
581
588
 
582
589
  What agents can't do, and what stops them:
583
590
  - **Hold credentials.** Sandboxes hold placeholders. The OpenShell proxy adds the Claude
584
- token on `api.anthropic.com` only, and signs AWS requests with the read-only role.
591
+ token on `api.anthropic.com` only, each key of `network.apis` on its API's host only,
592
+ and signs AWS requests with the read-only role.
585
593
  - **Reach the host.** Each agent runs in its own container, with no host folder mounted;
586
594
  code moves in and out as git bundles. The host never runs code or git hooks that an
587
595
  agent wrote.
@@ -594,7 +602,8 @@ What agents can't do, and what stops them:
594
602
  They change only through an Arch PR you merge, or your own commits.
595
603
  - **Change AWS.** A read-only role with explicit denies, and applies only in CI after
596
604
  your merge.
597
- - **Go anywhere online.** Sandboxes reach listed hosts, read-only. Other pages go
605
+ - **Go anywhere online.** Sandboxes reach listed hosts, read-only, and the APIs in
606
+ `network.apis` with any method; OpenShell has no rule for every host. Other pages go
598
607
  through `web_read`: GET only, public addresses only, never this machine's own
599
608
  addresses or the networks it is on (IPv6 included), checked again after every
600
609
  redirect, within 75 seconds and 2 MB, decompressed.
@@ -639,8 +648,8 @@ To report a vulnerability, see `SECURITY.md` in the source repository.
639
648
  |---|---|
640
649
  | `Docker: … permission denied` | Add yourself to the `docker` group, then log in again |
641
650
  | `Tailscale: an exit node is active`, or `container network` fails | An exit node can cut the containers' network: turn it off |
642
- | `OpenShell: … pinned to 0.1.2` | Install OpenShell 0.1.2 |
643
- | `OpenShell gateway` fails | `openshell gateway start` |
651
+ | `OpenShell: … pinned to 0.1.7` | Install OpenShell 0.1.7, from its Debian package |
652
+ | `OpenShell gateway` fails | `systemctl --user start openshell-gateway`: the gateway is a systemd user service |
644
653
  | `image: … has Claude Code X, this machine Y` | `harness doctor --build-images`, after each Claude Code update |
645
654
  | `terminals: node-pty is missing` | Install `python3`, `make` and `g++`, then reinstall with `--allow-scripts=node-pty`. `harness attach` works meanwhile. |
646
655
  | `Claude provider: … is missing` | `claude setup-token`, then `harness doctor` in a terminal: it asks for the token |
@@ -263,7 +263,7 @@ function refusedLines(attempt, failed) {
263
263
  ...shown.map((d) => `- ${d.target} ${d.program ? `from ${d.program}` : '(a DNS query)'}, ${d.count} time${d.count === 1 ? '' : 's'}: ${d.reason.replace(/_/g, ' ')}`),
264
264
  ...(refused.length > shown.length ? [`- and ${refused.length - shown.length} more`] : []),
265
265
  ...(failed
266
- ? ["A sandbox reaches only the hosts of network.bundles and network.extraHosts in harness/config.json, and the next attempt meets the same policy. If the feature can do without a host, give the way around it in your guidance. If it can't, ask_owner to allow the host, with blocking: true when no attempt can succeed without it: only the owner widens the policy."]
266
+ ? ["A sandbox reaches only the hosts of network.bundles, network.extraHosts and network.apis in harness/config.json, and the next attempt meets the same policy. If the feature can do without a host, give the way around it in your guidance. If it can't, ask_owner to allow the host, with blocking: true when no attempt can succeed without it: only the owner widens the policy."]
267
267
  : []),
268
268
  ];
269
269
  }
@@ -15,7 +15,7 @@ import { rulesProblems } from '../github/setup.js';
15
15
  import { toolsManifest } from '../roles.js';
16
16
  import { PROJECT_LABEL } from '../sandbox/driver.js';
17
17
  import { BUNDLES } from '../sandbox/allowlist.js';
18
- import { buildPolicy } from '../sandbox/policy.js';
18
+ import { apiProviderName, buildPolicy } from '../sandbox/policy.js';
19
19
  import { Denials, LOG_LINES } from '../sandbox/refusals.js';
20
20
  import { EventStore } from '../state/events.js';
21
21
  import { activeAttempts, apply, countedAttempts, featureKey, interruptions, isActive, managersOf, planKey, planMerged, project, writeSnapshot, } from '../state/state.js';
@@ -75,6 +75,8 @@ export class Orchestrator {
75
75
  stepErrors = { key: '', count: 0 };
76
76
  /** When the left-over sandboxes are next deleted: at the first pass, then every hour. */
77
77
  nextSweep = 0;
78
+ /** The gateway's providers, read at most once a minute, for the keys of the project's APIs. */
79
+ providerNames;
78
80
  /** Errors in a row of each plan's step, and when it is tried again: one plan that keeps failing never stops the others. */
79
81
  planErrors = new Map();
80
82
  /** The heads of Arch and Fix PRs already given the reviewers' statuses as "not reviewed". */
@@ -262,10 +264,11 @@ export class Orchestrator {
262
264
  this.record({ type: 'PIPELINE_PAUSED', payload: { reason, ...(until ? { until } : {}) } });
263
265
  return true;
264
266
  }
265
- /** The owner lifts a pause. */
267
+ /** The owner lifts a pause. An API key stored meanwhile counts at once. */
266
268
  resume() {
267
269
  if (!this.state.pipeline.paused)
268
270
  return false;
271
+ this.providerNames = undefined;
269
272
  this.record({ type: 'PIPELINE_RESUMED', payload: {} });
270
273
  this.kick();
271
274
  return true;
@@ -430,6 +433,8 @@ export class Orchestrator {
430
433
  return 'progress';
431
434
  if (paused)
432
435
  return 'wait';
436
+ if (await this.apiKeysMissing())
437
+ return 'wait';
433
438
  // Manual builds one feature at a time, and looks at nothing else while it runs. Swarm starts
434
439
  // more while others run, up to its limit, and follows its other plans meanwhile (v3 §6.2).
435
440
  const swarm = this.state.mode === 'swarm';
@@ -438,6 +443,32 @@ export class Orchestrator {
438
443
  return 'wait';
439
444
  return this.scheduleNext(active.length, limit);
440
445
  }
446
+ /**
447
+ * An API of the design whose key isn't stored on this machine pauses the pipeline, with
448
+ * what to do: every new sandbox carries the API's provider, and OpenShell refuses one
449
+ * that doesn't exist. A design change that adds an API leads here.
450
+ */
451
+ async apiKeysMissing() {
452
+ const keyed = this.design.config.network.apis.filter((api) => api.key);
453
+ if (keyed.length === 0)
454
+ return false;
455
+ if (!this.providerNames || this.now() - this.providerNames.at >= 60_000) {
456
+ try {
457
+ this.providerNames = { at: this.now(), names: await this.driver.providers() };
458
+ }
459
+ catch (error) {
460
+ // The gateway's trouble shows elsewhere; this check doesn't hold the pipeline for it.
461
+ this.log(`the providers could not be listed: ${error.message}`);
462
+ return false;
463
+ }
464
+ }
465
+ const project = this.design.config.project.name;
466
+ const missing = keyed.filter((api) => !this.providerNames.names.includes(apiProviderName(project, api.host)));
467
+ if (missing.length === 0)
468
+ return false;
469
+ this.pausePipeline(`no key stored on this machine for ${missing.map((api) => api.host).join(', ')}: run \`harness doctor\` in a terminal, which asks for it, then resume`);
470
+ return true;
471
+ }
441
472
  /** One attempt's next step. An error counts against this attempt alone, and interrupts it past the limit. */
442
473
  async stepAttempt(feature, attempt) {
443
474
  try {
@@ -1724,12 +1755,16 @@ export class Orchestrator {
1724
1755
  * A sandbox's providers, policy and settings. Agents get Claude; with AWS in the
1725
1756
  * project, they and the checks that need it also get the view role, which OpenShell
1726
1757
  * holds and signs with (§12.1). A provider is only attached where its endpoints are.
1758
+ * Every sandbox gets the keys of the project's APIs, for the code it runs.
1727
1759
  */
1728
1760
  access(options) {
1729
1761
  const aws = options.aws ? this.design.config.aws : undefined;
1730
1762
  const limits = this.state.sandboxLimits;
1763
+ const project = this.design.config.project.name;
1764
+ // The keys of the project's APIs, which OpenShell adds on requests to their hosts alone.
1765
+ const apiKeys = this.design.config.network.apis.flatMap((api) => (api.key ? [apiProviderName(project, api.host)] : []));
1731
1766
  return {
1732
- providers: [...(options.claude ? [this.design.config.sandbox.claudeProvider] : []), ...(aws ? [aws.provider] : [])],
1767
+ providers: [...(options.claude ? [this.design.config.sandbox.claudeProvider] : []), ...(aws ? [aws.provider] : []), ...apiKeys],
1733
1768
  policyFile: this.policyFile({ allBundles: options.allBundles ?? false, aws: aws !== undefined }),
1734
1769
  env: this.sandboxEnv(),
1735
1770
  // Every sandbox the daemon makes says whose it is: the dashboard tells them from other projects'.
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { z } from 'zod';
3
+ import { ALL_BUNDLES, BUNDLES } from '../sandbox/allowlist.js';
3
4
  import { canonicalJson, sha256 } from '../util.js';
4
5
  /** The agents' image is tagged with the harness's version, which may change what goes into it: `harness doctor --build-images` builds it. */
5
6
  const AGENT_IMAGE = `razzou-harness/agent:${JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version}`;
@@ -71,6 +72,14 @@ export function featureDependency(planId, dep) {
71
72
  const [a, b] = dep.split('/');
72
73
  return b === undefined ? { plan: planId, feature: a } : { plan: a, feature: b };
73
74
  }
75
+ /** Variables the harness, OpenShell or the system set in a sandbox: an API's key can't take one of their names. */
76
+ const RESERVED_VARIABLES = /^(AWS_|CLAUDE_|OPENSHELL_|HARNESS_|SSL_|NODE_|NPM_)|^(PATH|HOME|USER|SHELL|LANG|TERM|PWD|TMPDIR|REQUESTS_CA_BUNDLE|CURL_CA_BUNDLE|DISABLE_AUTOUPDATER|CHECKPOINT_DISABLE|MCP_TOOL_TIMEOUT)$/;
77
+ /** A third-party API of `network.apis`: an exact host, and the variable its key is read from, if it has one. */
78
+ const ApiSchema = z.object({
79
+ host: z.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/, 'a host name, like api.stripe.com'),
80
+ key: z.string().regex(/^[A-Z][A-Z0-9_]*$/, 'a variable name in capitals, like STRIPE_API_KEY')
81
+ .refine((name) => !RESERVED_VARIABLES.test(name), 'a variable the harness, OpenShell or the system sets; pick another name, like STRIPE_API_KEY').optional(),
82
+ }).strict();
74
83
  export const ConfigSchema = z.object({
75
84
  harnessVersion: z.string(),
76
85
  project: z.object({
@@ -117,8 +126,18 @@ export const ConfigSchema = z.object({
117
126
  .prefault({}),
118
127
  network: z
119
128
  .object({
120
- bundles: z.array(z.string()).default(['core']),
129
+ /** The hosts sandboxes reach directly, read-only, by bundle; `all`, the default, is every bundle. */
130
+ bundles: z.array(z.string().refine((name) => name === ALL_BUNDLES || name in BUNDLES, `a bundle: ${ALL_BUNDLES}, ${Object.keys(BUNDLES).join(', ')}`)).default([ALL_BUNDLES]),
121
131
  extraHosts: z.array(z.string()).default([]),
132
+ /**
133
+ * Third-party APIs sandboxes may call with any method, like a payment provider's test
134
+ * API. `key` names the variable the project's code reads the API's key from: in a
135
+ * sandbox it holds a placeholder, which OpenShell replaces with the key on requests to
136
+ * the host alone. `harness doctor` stores the key, at a hidden prompt.
137
+ */
138
+ apis: z.array(ApiSchema).default([])
139
+ .refine((apis) => new Set(apis.map((a) => a.host)).size === apis.length, 'each host once')
140
+ .refine((apis) => { const keys = apis.flatMap((a) => (a.key ? [a.key] : [])); return new Set(keys).size === keys.length; }, 'each key once'),
122
141
  })
123
142
  .prefault({}),
124
143
  checks: z.array(CheckSchema).default([]),
@@ -146,8 +165,8 @@ export const ConfigSchema = z.object({
146
165
  region: z.string().regex(/^[a-z]{2}(-[a-z]+)+-\d$/, 'an AWS region, like us-east-1'),
147
166
  /** The OpenShell provider holding the view role, like "aws-view-shop". */
148
167
  provider: z.string().min(1),
149
- /** The services the project uses: their endpoints are reachable, and signed. */
150
- services: z.array(z.string().regex(/^[a-z0-9-]+$/)).default(['sts', 's3', 'iam']),
168
+ /** The services whose endpoints are reachable, and signed; `all`, the default, is every one, so the view role alone decides. */
169
+ services: z.array(z.string().regex(/^[a-z0-9-]+$/)).default(['all']),
151
170
  })
152
171
  .optional(),
153
172
  /** Where the project's Terraform lives, and how a plan is made against the real state (§12.4). */
@@ -19,6 +19,12 @@ export const BUNDLES = {
19
19
  opentofu: read('registry.opentofu.org', 'opentofu.org'),
20
20
  browsers: read('cdn.playwright.dev', 'playwright.download.prss.microsoft.com'),
21
21
  };
22
+ /** Every bundle, those later versions add included: what a project gets unless it lists fewer. */
23
+ export const ALL_BUNDLES = 'all';
24
+ /** The bundles a project's list names, `all` standing for every one. */
25
+ export function bundleNames(names) {
26
+ return names.includes(ALL_BUNDLES) ? Object.keys(BUNDLES) : [...names];
27
+ }
22
28
  /** Throws on a host OpenShell would reject, so a bad entry fails at load time rather than at sandbox creation. */
23
29
  export function checkHost(host) {
24
30
  const labels = host.split('.');