software-defence-factory 0.3.1 → 0.3.2
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/docs/quickstart.md +11 -0
- package/factory/executor.mjs +9 -3
- package/factory/lib.mjs +2 -0
- package/package.json +1 -1
- package/scripts/probe-platform.mjs +14 -0
package/docs/quickstart.md
CHANGED
|
@@ -54,3 +54,14 @@ ssh -N -L 127.0.0.1:7331:127.0.0.1:7331 your-host
|
|
|
54
54
|
Open http://127.0.0.1:7331 on the client. Use the same local/remote port because the HTTP service validates its Host header. The SSH connection must remain open. Access also works across different networks when your configured private network connects the hosts.
|
|
55
55
|
|
|
56
56
|
Use `status`, `cancel JOB_ID`, `retry JOB_ID` and `stop`, always with the selected `--state`. `service` prints a systemd user-service definition; review and install it through the host's normal service management. It is not enabled by printing it. See [recovery](recovery.md).
|
|
57
|
+
|
|
58
|
+
## Native application builds
|
|
59
|
+
|
|
60
|
+
Pin an application-specific image with the required toolchains. In `factory.json`,
|
|
61
|
+
`cpus` (1–32, default 2), `pidsLimit` (64–16384, default 256), `memoryMiB` and
|
|
62
|
+
`timeoutSeconds` bound the job's resources. Verification uses a separate
|
|
63
|
+
disk-backed checkout, keeping the candidate read-only. Its private scratch
|
|
64
|
+
directory is removed after container termination is confirmed. Interrupted
|
|
65
|
+
executors may retain scratch under their attempt for recovery; stop/reconcile
|
|
66
|
+
the job before removing it. Ensure the state filesystem has sufficient space.
|
|
67
|
+
No Docker socket, operator credentials or unrelated project caches are mounted.
|
package/factory/executor.mjs
CHANGED
|
@@ -48,17 +48,22 @@ async function container(mode, input, command, writable = false, credentials = f
|
|
|
48
48
|
const name = `sdf-${instanceLabel(state)}-${attempt}-${mode}`;
|
|
49
49
|
const reportDir = join(folder, attempt, mode);
|
|
50
50
|
mkdirSync(reportDir, { recursive: true, mode: 0o700 });
|
|
51
|
+
// Native builds need disk-backed scratch space, not the small temporary RAM disk.
|
|
52
|
+
// Only this attempt can write here; the candidate and its Git metadata stay read-only.
|
|
53
|
+
const scratch = mode === 'verify' ? join(folder, attempt, 'check-workspace') : null;
|
|
54
|
+
if (scratch) mkdirSync(scratch, { mode: 0o700 });
|
|
51
55
|
const uid = process.getuid(), gid = process.getgid();
|
|
52
56
|
const args = ['run','--name',name,'--init','--read-only','--cap-drop=ALL','--security-opt=no-new-privileges',
|
|
53
|
-
'--pids-limit
|
|
57
|
+
'--pids-limit',String(config.pidsLimit ?? 256),'--memory',`${config.memoryMiB}m`,'--cpus',String(config.cpus ?? 2), '--user',`${uid}:${gid}`,
|
|
54
58
|
'--network',config.network,'--label',`sdf.factory=${instanceLabel(state)}`,'--label',`sdf.job=${job}`,
|
|
55
59
|
'--label',`sdf.run=${attempt}`,'--label',`sdf.deadline=${Date.now() + config.timeoutSeconds * 1000}`,
|
|
56
|
-
'--tmpfs','/tmp:rw,nosuid,size=1024m','--env'
|
|
60
|
+
'--tmpfs','/tmp:rw,nosuid,size=1024m','--env',`HOME=${scratch ? '/scratch/home' : '/tmp/home'}`,'--env',`FACTORY_PHASE=${mode}`,
|
|
57
61
|
'--mount',`type=bind,source=${workspace},target=/workspace${writable ? '' : ',readonly'}`,
|
|
58
62
|
'--mount',`type=bind,source=${join(workspace,'.git')},target=/workspace/.git,readonly`,
|
|
59
63
|
'--mount',`type=bind,source=${reportDir},target=/output`,
|
|
60
64
|
'--mount',`type=bind,source=${join(ROOT,'kit')},target=/factory-policy,readonly`,
|
|
61
65
|
'--mount',`type=bind,source=${join(ROOT,'.agents/skills')},target=/factory-skills,readonly`];
|
|
66
|
+
if (scratch) args.push('--mount',`type=bind,source=${scratch},target=/scratch`);
|
|
62
67
|
if (credentials) args.push('--env-file', join(state,'model.env'));
|
|
63
68
|
args.push('-i',config.image,'timeout','--signal=KILL',`${config.timeoutSeconds}s`,'sh','-c','mkdir -p "$HOME" && exec "$@"','factory',...command);
|
|
64
69
|
console.log(JSON.stringify({ phase: mode, event: 'started', synthetic: config.agent === 'mock' }));
|
|
@@ -79,6 +84,7 @@ async function container(mode, input, command, writable = false, credentials = f
|
|
|
79
84
|
const probe = run('docker',['ps','-aq','--filter',`name=^/${name}$`]);
|
|
80
85
|
if (probe) throw error;
|
|
81
86
|
}
|
|
87
|
+
if (scratch) rmSync(scratch, { recursive: true, force: true });
|
|
82
88
|
if (code !== 0) throw new Error(`${mode} exited ${code}; private log: ${logPath}`);
|
|
83
89
|
return reportDir;
|
|
84
90
|
}
|
|
@@ -109,7 +115,7 @@ try {
|
|
|
109
115
|
} else if (phase === 'verify') {
|
|
110
116
|
const meta = candidate();
|
|
111
117
|
if (!config.check?.trim()) throw new Error('Configure an actual app check before software delivery');
|
|
112
|
-
const check = ['sh','-c','mkdir -p /
|
|
118
|
+
const check = ['sh','-c','mkdir -p /scratch/check && cp -R /workspace/. /scratch/check/ && cd /scratch/check && exec sh -c "$1"','check',config.check];
|
|
113
119
|
await container('verify', '', check);
|
|
114
120
|
candidate();
|
|
115
121
|
const proof = { head: meta.head, policyHash, command: config.check, passed: true, finishedAt: new Date().toISOString(), synthetic: meta.synthetic };
|
package/factory/lib.mjs
CHANGED
|
@@ -33,6 +33,8 @@ export function configAt(state) {
|
|
|
33
33
|
if (!Number.isInteger(config.port) || config.port < 1024 || config.port > 65535) throw new Error('Invalid port');
|
|
34
34
|
if (!Number.isInteger(config.timeoutSeconds) || config.timeoutSeconds < 1 || config.timeoutSeconds > 7200) throw new Error('timeoutSeconds must be 1–7200');
|
|
35
35
|
if (!Number.isInteger(config.memoryMiB) || config.memoryMiB < 256) throw new Error('Invalid memory limit');
|
|
36
|
+
if (config.cpus !== undefined && (!Number.isInteger(config.cpus) || config.cpus < 1 || config.cpus > 32)) throw new Error('cpus must be 1–32');
|
|
37
|
+
if (config.pidsLimit !== undefined && (!Number.isInteger(config.pidsLimit) || config.pidsLimit < 64 || config.pidsLimit > 16384)) throw new Error('pidsLimit must be 64–16384');
|
|
36
38
|
if (!['none','bridge'].includes(config.network)) throw new Error('network must be none or bridge; host networking is not supported');
|
|
37
39
|
if (typeof config.image !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_./:@-]*$/.test(config.image)) throw new Error('Invalid container image');
|
|
38
40
|
if (typeof config.repo !== 'string' || !isAbsolute(config.repo) || [config.repo,state,ROOT].some(p=>/[,\n\r]/.test(p))) throw new Error('Expected absolute paths without commas or line breaks');
|
package/package.json
CHANGED
|
@@ -32,6 +32,19 @@ try {
|
|
|
32
32
|
await cli('approve',pass.id);await waitState(pass.id,'succeeded');
|
|
33
33
|
assert.equal(json(join(folder,'accepted.json')).head,candidate.head);record('software: exact revision, isolated checkout, checks, review, approval, handoff');
|
|
34
34
|
|
|
35
|
+
setConfig({cpus:4,pidsLimit:1024,check:"test \"$(cat value.txt)\" = fixed && dd if=/dev/zero of=large-build-fixture bs=1M count=1100 status=none && sleep 2"});
|
|
36
|
+
const native=await work('Synthetic disk-backed build verification');
|
|
37
|
+
const checking=await until(()=>containers(state).find(c=>c.Config.Labels['sdf.job']===native.id&&c.State.Running&&c.Config.Env.includes('FACTORY_PHASE=verify')));
|
|
38
|
+
assert.equal(checking.HostConfig.NanoCpus,4000000000);assert.equal(checking.HostConfig.PidsLimit,1024);
|
|
39
|
+
assert(checking.Mounts.some(m=>m.Destination==='/scratch'&&m.RW));
|
|
40
|
+
assert(checking.Mounts.some(m=>m.Destination==='/workspace'&&!m.RW));
|
|
41
|
+
await waitState(native.id,'awaiting_approval');
|
|
42
|
+
const verification=(await snapshot(native.id)).runs.find(r=>r.command==='verify');
|
|
43
|
+
assert(!existsSync(join(state,'jobs',native.id,verification.id,'check-workspace')));
|
|
44
|
+
assert(!existsSync(join(state,'jobs',native.id,'checkout/large-build-fixture')));
|
|
45
|
+
await cli('approve',native.id);await waitState(native.id,'succeeded');setConfig({});
|
|
46
|
+
record('native build scratch exceeds 1 GiB without changing the candidate; limits and cleanup verified');
|
|
47
|
+
|
|
35
48
|
const changed=await work('Synthetic changed revision guard');await waitState(changed.id,'awaiting_approval');
|
|
36
49
|
writeFileSync(join(state,'jobs',changed.id,'checkout/value.txt'),'changed after review\n');
|
|
37
50
|
await cli('approve',changed.id);await waitState(changed.id,'failed');
|
|
@@ -44,6 +57,7 @@ try {
|
|
|
44
57
|
setConfig({check:'exit 17'});
|
|
45
58
|
const fail=await work('Synthetic failing check');await waitState(fail.id,'failed');
|
|
46
59
|
assert.equal((await snapshot(fail.id)).runs.at(-1).command,'verify');
|
|
60
|
+
assert(!existsSync(join(state,'jobs',fail.id,(await snapshot(fail.id)).runs.at(-1).id,'check-workspace')));
|
|
47
61
|
setConfig({});await cli('retry',fail.id);await waitState(fail.id,'awaiting_approval');
|
|
48
62
|
await cli('approve',fail.id);await waitState(fail.id,'succeeded');record('failed app check blocks delivery; controlled retry rechecks same candidate');
|
|
49
63
|
|