From 31159627ef42c748ccafd8dbda5c68ec99b69978 Mon Sep 17 00:00:00 2001 From: sneakocom <192013763+sneakocom@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:25:43 +0000 Subject: [PATCH] feat: add semantic model evidence projection --- CHANGELOG.md | 10 ++++++++++ README.md | 25 ++++++++++++++++++++++++- SPEC.md | 20 ++++++++++++++++++-- bin/context-firewall.js | 15 +++++++++++++-- package.json | 2 +- src/README.md | 3 +++ src/reducer.js | 27 ++++++++++++++++++++++++++- tests/reducer.test.js | 29 +++++++++++++++++++++++++++++ 8 files changed, 124 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a90eef3..b6380d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.4.0 - 2026-09-06 + +- Added the compatible `opsle.context-firewall.model-evidence/v1` semantic-only + projection and optional `--model-evidence` sidecar. +- Kept the existing evidence packet, receipt, hashes, reduction, escalation, + and canonical stdout behavior unchanged apart from the declared reducer + version. +- Clarified that downstream consumers must measure actual provider submission; + producer packet-size measurements do not prove delivery. + ## 0.3.0 - 2026-08-25 - Added a dependency-free sibling `opsle.value-receipt.v1` for reductions with diff --git a/README.md b/README.md index 32c7760..77cd51f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ safely not see?** This repository does not yet answer it. ## Prototype scope -Version 0.3.0 is a dependency-free Node.js reference reducer for a documented +Version 0.4.0 is a dependency-free Node.js reference reducer for a documented flat TAP-compatible test-output subset. It: - reads caller-supplied stdout and stderr bytes plus process metadata; @@ -150,6 +150,29 @@ stderr and emit no success indicator. A negative avoided-byte delta is reported as packet expansion rather than fabricated savings; ratios are not directly summable. +## Semantic-only model evidence + +The canonical packet remains the compatible default stdout representation. A +caller that retains the packet as audit evidence can request an explicit +semantic-only sidecar with `--model-evidence PATH`. Its protocol is +`opsle.context-firewall.model-evidence/v1` and it contains only +`protocol_version`, `operation_id`, and the packet's byte-identical +`decision_evidence` value. It deliberately excludes the packet `receipt`. + +```bash +node ./bin/context-firewall.js reduce \ + --model-evidence model-evidence.json +``` + +`modelEvidenceForPacket()` and `serializeModelEvidence()` expose the same +deterministic projection to library callers. The sidecar is a supported +model-facing representation, not a second reduction: the full packet remains +the audit authority and the projection changes no classification, retention, +hash, receipt, or canonical stdout behavior. Callers must measure and record +what they actually submit; the producer's legacy `initial_model_visible_bytes` +measurement continues to describe canonical packet stdout, not downstream +delivery of this optional projection. + `--mechanism-revision` is caller supplied, affects only the sidecar, and defaults to `null`; the deterministic reducer never inspects ambient Git state. diff --git a/SPEC.md b/SPEC.md index 9b42866..1318dd6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2,7 +2,10 @@ Status: experimental prototype contract. -Version: `opsle.context-firewall.evidence-packet/v1`. +Packet version: `opsle.context-firewall.evidence-packet/v1`. + +Model-evidence projection version: +`opsle.context-firewall.model-evidence/v1`. ## Compatibility boundary @@ -46,11 +49,22 @@ environment value enters canonical output. ## Visible Value contract -The packet itself remains the compact model-visible output. A caller may derive a +The packet remains the compatible default model-visible stdout. A caller may derive a sibling `opsle.value-receipt.v1` with `reduceWithValueReceipt()` or request a canonical CLI sidecar with `--value-receipt`. The receipt is not embedded in the packet and does not increase model-visible stdout. +A caller that retains the full packet as audit evidence may request the +supported semantic-only `opsle.context-firewall.model-evidence/v1` projection +with `--model-evidence` or derive it with `modelEvidenceForPacket()`. The +projection contains the packet operation identity and its exact +`decision_evidence`, but excludes the packet `receipt`. It is deterministic and +does not change packet bytes, hashes, classification, retention, or escalation. +Downstream consumers remain responsible for measuring actual submission. The +existing `initial_model_visible_bytes` measurement continues to describe the +canonical packet stdout for compatibility; it does not claim that a downstream +consumer submitted either representation. + The mechanism identity is `opsle.context-firewall`, the operation is `test-output-reduction`, and the receipt contains `raw_bytes`, `initial_model_visible_bytes`, `bytes_initially_avoided`, @@ -107,6 +121,8 @@ otherwise valid test names or explicit notes have no special meaning. or writing the receipt never changes packet bytes. 10. Operator telemetry is derived from the completed sibling receipt and remains outside canonical stdout. +11. Model-evidence projection preserves packet `operation_id` and + `decision_evidence` exactly while excluding the packet `receipt`. ## Payload policy diff --git a/bin/context-firewall.js b/bin/context-firewall.js index 6d02d8a..f36cfcd 100755 --- a/bin/context-firewall.js +++ b/bin/context-firewall.js @@ -6,19 +6,22 @@ import { InputError, PayloadCeilingError, canonicalJson, + modelEvidenceForPacket, reduceWithValueReceipt, + serializeModelEvidence, serializePacket, } from '../src/reducer.js'; import { formatContextFirewallIndicator } from '../src/value-receipt.js'; function usage() { - return 'usage: context-firewall reduce [--input PATH|-] [--max-bytes N] [--mechanism-revision REV] [--value-receipt PATH]\n context-firewall conformance\n'; + return 'usage: context-firewall reduce [--input PATH|-] [--max-bytes N] [--mechanism-revision REV] [--model-evidence PATH] [--value-receipt PATH]\n context-firewall conformance\n'; } function parseReduceArgs(args) { let input = '-'; let maxOutputBytes = null; let mechanismRevision = null; + let modelEvidencePath = null; let valueReceiptPath = null; for (let index = 0; index < args.length; index += 1) { if (args[index] === '--input' && args[index + 1]) input = args[++index]; @@ -26,11 +29,13 @@ function parseReduceArgs(args) { maxOutputBytes = Number(args[++index]); } else if (args[index] === '--mechanism-revision' && args[index + 1]) { mechanismRevision = args[++index]; + } else if (args[index] === '--model-evidence' && args[index + 1]) { + modelEvidencePath = args[++index]; } else if (args[index] === '--value-receipt' && args[index + 1]) { valueReceiptPath = args[++index]; } else throw new InputError(`unknown or incomplete argument: ${args[index]}`); } - return { input, maxOutputBytes, mechanismRevision, valueReceiptPath }; + return { input, maxOutputBytes, mechanismRevision, modelEvidencePath, valueReceiptPath }; } async function readInput(path) { @@ -69,6 +74,12 @@ async function main() { if (options.valueReceiptPath) { await writeFile(options.valueReceiptPath, `${canonicalJson(valueReceipt)}\n`, 'utf8'); } + if (options.modelEvidencePath) { + await writeFile( + options.modelEvidencePath, + serializeModelEvidence(modelEvidenceForPacket(packet)), + ); + } process.stdout.write(serializePacket(packet)); process.stderr.write(`${formatContextFirewallIndicator(valueReceipt)}\n`); } diff --git a/package.json b/package.json index b3a4855..4730820 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@opsle/context-firewall", - "version": "0.3.0", + "version": "0.4.0", "private": true, "type": "module", "bin": { diff --git a/src/README.md b/src/README.md index d53ea44..a8c3336 100644 --- a/src/README.md +++ b/src/README.md @@ -5,6 +5,9 @@ - `normalizeInvocation(value)` validates and decodes the public input envelope; - `reduceTestRun(value, options)` returns a deterministic evidence packet; - `serializePacket(packet)` returns canonical UTF-8 JSON with a final newline; +- `modelEvidenceForPacket(packet)` returns the supported semantic-only + model-evidence projection; +- `serializeModelEvidence(value)` returns its canonical UTF-8 bytes; - `PayloadCeilingError` represents a ceiling too small for any safe packet. The core performs no I/O, model calls, network calls, persistence, or host diff --git a/src/reducer.js b/src/reducer.js index d541007..51827e6 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -4,8 +4,9 @@ import { buildReductionValueReceipt } from './value-receipt.js'; export const INPUT_PROTOCOL = 'opsle.context-firewall.test-run-input/v1'; export const PACKET_PROTOCOL = 'opsle.context-firewall.evidence-packet/v1'; +export const MODEL_EVIDENCE_PROTOCOL = 'opsle.context-firewall.model-evidence/v1'; export const REDUCER_NAME = '@opsle/context-firewall/test-output'; -export const REDUCER_VERSION = '0.3.0'; +export const REDUCER_VERSION = '0.4.0'; export const POLICY_REVISION = 'tap-subset-policy/v1'; const ANSI_PATTERN = /[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*)?\u0007)|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g; @@ -518,6 +519,30 @@ export function serializePacket(packet) { return Buffer.from(`${canonicalJson(packet)}\n`, 'utf8'); } +export function modelEvidenceForPacket(packet) { + if (!isPlainObject(packet) || packet.protocol_version !== PACKET_PROTOCOL + || typeof packet.operation_id !== 'string' || !packet.operation_id + || !isPlainObject(packet.decision_evidence)) { + throw new InputError('a valid Context Firewall evidence packet is required'); + } + return { + decision_evidence: packet.decision_evidence, + operation_id: packet.operation_id, + protocol_version: MODEL_EVIDENCE_PROTOCOL, + }; +} + +export function serializeModelEvidence(modelEvidence) { + if (!isPlainObject(modelEvidence) + || modelEvidence.protocol_version !== MODEL_EVIDENCE_PROTOCOL + || typeof modelEvidence.operation_id !== 'string' + || !modelEvidence.operation_id + || !isPlainObject(modelEvidence.decision_evidence)) { + throw new InputError('a valid Context Firewall model-evidence projection is required'); + } + return Buffer.from(`${canonicalJson(modelEvidence)}\n`, 'utf8'); +} + export function valueReceiptForPacket(packet, { mechanismRevision = null } = {}) { if (mechanismRevision != null && (typeof mechanismRevision !== 'string' || mechanismRevision.length === 0)) { throw new InputError('mechanismRevision must be a nonempty string or null'); diff --git a/tests/reducer.test.js b/tests/reducer.test.js index 2a7f657..b0ded04 100644 --- a/tests/reducer.test.js +++ b/tests/reducer.test.js @@ -8,13 +8,16 @@ import { fileURLToPath } from 'node:url'; import { INPUT_PROTOCOL, InputError, + MODEL_EVIDENCE_PROTOCOL, PACKET_PROTOCOL, POLICY_REVISION, PayloadCeilingError, REDUCER_VERSION, canonicalJson, + modelEvidenceForPacket, reduceTestRun, reduceWithValueReceipt, + serializeModelEvidence, serializePacket, } from '../src/reducer.js'; import { conformanceReport, corpus, executeFixture } from '../fixtures/corpus.js'; @@ -58,6 +61,24 @@ test('canonical output contains no generated time or random identity', () => { assert.equal(packet.operation_id, 'op-synthetic-001'); }); +test('model evidence is an explicit semantic-only projection', () => { + const packet = reduceTestRun(fixture('failure/stack-trace').input); + const projection = modelEvidenceForPacket(packet); + const output = serializeModelEvidence(projection); + assert.deepEqual(projection, { + decision_evidence: packet.decision_evidence, + operation_id: packet.operation_id, + protocol_version: MODEL_EVIDENCE_PROTOCOL, + }); + assert.equal('receipt' in projection, false); + assert.equal(output.toString('utf8'), `${canonicalJson(projection)}\n`); + assert.ok(output.length < serializePacket(packet).length); + assert.throws( + () => modelEvidenceForPacket({ protocol_version: PACKET_PROTOCOL }), + (error) => error instanceof InputError && error.code === 'INVALID_INPUT', + ); +}); + test('packet identifies exact protocol, reducer, policy, and configuration', () => { const packet = reduceTestRun(fixture('normal/small-all-pass').input); assert.equal(packet.protocol_version, PACKET_PROTOCOL); @@ -424,6 +445,7 @@ test('CLI reads JSON from stdin and emits the canonical packet', () => { test('CLI writes a canonical value receipt only to an explicitly requested sidecar', () => { const input = fixture('normal/large-all-pass').input; const directory = mkdtempSync(join(tmpdir(), 'context-firewall-value-')); + const modelEvidencePath = join(directory, 'model-evidence.json'); const receiptPath = join(directory, 'value-receipt.json'); const revision = 'dd34bd9f681314761f1ca87f339648bf611811f3'; try { @@ -432,6 +454,8 @@ test('CLI writes a canonical value receipt only to an explicitly requested sidec 'reduce', '--mechanism-revision', revision, + '--model-evidence', + modelEvidencePath, '--value-receipt', receiptPath, ], { @@ -444,7 +468,12 @@ test('CLI writes a canonical value receipt only to an explicitly requested sidec }); assert.equal(result.stdout, serializePacket(packet).toString('utf8')); assert.equal(readFileSync(receiptPath, 'utf8'), `${canonicalJson(valueReceipt)}\n`); + assert.equal( + readFileSync(modelEvidencePath, 'utf8'), + serializeModelEvidence(modelEvidenceForPacket(packet)).toString('utf8'), + ); assert.equal(JSON.parse(readFileSync(receiptPath, 'utf8')).schema, VALUE_RECEIPT_SCHEMA); + assert.equal(JSON.parse(readFileSync(modelEvidencePath, 'utf8')).protocol_version, MODEL_EVIDENCE_PROTOCOL); } finally { rmSync(directory, { recursive: true }); }