software-defence-factory 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/software-defence-factory.mjs +6 -4
- package/docs/definition.md +131 -9
- package/docs/interfaces.md +1 -1
- package/docs/npm.md +17 -3
- package/docs/setup.md +41 -0
- package/factory/definition-store.mjs +32 -20
- package/factory/execution-evidence.mjs +1 -1
- package/factory/execution-profile.mjs +15 -4
- package/factory/executor.mjs +14 -1
- package/factory/lib.mjs +6 -3
- package/factory/local-inference.mjs +71 -0
- package/factory/model-environment.mjs +1 -0
- package/factory/pi-local-launch.mjs +48 -0
- package/factory/processes.mjs +1 -0
- package/factory/queue.mjs +8 -0
- package/factory/role-definition.mjs +28 -7
- package/factory/server.mjs +9 -5
- package/factory/ui/assets/{index-CHHAC-bP.css → index-BDZ8MNoD.css} +1 -1
- package/factory/ui/assets/{index-3_2X3VdI.js → index-BqZ4LAbp.js} +2 -2
- package/factory/ui/index.html +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ The runtime supplies policy and six focused skills to its isolated jobs. `init`
|
|
|
48
48
|
|
|
49
49
|
Each result belongs to a specific candidate commit and policy. A failed check blocks delivery. Changing the candidate or check policy invalidates earlier evidence. Approval records a handoff; publishing, merging and deployment follow the application's separate authority.
|
|
50
50
|
|
|
51
|
-
The project dashboard has an **Inbox**, measured **Analytics**, **Agents**, **Skills**, **Automations**, **Definition** and **Infrastructure**. Inbox opens on a shared list/board of loaded repository issues and local work, with status, search and checkbox filters, readiness and linked execution attempts. New issue offers repository templates or a blank creation form. Create an issue on the supported repository provider, then choose Start work separately; local brief execution remains available. CLI `issue` exposes the same intake. Definition lives with settings above the theme control. The CLI reads the same definition and controller state. Agent roles use a selected harness such as Codex or Pi; a worker executes their isolated jobs on a host. See [concepts](docs/concepts.md) and [supported interfaces](docs/interfaces.md). Optional automations belong to the selected harness, which calls Factory CLI/API. Factory runs no cron scheduler. See [provider boundaries](docs/integrations.md).
|
|
51
|
+
The project dashboard has an **Inbox**, measured **Analytics**, **Agents**, **Skills**, **Automations**, **Definition** and **Infrastructure**. Inbox opens on a shared list/board of loaded repository issues and local work, with status, search and checkbox filters, readiness and linked execution attempts. New issue offers repository templates or a blank creation form. Create an issue on the supported repository provider, then choose Start work separately; local brief execution remains available. CLI `issue` exposes the same intake. Definition lives with settings above the theme control. The CLI reads the same definition and controller state. Agent roles use a selected harness such as Codex or Pi, with opt-in private keyless local bindings for local/hybrid setups; a worker executes their isolated jobs on a host. See [concepts](docs/concepts.md) and [supported interfaces](docs/interfaces.md). Optional automations belong to the selected harness, which calls Factory CLI/API. Factory runs no cron scheduler. See [provider boundaries](docs/integrations.md).
|
|
52
52
|
|
|
53
53
|
The optional **defence** workflow accepts scoped incident evidence and produces a private, read-only draft. It does not monitor production or claim verified recovery. See [defence integration](docs/defence-integration.md).
|
|
54
54
|
|
|
@@ -202,19 +202,21 @@ try {
|
|
|
202
202
|
if (command === 'definition' && positional.length) {
|
|
203
203
|
const action = positional[0];
|
|
204
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;
|
|
205
|
+
let value, bindings;
|
|
206
206
|
if (['validate', 'diff', 'apply'].includes(action)) {
|
|
207
207
|
if (!flags.file) throw new Error('--file is required');
|
|
208
208
|
value = JSON.parse(readFileSync(resolve(flags.file), 'utf8'));
|
|
209
|
+
if (flags['bindings-file']) bindings = JSON.parse(readFileSync(resolve(flags['bindings-file']), 'utf8'));
|
|
209
210
|
}
|
|
210
211
|
let result;
|
|
211
212
|
if (action === 'export') result = inspectDefinition(state).definition;
|
|
212
213
|
else if (action === 'rollback' && !flags['expected-revision']) result = previewRollback(state);
|
|
214
|
+
else if (action === 'validate' && bindings !== undefined && !existsSync(join(state, 'factory.json'))) throw new Error('Local binding validation requires an installation');
|
|
213
215
|
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);
|
|
216
|
+
else if (['validate', 'diff'].includes(action)) result = previewDefinition(state, value, bindings);
|
|
215
217
|
else {
|
|
216
218
|
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 } : {}) });
|
|
219
|
+
result = await api(state, `/api/v1/definition/${action}`, { expected_revision: flags['expected-revision'], ...(action === 'apply' ? { definition: value, ...(bindings === undefined ? {} : { local_bindings: bindings }) } : {}) });
|
|
218
220
|
}
|
|
219
221
|
console.log(JSON.stringify(result, null, 2));
|
|
220
222
|
process.exit(0);
|
|
@@ -343,7 +345,7 @@ Compatibility executable: software-defence-factory (same runtime and state)
|
|
|
343
345
|
web probe --state PATH Execute the pinned local Chromium readiness probe
|
|
344
346
|
foundation Read the operator setup skill; no installation required
|
|
345
347
|
definition | agents | skills Inspect roles, instructions and installation settings
|
|
346
|
-
definition export|validate|diff Portable roles; validate/diff require --file PATH
|
|
348
|
+
definition export|validate|diff Portable roles; validate/diff require --file PATH; optional --bindings-file PRIVATE_PATH
|
|
347
349
|
definition apply --file PATH --expected-revision HASH
|
|
348
350
|
definition rollback --expected-revision HASH Idle controller only
|
|
349
351
|
inbox [--page N] [--issue-state open|closed|all] [--source inbox|factory]
|
package/docs/definition.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Role definitions (0.
|
|
1
|
+
# Role definitions (0.15.0)
|
|
2
2
|
|
|
3
3
|
The portable definition selects harness/model profiles for **Implement**, **Review**
|
|
4
4
|
and **Investigate**. CLI, local API and the existing Agents/Definition pages use
|
|
@@ -18,8 +18,8 @@ does not make skills, resources, access, workflow gates or schedules editable.
|
|
|
18
18
|
|
|
19
19
|
The root accepts exactly `version` and `roles`. Version must be numeric `1`;
|
|
20
20
|
role names are exactly `implement`, `review`, `investigate`. Missing roles and
|
|
21
|
-
missing `harness` mean `inherit`. Each role accepts only `harness`, `model` and
|
|
22
|
-
`
|
|
21
|
+
missing `harness` mean `inherit`. Each role accepts only `harness`, `model`, `reasoningEffort` and
|
|
22
|
+
`localBinding`; unknown fields, roles, versions and incompatible combinations
|
|
23
23
|
fail explicitly. The authoritative implementation and capability matrix are in
|
|
24
24
|
[role-definition.mjs](../factory/role-definition.mjs), not a second schema copy.
|
|
25
25
|
|
|
@@ -41,14 +41,16 @@ fail explicitly. The authoritative implementation and capability matrix are in
|
|
|
41
41
|
positional prompt and duplicate model flags left by an effort-only selection
|
|
42
42
|
are refused before adoption. Use the Codex preset or correct the private
|
|
43
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
|
|
44
|
+
- Explicit `codex` or hosted `pi` uses the same maintained preset as `init`, even when
|
|
45
45
|
the installed harness has the same name. No command, path, privilege, network,
|
|
46
46
|
credential or provider-authorization argument can enter the portable payload.
|
|
47
47
|
- `model` is a user-selected identifier (1–128 identifier characters, no paths,
|
|
48
48
|
URLs, whitespace or command switches). Omit it for the explicit harness default;
|
|
49
|
-
`null` also requests that default. Explicit Pi requires a supported
|
|
49
|
+
`null` also requests that default. Explicit hosted Pi requires a supported
|
|
50
50
|
`provider/model` prefix so credential selection is unambiguous. For inherited Pi,
|
|
51
51
|
a model must agree with any installed private `inferenceProvider`.
|
|
52
|
+
- `localBinding` selects an explicitly adopted private keyless Pi binding instead
|
|
53
|
+
of a hosted model; see the opt-in contract below.
|
|
52
54
|
- This slice exposes Codex `reasoningEffort` values `low`, `medium`, `high`, passed
|
|
53
55
|
as `-c model_reasoning_effort="VALUE"`. Omission preserves private/default
|
|
54
56
|
behavior; it does not attest a detected effort. Pi effort control is unavailable.
|
|
@@ -124,7 +126,7 @@ the complete new record and history are present. Incomplete temporary files are
|
|
|
124
126
|
not loaded. The adopted record must be a regular private file; repository symlinks
|
|
125
127
|
are refused. Revisions include a monotonic sequence and the private base settings,
|
|
126
128
|
so an old revision is stale even after rollback. Back up the entire private state
|
|
127
|
-
while stopped; rollback restores
|
|
129
|
+
while stopped; rollback restores roles and adopted local bindings, not separately edited base
|
|
128
130
|
settings, credentials or historical attempts. New reads do not use a startup cache.
|
|
129
131
|
|
|
130
132
|
Before each attempt, trusted configuration resolves and freezes **all roles** in
|
|
@@ -135,7 +137,7 @@ phase settings. Protected v2 execution evidence records role, requested model,
|
|
|
135
137
|
provider, explicit effort and a digest binding the exact private command selection.
|
|
136
138
|
The executor checks that binding before launching. Role overrides require v2;
|
|
137
139
|
old v1 evidence cannot attest them. Unchanged legacy configurations still emit
|
|
138
|
-
v1 with the actual
|
|
140
|
+
v1 with the actual installed runtime version. See [compatibility](npm.md#protected-evidence-compatibility).
|
|
139
141
|
|
|
140
142
|
Changing a role invalidates prior acceptance/checkpoint evidence under that
|
|
141
143
|
policy. Rollback can restore the exact prior policy; it never edits evidence or
|
|
@@ -149,8 +151,8 @@ existing task-model behavior.
|
|
|
149
151
|
Only the selected provider's inference settings reach each agent phase. Codex
|
|
150
152
|
account-auth data never reaches Pi, including Pi using OpenAI. Check and Handoff
|
|
151
153
|
receive no inference credentials. Git/issue-provider identity remains on the
|
|
152
|
-
controller.
|
|
153
|
-
|
|
154
|
+
controller. Portable roles create no credentials or model downloads. The opt-in local binding
|
|
155
|
+
contract below explicitly adopts private endpoint details separately.
|
|
154
156
|
|
|
155
157
|
## Role and output boundaries
|
|
156
158
|
|
|
@@ -186,3 +188,123 @@ inspection in both themes remain separate gates. #53 stays open for per-role
|
|
|
186
188
|
skills/resources/access, broader project/flow/automation definitions and readiness
|
|
187
189
|
attestation. #69 owns local/hybrid model benchmarks, #51 measurement and #70
|
|
188
190
|
improvement proposals.
|
|
191
|
+
|
|
192
|
+
## Opt-in local bindings (0.15.0, #69)
|
|
193
|
+
|
|
194
|
+
A portable role can select `{"harness":"pi","localBinding":"local-worker"}`
|
|
195
|
+
instead of a hosted `model`. The same reference can serve Implement, Review and
|
|
196
|
+
Investigate, or any role can retain Codex/a hosted Pi model for a hybrid setup.
|
|
197
|
+
Binding references cannot be combined with a role model or reasoning effort.
|
|
198
|
+
They require the maintained Pi preset; private wrappers are not binding adapters.
|
|
199
|
+
|
|
200
|
+
Connection details belong to explicitly adopted **private installation state**,
|
|
201
|
+
not the portable definition. No repository filename, issue or model response is
|
|
202
|
+
an active configuration source. Save a private JSON binding map, for example:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"local-worker": {
|
|
207
|
+
"endpoint": "http://operator-selected-host:8080/v1",
|
|
208
|
+
"model": "exact-installed-model-id",
|
|
209
|
+
"contextWindow": 32768,
|
|
210
|
+
"maxTokens": 4096,
|
|
211
|
+
"reasoningEffort": "default",
|
|
212
|
+
"compat": {
|
|
213
|
+
"maxTokensField": "max_tokens",
|
|
214
|
+
"supportsUsageInStreaming": true,
|
|
215
|
+
"requiresToolResultName": false
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
These illustrative values are not defaults or measured recommendations. Select
|
|
222
|
+
the endpoint, exact model and limits for your installation. `endpoint` is a base
|
|
223
|
+
URL for OpenAI **chat completions**, not a full `/chat/completions` URL. The job's
|
|
224
|
+
isolated network must reach it; its loopback address is not the host's loopback.
|
|
225
|
+
Factory neither opens ports nor adjusts networking or inference services.
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
factory definition diff --state PRIVATE_STATE --file roles.json --bindings-file PRIVATE_BINDINGS.json
|
|
229
|
+
factory definition apply --state PRIVATE_STATE --file roles.json --bindings-file PRIVATE_BINDINGS.json --expected-revision HASH_FROM_DIFF
|
|
230
|
+
factory definition rollback --state PRIVATE_STATE --expected-revision CURRENT_HASH
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`validate`, `diff` and `apply` accept `--bindings-file`. Without it they retain the
|
|
234
|
+
installed binding map. Providing a map replaces it in full, so removing a referenced
|
|
235
|
+
binding fails until its roles are changed in the same request. `export` includes
|
|
236
|
+
only portable roles/references. CLI `definition` inspects private bindings locally.
|
|
237
|
+
The authenticated API adds optional `local_bindings` beside `definition` to
|
|
238
|
+
validate/diff/apply; `GET /api/v1/definition` returns both. Diff returns
|
|
239
|
+
`binding_changes`. Unauthenticated `/api/v1/definitions` omits endpoint maps;
|
|
240
|
+
authenticated readback includes them. The existing Agents/Definition editor uses
|
|
241
|
+
this same contract and shows a binding diff before Apply, including connection-only
|
|
242
|
+
changes. Configuration makes no inference/model-list requests and never starts work.
|
|
243
|
+
|
|
244
|
+
Bindings and roles share one revision, idle check, atomic record and at most ten
|
|
245
|
+
rollback snapshots. Rollback restores both together; stale/busy/invalid writes
|
|
246
|
+
preserve the prior state. Endpoint normalization is idempotent; URLs whose normalized
|
|
247
|
+
form contains forbidden escapes (including Unicode path characters) are rejected.
|
|
248
|
+
The complete serialized record and history are validated before replacement.
|
|
249
|
+
The version-2 private record reads old version-1 role
|
|
250
|
+
records without rewriting them. An old runtime cannot read a newly adopted v2
|
|
251
|
+
record: roll back profiles first and restore a stopped pre-upgrade private-state
|
|
252
|
+
backup before downgrading binaries. Do not hand-edit histories or frozen attempts.
|
|
253
|
+
|
|
254
|
+
The binding contract supports at most 16 named, keyless HTTP(S) endpoints. URLs
|
|
255
|
+
with user information, queries, fragments, escapes or malformed syntax are refused.
|
|
256
|
+
Model identifiers cannot contain shell syntax, whitespace or traversal. Context
|
|
257
|
+
must be 1,024–1,048,576 tokens; output must be 1–32,000 and smaller than context.
|
|
258
|
+
The output ceiling follows pinned Pi 0.73.1's simple chat adapter. Range acceptance
|
|
259
|
+
is **not** evidence of server allocation or useful task capacity. Only the three
|
|
260
|
+
shown compatibility fields are supported. Authentication, custom headers, shell
|
|
261
|
+
credential commands, arbitrary request fields, sampling overrides, custom thinking
|
|
262
|
+
maps and provider plugins are unsupported. Factory does not spoof local inference
|
|
263
|
+
as the hosted OpenAI provider.
|
|
264
|
+
|
|
265
|
+
The optional binding `reasoningEffort` accepts only these OpenAI-style requests:
|
|
266
|
+
|
|
267
|
+
| Binding choice | Chat-completions request |
|
|
268
|
+
| --- | --- |
|
|
269
|
+
| Omitted or `default` | No `reasoning_effort` override; server default applies |
|
|
270
|
+
| `none` | `reasoning_effort: "none"` |
|
|
271
|
+
| `low`, `medium`, `high` | `reasoning_effort` set to that exact value |
|
|
272
|
+
|
|
273
|
+
This is a request choice, not a measured thinking budget or quality claim. The
|
|
274
|
+
pinned Pi 0.73.1 adapter uses a fixed internal `thinkingLevelMap.off="none"` for
|
|
275
|
+
`none`; `--thinking off` alone does **not** disable a server's default thinking.
|
|
276
|
+
Low/medium/high use the corresponding explicit Pi thinking level. Existing
|
|
277
|
+
bindings with no choice retain their omitted request and private policy shape;
|
|
278
|
+
no default is inserted into their saved configuration. The binding editor,
|
|
279
|
+
CLI/API diff, private readback, frozen selection and rollback share this choice.
|
|
280
|
+
Separate role bindings can request different efforts. Endpoint support and actual
|
|
281
|
+
behavior remain unqualified until tested on that installation. A server rejection
|
|
282
|
+
fails the job without fallback or retrying with a different choice; a server that
|
|
283
|
+
silently ignores the field cannot be detected from successful transport alone.
|
|
284
|
+
|
|
285
|
+
Before admission, all selected references must resolve. Each attempt freezes the
|
|
286
|
+
chosen endpoint, exact model, declared limits, reasoning request, compatibility settings and maintained
|
|
287
|
+
command along with the common resource/timeout/skill policy. Unselected bindings
|
|
288
|
+
are not included. Local/hybrid attempts emit protected **v3** evidence across all
|
|
289
|
+
phases, binding the selection digest and common policy. Public facts show configured
|
|
290
|
+
context/output, unknown actual allocation and unqualified quality. Old v1/v2 writers
|
|
291
|
+
cannot attest local bindings; unchanged legacy profiles keep their policy bytes and
|
|
292
|
+
compatible evidence. See [the compatibility audit](npm.md#protected-evidence-compatibility).
|
|
293
|
+
|
|
294
|
+
For the selected local role only, the executor creates a private single-model
|
|
295
|
+
`models.json` and deterministic launcher, mounted read-only at `/factory-local`.
|
|
296
|
+
`PI_CODING_AGENT_DIR` selects that directory; HOME is still ephemeral. The launcher
|
|
297
|
+
checks explicit provider/model argv, removes inherited credential/override inputs
|
|
298
|
+
and propagates Pi JSON provider failures even if Pi exits zero. The registry uses
|
|
299
|
+
Pi's required fixed non-secret key placeholder (`factory-local-keyless`), which may
|
|
300
|
+
be sent as a bearer value; this is not endpoint authentication. No host home, provider
|
|
301
|
+
catalog or credential store is mounted. Repository files cannot replace this mount.
|
|
302
|
+
Files are removed after confirmed container shutdown; uncertain shutdown retains
|
|
303
|
+
them behind the existing recovery fence until normal recovery confirms absence.
|
|
304
|
+
|
|
305
|
+
Local roles receive no cloud model environment; cloud roles receive no local
|
|
306
|
+
registry/override. Verify and Handoff receive neither. The local preset disables
|
|
307
|
+
automatic skills/extensions/templates but explicitly adds `--skill /factory-skills`,
|
|
308
|
+
preserving the six packaged skills. Recommendations remain guidance, not per-role
|
|
309
|
+
access controls. Build still returns to Factory's independent Review and operator
|
|
310
|
+
handoff; no duplicate review workflow is launched.
|
package/docs/interfaces.md
CHANGED
|
@@ -21,7 +21,7 @@ shell endpoint or a second scheduler.
|
|
|
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
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
|
|
24
|
+
| Portable roles and private local bindings | `definition export/validate/diff/apply/rollback`, optional `--bindings-file` for validate/diff/apply | 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 role/binding history; configured context distinct from unknown allocation and unqualified quality; [request shapes](definition.md) |
|
|
25
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 |
|
|
26
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 |
|
|
27
27
|
| Analytics/filtering | Raw status available | Source queue records | Derived views | Expose equivalent queries/summaries without inventing usage data |
|
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.
|
|
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.
|
|
107
|
+
Factory 0.15.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, 0.14.0 and 0.15.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
|
|
@@ -172,7 +172,7 @@ Verify evidence and cannot authorize acceptance or publication.
|
|
|
172
172
|
|
|
173
173
|
Version 0.14.0 preserves the v1 writer only for unchanged inherited installations:
|
|
174
174
|
policy bytes, candidate/check/review/acceptance bindings, mounts and credential
|
|
175
|
-
rules stay compatible. Adopted role overrides use **v2 from 0.14.0
|
|
175
|
+
rules stay compatible. Adopted role overrides use **v2 from 0.14.0 and 0.15.0**, recording
|
|
176
176
|
role/provider/effort and an exact selection digest. The executor verifies the
|
|
177
177
|
frozen common configuration before phase selection; continuation and both delivery
|
|
178
178
|
validation paths require matching protected v2 evidence and private frozen config.
|
|
@@ -227,3 +227,17 @@ delivery under [#61](https://github.com/arcitai/software-and-defence-factory/iss
|
|
|
227
227
|
|
|
228
228
|
References: [npm/npx](https://docs.npmjs.com/cli/v11/commands/npx/),
|
|
229
229
|
[npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
The 0.15.0 compatibility audit preserves unchanged v1/v2 policy construction,
|
|
233
|
+
protected checks, candidate reconstruction, read-only review and acceptance guards.
|
|
234
|
+
Historical 0.14.0 v2 validation compares its actual writer version rather than
|
|
235
|
+
relabeling it as the installed version. Local registry selection is a new execution
|
|
236
|
+
contract: every phase of a local/hybrid attempt requires **v3 from 0.15.0** and its
|
|
237
|
+
exact protected frozen configuration. v1/v2 cannot attest local binding fields,
|
|
238
|
+
providers or a common policy containing a selected local role. A selected endpoint,
|
|
239
|
+
model, limit or compatibility change invalidates the current policy; explicit
|
|
240
|
+
rollback can restore it without editing historical evidence. Controlled regressions
|
|
241
|
+
exercise legacy v2 readback and local v3 rejection/tamper cases. Installed-package
|
|
242
|
+
Docker and independent Review remain delivery qualification, not inferred from this
|
|
243
|
+
compatibility audit.
|
package/docs/setup.md
CHANGED
|
@@ -389,3 +389,44 @@ To add operator scope at admission, use `issue start --url URL --workflow softwa
|
|
|
389
389
|
--brief-file operator.md --state PATH` with an optional UTF-8 brief of at most
|
|
390
390
|
16000 characters. This keeps the remote identity and current-content check;
|
|
391
391
|
local requests still use `issue start --file` or `--draft` without `--brief-file`.
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
## Optional keyless local or hybrid inference
|
|
395
|
+
|
|
396
|
+
Use the [local binding recipe](definition.md#opt-in-local-bindings-0150-69) to adopt
|
|
397
|
+
an explicit private endpoint/model map and portable Pi role references. Keep the
|
|
398
|
+
working default and a stopped private-state backup; preview, apply while idle and
|
|
399
|
+
use revision-guarded rollback. A local Implement role can be paired with an existing
|
|
400
|
+
cloud Review role. No downloads, service restarts, port changes or jobs occur during
|
|
401
|
+
configuration. Authenticated endpoints, sampling overrides and custom thinking
|
|
402
|
+
maps are unsupported. Local bindings may request OpenAI-style `reasoningEffort`:
|
|
403
|
+
`default` (or omitted), `none`, `low`, `medium` or `high`. Default omits the request;
|
|
404
|
+
it does not disable server thinking. Qualify the endpoint's actual semantics before
|
|
405
|
+
using a request as a thinking-budget control. Unsupported requests fail without
|
|
406
|
+
fallback when the endpoint rejects them; transport success alone cannot prove a
|
|
407
|
+
server honored the request. Existing omitted choices remain unchanged.
|
|
408
|
+
|
|
409
|
+
Qualify the actual installed image and endpoint from an isolated job before using
|
|
410
|
+
it for application work. Record server/harness versions, exact model ID and digest,
|
|
411
|
+
quantization, GPU/backend and actual offload, available and peak memory, endpoint
|
|
412
|
+
reachability, tool-call/result compatibility and report/check/review outcomes.
|
|
413
|
+
Keep host inference access separate from controller/forge credentials. Unknown
|
|
414
|
+
values stay unknown; registry discovery only proves client capability.
|
|
415
|
+
|
|
416
|
+
Reconcile the client's configured context with the server's actual allocation and
|
|
417
|
+
per-request/model overrides. Do not infer a 65k allocation from a remembered agent
|
|
418
|
+
setting or a server default. A bounded 64k/128k allocation experiment, where
|
|
419
|
+
supported, must record KV-cache and Flash Attention settings, memory and allocation
|
|
420
|
+
success separately from long-context task quality. This worker supplies no such
|
|
421
|
+
measurements or driver/kernel advice. Do not recommend larger windows from the
|
|
422
|
+
schema's upper limit.
|
|
423
|
+
|
|
424
|
+
The operator still owns #69's matched two-fixture comparison: pinned bases,
|
|
425
|
+
comparable prompts/contexts/trials, a measured existing local baseline and one
|
|
426
|
+
selected alternative, decisive independent checks and separate Review contexts.
|
|
427
|
+
Retain failures, repairs and human time. Record wall time, observable prefill/decode,
|
|
428
|
+
input/cached/output tokens, memory/offload and all reviewer usage. Factory currently
|
|
429
|
+
leaves Pi usage unknown; controlled protocol fixtures are not token or quality
|
|
430
|
+
benchmarks. Include cloud Review in hybrid totals. No provider token billing for
|
|
431
|
+
local inference does not mean zero electricity, hardware or operator cost. Only
|
|
432
|
+
then qualify a suitable real Factory issue; no customer jobs are part of this slice.
|
|
@@ -1,36 +1,43 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { parseLocalBindings } from './local-inference.mjs';
|
|
1
3
|
import { closeSync, existsSync, fsyncSync, openSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
4
|
import { join } from 'node:path';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
4
6
|
import { configAt, digest, harnessOf } from './lib.mjs';
|
|
5
|
-
import { DefinitionError, installedRoleRecord, parseRoleDefinition, publicRoleProfiles, resolveRoleProfiles, ROLE_CAPABILITIES } from './role-definition.mjs';
|
|
7
|
+
import { DefinitionError, installedRoleRecord, parseRoleRecord, parseRoleDefinition, publicRoleProfiles, resolveRoleProfiles, ROLE_CAPABILITIES } from './role-definition.mjs';
|
|
6
8
|
|
|
9
|
+
const revisionOf = (config, record) => digest(JSON.stringify({ config, sequence: record.sequence, definition: record.definition, localBindings: record.localBindings }));
|
|
7
10
|
function context(state) {
|
|
8
11
|
const config = configAt(state), record = installedRoleRecord(state);
|
|
9
|
-
delete config.roleDefinition;
|
|
10
|
-
|
|
12
|
+
delete config.roleDefinition; delete config.localBindings;
|
|
13
|
+
record.localBindings ||= {};
|
|
14
|
+
return { config, record, revision: revisionOf(config, record) };
|
|
11
15
|
}
|
|
12
16
|
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 };
|
|
17
|
+
return { revision, inherited_harness: harnessOf(config), definition: record.definition, local_bindings: record.localBindings, rollback_available: record.history.length > 0,
|
|
18
|
+
effective: publicRoleProfiles(resolveRoleProfiles({ ...config, localBindings: record.localBindings }, record.definition)), capabilities: ROLE_CAPABILITIES };
|
|
15
19
|
}
|
|
16
20
|
export function inspectDefinition(state) { return view(context(state)); }
|
|
17
|
-
export function previewDefinition(state, value) {
|
|
21
|
+
export function previewDefinition(state, value, bindings) {
|
|
18
22
|
const current = context(state), definition = parseRoleDefinition(value);
|
|
19
23
|
const before = view(current);
|
|
20
|
-
const
|
|
24
|
+
const localBindings = parseLocalBindings(bindings === undefined ? current.record.localBindings : bindings);
|
|
25
|
+
const effective = publicRoleProfiles(resolveRoleProfiles({ ...current.config, localBindings }, definition));
|
|
21
26
|
const changes = [];
|
|
22
27
|
for (const role of ROLE_CAPABILITIES.roles) {
|
|
23
28
|
if (JSON.stringify(before.definition.roles[role]) !== JSON.stringify(definition.roles[role])
|
|
24
29
|
|| JSON.stringify(before.effective[role]) !== JSON.stringify(effective[role]))
|
|
25
30
|
changes.push({ role, before: before.effective[role], after: effective[role], selection: definition.roles[role] });
|
|
26
31
|
}
|
|
27
|
-
return { revision: current.revision, definition, effective, changes,
|
|
32
|
+
return { revision: current.revision, definition, local_bindings: localBindings, effective, changes,
|
|
33
|
+
binding_changes: Object.keys({ ...current.record.localBindings, ...localBindings }).filter(name => JSON.stringify(current.record.localBindings[name]) !== JSON.stringify(localBindings[name]))
|
|
34
|
+
.map(name => ({ reference: name, before: current.record.localBindings[name] || null, after: localBindings[name] || null })), valid: true,
|
|
28
35
|
qualification: 'Configuration only; harness, model availability and quality are not qualified' };
|
|
29
36
|
}
|
|
30
37
|
export function previewRollback(state) {
|
|
31
38
|
const previous = context(state).record.history.at(-1);
|
|
32
39
|
if (!previous) throw new DefinitionError('No previous definition is available', 409);
|
|
33
|
-
return { ...previewDefinition(state, previous), rollback: true };
|
|
40
|
+
return { ...previewDefinition(state, previous.definition, previous.localBindings), rollback: true };
|
|
34
41
|
}
|
|
35
42
|
function assertIdle(queue, state) {
|
|
36
43
|
if (queue.closing || queue.maintenance || queue.active || queue.actions.size || queue.issueActions.size
|
|
@@ -40,12 +47,12 @@ function assertIdle(queue, state) {
|
|
|
40
47
|
if (existsSync(jobs) && readdirSync(jobs).some(id => /^job_[a-z0-9]+$/.test(id) && existsSync(join(jobs, id, 'active.json'))))
|
|
41
48
|
throw new DefinitionError('Executor recovery is required before applying a definition', 409);
|
|
42
49
|
}
|
|
43
|
-
function persist(state,
|
|
50
|
+
function persist(state, serialized) {
|
|
44
51
|
const destination = join(state, 'role-definition.json'), temp = `${destination}.${randomUUID()}.tmp`;
|
|
45
52
|
let fd;
|
|
46
53
|
try {
|
|
47
54
|
fd = openSync(temp, 'wx', 0o600);
|
|
48
|
-
writeFileSync(fd,
|
|
55
|
+
writeFileSync(fd, serialized); fsyncSync(fd); closeSync(fd); fd = undefined;
|
|
49
56
|
// Definition and rollback history share one atomic replacement. A crash before
|
|
50
57
|
// rename retains the old record; after rename it retains the complete new one.
|
|
51
58
|
renameSync(temp, destination);
|
|
@@ -54,19 +61,24 @@ function persist(state, record) {
|
|
|
54
61
|
// Called synchronously only by the single controller, after request body parsing.
|
|
55
62
|
// No await between CAS, idle check and atomic replacement; admission cannot interleave.
|
|
56
63
|
export function changeDefinition(state, queue, input, rollback = false) {
|
|
57
|
-
const allowed = rollback ? ['expected_revision'] : ['expected_revision', 'definition'];
|
|
64
|
+
const allowed = rollback ? ['expected_revision'] : ['expected_revision', 'definition', 'local_bindings'];
|
|
58
65
|
if (!input || Object.keys(input).some(key => !allowed.includes(key)) || typeof input.expected_revision !== 'string')
|
|
59
66
|
throw new DefinitionError('Expected definition and expected_revision (rollback accepts expected_revision only)');
|
|
60
67
|
const current = context(state);
|
|
61
68
|
if (input.expected_revision !== current.revision) throw new DefinitionError('Definition revision changed; refresh and preview again', 409);
|
|
62
69
|
assertIdle(queue, state);
|
|
63
|
-
const value = rollback ? current.record.history.at(-1) : input.definition;
|
|
70
|
+
const value = rollback ? current.record.history.at(-1) : { definition: input.definition, localBindings: input.local_bindings };
|
|
64
71
|
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)
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
const preview = previewDefinition(state, value.definition, value.localBindings);
|
|
73
|
+
if (!rollback && JSON.stringify(preview.definition) === JSON.stringify(current.record.definition)
|
|
74
|
+
&& JSON.stringify(preview.local_bindings) === JSON.stringify(current.record.localBindings)) return view(current);
|
|
75
|
+
const history = rollback ? current.record.history.slice(0, -1) : [...current.record.history, { definition: current.record.definition, localBindings: current.record.localBindings }].slice(-10);
|
|
76
|
+
const record = { version: 2, sequence: current.record.sequence + 1, definition: preview.definition, localBindings: preview.local_bindings, history };
|
|
77
|
+
const serialized = JSON.stringify(record, null, 2) + '\n';
|
|
78
|
+
if (Buffer.byteLength(serialized) > 256000) throw new DefinitionError('Private definition history exceeds 256 KB; shorten binding endpoints before applying');
|
|
79
|
+
const roundtrip = parseRoleRecord(JSON.parse(serialized));
|
|
80
|
+
if (!isDeepStrictEqual(record, roundtrip)) throw new DefinitionError('Private definition record must round-trip without normalization');
|
|
81
|
+
const next = view({ config: current.config, record: roundtrip, revision: revisionOf(current.config, roundtrip) });
|
|
82
|
+
persist(state, serialized);
|
|
83
|
+
return next;
|
|
72
84
|
}
|
|
@@ -37,7 +37,7 @@ export function trustedExecutionProfile(state, job, run, phase, expectedPolicy)
|
|
|
37
37
|
assertPrivateDirectory(path);
|
|
38
38
|
const profile = readPrivateJson(join(folder, 'artifacts', run.id, 'execution.json'));
|
|
39
39
|
if (profile.version === 1 && hasRoleOverrides(installedRoleRecord(state).definition)) return false;
|
|
40
|
-
if (profile.version
|
|
40
|
+
if ([2, 3].includes(profile.version)) {
|
|
41
41
|
const frozen = readPrivateJson(join(folder, run.id, 'execution-config.json'));
|
|
42
42
|
if (!frozen.roleDefinition || !frozen.resolvedRoleProfiles || digest(JSON.stringify(frozen)) !== expectedPolicy) return false;
|
|
43
43
|
assertFrozenExecution(frozen, profile, phase);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publicLocalBinding } from './local-inference.mjs';
|
|
1
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
3
|
import { resolveRoleProfiles, ROLE_PHASES } from './role-definition.mjs';
|
|
3
4
|
import { harnessOf } from './lib.mjs';
|
|
@@ -18,19 +19,22 @@ import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
|
|
|
18
19
|
// deterministic phases, protected checks, isolation and acceptance remain compatible.
|
|
19
20
|
// Role overrides require v2, with role/provider/reasoning and a private-command
|
|
20
21
|
// selection digest. V1 can never attest an adopted role override.
|
|
22
|
+
// 0.15.0 keeps unchanged v1/v2 policies and writers; local selections require
|
|
23
|
+
// v3 for every phase. Legacy evidence cannot attest the new registry/launcher.
|
|
21
24
|
// Older writers still require exact patch/tree reconstruction before publication.
|
|
22
25
|
// Deliberately independent of VERSION: a release bump is not
|
|
23
26
|
// evidence compatibility. Re-audit this list for every trust-relevant writer,
|
|
24
27
|
// isolation or validation change; remove versions whose guarantees no longer
|
|
25
28
|
// satisfy current policy. See docs/npm.md. This predicate alone grants no trust.
|
|
26
|
-
const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['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', '0.14.0']);
|
|
29
|
+
const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['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', '0.14.0', '0.15.0']);
|
|
27
30
|
|
|
28
31
|
export function isSupportedExecutionProfile(profile) {
|
|
29
|
-
if (profile?.version ===
|
|
32
|
+
if ([2, 3].includes(profile?.version)) return (profile.version === 3 ? profile.runtimeVersion === '0.15.0' && Object.hasOwn(profile, 'localBinding')
|
|
33
|
+
: ['0.14.0', '0.15.0'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
|
|
30
34
|
&& profile.role === (Object.keys(ROLE_PHASES).find(role => ROLE_PHASES[role] === profile.phase) || null)
|
|
31
35
|
&& (profile.reasoningEffort === null || ['low', 'medium', 'high'].includes(profile.reasoningEffort))
|
|
32
36
|
&& /^[a-f0-9]{64}$/.test(profile.selectionHash || '');
|
|
33
|
-
return profile?.version === 1 && !Object.hasOwn(profile, 'role') && !Object.hasOwn(profile, 'selectionHash')
|
|
37
|
+
return profile?.version === 1 && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local' && !Object.hasOwn(profile, 'role') && !Object.hasOwn(profile, 'selectionHash')
|
|
34
38
|
&& SUPPORTED_EXECUTION_RUNTIMES_V1.has(profile.runtimeVersion);
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -54,6 +58,7 @@ export function effectiveExecutionConfig(configuration, requestedModel, modelEnv
|
|
|
54
58
|
if (configuration.roleDefinition) {
|
|
55
59
|
const config = withRequestedModel(configuration, requestedModel);
|
|
56
60
|
config.resolvedRoleProfiles = resolveRoleProfiles(config, config.roleDefinition, modelEnvironmentPath);
|
|
61
|
+
delete config.localBindings; // Freeze only the bindings selected by admitted roles, never the catalog.
|
|
57
62
|
return config;
|
|
58
63
|
}
|
|
59
64
|
const inferenceProvider = effectiveInferenceProvider(configuration, modelEnvironmentPath);
|
|
@@ -77,7 +82,9 @@ export function executionProfile(commonConfig, phase) {
|
|
|
77
82
|
policyHash: digest(JSON.stringify(commonConfig)), hostName: hostname(),
|
|
78
83
|
};
|
|
79
84
|
if (commonConfig.resolvedRoleProfiles) {
|
|
80
|
-
|
|
85
|
+
const local = Object.values(commonConfig.resolvedRoleProfiles).some(p => p.localBinding);
|
|
86
|
+
profile.version = local ? 3 : 2;
|
|
87
|
+
if (local) profile.localBinding = deterministic ? null : publicLocalBinding(config.localBinding);
|
|
81
88
|
profile.role = Object.keys(ROLE_PHASES).find(role => ROLE_PHASES[role] === phase) || null;
|
|
82
89
|
profile.reasoningEffort = deterministic ? null : config.reasoningEffort || null;
|
|
83
90
|
profile.inferenceProvider = deterministic ? null : config.inferenceProvider || null;
|
|
@@ -123,6 +130,10 @@ export function assertFrozenExecution(config, execution, phase) {
|
|
|
123
130
|
const expected = executionProfile(config, phase);
|
|
124
131
|
// Host identity is presentation; all execution and policy selections must match.
|
|
125
132
|
delete expected.hostName;
|
|
133
|
+
// Audited legacy writers retain their real version. Never let v1/v2 attest
|
|
134
|
+
// a local binding, including a deterministic phase of a hybrid attempt.
|
|
135
|
+
if (expected.version < 3 && isSupportedExecutionProfile(execution) && execution.version === expected.version)
|
|
136
|
+
expected.runtimeVersion = execution.runtimeVersion;
|
|
126
137
|
const actual = { ...execution }; delete actual.hostName; delete actual.workerName;
|
|
127
138
|
if (!isDeepStrictEqual(expected, actual)) throw new Error('Admitted execution profile does not match this attempt');
|
|
128
139
|
}
|
package/factory/executor.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { localRegistry, LOCAL_AGENT_DIR } from './local-inference.mjs';
|
|
1
2
|
import { assertFrozenExecution, phaseExecutionConfig } from './execution-profile.mjs';
|
|
2
3
|
import { withReconstructedCandidate } from './candidate-patch.mjs';
|
|
3
4
|
import { harnessOf } from './lib.mjs';
|
|
@@ -32,7 +33,7 @@ let sourceAdmission, continuation;
|
|
|
32
33
|
try { sourceAdmission = JSON.parse(process.env.SDF_SOURCE_ADMISSION || 'null'); continuation = JSON.parse(process.env.SDF_CONTINUATION || 'null'); }
|
|
33
34
|
catch { throw new Error('Protected source admission metadata is malformed'); }
|
|
34
35
|
const policyHash = digest(JSON.stringify(commonConfig));
|
|
35
|
-
if (commonConfig.roleDefinition || execution.version
|
|
36
|
+
if (commonConfig.roleDefinition || [2, 3].includes(execution.version)) assertFrozenExecution(commonConfig, execution, phase);
|
|
36
37
|
else if (execution.policyHash !== policyHash || execution.phase !== phase) throw new Error('Admitted execution profile does not match this attempt');
|
|
37
38
|
const output = process.env.SDF_OUTPUT_DIR, result = process.env.SDF_STEP_RESULT_PATH;
|
|
38
39
|
if (!output || !result) throw new Error('Missing workflow result paths');
|
|
@@ -78,6 +79,7 @@ async function container(mode, input, command, options = {}) {
|
|
|
78
79
|
const reportDir = join(folder, attempt, mode);
|
|
79
80
|
mkdirSync(reportDir, { recursive: true, mode: 0o700 });
|
|
80
81
|
const modelEnvironmentPath = join(folder, attempt, `.model-${mode}.env`);
|
|
82
|
+
const localDirectory = mode !== 'verify' && config.localBinding ? join(folder, attempt, `.local-${mode}`) : null;
|
|
81
83
|
// Native builds need disk-backed scratch space, not the small temporary RAM disk.
|
|
82
84
|
// Only this attempt can write here; the candidate and its Git metadata stay read-only.
|
|
83
85
|
const scratch = mode === 'verify' ? join(folder, attempt, 'check-workspace') : null;
|
|
@@ -104,6 +106,14 @@ async function container(mode, input, command, options = {}) {
|
|
|
104
106
|
const log = new BoundedLog(), usageParser = execution.executor === 'codex' ? new CodexUsageParser() : null;
|
|
105
107
|
let exitSignal, selectedModelEnvironment = false, inferenceSecrets = [], code, cleanupError, reportError;
|
|
106
108
|
try {
|
|
109
|
+
if (localDirectory) {
|
|
110
|
+
mkdirSync(localDirectory, { mode: 0o700 });
|
|
111
|
+
const { reference, ...binding } = config.localBinding;
|
|
112
|
+
writeFileSync(join(localDirectory, 'launch.mjs'), readFileSync(join(ROOT, 'factory/pi-local-launch.mjs')), { mode: 0o600, flag: 'wx' });
|
|
113
|
+
writeFileSync(join(localDirectory, 'models.json'), JSON.stringify(localRegistry(binding)), { mode: 0o600, flag: 'wx' });
|
|
114
|
+
args.push('--mount', `type=bind,source=${localDirectory},target=${LOCAL_AGENT_DIR},readonly`,
|
|
115
|
+
'--env', `PI_CODING_AGENT_DIR=${LOCAL_AGENT_DIR}`);
|
|
116
|
+
}
|
|
107
117
|
selectedModelEnvironment = writeSelectedModelEnvironment(join(state, 'model.env'), modelEnvironmentPath, {
|
|
108
118
|
phase: mode, executor: execution.executor, inferenceProvider: config.inferenceProvider,
|
|
109
119
|
});
|
|
@@ -129,6 +139,7 @@ async function container(mode, input, command, options = {}) {
|
|
|
129
139
|
// selected inference file and mounted scratch until a host-side listing
|
|
130
140
|
// proves the container is absent. A client exit alone is not that proof.
|
|
131
141
|
removeContainerAndConfirmAbsence(name, mode === 'verify');
|
|
142
|
+
if (localDirectory) rmSync(localDirectory, { recursive: true, force: true });
|
|
132
143
|
try { redactRetainedPhaseOutputs(join(folder, attempt), mode, inferenceSecrets); }
|
|
133
144
|
catch (error) { reportError = error; }
|
|
134
145
|
try { if (scratch && mode === 'verify' && !keepScratch) removeScratch(scratch); } catch (error) { cleanupError = error; }
|
|
@@ -402,6 +413,8 @@ try {
|
|
|
402
413
|
}
|
|
403
414
|
// Failed filtering is recoverable state: keep its exact secret source and
|
|
404
415
|
// active fence until a later stopped-process recovery completes it.
|
|
416
|
+
if (['build', 'review', 'defence'].includes(phase))
|
|
417
|
+
rmSync(join(folder, attempt, `.local-${phase}`), { recursive: true, force: true });
|
|
405
418
|
if (outputRetentionComplete && ['build', 'review', 'defence'].includes(phase))
|
|
406
419
|
rmSync(join(folder, attempt, `.model-${phase}.env`), { force: true });
|
|
407
420
|
if (outputRetentionComplete) rmSync(lock);
|
package/factory/lib.mjs
CHANGED
|
@@ -80,9 +80,12 @@ export function configAt(state) {
|
|
|
80
80
|
if (typeof config.check !== 'string' || !config.scope || !['project','service','environment','owner'].every(k=>typeof config.scope[k]==='string'&&config.scope[k].trim())) throw new Error('Missing check or installation scope');
|
|
81
81
|
readinessMapping(config.issueReadinessLabels);
|
|
82
82
|
validateWebVerification(config.webVerification);
|
|
83
|
-
if (config.roleDefinition !== undefined || config.resolvedRoleProfiles !== undefined) throw new Error('Role profiles belong in the controller-managed definition; use definition apply');
|
|
84
|
-
const definition = installedRoleRecord(state)
|
|
85
|
-
if (hasRoleOverrides(definition))
|
|
83
|
+
if (config.roleDefinition !== undefined || config.resolvedRoleProfiles !== undefined || config.localBindings !== undefined) throw new Error('Role profiles belong in the controller-managed definition; use definition apply');
|
|
84
|
+
const { definition, localBindings } = installedRoleRecord(state);
|
|
85
|
+
if (hasRoleOverrides(definition)) {
|
|
86
|
+
config.roleDefinition = definition;
|
|
87
|
+
if (Object.keys(localBindings || {}).length) config.localBindings = localBindings;
|
|
88
|
+
}
|
|
86
89
|
return config;
|
|
87
90
|
}
|
|
88
91
|
export async function api(state, path, body, method, { timeoutMs = API_TIMEOUT_MS } = {}) {
|