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 +49 -0
- package/README.md +27 -18
- package/dist/daemon/items.js +1 -1
- package/dist/daemon/orchestrator.js +38 -3
- package/dist/design/schemas.js +22 -3
- package/dist/sandbox/allowlist.js +6 -0
- package/dist/sandbox/aws-services.js +428 -0
- package/dist/sandbox/openshell.js +15 -0
- package/dist/sandbox/policy.js +75 -17
- package/dist/setup/doctor.js +68 -2
- package/dist/setup/init.js +1 -1
- package/dist/setup/setup.js +9 -28
- package/dist/web/assets/{index-hRWASvyl.js → index-CLq7s9N9.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/roles/architect.md +6 -0
- package/roles/developer.md +4 -1
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.
|
|
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,
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
173
|
-
stack
|
|
174
|
-
|
|
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.
|
|
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": ["
|
|
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` | `["
|
|
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": ["
|
|
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
|
|
564
|
-
|
|
565
|
-
|
|
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,
|
|
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
|
|
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.
|
|
643
|
-
| `OpenShell gateway` fails | `openshell gateway
|
|
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 |
|
package/dist/daemon/items.js
CHANGED
|
@@ -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.
|
|
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'.
|
package/dist/design/schemas.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
150
|
-
services: z.array(z.string().regex(/^[a-z0-9-]+$/)).default(['
|
|
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('.');
|