@agexhq/core 1.0.1 → 1.0.2
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 +1 -1
- package/src/crypto/ecdh.js +3 -1
- package/src/crypto/index.js +14 -0
- package/src/crypto/keys.js +119 -0
- package/src/index.js +40 -0
- package/src/policy/index.js +133 -0
- package/src/protocol/index.js +107 -0
- package/src/schemas/index.js +153 -0
package/package.json
CHANGED
package/src/crypto/ecdh.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Pure JavaScript — works on Termux without native compilation
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { x25519
|
|
7
|
+
import { x25519 } from '@noble/curves/ed25519'
|
|
8
8
|
import { hkdf } from '@noble/hashes/hkdf'
|
|
9
9
|
import { sha256 } from '@noble/hashes/sha256'
|
|
10
10
|
import { randomBytes } from '@noble/hashes/utils'
|
|
@@ -105,6 +105,8 @@ export function ed25519PrivateToX25519 (ed25519PrivateBase64url) {
|
|
|
105
105
|
* Uses the birational map from Ed25519 to X25519 (Montgomery form).
|
|
106
106
|
*/
|
|
107
107
|
export function ed25519PublicToX25519 (publicKeyJWK) {
|
|
108
|
+
// @noble/curves provides this conversion
|
|
109
|
+
const { edwardsToMontgomeryPub } = await import('@noble/curves/ed25519')
|
|
108
110
|
const edPub = base64url.decode(publicKeyJWK.x)
|
|
109
111
|
return edwardsToMontgomeryPub(edPub)
|
|
110
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
|
+
})
|