Skip to content
Open
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
35 changes: 27 additions & 8 deletions interop/composition-test/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Composition Test: ScopeBlind + APS
# Composition Test: ASM + ScopeBlind + APS

**One tool call. Two governance evaluations. One verifier.**
**One service choice. One tool call. Two governance evaluations. One verifier.**

This test demonstrates the composition model for multi-engine agent governance: a single CrewAI tool call (`execute_api_call`) is independently evaluated by two different governance engines (ScopeBlind Cedar policy + APS delegation scope), both producing Ed25519-signed receipts in the IETF draft envelope format, both verifiable by the same offline tool.
This test demonstrates the composition model for service selection and multi-engine agent governance. ASM records why the fixture chooses one eligible API route before the call. A single CrewAI tool call (`execute_api_call`) is then independently evaluated by two different governance engines (ScopeBlind Cedar policy + APS delegation scope), both producing Ed25519-signed receipts in the IETF draft envelope format and both verifiable by the same offline tool.

## Scenario

Expand All @@ -11,6 +11,11 @@ A CrewAI research agent calls `execute_api_call`:
- **Spend:** $0.50
- **Model:** `anthropic/claude-sonnet-4-20250514`

### ASM selection (pre-call)
- Checks: cloud invocability, required chat/API functions, fixture price, and approval boundary
- Produces: an unsigned Selection Receipt that pins both candidate manifests by digest
- Authority: explains the choice only; it is not an authorization, execution, or payment receipt

### APS evaluation (delegation + scope)
- Checks: delegation scope includes `tools:api_call`, spend within $500 budget
- Produces: delegation receipt with `extensions.aps`
Expand All @@ -20,12 +25,13 @@ A CrewAI research agent calls `execute_api_call`:
- Produces: policy evaluation receipt + execution receipt with `extensions.scopeblind`

### Correlation
Both governance evaluations reference the same `action_ref` -- a SHA-256 hash of the canonical tool invocation (`agent_id + tool_name + args`). A verifier links the evaluations by this hash without needing to understand either engine's internals.
Both governance evaluations reference the same `action_ref` -- a SHA-256 hash of the canonical tool invocation (`agent_id + tool_name + args`). ScopeBlind's signed policy and execution receipts also carry `extensions.asm.selection_receipt`, which references the exact pre-call Selection Receipt digest. A verifier can therefore link **why this route** to **was this call allowed** and **what executed** without making any receipt authoritative for another event.

## Receipts

| File | Engine | Type | Chain |
|------|--------|------|-------|
| `asm-selection-receipt.json` | ASM | Pre-call selection explanation (unsigned v0.1) | Pins the two fixture manifests |
| `scopeblind-policy-eval.json` | ScopeBlind | Cedar policy evaluation | First in chain |
| `scopeblind-execution.json` | ScopeBlind | Tool execution result | Links to policy eval |

Expand All @@ -47,6 +53,16 @@ npx @veritasacta/verify@0.2.5 ../aps-test-vectors/receipt-commerce.json --key $A

All four should return exit code 0 (VALID).

Validate the two scenario-local selection manifests independently:

```bash
python -m pip install "asm-protocol==0.5.2"
asm-lint asm-openrouter-manifest.json --as-of 2026-08-17 --fail-on not-ready
asm-lint asm-direct-manifest.json --as-of 2026-08-17 --fail-on not-ready
```

These manifests deliberately describe fixed fixture values, not current provider pricing.

## Verify all at once

```bash
Expand All @@ -57,19 +73,21 @@ bash verify-all.sh

1. **Format convergence.** Two independent implementations (APS ProxyGateway + ScopeBlind protect-mcp) produce receipts that verify against the same tool, without coordination on the verification path.

2. **Extension isolation.** `extensions.scopeblind` carries Cedar policy results. `extensions.aps` carries delegation chains and spend tracking. Both are covered by the envelope signature (tamper-evident) but opaque to the other engine's verifier.
2. **Event authority stays separate.** The unsigned ASM receipt explains provider selection. ScopeBlind and APS remain authoritative for their own signed policy and execution events. Referencing the Selection Receipt digest does not turn ASM into an authorization or settlement proof.

3. **Extension isolation.** `extensions.asm` carries only a Selection Receipt reference, `extensions.scopeblind` carries Cedar results, and `extensions.aps` carries delegation chains and spend tracking. Signed extensions are tamper-evident but remain semantically owned by their issuers.

3. **Composable governance.** An agent can be simultaneously governed by APS (delegation scope, spend limits) and ScopeBlind (Cedar policy, rate limits) without either system needing to know about the other. The `action_ref` is the only shared anchor.
4. **Composable governance.** An agent can be simultaneously selected by a local ASM policy and governed by APS and ScopeBlind without any engine reproducing another engine's facts.

4. **IETF draft as interop baseline.** Both systems reference `draft-farley-acta-signed-receipts-01`. The draft defines the envelope; extensions carry engine-specific data.
5. **IETF draft as interop baseline.** The governance systems reference `draft-farley-acta-signed-receipts-01`. The draft defines their signed envelope; the ASM Selection Receipt remains a separate, digest-linked pre-call artifact.

## Generate fresh receipts

```bash
node generate-receipts.mjs
```

This generates new Ed25519 keys and fresh receipts for the scenario. The `action_ref` is deterministic for the same tool invocation.
This generates new Ed25519 keys and fresh governance receipts for the scenario. The `action_ref` and Selection Receipt digest are deterministic while their input files remain unchanged.

## Cedar policy used

Expand All @@ -95,3 +113,4 @@ Policy digest included in every ScopeBlind receipt. If the policy changes, the d
- [@veritasacta/verify](https://npmjs.com/package/@veritasacta/verify) (Apache-2.0)
- [protect-mcp](https://npmjs.com/package/protect-mcp) (MIT)
- [agent-passport-system](https://npmjs.com/package/agent-passport-system)
- [ASM Selection Receipt v0.1](https://github.com/YE-YI7/asm-spec/blob/main/docs/specs/selection-receipt.md)
2 changes: 1 addition & 1 deletion interop/composition-test/action-ref.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sha256:0985753552dfd9ca60c978d00309c844c512ab0ae20062f9bebeac826f00bbdf
31789af0e4b00d6c3f686ee56b5b361ba4f62055a37d52f6758d671d8ff60139
56 changes: 56 additions & 0 deletions interop/composition-test/asm-direct-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"asm_version": "0.3",
"service_id": "scopeblind-fixture/direct-provider-route@1.0",
"taxonomy": "ai.llm.chat",
"display_name": "Direct provider route (composition fixture)",
"provenance": {
"source_url": "https://github.com/ScopeBlind/examples/tree/main/interop/composition-test",
"retrieved_at": "2026-08-17T00:00:00Z",
"last_verified_at": "2026-08-17T00:00:00Z",
"verification_status": "self_reported",
"notes": "Scenario-local alternative used only to make the selection decision reproducible."
},
"capabilities": {
"description": "Alternative route for the same fixed chat-completion scenario.",
"functions": ["chat_completion", "paid_api_call"],
"input_modalities": ["text"],
"output_modalities": ["text"]
},
"pricing": {
"billing_dimensions": [
{
"dimension": "fixture_call",
"unit": "per_1",
"cost_per_unit": 0.8,
"currency": "USD"
}
]
},
"operational_constraints": {
"risk_class": "critical",
"side_effects": ["external_api_call", "network_access", "financial_charge"],
"approval": {
"required": "conditional",
"conditions": ["financial_charge"]
},
"spend_caps": {
"per_call_usd": 0.8,
"currency": "USD"
},
"receipt_required": true,
"policy_notes": "The $0.80 value belongs to this fixed composition scenario."
},
"invocation": {
"interface": "rest_api",
"reach": "cloud",
"agent_operable": true,
"auth_to_invoke": "api_token",
"agent_completable_setup": false,
"setup_requires": ["account_creation", "api_key_request"],
"platforms": ["any"]
},
"usage_terms": {
"automation_allowed": "conditional",
"notes": "Test fixture only; callers remain responsible for provider terms."
}
}
58 changes: 58 additions & 0 deletions interop/composition-test/asm-openrouter-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"asm_version": "0.3",
"service_id": "scopeblind-fixture/openrouter-route@1.0",
"taxonomy": "ai.llm.chat",
"display_name": "OpenRouter route (composition fixture)",
"provenance": {
"source_url": "https://github.com/ScopeBlind/examples/tree/main/interop/composition-test",
"retrieved_at": "2026-08-17T00:00:00Z",
"last_verified_at": "2026-08-17T00:00:00Z",
"verification_status": "self_reported",
"notes": "Scenario-local fixture values, not a claim about current OpenRouter pricing or availability."
},
"capabilities": {
"description": "Route the composition-test chat completion through the endpoint used by the scenario.",
"functions": ["chat_completion", "paid_api_call"],
"input_modalities": ["text"],
"output_modalities": ["text"]
},
"pricing": {
"billing_dimensions": [
{
"dimension": "fixture_call",
"unit": "per_1",
"cost_per_unit": 0.5,
"currency": "USD"
}
]
},
"operational_constraints": {
"risk_class": "critical",
"side_effects": ["external_api_call", "network_access", "financial_charge"],
"approval": {
"required": "conditional",
"conditions": ["financial_charge"],
"human_readable": "The composition fixture requires approval before the paid API call."
},
"spend_caps": {
"per_call_usd": 0.5,
"currency": "USD"
},
"receipt_required": true,
"policy_notes": "The $0.50 value belongs to this fixed composition scenario."
},
"invocation": {
"interface": "rest_api",
"reach": "cloud",
"agent_operable": true,
"auth_to_invoke": "api_token",
"agent_completable_setup": false,
"setup_requires": ["account_creation", "api_key_request"],
"platforms": ["any"],
"docs_url": "https://openrouter.ai/docs/api-reference/overview"
},
"usage_terms": {
"automation_allowed": "conditional",
"notes": "Test fixture only; callers remain responsible for provider terms."
}
}
1 change: 1 addition & 0 deletions interop/composition-test/asm-selection-receipt-digest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:4c4a9ebb847ff07721862a0f7e214c64606704b1ac31cac5cd6451488e6632e0
47 changes: 47 additions & 0 deletions interop/composition-test/asm-selection-receipt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"receipt_type": "selection",
"receipt_version": "0.1",
"selection_id": "5d7e46e7-3997-4db3-90fb-507f4a6d2b62",
"issued_at": "2026-08-17T00:00:00Z",
"selector": {
"name": "asm-protocol/0.5.2",
"policy": "gate: cloud agent-operable route with chat_completion and paid_api_call; rank: fixture cost within the approved per-call cap"
},
"request": {
"task": "execute the fixed paid chat-completion call used by the ScopeBlind composition test",
"taxonomy": "ai.llm.chat",
"agent_reach": "cloud",
"user_platform": "any",
"required_functions": ["chat_completion", "paid_api_call"],
"require_approval_for": ["financial_charge"],
"max_cost_usd": 0.5
},
"evidence": [
{
"service_id": "scopeblind-fixture/openrouter-route@1.0",
"manifest_digest": "sha256:57d3680b15b19d675ba11e125be77fc8072f12ba6754725a25ebc98b08481c3a"
},
{
"service_id": "scopeblind-fixture/direct-provider-route@1.0",
"manifest_digest": "sha256:3f1c9300151453bc66fb4b3b4fe7aaf9c87425cefc9b1458c479baf775bc160f"
}
],
"selected": {
"service_id": "scopeblind-fixture/openrouter-route@1.0",
"display_name": "OpenRouter route (composition fixture)",
"interface": "rest_api",
"reach": "cloud"
},
"selection_reason": "Both fixture routes are eligible; the OpenRouter route matches the approved $0.50 per-call cap while the $0.80 alternative does not.",
"risk_class": "critical",
"approval_required": true,
"side_effects": ["external_api_call", "network_access", "financial_charge"],
"alternatives": [],
"rejected": [
{
"service": "Direct provider route (composition fixture)",
"service_id": "scopeblind-fixture/direct-provider-route@1.0",
"reason": "fixture cost $0.80 exceeds max_cost_usd $0.50"
}
]
}
38 changes: 31 additions & 7 deletions interop/composition-test/generate-receipts.mjs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { createHash } from 'crypto';
import { generateKeyPairSync, sign } from 'crypto';
import { writeFileSync } from 'fs';
import { readFileSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

Expand Down Expand Up @@ -54,6 +54,20 @@ function sha256hex(data) {
return createHash('sha256').update(data).digest('hex');
}

// --- Pre-call selection artifact ---
// ASM owns only the choice record. ScopeBlind remains authoritative for its
// signed authorization and execution receipts, which reference this digest.
const selectionReceipt = JSON.parse(
readFileSync(join(__dirname, 'asm-selection-receipt.json'), 'utf8'),
);
const selectionReceiptDigest = 'sha256:' + sha256hex(canonicalize(selectionReceipt));
const selectionReceiptRef = {
receipt_type: selectionReceipt.receipt_type,
receipt_version: selectionReceipt.receipt_version,
selection_id: selectionReceipt.selection_id,
digest: selectionReceiptDigest,
};

// --- Shared scenario ---
const AGENT_ID = 'crewai-agent-research-001';
const TOOL_NAME = 'execute_api_call';
Expand All @@ -66,12 +80,14 @@ const TOOL_ARGS = {
};
const SPEND_USD = 0.50;

// --- action_ref: content hash anchoring both governance evaluations ---
function computeActionRef(agentId, toolName, args) {
const input = JSON.stringify({ agent_id: agentId, args, tool: toolName });
return 'sha256:' + sha256hex(input);
}
const actionRef = computeActionRef(AGENT_ID, TOOL_NAME, TOOL_ARGS);
// --- action_ref: the existing APS composition anchor ---
// APS owns the canonical action identifier in this checked-in composition
// fixture. Reuse it byte-for-byte so the two independently signed receipt
// families actually correlate; do not mint a second hash for the same call.
const actionRef = readFileSync(
join(__dirname, 'aps-receipts', 'action-ref.txt'),
'utf8',
).trim();

// --- Cedar policy ---
const CEDAR_POLICY = `permit(
Expand Down Expand Up @@ -112,6 +128,9 @@ const policyPayload = {
},
},
extensions: {
asm: {
selection_receipt: selectionReceiptRef,
},
scopeblind: {
cedar_decision: 'permit',
cedar_diagnostics: [],
Expand Down Expand Up @@ -163,6 +182,9 @@ const execPayload = {
currency: 'usd',
},
extensions: {
asm: {
selection_receipt: selectionReceiptRef,
},
scopeblind: {
cedar_decision: 'permit',
policy_digest: policyDigest,
Expand Down Expand Up @@ -192,10 +214,12 @@ writeFileSync(join(__dirname, 'scopeblind-policy-eval.json'), JSON.stringify(pol
writeFileSync(join(__dirname, 'scopeblind-execution.json'), JSON.stringify(execReceipt, null, 2) + '\n');
writeFileSync(join(__dirname, 'scopeblind-pubkey.txt'), pubKeyHex + '\n');
writeFileSync(join(__dirname, 'action-ref.txt'), actionRef + '\n');
writeFileSync(join(__dirname, 'asm-selection-receipt-digest.txt'), selectionReceiptDigest + '\n');
writeFileSync(join(__dirname, 'cedar-policy.cedar'), CEDAR_POLICY + '\n');

console.log('Generated ScopeBlind composition test receipts:');
console.log(` action_ref: ${actionRef}`);
console.log(` selection: ${selectionReceiptDigest}`);
console.log(` public key: ${pubKeyHex}`);
console.log(` policy receipt: scopeblind-policy-eval.json`);
console.log(` exec receipt: scopeblind-execution.json`);
Expand Down
22 changes: 15 additions & 7 deletions interop/composition-test/scopeblind-execution.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"payload": {
"spec": "draft-farley-acta-signed-receipts-01",
"receipt_id": "sha256:ace89c38ff7d7276b8245cc8d020a92150c6f4fd133d0b60b03d90e10e58d276",
"receipt_id": "sha256:1f4355395182edef4c3ecebcc8839a7dce6964c953e9ad39cfe19923818484bc",
"type": "tool:execution",
"issued_at": "2026-04-08T16:10:01.974Z",
"issuer_id": "sb:issuer:91e4d1a4f0f0",
"previousReceiptHash": "sha256:178ab43edefbbe99a89e9f2249c2ff995d2ab749ef532dc4a9bf6893ed748f37",
"action_ref": "sha256:0985753552dfd9ca60c978d00309c844c512ab0ae20062f9bebeac826f00bbdf",
"issued_at": "2026-08-17T01:18:22.141Z",
"issuer_id": "sb:issuer:93a2146e3977",
"previousReceiptHash": "sha256:24fabc778fefb0c79ac250a405107339af3f6e6adeab52b90d87462adacb6e59",
"action_ref": "31789af0e4b00d6c3f686ee56b5b361ba4f62055a37d52f6758d671d8ff60139",
"agentId": "crewai-agent-research-001",
"action": {
"tool": "execute_api_call",
Expand All @@ -23,6 +23,14 @@
"currency": "usd"
},
"extensions": {
"asm": {
"selection_receipt": {
"receipt_type": "selection",
"receipt_version": "0.1",
"selection_id": "5d7e46e7-3997-4db3-90fb-507f4a6d2b62",
"digest": "sha256:4c4a9ebb847ff07721862a0f7e214c64606704b1ac31cac5cd6451488e6632e0"
}
},
"scopeblind": {
"cedar_decision": "permit",
"policy_digest": "sha256:f381122b9014899cffcd2b9ead006ba8f2e2881121010dfef903f5a5db307b3f",
Expand All @@ -36,7 +44,7 @@
},
"signature": {
"alg": "EdDSA",
"kid": "sb:issuer:91e4d1a4f0f0",
"sig": "8159dc4e7cacfdf3e78706f50c3f837729e65c898fb458a2b252d531679064b23939431dcdb38d8a2024697c4672e9ce396947c4f5b6432c2570682cd368580a"
"kid": "sb:issuer:93a2146e3977",
"sig": "8a80fd24f32a5b60a74b598943351409848ff450de5ab736e150db385eebd63e28c7a6f2cc9d7adc716085072d6b3b748c06450c1ef4d667c5e3a4a67f4e3206"
}
}
Loading