software-defence-factory 0.7.0 → 0.8.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.
@@ -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';
@@ -34,7 +34,7 @@ let state = resolve(flags.state || DEFAULT_STATE);
34
34
  if(existsSync(state))state=realpathSync(state);
35
35
  const alive = pid => { try { process.kill(pid,0); return true; } catch(error) { if(error.code === 'ESRCH')return false; throw error; } };
36
36
 
37
- function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD') {
37
+ function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD', delivery, inferenceProvider) {
38
38
  repo=realpathSync(resolve(repo));
39
39
  if (existsSync(join(state,'factory.json'))) throw new Error('Already configured; edit the private factory.json explicitly or choose another --state');
40
40
  if ([repo,state,ROOT].some(p=>/[,\n\r]/.test(p))) throw new Error('Paths cannot contain commas or line breaks');
@@ -45,11 +45,11 @@ function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD') {
45
45
  if (!argv) throw new Error('Select codex, pi, mock or custom with --command-json');
46
46
  if (flags.model && ['codex','pi'].includes(harness)) argv.splice(harness==='codex'?argv.length-1:argv.length,0,'--model',flags.model);
47
47
  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,
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,...(inferenceProvider?{inferenceProvider}:{}),...(delivery?{delivery}:{}),
49
49
  scope:{project:'pilot',service:'app',environment:'test',owner:'operator'}});
50
50
  configAt(state);
51
51
  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});
52
+ writeFileSync(join(state,'model.env'),'# Supported inference settings only. Never add host, forge, deploy, Docker or cloud identity credentials.\n',{mode:0o600});
53
53
  registerInstallation(state);
54
54
  console.log(`Configured ${state}\nApp files were not changed. Only committed code is cloned into jobs.`);
55
55
  }
@@ -124,8 +124,39 @@ async function jobAction(action) {
124
124
  console.log(`${action}: ${id}`);
125
125
  }
126
126
 
127
+ async function publishJob(jobId) {
128
+ if (!/^job_[a-f0-9]{24}$/.test(jobId || '')) throw new Error('publish requires a Factory JOB_ID');
129
+ const snapshot=await api(state,'/api/v1/status'),job=snapshot.jobs.find(item=>item.id===jobId);
130
+ if(!job)throw new Error('Job not found');
131
+ const current=job.runs.at(-1);
132
+ const receipt=await api(state,`/api/v1/jobs/${jobId}/publish`,{run_id:current?.id},undefined,{timeoutMs:PUBLICATION_API_TIMEOUT_MS});
133
+ console.log(JSON.stringify(receipt,null,2));
134
+ }
135
+
136
+ async function abandonDeliveryJob(jobId) {
137
+ if (!/^job_[a-f0-9]{24}$/.test(jobId || '')) throw new Error('abandon-delivery requires a Factory JOB_ID');
138
+ const branchSha = flags['branch-sha'];
139
+ if (!/^[a-f0-9]{40}$/.test(branchSha || '')) throw new Error('abandon-delivery requires --branch-sha with the inspected remote SHA');
140
+ const snapshot=await api(state,'/api/v1/status'),job=snapshot.jobs.find(item=>item.id===jobId);
141
+ if(!job)throw new Error('Job not found');
142
+ const delivery=job.delivery_status;
143
+ if(!delivery?.can_abandon)throw new Error('This job has no resolvable pre-write branch collision; inspect status and reconcile unresolved provider effects.');
144
+ if(branchSha!==delivery.remote_collision?.sha)throw new Error('The supplied branch SHA differs from current status; inspect the current remote branch before resolving.');
145
+ const result=await api(state,`/api/v1/jobs/${jobId}/abandon-delivery`,{
146
+ run_id:job.runs.at(-1)?.id,delivery_identity:delivery.identity,branch_sha:branchSha,
147
+ },undefined,{timeoutMs:PUBLICATION_API_TIMEOUT_MS});
148
+ console.log(JSON.stringify(result,null,2));
149
+ }
150
+
127
151
  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'); }
152
+ if(command==='init') {
153
+ if(!flags.repo)throw new Error('init requires --repo /path/to/existing/git/repo');
154
+ if(flags.harness && flags.agent && flags.harness !== flags.agent)throw new Error('--harness conflicts with legacy --agent');
155
+ const deliveryFlags=[flags['delivery-provider'],flags['delivery-repository'],flags['delivery-target']];
156
+ 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');
157
+ const delivery=deliveryFlags.every(Boolean)?{provider:flags['delivery-provider'],repository:flags['delivery-repository'],target:flags['delivery-target']}:undefined;
158
+ init(flags.repo,flags.harness || flags.agent,flags.check,flags.port,flags['source-ref'] || 'HEAD',delivery,flags['inference-provider']);
159
+ }
129
160
  else if(command==='install')await withServiceOperation('install',install);
130
161
  else if(command==='up') { if(hasService(state))await manageService('controller','start',state);else await withServiceOperation('up',up); }
131
162
  else if(command==='stop') { if(hasService(state))await manageService('controller','stop',state);else await withServiceOperation('stop',stop); }
@@ -222,6 +253,8 @@ try {
222
253
  if(!flags.file)throw new Error('Use --file incident.json; see factory/examples/incident.json');
223
254
  console.log(JSON.stringify(await admitIncident(state,json(resolve(flags.file)),submit)));
224
255
  } else if(['approve','cancel','retry'].includes(command))await jobAction(command);
256
+ else if(command==='publish')await publishJob(positional[0]);
257
+ else if(command==='abandon-delivery')await abandonDeliveryJob(positional[0]);
225
258
  else if(command==='revise')await jobAction('request_changes');
226
259
  else if(command==='demo') {
227
260
  state=resolve(flags.state || DEFAULT_DEMO_STATE);
@@ -244,6 +277,8 @@ try {
244
277
  demo Install and run a synthetic sample (no model key)
245
278
  qualify --state PATH Exercise recovery and isolation with a stopped demo job
246
279
  init --repo PATH --harness codex|pi|custom --check "npm ci && npm test" [--source-ref REF]
280
+ [--inference-provider PROVIDER]
281
+ [--delivery-provider github --delivery-repository https://github.com/OWNER/REPO --delivery-target main|dev]
247
282
  install [--image LOCAL_REF] Build the standard image, or select an existing local image
248
283
  doctor | up | status | stop Inspect / operate your private installation
249
284
  foundation Read the operator setup skill; no installation required
@@ -280,6 +315,8 @@ try {
280
315
  [--source-ref REF] Pin a configured-repository ref before admission
281
316
  incident --file incident.json Submit a private, read-only incident draft
282
317
  approve JOB_ID | cancel JOB_ID Review gate / stop this attempt
318
+ publish JOB_ID Publish/reconcile the accepted candidate as one draft PR
319
+ abandon-delivery JOB_ID --branch-sha SHA Resolve an inspected pre-write collision; keep the remote branch
283
320
  retry JOB_ID Prove stop; retain old checkout and retry
284
321
  revise JOB_ID --file feedback.md [--source-ref REF]
285
322
  New build/check/review; source stays pinned unless a new ref is explicit
@@ -291,7 +328,7 @@ Runtime commands accept --state PATH. Default: ${DEFAULT_STATE}
291
328
  Demo default: ${DEFAULT_DEMO_STATE}
292
329
  The npm CLI keeps state outside the package; updates wait for stopped installations.
293
330
  Dashboard binds only to loopback; use SSH for remote access.
294
- Setup plan: ${join(ROOT, 'docs/setup.md')}
331
+ Setup plan: ${join(ROOT, 'docs/setup.md')}
295
332
  See docs/quickstart.md for task execution, evidence and recovery.`);
296
333
  else throw new Error(`Unknown command: ${command}`);
297
334
  } catch(error) {console.error(`Factory: ${error.message}`);process.exitCode=1;}
@@ -33,7 +33,7 @@ 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
37
 
38
38
  The current generic task form can name the Defence workflow; that is not a
39
39
  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,9 +18,14 @@ 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
 
@@ -67,6 +73,24 @@ agent reports, passing checks or review. Nonzero exits and missing/malformed
67
73
  reports still fail closed. Process termination during a host crash can leave
68
74
  incomplete logs and an interrupted attempt; apply process reconciliation above.
69
75
 
76
+ Before retention, Docker stdout/stderr logs have exact selected inference
77
+ credential values replaced. Mounted reports (`agent-report.md`, `review.json`,
78
+ `incident-report.json`) are sanitized after the phase container is confirmed
79
+ absent; if shutdown is uncertain, they remain in the private attempt directory
80
+ and ordinary recovery sanitizes them after confirming the stop. Filtering
81
+ includes selected API-key, token, secret, password and credential settings plus
82
+ credential fields inside selected Codex auth JSON. Reports are opened without
83
+ following symlinks and with nonblocking access before descriptor type checks, so
84
+ special files such as FIFOs are refused promptly. An unsafe or oversized report
85
+ cannot be promoted. If filtering cannot complete, recovery retains the selected
86
+ inference file and active fence; a repeated recovery remains blocked until the
87
+ fixed owned report can be safely filtered. Once process and container shutdown
88
+ are confirmed, repair or replace that report and retry ordinary recovery so it
89
+ can sanitize the output before removing the selected file and fence. This
90
+ bounded filter does not detect encoded or transformed values and does not
91
+ rewrite candidate files or patches; it is not a general data loss prevention
92
+ control.
93
+
70
94
  ## Recorded execution profiles
71
95
 
72
96
  Before each attempt executes, the controller freezes its effective configuration
@@ -126,3 +150,159 @@ Use `issue submissions` and `issue recover --key REQUEST_ID` against the same
126
150
  controller state. Recovery reads the original provider using the original
127
151
  identity; it does not publish again. Do not use a new request key to retry an
128
152
  uncertain write. See [provider ownership and recovery limits](integrations.md).
153
+
154
+ ## Trusted PR delivery
155
+
156
+ Delivery is available only for an accepted software job with current source,
157
+ candidate, check, independent review and approval records. Trusted publication
158
+ also requires matching private per-run execution records: native Codex/Pi for
159
+ build and review, deterministic verification and handoff, and explicitly
160
+ non-synthetic bound candidate/check/review artifacts. Missing, unknown,
161
+ synthetic or inconsistent evidence is not publication proof. Status, CLI, API
162
+ and dashboard use the same guard. The private operator configuration pins the
163
+ GitHub repository and target; task text and worker reports cannot provide
164
+ either. The controller stores the intent and remote branch/PR identifiers in
165
+ the job row before it writes. Status and task details show the receipt
166
+ separately from integration or deployment.
167
+
168
+ Inspect the same installation before recovery:
169
+
170
+ ```sh
171
+ software-defence-factory status --state /private/state/project
172
+ software-defence-factory publish JOB_ID --state /private/state/project
173
+ ```
174
+
175
+ After a lost response or controller restart, repeat `publish`. The controller
176
+ reads the exact configured target, generated branch and PR before attempting a
177
+ missing stage. A matching branch/PR is reused; a different remote head, target,
178
+ repository or collision is preserved and blocks recovery. It never force-pushes,
179
+ rebases, creates a second PR to avoid an uncertain response, or merges. Restore
180
+ the original trusted repository/target configuration if it changed; do not
181
+ redirect a saved intent.
182
+
183
+ Before any new content, branch or PR write, a saved intent is rechecked against
184
+ the protected run profiles and linked artifacts. A mock/synthetic record cannot
185
+ authorize a new write on retry. A known receipt or PR-creation checkpoint can
186
+ still use read-only provider reconciliation; if that cannot establish the
187
+ existing effect, the uncertain record remains blocked from further writes.
188
+
189
+ Before advertising or attempting a new write, status and the shared publisher
190
+ qualify the exact accepted Git trees again. Factory reads the immutable admitted
191
+ base commit and reconstructs the accepted candidate tree from the protected,
192
+ digest-bound patch. Added, changed, deleted or symlinked
193
+ `.github/workflows/*.yml` and `*.yaml` definitions block trusted publication.
194
+ Every base workflow must parse with the bundled YAML parser and use the
195
+ supported literal `push`, `pull_request`, `pull_request_target` or
196
+ `workflow_dispatch` trigger subset; dispatch inputs and other events such as
197
+ `workflow_run` or `workflow_call` are unsupported. A manual dispatch is
198
+ evaluated with the generated branch as its selected ref, as GitHub permits
199
+ 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)).
200
+ Any declared PR activity type is treated as potentially active for that
201
+ candidate PR. Branch filters are interpreted only when their patterns contain
202
+ ASCII letters, digits, `.`, `_`, `-` or `/`; exact literals are checked against
203
+ the generated branch and configured target. Other patterns are treated as
204
+ possible matches. This includes `*`, `**`, `?`, `+`, `[]`, `!` and escaped
205
+ characters from GitHub's [filter syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#filter-pattern-cheat-sheet).
206
+ Path filters are also treated as possible matches.
207
+
208
+ Conditions support only `&&`-joined `==`/`!=` comparisons against quoted
209
+ strings using `github.ref`, `github.event_name`, `github.head_ref`,
210
+ `github.base_ref` or `vars.NAME`; `||`, functions and other contexts are
211
+ refused. Supported ASCII string comparisons follow GitHub's case-insensitive
212
+ [expression semantics](https://docs.github.com/en/actions/reference/workflows-and-actions/expressions).
213
+ Non-ASCII mismatches stay unknown rather than guessing Unicode case folding.
214
+ For `pull_request`, `github.ref` stays unknown because the future merge ref
215
+ cannot be established from a guessed PR number. Unknown comparisons cannot
216
+ prove a privileged job inactive. Dynamic permissions, malformed YAML and
217
+ ambiguous guards block publication with a reason while leaving patch delivery
218
+ available.
219
+
220
+ Each job that could run for the generated branch push, a branch-selected
221
+ manual dispatch or a PR event must have explicit effective permissions limited
222
+ to `contents: read` or `none`, a known GitHub-hosted runner, and no secret
223
+ context, protected environment, reusable workflow call, OIDC permission or
224
+ deployment permission. A privileged release job can qualify only when a simple
225
+ literal conjunction proves it cannot run for each matching event. The current
226
+ `ci.yml` main-only release guard is supported. Before any content, branch or PR
227
+ write, the existing provider readback still confirms that the configured
228
+ target commit and tree are the exact accepted base. Active jobs must use one of
229
+ the supported literal runner labels (`ubuntu-latest`,
230
+ `ubuntu-22.04`, `ubuntu-24.04`, `ubuntu-26.04`, `windows-latest`,
231
+ `windows-2022`, `windows-2025`, `macos-latest`, `macos-14`, `macos-15` or
232
+ `macos-26`). External action steps must be local or pinned to an immutable
233
+ commit SHA or container digest.
234
+
235
+ This bounded check does not establish that all CI is safe. It does not inspect
236
+ repository/organization rules, webhooks, external CI or other organization
237
+ automation, nor does it audit the behavior of referenced third-party actions.
238
+ Unsupported cases refuse trusted publication and leave normal local checks,
239
+ review, approval and manual patch delivery available.
240
+
241
+ After publication is confirmed, readback follows the saved PR identity and
242
+ records its current open draft/ready, closed or merged state and checks. This
243
+ read-only refresh does not require the source branch to remain after closure or
244
+ the PR base commit to equal today's target commit. It still checks the saved
245
+ repository, target and branch names, PR head, and accepted delivery commit's
246
+ tree and parent. A changed identity or candidate stays a visible conflict; a
247
+ provider outage retains the last known publication receipt. First publication
248
+ continues to require the accepted current base and an open draft PR.
249
+
250
+ The dashboard offers reconciliation for saved `intent`, `publishing`,
251
+ `uncertain` and safely retryable `blocked` checkpoints. Legacy accepted jobs
252
+ without candidate-bound check, review and approval evidence are labelled
253
+ unverified and cannot be published. Deleting an issue with any unconfirmed
254
+ delivery record is blocked in the dashboard and controller API; inspect or
255
+ reconcile the saved remote effect before removing the job. A confirmed
256
+ `published` receipt is no longer unresolved.
257
+
258
+ The shared capability advertises new publication only with ready evidence and
259
+ no saved delivery or a known resumable write state. A branch-only `conflict`
260
+ has no publish action; inspect it and use explicit local abandonment only
261
+ after the exact branch head and absence of an associated PR are confirmed.
262
+ Conflicts with a saved PR receipt and pending PR-creation checkpoints retain a
263
+ read-only reconciliation action. Unknown and abandoned delivery states cannot
264
+ advertise or start publication.
265
+
266
+ The shared delivery status marks each available PR action as `publish` or
267
+ `reconcile`; task details use that mode for idle and pending button wording.
268
+ While a request is pending, publication shows “Publishing…” and read-only
269
+ recovery shows “Reconciling…”. A conflicted record with a saved PR receipt
270
+ therefore stays visibly a readback action.
271
+
272
+ A collision found while the record is still at `intent` is known to precede
273
+ provider writes. Inspect the exact branch in GitHub, then use the task action
274
+ **Abandon local delivery; keep remote branch**, or confirm its current head with
275
+ the CLI:
276
+
277
+ ```sh
278
+ software-defence-factory status --state /private/state/project
279
+ software-defence-factory abandon-delivery JOB_ID --branch-sha INSPECTED_SHA --state /private/state/project
280
+ ```
281
+
282
+ The controller checks the latest run, saved delivery identity, current branch
283
+ head and exact repository/target, then confirms that no PR is attached. It
284
+ records the inspected identity and local resolution in the job row. It does not
285
+ write, update or delete the remote branch or create/accept a PR. The `abandoned`
286
+ state disables later publication and permits local issue removal while retaining
287
+ the delivery record, run history and evidence. A stale or changed branch, any
288
+ associated/unknown PR, changed destination, later delivery stage or uncertain
289
+ provider effect remains blocked; reconcile or inspect it before taking another
290
+ action.
291
+
292
+ CLI publication and branch-resolution requests have a ten-minute deadline
293
+ because they can require multiple provider requests. Other CLI API requests
294
+ keep their five-second deadline. If the client reaches its deadline, inspect
295
+ `status` after the controller action finishes. Repeat `publish` to reconcile a
296
+ publication; repeat `abandon-delivery` only after inspecting the currently
297
+ reported branch identity.
298
+
299
+ The receipt reads the exact PR head's check runs and commit statuses. Raw
300
+ conclusions are retained. `success`, `skipped` and `neutral` do not make the
301
+ aggregate fail, while skipped/neutral are distinct from an executed passing
302
+ check. `pending`, `unknown`, failed and unavailable remain distinct; unknown
303
+ conclusions and incomplete pagination stay unknown. This aggregate does not
304
+ establish required-check completeness or merge authorization. A target/base,
305
+ candidate or policy change requires fresh applicable checks, review and
306
+ approval; do not edit SQLite or acceptance evidence to bypass the guard. A stale
307
+ target after branch creation leaves that unique branch for inspection and does
308
+ not open a PR. A PR does not imply integration or deployment.
package/docs/setup.md CHANGED
@@ -155,11 +155,79 @@ model servers, public listeners, Docker socket mounts or whole account folders.
155
155
  Cloud inference likewise needs a real provider/model connectivity check without
156
156
  printing credentials. A model-list/health response is connectivity evidence;
157
157
  qualifying model output requires a separately accepted bounded task.
158
+ Store only supported inference settings in `model.env`; jobs reject unrelated
159
+ names such as deployment credentials. Codex receives its OpenAI setting group,
160
+ and Pi receives only its operator-selected provider group. Use
161
+ `--inference-provider` with `init` when Pi's provider is not encoded in the
162
+ operator-configured model name or when multiple provider groups are stored.
163
+ An installation already using Codex account auth may set the validated
164
+ `FACTORY_CODEX_AUTH_JSON` single-line JSON value in private `model.env`; only
165
+ Codex agent phases receive it. Pi and checks do not. See the [quickstart
166
+ inference guidance](quickstart.md#connect-an-application) for constraints.
158
167
 
159
168
  Checkpoint: record the exact source revision, image ID, check command, resource
160
169
  limits, inference connectivity and CI result. Keep product controllers stopped
161
170
  until their tasks are explicitly ready to run.
162
171
 
172
+ ### Optional trusted PR delivery
173
+
174
+ Patch-only handoff is the default. When the operator intends to enable GitHub
175
+ delivery for this installation, configure the exact canonical repository and
176
+ target while initializing it, for example:
177
+
178
+ ```sh
179
+ software-defence-factory init --repo /absolute/path/to/app --harness pi \
180
+ --check "npm ci && npm test" --source-ref main \
181
+ --delivery-provider github \
182
+ --delivery-repository https://github.com/OWNER/REPO \
183
+ --delivery-target main --state /private/state/my-app
184
+ ```
185
+
186
+ The admitted source ref and PR target are independent. Only `main` and `dev`
187
+ are supported target choices in this release. The destination must match the
188
+ canonical origin retained at admission. A changed/renamed origin, moved target
189
+ base or changed Factory policy blocks delivery until fresh applicable evidence
190
+ exists. Configure this before admitting work. Unknown providers remain
191
+ patch-only.
192
+
193
+ Before enabling trusted PR delivery, confirm the repository workflows fit the
194
+ bounded qualification in [recovery](recovery.md#trusted-pr-delivery). Factory
195
+ compares the immutable admitted base and accepted candidate trees, refuses any
196
+ candidate change to a GitHub Actions workflow, and requires jobs that can run
197
+ for generated-branch pushes, PR events or selected-ref manual dispatches to use
198
+ explicit `contents: read` or `none`, a known GitHub-hosted runner, and no
199
+ secrets, protected environments, OIDC or deploy permissions. Unsupported
200
+ workflow syntax keeps the patch-only path available. Organization hooks and
201
+ other external CI automation remain operator-owned and are not audited by this
202
+ check.
203
+
204
+ The operator's authenticated `gh` identity stays on the controller. Keep GitHub
205
+ credentials out of `model.env`, project files and worker containers. Do not add
206
+ deploy credentials or a Docker socket for PR delivery. Use only the existing
207
+ authorized repository and the repository access already approved for the
208
+ operator; do not create a new fixture repository or request broader access.
209
+
210
+ The release lead owns the live provider/browser qualification after installing
211
+ the published candidate separately. Use a separate private state against the
212
+ already authorized Factory repository and `main`; ordinary issue admission,
213
+ checks, independent review and approval must produce the disposable candidate.
214
+ Give its issue a title beginning **[Factory PR handoff proof]**, then use the
215
+ normal **Publish accepted candidate as draft PR** action. Record the job,
216
+ candidate SHA/tree, generated unique branch, exact draft PR, base/head/tree and
217
+ triggered check states. The CLI allows up to ten minutes for this multi-request
218
+ controller action; other CLI API requests retain their five-second deadline.
219
+ If the client deadline expires, inspect status and repeat `publish JOB_ID` so
220
+ the controller can reconcile its saved intent. Restart the installed controller
221
+ and repeat `publish JOB_ID`; verify the same branch and PR head are read back
222
+ and no second PR appears. Delete issue is disabled while the delivery is
223
+ unresolved, and the controller rejects the same removal through its API. Leave
224
+ pending/unknown checks labelled as such. After inspection, close the proof PR
225
+ without merging its fixture change. Do not publish a worker candidate from
226
+ inside its sandbox.
227
+
228
+ Mocks exercise controller and receipt behavior only. They are not live GitHub
229
+ publication or browser proof; record each separately in [qualification](proof.md).
230
+
163
231
  ## 5. Enable only the intended background services
164
232
 
165
233
  A newly initialized state has no jobs. Before adopting older state, establish
@@ -242,6 +310,8 @@ Copy this private completion record into the installation's handoff:
242
310
  | History and intentionally stopped products preserved | Pending | |
243
311
  | Backups, logs, stop/update/rollback owner and guide | Pending | |
244
312
  | First bounded application task | Not started | Separate task authority and proof |
313
+ | Optional trusted PR provider and target explicitly configured, or patch-only retained | Pending | |
314
+ | Release lead live disposable Factory draft PR/readback/restart/close proof | Not started | Separate from mocked provider tests |
245
315
 
246
316
  Use Pass, Fail or Not applicable with a reason; never infer success from an
247
317
  installed file. Keep host identities, credentials, raw logs and customer details
@@ -0,0 +1,20 @@
1
+ import { githubDeliveryProvider } from './providers/github-delivery.mjs';
2
+
3
+ // Provider selection is a trusted installation setting. Unknown providers do
4
+ // not get guessed URLs, credentials or a write path; they remain patch-only.
5
+ export function deliveryProvider(config, integrations = {}) {
6
+ const provider = config.delivery?.provider;
7
+ if (provider !== 'github') return { id: provider || null, supported: false };
8
+ return integrations.deliveryProvider || githubDeliveryProvider();
9
+ }
10
+
11
+ export function deliveryProviderInfo(config, provider) {
12
+ const enabled = provider?.supported === true && config.delivery?.provider === provider.id;
13
+ return {
14
+ enabled,
15
+ mode: enabled ? 'trusted_pr' : 'patch_only',
16
+ provider: config.delivery?.provider || null,
17
+ repository: enabled ? config.delivery.repository : null,
18
+ target: enabled ? config.delivery.target : null,
19
+ };
20
+ }