software-defence-factory 0.15.2 → 0.15.3

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/setup.md CHANGED
@@ -432,6 +432,37 @@ local inference does not mean zero electricity, hardware or operator cost. Only
432
432
  then qualify a suitable real Factory issue; no customer jobs are part of this slice.
433
433
 
434
434
 
435
+ ## Local Pi project resources (0.15.3, #103)
436
+
437
+ The local launcher gives each invocation a private `0700` directory under the
438
+ job's existing `/tmp` tmpfs for Pi runtime stores/locks. `PI_CODING_AGENT_DIR`
439
+ points there; it is never the read-only `/factory-local` binding mount. The
440
+ selected `models.json` is linked to that mount, and the launcher/context adapter,
441
+ policy and six Factory skills remain read-only. No host home, saved trust,
442
+ authentication or settings are copied. Normal exit removes this state; container
443
+ teardown removes interrupted state. Build and Review never share it. The root
444
+ filesystem and Review checkout remain read-only.
445
+
446
+ Pinned Pi 0.87.1 consults project trust even for an empty `.agents/skills`
447
+ directory. Factory explicitly passes `--no-approve` to deny project settings,
448
+ packages and executable resource loading, while retaining `--no-extensions`,
449
+ `--no-skills` and `--no-prompt-templates`. A writable Pi home does not authorize
450
+ project dependency installation. Only the explicit bundled context extension
451
+ and `--skill /factory-skills` catalog are supported; their loading and the
452
+ context-ready handshake are tested against actual Pi. Task stdin and ordinary
453
+ `AGENTS.md` context remain available as untrusted text. Repository Pi settings,
454
+ extensions/packages and automatically discovered repository skills are not a
455
+ supported configuration path. Explicit task-driven file reads are still possible
456
+ within the sandbox and grant no additional authority.
457
+
458
+ Before accepting this repair, the lead must qualify the exact installed 0.15.3
459
+ package/image through CLI/API, representative native local Build and separate
460
+ read-only Review, and one actual local-model run. `npm run qualify:pi` uses actual
461
+ Pi with **synthetic HTTP inference**, not model judgment. See [trust-store
462
+ proof and limits](proof.md#pi-project-trust-startup-0153-103). Retain the failed
463
+ #51 attempt unchanged and unaccepted; retry only through a newly authorized
464
+ attempt after qualification. Do not modify private installations from a job.
465
+
435
466
  ## Local context budget and worker image (0.15.2, #100)
436
467
 
437
468
  The standard worker recipe pins **`@earendil-works/pi-coding-agent@0.87.1`** from
@@ -24,16 +24,19 @@ import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
24
24
  // 0.15.1 changes release downloads only; v1/v2/v3 writers and policy are unchanged.
25
25
  // 0.15.2 updates the opt-in local launcher and standard image; schemas, frozen
26
26
  // command/config/image hashes, protected checks and acceptance guards are unchanged.
27
+ // 0.15.3 isolates local Pi mutable state and denies project resource trust.
28
+ // Evidence schemas, candidate reconstruction and frozen profile guards are unchanged;
29
+ // compatibility does not qualify an old local startup or accept a failed attempt.
27
30
  // Older writers still require exact patch/tree reconstruction before publication.
28
31
  // Deliberately independent of VERSION: a release bump is not
29
32
  // evidence compatibility. Re-audit this list for every trust-relevant writer,
30
33
  // isolation or validation change; remove versions whose guarantees no longer
31
34
  // satisfy current policy. See docs/npm.md. This predicate alone grants no trust.
32
- 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', '0.15.1', '0.15.2']);
35
+ 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', '0.15.1', '0.15.2', '0.15.3']);
33
36
 
34
37
  export function isSupportedExecutionProfile(profile) {
35
- if ([2, 3].includes(profile?.version)) return (profile.version === 3 ? ['0.15.0', '0.15.1', '0.15.2'].includes(profile.runtimeVersion) && Object.hasOwn(profile, 'localBinding')
36
- : ['0.14.0', '0.15.0', '0.15.1', '0.15.2'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
38
+ if ([2, 3].includes(profile?.version)) return (profile.version === 3 ? ['0.15.0', '0.15.1', '0.15.2', '0.15.3'].includes(profile.runtimeVersion) && Object.hasOwn(profile, 'localBinding')
39
+ : ['0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
37
40
  && profile.role === (Object.keys(ROLE_PHASES).find(role => ROLE_PHASES[role] === profile.phase) || null)
38
41
  && (profile.reasoningEffort === null || ['low', 'medium', 'high'].includes(profile.reasoningEffort))
39
42
  && /^[a-f0-9]{64}$/.test(profile.selectionHash || '');
@@ -113,8 +113,7 @@ async function container(mode, input, command, options = {}) {
113
113
  for (const file of ['pi-context-extension.mjs', 'pi-context-budget.mjs'])
114
114
  writeFileSync(join(localDirectory, file), readFileSync(join(ROOT, 'factory', file)), { mode: 0o600, flag: 'wx' });
115
115
  writeFileSync(join(localDirectory, 'models.json'), JSON.stringify(localRegistry(binding)), { mode: 0o600, flag: 'wx' });
116
- args.push('--mount', `type=bind,source=${localDirectory},target=${LOCAL_AGENT_DIR},readonly`,
117
- '--env', `PI_CODING_AGENT_DIR=${LOCAL_AGENT_DIR}`);
116
+ args.push('--mount', `type=bind,source=${localDirectory},target=${LOCAL_AGENT_DIR},readonly`);
118
117
  }
119
118
  selectedModelEnvironment = writeSelectedModelEnvironment(join(state, 'model.env'), modelEnvironmentPath, {
120
119
  phase: mode, executor: execution.executor, inferenceProvider: config.inferenceProvider,
@@ -1,11 +1,10 @@
1
1
  // Explicitly mounted Factory adapter, never discovered from the host/repository.
2
2
  import { readFileSync, writeSync } from 'node:fs';
3
- import { join } from 'node:path';
4
3
  import { openAICompletionsApi } from '@earendil-works/pi-ai';
5
4
  import { bytes, fitToolText, projectedTokens, restoreOutputBudget, validatePairs } from './pi-context-budget.mjs';
6
5
 
7
6
  export default function factoryLocalContext(pi) {
8
- const registry = JSON.parse(readFileSync(join(process.env.PI_CODING_AGENT_DIR, 'models.json'), 'utf8'));
7
+ const registry = JSON.parse(readFileSync(new URL('./models.json', import.meta.url), 'utf8'));
9
8
  const configured = registry.providers['factory-local'].models[0];
10
9
  const window = configured.contextWindow, output = configured.maxTokens;
11
10
  let task;
@@ -1,6 +1,6 @@
1
1
  // Mounted read-only beside the single selected registry. Keep all JSON evidence;
2
2
  // process exit alone does not establish a successfully completed Pi turn.
3
- import { readFileSync } from 'node:fs';
3
+ import { mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { spawn, spawnSync } from 'node:child_process';
@@ -116,16 +116,24 @@ function launch() {
116
116
  // Never inherit credential inputs, provider overrides, NODE_OPTIONS or external
117
117
  // Pi directories. The runtime image supplies binaries; the repository supplies data.
118
118
  const env = Object.fromEntries(['PATH', 'HOME', 'LANG', 'TMPDIR', 'FACTORY_PHASE'].filter(key => process.env[key] !== undefined).map(key => [key, process.env[key]]));
119
- env.PI_CODING_AGENT_DIR = directory;
120
119
  for (const file of ['pi-context-extension.mjs', 'pi-context-budget.mjs']) readFileSync(join(directory, file));
121
120
  const version = spawnSync('pi', ['--version'], { env, encoding: 'utf8', timeout: 10000 });
122
121
  if (version.status !== 0 || version.stdout.trim() !== '0.87.1')
123
122
  throw new Error('Local context protection requires pinned Pi 0.87.1; qualify and select the worker image while idle. No fallback.');
124
- // The explicit non-secret runtime key bypasses Pi 0.87.1's credential-store
125
- // refresh, which otherwise tries to lock auth.json in this read-only mount.
126
- // --no-extensions still disables discovery. Only this read-only bundled path
127
- // is explicitly enabled; it cannot load repository or host extensions.
128
- const child = spawn('pi', [...args, '--extension', join(directory, 'pi-context-extension.mjs'), '--api-key', 'factory-local-keyless'], { env, stdio: ['inherit', 'pipe', 'inherit'] });
123
+ // Pi's home is mutable runtime state (including trust-store locks), not the
124
+ // operator-owned binding. Each launch gets a private directory in the existing
125
+ // sandbox tmpfs; no host trust/auth/settings are copied. Keep model input linked
126
+ // to its read-only mount, and load the adapter directly from that mount.
127
+ const runtimeDirectory = mkdtempSync('/tmp/factory-pi-');
128
+ const cleanup = () => rmSync(runtimeDirectory, { recursive: true, force: true });
129
+ process.on('exit', cleanup); // Container teardown also clears interrupted runs.
130
+ symlinkSync(join(directory, 'models.json'), join(runtimeDirectory, 'models.json'));
131
+ env.PI_CODING_AGENT_DIR = runtimeDirectory;
132
+ // Denying project trust also prevents project settings/packages and dependency
133
+ // installation, which --no-extensions alone does not prevent. Explicit CLI
134
+ // resources remain available in pinned Pi: only our adapter and mounted skills.
135
+ const child = spawn('pi', [...args, '--no-approve', '--no-extensions', '--no-skills', '--no-prompt-templates',
136
+ '--extension', join(directory, 'pi-context-extension.mjs'), '--api-key', 'factory-local-keyless'], { env, stdio: ['inherit', 'pipe', 'inherit'] });
129
137
  const completion = new PiCompletion();
130
138
  child.stdout.setEncoding('utf8');
131
139
  child.stdout.on('data', chunk => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "software-defence-factory",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "Factory: scoped software delivery and defence investigations with isolated jobs, evidence and review",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,7 +58,7 @@
58
58
  "check": "node scripts/check.mjs && node --test tests/*.test.mjs && npm run test:dashboard",
59
59
  "build:dashboard": "npm --prefix dashboard ci --ignore-scripts && npm --prefix dashboard run build",
60
60
  "test:dashboard": "npm --prefix dashboard test",
61
- "qualify:pi": "FACTORY_REQUIRE_PI=1 node --test tests/pi-local-adapter.test.mjs tests/pi-context.test.mjs tests/pi-context-batch.test.mjs"
61
+ "qualify:pi": "FACTORY_REQUIRE_PI=1 node --test tests/pi-local-adapter.test.mjs tests/pi-trust.test.mjs tests/pi-context.test.mjs tests/pi-context-batch.test.mjs"
62
62
  },
63
63
  "dependencies": {
64
64
  "yaml": "2.9.1"