razzou-harness 0.3.0 → 0.4.1

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,71 @@
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.1 (2026-11-08)
7
+
8
+ **Upgrading a project from 0.4.0:** install the new version, then rebuild the agents'
9
+ image with `harness doctor --build-images`: its tag follows the harness's version. A
10
+ project's network stays as its `harness/config.json` lists it; for every bundle, set
11
+ `"bundles": ["all"]`.
12
+
13
+ - **Every network bundle by default.** `"all"` in `network.bundles` stands for every
14
+ bundle, those later versions add included, and it is the default: `harness init`
15
+ writes it, and `harness setup` no longer asks which bundles the stack needs. The
16
+ sandboxes of every role reach every bundle's package registries and documentation,
17
+ read-only. A project narrows it by listing bundles. An unknown bundle now fails when
18
+ the config is read, not when a sandbox is made.
19
+ - **Third-party APIs**, in `network.apis`: the hosts the code may call with any method,
20
+ like a payment provider's test API. With `"key": "STRIPE_API_KEY"`, `harness doctor`
21
+ asks for the API's key at a hidden prompt and stores it in an OpenShell provider of
22
+ its own; every sandbox gets it, and holds a placeholder in that variable, which
23
+ OpenShell replaces with the key on requests to that host alone, in headers and URLs,
24
+ not in request bodies. While a key isn't stored on the machine, no new attempt
25
+ starts: the pipeline pauses and says so.
26
+
27
+ ## 0.4.0 (2026-11-08)
28
+
29
+ **Upgrading a project from 0.3.x:** install the new version, then rebuild the agents'
30
+ image with `harness doctor --build-images`, or run `harness setup`, which does it: the
31
+ image's tag follows the harness's version. The doctor now removes the 0.3.0 image once
32
+ no container uses it. Nothing else changes in a project.
33
+
34
+ - A **Sandboxes** view in the dashboard lists the sandboxes on the OpenShell gateway:
35
+ this project's, with what each is for and whether anything still uses it, then the
36
+ others'. A left-over sandbox of this project, which a crash or a failed delete left,
37
+ can be deleted there; one in use, or another project's, can't. Selecting a sandbox
38
+ shows the network requests its policy refused, from its log: the program, the host,
39
+ why, and how often. The daemon labels the sandboxes it makes with the project's name
40
+ (`harness.project`) to tell them apart; a left-over sandbox from 0.3.0 or earlier has
41
+ no label, so it shows among the others.
42
+ - The view shows what each sandbox uses of the machine now, its CPUs and memory against
43
+ its caps, and the total for every OpenShell sandbox, read from `docker stats` every
44
+ five seconds while the view is open.
45
+ - **Caps on each sandbox**, set in the Sandboxes view: how many CPUs and how much memory
46
+ each new sandbox may use, so that Developers, check runs and reviewers in parallel
47
+ leave the machine usable. The daemon records them, and creates every sandbox with
48
+ them from then on; those already there keep theirs. There is no cap until you set one.
49
+ - An attempt keeps the network requests its sandbox's policy refused. OpenShell keeps
50
+ only a sandbox's last 2,000 log lines, so the daemon reads the log every minute while
51
+ the Developer works, and once more before the sandbox goes. The attempt's card shows
52
+ them, and the Manager gets them with a failed attempt. A refused host is the
53
+ Manager's to resolve: it guides the next Developer around the host, or asks the owner
54
+ to allow it, since only the owner widens the network policy.
55
+
56
+ Disk space:
57
+ - The git bundles the daemon hands to sandboxes, each a copy of the plan branch's whole
58
+ history, are deleted once uploaded, and the ones it takes back once fetched. One was
59
+ kept for every attempt and every review, and never deleted, so `.harness/` grew with
60
+ each. The daemon deletes the ones earlier versions left in `.harness/modules/` when it
61
+ starts.
62
+ - The daemon deletes this project's left-over sandboxes when it starts and every hour:
63
+ those the Sandboxes view offers to delete, so a delete that failed is tried again. It
64
+ spares an Architect's, which holds the owner's conversation with it.
65
+ - `harness doctor --build-images` removes the agents' images the new one replaces, about
66
+ 2 GB each, once no container uses them: the other versions' tags, and the untagged
67
+ images that building a tag again leaves. It also shows the size of Docker's build
68
+ cache, with `docker builder prune` to free it; that cache is shared with every other
69
+ build on the machine, so the doctor leaves it alone.
70
+
6
71
  ## 0.3.0 (2026-11-07)
7
72
 
8
73
  **Upgrading a project from 0.2.x:** install the new version, then run `harness setup`
package/README.md CHANGED
@@ -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
 
@@ -261,7 +262,18 @@ behind a token. Its views:
261
262
  - **Project:** modules, proposals, plans and features, with attempts, findings, CI,
262
263
  deploys and `progress.txt`.
263
264
  - **Agents:** the Architect's terminal, the Manager's and the Developers' live
264
- transcripts, a message box for the Manager, and a shell in any agent's sandbox.
265
+ transcripts, a message box for the Manager, and a shell in any agent's sandbox. An
266
+ attempt's card keeps the network requests its sandbox's policy refused.
267
+ - **Sandboxes:** what runs on the OpenShell gateway. This project's sandboxes, with what
268
+ each is for, whether anything still uses it, and the CPUs and memory it uses now,
269
+ read from `docker stats` every five seconds. A left-over one, which a crash or a
270
+ failed delete left, has a button to delete it; the daemon deletes them itself when it
271
+ starts and every hour, except an Architect's. Select one to see the network requests
272
+ its policy refused, from its log: the program, the host, why, and how often. Then the
273
+ other projects' sandboxes, which the dashboard only lists, and the gateway's status.
274
+ Above them, the caps on each new sandbox: how many CPUs and how much memory it may
275
+ use, so that agents and checks in parallel leave the machine usable. There is no cap
276
+ until you set one.
265
277
  - **Events:** the event log, live, with a filter.
266
278
 
267
279
  The header pauses and resumes the pipeline: a pause lets what runs finish, and starts
@@ -421,14 +433,14 @@ through an `Arch:` pull request, or a commit of your own on `main`.
421
433
 
422
434
  ```json
423
435
  {
424
- "harnessVersion": "0.3.0",
436
+ "harnessVersion": "0.4.1",
425
437
  "project": { "name": "shop", "repo": "<org>/shop", "bot": "<org>-harness[bot]" },
426
438
  "roles": {
427
439
  "architect": { "model": "claude-opus-5-5", "effort": "max" },
428
440
  "manager": { "model": "claude-opus-5-5", "effort": "high" },
429
441
  "developer": { "model": "claude-opus-5-5", "effort": "xhigh", "attemptTimeoutMinutes": 90, "maxAttempts": 2 }
430
442
  },
431
- "network": { "bundles": ["core", "node"], "extraHosts": [] },
443
+ "network": { "bundles": ["all"], "apis": [{ "host": "api.stripe.com", "key": "STRIPE_API_KEY" }] },
432
444
  "checks": [{ "id": "unit", "run": "npm test", "phase": "pre-merge" }]
433
445
  }
434
446
  ```
@@ -441,8 +453,9 @@ through an `Arch:` pull request, or a commit of your own on `main`.
441
453
  | `roles.<role>.effort` | `max`, `high`, `xhigh` | Claude Code's `--effort`; `null` for a model without effort levels, such as Haiku |
442
454
  | `roles.developer.attemptTimeoutMinutes` | 120 | An attempt's deadline |
443
455
  | `roles.developer.maxAttempts` | 2 | Attempts per feature before it is escalated to you |
444
- | `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. |
445
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. |
446
459
  | `checks` | `[]` | Checks that every feature must keep passing, such as `npm run lint` |
447
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"]}` |
448
461
  | `sandbox.image` | `razzou-harness/agent:<version>` | The agents' image, tagged with the harness's version, which `harness doctor --build-images` builds |
@@ -570,7 +583,8 @@ RDS, `manage_master_user_password`); anything else uses write-only arguments.
570
583
 
571
584
  What agents can't do, and what stops them:
572
585
  - **Hold credentials.** Sandboxes hold placeholders. The OpenShell proxy adds the Claude
573
- token on `api.anthropic.com` only, and signs AWS requests with the read-only role.
586
+ token on `api.anthropic.com` only, each key of `network.apis` on its API's host only,
587
+ and signs AWS requests with the read-only role.
574
588
  - **Reach the host.** Each agent runs in its own container, with no host folder mounted;
575
589
  code moves in and out as git bundles. The host never runs code or git hooks that an
576
590
  agent wrote.
@@ -583,7 +597,8 @@ What agents can't do, and what stops them:
583
597
  They change only through an Arch PR you merge, or your own commits.
584
598
  - **Change AWS.** A read-only role with explicit denies, and applies only in CI after
585
599
  your merge.
586
- - **Go anywhere online.** Sandboxes reach listed hosts, read-only. Other pages go
600
+ - **Go anywhere online.** Sandboxes reach listed hosts, read-only, and the APIs in
601
+ `network.apis` with any method; OpenShell has no rule for every host. Other pages go
587
602
  through `web_read`: GET only, public addresses only, never this machine's own
588
603
  addresses or the networks it is on (IPv6 included), checked again after every
589
604
  redirect, within 75 seconds and 2 MB, decompressed.
@@ -56,7 +56,7 @@ export class Architects {
56
56
  throw new Error(`could not take the design: ${(result.stderr || result.stdout).trim()}`);
57
57
  const local = join(this.localDir(module.module), 'design.bundle');
58
58
  await o.driver.download(module.architect.sandbox, `${p.out}/design.bundle`, local);
59
- return { head: await o.repo.fetchBundle(local, `refs/architect/${module.module}`) };
59
+ return { head: await o.fetchBundle(local, `refs/architect/${module.module}`) };
60
60
  }
61
61
  async syncModule(module) {
62
62
  const o = this.o;
@@ -93,7 +93,7 @@ export class Architects {
93
93
  const branch = `arch/${module.module}`;
94
94
  const reviewing = module.status === 'in_review' && (await o.repo.revParse(`refs/heads/${branch}`)) !== undefined;
95
95
  await o.repo.bundle(reviewing ? ['refs/heads/main', `refs/heads/${branch}`] : 'refs/heads/main', join(dir, 'main.bundle'));
96
- await o.driver.upload(sandbox, join(dir, 'main.bundle'), p.harness);
96
+ await o.uploadBundle(sandbox, join(dir, 'main.bundle'), p.harness);
97
97
  const clone = await o.exec(sandbox, [
98
98
  `git clone --quiet --branch main ${sh(`${p.harness}/main.bundle`)} ${sh(p.work)}`,
99
99
  `cd ${sh(p.work)}`,
@@ -199,7 +199,7 @@ export class Architects {
199
199
  const p = o.paths(module.architect.sandbox);
200
200
  const local = join(this.localDir(module.module), 'arch.bundle');
201
201
  await o.repo.bundle(`refs/heads/arch/${module.module}`, local);
202
- await o.driver.upload(module.architect.sandbox, local, p.harness);
202
+ await o.uploadBundle(module.architect.sandbox, local, p.harness);
203
203
  const result = await o.exec(module.architect.sandbox, [
204
204
  `cd ${sh(p.work)}`,
205
205
  `git fetch --quiet --force ${sh(`${p.harness}/arch.bundle`)} ${sh(`refs/heads/arch/${module.module}:refs/remotes/harness/arch`)}`,
@@ -252,7 +252,7 @@ export class Architects {
252
252
  await o.forge.fetchMain(o.repo);
253
253
  const local = join(this.localDir(module.module), 'main.bundle');
254
254
  await o.repo.bundle('refs/heads/main', local);
255
- await o.driver.upload(module.architect.sandbox, local, p.harness);
255
+ await o.uploadBundle(module.architect.sandbox, local, p.harness);
256
256
  await o.exec(module.architect.sandbox, [
257
257
  `cd ${sh(p.work)}`,
258
258
  `git fetch --quiet --force ${sh(`${p.harness}/main.bundle`)} refs/heads/main:refs/heads/main`,
@@ -116,6 +116,7 @@ function reviewItem(o, feature, attempt) {
116
116
  `The Developer reports done: ${attempt.summary ?? '(no summary)'}`,
117
117
  ...(attempt.notes ? ['', `Its notes: ${attempt.notes}`] : []),
118
118
  ...questionsAsked(attempt),
119
+ ...refusedLines(attempt, false),
119
120
  '',
120
121
  'Every check passed in a fresh sandbox:',
121
122
  ...checkLines(attempt.checks ?? []),
@@ -155,6 +156,7 @@ function failureItem(o, feature, attempt) {
155
156
  ? ['', `Its finding (${finding.category}): ${finding.summary}`, ...(finding.tried?.length ? [`Tried: ${finding.tried.join('; ')}`] : []), ...(finding.needs ? [`Needs: ${finding.needs}`] : [])]
156
157
  : []),
157
158
  ...questionsAsked(attempt),
159
+ ...refusedLines(attempt, true),
158
160
  ...(failing.length ? ['', 'Failing checks:', ...checkLines(failing, true)] : []),
159
161
  '',
160
162
  head
@@ -245,6 +247,26 @@ function questionsAsked(attempt) {
245
247
  return [];
246
248
  return ['', 'Its questions:', ...asked.map((q) => `- ${clip(q.text, 500)} → ${q.answer === undefined ? '(unanswered)' : clip(q.answer, 500)}`)];
247
249
  }
250
+ /**
251
+ * The requests the attempt's sandbox policy refused. A host the feature needs and the
252
+ * policy doesn't allow explains many failures; the Manager resolves it, around the host
253
+ * or with the owner, who alone widens the policy.
254
+ */
255
+ function refusedLines(attempt, failed) {
256
+ const refused = attempt.refused ?? [];
257
+ if (refused.length === 0)
258
+ return [];
259
+ const shown = refused.slice(0, 12);
260
+ return [
261
+ '',
262
+ "Its sandbox's network policy refused:",
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
+ ...(refused.length > shown.length ? [`- and ${refused.length - shown.length} more`] : []),
265
+ ...(failed
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
+ : []),
268
+ ];
269
+ }
248
270
  function checkLines(results, withOutput = false) {
249
271
  return results.flatMap((r) => {
250
272
  const verdict = r.exit === 0 ? 'passed' : r.timedOut ? 'timed out' : `exited ${r.exit}`;
@@ -132,7 +132,8 @@ export class Managers {
132
132
  await o.repo.bundle(`refs/heads/${branch}`, join(dir, 'clone.bundle'));
133
133
  copyFileSync(join(o.options.packageDir, 'roles', 'manager.md'), join(dir, 'manager.md'));
134
134
  writeFileSync(join(dir, 'settings.json'), managerSettings());
135
- for (const file of ['clone.bundle', 'manager.md', 'settings.json'])
135
+ await o.uploadBundle(manager.sandbox, join(dir, 'clone.bundle'), p.harness);
136
+ for (const file of ['manager.md', 'settings.json'])
136
137
  await o.driver.upload(manager.sandbox, join(dir, file), p.harness);
137
138
  await o.uploadAgentFiles(manager.sandbox, 'manager', dir);
138
139
  const clone = await o.exec(manager.sandbox, `git clone --quiet --branch ${sh(branch)} ${sh(`${p.harness}/clone.bundle`)} ${sh(p.work)}`);
@@ -254,7 +255,7 @@ export class Managers {
254
255
  const local = join(this.localDir(manager), 'review.bundle');
255
256
  await o.repo.bundle([`refs/heads/${branch}`, ...(head && attemptRef ? [attemptRef] : [])], local);
256
257
  const p = this.paths(manager);
257
- await o.driver.upload(manager.sandbox, local, p.harness);
258
+ await o.uploadBundle(manager.sandbox, local, p.harness);
258
259
  const result = await o.exec(manager.sandbox, [
259
260
  `cd ${sh(p.work)}`,
260
261
  `git fetch --quiet --force ${sh(`${p.harness}/review.bundle`)} '+refs/heads/*:refs/remotes/harness/*' '+refs/attempts/*:refs/remotes/harness/attempts/*'`,