software-defence-factory 0.13.1 → 0.14.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.
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { harnessPreset, parseRoleDefinition, ROLE_CAPABILITIES } from '../factory/role-definition.mjs';
3
+ import { inspectDefinition, previewDefinition, previewRollback } from '../factory/definition-store.mjs';
2
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, openSync, closeSync, rmSync, renameSync, realpathSync } from 'node:fs';
3
5
  import { resolve, join } from 'node:path';
4
6
  import { randomBytes } from 'node:crypto';
@@ -41,8 +43,7 @@ function init(repo, harness='codex', check='', port=7331, sourceRef='HEAD', deli
41
43
  if ([repo,state,ROOT].some(p=>/[,\n\r]/.test(p))) throw new Error('Paths cannot contain commas or line breaks');
42
44
  if (runCandidateGit(repo,'rev-parse','--show-toplevel') !== repo) throw new Error('--repo must be the Git root');
43
45
  runCandidateGit(repo,'rev-parse','HEAD');
44
- const presets={codex:['codex','exec','--json','--ephemeral','--sandbox','danger-full-access','-'],pi:['pi','--mode','json','--print','--no-session','--no-extensions','--skill','/factory-skills'],mock:['node','/opt/factory/mock.mjs']};
45
- const argv=harness==='custom'?JSON.parse(flags['command-json'] || 'null'):presets[harness];
46
+ const argv=harness==='custom'?JSON.parse(flags['command-json'] || 'null'):harnessPreset(harness);
46
47
  if (!argv) throw new Error('Select codex, pi, mock or custom with --command-json');
47
48
  if (flags.model && ['codex','pi'].includes(harness)) argv.splice(harness==='codex'?argv.length-1:argv.length,0,'--model',flags.model);
48
49
  mkdirSync(state,{recursive:true,mode:0o700});state=realpathSync(state);chmodSync(state,0o700);
@@ -198,7 +199,28 @@ try {
198
199
  else if(command==='tunnel')await manageService('tunnel',positional[0],state,flags);
199
200
  else if(command==='foundation')console.log(foundationSkill().content);
200
201
  else if(['definition','workflows','agents','skills'].includes(command)) {
202
+ if (command === 'definition' && positional.length) {
203
+ const action = positional[0];
204
+ if (!['export', 'validate', 'diff', 'apply', 'rollback'].includes(action) || positional.length !== 1) throw new Error('Choose definition export|validate|diff|apply|rollback');
205
+ let value;
206
+ if (['validate', 'diff', 'apply'].includes(action)) {
207
+ if (!flags.file) throw new Error('--file is required');
208
+ value = JSON.parse(readFileSync(resolve(flags.file), 'utf8'));
209
+ }
210
+ let result;
211
+ if (action === 'export') result = inspectDefinition(state).definition;
212
+ else if (action === 'rollback' && !flags['expected-revision']) result = previewRollback(state);
213
+ else if (action === 'validate' && !existsSync(join(state, 'factory.json'))) result = { valid: true, definition: parseRoleDefinition(value), capabilities: ROLE_CAPABILITIES, resolution: 'Requires an installation for inherited settings' };
214
+ else if (['validate', 'diff'].includes(action)) result = previewDefinition(state, value);
215
+ else {
216
+ if (!flags['expected-revision']) throw new Error('--expected-revision from the current preview is required');
217
+ result = await api(state, `/api/v1/definition/${action}`, { expected_revision: flags['expected-revision'], ...(action === 'apply' ? { definition: value } : {}) });
218
+ }
219
+ console.log(JSON.stringify(result, null, 2));
220
+ process.exit(0);
221
+ }
201
222
  const definition=factoryDefinition(configAt(state));
223
+ definition.role_definition = inspectDefinition(state);
202
224
  const value=command==='agents'?definition.agents:command==='skills'?{agents:definition.skills,operators:definition.operator_skills}:definition;
203
225
  console.log(JSON.stringify(value,null,2));
204
226
  }
@@ -321,6 +343,9 @@ Compatibility executable: software-defence-factory (same runtime and state)
321
343
  web probe --state PATH Execute the pinned local Chromium readiness probe
322
344
  foundation Read the operator setup skill; no installation required
323
345
  definition | agents | skills Inspect roles, instructions and installation settings
346
+ definition export|validate|diff Portable roles; validate/diff require --file PATH
347
+ definition apply --file PATH --expected-revision HASH
348
+ definition rollback --expected-revision HASH Idle controller only
324
349
  inbox [--page N] [--issue-state open|closed|all] [--source inbox|factory]
325
350
  Repository backlog (default); factory: execution-only array
326
351
  infrastructure | automations Inspect host/worker and automation state
package/docs/concepts.md CHANGED
@@ -9,14 +9,14 @@ The CLI `definition` command and dashboard Definition page read that same catalo
9
9
  | Controller | One project's queue, policy, HTTP API and dashboard | One Node/SQLite service |
10
10
  | Worker | Executes jobs on a host | One local execution process; bounded Docker containers |
11
11
  | Harness | Runs an agent | Codex, Pi, a custom command, or synthetic mock |
12
- | Agent | Responsibility and instructions | Implement, Review, Investigate; one shared harness/model profile |
12
+ | Agent | Responsibility and instructions | Implement, Review, Investigate; inherited or explicit harness/model per role |
13
13
  | Skill | Reusable instructions | Six job skills; separate operator Factory Foundation |
14
14
  | Workflow | Ordered steps and gates | Software: Implement → Check → Review → Accept; Defence: Investigate |
15
15
  | Issue | Bounded work request | Remote issue lives in its provider; a local brief needs no remote issue |
16
16
  | Execution | Admitted work and its attempts | Stored in the private SQLite queue with source link and evidence |
17
17
  | Provider | Repository issue integration | GitHub adapter first; unknown remotes retain local execution |
18
18
  | Automation | External schedule and agent context | Owned by the selected harness; calls Factory CLI/API, no Factory cron |
19
- | Definition | Effective roles, workflows, skills and settings | Installed method plus private factory.json; read-only catalog |
19
+ | Definition | Effective roles, workflows, skills and settings | Installed method, private settings and explicitly adopted portable role definition |
20
20
 
21
21
  Inbox combines an explicitly loaded provider page with retained execution
22
22
  history in one list/board, grouped by canonical issue identity. Off-page sources
@@ -29,7 +29,8 @@ Accept is an operator gate. Triage/specification precede admission; evaluation
29
29
  is separately scoped work, not an automatic hidden agent phase.
30
30
 
31
31
  All job skills are available read-only. Role instructions identify relevant
32
- skills; per-role skill/access/harness profiles are not yet supported. Inference
32
+ skills; per-role skill/access controls remain deferred. Harness/model selection
33
+ uses the [role definition contract](definition.md). Inference
33
34
  authentication, GitHub identity and SSH access are separate boundaries. Browser
34
35
  GitHub sign-in does not configure `gh` on the controller host, and host `gh` auth
35
36
  does not sign a browser in. Writing a blank local issue needs neither a GitHub issue nor
@@ -0,0 +1,188 @@
1
+ # Role definitions (0.14.0)
2
+
3
+ The portable definition selects harness/model profiles for **Implement**, **Review**
4
+ and **Investigate**. CLI, local API and the existing Agents/Definition pages use
5
+ one parser, resolver, capability matrix and application path. This first #53 slice
6
+ does not make skills, resources, access, workflow gates or schedules editable.
7
+
8
+ ```json
9
+ {
10
+ "version": 1,
11
+ "roles": {
12
+ "implement": { "harness": "inherit" },
13
+ "review": { "harness": "pi", "model": "anthropic/operator-selected-model" },
14
+ "investigate": { "harness": "codex", "model": "operator-selected-model", "reasoningEffort": "high" }
15
+ }
16
+ }
17
+ ```
18
+
19
+ The root accepts exactly `version` and `roles`. Version must be numeric `1`;
20
+ role names are exactly `implement`, `review`, `investigate`. Missing roles and
21
+ missing `harness` mean `inherit`. Each role accepts only `harness`, `model` and
22
+ `reasoningEffort`; unknown fields, roles, versions and incompatible combinations
23
+ fail explicitly. The authoritative implementation and capability matrix are in
24
+ [role-definition.mjs](../factory/role-definition.mjs), not a second schema copy.
25
+
26
+ - `inherit` preserves the installed private command, including a custom wrapper.
27
+ An omitted model inherits the installed model. Model/reasoning overrides require
28
+ an inherited Codex/Pi adapter; custom and mock commands have no such controls.
29
+ For an inherited Codex model or effort override, one bounded option parser
30
+ supports direct `codex exec` argv (also `e`). Model selection replaces
31
+ separate/attached `-m` and `--model` forms and direct
32
+ `-c`/`--config model=...` settings. An explicit model emits one `--model VALUE`;
33
+ `null` removes those inherited selections. Unrelated supported options, their
34
+ values and the prompt remain intact; new options precede the prompt or `--`.
35
+ Effort selection replaces direct `model_reasoning_effort` settings in supported
36
+ separate/attached `-c`/`--config` forms with one setting. Effort-only changes
37
+ preserve inherited model arguments exactly, without substituting installed
38
+ model metadata. Model-only changes preserve inherited effort settings.
39
+ Opaque wrappers, unknown switches/arity, profile selectors (`-p`/`--profile` or
40
+ config profile layers), nested commands, multiple prompts, options after a
41
+ positional prompt and duplicate model flags left by an effort-only selection
42
+ are refused before adoption. Use the Codex preset or correct the private
43
+ command while stopped. All-inherit commands remain byte-for-byte unchanged.
44
+ - Explicit `codex` or `pi` uses the same maintained preset as `init`, even when
45
+ the installed harness has the same name. No command, path, privilege, network,
46
+ credential or provider-authorization argument can enter the portable payload.
47
+ - `model` is a user-selected identifier (1–128 identifier characters, no paths,
48
+ URLs, whitespace or command switches). Omit it for the explicit harness default;
49
+ `null` also requests that default. Explicit Pi requires a supported
50
+ `provider/model` prefix so credential selection is unambiguous. For inherited Pi,
51
+ a model must agree with any installed private `inferenceProvider`.
52
+ - This slice exposes Codex `reasoningEffort` values `low`, `medium`, `high`, passed
53
+ as `-c model_reasoning_effort="VALUE"`. Omission preserves private/default
54
+ behavior; it does not attest a detected effort. Pi effort control is unavailable.
55
+ The [Codex configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference)
56
+ documents the setting; actual supported levels depend on the selected model.
57
+ Validation does not call a model or prove availability, quality or readiness.
58
+
59
+ ## Inspect, review and adopt
60
+
61
+ ```sh
62
+ factory definition --state PATH
63
+ factory agents --state PATH
64
+ factory skills --state PATH
65
+ factory definition export --state PATH > factory-roles.json
66
+ factory definition validate --state PATH --file factory-roles.json
67
+ factory definition diff --state PATH --file factory-roles.json
68
+ factory definition apply --state PATH --file factory-roles.json --expected-revision REVISION
69
+ factory definition rollback --state PATH
70
+ factory definition rollback --state PATH --expected-revision REVISION
71
+ ```
72
+
73
+ `definition`, `agents`, `skills`, and compatibility `workflows` remain readable
74
+ while stopped. Export prints only portable JSON. Validate and diff resolve against
75
+ the installation and return the normalized definition, effective selections,
76
+ changes and **current** revision. With no installation, validate performs portable
77
+ syntax/explicit-combination validation and reports that inheritance still needs
78
+ an installation. No account or provider connection is needed for validation.
79
+ Rollback without `--expected-revision` previews the previous definition. Supply
80
+ that revision to perform rollback; it restores the previous selection, not the
81
+ old revision number. Keep `factory-roles.json` in a repository for ordinary code
82
+ review or in a private local file. No filename is automatically discovered or
83
+ adopted from source, issues, labels or a candidate checkout.
84
+
85
+ Apply and rollback require the running single controller. The browser uses the
86
+ same endpoints, with labelled role inputs and a diff before explicit Apply.
87
+ A matching revision and an idle controller are required: queued, running,
88
+ cancelling, awaiting-approval work, pending actions, maintenance and unreconciled
89
+ executor fences block application. Stale or invalid requests fail without
90
+ changing installed settings. Reload and preview again after a conflict.
91
+ Configuration adoption never admits work or installs/discovers a schedule.
92
+
93
+ Authenticated API requests (bearer operator token or local session):
94
+
95
+ | Request | JSON body / result |
96
+ | --- | --- |
97
+ | `GET /api/v1/definition` | Current `revision`, portable `definition`, `effective`, `inherited_harness`, `capabilities`, `rollback_available` |
98
+ | `POST /api/v1/definition/validate` | `{"definition": {...}}` → validation and effective diff |
99
+ | `POST /api/v1/definition/diff` | `{"definition": {...}}`, or `{"rollback": true}` → current revision, proposed definition, effective changes |
100
+ | `POST /api/v1/definition/apply` | `{"definition": {...}, "expected_revision": "HASH"}` → installed readback |
101
+ | `POST /api/v1/definition/rollback` | `{"expected_revision": "HASH"}` → installed readback |
102
+
103
+ `GET /api/v1/definitions` remains the safe catalog and includes `role_definition`.
104
+ HTTP 400 means invalid schema/combination; 403 means missing session or rejected
105
+ Host/Origin; 409 means stale/busy/recovery-required or no rollback available.
106
+ Storage errors return 500 with details in private controller logs. CLI errors
107
+ exit nonzero. Neither catalog nor role responses expose private command argv,
108
+ credential values or machine bindings. Unresolved provider defaults remain null;
109
+ execution resolves them from the private installation before launch.
110
+
111
+ ## Private state, policy and evidence
112
+
113
+ Existing `factory.json` is not rewritten or migrated. With no overrides, the
114
+ legacy effective configuration and policy hash are byte-compatible, including
115
+ private custom commands and legacy `agent` naming. All-inherit adoption is a
116
+ no-op. Shared resources, image, checks, repository identity and credentials stay
117
+ private and are changed while stopped through their existing operator workflow.
118
+ Foundation supplies setup guidance, not another configuration source.
119
+
120
+ The controller stores the adopted definition and up to ten prior definitions in
121
+ one private `role-definition.json`, using a flushed temporary file and atomic
122
+ rename. Process interruption before rename leaves the old record; after rename,
123
+ the complete new record and history are present. Incomplete temporary files are
124
+ not loaded. The adopted record must be a regular private file; repository symlinks
125
+ are refused. Revisions include a monotonic sequence and the private base settings,
126
+ so an old revision is stale even after rollback. Back up the entire private state
127
+ while stopped; rollback restores portable roles only, not separately edited base
128
+ settings, credentials or historical attempts. New reads do not use a startup cache.
129
+
130
+ Before each attempt, trusted configuration resolves and freezes **all roles** in
131
+ private `execution-config.json`. The executor selects its phase from that frozen
132
+ configuration. One policy hash covers the common configuration across Build,
133
+ deterministic Verify, Review and Handoff; it is not a hash of separately merged
134
+ phase settings. Protected v2 execution evidence records role, requested model,
135
+ provider, explicit effort and a digest binding the exact private command selection.
136
+ The executor checks that binding before launching. Role overrides require v2;
137
+ old v1 evidence cannot attest them. Unchanged legacy configurations still emit
138
+ v1 with the actual 0.14.0 runtime version. See [compatibility](npm.md#protected-evidence-compatibility).
139
+
140
+ Changing a role invalidates prior acceptance/checkpoint evidence under that
141
+ policy. Rollback can restore the exact prior policy; it never edits evidence or
142
+ automatically retries work. Recovery, reviewed-candidate continuation and new
143
+ publication use the same current-policy guards. Existing published receipt
144
+ reconciliation retains its read-only semantics. With any role override, task-wide
145
+ `--model`/API `model` is refused, including a value equal to the old shared model.
146
+ Clear it and use the role definition; unchanged legacy installations keep their
147
+ existing task-model behavior.
148
+
149
+ Only the selected provider's inference settings reach each agent phase. Codex
150
+ account-auth data never reaches Pi, including Pi using OpenAI. Check and Handoff
151
+ receive no inference credentials. Git/issue-provider identity remains on the
152
+ controller. No new credentials, endpoint binding, model download or provider is
153
+ created by a definition.
154
+
155
+ ## Role and output boundaries
156
+
157
+ Build implements, runs appropriate checks, writes its report and returns. Factory
158
+ owns separate Review; this slice grants no nested reviewer capability or budget.
159
+ Review receives the original source baseline and protected exact-candidate,
160
+ current-policy checks. Its candidate mount is read-only, without installed
161
+ project dependencies; `/tmp` is bounded/noexec. Reuse passing protected checks.
162
+ Additional reproduction requires suitable available capabilities; do not repeat
163
+ impossible installs or treat an unavailable rerun as a code finding or a pass.
164
+
165
+ Factory owns `/output/agent-report.md`, `/output/review.json` and
166
+ `/output/incident-report.json` as appropriate to the phase. Harness-only final
167
+ output belongs in ephemeral `/tmp/factory-final-message.md`, never those reports.
168
+ The maintained presets do not capture a final-message file over a Factory report.
169
+ Role adoption rejects recognized inherited Codex final-message flags targeting
170
+ non-ephemeral destinations; it never rewrites the private command. Opaque custom
171
+ wrappers remain operator-owned. Redaction covers owned reports/logs, not every
172
+ arbitrary file a custom harness might write. A missing required report remains
173
+ a failure, never a final-message substitution.
174
+
175
+ All six installed runtime skills remain available read-only, with content/source
176
+ hashes in the catalog. Per-role recommendations are guidance, not access controls.
177
+ Triage/spec are pre-admission method skills, not executable roles. Check remains
178
+ deterministic; acceptance is human-owned. No foreman, scheduler, application
179
+ publishing or production recovery authority is added.
180
+
181
+ Host, image/toolchain and provider readiness require actual probes on each target
182
+ platform. `doctor` and existing image/browser probes keep their bounded meanings;
183
+ configuration alone is not readiness attestation. Lead qualification of the exact
184
+ package, isolated mixed-role Docker execution and desktop/390/320px browser
185
+ inspection in both themes remain separate gates. #53 stays open for per-role
186
+ skills/resources/access, broader project/flow/automation definitions and readiness
187
+ attestation. #69 owns local/hybrid model benchmarks, #51 measurement and #70
188
+ improvement proposals.
@@ -20,7 +20,8 @@ shell endpoint or a second scheduler.
20
20
  | Request changes | `revise --file`, optional `--from admitted-source\|reviewed-candidate` or `--source-ref REF` | `request_changes` with `revision_mode`; continuation binds current run/head/tree from shared status | Explicit starting point, baseline and reviewed checkpoint; inline errors | JSON action result; unfinished Build checkpoints remain #42 |
21
21
  | Remove a stopped task | No command | `DELETE /api/v1/jobs/:id` | Remove action | Add CLI; keep existing recoverability/history semantics |
22
22
  | Evidence list/read/download | No command | Authenticated artifact routes | Files/preview/download | Add CLI with matching access and size/path rules |
23
- | Roles, workflows and packaged skills | `definition`, `agents`, `skills` JSON (also while stopped) | `GET /api/v1/definitions` | Agents, Skills and Definition | Shared read-only catalog; future editing must preserve common policy/gates |
23
+ | Roles, workflows and packaged skills | `definition`, `agents`, `skills` JSON (also while stopped) | `GET /api/v1/definitions` | Agents, Skills and Definition | Shared catalog plus bounded role harness/model editing; deterministic checks, gates, skills and resources stay shared |
24
+ | Portable role definitions | `definition export/validate/diff/apply/rollback` | Authenticated `/api/v1/definition` and typed operations | Agents/Definition role editor, diff, explicit idle Apply and rollback | One schema/capability matrix; revision CAS and bounded atomic history; [request shapes](definition.md) |
24
25
  | Project repository links | Validated links in `status` | `project_links` from configured Git origin | View repo / optional GitHub issue link | Provider creates/issues reads are separate from Git source links |
25
26
  | Recorded token usage | Per-attempt `usage` and `token_usage` in `status` | Same status records | Analytics, task rows, metadata/history | No billing estimate; partial/unknown coverage stays explicit |
26
27
  | Analytics/filtering | Raw status available | Source queue records | Derived views | Expose equivalent queries/summaries without inventing usage data |
@@ -35,7 +36,7 @@ shell endpoint or a second scheduler.
35
36
  | Synthetic qualification | `demo`, `qualify` | No qualification endpoint | Synthetic disclosure only | Explicit separate state; never target an application accidentally |
36
37
  | 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 | Issue Start work, local request and revision forms accept a ref; task detail shows requested ref, resolved SHA and prior source commits | Build/retry use retained objects; revisions start fresh by default; explicit continuation keeps the reviewed tree and recorded source; a new ref replaces the base; legacy source remains unknown |
37
38
  | 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 |
38
- | Delivered-commit check readback | `publish JOB_ID` reconciles, `status` returns saved checks | Existing publish/status routes return the same `delivery_status.checks` | Delivered-commit checks, per-row conclusions/scope, uncertainty and refresh | 0.13.1 verifies exact commit/provider identity; installed/live/browser qualification pending; no required-check or mergeability claim |
39
+ | Delivered-commit check readback | `publish JOB_ID` reconciles, `status` returns saved checks | Existing publish/status routes return the same `delivery_status.checks` | Delivered-commit checks, per-row conclusions/scope, uncertainty and refresh | 0.13.1 delivered in PR #97 with installed/live/browser qualification recorded; no required-check or mergeability claim |
39
40
  | 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) |
40
41
 
41
42
  The current generic task form can name the Defence workflow; that is not a
package/docs/npm.md CHANGED
@@ -104,8 +104,8 @@ installation or retained attempt needs them.
104
104
 
105
105
  ## Protected evidence compatibility
106
106
 
107
- Factory 0.13.1 recognizes version-1 execution profiles emitted by native
108
- **0.8.0, 0.9.0, 0.9.1, 0.10.0, 0.11.0, 0.11.1, 0.11.2, 0.12.0, 0.13.0 and 0.13.1**. This is an exact allowlist in
107
+ Factory 0.14.0 recognizes version-1 execution profiles emitted by native
108
+ **0.8.0, 0.9.0, 0.9.1, 0.10.0, 0.11.0, 0.11.1, 0.11.2, 0.12.0, 0.13.0, 0.13.1 and 0.14.0**. This is an exact allowlist in
109
109
  `factory/execution-profile.mjs`, independent of the installed package version;
110
110
  it is not a semver range or an automatic promise for later releases. Unknown
111
111
  runtime strings, unknown profile formats and incomplete legacy acceptance
@@ -170,6 +170,17 @@ and publication validation tests; old records remain immutable and unknown
170
170
  versions remain blocked. Provider check observations are separate from protected
171
171
  Verify evidence and cannot authorize acceptance or publication.
172
172
 
173
+ Version 0.14.0 preserves the v1 writer only for unchanged inherited installations:
174
+ policy bytes, candidate/check/review/acceptance bindings, mounts and credential
175
+ rules stay compatible. Adopted role overrides use **v2 from 0.14.0 only**, recording
176
+ role/provider/effort and an exact selection digest. The executor verifies the
177
+ frozen common configuration before phase selection; continuation and both delivery
178
+ validation paths require matching protected v2 evidence and private frozen config.
179
+ V1 cannot attest an override. Mixed roles share one policy across deterministic
180
+ and agent phases. Rollback can restore a prior policy without rewriting a record.
181
+ See the [definition contract](definition.md); installed Docker/provider qualification
182
+ remains separate from schema compatibility.
183
+
173
184
  Old installed releases retain their own files and mount paths until an idle,
174
185
  reviewed update. No installed catalog, execution profile or historical hash is
175
186
  rewritten by this layout change. Qualify the exact updated package/image before
package/docs/recovery.md CHANGED
@@ -13,7 +13,7 @@ Use `status --state PATH` and the private supervisor.log to identify the active
13
13
 
14
14
  Do not remove active.json merely to unblock a job. Establish that its PID, process group and labelled containers are stopped. PID reuse or missing process identity requires operator investigation. Preserve logs and work before cleanup.
15
15
 
16
- For backup, stop the installation and copy the complete private state directory, including SQLite files, factory.json and credentials, to an authorized private destination. Restore only while stopped. Update the repository path if it moved, verify ownership/permissions and the pinned image, then inspect state before any retry. Keep previous backups; no automatic destructive schema migration is provided.
16
+ For backup, stop the installation and copy the complete private state directory, including SQLite files, factory.json, role-definition.json when present, and credentials, to an authorized private destination. Restore only while stopped. Update the repository path if it moved, verify ownership/permissions and the pinned image, then inspect state before any retry. Keep previous backups; no automatic destructive schema migration is provided.
17
17
 
18
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.
19
19
 
@@ -136,6 +136,16 @@ Unknown formats/versions remain blocked. Published PR receipt reconciliation
136
136
  stays read-only, and compatibility does not relax the original-base/remote-target
137
137
  guard or add checkpoint continuation.
138
138
 
139
+ In 0.14.0, role overrides use v2 protected evidence and one frozen common policy.
140
+ Use [definition rollback](definition.md) through an idle controller to restore the
141
+ previous portable selection. It preserves all attempts and changes the definition
142
+ revision; it does not change separate private settings or credentials. Changed
143
+ roles invalidate checkpoint/new-publication evidence until the exact prior policy
144
+ is restored or a fresh complete cycle succeeds. Task-wide model overrides are
145
+ refused when role overrides are active; an old task with such a model needs a
146
+ replacement admission without it. Incomplete definition temporary files are ignored
147
+ on restart; never splice historical execution evidence into a new profile.
148
+
139
149
  Attempts made before this metadata existed display **Not recorded
140
150
  (legacy/unknown)**. Do not copy today's profile onto them. For an investigation,
141
151
  compare the retained per-attempt measurement, logs, candidate/check/review hashes
package/docs/setup.md CHANGED
@@ -183,6 +183,18 @@ An installation already using Codex account auth may set the validated
183
183
  Codex agent phases receive it. Pi and checks do not. See the [quickstart
184
184
  inference guidance](quickstart.md#connect-an-application) for constraints.
185
185
 
186
+ For separate Implement/Review/Investigate harnesses or models, export and review
187
+ [the portable role definition](definition.md), then explicitly apply its preview
188
+ through the idle controller. No overrides preserves the current private command
189
+ and legacy evidence. Do not put credential values, host paths, resources or
190
+ arbitrary command arguments in that file. Shared settings remain in private
191
+ `factory.json`; inference stays in `model.env`. Adoption starts no work or schedule.
192
+ Keep harness final-message capture in ephemeral `/tmp`, separate from durable
193
+ Factory reports. Review reuses protected exact-candidate checks; its read-only
194
+ workspace and noexec temporary space cannot repeat every dependency install or
195
+ executable fixture. Qualify each selected role with the actual image, provider
196
+ and platform; declared configuration and a discovered binary are not model proof.
197
+
186
198
  Checkpoint: record the exact source revision, image ID, check command, resource
187
199
  limits, inference connectivity and CI result. Keep product controllers stopped
188
200
  until their tasks are explicitly ready to run.
package/docs/workflows.md CHANGED
@@ -5,7 +5,7 @@ installation with `factory definition --state PATH`, or its
5
5
  Agents, Skills and Definition pages. Both read the same effective catalog.
6
6
 
7
7
  Software follows **Implement → Check → Review → Accept & hand off**. Implement
8
- and Review are separate agent invocations using the installation's harness/model.
8
+ and Review are separate agent invocations using their resolved role profiles.
9
9
  Check runs the project's command. Accept requires operator approval and confirms
10
10
  that the candidate and policy still match their evidence. It does not push,
11
11
  merge, deploy or publish. Revisions start a new build/check/review and preserve
@@ -139,11 +139,12 @@ handling, resource limits and stop behavior. No schedule is created by onboardin
139
139
 
140
140
  ## Change the definition
141
141
 
142
- Select the harness, model, check and resource limits in the private `factory.json`
143
- while the installation is stopped, then restart. Workflow order and packaged
144
- skills change through reviewed Factory releases. This release does not support
145
- per-role profiles or arbitrary editable workflow graphs. Versioned editable
146
- definitions are tracked in [#53](https://github.com/arcitai/software-and-defence-factory/issues/53).
142
+ Version 0.14.0 supports portable harness/model selections for Implement, Review
143
+ and Investigate. Use the shared [role definition workflow](definition.md) to
144
+ export, validate, preview, apply while idle and roll back. Missing overrides
145
+ inherit the exact private installation profile. Shared checks/resources remain
146
+ in private `factory.json`; workflow order and packaged skills follow reviewed
147
+ Factory releases. Broader definition editing stays under #53.
147
148
 
148
149
 
149
150
  Inbox groups URL case, HTTP/HTTPS, trailing-slash, query and fragment aliases by GitHub
@@ -0,0 +1,72 @@
1
+ import { closeSync, existsSync, fsyncSync, openSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { configAt, digest, harnessOf } from './lib.mjs';
5
+ import { DefinitionError, installedRoleRecord, parseRoleDefinition, publicRoleProfiles, resolveRoleProfiles, ROLE_CAPABILITIES } from './role-definition.mjs';
6
+
7
+ function context(state) {
8
+ const config = configAt(state), record = installedRoleRecord(state);
9
+ delete config.roleDefinition;
10
+ return { config, record, revision: digest(JSON.stringify({ config, sequence: record.sequence, definition: record.definition })) };
11
+ }
12
+ function view({ config, record, revision }) {
13
+ return { revision, inherited_harness: harnessOf(config), definition: record.definition, rollback_available: record.history.length > 0,
14
+ effective: publicRoleProfiles(resolveRoleProfiles(config, record.definition)), capabilities: ROLE_CAPABILITIES };
15
+ }
16
+ export function inspectDefinition(state) { return view(context(state)); }
17
+ export function previewDefinition(state, value) {
18
+ const current = context(state), definition = parseRoleDefinition(value);
19
+ const before = view(current);
20
+ const effective = publicRoleProfiles(resolveRoleProfiles(current.config, definition));
21
+ const changes = [];
22
+ for (const role of ROLE_CAPABILITIES.roles) {
23
+ if (JSON.stringify(before.definition.roles[role]) !== JSON.stringify(definition.roles[role])
24
+ || JSON.stringify(before.effective[role]) !== JSON.stringify(effective[role]))
25
+ changes.push({ role, before: before.effective[role], after: effective[role], selection: definition.roles[role] });
26
+ }
27
+ return { revision: current.revision, definition, effective, changes, valid: true,
28
+ qualification: 'Configuration only; harness, model availability and quality are not qualified' };
29
+ }
30
+ export function previewRollback(state) {
31
+ const previous = context(state).record.history.at(-1);
32
+ if (!previous) throw new DefinitionError('No previous definition is available', 409);
33
+ return { ...previewDefinition(state, previous), rollback: true };
34
+ }
35
+ function assertIdle(queue, state) {
36
+ if (queue.closing || queue.maintenance || queue.active || queue.actions.size || queue.issueActions.size
37
+ || queue.all().some(job => !['succeeded', 'failed', 'cancelled', 'blocked', 'interrupted'].includes(job.state)))
38
+ throw new DefinitionError('Controller is busy; finish or cancel active and awaiting-approval work before applying a definition', 409);
39
+ const jobs = join(state, 'jobs');
40
+ if (existsSync(jobs) && readdirSync(jobs).some(id => /^job_[a-z0-9]+$/.test(id) && existsSync(join(jobs, id, 'active.json'))))
41
+ throw new DefinitionError('Executor recovery is required before applying a definition', 409);
42
+ }
43
+ function persist(state, record) {
44
+ const destination = join(state, 'role-definition.json'), temp = `${destination}.${randomUUID()}.tmp`;
45
+ let fd;
46
+ try {
47
+ fd = openSync(temp, 'wx', 0o600);
48
+ writeFileSync(fd, JSON.stringify(record, null, 2) + '\n'); fsyncSync(fd); closeSync(fd); fd = undefined;
49
+ // Definition and rollback history share one atomic replacement. A crash before
50
+ // rename retains the old record; after rename it retains the complete new one.
51
+ renameSync(temp, destination);
52
+ } finally { if (fd !== undefined) closeSync(fd); rmSync(temp, { force: true }); }
53
+ }
54
+ // Called synchronously only by the single controller, after request body parsing.
55
+ // No await between CAS, idle check and atomic replacement; admission cannot interleave.
56
+ export function changeDefinition(state, queue, input, rollback = false) {
57
+ const allowed = rollback ? ['expected_revision'] : ['expected_revision', 'definition'];
58
+ if (!input || Object.keys(input).some(key => !allowed.includes(key)) || typeof input.expected_revision !== 'string')
59
+ throw new DefinitionError('Expected definition and expected_revision (rollback accepts expected_revision only)');
60
+ const current = context(state);
61
+ if (input.expected_revision !== current.revision) throw new DefinitionError('Definition revision changed; refresh and preview again', 409);
62
+ assertIdle(queue, state);
63
+ const value = rollback ? current.record.history.at(-1) : input.definition;
64
+ if (!value) throw new DefinitionError('No previous definition is available', 409);
65
+ const preview = previewDefinition(state, value);
66
+ if (!rollback && JSON.stringify(preview.definition) === JSON.stringify(current.record.definition)) return view(current);
67
+ const history = rollback ? current.record.history.slice(0, -1) : [...current.record.history, current.record.definition].slice(-10);
68
+ const record = { version: 1, sequence: current.record.sequence + 1, definition: preview.definition, history };
69
+ persist(state, record);
70
+ return { revision: digest(JSON.stringify({ config: current.config, sequence: record.sequence, definition: record.definition })),
71
+ inherited_harness: harnessOf(current.config), definition: record.definition, effective: preview.effective, rollback_available: history.length > 0, capabilities: ROLE_CAPABILITIES };
72
+ }
@@ -1,3 +1,4 @@
1
+ import { resolveRoleProfiles, publicRoleProfiles } from './role-definition.mjs';
1
2
  import { readinessMapping } from './issue-lifecycle.mjs';
2
3
  import { readFileSync } from 'node:fs';
3
4
  import { join } from 'node:path';
@@ -23,6 +24,7 @@ const skillRoles = {
23
24
  };
24
25
  export function factoryDefinition(config) {
25
26
  const harness = harnessOf(config);
27
+ const profiles = publicRoleProfiles(resolveRoleProfiles(config));
26
28
  const skills = Object.entries(skillRoles).map(([role, purpose]) => {
27
29
  const id = `factory-${role}`, path = `kit/skills/${id}/SKILL.md`;
28
30
  const content = readFileSync(join(ROOT, path), 'utf8');
@@ -33,12 +35,12 @@ export function factoryDefinition(config) {
33
35
  version: 1, workflows, terminology: JSON.parse(readFileSync(join(ROOT, 'factory/terminology.json'), 'utf8')),
34
36
  agents: Object.entries(phaseInfo).filter(([, info]) => info.owner === 'agent').map(([phase, info]) => ({
35
37
  id: phase === 'build' ? 'implement' : phase === 'defence' ? 'investigate' : phase,
36
- phase, title: info.title, responsibility: info.description, harness, model: config.model || null, skills: info.skills,
38
+ phase, title: info.title, responsibility: info.description, ...profiles[phase === 'build' ? 'implement' : phase === 'defence' ? 'investigate' : phase], skills: info.skills,
37
39
  })),
38
40
  operator_skills: [foundationSkill()],
39
41
  automations: { owner: 'harness', harness, managed_by_factory: false, discovery: 'unavailable', items: null },
40
42
  commands: Object.entries(phaseInfo).map(([name, info]) => ({ name, ...info, prompt: info.description,
41
- executor: info.owner === 'agent' ? harnessOf(config) : 'factory', timeout: `${config.timeoutSeconds}s` })),
43
+ executor: info.owner === 'agent' ? profiles[name === 'build' ? 'implement' : name === 'defence' ? 'investigate' : name].harness : 'factory', timeout: `${config.timeoutSeconds}s` })),
42
44
  skills,
43
45
  configuration: { issueReadinessLabels: readinessMapping(config.issueReadinessLabels), harness, agent: harness, // agent is a v1 compatibility alias
44
46
  model: config.model || null, check: config.check, timeoutSeconds: config.timeoutSeconds,
@@ -52,7 +54,7 @@ export function factoryDefinition(config) {
52
54
  method: {
53
55
  preparation: ['factory-triage', 'factory-spec'], evaluation: ['factory-evaluate'],
54
56
  instructions: 'All six skills are available read-only to agent phases. A skill is an instruction set, not a separate agent or an automatic workflow step.',
55
- customization: 'Choose the harness, model, project check and resource limits in the installation’s private factory.json while stopped, then restart. Task scope belongs in the issue or work instructions. Phase order, gates and packaged skills change through a reviewed Factory release; they are not editable prompt templates.',
57
+ customization: 'Use definition export/validate/diff/apply/rollback for role harness and model selections. Shared checks and resources remain in private factory.json, edited while stopped. Task scope belongs in the issue or work instructions. Phase order, gates and packaged skills change through a reviewed Factory release; they are not editable prompt templates.',
56
58
  },
57
59
  };
58
60
  }
@@ -1,8 +1,9 @@
1
+ import { installedRoleRecord, hasRoleOverrides } from './role-definition.mjs';
1
2
  import { lstatSync, readFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { isDeepStrictEqual } from 'node:util';
4
5
  import { configAt, digest } from './lib.mjs';
5
- import { effectiveExecutionConfig, isSupportedExecutionProfile } from './execution-profile.mjs';
6
+ import { effectiveExecutionConfig, isSupportedExecutionProfile, assertFrozenExecution } from './execution-profile.mjs';
6
7
  import { assertCurrentWebArtifacts, assertCurrentWebEvidence } from './web-verification.mjs';
7
8
 
8
9
  export function assertCurrentHandoffEvidence(meta, checks, review, policyHash, config = {}, job) {
@@ -35,6 +36,12 @@ export function trustedExecutionProfile(state, job, run, phase, expectedPolicy)
35
36
  for (const path of [join(state, 'jobs'), folder, join(folder, 'artifacts'), join(folder, 'artifacts', run.id)])
36
37
  assertPrivateDirectory(path);
37
38
  const profile = readPrivateJson(join(folder, 'artifacts', run.id, 'execution.json'));
39
+ if (profile.version === 1 && hasRoleOverrides(installedRoleRecord(state).definition)) return false;
40
+ if (profile.version === 2) {
41
+ const frozen = readPrivateJson(join(folder, run.id, 'execution-config.json'));
42
+ if (!frozen.roleDefinition || !frozen.resolvedRoleProfiles || digest(JSON.stringify(frozen)) !== expectedPolicy) return false;
43
+ assertFrozenExecution(frozen, profile, phase);
44
+ }
38
45
  const agentPhase = phase === 'build' || phase === 'review';
39
46
  const executorMatches = agentPhase
40
47
  ? ['codex', 'pi'].includes(profile.executor)