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 +65 -0
- package/README.md +27 -12
- package/dist/daemon/architects.js +4 -4
- package/dist/daemon/items.js +22 -0
- package/dist/daemon/manager.js +3 -2
- package/dist/daemon/orchestrator.js +195 -30
- package/dist/daemon/review.js +1 -1
- package/dist/daemon/sandboxes.js +115 -0
- package/dist/dashboard/server.js +24 -1
- package/dist/design/schemas.js +20 -1
- package/dist/sandbox/allowlist.js +6 -0
- package/dist/sandbox/docker.js +66 -0
- package/dist/sandbox/driver.js +2 -1
- package/dist/sandbox/openshell.js +59 -0
- package/dist/sandbox/policy.js +39 -4
- package/dist/sandbox/refusals.js +92 -0
- package/dist/setup/doctor.js +78 -0
- package/dist/setup/images.js +41 -0
- package/dist/setup/init.js +1 -1
- package/dist/setup/setup.js +5 -24
- package/dist/state/state.js +11 -0
- package/dist/util.js +5 -0
- package/dist/web/assets/index-CLq7s9N9.js +96 -0
- package/dist/web/assets/{index-CMViLVQH.css → index-CRYKs8kt.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/roles/architect.md +6 -0
- package/roles/developer.md +4 -1
- package/roles/manager.md +8 -0
- package/dist/web/assets/index-CPJJsPL6.js +0 -96
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,
|
|
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
|
|
|
@@ -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.
|
|
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": ["
|
|
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` | `["
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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`,
|
package/dist/daemon/items.js
CHANGED
|
@@ -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}`;
|
package/dist/daemon/manager.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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/*'`,
|