@provnai/vex-sdk 1.5.0 → 1.6.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 +1 -1
- package/dist/agent.d.ts +23 -3
- package/dist/agent.js +118 -5
- package/dist/builder.d.ts +25 -2
- package/dist/middleware.d.ts +2 -2
- package/package.json +34 -31
- package/src/agent.ts +127 -15
- package/src/builder.ts +26 -2
- package/src/index.ts +0 -2
- package/src/middleware.ts +1 -1
- package/tests/agent.test.ts +7 -3
- package/tests/binary_parity.test.ts +8 -0
- package/tests/parity.test.ts +17 -13
package/README.md
CHANGED
package/dist/agent.d.ts
CHANGED
|
@@ -13,26 +13,46 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import { VexCapsule } from './builder';
|
|
16
17
|
export interface VexConfig {
|
|
17
18
|
identityKey: string;
|
|
18
19
|
vanguardUrl: string;
|
|
20
|
+
aid?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface VexResult {
|
|
23
|
+
status: string;
|
|
24
|
+
outcome: 'ALLOW' | 'HALT' | 'ESCALATE';
|
|
25
|
+
reason_code?: string;
|
|
26
|
+
capsule_root: string;
|
|
27
|
+
capability_token?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
19
29
|
}
|
|
20
30
|
export declare class VexAgent {
|
|
21
31
|
private config;
|
|
22
32
|
private sdk;
|
|
33
|
+
private currentToken?;
|
|
23
34
|
constructor(config: VexConfig);
|
|
24
35
|
private ensureSDK;
|
|
36
|
+
/**
|
|
37
|
+
* Retrieves the Gate's public key for HPKE encryption.
|
|
38
|
+
*/
|
|
39
|
+
fetchPublicKey(): Promise<string>;
|
|
25
40
|
/**
|
|
26
41
|
* Executes a tool via the VEX verifiable execution loop.
|
|
27
42
|
*/
|
|
28
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Locally verifies a VEX Continuation Token (v3) against the Gate's public key.
|
|
45
|
+
* Ensures the token was signed by the authoritative Gate and binds to the current capsule.
|
|
46
|
+
*/
|
|
47
|
+
verifyToken(tokenBase64: string, expectedCapsuleRoot?: string): Promise<boolean>;
|
|
48
|
+
execute(toolName: string, params: Record<string, unknown>, intentContext?: string): Promise<VexResult>;
|
|
29
49
|
/**
|
|
30
50
|
* Manually construct a signed Evidence Capsule without dispatching it.
|
|
31
51
|
*/
|
|
32
|
-
buildCapsule(toolName: string, params: Record<string,
|
|
52
|
+
buildCapsule(toolName: string, params: Record<string, unknown>, intentContext?: string): Promise<VexCapsule>;
|
|
33
53
|
/**
|
|
34
54
|
* Serializes the Evidence Capsule into the v0x03 Binary Wire format.
|
|
35
55
|
*/
|
|
36
|
-
toBinary(capsule:
|
|
56
|
+
toBinary(capsule: VexCapsule): Buffer;
|
|
37
57
|
private hashObject;
|
|
38
58
|
}
|
package/dist/agent.js
CHANGED
|
@@ -14,14 +14,52 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
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
|
+
})();
|
|
17
50
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
52
|
};
|
|
20
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
54
|
exports.VexAgent = void 0;
|
|
55
|
+
const crypto = __importStar(require("crypto"));
|
|
56
|
+
const { canonicalize } = require('json-canonicalize');
|
|
22
57
|
const axios_1 = __importDefault(require("axios"));
|
|
23
58
|
const builder_1 = require("./builder");
|
|
24
59
|
const sdk_1 = require("@provncloud/sdk");
|
|
60
|
+
const { CipherSuite } = require('hpke-js');
|
|
61
|
+
const { DhkemX25519HkdfSha256, HkdfSha256 } = require('@hpke/dhkem-x25519');
|
|
62
|
+
const { Aes128Gcm } = require('@hpke/core');
|
|
25
63
|
class VexAgent {
|
|
26
64
|
constructor(config) {
|
|
27
65
|
this.sdk = null;
|
|
@@ -33,9 +71,53 @@ class VexAgent {
|
|
|
33
71
|
this.sdk = new sdk_1.ProvnSDK();
|
|
34
72
|
}
|
|
35
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Retrieves the Gate's public key for HPKE encryption.
|
|
76
|
+
*/
|
|
77
|
+
async fetchPublicKey() {
|
|
78
|
+
const response = await axios_1.default.get(`${this.config.vanguardUrl}/public_key`);
|
|
79
|
+
return response.data.public_key;
|
|
80
|
+
}
|
|
36
81
|
/**
|
|
37
82
|
* Executes a tool via the VEX verifiable execution loop.
|
|
38
83
|
*/
|
|
84
|
+
/**
|
|
85
|
+
* Locally verifies a VEX Continuation Token (v3) against the Gate's public key.
|
|
86
|
+
* Ensures the token was signed by the authoritative Gate and binds to the current capsule.
|
|
87
|
+
*/
|
|
88
|
+
async verifyToken(tokenBase64, expectedCapsuleRoot) {
|
|
89
|
+
const { canonicalize } = require('json-canonicalize');
|
|
90
|
+
const crypto = require('crypto');
|
|
91
|
+
try {
|
|
92
|
+
const token = JSON.parse(Buffer.from(tokenBase64, 'base64').toString());
|
|
93
|
+
const gatePkBase64 = await this.fetchPublicKey();
|
|
94
|
+
const gatePublicKey = crypto.createPublicKey({
|
|
95
|
+
key: Buffer.from(gatePkBase64, 'base64'),
|
|
96
|
+
format: 'der',
|
|
97
|
+
type: 'spki', // Ed25519 in SPKI format
|
|
98
|
+
});
|
|
99
|
+
// 1. Re-hash the payload (JCS)
|
|
100
|
+
const payloadHash = crypto.createHash('sha256')
|
|
101
|
+
.update(canonicalize(token.payload))
|
|
102
|
+
.digest();
|
|
103
|
+
// 2. Verify signature
|
|
104
|
+
const isSignatureValid = crypto.verify(null, payloadHash, gatePublicKey, Buffer.from(token.signature, 'hex'));
|
|
105
|
+
if (!isSignatureValid) {
|
|
106
|
+
console.error('VEX Token Verification Failed: Invalid Signature');
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
// 3. Bind to capsule root
|
|
110
|
+
if (expectedCapsuleRoot && token.payload.source_capsule_root !== expectedCapsuleRoot) {
|
|
111
|
+
console.error('VEX Token Verification Failed: Root Mismatch');
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('VEX Token Verification Error:', error);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
39
121
|
async execute(toolName, params, intentContext) {
|
|
40
122
|
await this.ensureSDK();
|
|
41
123
|
// 1. Build Capsule
|
|
@@ -71,7 +153,8 @@ class VexAgent {
|
|
|
71
153
|
return result;
|
|
72
154
|
}
|
|
73
155
|
catch (error) {
|
|
74
|
-
|
|
156
|
+
const err = error;
|
|
157
|
+
console.error('VEX Execution Failed:', err.response?.data || err.message);
|
|
75
158
|
throw error;
|
|
76
159
|
}
|
|
77
160
|
}
|
|
@@ -83,7 +166,10 @@ class VexAgent {
|
|
|
83
166
|
// This is a simplified implementation for the SDK.
|
|
84
167
|
// It demonstrates how the builder is leveraged.
|
|
85
168
|
const intent = {
|
|
169
|
+
schema: 'vex/intent/v3',
|
|
170
|
+
aid: this.config.aid || '00'.repeat(32),
|
|
86
171
|
request_sha256: this.hashObject(params),
|
|
172
|
+
commands: [toolName, params],
|
|
87
173
|
confidence: 1.0,
|
|
88
174
|
capabilities: ['sdk_execution']
|
|
89
175
|
};
|
|
@@ -97,6 +183,7 @@ class VexAgent {
|
|
|
97
183
|
trace_root: '00'.repeat(32),
|
|
98
184
|
nonce: Date.now(),
|
|
99
185
|
prev_hash: '00'.repeat(32), // Start of chain
|
|
186
|
+
binding_status: 'UNBOUND',
|
|
100
187
|
supervision: {
|
|
101
188
|
branch_completeness: 1.0,
|
|
102
189
|
contradictions: 0,
|
|
@@ -104,7 +191,7 @@ class VexAgent {
|
|
|
104
191
|
}
|
|
105
192
|
};
|
|
106
193
|
const identity = {
|
|
107
|
-
aid: '00'.repeat(32),
|
|
194
|
+
aid: this.config.aid || '00'.repeat(32),
|
|
108
195
|
identity_type: 'software_sim',
|
|
109
196
|
pcrs: { '0': '00'.repeat(32) }
|
|
110
197
|
};
|
|
@@ -113,7 +200,35 @@ class VexAgent {
|
|
|
113
200
|
receipt_hash: '00'.repeat(32),
|
|
114
201
|
timestamp: Math.floor(Date.now() / 1000)
|
|
115
202
|
};
|
|
116
|
-
|
|
203
|
+
// --- Phase 2: HPKE Encryption (Optional/v3) ---
|
|
204
|
+
let intent_hash;
|
|
205
|
+
const gatePkBase64 = await this.fetchPublicKey().catch(() => null);
|
|
206
|
+
if (gatePkBase64) {
|
|
207
|
+
const suite = new CipherSuite({
|
|
208
|
+
kem: new DhkemX25519HkdfSha256(),
|
|
209
|
+
kdf: new HkdfSha256(),
|
|
210
|
+
aead: new Aes128Gcm(),
|
|
211
|
+
});
|
|
212
|
+
const recipientPublicKeyRaw = new Uint8Array(Buffer.from(gatePkBase64, 'base64'));
|
|
213
|
+
const recipientKey = await suite.importKey('raw', recipientPublicKeyRaw, true);
|
|
214
|
+
const info = new Uint8Array(Buffer.from('vex/intent/v3'));
|
|
215
|
+
const { enc, ct } = await suite.seal({
|
|
216
|
+
recipientPublicKey: recipientKey,
|
|
217
|
+
info: info
|
|
218
|
+
}, new Uint8Array(Buffer.from(JSON.stringify(intent))));
|
|
219
|
+
// In v1.6.0, the "Intent Pillar" commitment is the hash of the ciphertext
|
|
220
|
+
intent_hash = require('crypto').createHash('sha256').update(Buffer.from(ct)).digest('hex');
|
|
221
|
+
// Add HPKE metadata to the segment so the Gate can decrypt
|
|
222
|
+
intent.hpke = {
|
|
223
|
+
enc: Buffer.from(enc).toString('base64'),
|
|
224
|
+
ciphertext: Buffer.from(ct).toString('base64'),
|
|
225
|
+
schema: 'vex/intent/v3/encrypted'
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Fallback: Standard JCS hash for Transparent intents
|
|
230
|
+
intent_hash = builder_1.VEPBuilder.hashSegment(intent);
|
|
231
|
+
}
|
|
117
232
|
const authority_hash = builder_1.VEPBuilder.hashSegment(authority);
|
|
118
233
|
const identity_hash = builder_1.VEPBuilder.hashSegment(identity);
|
|
119
234
|
const witness_hash = builder_1.VEPBuilder.hashSegment(witness, false); // Minimal scope
|
|
@@ -180,8 +295,6 @@ class VexAgent {
|
|
|
180
295
|
return Buffer.concat([header, ...segments, header]);
|
|
181
296
|
}
|
|
182
297
|
hashObject(obj) {
|
|
183
|
-
const { canonicalize } = require('json-canonicalize');
|
|
184
|
-
const crypto = require('crypto');
|
|
185
298
|
const canonicalJSON = canonicalize(obj);
|
|
186
299
|
return crypto.createHash('sha256').update(canonicalJSON).digest('hex');
|
|
187
300
|
}
|
package/dist/builder.d.ts
CHANGED
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
export interface IntentSegment {
|
|
17
|
+
schema: string;
|
|
18
|
+
aid: string;
|
|
17
19
|
request_sha256: string;
|
|
20
|
+
commands: unknown[];
|
|
18
21
|
confidence: number;
|
|
19
22
|
capabilities: string[];
|
|
20
23
|
magpie_source?: string;
|
|
24
|
+
intent_context?: string;
|
|
25
|
+
[key: string]: unknown;
|
|
21
26
|
}
|
|
22
27
|
export interface AuthoritySegment {
|
|
23
28
|
capsule_id: string;
|
|
@@ -26,22 +31,27 @@ export interface AuthoritySegment {
|
|
|
26
31
|
trace_root: string;
|
|
27
32
|
nonce: number;
|
|
28
33
|
prev_hash?: string;
|
|
34
|
+
binding_status: string;
|
|
35
|
+
continuation_token?: string;
|
|
29
36
|
supervision?: {
|
|
30
37
|
branch_completeness?: number;
|
|
31
38
|
contradictions?: number;
|
|
32
39
|
confidence?: number;
|
|
33
40
|
};
|
|
34
|
-
gate_sensors?: Record<string,
|
|
41
|
+
gate_sensors?: Record<string, unknown>;
|
|
42
|
+
[key: string]: unknown;
|
|
35
43
|
}
|
|
36
44
|
export interface IdentitySegment {
|
|
37
45
|
aid: string;
|
|
38
46
|
identity_type: string;
|
|
39
47
|
pcrs: Record<string, string>;
|
|
48
|
+
[key: string]: unknown;
|
|
40
49
|
}
|
|
41
50
|
export interface WitnessSegment {
|
|
42
51
|
chora_node_id: string;
|
|
43
52
|
receipt_hash: string;
|
|
44
53
|
timestamp: number;
|
|
54
|
+
[key: string]: unknown;
|
|
45
55
|
}
|
|
46
56
|
export interface VexPillars {
|
|
47
57
|
intent: IntentSegment;
|
|
@@ -49,11 +59,24 @@ export interface VexPillars {
|
|
|
49
59
|
identity: IdentitySegment;
|
|
50
60
|
witness: WitnessSegment;
|
|
51
61
|
}
|
|
62
|
+
export interface VexCapsule extends VexPillars {
|
|
63
|
+
intent_hash: string;
|
|
64
|
+
authority_hash: string;
|
|
65
|
+
identity_hash: string;
|
|
66
|
+
witness_hash: string;
|
|
67
|
+
capsule_root: string;
|
|
68
|
+
crypto: {
|
|
69
|
+
algo: string;
|
|
70
|
+
signature_scope: string;
|
|
71
|
+
signature_b64: string;
|
|
72
|
+
signature_raw: Buffer;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
52
75
|
export declare class VEPBuilder {
|
|
53
76
|
/**
|
|
54
77
|
* Computes the SHA-256 hash of a JCS canonicalized object.
|
|
55
78
|
*/
|
|
56
|
-
static hashSegment(segment:
|
|
79
|
+
static hashSegment(segment: Record<string, unknown>, inclusive?: boolean): string;
|
|
57
80
|
/**
|
|
58
81
|
* Calculates the capsule_root commitment using a 4-leaf binary Merkle tree
|
|
59
82
|
* with domain separation (0x00 for leaves, 0x01 for internal nodes).
|
package/dist/middleware.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@provnai/vex-sdk",
|
|
3
|
-
"version": "1.
|
|
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
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"@
|
|
22
|
-
"@
|
|
23
|
-
"eslint": "^
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
}
|
|
31
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@provnai/vex-sdk",
|
|
3
|
+
"version": "1.6.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
|
+
"hpke-js": "^1.2.0",
|
|
15
|
+
"@hpke/core": "^1.2.7",
|
|
16
|
+
"@hpke/dhkem-x25519": "^1.2.7",
|
|
17
|
+
"axios": "^1.6.0",
|
|
18
|
+
"json-canonicalize": "^1.0.4"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/jest": "^29.5.0",
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
24
|
+
"@typescript-eslint/parser": "^8.57.1",
|
|
25
|
+
"@eslint/js": "^9.39.4",
|
|
26
|
+
"eslint": "^9.11.0",
|
|
27
|
+
"jest": "^29.5.0",
|
|
28
|
+
"ts-jest": "^29.1.0",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/agent.ts
CHANGED
|
@@ -14,19 +14,36 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import * as crypto from 'crypto';
|
|
18
|
+
const { canonicalize } = require('json-canonicalize');
|
|
17
19
|
import axios from 'axios';
|
|
18
|
-
import { VEPBuilder,
|
|
20
|
+
import { VEPBuilder, VexCapsule, IntentSegment, AuthoritySegment } from './builder';
|
|
19
21
|
import { ProvnSDK, init } from '@provncloud/sdk';
|
|
22
|
+
const { CipherSuite } = require('hpke-js');
|
|
23
|
+
const { DhkemX25519HkdfSha256, HkdfSha256 } = require('@hpke/dhkem-x25519');
|
|
24
|
+
const { Aes128Gcm } = require('@hpke/core');
|
|
20
25
|
|
|
21
26
|
export interface VexConfig {
|
|
22
27
|
identityKey: string; // Hex or Base64 Ed25519 private key
|
|
23
28
|
vanguardUrl: string; // Target proxy endpoint (McpVanguard/VEX sidecar)
|
|
29
|
+
aid?: string; // Agent Identity ID (NEW in v3)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface VexResult {
|
|
33
|
+
status: string;
|
|
34
|
+
outcome: 'ALLOW' | 'HALT' | 'ESCALATE';
|
|
35
|
+
reason_code?: string;
|
|
36
|
+
capsule_root: string;
|
|
37
|
+
capability_token?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
export class VexAgent {
|
|
27
42
|
private config: VexConfig;
|
|
28
43
|
private sdk: ProvnSDK | null = null;
|
|
29
44
|
|
|
45
|
+
private currentToken?: string;
|
|
46
|
+
|
|
30
47
|
constructor(config: VexConfig) {
|
|
31
48
|
this.config = config;
|
|
32
49
|
}
|
|
@@ -38,10 +55,66 @@ export class VexAgent {
|
|
|
38
55
|
}
|
|
39
56
|
}
|
|
40
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Retrieves the Gate's public key for HPKE encryption.
|
|
60
|
+
*/
|
|
61
|
+
async fetchPublicKey(): Promise<string> {
|
|
62
|
+
const response = await axios.get(`${this.config.vanguardUrl}/public_key`);
|
|
63
|
+
return response.data.public_key;
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
/**
|
|
42
67
|
* Executes a tool via the VEX verifiable execution loop.
|
|
43
68
|
*/
|
|
44
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Locally verifies a VEX Continuation Token (v3) against the Gate's public key.
|
|
71
|
+
* Ensures the token was signed by the authoritative Gate and binds to the current capsule.
|
|
72
|
+
*/
|
|
73
|
+
async verifyToken(tokenBase64: string, expectedCapsuleRoot?: string): Promise<boolean> {
|
|
74
|
+
const { canonicalize } = require('json-canonicalize');
|
|
75
|
+
const crypto = require('crypto');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const token = JSON.parse(Buffer.from(tokenBase64, 'base64').toString());
|
|
79
|
+
const gatePkBase64 = await this.fetchPublicKey();
|
|
80
|
+
const gatePublicKey = crypto.createPublicKey({
|
|
81
|
+
key: Buffer.from(gatePkBase64, 'base64'),
|
|
82
|
+
format: 'der',
|
|
83
|
+
type: 'spki', // Ed25519 in SPKI format
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 1. Re-hash the payload (JCS)
|
|
87
|
+
const payloadHash = crypto.createHash('sha256')
|
|
88
|
+
.update(canonicalize(token.payload))
|
|
89
|
+
.digest();
|
|
90
|
+
|
|
91
|
+
// 2. Verify signature
|
|
92
|
+
const isSignatureValid = crypto.verify(
|
|
93
|
+
null,
|
|
94
|
+
payloadHash,
|
|
95
|
+
gatePublicKey,
|
|
96
|
+
Buffer.from(token.signature, 'hex')
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!isSignatureValid) {
|
|
100
|
+
console.error('VEX Token Verification Failed: Invalid Signature');
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 3. Bind to capsule root
|
|
105
|
+
if (expectedCapsuleRoot && token.payload.source_capsule_root !== expectedCapsuleRoot) {
|
|
106
|
+
console.error('VEX Token Verification Failed: Root Mismatch');
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('VEX Token Verification Error:', error);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async execute(toolName: string, params: Record<string, unknown>, intentContext?: string): Promise<VexResult> {
|
|
45
118
|
await this.ensureSDK();
|
|
46
119
|
|
|
47
120
|
// 1. Build Capsule
|
|
@@ -79,12 +152,13 @@ export class VexAgent {
|
|
|
79
152
|
|
|
80
153
|
// 4. Store Capability Token
|
|
81
154
|
if (result.capability_token) {
|
|
82
|
-
|
|
155
|
+
this.currentToken = result.capability_token;
|
|
83
156
|
}
|
|
84
157
|
|
|
85
|
-
return result;
|
|
86
|
-
} catch (error
|
|
87
|
-
|
|
158
|
+
return result as VexResult;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const err = error as { response?: { data: Record<string, unknown> }, message: string };
|
|
161
|
+
console.error('VEX Execution Failed:', err.response?.data || err.message);
|
|
88
162
|
throw error;
|
|
89
163
|
}
|
|
90
164
|
}
|
|
@@ -92,13 +166,16 @@ export class VexAgent {
|
|
|
92
166
|
/**
|
|
93
167
|
* Manually construct a signed Evidence Capsule without dispatching it.
|
|
94
168
|
*/
|
|
95
|
-
async buildCapsule(toolName: string, params: Record<string,
|
|
169
|
+
async buildCapsule(toolName: string, params: Record<string, unknown>, intentContext?: string): Promise<VexCapsule> {
|
|
96
170
|
await this.ensureSDK();
|
|
97
171
|
// This is a simplified implementation for the SDK.
|
|
98
172
|
// It demonstrates how the builder is leveraged.
|
|
99
173
|
|
|
100
|
-
const intent:
|
|
174
|
+
const intent: IntentSegment = {
|
|
175
|
+
schema: 'vex/intent/v3',
|
|
176
|
+
aid: this.config.aid || '00'.repeat(32),
|
|
101
177
|
request_sha256: this.hashObject(params),
|
|
178
|
+
commands: [toolName, params],
|
|
102
179
|
confidence: 1.0,
|
|
103
180
|
capabilities: ['sdk_execution']
|
|
104
181
|
};
|
|
@@ -106,13 +183,14 @@ export class VexAgent {
|
|
|
106
183
|
intent.intent_context = intentContext;
|
|
107
184
|
}
|
|
108
185
|
|
|
109
|
-
const authority = {
|
|
186
|
+
const authority: AuthoritySegment = {
|
|
110
187
|
capsule_id: require('crypto').randomUUID(),
|
|
111
188
|
outcome: 'ALLOW',
|
|
112
189
|
reason_code: 'SDK_GENERATED',
|
|
113
190
|
trace_root: '00'.repeat(32),
|
|
114
191
|
nonce: Date.now(),
|
|
115
192
|
prev_hash: '00'.repeat(32), // Start of chain
|
|
193
|
+
binding_status: 'UNBOUND',
|
|
116
194
|
supervision: {
|
|
117
195
|
branch_completeness: 1.0,
|
|
118
196
|
contradictions: 0,
|
|
@@ -121,7 +199,7 @@ export class VexAgent {
|
|
|
121
199
|
};
|
|
122
200
|
|
|
123
201
|
const identity = {
|
|
124
|
-
aid: '00'.repeat(32),
|
|
202
|
+
aid: this.config.aid || '00'.repeat(32),
|
|
125
203
|
identity_type: 'software_sim',
|
|
126
204
|
pcrs: { '0': '00'.repeat(32) }
|
|
127
205
|
};
|
|
@@ -132,7 +210,43 @@ export class VexAgent {
|
|
|
132
210
|
timestamp: Math.floor(Date.now() / 1000)
|
|
133
211
|
};
|
|
134
212
|
|
|
135
|
-
|
|
213
|
+
// --- Phase 2: HPKE Encryption (Optional/v3) ---
|
|
214
|
+
let intent_hash: string;
|
|
215
|
+
const gatePkBase64 = await this.fetchPublicKey().catch(() => null);
|
|
216
|
+
|
|
217
|
+
if (gatePkBase64) {
|
|
218
|
+
const suite = new CipherSuite({
|
|
219
|
+
kem: new DhkemX25519HkdfSha256(),
|
|
220
|
+
kdf: new HkdfSha256(),
|
|
221
|
+
aead: new Aes128Gcm(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const recipientPublicKeyRaw = new Uint8Array(Buffer.from(gatePkBase64, 'base64'));
|
|
225
|
+
const recipientKey = await suite.importKey('raw', recipientPublicKeyRaw, true);
|
|
226
|
+
const info = new Uint8Array(Buffer.from('vex/intent/v3'));
|
|
227
|
+
|
|
228
|
+
const { enc, ct } = await suite.seal(
|
|
229
|
+
{
|
|
230
|
+
recipientPublicKey: recipientKey,
|
|
231
|
+
info: info
|
|
232
|
+
},
|
|
233
|
+
new Uint8Array(Buffer.from(JSON.stringify(intent)))
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// In v1.6.0, the "Intent Pillar" commitment is the hash of the ciphertext
|
|
237
|
+
intent_hash = require('crypto').createHash('sha256').update(Buffer.from(ct)).digest('hex');
|
|
238
|
+
|
|
239
|
+
// Add HPKE metadata to the segment so the Gate can decrypt
|
|
240
|
+
intent.hpke = {
|
|
241
|
+
enc: Buffer.from(enc).toString('base64'),
|
|
242
|
+
ciphertext: Buffer.from(ct).toString('base64'),
|
|
243
|
+
schema: 'vex/intent/v3/encrypted'
|
|
244
|
+
};
|
|
245
|
+
} else {
|
|
246
|
+
// Fallback: Standard JCS hash for Transparent intents
|
|
247
|
+
intent_hash = VEPBuilder.hashSegment(intent);
|
|
248
|
+
}
|
|
249
|
+
|
|
136
250
|
const authority_hash = VEPBuilder.hashSegment(authority);
|
|
137
251
|
const identity_hash = VEPBuilder.hashSegment(identity);
|
|
138
252
|
const witness_hash = VEPBuilder.hashSegment(witness, false); // Minimal scope
|
|
@@ -176,7 +290,7 @@ export class VexAgent {
|
|
|
176
290
|
/**
|
|
177
291
|
* Serializes the Evidence Capsule into the v0x03 Binary Wire format.
|
|
178
292
|
*/
|
|
179
|
-
toBinary(capsule:
|
|
293
|
+
toBinary(capsule: VexCapsule): Buffer {
|
|
180
294
|
const { canonicalize } = require('json-canonicalize');
|
|
181
295
|
|
|
182
296
|
// --- Header (76 Bytes) ---
|
|
@@ -207,9 +321,7 @@ export class VexAgent {
|
|
|
207
321
|
return Buffer.concat([header, ...segments, header]);
|
|
208
322
|
}
|
|
209
323
|
|
|
210
|
-
private hashObject(obj:
|
|
211
|
-
const { canonicalize } = require('json-canonicalize');
|
|
212
|
-
const crypto = require('crypto');
|
|
324
|
+
private hashObject(obj: Record<string, unknown>): string {
|
|
213
325
|
const canonicalJSON = canonicalize(obj);
|
|
214
326
|
return crypto.createHash('sha256').update(canonicalJSON).digest('hex');
|
|
215
327
|
}
|
package/src/builder.ts
CHANGED
|
@@ -18,10 +18,15 @@ import * as crypto from 'crypto';
|
|
|
18
18
|
const { canonicalize } = require('json-canonicalize');
|
|
19
19
|
|
|
20
20
|
export interface IntentSegment {
|
|
21
|
+
schema: string; // NEW (v3)
|
|
22
|
+
aid: string; // NEW (v3)
|
|
21
23
|
request_sha256: string;
|
|
24
|
+
commands: unknown[]; // NEW (v3)
|
|
22
25
|
confidence: number;
|
|
23
26
|
capabilities: string[];
|
|
24
27
|
magpie_source?: string;
|
|
28
|
+
intent_context?: string; // Context for v3 enforcement
|
|
29
|
+
[key: string]: unknown; // For HPKE and other metadata
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export interface AuthoritySegment {
|
|
@@ -31,24 +36,29 @@ export interface AuthoritySegment {
|
|
|
31
36
|
trace_root: string;
|
|
32
37
|
nonce: number;
|
|
33
38
|
prev_hash?: string; // VEX Ledger Link
|
|
39
|
+
binding_status: string; // NEW (v3)
|
|
40
|
+
continuation_token?: string; // NEW (v3)
|
|
34
41
|
supervision?: { // MCS Signals
|
|
35
42
|
branch_completeness?: number;
|
|
36
43
|
contradictions?: number;
|
|
37
44
|
confidence?: number;
|
|
38
45
|
};
|
|
39
|
-
gate_sensors?: Record<string,
|
|
46
|
+
gate_sensors?: Record<string, unknown>;
|
|
47
|
+
[key: string]: unknown;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export interface IdentitySegment {
|
|
43
51
|
aid: string;
|
|
44
52
|
identity_type: string;
|
|
45
53
|
pcrs: Record<string, string>;
|
|
54
|
+
[key: string]: unknown;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
export interface WitnessSegment {
|
|
49
58
|
chora_node_id: string;
|
|
50
59
|
receipt_hash: string;
|
|
51
60
|
timestamp: number;
|
|
61
|
+
[key: string]: unknown;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export interface VexPillars {
|
|
@@ -58,11 +68,25 @@ export interface VexPillars {
|
|
|
58
68
|
witness: WitnessSegment;
|
|
59
69
|
}
|
|
60
70
|
|
|
71
|
+
export interface VexCapsule extends VexPillars {
|
|
72
|
+
intent_hash: string;
|
|
73
|
+
authority_hash: string;
|
|
74
|
+
identity_hash: string;
|
|
75
|
+
witness_hash: string;
|
|
76
|
+
capsule_root: string;
|
|
77
|
+
crypto: {
|
|
78
|
+
algo: string;
|
|
79
|
+
signature_scope: string;
|
|
80
|
+
signature_b64: string;
|
|
81
|
+
signature_raw: Buffer;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
61
85
|
export class VEPBuilder {
|
|
62
86
|
/**
|
|
63
87
|
* Computes the SHA-256 hash of a JCS canonicalized object.
|
|
64
88
|
*/
|
|
65
|
-
static hashSegment(segment:
|
|
89
|
+
static hashSegment(segment: Record<string, unknown>, inclusive: boolean = true): string {
|
|
66
90
|
let dataToHash = segment;
|
|
67
91
|
|
|
68
92
|
if (!inclusive) {
|
package/src/index.ts
CHANGED
package/src/middleware.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const vexMiddleware = (config: { identityKey: string, vanguardUrl: string
|
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
// This is a pattern used by Vercel AI SDK to intercept tool calls
|
|
27
|
-
async onToolCall({ toolName, args }: { toolName: string, args:
|
|
27
|
+
async onToolCall({ toolName, args }: { toolName: string, args: Record<string, unknown> }) {
|
|
28
28
|
console.log(`[VEX] Securing tool call: ${toolName}`);
|
|
29
29
|
|
|
30
30
|
try {
|
package/tests/agent.test.ts
CHANGED
|
@@ -19,11 +19,15 @@ describe('VexAgent', () => {
|
|
|
19
19
|
vanguardUrl: 'http://localhost:3000'
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Valid 32-byte X25519 public key (non-zero)
|
|
24
|
+
const validPkBase64 = 'uSJQ6R87m8qPk96hQc64b5S67890123456789012344=';
|
|
25
|
+
mockedAxios.get.mockResolvedValue({ data: { public_key: validPkBase64 } });
|
|
26
|
+
mockedAxios.post.mockResolvedValue({ data: { status: 'verified', outcome: 'ALLOW' } });
|
|
27
|
+
});
|
|
28
|
+
|
|
22
29
|
test('execute() constructs a capsule and sends it via POST', async () => {
|
|
23
30
|
const agent = new VexAgent(config);
|
|
24
|
-
|
|
25
|
-
mockedAxios.post.mockResolvedValue({ data: { status: 'verified' } });
|
|
26
|
-
|
|
27
31
|
const result = await agent.execute('test_tool', { foo: 'bar' });
|
|
28
32
|
|
|
29
33
|
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { VexAgent } from '../src/agent';
|
|
2
2
|
|
|
3
|
+
jest.mock('axios');
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
6
|
+
|
|
3
7
|
jest.mock('@provncloud/sdk', () => ({
|
|
4
8
|
ProvnSDK: jest.fn().mockImplementation(() => ({
|
|
5
9
|
generateKeypair: jest.fn(),
|
|
@@ -25,6 +29,10 @@ describe('Binary Parity', () => {
|
|
|
25
29
|
const crypto = require('crypto');
|
|
26
30
|
jest.spyOn(crypto, 'randomUUID').mockReturnValue('0'.repeat(64));
|
|
27
31
|
|
|
32
|
+
// Mock fetchPublicKey to avoid network calls
|
|
33
|
+
const validPkBase64 = 'uSJQ6R87m8qPk96hQc64b5S67890123456789012344=';
|
|
34
|
+
mockedAxios.get.mockResolvedValue({ data: { public_key: validPkBase64 } });
|
|
35
|
+
|
|
28
36
|
// Mock SDK signing
|
|
29
37
|
const mockSDK = {
|
|
30
38
|
createClaimNow: jest.fn().mockReturnValue({}),
|
package/tests/parity.test.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { VEPBuilder } from '../src/builder';
|
|
2
2
|
|
|
3
3
|
describe('VEX SDK Parity Verification', () => {
|
|
4
|
-
test('calculateCapsuleRoot matches Rust test vector (v1.
|
|
5
|
-
// Test hashes provided in COWORKER_HANDOFF.md.resolved
|
|
4
|
+
test('calculateCapsuleRoot matches Rust test vector (v1.6.0 Protocol Alignment)', () => {
|
|
6
5
|
const parityHashes = {
|
|
7
|
-
authority_hash: "
|
|
6
|
+
authority_hash: "1f66eab08c7276b5bd65b6624193eb216159a675e43b85d827de85ec065495c6",
|
|
8
7
|
identity_hash: "7869bae0249b33e09b881a0b44faba6ee3f4bab7edcc2aa5a5e9290e2563c828",
|
|
9
|
-
intent_hash: "
|
|
10
|
-
witness_hash: "
|
|
8
|
+
intent_hash: "ce4041d35af4dd0c00b60a04c80779516178097f7ab7e20fea6da2996dc06446",
|
|
9
|
+
witness_hash: "2aa5ae39acd791e6ae12341b4e82ec16cfcdd2ab4e46a8fb48389dff6217fd42"
|
|
11
10
|
};
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
const expectedRoot = "8acf6d45aedaf61c61142ea8f9f7a89bc90994532313f20fcc1493a95e36d405";
|
|
12
|
+
const expectedRoot = "35ef4684c3168f54f040e0e6a24d5bde35464731e6c32bb34bcc30fbb69c8255";
|
|
15
13
|
|
|
16
14
|
const calculatedRoot = VEPBuilder.calculateCapsuleRoot(parityHashes);
|
|
17
15
|
expect(calculatedRoot).toBe(expectedRoot);
|
|
@@ -31,16 +29,22 @@ describe('VEX SDK Parity Verification', () => {
|
|
|
31
29
|
test('hashSegment matches expected output (Verification of JCS + SHA256)', () => {
|
|
32
30
|
// Intent Pillar (Inclusive)
|
|
33
31
|
const intent = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
schema: "vex/intent/v3",
|
|
33
|
+
aid: "00".repeat(32),
|
|
34
|
+
request_sha256: "ce4041d35af4dd0c00b60a04c80779516178097f7ab7e20fea6da2996dc06446",
|
|
35
|
+
commands: ["test_tool", { foo: "bar" }],
|
|
36
|
+
confidence: 1.0,
|
|
37
|
+
capabilities: ["sdk_execution"]
|
|
37
38
|
};
|
|
38
39
|
// This is a sanity check that JCS stable-sorting works
|
|
39
40
|
const hash1 = VEPBuilder.hashSegment(intent);
|
|
40
41
|
const hash2 = VEPBuilder.hashSegment({
|
|
41
|
-
capabilities: ["
|
|
42
|
-
confidence: 0
|
|
43
|
-
|
|
42
|
+
capabilities: ["sdk_execution"],
|
|
43
|
+
confidence: 1.0,
|
|
44
|
+
commands: ["test_tool", { foo: "bar" }],
|
|
45
|
+
request_sha256: "ce4041d35af4dd0c00b60a04c80779516178097f7ab7e20fea6da2996dc06446",
|
|
46
|
+
aid: "00".repeat(32),
|
|
47
|
+
schema: "vex/intent/v3"
|
|
44
48
|
});
|
|
45
49
|
|
|
46
50
|
expect(hash1).toBe(hash2);
|