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
72 changes: 70 additions & 2 deletions harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ src/
tools/
mock-tools.ts search, calculator, readFile, writeFile, runTests
agents/
chat-agent.ts LangGraph-style conversational agent (plan -> act -> verify)
coder-agent.ts Plan-do-act coder (plan -> inspect -> act -> verify -> repair -> summarize)
chat-agent.ts Conversational loop with policy-mediated authority checks
coder-agent.ts Coder loop with policy-mediated authority + repair
governed-agent.ts Full RFC 0007 governed flow with receipts + audit sealing
```

## Quick start
Expand All @@ -58,6 +59,73 @@ npx tsx examples/chat-demo.ts
npx tsx examples/coder-demo.ts
```

### Run the governed agent demo

```bash
npx tsx examples/governed-demo.ts
```

Policy modes:

```bash
npx tsx examples/governed-demo.ts --deny "search for info"
npx tsx examples/governed-demo.ts --narrow "search for info"
```

### Choose a policy engine for governed demo

Use `POLICY_ENGINE`:

- `inprocess` (default): uses the built-in evaluator
- `opa`: sends delegation requests to OPA and maps decisions into Open CoT objects

```bash
POLICY_ENGINE=inprocess npx tsx examples/governed-demo.ts
```

```bash
POLICY_ENGINE=opa \
OPA_BASE_URL=http://127.0.0.1:8181 \
OPA_POLICY_PATH=open_cot/delegation \
npx tsx examples/governed-demo.ts
```

Optional OPA env vars:

- `OPA_BEARER_TOKEN`
- `OPA_TIMEOUT_MS` (default `2000`)
- `OPA_FALLBACK_INPROCESS` (`true` by default)

Starter OPA policy package: `examples/opa/README.md`

Live OPA integration test (targets `http://127.0.0.1:8181` by default):

```bash
npm run test:opa-live
```

Override defaults if needed:

```bash
OPA_BASE_URL=http://127.0.0.1:8181 \
OPA_POLICY_PATH=open_cot/delegation \
OPA_LIVE_POLICY_MODE=allow \
npm run test:opa-live
```

`npm test` still auto-skips the live OPA suite when `OPA_BASE_URL` is not set.

## Runtime governance guarantees

Current harness behavior (runtime, not just schema/docs):

- **Policy mediation for all shipped agents**: `chat-agent`, `coder-agent`, and `governed-agent` route tool execution through a `DelegationPolicyEngine` before dispatch.
- **Dispatch-time least privilege enforcement**: tool arguments are schema-validated and checked against delegated scope constraints (`allowed_fields`, `excluded_fields`, `max_results`) in `ToolRegistry`.
- **Phase consultation checks**: policy consultation hooks are enforced at `frame`, `plan`, `observe_result`, `critique_verify`, and `finalize`.
- **Manifest/policy reconciliation**: capability manifests can be compiled from policy-engine tool previews (including OPA-backed decisions), so model-visible tool posture reflects live policy outcomes.

`chat-agent` and `coder-agent` default to an in-process policy derived from sandbox allow/block lists. You can override this by passing explicit `policies` and/or a custom `policyEngine`.

### Use a real LLM (Ollama example)

```bash
Expand Down
73 changes: 72 additions & 1 deletion harness/examples/governed-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
* npx tsx examples/governed-demo.ts "calculate 2+2" # calculator (allowed)
* npx tsx examples/governed-demo.ts "search for open source" # search (allowed)
* npx tsx examples/governed-demo.ts --deny "search for info" # search (denied by policy)
*
* Policy engine selection via env:
* POLICY_ENGINE=inprocess|opa
*
* OPA settings (when POLICY_ENGINE=opa):
* OPA_BASE_URL=http://127.0.0.1:8181
* OPA_POLICY_PATH=open_cot/delegation
* OPA_BEARER_TOKEN=...
* OPA_TIMEOUT_MS=2000
* OPA_FALLBACK_INPROCESS=true|false
*/

import { runGovernedAgent } from "../src/agents/governed-agent.js";
Expand All @@ -21,6 +31,11 @@ import { OpenAICompatBackend } from "../src/backends/openai-compat.js";
import { createMockToolRegistry } from "../src/tools/mock-tools.js";
import type { PolicySet } from "../src/governance/policy-evaluator.js";
import type { LLMBackend } from "../src/backends/types.js";
import {
InProcessPolicyEngine,
OpaPolicyEngine,
type DelegationPolicyEngine,
} from "../src/governance/index.js";

function pickBackend(): LLMBackend {
if (process.env["OPENAI_BASE_URL"] || process.env["OPENAI_API_KEY"]) {
Expand Down Expand Up @@ -76,6 +91,56 @@ const NARROW_SEARCH_POLICY: PolicySet = {
priority: 10,
};

interface PolicyEngineSelection {
engine: DelegationPolicyEngine;
engineLabel: string;
manifestPolicies: PolicySet[];
}

function parsePositiveInt(value: string | undefined): number | undefined {
if (!value) return undefined;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
return parsed;
}

function pickPolicyEngine(
policies: PolicySet[],
policyMode: "allow" | "deny" | "narrow",
): PolicyEngineSelection {
const engineChoice = (process.env["POLICY_ENGINE"] ?? "inprocess").toLowerCase();
if (engineChoice !== "opa") {
return {
engine: new InProcessPolicyEngine(policies),
engineLabel: "in-process",
manifestPolicies: policies,
};
}

const opaBaseUrl = process.env["OPA_BASE_URL"] ?? "http://127.0.0.1:8181";
const opaPolicyPath = process.env["OPA_POLICY_PATH"] ?? "open_cot/delegation";
const fallbackEnabled =
(process.env["OPA_FALLBACK_INPROCESS"] ?? "true").toLowerCase() !== "false";
const fallbackEngine = fallbackEnabled
? new InProcessPolicyEngine(policies)
: undefined;

return {
engine: new OpaPolicyEngine({
baseUrl: opaBaseUrl,
policyPath: opaPolicyPath,
bearerToken: process.env["OPA_BEARER_TOKEN"],
timeoutMs: parsePositiveInt(process.env["OPA_TIMEOUT_MS"]),
inputContext: {
policy_mode: policyMode,
},
fallbackEngine,
}),
engineLabel: `opa (${opaBaseUrl}/v1/data/${opaPolicyPath})${fallbackEnabled ? " + fallback" : ""}`,
manifestPolicies: fallbackEnabled ? policies : [],
};
}

async function main() {
const args = process.argv.slice(2);
const denyMode = args.includes("--deny");
Expand All @@ -85,24 +150,30 @@ async function main() {

const policies: PolicySet[] = [ALLOW_ALL_POLICY];
let mode = "ALLOW ALL";
let policyMode: "allow" | "deny" | "narrow" = "allow";

if (denyMode) {
policies.unshift(DENY_SEARCH_POLICY);
mode = "DENY SEARCH";
policyMode = "deny";
} else if (narrowMode) {
policies.unshift(NARROW_SEARCH_POLICY);
mode = "NARROW SEARCH";
policyMode = "narrow";
}
const policyEngineSelection = pickPolicyEngine(policies, policyMode);

console.log(`\n--- Governed Agent Demo ---`);
console.log(`Policy mode: ${mode}`);
console.log(`Policy engine: ${policyEngineSelection.engineLabel}`);
console.log(`Question: ${question}\n`);

const config: GovernedAgentConfig = {
objective: question,
backend: pickBackend(),
toolRegistry: createMockToolRegistry(),
policies,
policies: policyEngineSelection.manifestPolicies,
policyEngine: policyEngineSelection.engine,
agentId: "demo-agent-01",
};

Expand Down
96 changes: 96 additions & 0 deletions harness/examples/opa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Starter OPA Policy Package

This folder contains a minimal OPA/Rego package you can use with the governed demo.

## Start OPA with the starter policies

```bash
cd harness
opa run --server examples/opa/policies
```

The starter package exposes decision data at `open_cot/delegation`.

## Run governed demo against OPA

```bash
cd harness
POLICY_ENGINE=opa \
OPA_BASE_URL=http://127.0.0.1:8181 \
OPA_POLICY_PATH=open_cot/delegation \
npx tsx examples/governed-demo.ts
```

## Try policy modes

The governed demo sends `input.context.policy_mode` to OPA:

- default: `"allow"`
- `--deny`: `"deny"` (deny search)
- `--narrow`: `"narrow"` (narrow search scope)

Examples:

```bash
# deny search requests
POLICY_ENGINE=opa OPA_BASE_URL=http://127.0.0.1:8181 \
npx tsx examples/governed-demo.ts --deny "search for open source"

# narrow search requests
POLICY_ENGINE=opa OPA_BASE_URL=http://127.0.0.1:8181 \
npx tsx examples/governed-demo.ts --narrow "search for open source"
```

## Response contract expected by harness

OPA `result` should return an object like:

```json
{
"status": "approved | denied | narrowed | escalated",
"policy_refs": ["policy.id"],
"narrowed_scope": {
"resource": "tool:search",
"action": "execute",
"constraints": { "max_results": 5 }
},
"denial_reason": "optional reason",
"escalation_target": "optional target",
"decided_by": { "kind": "policy", "policy_id": "policy.id" }
}
```

The harness uses this same decision shape for:

- tool authorization requests (`resource: "tool:<name>"`)
- manifest reconciliation previews (`resource: "tool:<name>"`, preview context)
- phase consultation hooks (`resource: "phase:<phase>"`)

The starter policy allows `phase:*` by default so runtime consultation does not block the run unless you explicitly add phase-deny rules.

Conformance fixtures for this mapping live at:

- `tests/fixtures/opa-decision-conformance.json`
- `tests/policy-engine-conformance.test.ts`

## Optional live OPA integration test

Use the dedicated script:

```bash
cd harness
npm run test:opa-live
```

Override defaults if needed:

```bash
cd harness
OPA_BASE_URL=http://127.0.0.1:8181 \
OPA_POLICY_PATH=open_cot/delegation \
OPA_LIVE_POLICY_MODE=allow \
npm run test:opa-live
```

The live test checks end-to-end request/response integration and decision-shape mapping
against a real OPA server.
81 changes: 81 additions & 0 deletions harness/examples/opa/policies/open_cot/delegation.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package open_cot.delegation

import rego.v1

policy_mode := object.get(object.get(input, "context", {}), "policy_mode", "allow")
requested_scope := input.request.requested_scope
is_search_request if requested_scope.resource == "tool:search"

default_result := {
"status": "denied",
"policy_refs": ["starter.default_deny"],
"denial_reason": "No matching rule in starter OPA policy",
"decided_by": {
"kind": "policy",
"policy_id": "starter.default_deny",
},
}

allow_result := {
"status": "approved",
"policy_refs": ["starter.allow_all_tools"],
"decided_by": {
"kind": "policy",
"policy_id": "starter.allow_all_tools",
},
}

allow_phase_result := {
"status": "approved",
"policy_refs": ["starter.allow_phase_hooks"],
"decided_by": {
"kind": "policy",
"policy_id": "starter.allow_phase_hooks",
},
}

deny_result := {
"status": "denied",
"policy_refs": ["starter.deny_search"],
"denial_reason": "Search access is restricted by starter OPA policy",
"decided_by": {
"kind": "policy",
"policy_id": "starter.deny_search",
},
}

narrow_result := {
"status": "narrowed",
"policy_refs": ["starter.narrow_search"],
"narrowed_scope": {
"resource": requested_scope.resource,
"action": requested_scope.action,
"constraints": object.union(
object.get(requested_scope, "constraints", {}),
{
"max_results": 5,
"excluded_fields": ["raw_html", "cached_page"],
},
),
},
"decided_by": {
"kind": "policy",
"policy_id": "starter.narrow_search",
},
}

result := deny_result if {
policy_mode == "deny"
is_search_request
}
else := narrow_result if {
policy_mode == "narrow"
is_search_request
}
else := allow_result if {
startswith(requested_scope.resource, "tool:")
}
else := allow_phase_result if {
startswith(requested_scope.resource, "phase:")
}
else := default_result
1 change: 1 addition & 0 deletions harness/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"scripts": {
"build": "tsc",
"test": "vitest run",
"test:opa-live": "OPA_BASE_URL=${OPA_BASE_URL:-http://127.0.0.1:8181} OPA_POLICY_PATH=${OPA_POLICY_PATH:-open_cot/delegation} OPA_LIVE_POLICY_MODE=${OPA_LIVE_POLICY_MODE:-allow} vitest run tests/policy-engine-live.test.ts",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"chat-demo": "tsx examples/chat-demo.ts",
Expand Down
Loading
Loading