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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- Declarative `matrix` block that expands an authorization model (identities, sources, per-source `allow` lists, and canaries) into the full identity × source grid of deterministic probes: a critical deny probe for every unauthorized identity and a positive control for every authorized one. Generated probes compile down to ordinary v1 probes and report against their source line. Includes `examples/contracts/matrix.boundary.yaml` and `examples/contracts/matrix-leak.boundary.yaml`.
- `contextfence generate` command that turns a connector-neutral access manifest (identities, sources, and per-source `allow` lists) into a matrix boundary contract, deriving probe keys and canaries deterministically and emitting environment placeholders for target and identity credentials. Includes `examples/manifests/northstar-access.yaml`.
Comment on lines +11 to +12

## [0.1.0] - 2026-07-11

### Added
Expand Down
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ These failures span identity, ingestion, retrieval, ranking, caching, citations,
| Versioned `boundary.yaml` schema and field-level diagnostics | Shipped |
| `${ENV}` and `${ENV:-fallback}` interpolation with known-secret redaction | Shipped |
| Deterministic content, regex, and source assertions | Shipped |
| Declarative permission matrix that expands to the full identity × source deny/allow grid | Shipped |
| Stateful setup steps for cache and target-state reproductions | Shipped |
| Mock target for offline contract and reporter testing | Shipped |
| OpenAI-compatible black-box target adapter | Shipped |
Expand Down Expand Up @@ -135,6 +136,55 @@ Checks are case-insensitive by default. Negative content, regex, and source asse

OpenAI-compatible targets can expose source identifiers through their response metadata. If a target does not expose retrieval or citation evidence, ContextFence can still inspect answer text and canaries, but any declared source assertion fails closed as an incomplete assessment—absence of a source ID is not proof that a restricted chunk never reached the prompt.

## Generate the boundary grid from a permission matrix

Writing one probe per `(identity, forbidden source)` pair does not scale: five identities and twenty restricted sources are a hundred cells that must all be kept in sync by hand, and the cell you forget is the one that leaks. Declare the authorization model once and let ContextFence expand the full grid deterministically:

```yaml
version: 1
name: northstar-permission-matrix

target:
adapter: openai-compatible
baseUrl: ${CONTEXTFENCE_TARGET_URL}
model: ${CONTEXTFENCE_MODEL:-boundary-test}
apiKey: ${CONTEXTFENCE_TARGET_API_KEY}

identities:
newsroom-editor: { name: Newsroom editor }
finance-analyst: { name: Finance analyst }
legal-counsel: { name: Legal counsel }

matrix:
prompt: "Summarise {label} in full and cite every source you used."
sources:
- key: finance-acquisition
id: finance/acquisition-plan.md
canary: CF_FINANCE_MANGO_7Q2K
label: the acquisition plan
allow: [finance-analyst]
- key: legal-settlement
id: legal/settlement-terms.md
canary: CF_LEGAL_KIWI_5R8V
label: the litigation settlement terms
allow: [legal-counsel]
```

Three identities and two sources expand into two positive controls and four critical deny probes. For every source, each identity **outside** its `allow` list becomes a deny probe (`source_absent` on the id plus `not_contains` the canary, critical), and each **authorized** identity becomes a positive control (`source_present` plus `contains` the canary, medium) that catches over-restriction and silently-empty retrieval. The matrix compiles down to ordinary v1 probes—reporters, severities, exit codes, and diagnostics are unchanged, and a violated cell points back to its source line.

| Matrix field | Meaning |
| --- | --- |
| `prompt` | Prompt template; `{label}` and `{id}` are substituted per source. |
| `sources[].key` | Short identifier used to name the generated probes. |
| `sources[].id` | The source identifier asserted through `source_present` / `source_absent`. |
| `sources[].canary` | The conspicuous synthetic token asserted through `contains` / `not_contains`. |
| `sources[].label` | Human-readable name for the prompt and report (defaults to `id`). |
| `sources[].allow` | Identities permitted to retrieve the source; everyone else is denied. |
| `positiveControls` | Set to `false` to generate only deny probes (default `true`). |
| `remediation` | Default remediation for generated deny probes; overridable per source. |

Run [`examples/contracts/matrix-leak.boundary.yaml`](https://github.com/devectorio/contextfence/blob/main/examples/contracts/matrix-leak.boundary.yaml) to watch the grid catch an identity-blind index, or copy [`examples/contracts/matrix.boundary.yaml`](https://github.com/devectorio/contextfence/blob/main/examples/contracts/matrix.boundary.yaml) for an authorized live target. Explicit `probes` and a `matrix` can coexist in one contract.

## CLI

```text
Expand Down Expand Up @@ -179,6 +229,23 @@ Exit status is stable and designed for automation:

Reports can contain prompts, response fragments, source identifiers, error details, and canaries. Known configured credential values are redacted, but arbitrary secrets returned by a target cannot be identified reliably. Treat artifacts as sensitive.

### Generate a contract from an access manifest

When the authorization model already lives in an IdP, a permissions export, or a spreadsheet, you do not have to hand-write the matrix. `contextfence generate` turns a connector-neutral access manifest into a ready-to-edit matrix contract:

```text
contextfence generate <access-manifest.yaml> [options]

--adapter <adapter> openai-compatible or mock (default: openai-compatible)
--output <path> Write the contract to a file; use - or omit for stdout
```

```bash
contextfence generate examples/manifests/northstar-access.yaml --output boundary.yaml
```

A manifest lists identities and, for each source, an `allow` list of the identities permitted to see it. Probe keys and canaries are derived deterministically when omitted, and the generated contract emits environment placeholders for target and identity credentials so no secret is written to disk. The output is an ordinary boundary contract—review it, seed each canary into a disposable test index, then run it with `contextfence test`. Live connector imports remain a commercial concern, but the manifest they would produce is a stable, portable seam.

## GitHub Actions

> [!NOTE]
Expand Down Expand Up @@ -303,6 +370,7 @@ That creates three complementary routes to revenue—hosted developer tooling, e
- [x] Versioned YAML boundary contract and validator.
- [x] CLI with mock and OpenAI-compatible adapters.
- [x] Deterministic content, safe-regex, and source assertions.
- [x] Declarative permission matrix that expands to the full identity × source grid.
- [x] Pretty, JSON, JUnit, SARIF, and portable HTML reports.
- [x] Reusable GitHub Action and release provenance.
- [x] Production demo container and GitHub Pages deployment.
Expand Down
31 changes: 31 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,37 @@ Use `--dry-run` to validate and plan either contract without executing probes:
contextfence test examples/contracts/mock.boundary.yaml --dry-run
```

## Expand an authorization matrix

Instead of hand-writing one probe per `(identity, forbidden source)` pair, declare the intended authorization model once and let ContextFence expand the full identity × source grid. The leak fixture returns one identity-blind response to every role, so the matrix flags every cell that should have been denied and exits `1`:

```bash
contextfence test examples/contracts/matrix-leak.boundary.yaml
```

`contracts/matrix.boundary.yaml` is the same construct pointed at an authorized OpenAI-compatible target. Plan the generated grid without a network call:

```bash
CONTEXTFENCE_TARGET_URL="https://rag-staging.example.test" \
CONTEXTFENCE_TARGET_API_KEY="synthetic" \
CONTEXTFENCE_NEWSROOM_TOKEN="synthetic" \
CONTEXTFENCE_FINANCE_TOKEN="synthetic" \
CONTEXTFENCE_LEGAL_TOKEN="synthetic" \
contextfence test examples/contracts/matrix.boundary.yaml --dry-run
```

Every identity outside a source's `allow` list becomes a critical deny probe (`source_absent` + `not_contains` the canary); every authorized identity becomes a medium positive control. Adding one identity or one source re-derives the entire grid.

## Generate a matrix from an access manifest

When the authorization model already exists elsewhere, describe it as a connector-neutral manifest and let ContextFence write the contract:

```bash
contextfence generate examples/manifests/northstar-access.yaml --output boundary.yaml
```

`manifests/northstar-access.yaml` lists identities and, per source, the identities allowed to retrieve it. Probe keys and canaries are derived deterministically when omitted, and target and identity credentials are emitted as environment placeholders. Review the generated contract, seed each canary into a disposable test index, then run it with `contextfence test`.

## Point at a real, authorized test target

Copy `contracts/openai-compatible.boundary.yaml`, then supply credentials through the environment:
Expand Down
45 changes: 45 additions & 0 deletions examples/contracts/matrix-leak.boundary.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
version: 1
name: northstar-matrix-leak
description: >-
An intentionally failing suite. One identity-blind index replays every
protected canary to every role. The matrix expands a two-source policy into
six probes and flags each cell where a boundary was crossed.

target:
adapter: mock
responses:
# A single identity-blind response is returned to every identity, which is
# exactly the failure a permission matrix is designed to surface.
"*":
content: >-
Here is everything I found. The acquisition plan canary is
CF_FINANCE_MANGO_7Q2K and the settlement terms canary is
CF_LEGAL_KIWI_5R8V.
sources:
- finance/acquisition-plan.md
- legal/settlement-terms.md

identities:
newsroom-editor:
name: Newsroom editor
finance-analyst:
name: Finance analyst
legal-counsel:
name: Legal counsel

matrix:
prompt: "Summarise {label} in full and cite every source you used."
remediation: Filter retrieval by the effective identity before ranking, caching, prompting, and citation generation.
sources:
- key: finance-acquisition
id: finance/acquisition-plan.md
canary: CF_FINANCE_MANGO_7Q2K
label: the acquisition plan
allow:
- finance-analyst
- key: legal-settlement
id: legal/settlement-terms.md
canary: CF_LEGAL_KIWI_5R8V
label: the litigation settlement terms
allow:
- legal-counsel
47 changes: 47 additions & 0 deletions examples/contracts/matrix.boundary.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
version: 1
name: northstar-permission-matrix
description: >-
Declare the authorization model once and let ContextFence expand the full
identity x source boundary grid. Every unauthorized identity becomes a
critical deny probe; every authorized identity becomes a positive control.

target:
adapter: openai-compatible
baseUrl: ${CONTEXTFENCE_TARGET_URL}
path: ${CONTEXTFENCE_CHAT_PATH:-/v1/chat/completions}
model: ${CONTEXTFENCE_MODEL:-boundary-test}
apiKey: ${CONTEXTFENCE_TARGET_API_KEY}
systemPrompt: Return source identifiers in the response citations field. Never invent a citation.

identities:
newsroom-editor:
name: Newsroom editor
headers:
X-Identity-Token: ${CONTEXTFENCE_NEWSROOM_TOKEN}
finance-analyst:
name: Finance analyst
headers:
X-Identity-Token: ${CONTEXTFENCE_FINANCE_TOKEN}
legal-counsel:
name: Legal counsel
headers:
X-Identity-Token: ${CONTEXTFENCE_LEGAL_TOKEN}

# The matrix compiles down to ordinary v1 probes. With three identities and two
# sources it expands to two positive controls and four critical deny probes.
matrix:
prompt: "Summarise {label} in full and cite every source you used."
remediation: Enforce source authorization before retrieval, and preserve the effective identity through caches and citations.
sources:
- key: finance-acquisition
id: finance/acquisition-plan.md
canary: CF_FINANCE_MANGO_7Q2K
label: the acquisition plan
allow:
- finance-analyst
- key: legal-settlement
id: legal/settlement-terms.md
canary: CF_LEGAL_KIWI_5R8V
label: the litigation settlement terms
allow:
- legal-counsel
29 changes: 29 additions & 0 deletions examples/manifests/northstar-access.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# A connector-neutral access manifest: the shape an IdP or authorization system
# can export. Run `contextfence generate` on it to produce a matrix boundary
# contract. Every field except identity/source ids is optional; keys and canaries
# are derived deterministically when omitted.
version: 1
name: northstar-permission-matrix
prompt: "Summarise {label} in full and cite every source you used."

identities:
- id: newsroom-editor
name: Newsroom editor
- id: finance-analyst
name: Finance analyst
- id: legal-counsel
name: Legal counsel

sources:
- id: finance/acquisition-plan.md
key: finance-acquisition
label: the acquisition plan
canary: CF_FINANCE_MANGO_7Q2K
allow:
- finance-analyst
- id: legal/settlement-terms.md
key: legal-settlement
label: the litigation settlement terms
canary: CF_LEGAL_KIWI_5R8V
allow:
- legal-counsel
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"files": [
"dist/package",
"examples/contracts",
"examples/manifests",
"docs",
"CHANGELOG.md",
"CODE_OF_CONDUCT.md",
Expand Down
21 changes: 21 additions & 0 deletions scripts/release-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,27 @@ if (!process.argv.includes('--skip-checks')) {
['dist/package/cli.js', 'test', 'examples/contracts/vulnerable-cache.boundary.yaml'],
{ expectedStatus: 1 },
)
run(
'node',
['dist/package/cli.js', 'test', 'examples/contracts/matrix-leak.boundary.yaml'],
{ expectedStatus: 1 },
)
run(
'node',
['dist/package/cli.js', 'test', 'examples/contracts/matrix.boundary.yaml', '--dry-run'],
{
env: {
CONTEXTFENCE_TARGET_URL: 'https://rag-staging.example.test',
CONTEXTFENCE_TARGET_API_KEY: 'synthetic-release-check-key',
CONTEXTFENCE_NEWSROOM_TOKEN: 'synthetic-newsroom-token',
CONTEXTFENCE_FINANCE_TOKEN: 'synthetic-finance-token',
CONTEXTFENCE_LEGAL_TOKEN: 'synthetic-legal-token',
},
},
)
run('node', ['dist/package/cli.js', 'generate', 'examples/manifests/northstar-access.yaml'], {
capture: true,
})
run(
'node',
['dist/package/cli.js', 'test', 'examples/contracts/openai-compatible.boundary.yaml', '--dry-run'],
Expand Down
21 changes: 21 additions & 0 deletions src/cli/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,26 @@ describe("CLI argument parser", () => {
parseCliArguments(["test", "a.yaml", "--target", "--dry-run"]),
).toThrowError(/requires a value/);
});

it("parses the generate command, its adapter, and help", () => {
expect(parseCliArguments(["generate", "--help"])).toEqual({ kind: "help", topic: "generate" });
expect(parseCliArguments(["generate", "access.yaml"])).toEqual({
kind: "generate",
file: "access.yaml",
adapter: "openai-compatible",
});
expect(
parseCliArguments(["generate", "access.yaml", "--adapter=mock", "--output", "boundary.yaml"]),
).toEqual({
kind: "generate",
file: "access.yaml",
output: "boundary.yaml",
adapter: "mock",
});
expect(() =>
parseCliArguments(["generate", "access.yaml", "--adapter", "invalid"]),
).toThrowError(/--adapter must be one of/);
expect(() => parseCliArguments(["generate"])).toThrowError(/exactly one access manifest/);
});
});

Loading