@provnai/vex-sdk 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/agent.d.ts +38 -0
- package/dist/agent.js +189 -0
- package/dist/builder.d.ts +68 -0
- package/dist/builder.js +98 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +34 -0
- package/dist/middleware.d.ts +30 -0
- package/dist/middleware.js +44 -0
- package/eslint.config.js +36 -0
- package/jest.config.js +5 -0
- package/package.json +31 -0
- package/src/agent.ts +217 -0
- package/src/builder.ts +117 -0
- package/src/index.ts +33 -0
- package/src/middleware.ts +47 -0
- package/tests/agent.test.ts +50 -0
- package/tests/binary_parity.test.ts +48 -0
- package/tests/parity.test.ts +48 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# VEX SDK for TypeScript (v1.5.0) ð¡ïžjs
|
|
2
|
+
## Cognitive Routing & Silicon-Rooted Evidence
|
|
3
|
+
|
|
4
|
+
Official TypeScript implementation of the VEX Protocol. Designed to wrap agent tool-calls in a cryptographically verifiable envelope.
|
|
5
|
+
|
|
6
|
+
### Installation
|
|
7
|
+
```bash
|
|
8
|
+
npm install @provnai/vex-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### ð Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { VexAgent, vexMiddleware } from '@provnai/vex-sdk';
|
|
15
|
+
|
|
16
|
+
const agent = new VexAgent({
|
|
17
|
+
identityKey: process.env.VEX_IDENTITY_KEY!,
|
|
18
|
+
vanguardUrl: 'https://vanguard.provn.ai'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Use with Vercel AI SDK or custom tool loops
|
|
22
|
+
const securedResult = await agent.execute(
|
|
23
|
+
'transfer_funds',
|
|
24
|
+
{ amount: 1000, currency: 'USD' },
|
|
25
|
+
'Authorize emergency liquidation'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
console.log(`VEX Outcome: ${securedResult.outcome}`); // ALLOWED
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
**Note:** This SDK natively integrates with `@provncloud/sdk` for hardware-rooted trust.
|
|
33
|
+
ð¡ïžâð
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 ProvnAI
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export interface VexConfig {
|
|
17
|
+
identityKey: string;
|
|
18
|
+
vanguardUrl: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class VexAgent {
|
|
21
|
+
private config;
|
|
22
|
+
private sdk;
|
|
23
|
+
constructor(config: VexConfig);
|
|
24
|
+
private ensureSDK;
|
|
25
|
+
/**
|
|
26
|
+
* Executes a tool via the VEX verifiable execution loop.
|
|
27
|
+
*/
|
|
28
|
+
execute(toolName: string, params: Record<string, any>, intentContext?: string): Promise<any>;
|
|
29
|
+
/**
|
|
30
|
+
* Manually construct a signed Evidence Capsule without dispatching it.
|
|
31
|
+
*/
|
|
32
|
+
buildCapsule(toolName: string, params: Record<string, any>, intentContext?: string): Promise<any>;
|
|
33
|
+
/**
|
|
34
|
+
* Serializes the Evidence Capsule into the v0x03 Binary Wire format.
|
|
35
|
+
*/
|
|
36
|
+
toBinary(capsule: any): Buffer;
|
|
37
|
+
private hashObject;
|
|
38
|
+
}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 ProvnAI
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.VexAgent = void 0;
|
|
22
|
+
const axios_1 = __importDefault(require("axios"));
|
|
23
|
+
const builder_1 = require("./builder");
|
|
24
|
+
const sdk_1 = require("@provncloud/sdk");
|
|
25
|
+
class VexAgent {
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.sdk = null;
|
|
28
|
+
this.config = config;
|
|
29
|
+
}
|
|
30
|
+
async ensureSDK() {
|
|
31
|
+
if (!this.sdk) {
|
|
32
|
+
await (0, sdk_1.init)();
|
|
33
|
+
this.sdk = new sdk_1.ProvnSDK();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Executes a tool via the VEX verifiable execution loop.
|
|
38
|
+
*/
|
|
39
|
+
async execute(toolName, params, intentContext) {
|
|
40
|
+
await this.ensureSDK();
|
|
41
|
+
// 1. Build Capsule
|
|
42
|
+
const capsuleData = await this.buildCapsule(toolName, params, intentContext);
|
|
43
|
+
const capsuleId = capsuleData.authority.capsule_id;
|
|
44
|
+
// 2. Dispatch to Vanguard
|
|
45
|
+
try {
|
|
46
|
+
const response = await axios_1.default.post(`${this.config.vanguardUrl}/dispatch`, capsuleData, {
|
|
47
|
+
headers: { 'Content-Type': 'application/json' }
|
|
48
|
+
});
|
|
49
|
+
let result = response.data;
|
|
50
|
+
// 3. Handle AEM (ESCALATE Loop)
|
|
51
|
+
let attempts = 0;
|
|
52
|
+
const maxAttempts = 15; // ~30 seconds
|
|
53
|
+
while (result.outcome === 'ESCALATE' && attempts < maxAttempts) {
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
55
|
+
attempts++;
|
|
56
|
+
const pollResp = await axios_1.default.get(`${this.config.vanguardUrl}/capsule/${capsuleId}/status`, {
|
|
57
|
+
headers: { 'Accept': 'application/json' }
|
|
58
|
+
});
|
|
59
|
+
result = pollResp.data;
|
|
60
|
+
}
|
|
61
|
+
if (result.outcome === 'HALT') {
|
|
62
|
+
throw new Error(`VEX Execution HALTED: ${result.reason_code}`);
|
|
63
|
+
}
|
|
64
|
+
if (result.outcome === 'ESCALATE') {
|
|
65
|
+
throw new Error('VEX Escalated resolution timed out.');
|
|
66
|
+
}
|
|
67
|
+
// 4. Store Capability Token
|
|
68
|
+
if (result.capability_token) {
|
|
69
|
+
this.currentToken = result.capability_token;
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('VEX Execution Failed:', error.response?.data || error.message);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Manually construct a signed Evidence Capsule without dispatching it.
|
|
80
|
+
*/
|
|
81
|
+
async buildCapsule(toolName, params, intentContext) {
|
|
82
|
+
await this.ensureSDK();
|
|
83
|
+
// This is a simplified implementation for the SDK.
|
|
84
|
+
// It demonstrates how the builder is leveraged.
|
|
85
|
+
const intent = {
|
|
86
|
+
request_sha256: this.hashObject(params),
|
|
87
|
+
confidence: 1.0,
|
|
88
|
+
capabilities: ['sdk_execution']
|
|
89
|
+
};
|
|
90
|
+
if (intentContext) {
|
|
91
|
+
intent.intent_context = intentContext;
|
|
92
|
+
}
|
|
93
|
+
const authority = {
|
|
94
|
+
capsule_id: require('crypto').randomUUID(),
|
|
95
|
+
outcome: 'ALLOW',
|
|
96
|
+
reason_code: 'SDK_GENERATED',
|
|
97
|
+
trace_root: '00'.repeat(32),
|
|
98
|
+
nonce: Date.now(),
|
|
99
|
+
prev_hash: '00'.repeat(32), // Start of chain
|
|
100
|
+
supervision: {
|
|
101
|
+
branch_completeness: 1.0,
|
|
102
|
+
contradictions: 0,
|
|
103
|
+
confidence: 1.0
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const identity = {
|
|
107
|
+
aid: '00'.repeat(32), // Placeholder for hardware AID
|
|
108
|
+
identity_type: 'software_sim',
|
|
109
|
+
pcrs: { '0': '00'.repeat(32) }
|
|
110
|
+
};
|
|
111
|
+
const witness = {
|
|
112
|
+
chora_node_id: 'local_witness',
|
|
113
|
+
receipt_hash: '00'.repeat(32),
|
|
114
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
115
|
+
};
|
|
116
|
+
const intent_hash = builder_1.VEPBuilder.hashSegment(intent);
|
|
117
|
+
const authority_hash = builder_1.VEPBuilder.hashSegment(authority);
|
|
118
|
+
const identity_hash = builder_1.VEPBuilder.hashSegment(identity);
|
|
119
|
+
const witness_hash = builder_1.VEPBuilder.hashSegment(witness, false); // Minimal scope
|
|
120
|
+
const capsule_root = builder_1.VEPBuilder.calculateCapsuleRoot({
|
|
121
|
+
intent_hash,
|
|
122
|
+
authority_hash,
|
|
123
|
+
identity_hash,
|
|
124
|
+
witness_hash
|
|
125
|
+
});
|
|
126
|
+
// Sign the capsule_root
|
|
127
|
+
const claim = this.sdk.createClaimNow(capsule_root);
|
|
128
|
+
const keyPair = {
|
|
129
|
+
private_key: this.config.identityKey,
|
|
130
|
+
public_key: '00'.repeat(32) // In real use, this comes from the config/key derivation
|
|
131
|
+
};
|
|
132
|
+
this.sdk.setKeypair(keyPair);
|
|
133
|
+
const signedClaim = this.sdk.signClaim(claim);
|
|
134
|
+
const signature = Buffer.from(signedClaim.signature, 'hex');
|
|
135
|
+
return {
|
|
136
|
+
intent,
|
|
137
|
+
authority,
|
|
138
|
+
identity,
|
|
139
|
+
witness,
|
|
140
|
+
intent_hash,
|
|
141
|
+
authority_hash,
|
|
142
|
+
identity_hash,
|
|
143
|
+
witness_hash,
|
|
144
|
+
capsule_root,
|
|
145
|
+
crypto: {
|
|
146
|
+
algo: 'ed25519',
|
|
147
|
+
signature_scope: 'capsule_root',
|
|
148
|
+
signature_b64: signature.toString('base64'),
|
|
149
|
+
signature_raw: signature // Keep raw for binary spec
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Serializes the Evidence Capsule into the v0x03 Binary Wire format.
|
|
155
|
+
*/
|
|
156
|
+
toBinary(capsule) {
|
|
157
|
+
const { canonicalize } = require('json-canonicalize');
|
|
158
|
+
// --- Header (76 Bytes) ---
|
|
159
|
+
const header = Buffer.alloc(76);
|
|
160
|
+
header.write('VEP', 0);
|
|
161
|
+
header.writeUInt8(0x03, 3);
|
|
162
|
+
header.write(capsule.identity.aid.replace(/-/g, ''), 4, 'hex'); // Ensure hex
|
|
163
|
+
header.write(capsule.capsule_root, 36, 'hex');
|
|
164
|
+
header.writeBigUInt64BE(BigInt(capsule.authority.nonce), 68);
|
|
165
|
+
// --- TLV Body ---
|
|
166
|
+
const packTLV = (tag, data) => {
|
|
167
|
+
const tlv = Buffer.alloc(5 + data.length);
|
|
168
|
+
tlv.writeUInt8(tag, 0);
|
|
169
|
+
tlv.writeUInt32BE(data.length, 1);
|
|
170
|
+
data.copy(tlv, 5);
|
|
171
|
+
return tlv;
|
|
172
|
+
};
|
|
173
|
+
const segments = [
|
|
174
|
+
packTLV(0x01, Buffer.from(canonicalize(capsule.intent))),
|
|
175
|
+
packTLV(0x02, Buffer.from(canonicalize(capsule.authority))),
|
|
176
|
+
packTLV(0x03, Buffer.from(canonicalize(capsule.identity))),
|
|
177
|
+
packTLV(0x05, Buffer.from(canonicalize(capsule.witness))),
|
|
178
|
+
packTLV(0x06, capsule.crypto.signature_raw)
|
|
179
|
+
];
|
|
180
|
+
return Buffer.concat([header, ...segments, header]);
|
|
181
|
+
}
|
|
182
|
+
hashObject(obj) {
|
|
183
|
+
const { canonicalize } = require('json-canonicalize');
|
|
184
|
+
const crypto = require('crypto');
|
|
185
|
+
const canonicalJSON = canonicalize(obj);
|
|
186
|
+
return crypto.createHash('sha256').update(canonicalJSON).digest('hex');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
exports.VexAgent = VexAgent;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 ProvnAI
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export interface IntentSegment {
|
|
17
|
+
request_sha256: string;
|
|
18
|
+
confidence: number;
|
|
19
|
+
capabilities: string[];
|
|
20
|
+
magpie_source?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AuthoritySegment {
|
|
23
|
+
capsule_id: string;
|
|
24
|
+
outcome: 'ALLOW' | 'HALT' | 'ESCALATE';
|
|
25
|
+
reason_code: string;
|
|
26
|
+
trace_root: string;
|
|
27
|
+
nonce: number;
|
|
28
|
+
prev_hash?: string;
|
|
29
|
+
supervision?: {
|
|
30
|
+
branch_completeness?: number;
|
|
31
|
+
contradictions?: number;
|
|
32
|
+
confidence?: number;
|
|
33
|
+
};
|
|
34
|
+
gate_sensors?: Record<string, any>;
|
|
35
|
+
}
|
|
36
|
+
export interface IdentitySegment {
|
|
37
|
+
aid: string;
|
|
38
|
+
identity_type: string;
|
|
39
|
+
pcrs: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
export interface WitnessSegment {
|
|
42
|
+
chora_node_id: string;
|
|
43
|
+
receipt_hash: string;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
}
|
|
46
|
+
export interface VexPillars {
|
|
47
|
+
intent: IntentSegment;
|
|
48
|
+
authority: AuthoritySegment;
|
|
49
|
+
identity: IdentitySegment;
|
|
50
|
+
witness: WitnessSegment;
|
|
51
|
+
}
|
|
52
|
+
export declare class VEPBuilder {
|
|
53
|
+
/**
|
|
54
|
+
* Computes the SHA-256 hash of a JCS canonicalized object.
|
|
55
|
+
*/
|
|
56
|
+
static hashSegment(segment: any, inclusive?: boolean): string;
|
|
57
|
+
/**
|
|
58
|
+
* Calculates the capsule_root commitment using a 4-leaf binary Merkle tree
|
|
59
|
+
* with domain separation (0x00 for leaves, 0x01 for internal nodes).
|
|
60
|
+
* Pillar Order: Intent, Authority, Identity, Witness.
|
|
61
|
+
*/
|
|
62
|
+
static calculateCapsuleRoot(hashes: {
|
|
63
|
+
intent_hash: string;
|
|
64
|
+
authority_hash: string;
|
|
65
|
+
identity_hash: string;
|
|
66
|
+
witness_hash: string;
|
|
67
|
+
}): string;
|
|
68
|
+
}
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 ProvnAI
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.VEPBuilder = void 0;
|
|
52
|
+
const crypto = __importStar(require("crypto"));
|
|
53
|
+
const { canonicalize } = require('json-canonicalize');
|
|
54
|
+
class VEPBuilder {
|
|
55
|
+
/**
|
|
56
|
+
* Computes the SHA-256 hash of a JCS canonicalized object.
|
|
57
|
+
*/
|
|
58
|
+
static hashSegment(segment, inclusive = true) {
|
|
59
|
+
let dataToHash = segment;
|
|
60
|
+
if (!inclusive) {
|
|
61
|
+
// Witness pillar uses Minimal scope (Explicit fields only, EXCLUDING receipt_hash)
|
|
62
|
+
const minimal = {
|
|
63
|
+
chora_node_id: segment.chora_node_id,
|
|
64
|
+
timestamp: segment.timestamp
|
|
65
|
+
};
|
|
66
|
+
dataToHash = minimal;
|
|
67
|
+
}
|
|
68
|
+
const canonicalJSON = canonicalize(dataToHash);
|
|
69
|
+
return crypto.createHash('sha256').update(canonicalJSON).digest('hex');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Calculates the capsule_root commitment using a 4-leaf binary Merkle tree
|
|
73
|
+
* with domain separation (0x00 for leaves, 0x01 for internal nodes).
|
|
74
|
+
* Pillar Order: Intent, Authority, Identity, Witness.
|
|
75
|
+
*/
|
|
76
|
+
static calculateCapsuleRoot(hashes) {
|
|
77
|
+
const hashLeaf = (data_hex) => {
|
|
78
|
+
const leaf = Buffer.concat([Buffer.from([0x00]), Buffer.from(data_hex, 'hex')]);
|
|
79
|
+
return crypto.createHash('sha256').update(leaf).digest();
|
|
80
|
+
};
|
|
81
|
+
const hashInternal = (left, right) => {
|
|
82
|
+
const internal = Buffer.concat([Buffer.from([0x01]), left, right]);
|
|
83
|
+
return crypto.createHash('sha256').update(internal).digest();
|
|
84
|
+
};
|
|
85
|
+
// 1. Leaf Hashes
|
|
86
|
+
const hi = hashLeaf(hashes.intent_hash);
|
|
87
|
+
const ha = hashLeaf(hashes.authority_hash);
|
|
88
|
+
const hid = hashLeaf(hashes.identity_hash);
|
|
89
|
+
const hw = hashLeaf(hashes.witness_hash);
|
|
90
|
+
// 2. Layer 1 (Internal Nodes)
|
|
91
|
+
const h12 = hashInternal(hi, ha);
|
|
92
|
+
const h34 = hashInternal(hid, hw);
|
|
93
|
+
// 3. Root
|
|
94
|
+
const rootDigest = hashInternal(h12, h34);
|
|
95
|
+
return rootDigest.toString('hex');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.VEPBuilder = VEPBuilder;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 ProvnAI
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export * from './agent';
|
|
17
|
+
export * from './builder';
|
|
18
|
+
export * from './middleware';
|
|
19
|
+
export type { VexConfig } from './agent';
|
|
20
|
+
export type { IntentSegment, AuthoritySegment, IdentitySegment, WitnessSegment, VexPillars } from './builder';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 ProvnAI
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
29
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
30
|
+
};
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
__exportStar(require("./agent"), exports);
|
|
33
|
+
__exportStar(require("./builder"), exports);
|
|
34
|
+
__exportStar(require("./middleware"), exports);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 ProvnAI
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Vercel AI SDK compatible middleware for VEX-secured tool execution.
|
|
18
|
+
*/
|
|
19
|
+
export declare const vexMiddleware: (config: {
|
|
20
|
+
identityKey: string;
|
|
21
|
+
vanguardUrl: string;
|
|
22
|
+
}) => {
|
|
23
|
+
onToolCall({ toolName, args }: {
|
|
24
|
+
toolName: string;
|
|
25
|
+
args: any;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
status: string;
|
|
28
|
+
vex_root: any;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 ProvnAI
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.vexMiddleware = void 0;
|
|
19
|
+
const agent_1 = require("./agent");
|
|
20
|
+
/**
|
|
21
|
+
* Vercel AI SDK compatible middleware for VEX-secured tool execution.
|
|
22
|
+
*/
|
|
23
|
+
const vexMiddleware = (config) => {
|
|
24
|
+
const agent = new agent_1.VexAgent(config);
|
|
25
|
+
return {
|
|
26
|
+
// This is a pattern used by Vercel AI SDK to intercept tool calls
|
|
27
|
+
async onToolCall({ toolName, args }) {
|
|
28
|
+
console.log(`[VEX] Securing tool call: ${toolName}`);
|
|
29
|
+
try {
|
|
30
|
+
const result = await agent.execute(toolName, args, `Vercel AI SDK forced verification for ${toolName}`);
|
|
31
|
+
// If execute doesn't throw, it means Vanguard/VEX sidecar allowed it
|
|
32
|
+
return {
|
|
33
|
+
status: 'verified',
|
|
34
|
+
vex_root: result.capsule_root
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error(`[VEX] Blocked tool execution: ${toolName}`);
|
|
39
|
+
throw new Error(`VEX Verification Failed: ${toolName} execution not authorized. Reason: ${error instanceof Error ? error.message : String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
exports.vexMiddleware = vexMiddleware;
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const tsParser = require('@typescript-eslint/parser');
|
|
2
|
+
const tsPlugin = require('@typescript-eslint/eslint-plugin');
|
|
3
|
+
const js = require('@eslint/js');
|
|
4
|
+
|
|
5
|
+
module.exports = [
|
|
6
|
+
js.configs.recommended,
|
|
7
|
+
{
|
|
8
|
+
files: ['src/**/*.ts'],
|
|
9
|
+
languageOptions: {
|
|
10
|
+
parser: tsParser,
|
|
11
|
+
globals: {
|
|
12
|
+
node: true,
|
|
13
|
+
jest: true,
|
|
14
|
+
process: true,
|
|
15
|
+
console: true,
|
|
16
|
+
Buffer: true,
|
|
17
|
+
setTimeout: true,
|
|
18
|
+
Math: true,
|
|
19
|
+
require: true,
|
|
20
|
+
module: true
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
plugins: {
|
|
24
|
+
'@typescript-eslint': tsPlugin
|
|
25
|
+
},
|
|
26
|
+
rules: {
|
|
27
|
+
...tsPlugin.configs.recommended.rules,
|
|
28
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
29
|
+
'@typescript-eslint/no-unused-vars': 'warn',
|
|
30
|
+
'@typescript-eslint/no-require-imports': 'off',
|
|
31
|
+
'no-console': 'off',
|
|
32
|
+
'semi': ['error', 'always'],
|
|
33
|
+
'quotes': ['error', 'single']
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
];
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@provnai/vex-sdk",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "High-level routing and cognitive abstraction SDK for the VEX Protocol",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"lint": "eslint src/**/*.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@provncloud/sdk": "^0.3.0",
|
|
14
|
+
"axios": "^1.6.0",
|
|
15
|
+
"json-canonicalize": "^1.0.4"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/jest": "^29.5.0",
|
|
19
|
+
"@types/node": "^20.0.0",
|
|
20
|
+
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
21
|
+
"@typescript-eslint/parser": "^8.57.1",
|
|
22
|
+
"@eslint/js": "^9.39.4",
|
|
23
|
+
"eslint": "^9.11.0",
|
|
24
|
+
"jest": "^29.5.0",
|
|
25
|
+
"ts-jest": "^29.1.0",
|
|
26
|
+
"typescript": "^5.0.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 ProvnAI
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import axios from 'axios';
|
|
18
|
+
import { VEPBuilder, VexPillars } from './builder';
|
|
19
|
+
import { ProvnSDK, init } from '@provncloud/sdk';
|
|
20
|
+
|
|
21
|
+
export interface VexConfig {
|
|
22
|
+
identityKey: string; // Hex or Base64 Ed25519 private key
|
|
23
|
+
vanguardUrl: string; // Target proxy endpoint (McpVanguard/VEX sidecar)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class VexAgent {
|
|
27
|
+
private config: VexConfig;
|
|
28
|
+
private sdk: ProvnSDK | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(config: VexConfig) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async ensureSDK() {
|
|
35
|
+
if (!this.sdk) {
|
|
36
|
+
await init();
|
|
37
|
+
this.sdk = new ProvnSDK();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Executes a tool via the VEX verifiable execution loop.
|
|
43
|
+
*/
|
|
44
|
+
async execute(toolName: string, params: Record<string, any>, intentContext?: string): Promise<any> {
|
|
45
|
+
await this.ensureSDK();
|
|
46
|
+
|
|
47
|
+
// 1. Build Capsule
|
|
48
|
+
const capsuleData = await this.buildCapsule(toolName, params, intentContext);
|
|
49
|
+
const capsuleId = capsuleData.authority.capsule_id;
|
|
50
|
+
|
|
51
|
+
// 2. Dispatch to Vanguard
|
|
52
|
+
try {
|
|
53
|
+
const response = await axios.post(`${this.config.vanguardUrl}/dispatch`, capsuleData, {
|
|
54
|
+
headers: { 'Content-Type': 'application/json' }
|
|
55
|
+
});
|
|
56
|
+
let result = response.data;
|
|
57
|
+
|
|
58
|
+
// 3. Handle AEM (ESCALATE Loop)
|
|
59
|
+
let attempts = 0;
|
|
60
|
+
const maxAttempts = 15; // ~30 seconds
|
|
61
|
+
|
|
62
|
+
while (result.outcome === 'ESCALATE' && attempts < maxAttempts) {
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
64
|
+
attempts++;
|
|
65
|
+
|
|
66
|
+
const pollResp = await axios.get(`${this.config.vanguardUrl}/capsule/${capsuleId}/status`, {
|
|
67
|
+
headers: { 'Accept': 'application/json' }
|
|
68
|
+
});
|
|
69
|
+
result = pollResp.data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (result.outcome === 'HALT') {
|
|
73
|
+
throw new Error(`VEX Execution HALTED: ${result.reason_code}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (result.outcome === 'ESCALATE') {
|
|
77
|
+
throw new Error('VEX Escalated resolution timed out.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. Store Capability Token
|
|
81
|
+
if (result.capability_token) {
|
|
82
|
+
(this as any).currentToken = result.capability_token;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
console.error('VEX Execution Failed:', error.response?.data || error.message);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Manually construct a signed Evidence Capsule without dispatching it.
|
|
94
|
+
*/
|
|
95
|
+
async buildCapsule(toolName: string, params: Record<string, any>, intentContext?: string): Promise<any> {
|
|
96
|
+
await this.ensureSDK();
|
|
97
|
+
// This is a simplified implementation for the SDK.
|
|
98
|
+
// It demonstrates how the builder is leveraged.
|
|
99
|
+
|
|
100
|
+
const intent: any = {
|
|
101
|
+
request_sha256: this.hashObject(params),
|
|
102
|
+
confidence: 1.0,
|
|
103
|
+
capabilities: ['sdk_execution']
|
|
104
|
+
};
|
|
105
|
+
if (intentContext) {
|
|
106
|
+
intent.intent_context = intentContext;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const authority = {
|
|
110
|
+
capsule_id: require('crypto').randomUUID(),
|
|
111
|
+
outcome: 'ALLOW',
|
|
112
|
+
reason_code: 'SDK_GENERATED',
|
|
113
|
+
trace_root: '00'.repeat(32),
|
|
114
|
+
nonce: Date.now(),
|
|
115
|
+
prev_hash: '00'.repeat(32), // Start of chain
|
|
116
|
+
supervision: {
|
|
117
|
+
branch_completeness: 1.0,
|
|
118
|
+
contradictions: 0,
|
|
119
|
+
confidence: 1.0
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const identity = {
|
|
124
|
+
aid: '00'.repeat(32), // Placeholder for hardware AID
|
|
125
|
+
identity_type: 'software_sim',
|
|
126
|
+
pcrs: { '0': '00'.repeat(32) }
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const witness = {
|
|
130
|
+
chora_node_id: 'local_witness',
|
|
131
|
+
receipt_hash: '00'.repeat(32),
|
|
132
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const intent_hash = VEPBuilder.hashSegment(intent);
|
|
136
|
+
const authority_hash = VEPBuilder.hashSegment(authority);
|
|
137
|
+
const identity_hash = VEPBuilder.hashSegment(identity);
|
|
138
|
+
const witness_hash = VEPBuilder.hashSegment(witness, false); // Minimal scope
|
|
139
|
+
|
|
140
|
+
const capsule_root = VEPBuilder.calculateCapsuleRoot({
|
|
141
|
+
intent_hash,
|
|
142
|
+
authority_hash,
|
|
143
|
+
identity_hash,
|
|
144
|
+
witness_hash
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Sign the capsule_root
|
|
148
|
+
const claim = this.sdk!.createClaimNow(capsule_root);
|
|
149
|
+
const keyPair = {
|
|
150
|
+
private_key: this.config.identityKey,
|
|
151
|
+
public_key: '00'.repeat(32) // In real use, this comes from the config/key derivation
|
|
152
|
+
};
|
|
153
|
+
this.sdk!.setKeypair(keyPair);
|
|
154
|
+
const signedClaim = this.sdk!.signClaim(claim);
|
|
155
|
+
const signature = Buffer.from(signedClaim.signature, 'hex');
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
intent,
|
|
159
|
+
authority,
|
|
160
|
+
identity,
|
|
161
|
+
witness,
|
|
162
|
+
intent_hash,
|
|
163
|
+
authority_hash,
|
|
164
|
+
identity_hash,
|
|
165
|
+
witness_hash,
|
|
166
|
+
capsule_root,
|
|
167
|
+
crypto: {
|
|
168
|
+
algo: 'ed25519',
|
|
169
|
+
signature_scope: 'capsule_root',
|
|
170
|
+
signature_b64: signature.toString('base64'),
|
|
171
|
+
signature_raw: signature // Keep raw for binary spec
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Serializes the Evidence Capsule into the v0x03 Binary Wire format.
|
|
178
|
+
*/
|
|
179
|
+
toBinary(capsule: any): Buffer {
|
|
180
|
+
const { canonicalize } = require('json-canonicalize');
|
|
181
|
+
|
|
182
|
+
// --- Header (76 Bytes) ---
|
|
183
|
+
const header = Buffer.alloc(76);
|
|
184
|
+
header.write('VEP', 0);
|
|
185
|
+
header.writeUInt8(0x03, 3);
|
|
186
|
+
header.write(capsule.identity.aid.replace(/-/g, ''), 4, 'hex'); // Ensure hex
|
|
187
|
+
header.write(capsule.capsule_root, 36, 'hex');
|
|
188
|
+
header.writeBigUInt64BE(BigInt(capsule.authority.nonce), 68);
|
|
189
|
+
|
|
190
|
+
// --- TLV Body ---
|
|
191
|
+
const packTLV = (tag: number, data: Buffer) => {
|
|
192
|
+
const tlv = Buffer.alloc(5 + data.length);
|
|
193
|
+
tlv.writeUInt8(tag, 0);
|
|
194
|
+
tlv.writeUInt32BE(data.length, 1);
|
|
195
|
+
data.copy(tlv, 5);
|
|
196
|
+
return tlv;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const segments = [
|
|
200
|
+
packTLV(0x01, Buffer.from(canonicalize(capsule.intent))),
|
|
201
|
+
packTLV(0x02, Buffer.from(canonicalize(capsule.authority))),
|
|
202
|
+
packTLV(0x03, Buffer.from(canonicalize(capsule.identity))),
|
|
203
|
+
packTLV(0x05, Buffer.from(canonicalize(capsule.witness))),
|
|
204
|
+
packTLV(0x06, capsule.crypto.signature_raw)
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
return Buffer.concat([header, ...segments, header]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private hashObject(obj: any): string {
|
|
211
|
+
const { canonicalize } = require('json-canonicalize');
|
|
212
|
+
const crypto = require('crypto');
|
|
213
|
+
const canonicalJSON = canonicalize(obj);
|
|
214
|
+
return crypto.createHash('sha256').update(canonicalJSON).digest('hex');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
package/src/builder.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 ProvnAI
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as crypto from 'crypto';
|
|
18
|
+
const { canonicalize } = require('json-canonicalize');
|
|
19
|
+
|
|
20
|
+
export interface IntentSegment {
|
|
21
|
+
request_sha256: string;
|
|
22
|
+
confidence: number;
|
|
23
|
+
capabilities: string[];
|
|
24
|
+
magpie_source?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AuthoritySegment {
|
|
28
|
+
capsule_id: string;
|
|
29
|
+
outcome: 'ALLOW' | 'HALT' | 'ESCALATE';
|
|
30
|
+
reason_code: string;
|
|
31
|
+
trace_root: string;
|
|
32
|
+
nonce: number;
|
|
33
|
+
prev_hash?: string; // VEX Ledger Link
|
|
34
|
+
supervision?: { // MCS Signals
|
|
35
|
+
branch_completeness?: number;
|
|
36
|
+
contradictions?: number;
|
|
37
|
+
confidence?: number;
|
|
38
|
+
};
|
|
39
|
+
gate_sensors?: Record<string, any>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IdentitySegment {
|
|
43
|
+
aid: string;
|
|
44
|
+
identity_type: string;
|
|
45
|
+
pcrs: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WitnessSegment {
|
|
49
|
+
chora_node_id: string;
|
|
50
|
+
receipt_hash: string;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface VexPillars {
|
|
55
|
+
intent: IntentSegment;
|
|
56
|
+
authority: AuthoritySegment;
|
|
57
|
+
identity: IdentitySegment;
|
|
58
|
+
witness: WitnessSegment;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class VEPBuilder {
|
|
62
|
+
/**
|
|
63
|
+
* Computes the SHA-256 hash of a JCS canonicalized object.
|
|
64
|
+
*/
|
|
65
|
+
static hashSegment(segment: any, inclusive: boolean = true): string {
|
|
66
|
+
let dataToHash = segment;
|
|
67
|
+
|
|
68
|
+
if (!inclusive) {
|
|
69
|
+
// Witness pillar uses Minimal scope (Explicit fields only, EXCLUDING receipt_hash)
|
|
70
|
+
const minimal = {
|
|
71
|
+
chora_node_id: segment.chora_node_id,
|
|
72
|
+
timestamp: segment.timestamp
|
|
73
|
+
};
|
|
74
|
+
dataToHash = minimal;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const canonicalJSON = canonicalize(dataToHash);
|
|
78
|
+
return crypto.createHash('sha256').update(canonicalJSON).digest('hex');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Calculates the capsule_root commitment using a 4-leaf binary Merkle tree
|
|
83
|
+
* with domain separation (0x00 for leaves, 0x01 for internal nodes).
|
|
84
|
+
* Pillar Order: Intent, Authority, Identity, Witness.
|
|
85
|
+
*/
|
|
86
|
+
static calculateCapsuleRoot(hashes: {
|
|
87
|
+
intent_hash: string;
|
|
88
|
+
authority_hash: string;
|
|
89
|
+
identity_hash: string;
|
|
90
|
+
witness_hash: string;
|
|
91
|
+
}): string {
|
|
92
|
+
const hashLeaf = (data_hex: string) => {
|
|
93
|
+
const leaf = Buffer.concat([Buffer.from([0x00]), Buffer.from(data_hex, 'hex')]);
|
|
94
|
+
return crypto.createHash('sha256').update(leaf).digest();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const hashInternal = (left: Buffer, right: Buffer) => {
|
|
98
|
+
const internal = Buffer.concat([Buffer.from([0x01]), left, right]);
|
|
99
|
+
return crypto.createHash('sha256').update(internal).digest();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// 1. Leaf Hashes
|
|
103
|
+
const hi = hashLeaf(hashes.intent_hash);
|
|
104
|
+
const ha = hashLeaf(hashes.authority_hash);
|
|
105
|
+
const hid = hashLeaf(hashes.identity_hash);
|
|
106
|
+
const hw = hashLeaf(hashes.witness_hash);
|
|
107
|
+
|
|
108
|
+
// 2. Layer 1 (Internal Nodes)
|
|
109
|
+
const h12 = hashInternal(hi, ha);
|
|
110
|
+
const h34 = hashInternal(hid, hw);
|
|
111
|
+
|
|
112
|
+
// 3. Root
|
|
113
|
+
const rootDigest = hashInternal(h12, h34);
|
|
114
|
+
return rootDigest.toString('hex');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 ProvnAI
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { VexAgent } from './agent';
|
|
18
|
+
import { VEPBuilder } from './builder';
|
|
19
|
+
|
|
20
|
+
export * from './agent';
|
|
21
|
+
export * from './builder';
|
|
22
|
+
export * from './middleware';
|
|
23
|
+
|
|
24
|
+
// Explicit type exports for better IDE support
|
|
25
|
+
export type { VexConfig } from './agent';
|
|
26
|
+
export type {
|
|
27
|
+
IntentSegment,
|
|
28
|
+
AuthoritySegment,
|
|
29
|
+
IdentitySegment,
|
|
30
|
+
WitnessSegment,
|
|
31
|
+
VexPillars
|
|
32
|
+
} from './builder';
|
|
33
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 ProvnAI
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { VexAgent } from './agent';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Vercel AI SDK compatible middleware for VEX-secured tool execution.
|
|
21
|
+
*/
|
|
22
|
+
export const vexMiddleware = (config: { identityKey: string, vanguardUrl: string }) => {
|
|
23
|
+
const agent = new VexAgent(config);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
// This is a pattern used by Vercel AI SDK to intercept tool calls
|
|
27
|
+
async onToolCall({ toolName, args }: { toolName: string, args: any }) {
|
|
28
|
+
console.log(`[VEX] Securing tool call: ${toolName}`);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const result = await agent.execute(toolName, args, `Vercel AI SDK forced verification for ${toolName}`);
|
|
32
|
+
|
|
33
|
+
// If execute doesn't throw, it means Vanguard/VEX sidecar allowed it
|
|
34
|
+
return {
|
|
35
|
+
status: 'verified',
|
|
36
|
+
vex_root: result.capsule_root
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`[VEX] Blocked tool execution: ${toolName}`);
|
|
40
|
+
throw new Error(
|
|
41
|
+
`VEX Verification Failed: ${toolName} execution not authorized. Reason: ${error instanceof Error ? error.message : String(error)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { VexAgent, VexConfig } from '../src/agent';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
jest.mock('axios');
|
|
5
|
+
jest.mock('@provncloud/sdk', () => ({
|
|
6
|
+
ProvnSDK: jest.fn().mockImplementation(() => ({
|
|
7
|
+
generateKeypair: jest.fn(),
|
|
8
|
+
setKeypair: jest.fn(),
|
|
9
|
+
createClaimNow: jest.fn().mockReturnValue({ data: 'root', timestamp: 123 }),
|
|
10
|
+
signClaim: jest.fn().mockReturnValue({ signature: '66616b655f7369675f686578' })
|
|
11
|
+
})),
|
|
12
|
+
init: jest.fn().mockResolvedValue(undefined)
|
|
13
|
+
}));
|
|
14
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
15
|
+
|
|
16
|
+
describe('VexAgent', () => {
|
|
17
|
+
const config: VexConfig = {
|
|
18
|
+
identityKey: 'fake_key',
|
|
19
|
+
vanguardUrl: 'http://localhost:3000'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('execute() constructs a capsule and sends it via POST', async () => {
|
|
23
|
+
const agent = new VexAgent(config);
|
|
24
|
+
|
|
25
|
+
mockedAxios.post.mockResolvedValue({ data: { status: 'verified' } });
|
|
26
|
+
|
|
27
|
+
const result = await agent.execute('test_tool', { foo: 'bar' });
|
|
28
|
+
|
|
29
|
+
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
30
|
+
`${config.vanguardUrl}/dispatch`,
|
|
31
|
+
expect.objectContaining({
|
|
32
|
+
intent_hash: expect.any(String),
|
|
33
|
+
capsule_root: expect.any(String)
|
|
34
|
+
}),
|
|
35
|
+
expect.any(Object)
|
|
36
|
+
);
|
|
37
|
+
expect(result.status).toBe('verified');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('buildCapsule() produces expected structure', async () => {
|
|
41
|
+
const agent = new VexAgent(config);
|
|
42
|
+
const capsule = await agent.buildCapsule('test_tool', { foo: 'bar' });
|
|
43
|
+
|
|
44
|
+
expect(capsule).toHaveProperty('intent');
|
|
45
|
+
expect(capsule).toHaveProperty('authority');
|
|
46
|
+
expect(capsule).toHaveProperty('identity');
|
|
47
|
+
expect(capsule).toHaveProperty('witness');
|
|
48
|
+
expect(capsule).toHaveProperty('capsule_root');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { VexAgent } from '../src/agent';
|
|
2
|
+
|
|
3
|
+
jest.mock('@provncloud/sdk', () => ({
|
|
4
|
+
ProvnSDK: jest.fn().mockImplementation(() => ({
|
|
5
|
+
generateKeypair: jest.fn(),
|
|
6
|
+
setKeypair: jest.fn(),
|
|
7
|
+
createClaimNow: jest.fn().mockReturnValue({ data: 'root', timestamp: 123 }),
|
|
8
|
+
signClaim: jest.fn().mockReturnValue({ signature: '00'.repeat(64) })
|
|
9
|
+
})),
|
|
10
|
+
init: jest.fn().mockResolvedValue(undefined)
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('Binary Parity', () => {
|
|
14
|
+
test('produces deterministic binary hex', async () => {
|
|
15
|
+
const config = {
|
|
16
|
+
identityKey: '0'.repeat(64),
|
|
17
|
+
vanguardUrl: 'http://localhost:3000'
|
|
18
|
+
};
|
|
19
|
+
const agent = new VexAgent(config as any);
|
|
20
|
+
|
|
21
|
+
// Mock Date.now and crypto.randomUUID
|
|
22
|
+
const fixedNow = 1710500000000;
|
|
23
|
+
jest.spyOn(Date, 'now').mockReturnValue(fixedNow);
|
|
24
|
+
|
|
25
|
+
const crypto = require('crypto');
|
|
26
|
+
jest.spyOn(crypto, 'randomUUID').mockReturnValue('0'.repeat(64));
|
|
27
|
+
|
|
28
|
+
// Mock SDK signing
|
|
29
|
+
const mockSDK = {
|
|
30
|
+
createClaimNow: jest.fn().mockReturnValue({}),
|
|
31
|
+
setKeypair: jest.fn(),
|
|
32
|
+
signClaim: jest.fn().mockReturnValue({ signature: '00'.repeat(64) })
|
|
33
|
+
};
|
|
34
|
+
(agent as any).sdk = mockSDK;
|
|
35
|
+
(agent as any).ensureSDK = jest.fn().mockResolvedValue(undefined);
|
|
36
|
+
|
|
37
|
+
const capsule = await agent.buildCapsule('test_tool', { foo: 'bar' });
|
|
38
|
+
const binary = agent.toBinary(capsule);
|
|
39
|
+
|
|
40
|
+
require('fs').writeFileSync('../python/ts_payload.hex', binary.toString('hex'));
|
|
41
|
+
expect(binary.subarray(0, 4).toString()).toBe('VEP\x03');
|
|
42
|
+
// Forensic Footer Check
|
|
43
|
+
const header = binary.subarray(0, 76);
|
|
44
|
+
const footer = binary.subarray(binary.length - 76);
|
|
45
|
+
expect(footer.equals(header)).toBe(true);
|
|
46
|
+
expect(binary.length).toBeGreaterThan(152);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { VEPBuilder } from '../src/builder';
|
|
2
|
+
|
|
3
|
+
describe('VEX SDK Parity Verification', () => {
|
|
4
|
+
test('calculateCapsuleRoot matches Rust test vector (v1.5.0 Merkle Shift)', () => {
|
|
5
|
+
// Test hashes provided in COWORKER_HANDOFF.md.resolved
|
|
6
|
+
const parityHashes = {
|
|
7
|
+
authority_hash: "6fac0de31355fc1dfe36eee1e0c226f7cc36dd58eaad0aca0c2d3873b4784d35",
|
|
8
|
+
identity_hash: "7869bae0249b33e09b881a0b44faba6ee3f4bab7edcc2aa5a5e9290e2563c828",
|
|
9
|
+
intent_hash: "e02504ea88bd9f05a744cd8a462a114dc2045eb7210ea8c6f5ff2679663c92cb",
|
|
10
|
+
witness_hash: "174dfb80917cca8a8d4760b82656e78df0778cb3aadd60b51cd018b3313d5733"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Recalculated for 4-leaf Merkle Tree with 0x00/0x01 domain separation (Definitive v1.5.0)
|
|
14
|
+
const expectedRoot = "8acf6d45aedaf61c61142ea8f9f7a89bc90994532313f20fcc1493a95e36d405";
|
|
15
|
+
|
|
16
|
+
const calculatedRoot = VEPBuilder.calculateCapsuleRoot(parityHashes);
|
|
17
|
+
expect(calculatedRoot).toBe(expectedRoot);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('calculateCapsuleRoot matches Rust test vector (Empty Tree)', () => {
|
|
21
|
+
const hashes = {
|
|
22
|
+
intent_hash: '00'.repeat(32),
|
|
23
|
+
authority_hash: '00'.repeat(32),
|
|
24
|
+
identity_hash: '00'.repeat(32),
|
|
25
|
+
witness_hash: '00'.repeat(32)
|
|
26
|
+
};
|
|
27
|
+
const root = VEPBuilder.calculateCapsuleRoot(hashes);
|
|
28
|
+
expect(root).toBe('b46fd516fa6c7dcddd52ac2be2a014d8a8de4eaa059f79ccfcff4b8afc4e7ddc');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('hashSegment matches expected output (Verification of JCS + SHA256)', () => {
|
|
32
|
+
// Intent Pillar (Inclusive)
|
|
33
|
+
const intent = {
|
|
34
|
+
request_sha256: "e02504ea88bd9f05a744cd8a462a114dc2045eb7210ea8c6f5ff2679663c92cb",
|
|
35
|
+
confidence: 0.95,
|
|
36
|
+
capabilities: ["filesystem", "network"]
|
|
37
|
+
};
|
|
38
|
+
// This is a sanity check that JCS stable-sorting works
|
|
39
|
+
const hash1 = VEPBuilder.hashSegment(intent);
|
|
40
|
+
const hash2 = VEPBuilder.hashSegment({
|
|
41
|
+
capabilities: ["filesystem", "network"],
|
|
42
|
+
confidence: 0.95,
|
|
43
|
+
request_sha256: "e02504ea88bd9f05a744cd8a462a114dc2045eb7210ea8c6f5ff2679663c92cb"
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(hash1).toBe(hash2);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"moduleResolution": "node"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
|
15
|
+
}
|