software-defence-factory 0.7.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.
Files changed (42) hide show
  1. package/THIRD_PARTY_NOTICES.md +10 -0
  2. package/bin/software-defence-factory.mjs +58 -8
  3. package/docs/interfaces.md +2 -1
  4. package/docs/quickstart.md +121 -6
  5. package/docs/recovery.md +199 -4
  6. package/docs/setup.md +78 -0
  7. package/docs/web-verification.md +167 -0
  8. package/factory/definition.mjs +9 -2
  9. package/factory/delivery-provider.mjs +20 -0
  10. package/factory/delivery.mjs +901 -0
  11. package/factory/execution-evidence.mjs +11 -0
  12. package/factory/execution-profile.mjs +19 -1
  13. package/factory/executor.mjs +275 -57
  14. package/factory/inference-redaction.mjs +123 -0
  15. package/factory/lib.mjs +37 -6
  16. package/factory/model-environment.mjs +166 -0
  17. package/factory/processes.mjs +55 -16
  18. package/factory/providers/github-delivery.mjs +161 -0
  19. package/factory/queue.mjs +14 -1
  20. package/factory/scratch.mjs +15 -0
  21. package/factory/server.mjs +50 -12
  22. package/factory/source-admission.mjs +6 -0
  23. package/factory/ui/assets/index-CLrogux1.css +1 -0
  24. package/factory/ui/assets/index-Cjk_XtEU.js +13 -0
  25. package/factory/ui/index.html +2 -2
  26. package/factory/updates.mjs +1 -1
  27. package/factory/web/Dockerfile +6 -0
  28. package/factory/web/containers.mjs +189 -0
  29. package/factory/web/package-lock.json +42 -0
  30. package/factory/web/package.json +9 -0
  31. package/factory/web/qualification-fixture.mjs +97 -0
  32. package/factory/web/readiness.mjs +26 -0
  33. package/factory/web/runner.mjs +236 -0
  34. package/factory/web-readiness.mjs +94 -0
  35. package/factory/web-verification.mjs +250 -0
  36. package/factory/workflow-qualification.mjs +541 -0
  37. package/operator-skills/factory-foundation/SKILL.md +5 -1
  38. package/package.json +3 -1
  39. package/scripts/probe-platform.mjs +34 -5
  40. package/scripts/probe-web.mjs +104 -0
  41. package/factory/ui/assets/index-Bz6ECdy_.css +0 -1
  42. package/factory/ui/assets/index-DqeOnrGK.js +0 -13
@@ -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.
@@ -4,7 +4,7 @@ import { resolve, join } from 'node:path';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { createServer } from 'node:net';
7
- import { ROOT, PINS, DEFAULT_STATE, configAt, save, json, run, stream, digest, api, sleep, stopContainers } from '../factory/lib.mjs';
7
+ import { ROOT, PINS, DEFAULT_STATE, configAt, save, json, run, stream, digest, api, sleep, stopContainers, PUBLICATION_API_TIMEOUT_MS } from '../factory/lib.mjs';
8
8
  import { assertInstalledJobImage, installCustomJobImage, installStandardJobImage, inspectImageInstallation } from '../factory/image-install.mjs';
9
9
  import { listIssues, readIssue } from '../factory/issue-intake.mjs';
10
10
  import { recommendWork } from '../factory/intake.mjs';
@@ -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));
@@ -34,7 +35,7 @@ let state = resolve(flags.state || DEFAULT_STATE);
34
35
  if(existsSync(state))state=realpathSync(state);
35
36
  const alive = pid => { try { process.kill(pid,0); return true; } catch(error) { if(error.code === 'ESRCH')return false; throw error; } };
36
37
 
37
- function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD') {
38
+ function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD', delivery, inferenceProvider) {
38
39
  repo=realpathSync(resolve(repo));
39
40
  if (existsSync(join(state,'factory.json'))) throw new Error('Already configured; edit the private factory.json explicitly or choose another --state');
40
41
  if ([repo,state,ROOT].some(p=>/[,\n\r]/.test(p))) throw new Error('Paths cannot contain commas or line breaks');
@@ -45,11 +46,11 @@ function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD') {
45
46
  if (!argv) throw new Error('Select codex, pi, mock or custom with --command-json');
46
47
  if (flags.model && ['codex','pi'].includes(harness)) argv.splice(harness==='codex'?argv.length-1:argv.length,0,'--model',flags.model);
47
48
  mkdirSync(state,{recursive:true,mode:0o700});state=realpathSync(state);chmodSync(state,0o700);
48
- save(join(state,'factory.json'),{version:1,repo,sourceRef,harness,command:argv,check,port:Number(port),image:PINS.jobImage,network:harness==='mock'?'none':'bridge',timeoutSeconds:1800,memoryMiB:2048,model:flags.model || null,
49
+ save(join(state,'factory.json'),{version:1,repo,sourceRef,harness,command:argv,check,port:Number(port),image:PINS.jobImage,network:harness==='mock'?'none':'bridge',timeoutSeconds:1800,memoryMiB:2048,model:flags.model || null,...(inferenceProvider?{inferenceProvider}:{}),...(delivery?{delivery}:{}),
49
50
  scope:{project:'pilot',service:'app',environment:'test',owner:'operator'}});
50
51
  configAt(state);
51
52
  writeFileSync(join(state,'worker.token'),randomBytes(32).toString('hex')+'\n',{mode:0o600});
52
- writeFileSync(join(state,'model.env'),'# Only inference credentials belong here. Never add GitHub, deploy or cloud credentials.\n',{mode:0o600});
53
+ writeFileSync(join(state,'model.env'),'# Supported inference settings only. Never add host, forge, deploy, Docker or cloud identity credentials.\n',{mode:0o600});
53
54
  registerInstallation(state);
54
55
  console.log(`Configured ${state}\nApp files were not changed. Only committed code is cloned into jobs.`);
55
56
  }
@@ -124,8 +125,39 @@ async function jobAction(action) {
124
125
  console.log(`${action}: ${id}`);
125
126
  }
126
127
 
128
+ async function publishJob(jobId) {
129
+ if (!/^job_[a-f0-9]{24}$/.test(jobId || '')) throw new Error('publish requires a Factory JOB_ID');
130
+ const snapshot=await api(state,'/api/v1/status'),job=snapshot.jobs.find(item=>item.id===jobId);
131
+ if(!job)throw new Error('Job not found');
132
+ const current=job.runs.at(-1);
133
+ const receipt=await api(state,`/api/v1/jobs/${jobId}/publish`,{run_id:current?.id},undefined,{timeoutMs:PUBLICATION_API_TIMEOUT_MS});
134
+ console.log(JSON.stringify(receipt,null,2));
135
+ }
136
+
137
+ async function abandonDeliveryJob(jobId) {
138
+ if (!/^job_[a-f0-9]{24}$/.test(jobId || '')) throw new Error('abandon-delivery requires a Factory JOB_ID');
139
+ const branchSha = flags['branch-sha'];
140
+ if (!/^[a-f0-9]{40}$/.test(branchSha || '')) throw new Error('abandon-delivery requires --branch-sha with the inspected remote SHA');
141
+ const snapshot=await api(state,'/api/v1/status'),job=snapshot.jobs.find(item=>item.id===jobId);
142
+ if(!job)throw new Error('Job not found');
143
+ const delivery=job.delivery_status;
144
+ if(!delivery?.can_abandon)throw new Error('This job has no resolvable pre-write branch collision; inspect status and reconcile unresolved provider effects.');
145
+ if(branchSha!==delivery.remote_collision?.sha)throw new Error('The supplied branch SHA differs from current status; inspect the current remote branch before resolving.');
146
+ const result=await api(state,`/api/v1/jobs/${jobId}/abandon-delivery`,{
147
+ run_id:job.runs.at(-1)?.id,delivery_identity:delivery.identity,branch_sha:branchSha,
148
+ },undefined,{timeoutMs:PUBLICATION_API_TIMEOUT_MS});
149
+ console.log(JSON.stringify(result,null,2));
150
+ }
151
+
127
152
  try {
128
- if(command==='init') { if(!flags.repo)throw new Error('init requires --repo /path/to/existing/git/repo');if(flags.harness && flags.agent && flags.harness !== flags.agent)throw new Error('--harness conflicts with legacy --agent');init(flags.repo,flags.harness || flags.agent,flags.check,flags.port,flags['source-ref'] || 'HEAD'); }
153
+ if(command==='init') {
154
+ if(!flags.repo)throw new Error('init requires --repo /path/to/existing/git/repo');
155
+ if(flags.harness && flags.agent && flags.harness !== flags.agent)throw new Error('--harness conflicts with legacy --agent');
156
+ const deliveryFlags=[flags['delivery-provider'],flags['delivery-repository'],flags['delivery-target']];
157
+ if(deliveryFlags.some(Boolean)&&deliveryFlags.some(value=>!value))throw new Error('Trusted PR delivery requires --delivery-provider github --delivery-repository URL --delivery-target main|dev');
158
+ const delivery=deliveryFlags.every(Boolean)?{provider:flags['delivery-provider'],repository:flags['delivery-repository'],target:flags['delivery-target']}:undefined;
159
+ init(flags.repo,flags.harness || flags.agent,flags.check,flags.port,flags['source-ref'] || 'HEAD',delivery,flags['inference-provider']);
160
+ }
129
161
  else if(command==='install')await withServiceOperation('install',install);
130
162
  else if(command==='up') { if(hasService(state))await manageService('controller','start',state);else await withServiceOperation('up',up); }
131
163
  else if(command==='stop') { if(hasService(state))await manageService('controller','stop',state);else await withServiceOperation('stop',stop); }
@@ -159,8 +191,9 @@ try {
159
191
  else if(command==='status') { const snapshot=await api(state,'/api/v1/status');delete snapshot.csrf_token;console.log(JSON.stringify(snapshot,null,2)); }
160
192
  else if(command==='doctor') {
161
193
  const config=configAt(state),dockerVersion=run('docker',['info','--format','{{.ServerVersion}}']),imageStatus=inspectImageInstallation(state,config);
162
- 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}`},null,2));
163
- if(!imageStatus.installed)process.exitCode=1;
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;
164
197
  } else if(command==='issue') {
165
198
  const action=positional[0], sourceURL=flags.url || flags.github;
166
199
  if(action==='list') {
@@ -222,6 +255,8 @@ try {
222
255
  if(!flags.file)throw new Error('Use --file incident.json; see factory/examples/incident.json');
223
256
  console.log(JSON.stringify(await admitIncident(state,json(resolve(flags.file)),submit)));
224
257
  } else if(['approve','cancel','retry'].includes(command))await jobAction(command);
258
+ else if(command==='publish')await publishJob(positional[0]);
259
+ else if(command==='abandon-delivery')await abandonDeliveryJob(positional[0]);
225
260
  else if(command==='revise')await jobAction('request_changes');
226
261
  else if(command==='demo') {
227
262
  state=resolve(flags.state || DEFAULT_DEMO_STATE);
@@ -230,11 +265,20 @@ try {
230
265
  initializeDemoRepository(repo);
231
266
  init(repo,'mock',"test \"$(cat value.txt)\" = fixed",Number(flags.port || 7332));
232
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'});
233
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.')));
234
270
  console.log('Review the synthetic change in the dashboard and approve its handoff.');
235
271
  } else if(['version','--version','-v'].includes(command))console.log(VERSION);
236
272
  else if(command==='qualify') {
237
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;
238
282
  } else if(command==='kit') {
239
283
  if(!flags.output)throw new Error('kit requires --output NEW_DIRECTORY');
240
284
  await stream(process.execPath,[join(ROOT,'scripts/export-kit.mjs'),resolve(flags.output)]);
@@ -243,9 +287,13 @@ try {
243
287
  kit --output NEW_DIRECTORY Export the portable method without a runtime
244
288
  demo Install and run a synthetic sample (no model key)
245
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
246
291
  init --repo PATH --harness codex|pi|custom --check "npm ci && npm test" [--source-ref REF]
292
+ [--inference-provider PROVIDER]
293
+ [--delivery-provider github --delivery-repository https://github.com/OWNER/REPO --delivery-target main|dev]
247
294
  install [--image LOCAL_REF] Build the standard image, or select an existing local image
248
295
  doctor | up | status | stop Inspect / operate your private installation
296
+ web probe --state PATH Execute the pinned local Chromium readiness probe
249
297
  foundation Read the operator setup skill; no installation required
250
298
  definition | agents | skills Inspect roles, instructions and installation settings
251
299
  inbox | infrastructure | automations Inspect live tasks, host/worker and automation state
@@ -280,6 +328,8 @@ try {
280
328
  [--source-ref REF] Pin a configured-repository ref before admission
281
329
  incident --file incident.json Submit a private, read-only incident draft
282
330
  approve JOB_ID | cancel JOB_ID Review gate / stop this attempt
331
+ publish JOB_ID Publish/reconcile the accepted candidate as one draft PR
332
+ abandon-delivery JOB_ID --branch-sha SHA Resolve an inspected pre-write collision; keep the remote branch
283
333
  retry JOB_ID Prove stop; retain old checkout and retry
284
334
  revise JOB_ID --file feedback.md [--source-ref REF]
285
335
  New build/check/review; source stays pinned unless a new ref is explicit
@@ -291,7 +341,7 @@ Runtime commands accept --state PATH. Default: ${DEFAULT_STATE}
291
341
  Demo default: ${DEFAULT_DEMO_STATE}
292
342
  The npm CLI keeps state outside the package; updates wait for stopped installations.
293
343
  Dashboard binds only to loopback; use SSH for remote access.
294
- Setup plan: ${join(ROOT, 'docs/setup.md')}
344
+ Setup plan: ${join(ROOT, 'docs/setup.md')}
295
345
  See docs/quickstart.md for task execution, evidence and recovery.`);
296
346
  else throw new Error(`Unknown command: ${command}`);
297
347
  } catch(error) {console.error(`Factory: ${error.message}`);process.exitCode=1;}
@@ -33,7 +33,8 @@ shell endpoint or a second scheduler.
33
33
  | Method export | `kit --output` | No export endpoint | None | Equivalent download/export preserving staging-only adoption |
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
- | Trusted PR handoff | Operator applies accepted patch | Not implemented | Not implemented | #29; credentials remain outside jobs |
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
@@ -28,15 +28,47 @@ software-defence-factory install --state /private/state/my-app
28
28
  software-defence-factory doctor --state /private/state/my-app
29
29
  ```
30
30
 
31
- `init --source-ref` selects the configured default ref (`HEAD` when omitted). Each job resolves that ref, or an explicit `--source-ref` on `run`/`issue start`, in the configured repository and durably retains its commit before acknowledging admission. The CLI and dashboard show the requested ref and resolved SHA. Task text and reference links do not select a repository or ref.
31
+ `init --source-ref` selects the configured default ref (`HEAD` when omitted). Each job resolves that ref, or an explicit `--source-ref` on `run`/`issue start`, in the configured repository and durably retains its commit before acknowledging admission. It records the canonical GitHub origin identity when available; the CLI and dashboard show the requested ref and resolved SHA. Task text and reference links do not select a repository, source ref or PR target.
32
32
 
33
33
  Verification commands receive `FACTORY_BASE_REVISION`, the resolved admission commit recorded as the candidate base. Diff-based checks should compare against this revision; the isolated checkout has no origin remote. The value comes from protected controller metadata, not the task text.
34
34
 
35
35
  Replace the check with the application's actual verification command. `init` does not edit the app, copy global skills or start work. It creates factory.json, worker.token and model.env with private permissions. Each installation has one repository and a distinct state path/port. `--harness pi` selects Pi; `--harness custom --command-json '["executable","argument"]'` selects an available command in the job image. The bundled image provides Node, Git, Codex and Pi. Other toolchains require an intentionally built compatible image; do not claim Rust/mobile/browser capabilities from this image alone.
36
36
 
37
- Configure inference credentials in the private model.env file. Do not copy the operator's entire account environment or authentication folders. Codex uses its supported API credential environment; Pi uses the selected provider's configuration. Use `--model` with init for a specific model. Task-level model overrides are supported only for Codex/Pi and do not prove that the provider serves that model.
38
-
39
- A local model endpoint must be reachable from inside the job container. Host loopback addresses do not automatically refer to the host from Docker. Configure and qualify the chosen adapter/network path before dispatch; this package does not automatically expose Ollama or import its models.
37
+ Configure supported inference settings in the private `model.env` file. It
38
+ rejects unrelated names such as `DEPLOY_TOKEN`; do not copy the operator's
39
+ account environment or authentication folders. Codex receives only its OpenAI
40
+ settings. Pi receives only the selected provider's settings. Set
41
+ `--inference-provider PROVIDER` during `init`, or pin an operator-selected
42
+ `provider/model` with `--model`; if multiple provider credential groups are in
43
+ `model.env`, an explicit provider is required. Task-level model overrides never
44
+ select a credential group. See the `inferenceProvider` entry in private
45
+ `factory.json` when editing trusted installation configuration directly.
46
+
47
+ Installations using the existing Codex account-auth command may also put its
48
+ single-line JSON object in `FACTORY_CODEX_AUTH_JSON` in private `model.env`.
49
+ Factory validates that value as JSON and gives it only to the selected Codex
50
+ build/review/defence worker through a temporary private env file; the configured
51
+ operator command that consumes this setting must materialize its temporary
52
+ native `auth.json` before invoking Codex; the bundled stock `codex exec` does
53
+ not do this by itself. Pi never receives this setting, including with the OpenAI provider, and
54
+ deterministic checks and output artifacts do not receive it. Keep `model.env`
55
+ private with mode `0600`; do not copy account folders or place the value in a
56
+ task, source file or report. This setting does not replace provider API-key or
57
+ local OpenAI-compatible endpoint configuration.
58
+
59
+ Factory's Pi provider identifiers are `anthropic`, `azure-openai-responses`,
60
+ `cerebras`, `cloudflare-ai-gateway`, `cloudflare-workers-ai`, `deepseek`,
61
+ `google`, `groq`, `huggingface`, `kimi-coding`, `minimax`, `minimax-cn`,
62
+ `mistral`, `openai`, `opencode`, `opencode-go`, `openrouter`,
63
+ `vercel-ai-gateway`, `xiaomi`, `xiaomi-token-plan-ams`,
64
+ `xiaomi-token-plan-cn`, `xiaomi-token-plan-sgp`, `xai` and `zai`. Factory
65
+ forwards provider API-key and endpoint settings from this map; it does not
66
+ forward OAuth files or ambient cloud identity credentials.
67
+
68
+ `OPENAI_BASE_URL` remains available for an OpenAI-compatible local endpoint
69
+ selected for Codex; the endpoint must be reachable from inside the job
70
+ container. Host loopback addresses do not automatically refer to the host from
71
+ Docker. The package does not automatically expose Ollama or import models.
40
72
 
41
73
  ```sh
42
74
  software-defence-factory up --state /private/state/my-app
@@ -51,6 +83,86 @@ Inspect the task's Result, Files and History tabs. Task details show the request
51
83
 
52
84
  The source application is not changed and no branch, PR, merge or deployment is published automatically. A reviewed change.patch can be checked and applied with `git apply --check` and `git apply` on an appropriate branch at its recorded base revision; then follow the application's normal integrated checks and delivery policy.
53
85
 
86
+ ### Optional trusted PR delivery
87
+
88
+ Patch-only remains the default. To enable the first delivery provider, select a
89
+ canonical GitHub origin and an explicit target while initializing the private
90
+ installation:
91
+
92
+ ```sh
93
+ software-defence-factory init --repo /absolute/path/to/app --harness codex \
94
+ --check "npm ci && npm test" --source-ref main \
95
+ --delivery-provider github \
96
+ --delivery-repository https://github.com/OWNER/REPO \
97
+ --delivery-target main --state /private/state/my-app
98
+ ```
99
+
100
+ Use `dev` only when it is the intended target. The source ref (`main` above)
101
+ and PR target are separate settings. The configured GitHub repository must
102
+ match the canonical origin captured at admission; a later repository rename or
103
+ remote change blocks delivery. Configure this before admitting work because a
104
+ configuration change invalidates earlier check/review/approval policy evidence.
105
+ Unknown providers and installations without `delivery` configuration keep the
106
+ patch-only flow.
107
+
108
+ After the ordinary check, independent review and operator approval complete,
109
+ use **Publish accepted candidate as draft PR** in task details or run:
110
+
111
+ ```sh
112
+ software-defence-factory publish JOB_ID --state /private/state/my-app
113
+ ```
114
+
115
+ Trusted publication requires protected per-run execution records for native
116
+ Codex/Pi build and review plus deterministic verification and handoff, bound to
117
+ non-synthetic candidate, check and review artifacts. Mock qualification remains
118
+ local exploration and cannot be published; missing or inconsistent provenance
119
+ keeps the action unavailable in status, CLI, API and dashboard. A saved intent
120
+ is checked again before new provider writes. Known PR receipts and PR-creation
121
+ checkpoints still allow read-only reconciliation.
122
+
123
+ New writes also require the shared GitHub Actions qualification described in
124
+ [setup](setup.md#optional-trusted-pr-delivery) and [recovery](recovery.md#trusted-pr-delivery).
125
+ Unsupported or candidate-changed workflows keep publication unavailable in
126
+ status, CLI, API and dashboard; the accepted patch remains available for normal
127
+ manual delivery.
128
+
129
+ For `push`, the qualifier evaluates branch filters when either `branches` or
130
+ `branches-ignore` is declared, even alongside tag filters; tag-only filters do
131
+ not activate a generated branch push. Docker phase logs redact literal selected
132
+ inference credential values before retention. Mounted worker reports are
133
+ redacted after the container stops and before promotion; if shutdown is
134
+ uncertain, recovery handles them after confirming the stop. This is a bounded
135
+ output filter, not general protection against encoded or transformed values.
136
+
137
+ The controller uses its existing `gh` identity. GitHub credentials are never
138
+ copied to `model.env` or mounted into jobs. The intent, generated branch, PR
139
+ identity, actual base/head/tree and triggered PR check results appear in the
140
+ same job. Unknown and pending checks remain visible and are not reported as
141
+ success. GitHub's raw `success`, `skipped` and `neutral` conclusions are
142
+ non-blocking in the Factory check summary; skipped and neutral remain visibly
143
+ distinct from an executed passing check. Unknown conclusions and incomplete
144
+ pagination keep the aggregate unknown. This summary does not determine branch
145
+ protection requirements or grant merge authorization. See [GitHub's required
146
+ status check guidance](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks).
147
+ Repeating `publish` refreshes/reconciles the same branch and PR; it does not
148
+ create a second PR or overwrite a changed branch. The CLI gives this bounded
149
+ multi-request action ten minutes; branch resolution uses the same bound and
150
+ other API calls keep their five-second deadline. If a client deadline expires,
151
+ inspect status and repeat `publish` to reconcile or recheck the reported branch
152
+ identity before `abandon-delivery`. Delete issue stays disabled while delivery
153
+ is unresolved, and the controller enforces the same guard on its API.
154
+ Publication does not merge, integrate, release or deploy. See [delivery
155
+ recovery](recovery.md#trusted-pr-delivery).
156
+
157
+ If status reports a reserved branch collision at the untouched `intent` stage,
158
+ inspect that branch in GitHub first. The task detail action **Abandon local
159
+ delivery; keep remote branch** or `abandon-delivery JOB_ID --branch-sha SHA`
160
+ records only a local resolution after the controller confirms the same branch
161
+ head and no associated PR. It preserves the remote branch and accepted evidence,
162
+ permits local issue removal, and permanently disables publication for that
163
+ delivery record. Changed identities, PRs and uncertain provider effects remain
164
+ blocked; see [recovery](recovery.md#trusted-pr-delivery).
165
+
54
166
  ## Remote access and operation
55
167
 
56
168
  ```sh
@@ -109,8 +221,11 @@ is tested at 100%; changing browser zoom is separate from a project theme.
109
221
  ## Environment
110
222
 
111
223
  Factory does not load a repository `.env` file. Configure the private
112
- `factory.json` through `init`; put inference credentials only in its private
113
- `model.env`. A repository `.env.example` is unnecessary for this CLI. Optional
224
+ `factory.json` through `init`; put supported inference settings only in its
225
+ private `model.env`. A provider allowlist selects the configured Codex/Pi
226
+ settings before a worker starts; unrelated host, forge, deployment, cloud
227
+ identity and application variables are rejected. A repository `.env.example`
228
+ is unnecessary for this CLI. Optional
114
229
  process settings are `SDF_AUTO_UPDATE=0` (skip automatic CLI update checks),
115
230
  `XDG_STATE_HOME`, `XDG_DATA_HOME` and `XDG_CONFIG_HOME` (user-owned state, release
116
231
  and service locations). They must be exported in the process environment.
package/docs/recovery.md CHANGED
@@ -6,7 +6,8 @@ Use `status --state PATH` and the private supervisor.log to identify the active
6
6
  - An unconfirmed running attempt becomes `interrupted` on controller restart. It is never silently considered successful.
7
7
  - Admission resolves the configured source ref or an explicit `--source-ref` in the configured repository, records its identity/ref/SHA in private job metadata and retains its Git objects under that job. Build and retry restore from this retained revision; moving or deleting the source ref does not select a new commit.
8
8
  - `retry JOB_ID` verifies retained objects and reconciles the previous process group and containers. A missing/corrupt retained source, live writer or unknown process blocks retry. For a new build/defence attempt, the prior checkout is retained as previous-checkout-*.
9
- - A failed verification can retry the same unchanged candidate after the check environment is repaired. A changed candidate needs a fresh verification/review sequence.
9
+ - A forced host/controller stop can leave the selected inference env file in the private attempt directory. Normal executor cleanup removes it only after a Docker listing confirms the named container is absent. Retry/reconciliation removes a leftover copy only after the prior process group is confirmed stopped and a Docker listing confirms the labelled containers are absent; a `docker rm` or `docker stop` exit alone does not prove shutdown. If identity, a Docker probe or shutdown is uncertain, the recovery fence and file remain; do not clean it manually while a worker may still be active. After shutdown is confirmed, recovery also redacts the fixed phase reports and log using the selected environment values before removing the env file.
10
+ - Retry repeats the stopped phase. A failed verification may retry the same candidate when its policy is unchanged and the check can now pass. If the check or execution policy changed after build, retrying verify/review cannot reuse the earlier build for handoff; handoff remains blocked. Preserve that failed attempt, then use an eligible requested revision or submit a replacement task to build under the current policy. Do not edit SQLite or acceptance evidence to bypass the guard.
10
11
  - A requested revision keeps the recorded source by default, retains previous evidence and starts a new build with accumulated feedback. Supplying a deliberate new `--source-ref` resolves and retains that base before the action; prior source metadata stays in history, and the new build gets fresh checks and review.
11
12
  - Removing a stopped task from the dashboard hides its queue record. Private artifacts and its deleted_at record remain on disk; this is not secure erasure.
12
13
 
@@ -17,12 +18,32 @@ For backup, stop the installation and copy the complete private state directory,
17
18
  Earlier experimental engines use a different journal. Start a new state directory for the native 0.3 runtime; preserve old journals separately. There is no automatic import of their jobs or approval state.
18
19
 
19
20
  Verification cleanup makes owned scratch directories traversable before removing
20
- them and never follows their symlinks. It runs only after container stop is
21
- confirmed. If a filesystem error still prevents cleanup, the attempt fails and
22
- retains the original check exit and private log path alongside the cleanup error.
21
+ them and never follows their symlinks. The executor and recovery paths remove
22
+ `check-workspace` only after Docker confirms the named container is absent; a
23
+ client exit, failed remove or failed/unknown probe does not establish shutdown.
24
+ While shutdown is uncertain, the scratch directory and active recovery fence
25
+ remain. Ordinary reconciliation removes scratch after the process and labelled
26
+ containers are confirmed stopped. If a filesystem error still prevents cleanup,
27
+ the attempt fails and retains the original check exit and private log path
28
+ alongside the cleanup error.
23
29
  Inspect that retained attempt before manual removal; never substitute the source
24
30
  candidate path for the scratch path.
25
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
+
26
47
  ## Review feedback or phase retry
27
48
 
28
49
  Use **Request changes** on a failed software review only when its validated
@@ -67,6 +88,24 @@ agent reports, passing checks or review. Nonzero exits and missing/malformed
67
88
  reports still fail closed. Process termination during a host crash can leave
68
89
  incomplete logs and an interrupted attempt; apply process reconciliation above.
69
90
 
91
+ Before retention, Docker stdout/stderr logs have exact selected inference
92
+ credential values replaced. Mounted reports (`agent-report.md`, `review.json`,
93
+ `incident-report.json`) are sanitized after the phase container is confirmed
94
+ absent; if shutdown is uncertain, they remain in the private attempt directory
95
+ and ordinary recovery sanitizes them after confirming the stop. Filtering
96
+ includes selected API-key, token, secret, password and credential settings plus
97
+ credential fields inside selected Codex auth JSON. Reports are opened without
98
+ following symlinks and with nonblocking access before descriptor type checks, so
99
+ special files such as FIFOs are refused promptly. An unsafe or oversized report
100
+ cannot be promoted. If filtering cannot complete, recovery retains the selected
101
+ inference file and active fence; a repeated recovery remains blocked until the
102
+ fixed owned report can be safely filtered. Once process and container shutdown
103
+ are confirmed, repair or replace that report and retry ordinary recovery so it
104
+ can sanitize the output before removing the selected file and fence. This
105
+ bounded filter does not detect encoded or transformed values and does not
106
+ rewrite candidate files or patches; it is not a general data loss prevention
107
+ control.
108
+
70
109
  ## Recorded execution profiles
71
110
 
72
111
  Before each attempt executes, the controller freezes its effective configuration
@@ -126,3 +165,159 @@ Use `issue submissions` and `issue recover --key REQUEST_ID` against the same
126
165
  controller state. Recovery reads the original provider using the original
127
166
  identity; it does not publish again. Do not use a new request key to retry an
128
167
  uncertain write. See [provider ownership and recovery limits](integrations.md).
168
+
169
+ ## Trusted PR delivery
170
+
171
+ Delivery is available only for an accepted software job with current source,
172
+ candidate, check, independent review and approval records. Trusted publication
173
+ also requires matching private per-run execution records: native Codex/Pi for
174
+ build and review, deterministic verification and handoff, and explicitly
175
+ non-synthetic bound candidate/check/review artifacts. Missing, unknown,
176
+ synthetic or inconsistent evidence is not publication proof. Status, CLI, API
177
+ and dashboard use the same guard. The private operator configuration pins the
178
+ GitHub repository and target; task text and worker reports cannot provide
179
+ either. The controller stores the intent and remote branch/PR identifiers in
180
+ the job row before it writes. Status and task details show the receipt
181
+ separately from integration or deployment.
182
+
183
+ Inspect the same installation before recovery:
184
+
185
+ ```sh
186
+ software-defence-factory status --state /private/state/project
187
+ software-defence-factory publish JOB_ID --state /private/state/project
188
+ ```
189
+
190
+ After a lost response or controller restart, repeat `publish`. The controller
191
+ reads the exact configured target, generated branch and PR before attempting a
192
+ missing stage. A matching branch/PR is reused; a different remote head, target,
193
+ repository or collision is preserved and blocks recovery. It never force-pushes,
194
+ rebases, creates a second PR to avoid an uncertain response, or merges. Restore
195
+ the original trusted repository/target configuration if it changed; do not
196
+ redirect a saved intent.
197
+
198
+ Before any new content, branch or PR write, a saved intent is rechecked against
199
+ the protected run profiles and linked artifacts. A mock/synthetic record cannot
200
+ authorize a new write on retry. A known receipt or PR-creation checkpoint can
201
+ still use read-only provider reconciliation; if that cannot establish the
202
+ existing effect, the uncertain record remains blocked from further writes.
203
+
204
+ Before advertising or attempting a new write, status and the shared publisher
205
+ qualify the exact accepted Git trees again. Factory reads the immutable admitted
206
+ base commit and reconstructs the accepted candidate tree from the protected,
207
+ digest-bound patch. Added, changed, deleted or symlinked
208
+ `.github/workflows/*.yml` and `*.yaml` definitions block trusted publication.
209
+ Every base workflow must parse with the bundled YAML parser and use the
210
+ supported literal `push`, `pull_request`, `pull_request_target` or
211
+ `workflow_dispatch` trigger subset; dispatch inputs and other events such as
212
+ `workflow_run` or `workflow_call` are unsupported. A manual dispatch is
213
+ evaluated with the generated branch as its selected ref, as GitHub permits
214
+ dispatching a workflow against a selected branch ([manual workflow runs](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow)).
215
+ Any declared PR activity type is treated as potentially active for that
216
+ candidate PR. Branch filters are interpreted only when their patterns contain
217
+ ASCII letters, digits, `.`, `_`, `-` or `/`; exact literals are checked against
218
+ the generated branch and configured target. Other patterns are treated as
219
+ possible matches. This includes `*`, `**`, `?`, `+`, `[]`, `!` and escaped
220
+ characters from GitHub's [filter syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#filter-pattern-cheat-sheet).
221
+ Path filters are also treated as possible matches.
222
+
223
+ Conditions support only `&&`-joined `==`/`!=` comparisons against quoted
224
+ strings using `github.ref`, `github.event_name`, `github.head_ref`,
225
+ `github.base_ref` or `vars.NAME`; `||`, functions and other contexts are
226
+ refused. Supported ASCII string comparisons follow GitHub's case-insensitive
227
+ [expression semantics](https://docs.github.com/en/actions/reference/workflows-and-actions/expressions).
228
+ Non-ASCII mismatches stay unknown rather than guessing Unicode case folding.
229
+ For `pull_request`, `github.ref` stays unknown because the future merge ref
230
+ cannot be established from a guessed PR number. Unknown comparisons cannot
231
+ prove a privileged job inactive. Dynamic permissions, malformed YAML and
232
+ ambiguous guards block publication with a reason while leaving patch delivery
233
+ available.
234
+
235
+ Each job that could run for the generated branch push, a branch-selected
236
+ manual dispatch or a PR event must have explicit effective permissions limited
237
+ to `contents: read` or `none`, a known GitHub-hosted runner, and no secret
238
+ context, protected environment, reusable workflow call, OIDC permission or
239
+ deployment permission. A privileged release job can qualify only when a simple
240
+ literal conjunction proves it cannot run for each matching event. The current
241
+ `ci.yml` main-only release guard is supported. Before any content, branch or PR
242
+ write, the existing provider readback still confirms that the configured
243
+ target commit and tree are the exact accepted base. Active jobs must use one of
244
+ the supported literal runner labels (`ubuntu-latest`,
245
+ `ubuntu-22.04`, `ubuntu-24.04`, `ubuntu-26.04`, `windows-latest`,
246
+ `windows-2022`, `windows-2025`, `macos-latest`, `macos-14`, `macos-15` or
247
+ `macos-26`). External action steps must be local or pinned to an immutable
248
+ commit SHA or container digest.
249
+
250
+ This bounded check does not establish that all CI is safe. It does not inspect
251
+ repository/organization rules, webhooks, external CI or other organization
252
+ automation, nor does it audit the behavior of referenced third-party actions.
253
+ Unsupported cases refuse trusted publication and leave normal local checks,
254
+ review, approval and manual patch delivery available.
255
+
256
+ After publication is confirmed, readback follows the saved PR identity and
257
+ records its current open draft/ready, closed or merged state and checks. This
258
+ read-only refresh does not require the source branch to remain after closure or
259
+ the PR base commit to equal today's target commit. It still checks the saved
260
+ repository, target and branch names, PR head, and accepted delivery commit's
261
+ tree and parent. A changed identity or candidate stays a visible conflict; a
262
+ provider outage retains the last known publication receipt. First publication
263
+ continues to require the accepted current base and an open draft PR.
264
+
265
+ The dashboard offers reconciliation for saved `intent`, `publishing`,
266
+ `uncertain` and safely retryable `blocked` checkpoints. Legacy accepted jobs
267
+ without candidate-bound check, review and approval evidence are labelled
268
+ unverified and cannot be published. Deleting an issue with any unconfirmed
269
+ delivery record is blocked in the dashboard and controller API; inspect or
270
+ reconcile the saved remote effect before removing the job. A confirmed
271
+ `published` receipt is no longer unresolved.
272
+
273
+ The shared capability advertises new publication only with ready evidence and
274
+ no saved delivery or a known resumable write state. A branch-only `conflict`
275
+ has no publish action; inspect it and use explicit local abandonment only
276
+ after the exact branch head and absence of an associated PR are confirmed.
277
+ Conflicts with a saved PR receipt and pending PR-creation checkpoints retain a
278
+ read-only reconciliation action. Unknown and abandoned delivery states cannot
279
+ advertise or start publication.
280
+
281
+ The shared delivery status marks each available PR action as `publish` or
282
+ `reconcile`; task details use that mode for idle and pending button wording.
283
+ While a request is pending, publication shows “Publishing…” and read-only
284
+ recovery shows “Reconciling…”. A conflicted record with a saved PR receipt
285
+ therefore stays visibly a readback action.
286
+
287
+ A collision found while the record is still at `intent` is known to precede
288
+ provider writes. Inspect the exact branch in GitHub, then use the task action
289
+ **Abandon local delivery; keep remote branch**, or confirm its current head with
290
+ the CLI:
291
+
292
+ ```sh
293
+ software-defence-factory status --state /private/state/project
294
+ software-defence-factory abandon-delivery JOB_ID --branch-sha INSPECTED_SHA --state /private/state/project
295
+ ```
296
+
297
+ The controller checks the latest run, saved delivery identity, current branch
298
+ head and exact repository/target, then confirms that no PR is attached. It
299
+ records the inspected identity and local resolution in the job row. It does not
300
+ write, update or delete the remote branch or create/accept a PR. The `abandoned`
301
+ state disables later publication and permits local issue removal while retaining
302
+ the delivery record, run history and evidence. A stale or changed branch, any
303
+ associated/unknown PR, changed destination, later delivery stage or uncertain
304
+ provider effect remains blocked; reconcile or inspect it before taking another
305
+ action.
306
+
307
+ CLI publication and branch-resolution requests have a ten-minute deadline
308
+ because they can require multiple provider requests. Other CLI API requests
309
+ keep their five-second deadline. If the client reaches its deadline, inspect
310
+ `status` after the controller action finishes. Repeat `publish` to reconcile a
311
+ publication; repeat `abandon-delivery` only after inspecting the currently
312
+ reported branch identity.
313
+
314
+ The receipt reads the exact PR head's check runs and commit statuses. Raw
315
+ conclusions are retained. `success`, `skipped` and `neutral` do not make the
316
+ aggregate fail, while skipped/neutral are distinct from an executed passing
317
+ check. `pending`, `unknown`, failed and unavailable remain distinct; unknown
318
+ conclusions and incomplete pagination stay unknown. This aggregate does not
319
+ establish required-check completeness or merge authorization. A target/base,
320
+ candidate or policy change requires fresh applicable checks, review and
321
+ approval; do not edit SQLite or acceptance evidence to bypass the guard. A stale
322
+ target after branch creation leaves that unique branch for inspection and does
323
+ not open a PR. A PR does not imply integration or deployment.