Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.

Expand Down
20 changes: 18 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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

Expand Down
15 changes: 13 additions & 2 deletions bin/context-firewall.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,36 @@ 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];
else if (args[index] === '--max-bytes' && args[index + 1]) {
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) {
Expand Down Expand Up @@ -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`);
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opsle/context-firewall",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"bin": {
Expand Down
3 changes: 3 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down
29 changes: 29 additions & 0 deletions tests/reducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
], {
Expand All @@ -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 });
}
Expand Down
Loading