software-defence-factory 0.8.0 → 0.9.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/THIRD_PARTY_NOTICES.md +10 -0
- package/bin/software-defence-factory.mjs +15 -2
- package/docs/interfaces.md +1 -0
- package/docs/recovery.md +15 -0
- package/docs/setup.md +8 -0
- package/docs/web-verification.md +167 -0
- package/factory/definition.mjs +9 -2
- package/factory/delivery.mjs +21 -1
- package/factory/execution-evidence.mjs +5 -1
- package/factory/execution-profile.mjs +8 -1
- package/factory/executor.mjs +186 -24
- package/factory/lib.mjs +2 -0
- package/factory/processes.mjs +8 -2
- package/factory/queue.mjs +2 -0
- package/factory/server.mjs +35 -9
- package/factory/ui/assets/index-CLrogux1.css +1 -0
- package/factory/ui/assets/index-Cjk_XtEU.js +13 -0
- package/factory/ui/index.html +2 -2
- package/factory/updates.mjs +1 -1
- package/factory/web/Dockerfile +6 -0
- package/factory/web/containers.mjs +189 -0
- package/factory/web/package-lock.json +42 -0
- package/factory/web/package.json +9 -0
- package/factory/web/qualification-fixture.mjs +97 -0
- package/factory/web/readiness.mjs +26 -0
- package/factory/web/runner.mjs +236 -0
- package/factory/web-readiness.mjs +94 -0
- package/factory/web-verification.mjs +250 -0
- package/package.json +3 -1
- package/scripts/probe-web.mjs +104 -0
- package/factory/ui/assets/index-B13yiAWU.css +0 -1
- package/factory/ui/assets/index-BuZQnEyv.js +0 -13
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -152,6 +152,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
152
152
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
153
153
|
SOFTWARE.
|
|
154
154
|
|
|
155
|
+
|
|
155
156
|
## @radix-ui/react-compose-refs 1.1.5
|
|
156
157
|
|
|
157
158
|
MIT License
|
|
@@ -574,3 +575,12 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
574
575
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
575
576
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
576
577
|
SOFTWARE.
|
|
578
|
+
|
|
579
|
+
## Optional browser verification image
|
|
580
|
+
|
|
581
|
+
The operator-built Playwright runner image installs `playwright` and
|
|
582
|
+
`playwright-core` 1.63.0 from the exact lockfile in `factory/web/` under the
|
|
583
|
+
Apache License 2.0. The original license files are retained in the built image
|
|
584
|
+
under `/opt/factory-web/node_modules/playwright/LICENSE` and
|
|
585
|
+
`/opt/factory-web/node_modules/playwright-core/LICENSE`. Factory does not bundle
|
|
586
|
+
the built browser image in its npm package.
|
|
@@ -16,6 +16,7 @@ import { bootstrap, registerInstallation, VERSION } from '../factory/updates.mjs
|
|
|
16
16
|
import { hasService, manageService, serviceDefinition, withServiceOperation, isManagedLaunch } from '../factory/services.mjs';
|
|
17
17
|
import { runCandidateGit } from '../factory/git-environment.mjs';
|
|
18
18
|
import { initializeDemoRepository } from '../factory/demo-fixture.mjs';
|
|
19
|
+
import { probeWebBrowser } from '../factory/web-readiness.mjs';
|
|
19
20
|
|
|
20
21
|
try {
|
|
21
22
|
const handled = await bootstrap(process.argv.slice(2));
|
|
@@ -190,8 +191,9 @@ try {
|
|
|
190
191
|
else if(command==='status') { const snapshot=await api(state,'/api/v1/status');delete snapshot.csrf_token;console.log(JSON.stringify(snapshot,null,2)); }
|
|
191
192
|
else if(command==='doctor') {
|
|
192
193
|
const config=configAt(state),dockerVersion=run('docker',['info','--format','{{.ServerVersion}}']),imageStatus=inspectImageInstallation(state,config);
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
const webVerification=config.webVerification?.enabled?probeWebBrowser(config,state):null;
|
|
195
|
+
console.log(JSON.stringify({node:process.version,docker:dockerVersion,engineInstalled:imageStatus.installed,image:imageStatus.image,repo:config.repo,harness:harnessOf(config),agent:harnessOf(config),checksConfigured:!!config.check?.trim(),inference:'Not called or verified',qualification:{model:'not assessed',toolchain:'not assessed'},dashboard:`http://127.0.0.1:${config.port}`,...(webVerification?{web_verification:webVerification}:{})},null,2));
|
|
196
|
+
if(!imageStatus.installed||webVerification&&!webVerification.ready)process.exitCode=1;
|
|
195
197
|
} else if(command==='issue') {
|
|
196
198
|
const action=positional[0], sourceURL=flags.url || flags.github;
|
|
197
199
|
if(action==='list') {
|
|
@@ -263,11 +265,20 @@ try {
|
|
|
263
265
|
initializeDemoRepository(repo);
|
|
264
266
|
init(repo,'mock',"test \"$(cat value.txt)\" = fixed",Number(flags.port || 7332));
|
|
265
267
|
} else if(harnessOf(configAt(state))!=='mock')throw new Error('Demo requires a mock configuration');
|
|
268
|
+
save(join(state,'synthetic-demo.json'),{version:1,createdAt:new Date().toISOString(),purpose:'Disposable Factory runtime qualification only'});
|
|
266
269
|
await withServiceOperation('demo startup',async()=>{await install();await up();});console.log(JSON.stringify(await submit('software','Synthetic installation qualification: fix value.txt. No inference is used.')));
|
|
267
270
|
console.log('Review the synthetic change in the dashboard and approve its handoff.');
|
|
268
271
|
} else if(['version','--version','-v'].includes(command))console.log(VERSION);
|
|
269
272
|
else if(command==='qualify') {
|
|
270
273
|
await stream(process.execPath,[join(ROOT,'scripts/probe-platform.mjs'),state]);
|
|
274
|
+
} else if(command==='qualify-web') {
|
|
275
|
+
if(!flags.image)throw new Error('qualify-web requires --image sha256:<local browser image ID>');
|
|
276
|
+
await stream(process.execPath,[join(ROOT,'scripts/probe-web.mjs'),state,flags.image]);
|
|
277
|
+
} else if(command==='web') {
|
|
278
|
+
if(positional[0]!=='probe'||positional.length!==1)throw new Error('Use web probe to execute the configured browser readiness check');
|
|
279
|
+
const readiness=probeWebBrowser(configAt(state),state);
|
|
280
|
+
console.log(JSON.stringify(readiness,null,2));
|
|
281
|
+
if(readiness.enabled&&!readiness.ready)process.exitCode=1;
|
|
271
282
|
} else if(command==='kit') {
|
|
272
283
|
if(!flags.output)throw new Error('kit requires --output NEW_DIRECTORY');
|
|
273
284
|
await stream(process.execPath,[join(ROOT,'scripts/export-kit.mjs'),resolve(flags.output)]);
|
|
@@ -276,11 +287,13 @@ try {
|
|
|
276
287
|
kit --output NEW_DIRECTORY Export the portable method without a runtime
|
|
277
288
|
demo Install and run a synthetic sample (no model key)
|
|
278
289
|
qualify --state PATH Exercise recovery and isolation with a stopped demo job
|
|
290
|
+
qualify-web --state PATH --image ID Exercise Playwright Verify with a synthetic delayed-action fixture
|
|
279
291
|
init --repo PATH --harness codex|pi|custom --check "npm ci && npm test" [--source-ref REF]
|
|
280
292
|
[--inference-provider PROVIDER]
|
|
281
293
|
[--delivery-provider github --delivery-repository https://github.com/OWNER/REPO --delivery-target main|dev]
|
|
282
294
|
install [--image LOCAL_REF] Build the standard image, or select an existing local image
|
|
283
295
|
doctor | up | status | stop Inspect / operate your private installation
|
|
296
|
+
web probe --state PATH Execute the pinned local Chromium readiness probe
|
|
284
297
|
foundation Read the operator setup skill; no installation required
|
|
285
298
|
definition | agents | skills Inspect roles, instructions and installation settings
|
|
286
299
|
inbox | infrastructure | automations Inspect live tasks, host/worker and automation state
|
package/docs/interfaces.md
CHANGED
|
@@ -34,6 +34,7 @@ shell endpoint or a second scheduler.
|
|
|
34
34
|
| Synthetic qualification | `demo`, `qualify` | No qualification endpoint | Synthetic disclosure only | Explicit separate state; never target an application accidentally |
|
|
35
35
|
| Immutable source admission | `init --source-ref`, `run --source-ref`, `issue start --source-ref`; status and build evidence carry the resolved SHA | `POST /api/v1/jobs` resolves/retains before acknowledgement; shared source metadata in status | New issue and revision forms accept a ref; task detail shows requested ref, resolved SHA and prior source commits | Build/retry use retained objects; revisions keep the recorded source unless a new ref is explicit; legacy source remains unknown |
|
|
36
36
|
| Trusted PR handoff | `publish JOB_ID` publishes/reconciles; `abandon-delivery JOB_ID --branch-sha SHA` records a checked local resolution for a pre-write branch collision | Authenticated `POST /api/v1/jobs/:id/publish` and `/abandon-delivery`; shared receipt, conflict identity and removal policy | Publish/reconcile and explicit “Abandon local delivery; keep remote branch” actions share controller state; errors/results and inspected branch identity are visible | New writes require matching protected Codex/Pi build/review provenance, deterministic verify/handoff provenance, non-synthetic bound artifacts and a qualified GitHub Actions tree. Shared delivery status exposes `workflow_qualification` and the same reason blocks CLI/API/dashboard capability and publication/retry. Candidate workflow changes, unsupported triggers/syntax, or active generated-push, selected-ref-dispatch and PR jobs with write/secrets/environment/OIDC/deploy access, self-hosted runners or ambiguous privileged guards refuse trusted writes. Supported ASCII guard comparisons follow GitHub's case-insensitive string semantics; unknown PR refs, non-ASCII mismatches, and glob/escaped branch filters cannot prove a privileged job inactive. The shared summary's `action_mode` distinguishes new/resumable publication from read-only reconciliation and drives idle and pending task button wording. Branch-only collisions and unknown/abandoned states offer neither; known PR receipts and pending PR-creation checkpoints retain read-only reconciliation. Abandonment checks the current run, saved intent, exact branch head and absence of an associated PR; it writes no provider data, preserves the remote branch/evidence, disables republishing and permits local removal. Uncertain effects and incompatible evidence stay blocked. Destination remains private operator config; patch-only remains default |
|
|
37
|
+
| Optional trusted web verification | `web probe` performs a real local Chromium interaction; `doctor` reports readiness | Verify stores a shared story summary and protected JSON artifact in the run | Task history shows passed/failed/unavailable/inconclusive plus tool, candidate, policy and story hashes | Disabled by default. Required operator stories and Playwright/Chromium image ID are frozen in attempt policy. Linux Chromium proof cannot qualify native/mobile OS behavior; see [the browser contract](web-verification.md) |
|
|
37
38
|
|
|
38
39
|
The current generic task form can name the Defence workflow; that is not a
|
|
39
40
|
substitute for the CLI's validated incident admission. Treat the typed intake
|
package/docs/recovery.md
CHANGED
|
@@ -29,6 +29,21 @@ alongside the cleanup error.
|
|
|
29
29
|
Inspect that retained attempt before manual removal; never substitute the source
|
|
30
30
|
candidate path for the scratch path.
|
|
31
31
|
|
|
32
|
+
When trusted web verification is enabled, Verify keeps the copied check output
|
|
33
|
+
as disposable preview scratch and creates separate preview and browser
|
|
34
|
+
containers. The preview uses `network none`; the browser shares only that exact
|
|
35
|
+
preview's isolated network namespace for loopback. Their mounts and PID
|
|
36
|
+
namespaces remain separate, and only the browser mounts its private result and
|
|
37
|
+
screenshot directory. The `web-policy.json`, preview scratch, browser output
|
|
38
|
+
and active fence remain until both exact labelled containers are confirmed
|
|
39
|
+
removed. Normal stop/retry recovery reconciles all Factory-labelled containers
|
|
40
|
+
before removing those files. An unknown stop, deadline or cancellation keeps
|
|
41
|
+
the state and fence for recovery. A missing browser, unsupported Linux
|
|
42
|
+
capability, interrupted story or malformed evidence blocks acceptance; never
|
|
43
|
+
retry around a retained writer or reuse earlier story evidence for a changed
|
|
44
|
+
candidate or policy. Browser traces and screenshots are private evidence and
|
|
45
|
+
are presented through the shared interfaces. See [the web contract](web-verification.md).
|
|
46
|
+
|
|
32
47
|
## Review feedback or phase retry
|
|
33
48
|
|
|
34
49
|
Use **Request changes** on a failed software review only when its validated
|
package/docs/setup.md
CHANGED
|
@@ -169,6 +169,14 @@ Checkpoint: record the exact source revision, image ID, check command, resource
|
|
|
169
169
|
limits, inference connectivity and CI result. Keep product controllers stopped
|
|
170
170
|
until their tasks are explicitly ready to run.
|
|
171
171
|
|
|
172
|
+
Optional web verification is configured separately from the selected harness
|
|
173
|
+
and model. The execution-host owner prepares and pins the Playwright/Chromium
|
|
174
|
+
image, writes trusted stories in private `factory.json`, then runs `web probe`
|
|
175
|
+
and `doctor` on that installation. Use [the browser setup and story contract](web-verification.md)
|
|
176
|
+
for the recipe, dependency preparation and limits. Verify uses separate isolated
|
|
177
|
+
Linux preview and browser containers and cannot qualify native/mobile operating
|
|
178
|
+
systems.
|
|
179
|
+
|
|
172
180
|
### Optional trusted PR delivery
|
|
173
181
|
|
|
174
182
|
Patch-only handoff is the default. When the operator intends to enable GitHub
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Optional trusted web verification
|
|
2
|
+
|
|
3
|
+
Browser verification is an operator-controlled extension of the existing
|
|
4
|
+
Verify phase. It is disabled when `webVerification` is absent. Issue text,
|
|
5
|
+
candidate files and generated tests cannot enable it or change its required
|
|
6
|
+
stories. The selected harness and model remain independent of this capability.
|
|
7
|
+
|
|
8
|
+
## Prepare and qualify Playwright/Chromium
|
|
9
|
+
|
|
10
|
+
The first adapter uses the Playwright 1.63.0 package and Chromium version from
|
|
11
|
+
the matching Microsoft Playwright image. Build it explicitly on the execution
|
|
12
|
+
host from the installed Factory package (or a trusted Factory source checkout):
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
docker build --pull -f factory/web/Dockerfile \
|
|
16
|
+
-t software-defence-factory-playwright:1.63.0 factory/web
|
|
17
|
+
docker image inspect --format '{{.Id}}' software-defence-factory-playwright:1.63.0
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Copy the resulting `sha256:…` image ID into private `factory.json`. The
|
|
21
|
+
Dockerfile uses the versioned `mcr.microsoft.com/playwright:v1.63.0-noble`
|
|
22
|
+
base and `npm ci` with the committed lockfile. Factory resolves no tag, pulls
|
|
23
|
+
no image, installs no package and falls back to no other browser during a job.
|
|
24
|
+
Rebuild and deliberately select a new image ID when upgrading the base or
|
|
25
|
+
Playwright version.
|
|
26
|
+
|
|
27
|
+
For a global npm installation, resolve the packaged recipe directory first:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
factory_package="$(npm root -g)/software-defence-factory"
|
|
31
|
+
docker build --pull -f "$factory_package/factory/web/Dockerfile" \
|
|
32
|
+
-t software-defence-factory-playwright:1.63.0 "$factory_package/factory/web"
|
|
33
|
+
browser_image="$(docker image inspect --format '{{.Id}}' software-defence-factory-playwright:1.63.0)"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run the readiness check from the selected installation:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
software-defence-factory web probe --state /private/state/project
|
|
40
|
+
software-defence-factory doctor --state /private/state/project
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The probe runs a real headless Chromium click and result assertion in a bounded,
|
|
44
|
+
read-only, `network none` Docker container as the controller UID. Its real
|
|
45
|
+
wall-clock deadline stops the exact labelled probe, and readiness is reported
|
|
46
|
+
only after that container is confirmed removed. A missing image, unsupported
|
|
47
|
+
adapter/version or browser launch failure reports unavailable. The probe never
|
|
48
|
+
downloads or builds an image.
|
|
49
|
+
|
|
50
|
+
## Configure trusted stories
|
|
51
|
+
|
|
52
|
+
Stop the installation before editing its private `factory.json`. Add an
|
|
53
|
+
operator-authored value like this and adapt names, routes, actions and result
|
|
54
|
+
text to the application:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"webVerification": {
|
|
59
|
+
"enabled": true,
|
|
60
|
+
"adapter": "playwright",
|
|
61
|
+
"version": "1.63.0",
|
|
62
|
+
"image": "sha256:REPLACE_WITH_LOCAL_IMAGE_ID",
|
|
63
|
+
"previewCommand": ["npm", "run", "preview", "--", "--host", "127.0.0.1", "--port", "4173"],
|
|
64
|
+
"port": 4173,
|
|
65
|
+
"timeoutSeconds": 90,
|
|
66
|
+
"themes": ["light", "dark"],
|
|
67
|
+
"stories": []
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`stories` is a small declarative contract; the empty array in this sketch is a
|
|
73
|
+
placeholder and is rejected until the complete trusted story set is supplied.
|
|
74
|
+
Every selected theme requires an interactive `desktop-<theme>` and
|
|
75
|
+
`narrow-<theme>` story. All installations
|
|
76
|
+
also require these named stories, using the first configured theme at desktop
|
|
77
|
+
size:
|
|
78
|
+
|
|
79
|
+
- `busy-disabled`: click an action, observe its disabled state, wait for it to
|
|
80
|
+
become enabled, and check the result text.
|
|
81
|
+
- `result`: exercise an action and assert its result text.
|
|
82
|
+
- `failure-retry`: assert a `role: "alert"` failure, click Retry, then assert a
|
|
83
|
+
`role: "status"` result.
|
|
84
|
+
- `keyboard-focus`: press a supported key and assert the expected control has
|
|
85
|
+
focus.
|
|
86
|
+
|
|
87
|
+
Each story names `viewport` (`desktop` or `narrow`), `theme` (`light` or
|
|
88
|
+
`dark`), a local `path` beginning with `/`, and ordered `steps`. Supported
|
|
89
|
+
steps are `click`, `expect-visible`, `expect-enabled`, `expect-disabled`,
|
|
90
|
+
`expect-text`, `press` and `expect-focused`. Actions and assertions use exact
|
|
91
|
+
accessible roles and names. Arbitrary selectors, JavaScript, source tests and
|
|
92
|
+
candidate-supplied story files are not accepted. `factory/web/qualification-fixture.mjs`
|
|
93
|
+
shows the complete eight-story contract used by the disposable installed
|
|
94
|
+
qualification.
|
|
95
|
+
|
|
96
|
+
`execution.json` freezes and hashes this complete operator configuration with
|
|
97
|
+
the attempt policy. The check evidence records the candidate head/tree, Verify
|
|
98
|
+
attempt, policy hash, immutable browser image ID, adapter/tool/browser versions,
|
|
99
|
+
story-set hash and each story's content hash. Any changed tree, tool, story or
|
|
100
|
+
policy makes prior evidence stale. Missing, failed, unavailable, inconclusive
|
|
101
|
+
or malformed required evidence blocks Review, handoff and trusted PR delivery.
|
|
102
|
+
The run summary and `web-verification.json` artifact are shared through CLI,
|
|
103
|
+
API and dashboard; the dashboard renders traces as text and retained PNGs as
|
|
104
|
+
images. It does not execute candidate HTML in the controller origin.
|
|
105
|
+
|
|
106
|
+
## Package and preview guidance
|
|
107
|
+
|
|
108
|
+
Use the configured project check for dependency preparation and build output.
|
|
109
|
+
For Node projects, install from the committed lockfile with the project's
|
|
110
|
+
frozen install command (for example `npm ci --ignore-scripts`), run the build,
|
|
111
|
+
then configure `previewCommand` to start the already-prepared local preview
|
|
112
|
+
from the project root in the disposable check copy.
|
|
113
|
+
The preview phase reuses that check's disposable scratch copy. It does not run
|
|
114
|
+
another install or contact a package registry. The qualified project job image
|
|
115
|
+
provides the preview runtime. Do not add secrets to the preview environment or
|
|
116
|
+
image.
|
|
117
|
+
|
|
118
|
+
Verify starts two containers with separate mount and PID namespaces. The
|
|
119
|
+
preview uses the configured project job image, the candidate at `/workspace`
|
|
120
|
+
read-only, and the writable `/scratch` copy. It runs as the controller UID with
|
|
121
|
+
`network none`, dropped capabilities, a read-only root and bounded resources.
|
|
122
|
+
The browser uses the pinned Playwright image as the same UID, with its own
|
|
123
|
+
read-only root, writable temporary profile and a separate private output mount.
|
|
124
|
+
It receives the frozen trusted stories on stdin and does not mount the
|
|
125
|
+
candidate, project scratch or controller results. The browser shares only the
|
|
126
|
+
exact preview container's `network none` namespace to reach its loopback port;
|
|
127
|
+
each container retains its own PID namespace. Neither container gets host
|
|
128
|
+
networking, published ports, extra capabilities, a Docker socket, controller,
|
|
129
|
+
forge or inference credentials, or a user profile.
|
|
130
|
+
|
|
131
|
+
The controller enforces a real wall-clock deadline, stops and reconciles both
|
|
132
|
+
exact labelled containers, then reads the bounded result and PNG screenshots
|
|
133
|
+
from the browser-only mount. The private result contains candidate/tree,
|
|
134
|
+
attempt, policy, pinned tool/image and story hashes, bounded action traces and
|
|
135
|
+
screenshot hashes. CLI, API and dashboard expose the same evidence; screenshots
|
|
136
|
+
can be viewed as images, and no candidate HTML is served on the controller
|
|
137
|
+
origin. Chromium is headless and uses the outer Docker boundary; this is not a
|
|
138
|
+
hosted browser service.
|
|
139
|
+
|
|
140
|
+
This adapter proves only Linux Chromium interaction with a local web preview.
|
|
141
|
+
It does not qualify Android/iOS, desktop-native applications, Safari, Firefox,
|
|
142
|
+
mobile operating systems, production access or an application's overall
|
|
143
|
+
correctness. Deterministic checks and independent Review remain separate.
|
|
144
|
+
|
|
145
|
+
## Installed synthetic fixture qualification
|
|
146
|
+
|
|
147
|
+
The release lead owns this Docker/browser qualification on the installed
|
|
148
|
+
execution host. Use a separate demo state, complete and inspect the demo's
|
|
149
|
+
normal sample handoff, stop that controller, then run the exact qualification
|
|
150
|
+
command with the already-built browser image ID:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
software-defence-factory demo --state /private/state/sdf-web-proof --port 7348
|
|
154
|
+
# Review and approve the synthetic demo task in its dashboard.
|
|
155
|
+
software-defence-factory stop --state /private/state/sdf-web-proof
|
|
156
|
+
browser_image="$(docker image inspect --format '{{.Id}}' software-defence-factory-playwright:1.63.0)"
|
|
157
|
+
software-defence-factory qualify-web --state /private/state/sdf-web-proof --image "$browser_image"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The installed command probes actual Chromium interaction, then runs a
|
|
161
|
+
disposable delayed-save/failure-retry fixture through mock Build/Review and
|
|
162
|
+
real Docker Verify. Its deliberate variant removes the busy disabled state and
|
|
163
|
+
changes the result text; Verify must fail and no acceptance record may appear.
|
|
164
|
+
The command records a private `qualification.json` under the demo state's
|
|
165
|
+
private qualification directory. It never targets an application or calls a
|
|
166
|
+
model/provider. Do not report this as passed until the lead observes the
|
|
167
|
+
installed invocation and its result.
|
package/factory/definition.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ROOT, digest, harnessOf } from './lib.mjs';
|
|
4
|
+
import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
|
|
4
5
|
|
|
5
6
|
// Execution order is shared with the queue; presentation cannot invent phases.
|
|
6
7
|
export const WORKFLOWS = Object.freeze({
|
|
@@ -9,7 +10,7 @@ export const WORKFLOWS = Object.freeze({
|
|
|
9
10
|
});
|
|
10
11
|
const phaseInfo = {
|
|
11
12
|
build: { title: 'Implement', owner: 'agent', skills: ['factory-implement'], description: 'Implement the accepted scope in an isolated checkout. Produce a candidate and evidence.' },
|
|
12
|
-
verify: { title: 'Check', owner: 'factory', skills: [], description: 'Run
|
|
13
|
+
verify: { title: 'Check', owner: 'factory', skills: [], description: 'Run project checks and any optional trusted browser stories against the candidate. Missing or non-passing required proof stops delivery.' },
|
|
13
14
|
review: { title: 'Review', owner: 'agent', skills: ['factory-review', 'factory-security'], description: 'Review the candidate and evidence in a separate agent invocation. Security review applies when required by the accepted scope.' },
|
|
14
15
|
handoff: { title: 'Accept & hand off', owner: 'operator', skills: [], description: 'Wait for operator approval, then confirm the candidate and policy still match the checks and review. Record acceptance; do not push, merge or deploy.' },
|
|
15
16
|
defence: { title: 'Investigate', owner: 'agent', skills: ['factory-security'], description: 'Investigate supplied incident evidence within the accepted scope. Produce a private draft with findings and unknowns. No production access or recovery action is granted.' },
|
|
@@ -40,7 +41,13 @@ export function factoryDefinition(config) {
|
|
|
40
41
|
skills,
|
|
41
42
|
configuration: { harness, agent: harness, // agent is a v1 compatibility alias
|
|
42
43
|
model: config.model || null, check: config.check, timeoutSeconds: config.timeoutSeconds,
|
|
43
|
-
memoryMiB: config.memoryMiB, cpus: config.cpus || 2
|
|
44
|
+
memoryMiB: config.memoryMiB, cpus: config.cpus || 2,
|
|
45
|
+
web_verification: config.webVerification?.enabled ? {
|
|
46
|
+
enabled: true, adapter: config.webVerification.adapter, version: config.webVerification.version,
|
|
47
|
+
browser: 'chromium', image: config.webVerification.image, policy_hash: webPolicyHash(config.webVerification),
|
|
48
|
+
required_stories: expectedWebStories(config.webVerification), platform: 'linux-container', coverage: 'web',
|
|
49
|
+
} : { enabled: false },
|
|
50
|
+
},
|
|
44
51
|
method: {
|
|
45
52
|
preparation: ['factory-triage', 'factory-spec'], evaluation: ['factory-evaluate'],
|
|
46
53
|
instructions: 'All six skills are available read-only to agent phases. A skill is an instruction set, not a separate agent or an automatic workflow step.',
|
package/factory/delivery.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { publicSourceAdmission } from './source-admission.mjs';
|
|
|
10
10
|
import { QueueError } from './queue.mjs';
|
|
11
11
|
import { readProjectLinks } from './project-links.mjs';
|
|
12
12
|
import { qualifyGitHubActions } from './workflow-qualification.mjs';
|
|
13
|
+
import { assertCurrentWebEvidence, assertCurrentWebArtifacts } from './web-verification.mjs';
|
|
13
14
|
|
|
14
15
|
const MAX_PATCH_BYTES = 8 * 1024 * 1024;
|
|
15
16
|
const MAX_CHANGED_FILES = 500;
|
|
@@ -269,6 +270,16 @@ function acceptanceSummary(job, state, config, sourceAdmission) {
|
|
|
269
270
|
const byID = id => job.runs.find(run => run.id === id);
|
|
270
271
|
const buildRun = byID(accepted.build_run_id), checkRun = byID(accepted.checks_run_id), reviewRun = byID(accepted.review_run_id);
|
|
271
272
|
const handoffRun = byID(accepted.handoff_run_id);
|
|
273
|
+
let browserEvidenceCurrent = true;
|
|
274
|
+
if (config.webVerification?.enabled) {
|
|
275
|
+
try {
|
|
276
|
+
assertCurrentWebEvidence(config.webVerification, checks.web_verification,
|
|
277
|
+
{ job: job.id, attempt: checkRun?.id, meta: candidate, policyHash: expectedPolicy });
|
|
278
|
+
assertCurrentWebArtifacts(config.webVerification, checks.web_verification,
|
|
279
|
+
join(state, 'jobs', job.id, 'artifacts', checks.web_verification.attempt));
|
|
280
|
+
}
|
|
281
|
+
catch { browserEvidenceCurrent = false; }
|
|
282
|
+
}
|
|
272
283
|
const phaseEvidence = trustedPhaseEvidence(state, job, expectedPolicy,
|
|
273
284
|
{ build: buildRun, verify: checkRun, review: reviewRun, handoff: handoffRun }, candidate, checks, review);
|
|
274
285
|
if (!phaseEvidence.trusted) return { state: 'blocked', reason: phaseEvidence.reason };
|
|
@@ -282,7 +293,7 @@ function acceptanceSummary(job, state, config, sourceAdmission) {
|
|
|
282
293
|
&& accepted.base === job.source_admission?.resolved_sha && accepted.policyHash === expectedPolicy
|
|
283
294
|
&& accepted.checks_run_id === checkRun.id && accepted.review_run_id === reviewRun.id
|
|
284
295
|
&& checks.run_id === checkRun.id && checks.passed === true && checks.head === accepted.head
|
|
285
|
-
&& checks.tree === accepted.tree && checks.policyHash === expectedPolicy && checks.command === config.check
|
|
296
|
+
&& checks.tree === accepted.tree && checks.policyHash === expectedPolicy && checks.command === config.check && browserEvidenceCurrent
|
|
286
297
|
&& review.run_id === reviewRun.id && review.verdict === 'pass' && review.head === accepted.head
|
|
287
298
|
&& review.tree === accepted.tree && review.policyHash === expectedPolicy && reviewRun.review_verdict === 'pass';
|
|
288
299
|
if (!bound) return { state: 'blocked', reason: 'Candidate, checks, review or approval evidence is stale or does not match the current Factory policy.' };
|
|
@@ -515,6 +526,15 @@ export class DeliveryService {
|
|
|
515
526
|
|| review.run_id !== reviewRun.id || review.verdict !== 'pass' || review.head !== accepted.head || review.tree !== accepted.tree
|
|
516
527
|
|| review.policyHash !== expectedPolicy || reviewRun.review_verdict !== 'pass')
|
|
517
528
|
throw new QueueError('Current successful checks and independent review for this candidate are required.');
|
|
529
|
+
if (config.webVerification?.enabled) {
|
|
530
|
+
try {
|
|
531
|
+
assertCurrentWebEvidence(config.webVerification, checks.web_verification,
|
|
532
|
+
{ job: job.id, attempt: checkRun.id, meta: candidate, policyHash: expectedPolicy });
|
|
533
|
+
assertCurrentWebArtifacts(config.webVerification, checks.web_verification,
|
|
534
|
+
join(this.state, 'jobs', job.id, 'artifacts', checks.web_verification.attempt));
|
|
535
|
+
}
|
|
536
|
+
catch { throw new QueueError('Required browser evidence is missing, stale or non-passing; publication is blocked.'); }
|
|
537
|
+
}
|
|
518
538
|
if (!SHA1.test(accepted.base || '') || !SHA1.test(accepted.head || '') || !SHA1.test(accepted.tree || '')
|
|
519
539
|
|| accepted.head === accepted.base || candidate.parent !== accepted.base)
|
|
520
540
|
throw new QueueError('Candidate base, head or tree is invalid for trusted PR delivery.');
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { assertCurrentWebEvidence } from './web-verification.mjs';
|
|
2
|
+
|
|
3
|
+
export function assertCurrentHandoffEvidence(meta, checks, review, policyHash, config = {}, job) {
|
|
2
4
|
if (review.verdict !== 'pass' || review.head !== meta.head || review.tree !== meta.tree
|
|
3
5
|
|| !checks.passed || checks.head !== meta.head || checks.tree !== meta.tree
|
|
4
6
|
|| checks.policyHash !== policyHash || review.policyHash !== policyHash
|
|
5
7
|
|| meta.build_policy_hash !== policyHash)
|
|
6
8
|
throw new Error('Build/checks/review do not cover candidate and current policy');
|
|
9
|
+
if (config.webVerification?.enabled) assertCurrentWebEvidence(config.webVerification, checks.web_verification,
|
|
10
|
+
{ job, attempt: checks.run_id, meta, policyHash });
|
|
7
11
|
}
|
|
@@ -4,6 +4,7 @@ import { digest } from './lib.mjs';
|
|
|
4
4
|
import { VERSION } from './updates.mjs';
|
|
5
5
|
import { usageFields } from './usage.mjs';
|
|
6
6
|
import { effectiveInferenceProvider } from './model-environment.mjs';
|
|
7
|
+
import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
|
|
7
8
|
|
|
8
9
|
export function withRequestedModel(configuration, requestedModel) {
|
|
9
10
|
const config = structuredClone(configuration);
|
|
@@ -33,13 +34,19 @@ export function executionProfile(config, phase) {
|
|
|
33
34
|
const applicable = !deterministic && harnessOf(config) !== 'mock';
|
|
34
35
|
const provider = applicable && ['codex', 'pi'].includes(harnessOf(config));
|
|
35
36
|
const requestedModel = provider ? config.model || null : null;
|
|
36
|
-
|
|
37
|
+
const profile = {
|
|
37
38
|
version: 1, phase, executor: deterministic ? 'deterministic' : harnessOf(config),
|
|
38
39
|
requestedModel,
|
|
39
40
|
modelSelection: !applicable ? 'not_applicable' : !provider ? 'unknown' : requestedModel ? 'explicit' : 'provider_default',
|
|
40
41
|
runtimeVersion: VERSION, image: phase === 'handoff' ? null : config.image,
|
|
41
42
|
policyHash: digest(JSON.stringify(config)), hostName: hostname(),
|
|
42
43
|
};
|
|
44
|
+
if (config.webVerification?.enabled) profile.webVerification = {
|
|
45
|
+
enabled: true, adapter: config.webVerification.adapter, version: config.webVerification.version,
|
|
46
|
+
browser: 'chromium', image: config.webVerification.image, policyHash: webPolicyHash(config.webVerification),
|
|
47
|
+
requiredStories: expectedWebStories(config.webVerification), platform: 'linux-container',
|
|
48
|
+
};
|
|
49
|
+
return profile;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export function attemptPresentation(attempt, recoveredUsage) {
|