Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/technical/PHASES_1_2_3_INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Integration Contract — Phases 1, 2 and 3

Related roadmap: #62

## Objective

Link the internal model supply chain, infrastructure egress controls and governed long-term memory into one fail-closed execution path.

## Execution algorithm

1. Resolve a requested model alias and version from the signed internal registry.
2. Verify Ed25519 signature, SHA-256, SHA-512, license, provenance, runtime compatibility and promotion state.
3. Require the model to be `approved` or `recovery-ready`.
4. Bind the verified model digest to an approved AI runtime image and SBOM digest.
5. Start the runtime only inside the Phase 2 restricted network profile.
6. Verify no public port, public DNS or public IP egress is available.
7. Allow only approved internal dependencies: TrustGraph, Ollama/NemoClaw, PostgreSQL, internal DNS and explicitly approved services.
8. Build the Phase 3 memory namespace from user, role and domain.
9. Authorize memory access before retrieval or write.
10. Require provenance, confidence, version, TTL and human approval where policy requires it.
11. Reject unverified AI output as long-term truth.
12. Emit an immutable execution evidence record linking model digest, runtime image digest, network policy digest, memory namespace, authorization decision and operator/request identity.

## Fail-closed conditions

Execution must stop when any of the following occurs:

- unsigned or tampered model manifest;
- checksum mismatch or missing license/provenance;
- model not approved;
- missing or invalid SBOM binding;
- runtime outside the restricted network profile;
- public port or public egress detected;
- unauthorized cross-namespace memory access;
- memory write without provenance or required approval;
- autonomous write to operational data.

## Rollback chain

Rollback must:

1. select the last `recovery-ready` model;
2. restore its verified runtime image and SBOM binding;
3. preserve the deny-by-default network policy;
4. avoid reopening public AI access;
5. keep existing governed-memory audit evidence immutable;
6. mark superseded memory records rather than silently overwriting them;
7. emit one linked rollback evidence record.

## Delivery controls

The integration proof must run in CI and pass CodeQL, Security and Acceptance Gate, Docker Acceptance Gate and HURC1 IRONCLAD. The integration does not authorize public AI endpoints, runtime model downloads or autonomous writes to operational data.
109 changes: 109 additions & 0 deletions scripts/phase123-orchestrator.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env node
import { createHash } from 'node:crypto';

const APPROVED_MODEL_STATES = new Set(['approved', 'recovery-ready']);
const MEMORY_TYPES = new Set(['episodic', 'semantic', 'decision', 'task', 'preference']);
const APPROVED_SERVICES = new Set(['trustgraph', 'ollama', 'nemoclaw', 'postgresql', 'internal-dns']);

function requireValue(condition, message) {
if (!condition) throw new Error(message);
}

function stableStringify(value) {
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
if (value && typeof value === 'object') {
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
}
return JSON.stringify(value);
}

function digest(value) {
return createHash('sha256').update(stableStringify(value)).digest('hex');
}

function validateModel(model) {
requireValue(model?.signatureVerified === true, 'Model signature is not verified');
requireValue(/^[a-f0-9]{64}$/.test(model.sha256 || ''), 'Model SHA-256 is invalid');
requireValue(/^[a-f0-9]{128}$/.test(model.sha512 || ''), 'Model SHA-512 is invalid');
requireValue(typeof model.license === 'string' && model.license.length > 0, 'Model license is missing');
requireValue(typeof model.provenance === 'string' && model.provenance.length > 0, 'Model provenance is missing');
requireValue(APPROVED_MODEL_STATES.has(model.state), `Model state is not loadable: ${model.state}`);
requireValue(/^[a-f0-9]{64}$/.test(model.imageDigest || ''), 'Runtime image digest is invalid');
requireValue(/^[a-f0-9]{64}$/.test(model.sbomDigest || ''), 'SBOM digest is invalid');
}

function validateNetwork(network) {
requireValue(network?.restrictedProfile === true, 'Restricted AI network profile is not enabled');
requireValue(network.publicPortPublished === false, 'Public AI port is published');
requireValue(network.publicDnsReachable === false, 'Public DNS is reachable');
requireValue(network.publicIpReachable === false, 'Public IP is reachable');
requireValue(Array.isArray(network.allowedServices), 'Network allowlist is missing');
for (const service of network.allowedServices) {
requireValue(APPROVED_SERVICES.has(service), `Unapproved internal service: ${service}`);
}
}

function validateMemory(memory, request) {
requireValue(MEMORY_TYPES.has(memory.type), `Invalid memory type: ${memory.type}`);
requireValue(memory.namespace?.userId === request.userId, 'Cross-user memory access denied');
requireValue(memory.namespace?.roleId === request.roleId, 'Cross-role memory access denied');
requireValue(memory.namespace?.domain === request.domain, 'Cross-domain memory access denied');
requireValue(typeof memory.provenance === 'string' && memory.provenance.length > 0, 'Memory provenance is required');
requireValue(Number.isFinite(memory.confidence) && memory.confidence >= 0 && memory.confidence <= 1, 'Memory confidence is invalid');
requireValue(Number.isInteger(memory.version) && memory.version > 0, 'Memory version is invalid');
requireValue(Number.isInteger(memory.ttlSeconds) && memory.ttlSeconds > 0, 'Memory TTL is invalid');
requireValue(memory.aiOutputUnverified !== true, 'Unverified AI output cannot become long-term truth');
if (memory.requiresHumanApproval) {
requireValue(memory.humanApproved === true, 'Human approval is required');
}
requireValue(memory.operationalWrite === false, 'Autonomous operational data write is prohibited');
if (memory.type === 'preference') {
requireValue(memory.containsSecret === false, 'Preference memory must not contain secrets');
}
}

export function authorizeIntegratedExecution(input) {
requireValue(input?.request?.requestId, 'Request ID is required');
validateModel(input.model);
validateNetwork(input.network);
validateMemory(input.memory, input.request);

const evidence = {
schemaVersion: '1.0.0',
decision: 'allow',
requestId: input.request.requestId,
actor: {
userId: input.request.userId,
roleId: input.request.roleId,
domain: input.request.domain
},
model: {
alias: input.model.alias,
version: input.model.version,
sha256: input.model.sha256,
sha512: input.model.sha512,
imageDigest: input.model.imageDigest,
sbomDigest: input.model.sbomDigest,
state: input.model.state
},
networkPolicyDigest: digest(input.network),
memoryNamespace: input.memory.namespace,
memoryPolicyDigest: digest(input.memory),
generatedAt: new Date().toISOString()
};
evidence.evidenceDigest = digest(evidence);
return evidence;
}

if (import.meta.url === `file://${process.argv[1]}`) {
process.stdin.setEncoding('utf8');
let raw = '';
for await (const chunk of process.stdin) raw += chunk;
try {
const evidence = authorizeIntegratedExecution(JSON.parse(raw));
process.stdout.write(`${JSON.stringify(evidence, null, 2)}\n`);
} catch (error) {
console.error(error.message);
process.exit(1);
}
}
85 changes: 85 additions & 0 deletions scripts/test-phase123-integration.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env node
import { authorizeIntegratedExecution } from './phase123-orchestrator.mjs';

const sha256 = 'a'.repeat(64);
const sha512 = 'b'.repeat(128);

function fixture() {
return {
request: {
requestId: 'ci-phase123-proof',
userId: 'user-001',
roleId: 'engineering-safety',
domain: 'metro-line-1'
},
model: {
alias: 'internal-rag-model',
version: '1.0.0',
signatureVerified: true,
sha256,
sha512,
license: 'Apache-2.0',
provenance: 'internal-registry/model/internal-rag-model/1.0.0',
state: 'recovery-ready',
imageDigest: 'c'.repeat(64),
sbomDigest: 'd'.repeat(64)
},
network: {
restrictedProfile: true,
publicPortPublished: false,
publicDnsReachable: false,
publicIpReachable: false,
allowedServices: ['trustgraph', 'ollama', 'postgresql', 'internal-dns']
},
memory: {
type: 'decision',
namespace: {
userId: 'user-001',
roleId: 'engineering-safety',
domain: 'metro-line-1'
},
provenance: 'approved-document:DNF-2026-001',
confidence: 0.98,
version: 1,
ttlSeconds: 86400,
requiresHumanApproval: true,
humanApproved: true,
aiOutputUnverified: false,
operationalWrite: false,
containsSecret: false
}
};
}

function expectReject(name, mutate, message) {
const input = fixture();
mutate(input);
try {
authorizeIntegratedExecution(input);
throw new Error(`${name}: unexpectedly allowed`);
} catch (error) {
if (!String(error.message).includes(message)) {
throw new Error(`${name}: expected '${message}', got '${error.message}'`);
}
}
}

const evidence = authorizeIntegratedExecution(fixture());
if (evidence.decision !== 'allow') throw new Error('Valid integrated execution was not allowed');
if (!/^[a-f0-9]{64}$/.test(evidence.evidenceDigest)) throw new Error('Evidence digest is invalid');

expectReject('unsigned model', (x) => { x.model.signatureVerified = false; }, 'Model signature is not verified');
expectReject('unapproved model', (x) => { x.model.state = 'standardized'; }, 'Model state is not loadable');
expectReject('missing provenance', (x) => { x.model.provenance = ''; }, 'Model provenance is missing');
expectReject('public port', (x) => { x.network.publicPortPublished = true; }, 'Public AI port is published');
expectReject('public DNS', (x) => { x.network.publicDnsReachable = true; }, 'Public DNS is reachable');
expectReject('public IP', (x) => { x.network.publicIpReachable = true; }, 'Public IP is reachable');
expectReject('unapproved service', (x) => { x.network.allowedServices.push('public-internet'); }, 'Unapproved internal service');
expectReject('cross-user retrieval', (x) => { x.memory.namespace.userId = 'user-999'; }, 'Cross-user memory access denied');
expectReject('missing memory provenance', (x) => { x.memory.provenance = ''; }, 'Memory provenance is required');
expectReject('unverified AI truth', (x) => { x.memory.aiOutputUnverified = true; }, 'Unverified AI output cannot become long-term truth');
expectReject('missing approval', (x) => { x.memory.humanApproved = false; }, 'Human approval is required');
expectReject('operational write', (x) => { x.memory.operationalWrite = true; }, 'Autonomous operational data write is prohibited');
expectReject('secret preference', (x) => { x.memory.type = 'preference'; x.memory.containsSecret = true; }, 'Preference memory must not contain secrets');

console.log('Phase 1-2-3 integration proof passed with fail-closed rejection cases');
Loading