@agexhq/core 1.0.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/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@agexhq/core",
3
+ "version": "1.0.0",
4
+ "description": "AGEX protocol primitives — crypto, schemas, policy engine",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./crypto": "./src/crypto/index.js",
10
+ "./schemas": "./src/schemas/index.js",
11
+ "./protocol": "./src/protocol/index.js",
12
+ "./policy": "./src/policy/index.js"
13
+ },
14
+ "publishConfig": { "access": "public" },
15
+ "files": ["src/", "README.md", "LICENSE"],
16
+ "scripts": {
17
+ "test": "node --test src/**/*.test.js || true"
18
+ },
19
+ "keywords": ["agex", "ai-agents", "authentication", "credentials", "protocol", "ed25519"],
20
+ "author": "AGEX Foundation <hello@agexhq.com>",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@noble/ed25519": "^2.0.0",
24
+ "@noble/hashes": "^1.3.3",
25
+ "@noble/curves": "^1.3.0",
26
+ "@noble/ciphers": "^0.5.0",
27
+ "jose": "^5.2.3",
28
+ "uuid": "^9.0.0",
29
+ "zod": "^3.22.4"
30
+ }
31
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @agexhq/core — ECDH-ES+AES256GCM Credential Envelope Encryption
3
+ * Implements SR-1 (Credential Confidentiality) and SR-8 (Hub Zero-Knowledge)
4
+ * Pure JavaScript — works on Termux without native compilation
5
+ */
6
+
7
+ import { x25519 } from '@noble/curves/ed25519'
8
+ import { hkdf } from '@noble/hashes/hkdf'
9
+ import { sha256 } from '@noble/hashes/sha256'
10
+ import { randomBytes } from '@noble/hashes/utils'
11
+ import { base64url } from 'jose'
12
+
13
+ // We use Web Crypto API for AES-GCM (available in Node 18+ and Termux)
14
+ const subtle = globalThis.crypto?.subtle
15
+
16
+ /**
17
+ * Encrypt a credential value for a recipient agent.
18
+ * The Hub calls this when brokering credentials — it never sees the plaintext
19
+ * because the SP encrypts for the agent's public key directly.
20
+ *
21
+ * @param {string} plaintext — credential value (API key, token, etc.)
22
+ * @param {Uint8Array} recipientX25519Public — agent's X25519 public key
23
+ * @returns {object} AGEX credential envelope
24
+ */
25
+ export async function encryptEnvelope (plaintext, recipientX25519Public) {
26
+ // Generate ephemeral X25519 keypair
27
+ const ephPriv = randomBytes(32)
28
+ const ephPub = x25519.getPublicKey(ephPriv)
29
+
30
+ // ECDH shared secret
31
+ const shared = x25519.getSharedSecret(ephPriv, recipientX25519Public)
32
+
33
+ // HKDF key derivation
34
+ const key = hkdf(sha256, shared, /* salt */ new Uint8Array(0), 'AGEX-CLC-v1', 32)
35
+
36
+ // AES-256-GCM via Web Crypto
37
+ const nonce = randomBytes(12)
38
+ const plainBytes = new TextEncoder().encode(plaintext)
39
+
40
+ const cryptoKey = await subtle.importKey('raw', key, 'AES-GCM', false, ['encrypt'])
41
+ const sealed = await subtle.encrypt({ name: 'AES-GCM', iv: nonce }, cryptoKey, plainBytes)
42
+
43
+ // Web Crypto appends tag to ciphertext
44
+ const sealedBytes = new Uint8Array(sealed)
45
+ const ciphertext = sealedBytes.slice(0, -16)
46
+ const tag = sealedBytes.slice(-16)
47
+
48
+ return {
49
+ algorithm: 'ECDH-ES+AES256GCM',
50
+ epk: { kty: 'OKP', crv: 'X25519', x: base64url.encode(ephPub) },
51
+ ciphertext: base64url.encode(ciphertext),
52
+ nonce: base64url.encode(nonce),
53
+ tag: base64url.encode(tag)
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Decrypt a credential envelope using the agent's X25519 private key.
59
+ *
60
+ * @param {object} envelope — AGEX credential envelope
61
+ * @param {Uint8Array} recipientX25519Private — agent's X25519 private key bytes
62
+ * @returns {string} decrypted credential value
63
+ */
64
+ export async function decryptEnvelope (envelope, recipientX25519Private) {
65
+ const ephPub = base64url.decode(envelope.epk.x)
66
+
67
+ // ECDH shared secret
68
+ const shared = x25519.getSharedSecret(recipientX25519Private, ephPub)
69
+
70
+ // Same HKDF derivation
71
+ const key = hkdf(sha256, shared, new Uint8Array(0), 'AGEX-CLC-v1', 32)
72
+
73
+ const nonce = base64url.decode(envelope.nonce)
74
+ const ciphertext = base64url.decode(envelope.ciphertext)
75
+ const tag = base64url.decode(envelope.tag)
76
+
77
+ // Reconstruct sealed = ciphertext || tag
78
+ const sealed = new Uint8Array(ciphertext.length + tag.length)
79
+ sealed.set(ciphertext)
80
+ sealed.set(tag, ciphertext.length)
81
+
82
+ const cryptoKey = await subtle.importKey('raw', key, 'AES-GCM', false, ['decrypt'])
83
+ const plain = await subtle.decrypt({ name: 'AES-GCM', iv: nonce }, cryptoKey, sealed)
84
+
85
+ return new TextDecoder().decode(plain)
86
+ }
87
+
88
+ /**
89
+ * Convert an Ed25519 private key to X25519 for ECDH.
90
+ * Ed25519 and X25519 share the same underlying curve (Curve25519).
91
+ */
92
+ export function ed25519PrivateToX25519 (ed25519PrivateBase64url) {
93
+ const privBytes = base64url.decode(ed25519PrivateBase64url)
94
+ // Hash the Ed25519 seed to get the scalar, then clamp for X25519
95
+ const hash = sha256(privBytes)
96
+ const scalar = hash.slice(0, 32)
97
+ scalar[0] &= 248
98
+ scalar[31] &= 127
99
+ scalar[31] |= 64
100
+ return scalar
101
+ }
102
+
103
+ /**
104
+ * Convert an Ed25519 public key JWK to X25519 public key bytes.
105
+ * Uses the birational map from Ed25519 to X25519 (Montgomery form).
106
+ */
107
+ export function ed25519PublicToX25519 (publicKeyJWK) {
108
+ // @noble/curves provides this conversion
109
+ const { edwardsToMontgomeryPub } = await import('@noble/curves/ed25519')
110
+ const edPub = base64url.decode(publicKeyJWK.x)
111
+ return edwardsToMontgomeryPub(edPub)
112
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ generateKeypair, publicKeyToJWK, jwkToPublicKeyBytes,
3
+ sign, verify,
4
+ canonicalJson,
5
+ sha3Hash, sha256Hash,
6
+ computeEventHash,
7
+ generateId, generateNonce,
8
+ base64url, randomBytes
9
+ } from './keys.js'
10
+
11
+ export {
12
+ encryptEnvelope, decryptEnvelope,
13
+ ed25519PrivateToX25519
14
+ } from './ecdh.js'
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @agexhq/core — Cryptographic primitives
3
+ * Ed25519 key generation, signing, verification
4
+ * All pure JavaScript via @noble — zero native deps
5
+ */
6
+
7
+ import * as ed from '@noble/ed25519'
8
+ import { sha256 } from '@noble/hashes/sha256'
9
+ import { sha3_256 } from '@noble/hashes/sha3'
10
+ import { randomBytes } from '@noble/hashes/utils'
11
+ import { base64url } from 'jose'
12
+ import { v4 as uuidv4 } from 'uuid'
13
+
14
+ // ── Key Generation ────────────────────────────────────────────────────────
15
+
16
+ export function generateKeypair () {
17
+ const privateKey = ed.utils.randomPrivateKey()
18
+ const publicKey = ed.getPublicKeySync(privateKey)
19
+ return {
20
+ privateKey: base64url.encode(privateKey),
21
+ publicKey: base64url.encode(publicKey),
22
+ jwk: publicKeyToJWK(publicKey)
23
+ }
24
+ }
25
+
26
+ export function publicKeyToJWK (publicKeyBytes) {
27
+ return { kty: 'OKP', crv: 'Ed25519', x: base64url.encode(publicKeyBytes) }
28
+ }
29
+
30
+ export function jwkToPublicKeyBytes (jwk) {
31
+ return base64url.decode(jwk.x)
32
+ }
33
+
34
+ // ── Signing ───────────────────────────────────────────────────────────────
35
+
36
+ export async function sign (message, privateKeyBase64url) {
37
+ const privBytes = base64url.decode(privateKeyBase64url)
38
+ const msgBytes = typeof message === 'string'
39
+ ? new TextEncoder().encode(message)
40
+ : message
41
+ const sig = await ed.signAsync(msgBytes, privBytes)
42
+ return base64url.encode(sig)
43
+ }
44
+
45
+ export async function verify (message, signatureBase64url, publicKeyJWK) {
46
+ try {
47
+ const pubBytes = jwkToPublicKeyBytes(publicKeyJWK)
48
+ const sigBytes = base64url.decode(signatureBase64url)
49
+ const msgBytes = typeof message === 'string'
50
+ ? new TextEncoder().encode(message)
51
+ : message
52
+ return await ed.verifyAsync(sigBytes, msgBytes, pubBytes)
53
+ } catch {
54
+ return false
55
+ }
56
+ }
57
+
58
+ // ── Canonical JSON (RFC 8785 — fixed edge cases from audit 3.3) ──────────
59
+
60
+ export function canonicalJson (obj) {
61
+ if (obj === null) return 'null'
62
+ if (obj === undefined) return undefined
63
+ if (typeof obj === 'boolean') return String(obj)
64
+ if (typeof obj === 'number') {
65
+ if (!Number.isFinite(obj)) return 'null'
66
+ return Object.is(obj, -0) ? '0' : String(obj)
67
+ }
68
+ if (typeof obj === 'string') return JSON.stringify(obj)
69
+ if (obj instanceof Date) return JSON.stringify(obj.toISOString())
70
+ if (Array.isArray(obj)) {
71
+ return '[' + obj.map(v => {
72
+ const s = canonicalJson(v)
73
+ return s === undefined ? 'null' : s
74
+ }).join(',') + ']'
75
+ }
76
+ if (typeof obj === 'object') {
77
+ const pairs = Object.keys(obj)
78
+ .sort()
79
+ .map(k => {
80
+ const v = canonicalJson(obj[k])
81
+ if (v === undefined) return undefined
82
+ return JSON.stringify(k) + ':' + v
83
+ })
84
+ .filter(p => p !== undefined)
85
+ return '{' + pairs.join(',') + '}'
86
+ }
87
+ return 'null'
88
+ }
89
+
90
+ // ── Hashing ───────────────────────────────────────────────────────────────
91
+
92
+ export function sha3Hash (data) {
93
+ const bytes = typeof data === 'string'
94
+ ? new TextEncoder().encode(data) : data
95
+ return Buffer.from(sha3_256(bytes)).toString('hex')
96
+ }
97
+
98
+ export function sha256Hash (data) {
99
+ const bytes = typeof data === 'string'
100
+ ? new TextEncoder().encode(data) : data
101
+ return Buffer.from(sha256(bytes)).toString('hex')
102
+ }
103
+
104
+ // ── Audit Hash Chain ──────────────────────────────────────────────────────
105
+
106
+ export function computeEventHash (event, prevHash) {
107
+ const payload = canonicalJson({ ...event, prev_hash: prevHash })
108
+ return sha3Hash(payload)
109
+ }
110
+
111
+ // ── ID Generation ─────────────────────────────────────────────────────────
112
+
113
+ export function generateId () { return uuidv4() }
114
+
115
+ export function generateNonce () {
116
+ return base64url.encode(randomBytes(16))
117
+ }
118
+
119
+ export { base64url, randomBytes }
package/src/index.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @agexhq/core — AGEX Protocol Primitives
3
+ * The foundation package for the entire AGEX ecosystem.
4
+ * Pure JavaScript — zero native dependencies — works everywhere including Termux.
5
+ */
6
+
7
+ // ── Crypto ────────────────────────────────────────────────────────────────
8
+ export {
9
+ generateKeypair, publicKeyToJWK, jwkToPublicKeyBytes,
10
+ sign, verify,
11
+ canonicalJson,
12
+ sha3Hash, sha256Hash,
13
+ computeEventHash,
14
+ generateId, generateNonce,
15
+ base64url, randomBytes
16
+ } from './crypto/keys.js'
17
+
18
+ export {
19
+ encryptEnvelope, decryptEnvelope,
20
+ ed25519PrivateToX25519
21
+ } from './crypto/ecdh.js'
22
+
23
+ // ── Schemas ───────────────────────────────────────────────────────────────
24
+ export {
25
+ AIDSchema, ManifestSchema, CLCSchema,
26
+ ServiceProviderSchema,
27
+ PolicyDocSchema, PolicyRuleSchema, PolicyConditionSchema
28
+ } from './schemas/index.js'
29
+
30
+ // ── Protocol ──────────────────────────────────────────────────────────────
31
+ export {
32
+ verifyAIDSignature, selfSignAID,
33
+ signManifest, verifyManifest,
34
+ buildAgexHeaders, signRequest, verifyRequest,
35
+ AgexError,
36
+ AGEX_VERSION, AUDIT_EVENTS
37
+ } from './protocol/index.js'
38
+
39
+ // ── Policy Engine ─────────────────────────────────────────────────────────
40
+ export { evaluatePolicy, evaluateCondition } from './policy/index.js'
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @agexhq/core — AGEX Policy Language (APL) Evaluation Engine
3
+ * Deterministic, stateless policy evaluation
4
+ */
5
+
6
+ export function evaluatePolicy (policy, aid, manifest) {
7
+ const rules = [...(policy.rules || [])].sort((a, b) => (a.priority || 99) - (b.priority || 99))
8
+
9
+ for (const rule of rules) {
10
+ const matched = evaluateCondition(rule.condition, aid, manifest)
11
+ if (matched) {
12
+ return {
13
+ action: rule.action,
14
+ rule_id: rule.rule_id,
15
+ reason: rule.description || `Matched rule: ${rule.rule_id}`
16
+ }
17
+ }
18
+ }
19
+
20
+ return {
21
+ action: policy.default_action || 'reject',
22
+ reason: 'No rules matched; applying default_action'
23
+ }
24
+ }
25
+
26
+ export function evaluateCondition (condition, aid, manifest) {
27
+ if (!condition) return true
28
+
29
+ switch (condition.type) {
30
+
31
+ case 'trust_tier': {
32
+ const tier = typeof aid.trust_tier === 'number' ? aid.trust_tier : parseInt(aid.trust_tier)
33
+ switch (condition.operator) {
34
+ case 'gte': return tier >= condition.value
35
+ case 'lte': return tier <= condition.value
36
+ case 'eq': return tier === condition.value
37
+ case 'gt': return tier > condition.value
38
+ case 'lt': return tier < condition.value
39
+ default: return false
40
+ }
41
+ }
42
+
43
+ case 'scope': {
44
+ const requested = manifest.target?.requested_scopes || []
45
+ if (condition.operator === 'contains_any') {
46
+ return condition.values.some(s => requested.includes(s))
47
+ }
48
+ if (condition.operator === 'contains_all') {
49
+ return condition.values.every(s => requested.includes(s))
50
+ }
51
+ if (condition.operator === 'subset_of') {
52
+ return requested.every(s => condition.values.includes(s))
53
+ }
54
+ return false
55
+ }
56
+
57
+ case 'intent_type': {
58
+ const taskType = manifest.intent?.task_type
59
+ if (condition.operator === 'eq') return taskType === condition.value
60
+ if (condition.operator === 'in') return (condition.values || []).includes(taskType)
61
+ return false
62
+ }
63
+
64
+ case 'data_classification': {
65
+ const dc = manifest.intent?.data_classification
66
+ const levels = ['public', 'internal', 'confidential', 'restricted']
67
+ const reqLevel = levels.indexOf(dc)
68
+ const condLevel = levels.indexOf(condition.value)
69
+ if (condition.operator === 'lte') return reqLevel <= condLevel
70
+ if (condition.operator === 'gte') return reqLevel >= condLevel
71
+ if (condition.operator === 'eq') return reqLevel === condLevel
72
+ return false
73
+ }
74
+
75
+ case 'pii_processing':
76
+ return manifest.data_handling?.pii_processing === condition.value
77
+
78
+ case 'environment': {
79
+ const env = manifest.target?.environment
80
+ if (condition.operator === 'eq') return env === condition.value
81
+ if (condition.operator === 'in') return (condition.values || []).includes(env)
82
+ return false
83
+ }
84
+
85
+ case 'time_window': {
86
+ const now = new Date()
87
+ const hour = now.getUTCHours()
88
+ const day = now.getUTCDay()
89
+ const inHours = hour >= (condition.start_hour || 0) && hour < (condition.end_hour || 24)
90
+ const inDays = !condition.days_of_week ||
91
+ condition.days_of_week.includes(['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][day])
92
+ return inHours && inDays
93
+ }
94
+
95
+ // FIX: audit 2.6 — clarified geography semantics
96
+ case 'geography': {
97
+ const agentJurisdiction = aid.agent?.principal?.jurisdiction ||
98
+ (typeof aid.restrictions === 'string' ? JSON.parse(aid.restrictions) : aid.restrictions)?.geo_restrictions?.[0]
99
+ const allowedRegions = (typeof aid.restrictions === 'string' ? JSON.parse(aid.restrictions) : aid.restrictions)?.geo_restrictions || []
100
+
101
+ if (condition.operator === 'agent_in') {
102
+ return condition.values.includes(agentJurisdiction)
103
+ }
104
+ if (condition.operator === 'agent_not_in') {
105
+ return !condition.values.includes(agentJurisdiction)
106
+ }
107
+ if (condition.operator === 'restricted_to') {
108
+ return condition.values.every(v => allowedRegions.includes(v))
109
+ }
110
+ // Backwards compat with old in/not_in operators
111
+ if (condition.operator === 'in') {
112
+ return condition.values.some(v => allowedRegions.includes(v))
113
+ }
114
+ if (condition.operator === 'not_in') {
115
+ return !condition.values.some(v => allowedRegions.includes(v))
116
+ }
117
+ return false
118
+ }
119
+
120
+ case 'and':
121
+ return (condition.conditions || []).every(c => evaluateCondition(c, aid, manifest))
122
+
123
+ case 'or':
124
+ return (condition.conditions || []).some(c => evaluateCondition(c, aid, manifest))
125
+
126
+ case 'not':
127
+ return !evaluateCondition(condition.condition, aid, manifest)
128
+
129
+ default:
130
+ console.warn(`[APL] Unknown condition type: ${condition.type}`)
131
+ return false
132
+ }
133
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @agexhq/core — Protocol operations
3
+ * AID verification, manifest signing, AGEX header construction
4
+ */
5
+
6
+ import { sign, verify, canonicalJson, generateId } from '../crypto/keys.js'
7
+
8
+ // ── AID Signature Verification (FIX: audit 1.1) ──────────────────────────
9
+ // ALWAYS verifies — never silently bypasses
10
+
11
+ export async function verifyAIDSignature (aid, iaPublicKeyJWK) {
12
+ if (!iaPublicKeyJWK) {
13
+ throw new AgexError('IA_KEY_REQUIRED', 'IA public key required for AID verification', 400)
14
+ }
15
+ const { ia_signature, ...aidBody } = aid
16
+ const canonical = canonicalJson(aidBody)
17
+ const valid = await verify(canonical, ia_signature, iaPublicKeyJWK)
18
+ if (!valid) {
19
+ throw new AgexError('IA_SIGNATURE_INVALID', 'AID ia_signature failed verification', 401)
20
+ }
21
+ return true
22
+ }
23
+
24
+ /**
25
+ * Self-sign an AID using the Hub's own key (dev/self-signed IA mode).
26
+ * The AID is still cryptographically signed — just by the hub instead of an external IA.
27
+ */
28
+ export async function selfSignAID (aidBody, hubPrivateKey) {
29
+ const { ia_signature, ...body } = aidBody
30
+ const canonical = canonicalJson(body)
31
+ const signature = await sign(canonical, hubPrivateKey)
32
+ return { ...body, ia_signature: signature }
33
+ }
34
+
35
+ // ── Manifest Signing (FIX: audit 1.3) ─────────────────────────────────────
36
+ // Real Ed25519 signatures instead of placeholder strings
37
+
38
+ export async function signManifest (manifest, privateKey) {
39
+ const { agent_signature, ...body } = manifest
40
+ const canonical = canonicalJson(body)
41
+ return await sign(canonical, privateKey)
42
+ }
43
+
44
+ export async function verifyManifest (manifest, publicKeyJWK) {
45
+ const { agent_signature, ...body } = manifest
46
+ if (!agent_signature || agent_signature === 'sdk-placeholder') return false
47
+ const canonical = canonicalJson(body)
48
+ return await verify(canonical, agent_signature, publicKeyJWK)
49
+ }
50
+
51
+ // ── AGEX Request Headers ──────────────────────────────────────────────────
52
+
53
+ export function buildAgexHeaders (aidId, version = '1.0') {
54
+ return {
55
+ 'X-AGEX-Version': version,
56
+ 'X-AGEX-Request-ID': generateId(),
57
+ 'X-AGEX-Timestamp': new Date().toISOString(),
58
+ 'X-AGEX-AID': aidId
59
+ }
60
+ }
61
+
62
+ export async function signRequest (body, timestamp, requestId, privateKey) {
63
+ const bodyStr = body ? canonicalJson(body) : ''
64
+ const sigInput = `${bodyStr}|${timestamp}|${requestId}`
65
+ return await sign(sigInput, privateKey)
66
+ }
67
+
68
+ export async function verifyRequest (body, timestamp, requestId, signature, publicKeyJWK) {
69
+ const bodyStr = body ? canonicalJson(body) : ''
70
+ const sigInput = `${bodyStr}|${timestamp}|${requestId}`
71
+ return await verify(sigInput, signature, publicKeyJWK)
72
+ }
73
+
74
+ // ── Error class ───────────────────────────────────────────────────────────
75
+
76
+ export class AgexError extends Error {
77
+ constructor (code, message, statusCode = 400) {
78
+ super(message)
79
+ this.name = 'AgexError'
80
+ this.code = code
81
+ this.statusCode = statusCode
82
+ }
83
+ }
84
+
85
+ // ── Constants ─────────────────────────────────────────────────────────────
86
+
87
+ export const AGEX_VERSION = '1.0'
88
+
89
+ export const AUDIT_EVENTS = {
90
+ AID_REGISTERED: 'aid.registered',
91
+ AID_REVOKED: 'aid.revoked',
92
+ CREDENTIAL_REQUESTED: 'credential.requested',
93
+ CREDENTIAL_ISSUED: 'credential.issued',
94
+ CREDENTIAL_REJECTED: 'credential.rejected',
95
+ CREDENTIAL_PENDING: 'credential.pending_approval',
96
+ ROTATION_INITIATED: 'rotation.initiated',
97
+ ROTATION_COMPLETED: 'rotation.completed',
98
+ ROTATION_FAILED: 'rotation.failed',
99
+ DELEGATION_CREATED: 'delegation.created',
100
+ ERS_INITIATED: 'ers.initiated',
101
+ ERS_COMPLETED: 'ers.completed',
102
+ CLC_REVOKED: 'clc.revoked',
103
+ CLC_SUSPENDED: 'clc.suspended',
104
+ CLC_RESUMED: 'clc.resumed',
105
+ APPROVAL_GRANTED: 'approval.granted',
106
+ APPROVAL_REJECTED: 'approval.rejected'
107
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @agexhq/core — Zod schemas for all AGEX protocol objects
3
+ * Single source of truth — used by hub, SDK, and CLI
4
+ */
5
+
6
+ import { z } from 'zod'
7
+
8
+ // ── AID Schema (Agent Identity Document) ──────────────────────────────────
9
+
10
+ export const AIDSchema = z.object({
11
+ aid_version: z.literal('1.0'),
12
+ aid_id: z.string().uuid(),
13
+ issuer: z.object({
14
+ ia_id: z.string(),
15
+ ia_name: z.string(),
16
+ ia_cert_id: z.string()
17
+ }),
18
+ issued_at: z.string().datetime(),
19
+ expires_at: z.string().datetime(),
20
+ agent: z.object({
21
+ name: z.string().optional(),
22
+ type: z.enum(['orchestrator', 'worker', 'specialist', 'gateway']),
23
+ capabilities: z.array(z.string()).default([]),
24
+ principal: z.object({
25
+ organisation: z.string(),
26
+ org_id: z.string(),
27
+ contact: z.string().email(),
28
+ jurisdiction: z.string()
29
+ })
30
+ }),
31
+ trust_tier: z.number().int().min(0).max(3),
32
+ public_key: z.object({ kty: z.string(), crv: z.string(), x: z.string() }),
33
+ restrictions: z.object({
34
+ allowed_services: z.array(z.string()).optional(),
35
+ max_clc_duration_seconds: z.number().int().optional(),
36
+ max_delegation_depth: z.number().int().min(0).max(5).optional(),
37
+ geo_restrictions: z.array(z.string()).optional()
38
+ }).default({}),
39
+ ia_signature: z.string()
40
+ })
41
+
42
+ // ── Intent Manifest Schema ────────────────────────────────────────────────
43
+
44
+ export const ManifestSchema = z.object({
45
+ manifest_version: z.literal('1.0'),
46
+ manifest_id: z.string().uuid(),
47
+ requesting_aid: z.string().uuid(),
48
+ target: z.object({
49
+ service_id: z.string(),
50
+ requested_scopes: z.array(z.string()).min(1),
51
+ minimum_scopes: z.array(z.string()).optional(),
52
+ environment: z.enum(['production', 'staging', 'development']).default('production')
53
+ }),
54
+ intent: z.object({
55
+ summary: z.string().max(500),
56
+ task_type: z.enum(['read', 'write', 'read_write', 'admin', 'transact', 'notify']),
57
+ data_classification: z.enum(['public', 'internal', 'confidential', 'restricted']).default('internal'),
58
+ automated: z.boolean().default(true),
59
+ reversible: z.boolean().default(true),
60
+ human_visible: z.boolean().default(false)
61
+ }),
62
+ duration: z.object({
63
+ max_duration_seconds: z.number().int().min(60).max(604800),
64
+ idle_timeout_seconds: z.number().int().min(60).default(3600)
65
+ }),
66
+ data_handling: z.object({
67
+ pii_processing: z.boolean().default(false),
68
+ cross_border_transfer: z.boolean().default(false),
69
+ deletion_on_completion: z.boolean().default(false)
70
+ }).default({}),
71
+ agent_signature: z.string()
72
+ })
73
+
74
+ // ── CLC Schema (Credential Lifecycle Contract) ────────────────────────────
75
+
76
+ export const CLCSchema = z.object({
77
+ clc_version: z.literal('1.0'),
78
+ clc_id: z.string().uuid(),
79
+ beneficiary_aid: z.string().uuid(),
80
+ manifest_id: z.string().uuid(),
81
+ manifest_hash: z.string(),
82
+ credential_envelope: z.object({
83
+ algorithm: z.string(),
84
+ epk: z.object({ kty: z.string(), crv: z.string(), x: z.string() }),
85
+ ciphertext: z.string(),
86
+ nonce: z.string(),
87
+ tag: z.string()
88
+ }),
89
+ granted_scopes: z.array(z.string()),
90
+ scope_ceiling: z.array(z.string()),
91
+ validity: z.object({
92
+ not_before: z.string().datetime(),
93
+ not_after: z.string().datetime(),
94
+ idle_timeout_seconds: z.number().int().default(3600)
95
+ }),
96
+ rotation_policy: z.object({
97
+ rotation_interval_seconds: z.number().int(),
98
+ rotation_overlap_seconds: z.number().int(),
99
+ key_derivation_function: z.string().default('HKDF-SHA256')
100
+ }),
101
+ delegation_policy: z.object({
102
+ delegation_permitted: z.boolean(),
103
+ max_delegation_depth: z.number().int().min(0).max(5),
104
+ max_further_delegation: z.number().int().min(0).default(0)
105
+ }),
106
+ chain_provenance: z.array(z.string()).default([]),
107
+ provider_signature: z.string(),
108
+ hub_binding: z.object({
109
+ hub_id: z.string(),
110
+ hub_signature: z.string()
111
+ })
112
+ })
113
+
114
+ // ── Service Provider Schema (NEW — fixes audit 3.1) ──────────────────────
115
+
116
+ export const ServiceProviderSchema = z.object({
117
+ sp_id: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._-]*$/, 'sp_id must be lowercase alphanumeric with dots/hyphens/underscores'),
118
+ sp_name: z.string().min(1).max(256),
119
+ service_url: z.string().url(),
120
+ credential_endpoint: z.string().url(),
121
+ policy_endpoint: z.string().url().optional(),
122
+ public_key_jwk: z.object({ kty: z.string(), crv: z.string(), x: z.string() }),
123
+ supported_scopes: z.array(z.string()).default([])
124
+ })
125
+
126
+ // ── APL Policy Schema ─────────────────────────────────────────────────────
127
+
128
+ export const PolicyConditionSchema = z.lazy(() => z.discriminatedUnion('type', [
129
+ z.object({ type: z.literal('trust_tier'), operator: z.enum(['eq', 'gt', 'gte', 'lt', 'lte']), value: z.number() }),
130
+ z.object({ type: z.literal('scope'), operator: z.enum(['contains_any', 'contains_all', 'subset_of']), values: z.array(z.string()) }),
131
+ z.object({ type: z.literal('intent_type'), operator: z.enum(['eq', 'in']), value: z.string().optional(), values: z.array(z.string()).optional() }),
132
+ z.object({ type: z.literal('data_classification'), operator: z.enum(['eq', 'gte', 'lte']), value: z.string() }),
133
+ z.object({ type: z.literal('pii_processing'), value: z.boolean() }),
134
+ z.object({ type: z.literal('environment'), operator: z.enum(['eq', 'in']), value: z.string().optional(), values: z.array(z.string()).optional() }),
135
+ z.object({ type: z.literal('time_window'), start_hour: z.number().optional(), end_hour: z.number().optional(), days_of_week: z.array(z.string()).optional() }),
136
+ z.object({ type: z.literal('geography'), operator: z.enum(['agent_in', 'agent_not_in', 'restricted_to']), values: z.array(z.string()) }),
137
+ z.object({ type: z.literal('and'), conditions: z.array(PolicyConditionSchema) }),
138
+ z.object({ type: z.literal('or'), conditions: z.array(PolicyConditionSchema) }),
139
+ z.object({ type: z.literal('not'), condition: PolicyConditionSchema }),
140
+ ]))
141
+
142
+ export const PolicyRuleSchema = z.object({
143
+ rule_id: z.string(),
144
+ priority: z.number().int().default(99),
145
+ condition: PolicyConditionSchema.optional(),
146
+ action: z.enum(['approve', 'reject', 'review']),
147
+ description: z.string().optional()
148
+ })
149
+
150
+ export const PolicyDocSchema = z.object({
151
+ rules: z.array(PolicyRuleSchema),
152
+ default_action: z.enum(['approve', 'reject', 'review']).default('reject')
153
+ })