@7h3/protocol 0.1.2 → 0.4.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/.dockerignore +19 -0
- package/.github/dependabot.yml +32 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/publish.yml +59 -0
- package/.github/workflows/scorecard.yml +37 -0
- package/7h3.example.yaml +125 -0
- package/CHANGELOG.md +92 -0
- package/CONTRIBUTING.md +82 -0
- package/Dockerfile +73 -0
- package/GOVERNANCE.md +62 -0
- package/README.md +323 -6
- package/SECURITY.md +70 -0
- package/bench-results/replay-cache-full-1777891033256.json +10 -0
- package/bench-results/replay-cache-full-1777896317488.json +10 -0
- package/bench-results/replay-cache-full-1777900993184.json +10 -0
- package/bench-results/replay-cache-full-1777901019285.json +10 -0
- package/bench-results/replay-cache-quick-1777870170126.json +10 -0
- package/bench-results/signature-profiles-quick-1775875160079.json +85 -0
- package/bench-results/signature-profiles-quick-1775983539716.json +85 -0
- package/bench-results/signature-profiles-quick-1776237913190.json +85 -0
- package/bench-results/wire-codecs-full-1777891019803.json +93 -0
- package/bench-results/wire-codecs-full-1777896260964.json +93 -0
- package/bench-results/wire-codecs-full-1777901004247.json +93 -0
- package/bench-results/wire-codecs-quick-1775972879056.json +93 -0
- package/bench-results/wire-codecs-quick-1775983541111.json +93 -0
- package/bench-results/wire-codecs-quick-1776237914299.json +93 -0
- package/bench-results/wire-codecs-quick-1777841285236.json +93 -0
- package/bench-results/wire-codecs-quick-1777841321772.json +93 -0
- package/bench-results/wire-codecs-quick-1777841330408.json +93 -0
- package/bench-results/wire-codecs-quick-1777852886082.json +93 -0
- package/bench-results/wire-codecs-quick-1777852988773.json +93 -0
- package/bench-results/wire-codecs-quick-1777870188095.json +93 -0
- package/bench-results/wire-codecs-quick-1777870263918.json +93 -0
- package/bench-results/wire-codecs-quick-1777870455034.json +93 -0
- package/bench-results/wire-codecs-quick-1778816163081.json +93 -0
- package/bench-results/wire-codecs-quick-1778843936130.json +93 -0
- package/bin/7h3.ts +385 -0
- package/conformance/7h3_v0_1.json +77 -0
- package/conformance/7h3_v0_1_binary.json +20 -0
- package/conformance/aip_v0_1_binary.json +20 -0
- package/docker-compose.yaml +77 -0
- package/docs/ADOPTION_PLAN.md +120 -0
- package/docs/AGENTS.md +77 -0
- package/docs/AIP_RFC_v0.1.md +97 -0
- package/docs/AI_DECISION_CARD.md +122 -0
- package/docs/AI_RUNTIME_POLICY.json +126 -0
- package/docs/AI_RUNTIME_POLICY.yaml +110 -0
- package/docs/BACKPRESSURE_TUNING.md +65 -0
- package/docs/BENCHMARK_CLAIM_MATRIX.md +42 -0
- package/docs/BENCHMARK_REPORT_TEMPLATE.md +169 -0
- package/docs/BINARY_CODEC_BENCH.md +23 -0
- package/docs/CLEAN_CLONE_RUNBOOK.md +36 -0
- package/docs/CLOCK_SKEW_POLICY.md +30 -0
- package/docs/DISTRIBUTED_REPLAY.md +142 -0
- package/docs/FUZZ_CAMPAIGN.md +121 -0
- package/docs/GATEWAY.md +195 -0
- package/docs/KEY_MANAGEMENT_POLICY.md +53 -0
- package/docs/KEY_REVOCATION.md +69 -0
- package/docs/MCP_WRAPPER.md +159 -0
- package/docs/MIGRATION_GUIDE.md +40 -0
- package/docs/OPERATORS.md +184 -0
- package/docs/PERF_REGRESSION_POLICY.md +34 -0
- package/docs/PROJECT_EXAMINATION_2026-05-31.md +219 -0
- package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +135 -0
- package/docs/RELEASE_GATE.md +25 -0
- package/docs/RELEASE_NOTES_v0.1.0.md +54 -0
- package/docs/SECURITY_REVIEW_2026-06-05.md +165 -0
- package/docs/TELEMETRY.md +41 -0
- package/docs/THREAT_MODEL.md +89 -0
- package/docs/VERSIONING_POLICY.md +30 -0
- package/docs/assets/banner.png +0 -0
- package/eslint.config.js +15 -0
- package/fuzz/ts/harness-decode.ts +136 -0
- package/fuzz/ts/harness-verify.ts +121 -0
- package/fuzz/ts/run.ts +35 -0
- package/mcp-server/README.md +38 -0
- package/mcp-server/package-lock.json +1187 -0
- package/mcp-server/package.json +35 -0
- package/mcp-server/src/index.ts +236 -0
- package/mcp-server/tsconfig.json +14 -0
- package/package.json +79 -13
- package/scripts/aip-framework-quickstart.ts +110 -0
- package/scripts/aip-mcp-gateway.ts +38 -0
- package/scripts/aip-mcp-wrap-demo.ts +72 -0
- package/scripts/aip-quickstart.ts +60 -0
- package/scripts/bench-diff.ts +118 -0
- package/scripts/bench-protocol-e2e.ts +937 -0
- package/scripts/bench-protocol-openloop.ts +1397 -0
- package/scripts/bench-replay-cache.ts +76 -0
- package/scripts/bench-signature-profiles.ts +180 -0
- package/scripts/bench-wire-codecs.ts +161 -0
- package/scripts/build-binary-conformance.ts +36 -0
- package/scripts/build-release-dashboard.ts +175 -0
- package/scripts/canary-rollout.ts +38 -0
- package/scripts/mcpGatewayCli.test.ts +116 -0
- package/scripts/prepare-aip-package.ts +88 -0
- package/scripts/regen-conformance-sigs.ts +18 -0
- package/scripts/release-gate.ts +19 -0
- package/scripts/validate-runtime-policy.ts +18 -0
- package/sdk/browser/index.test.ts +162 -0
- package/sdk/browser/index.ts +257 -0
- package/sdk/browser/package.json +13 -0
- package/sdk/go/go.mod +3 -0
- package/sdk/go/http.go +135 -0
- package/sdk/go/protocol.go +324 -0
- package/sdk/go/protocol_test.go +334 -0
- package/sdk/go/webhook.go +136 -0
- package/sdk/python/README.md +18 -0
- package/sdk/python/protocol_7h3/__init__.py +46 -0
- package/sdk/python/protocol_7h3/http.py +212 -0
- package/sdk/python/protocol_7h3/keys.py +149 -0
- package/sdk/python/protocol_7h3/protocol.py +525 -0
- package/sdk/python/protocol_7h3/queue.py +118 -0
- package/sdk/python/protocol_7h3/webhook.py +116 -0
- package/sdk/python/pyproject.toml +40 -0
- package/sdk/python/tests/test_conformance.py +110 -0
- package/sdk/python/tests/test_http.py +305 -0
- package/sdk/python/tests/test_keys.py +417 -0
- package/sdk/python/tests/test_queue.py +120 -0
- package/sdk/python/tests/test_webhook.py +345 -0
- package/sdk/rust/Cargo.lock +371 -0
- package/sdk/rust/Cargo.toml +25 -0
- package/sdk/rust/README.md +31 -0
- package/sdk/rust/fuzz/Cargo.toml +29 -0
- package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +46 -0
- package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +11 -0
- package/sdk/rust/src/bin/aip_mcp_gateway.rs +59 -0
- package/sdk/rust/src/http.rs +145 -0
- package/sdk/rust/src/keys.rs +161 -0
- package/sdk/rust/src/lib.rs +688 -0
- package/sdk/rust/src/queue.rs +79 -0
- package/sdk/rust/src/webhook.rs +86 -0
- package/sdk/rust/tests/conformance.rs +148 -0
- package/sdk/rust/tests/gateway.rs +130 -0
- package/sdk/rust/tests/http_webhook_queue.rs +201 -0
- package/sdk/rust/tests/keys.rs +189 -0
- package/src/agentAdapter.test.ts +48 -0
- package/src/agentAdapter.ts +56 -0
- package/src/auditLog.test.ts +145 -0
- package/src/auditLog.ts +147 -0
- package/src/conformance.test.ts +136 -0
- package/src/conformanceVectors.ts +99 -0
- package/src/frameworkAdapters.test.ts +290 -0
- package/src/frameworkAdapters.ts +261 -0
- package/src/gateway.test.ts +343 -0
- package/src/gateway.ts +171 -0
- package/src/grpcBinding.test.ts +211 -0
- package/src/grpcBinding.ts +103 -0
- package/src/httpBinding.test.ts +376 -0
- package/src/httpBinding.ts +163 -0
- package/src/index.ts +32 -0
- package/src/keyInfra.test.ts +278 -0
- package/src/keyInfra.ts +228 -0
- package/src/keyRegistry.ts +59 -0
- package/src/keyRotation.test.ts +78 -0
- package/src/keyRotation.ts +72 -0
- package/src/mcpGateway.test.ts +129 -0
- package/src/mcpGateway.ts +250 -0
- package/src/mcpTransports.test.ts +92 -0
- package/src/mcpTransports.ts +169 -0
- package/src/mcpWrapper.test.ts +179 -0
- package/src/mcpWrapper.ts +206 -0
- package/src/policyEnforcer.test.ts +99 -0
- package/src/policyEnforcer.ts +169 -0
- package/src/policyTelemetryFeedback.test.ts +25 -0
- package/src/policyTelemetryFeedback.ts +38 -0
- package/src/protocol.bench.ts +37 -0
- package/src/protocol.test.ts +155 -0
- package/src/protocol.ts +413 -0
- package/src/protocolAgent.test.ts +105 -0
- package/src/protocolAgent.ts +169 -0
- package/src/protocolBinary.test.ts +165 -0
- package/src/protocolBinary.ts +312 -0
- package/src/protocolCapabilities.ts +70 -0
- package/src/protocolFuzz.advanced.test.ts +235 -0
- package/src/protocolFuzz.test.ts +111 -0
- package/src/protocolNegative.test.ts +97 -0
- package/src/protocolReplay.test.ts +71 -0
- package/src/protocolReplay.ts +194 -0
- package/src/protocolTransport.test.ts +556 -0
- package/src/protocolTransport.ts +483 -0
- package/src/queueBinding.test.ts +130 -0
- package/src/queueBinding.ts +102 -0
- package/src/rateLimiter.test.ts +96 -0
- package/src/rateLimiter.ts +46 -0
- package/src/redisClient.ts +140 -0
- package/src/redisIntegration.test.ts +134 -0
- package/src/replayStores.test.ts +141 -0
- package/src/replayStores.ts +82 -0
- package/src/revocation.test.ts +98 -0
- package/src/revocation.ts +0 -0
- package/src/routePolicy.test.ts +87 -0
- package/src/routePolicy.ts +72 -0
- package/src/runtimePolicy.test.ts +49 -0
- package/src/runtimePolicy.ts +81 -0
- package/src/runtimePolicyManager.test.ts +29 -0
- package/src/runtimePolicyManager.ts +50 -0
- package/src/runtimePolicyPresets.ts +43 -0
- package/src/signedResponse.test.ts +111 -0
- package/src/signedResponse.ts +83 -0
- package/src/webhookBinding.test.ts +144 -0
- package/src/webhookBinding.ts +115 -0
- package/src/wsBinding.test.ts +221 -0
- package/src/wsBinding.ts +100 -0
- package/tsconfig.json +15 -0
- package/tsconfig.lib.json +23 -0
- package/vite.lib.config.ts +16 -0
- package/agentAdapter.d.ts +0 -26
- package/conformanceVectors.d.ts +0 -20
- package/frameworkAdapters.d.ts +0 -72
- package/index.d.ts +0 -20
- package/index.js +0 -1702
- package/keyRotation.d.ts +0 -20
- package/mcpGateway.d.ts +0 -37
- package/mcpTransports.d.ts +0 -62
- package/mcpWrapper.d.ts +0 -83
- package/policyEnforcer.d.ts +0 -50
- package/policyTelemetryFeedback.d.ts +0 -11
- package/protocol.d.ts +0 -66
- package/protocolAgent.d.ts +0 -58
- package/protocolBinary.d.ts +0 -8
- package/protocolCapabilities.d.ts +0 -24
- package/protocolReplay.d.ts +0 -35
- package/protocolTransport.d.ts +0 -73
- package/redisClient.d.ts +0 -49
- package/replayStores.d.ts +0 -32
- package/revocation.d.ts +0 -71
- package/runtimePolicy.d.ts +0 -24
- package/runtimePolicyManager.d.ts +0 -15
- package/runtimePolicyPresets.d.ts +0 -11
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
export type ProtocolVersion = '7h3/0.1'
|
|
2
|
+
|
|
3
|
+
export type IntentKind =
|
|
4
|
+
| 'PING'
|
|
5
|
+
| 'PONG'
|
|
6
|
+
| 'CAPS'
|
|
7
|
+
| 'TASK'
|
|
8
|
+
| 'RESULT'
|
|
9
|
+
| 'ERROR'
|
|
10
|
+
|
|
11
|
+
export interface ProtocolHeader {
|
|
12
|
+
version: ProtocolVersion
|
|
13
|
+
messageId: string
|
|
14
|
+
timestampMs: number
|
|
15
|
+
ttlMs: number
|
|
16
|
+
sender: string
|
|
17
|
+
recipient?: string
|
|
18
|
+
nonce: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProtocolBody {
|
|
22
|
+
intent: IntentKind
|
|
23
|
+
content: string
|
|
24
|
+
capability?: string
|
|
25
|
+
correlationId?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProtocolSignature {
|
|
29
|
+
alg: 'HS256' | 'ED25519'
|
|
30
|
+
keyId: string
|
|
31
|
+
value: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProtocolEnvelope {
|
|
35
|
+
header: ProtocolHeader
|
|
36
|
+
body: ProtocolBody
|
|
37
|
+
signature?: ProtocolSignature
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ProtocolDiagnostic {
|
|
41
|
+
level: 'error' | 'warning'
|
|
42
|
+
message: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const textEncoder = new TextEncoder()
|
|
46
|
+
const HMAC_KEY_CACHE_LIMIT = 256
|
|
47
|
+
const hmacKeyCache = new Map<string, Promise<CryptoKey>>()
|
|
48
|
+
const ED25519_KEY_CACHE_LIMIT = 256
|
|
49
|
+
const ed25519PrivateKeyCache = new Map<string, Promise<CryptoKey>>()
|
|
50
|
+
const ed25519PublicKeyCache = new Map<string, Promise<CryptoKey>>()
|
|
51
|
+
|
|
52
|
+
export type SignatureVerificationMaterial =
|
|
53
|
+
| { alg: 'HS256'; secret: string }
|
|
54
|
+
| { alg: 'ED25519'; publicKey: string }
|
|
55
|
+
|
|
56
|
+
type BufferLike = {
|
|
57
|
+
from: (input: string | Uint8Array, encoding?: string) => Uint8Array & { toString: (encoding?: string) => string }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getBufferLike(): BufferLike | null {
|
|
61
|
+
const candidate = globalThis as unknown as { Buffer?: BufferLike }
|
|
62
|
+
return candidate.Buffer ?? null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function serializeHeaderCanonical(header: ProtocolHeader): string {
|
|
66
|
+
const parts: string[] = [
|
|
67
|
+
`"messageId":${JSON.stringify(header.messageId)}`,
|
|
68
|
+
`"nonce":${JSON.stringify(header.nonce)}`,
|
|
69
|
+
]
|
|
70
|
+
if (header.recipient !== undefined) {
|
|
71
|
+
parts.push(`"recipient":${JSON.stringify(header.recipient)}`)
|
|
72
|
+
}
|
|
73
|
+
parts.push(`"sender":${JSON.stringify(header.sender)}`)
|
|
74
|
+
parts.push(`"timestampMs":${header.timestampMs}`)
|
|
75
|
+
parts.push(`"ttlMs":${header.ttlMs}`)
|
|
76
|
+
parts.push(`"version":${JSON.stringify(header.version)}`)
|
|
77
|
+
return `{${parts.join(',')}}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function serializeBodyCanonical(body: ProtocolBody): string {
|
|
81
|
+
const parts: string[] = []
|
|
82
|
+
if (body.capability !== undefined) {
|
|
83
|
+
parts.push(`"capability":${JSON.stringify(body.capability)}`)
|
|
84
|
+
}
|
|
85
|
+
parts.push(`"content":${JSON.stringify(body.content)}`)
|
|
86
|
+
if (body.correlationId !== undefined) {
|
|
87
|
+
parts.push(`"correlationId":${JSON.stringify(body.correlationId)}`)
|
|
88
|
+
}
|
|
89
|
+
parts.push(`"intent":${JSON.stringify(body.intent)}`)
|
|
90
|
+
return `{${parts.join(',')}}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getCachedHmacKey(secret: string): Promise<CryptoKey> {
|
|
94
|
+
const cacheKey = `hs256:${secret}`
|
|
95
|
+
const cached = hmacKeyCache.get(cacheKey)
|
|
96
|
+
if (cached) return cached
|
|
97
|
+
|
|
98
|
+
if (hmacKeyCache.size >= HMAC_KEY_CACHE_LIMIT) {
|
|
99
|
+
hmacKeyCache.clear()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const subtle = requireCryptoSubtle()
|
|
103
|
+
const imported = subtle
|
|
104
|
+
.importKey(
|
|
105
|
+
'raw',
|
|
106
|
+
textEncoder.encode(secret),
|
|
107
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
108
|
+
false,
|
|
109
|
+
['sign', 'verify'],
|
|
110
|
+
)
|
|
111
|
+
.catch((error: unknown) => {
|
|
112
|
+
hmacKeyCache.delete(cacheKey)
|
|
113
|
+
throw error
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
hmacKeyCache.set(cacheKey, imported)
|
|
117
|
+
return imported
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getCachedEd25519PrivateKey(privateKeyPkcs8Base64Url: string): Promise<CryptoKey> {
|
|
121
|
+
const cacheKey = `ed25519:pkcs8:${privateKeyPkcs8Base64Url}`
|
|
122
|
+
const cached = ed25519PrivateKeyCache.get(cacheKey)
|
|
123
|
+
if (cached) return cached
|
|
124
|
+
|
|
125
|
+
if (ed25519PrivateKeyCache.size >= ED25519_KEY_CACHE_LIMIT) {
|
|
126
|
+
ed25519PrivateKeyCache.clear()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const subtle = requireCryptoSubtle()
|
|
130
|
+
const imported = subtle
|
|
131
|
+
.importKey('pkcs8', toArrayBuffer(fromBase64Url(privateKeyPkcs8Base64Url)), { name: 'Ed25519' }, false, ['sign'])
|
|
132
|
+
.catch((error: unknown) => {
|
|
133
|
+
ed25519PrivateKeyCache.delete(cacheKey)
|
|
134
|
+
throw error
|
|
135
|
+
})
|
|
136
|
+
ed25519PrivateKeyCache.set(cacheKey, imported)
|
|
137
|
+
return imported
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getCachedEd25519PublicKey(publicKeySpkiBase64Url: string): Promise<CryptoKey> {
|
|
141
|
+
const cacheKey = `ed25519:spki:${publicKeySpkiBase64Url}`
|
|
142
|
+
const cached = ed25519PublicKeyCache.get(cacheKey)
|
|
143
|
+
if (cached) return cached
|
|
144
|
+
|
|
145
|
+
if (ed25519PublicKeyCache.size >= ED25519_KEY_CACHE_LIMIT) {
|
|
146
|
+
ed25519PublicKeyCache.clear()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const subtle = requireCryptoSubtle()
|
|
150
|
+
const imported = subtle
|
|
151
|
+
.importKey('spki', toArrayBuffer(fromBase64Url(publicKeySpkiBase64Url)), { name: 'Ed25519' }, false, ['verify'])
|
|
152
|
+
.catch((error: unknown) => {
|
|
153
|
+
ed25519PublicKeyCache.delete(cacheKey)
|
|
154
|
+
throw error
|
|
155
|
+
})
|
|
156
|
+
ed25519PublicKeyCache.set(cacheKey, imported)
|
|
157
|
+
return imported
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toBase64Url(bytes: Uint8Array): string {
|
|
161
|
+
const bufferLike = getBufferLike()
|
|
162
|
+
const base64 = bufferLike ? bufferLike.from(bytes).toString('base64') : btoa(String.fromCharCode(...bytes))
|
|
163
|
+
return base64
|
|
164
|
+
.replace(/\+/g, '-')
|
|
165
|
+
.replace(/\//g, '_')
|
|
166
|
+
.replace(/=+$/g, '')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function fromBase64Url(value: string): Uint8Array {
|
|
170
|
+
const padded = value
|
|
171
|
+
.replace(/-/g, '+')
|
|
172
|
+
.replace(/_/g, '/')
|
|
173
|
+
.padEnd(Math.ceil(value.length / 4) * 4, '=')
|
|
174
|
+
|
|
175
|
+
const bufferLike = getBufferLike()
|
|
176
|
+
if (bufferLike) {
|
|
177
|
+
return new Uint8Array(bufferLike.from(padded, 'base64'))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const binary = atob(padded)
|
|
181
|
+
const bytes = new Uint8Array(binary.length)
|
|
182
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
183
|
+
bytes[i] = binary.charCodeAt(i)
|
|
184
|
+
}
|
|
185
|
+
return bytes
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
189
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function canonicalizeEnvelope(envelope: Omit<ProtocolEnvelope, 'signature'>): string {
|
|
193
|
+
return `{"body":${serializeBodyCanonical(envelope.body)},"header":${serializeHeaderCanonical(envelope.header)}}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function requireCryptoSubtle(): SubtleCrypto {
|
|
197
|
+
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
|
198
|
+
throw new Error('Web Crypto API is not available in this runtime')
|
|
199
|
+
}
|
|
200
|
+
return crypto.subtle
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function hmacSign(payload: string, secret: string): Promise<string> {
|
|
204
|
+
const subtle = requireCryptoSubtle()
|
|
205
|
+
const key = await getCachedHmacKey(secret)
|
|
206
|
+
const signature = await subtle.sign('HMAC', key, textEncoder.encode(payload))
|
|
207
|
+
return toBase64Url(new Uint8Array(signature))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function hmacVerify(payload: string, signature: string, secret: string): Promise<boolean> {
|
|
211
|
+
const subtle = requireCryptoSubtle()
|
|
212
|
+
const key = await getCachedHmacKey(secret)
|
|
213
|
+
const signatureBytes = fromBase64Url(signature)
|
|
214
|
+
return subtle.verify('HMAC', key, signatureBytes.buffer as ArrayBuffer, textEncoder.encode(payload))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function ed25519Sign(payload: string, privateKeyPkcs8Base64Url: string): Promise<string> {
|
|
218
|
+
const subtle = requireCryptoSubtle()
|
|
219
|
+
const key = await getCachedEd25519PrivateKey(privateKeyPkcs8Base64Url)
|
|
220
|
+
const signature = await subtle.sign('Ed25519', key, textEncoder.encode(payload))
|
|
221
|
+
return toBase64Url(new Uint8Array(signature))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function ed25519Verify(payload: string, signature: string, publicKeySpkiBase64Url: string): Promise<boolean> {
|
|
225
|
+
const subtle = requireCryptoSubtle()
|
|
226
|
+
const key = await getCachedEd25519PublicKey(publicKeySpkiBase64Url)
|
|
227
|
+
const signatureBytes = fromBase64Url(signature)
|
|
228
|
+
return subtle.verify('Ed25519', key, signatureBytes.buffer as ArrayBuffer, textEncoder.encode(payload))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function signCanonicalPayloadHmac(payload: string, secret: string): Promise<string> {
|
|
232
|
+
return hmacSign(payload, secret)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function verifyCanonicalPayloadHmac(payload: string, signature: string, secret: string): Promise<boolean> {
|
|
236
|
+
return hmacVerify(payload, signature, secret)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function generateEd25519KeypairBase64Url(): Promise<{ publicKey: string; privateKey: string }> {
|
|
240
|
+
const subtle = requireCryptoSubtle()
|
|
241
|
+
const pair = await subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])
|
|
242
|
+
const privateKeyRaw = await subtle.exportKey('pkcs8', pair.privateKey)
|
|
243
|
+
const publicKeyRaw = await subtle.exportKey('spki', pair.publicKey)
|
|
244
|
+
return {
|
|
245
|
+
privateKey: toBase64Url(new Uint8Array(privateKeyRaw)),
|
|
246
|
+
publicKey: toBase64Url(new Uint8Array(publicKeyRaw)),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function signCanonicalPayloadEd25519(payload: string, privateKeyPkcs8Base64Url: string): Promise<string> {
|
|
251
|
+
return ed25519Sign(payload, privateKeyPkcs8Base64Url)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function verifyCanonicalPayloadEd25519(
|
|
255
|
+
payload: string,
|
|
256
|
+
signature: string,
|
|
257
|
+
publicKeySpkiBase64Url: string,
|
|
258
|
+
): Promise<boolean> {
|
|
259
|
+
return ed25519Verify(payload, signature, publicKeySpkiBase64Url)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function signEnvelopeHmac(
|
|
263
|
+
envelope: Omit<ProtocolEnvelope, 'signature'>,
|
|
264
|
+
secret: string,
|
|
265
|
+
keyId = 'local-dev-key',
|
|
266
|
+
): Promise<ProtocolEnvelope> {
|
|
267
|
+
const payload = canonicalizeEnvelope(envelope)
|
|
268
|
+
const signature = await hmacSign(payload, secret)
|
|
269
|
+
return {
|
|
270
|
+
...envelope,
|
|
271
|
+
signature: {
|
|
272
|
+
alg: 'HS256',
|
|
273
|
+
keyId,
|
|
274
|
+
value: signature,
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function verifyEnvelopeHmac(envelope: ProtocolEnvelope, secret: string): Promise<boolean> {
|
|
280
|
+
if (!envelope.signature) return false
|
|
281
|
+
if (envelope.signature.alg !== 'HS256') return false
|
|
282
|
+
|
|
283
|
+
const unsigned: Omit<ProtocolEnvelope, 'signature'> = {
|
|
284
|
+
header: envelope.header,
|
|
285
|
+
body: envelope.body,
|
|
286
|
+
}
|
|
287
|
+
const payload = canonicalizeEnvelope(unsigned)
|
|
288
|
+
return hmacVerify(payload, envelope.signature.value, secret)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function signEnvelopeEd25519(
|
|
292
|
+
envelope: Omit<ProtocolEnvelope, 'signature'>,
|
|
293
|
+
privateKeyPkcs8Base64Url: string,
|
|
294
|
+
keyId = 'local-ed25519-key',
|
|
295
|
+
): Promise<ProtocolEnvelope> {
|
|
296
|
+
const payload = canonicalizeEnvelope(envelope)
|
|
297
|
+
const signature = await ed25519Sign(payload, privateKeyPkcs8Base64Url)
|
|
298
|
+
return {
|
|
299
|
+
...envelope,
|
|
300
|
+
signature: {
|
|
301
|
+
alg: 'ED25519',
|
|
302
|
+
keyId,
|
|
303
|
+
value: signature,
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function verifyEnvelopeEd25519(envelope: ProtocolEnvelope, publicKeySpkiBase64Url: string): Promise<boolean> {
|
|
309
|
+
if (!envelope.signature) return false
|
|
310
|
+
if (envelope.signature.alg !== 'ED25519') return false
|
|
311
|
+
|
|
312
|
+
const unsigned: Omit<ProtocolEnvelope, 'signature'> = {
|
|
313
|
+
header: envelope.header,
|
|
314
|
+
body: envelope.body,
|
|
315
|
+
}
|
|
316
|
+
const payload = canonicalizeEnvelope(unsigned)
|
|
317
|
+
return ed25519Verify(payload, envelope.signature.value, publicKeySpkiBase64Url)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function verifyEnvelopeSignature(
|
|
321
|
+
envelope: ProtocolEnvelope,
|
|
322
|
+
material: SignatureVerificationMaterial,
|
|
323
|
+
): Promise<boolean> {
|
|
324
|
+
if (!envelope.signature) return false
|
|
325
|
+
if (envelope.signature.alg !== material.alg) return false
|
|
326
|
+
if (material.alg === 'HS256') {
|
|
327
|
+
return verifyEnvelopeHmac(envelope, material.secret)
|
|
328
|
+
}
|
|
329
|
+
return verifyEnvelopeEd25519(envelope, material.publicKey)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function verifyCanonicalPayloadSignature(
|
|
333
|
+
payload: string,
|
|
334
|
+
signature: ProtocolSignature | undefined,
|
|
335
|
+
material: SignatureVerificationMaterial,
|
|
336
|
+
): Promise<boolean> {
|
|
337
|
+
if (!signature) return false
|
|
338
|
+
if (signature.alg !== material.alg) return false
|
|
339
|
+
if (material.alg === 'HS256') {
|
|
340
|
+
return verifyCanonicalPayloadHmac(payload, signature.value, material.secret)
|
|
341
|
+
}
|
|
342
|
+
return verifyCanonicalPayloadEd25519(payload, signature.value, material.publicKey)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function validateEnvelope(envelope: ProtocolEnvelope, nowMs = Date.now()): ProtocolDiagnostic[] {
|
|
346
|
+
const diagnostics: ProtocolDiagnostic[] = []
|
|
347
|
+
const header = (envelope as Partial<ProtocolEnvelope>).header ?? ({} as Partial<ProtocolHeader>)
|
|
348
|
+
const body = (envelope as Partial<ProtocolEnvelope>).body ?? ({} as Partial<ProtocolBody>)
|
|
349
|
+
|
|
350
|
+
const version = typeof header.version === 'string' ? header.version : ''
|
|
351
|
+
const messageId = typeof header.messageId === 'string' ? header.messageId : ''
|
|
352
|
+
const sender = typeof header.sender === 'string' ? header.sender : ''
|
|
353
|
+
const nonce = typeof header.nonce === 'string' ? header.nonce : ''
|
|
354
|
+
const timestampMs = typeof header.timestampMs === 'number' ? header.timestampMs : 0
|
|
355
|
+
const ttlMs = typeof header.ttlMs === 'number' ? header.ttlMs : 0
|
|
356
|
+
const content = typeof body.content === 'string' ? body.content : ''
|
|
357
|
+
|
|
358
|
+
if (version !== '7h3/0.1') {
|
|
359
|
+
diagnostics.push({ level: 'error', message: `Unsupported protocol version '${version}'` })
|
|
360
|
+
}
|
|
361
|
+
if (!messageId.trim()) {
|
|
362
|
+
diagnostics.push({ level: 'error', message: 'Missing messageId' })
|
|
363
|
+
}
|
|
364
|
+
if (!sender.trim()) {
|
|
365
|
+
diagnostics.push({ level: 'error', message: 'Missing sender identity' })
|
|
366
|
+
}
|
|
367
|
+
if (!nonce.trim()) {
|
|
368
|
+
diagnostics.push({ level: 'error', message: 'Missing nonce — replay protection requires a unique nonce per message' })
|
|
369
|
+
}
|
|
370
|
+
if (ttlMs <= 0) {
|
|
371
|
+
diagnostics.push({ level: 'error', message: 'ttlMs must be greater than zero' })
|
|
372
|
+
}
|
|
373
|
+
if (timestampMs + ttlMs < nowMs) {
|
|
374
|
+
diagnostics.push({ level: 'error', message: 'Message TTL expired' })
|
|
375
|
+
}
|
|
376
|
+
if (!content.trim()) {
|
|
377
|
+
diagnostics.push({ level: 'warning', message: 'Empty content payload' })
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return diagnostics
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function createEnvelope(input: {
|
|
384
|
+
sender: string
|
|
385
|
+
recipient?: string
|
|
386
|
+
intent: IntentKind
|
|
387
|
+
content: string
|
|
388
|
+
capability?: string
|
|
389
|
+
correlationId?: string
|
|
390
|
+
ttlMs?: number
|
|
391
|
+
messageId?: string
|
|
392
|
+
nonce?: string
|
|
393
|
+
nowMs?: number
|
|
394
|
+
}): Omit<ProtocolEnvelope, 'signature'> {
|
|
395
|
+
const nowMs = input.nowMs ?? Date.now()
|
|
396
|
+
return {
|
|
397
|
+
header: {
|
|
398
|
+
version: '7h3/0.1',
|
|
399
|
+
messageId: input.messageId ?? `msg-${nowMs}-${Math.random().toString(36).slice(2, 10)}`,
|
|
400
|
+
timestampMs: nowMs,
|
|
401
|
+
ttlMs: input.ttlMs ?? 60_000,
|
|
402
|
+
sender: input.sender,
|
|
403
|
+
recipient: input.recipient,
|
|
404
|
+
nonce: input.nonce ?? Math.random().toString(36).slice(2, 12),
|
|
405
|
+
},
|
|
406
|
+
body: {
|
|
407
|
+
intent: input.intent,
|
|
408
|
+
content: input.content,
|
|
409
|
+
capability: input.capability,
|
|
410
|
+
correlationId: input.correlationId,
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { AgentSession } from './protocolAgent'
|
|
3
|
+
import { negotiateAipCapabilities, parseAipCapabilities } from './protocolCapabilities'
|
|
4
|
+
import { encodeEnvelope, receiveEnvelope } from './protocolTransport'
|
|
5
|
+
|
|
6
|
+
describe('AgentSession protocol loop', () => {
|
|
7
|
+
const alpha = new AgentSession({
|
|
8
|
+
agentId: 'agent.alpha',
|
|
9
|
+
outboundSecret: 'alpha-secret',
|
|
10
|
+
sharedSecrets: {
|
|
11
|
+
'agent.beta': 'beta-secret',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const beta = new AgentSession({
|
|
16
|
+
agentId: 'agent.beta',
|
|
17
|
+
outboundSecret: 'beta-secret',
|
|
18
|
+
capabilities: ['math.mul2', 'task.plan'],
|
|
19
|
+
sharedSecrets: {
|
|
20
|
+
'agent.alpha': 'alpha-secret',
|
|
21
|
+
},
|
|
22
|
+
onTask: async (envelope) => ({
|
|
23
|
+
intent: 'RESULT',
|
|
24
|
+
content: `task accepted:${envelope.body.content}`,
|
|
25
|
+
capability: envelope.body.capability,
|
|
26
|
+
}),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('responds to PING with signed PONG', async () => {
|
|
30
|
+
const ping = await alpha.createSignedIntent({
|
|
31
|
+
recipient: 'agent.beta',
|
|
32
|
+
intent: 'PING',
|
|
33
|
+
content: 'hello',
|
|
34
|
+
messageId: 'p-1',
|
|
35
|
+
nonce: 'n-1',
|
|
36
|
+
nowMs: 1000,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const receivedByBeta = await beta.receiveAndRespond(encodeEnvelope(ping), 1000)
|
|
40
|
+
expect(receivedByBeta.ok).toBe(true)
|
|
41
|
+
expect(receivedByBeta.response?.body.intent).toBe('PONG')
|
|
42
|
+
|
|
43
|
+
const receivedByAlpha = await receiveEnvelope(receivedByBeta.response!, {
|
|
44
|
+
nowMs: 1001,
|
|
45
|
+
secretResolver: async () => 'beta-secret',
|
|
46
|
+
requireSignature: true,
|
|
47
|
+
})
|
|
48
|
+
expect(receivedByAlpha.ok).toBe(true)
|
|
49
|
+
expect(receivedByAlpha.envelope?.body.correlationId).toBe('p-1')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('responds to CAPS with capability payload', async () => {
|
|
53
|
+
const capsReq = await alpha.createSignedIntent({
|
|
54
|
+
recipient: 'agent.beta',
|
|
55
|
+
intent: 'CAPS',
|
|
56
|
+
content: 'what can you do?',
|
|
57
|
+
messageId: 'c-1',
|
|
58
|
+
nonce: 'n-2',
|
|
59
|
+
nowMs: 1100,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const receivedByBeta = await beta.receiveAndRespond(capsReq, 1100)
|
|
63
|
+
expect(receivedByBeta.ok).toBe(true)
|
|
64
|
+
expect(receivedByBeta.response?.body.intent).toBe('RESULT')
|
|
65
|
+
|
|
66
|
+
const parsed = JSON.parse(receivedByBeta.response?.body.content ?? '{}') as {
|
|
67
|
+
agent?: string
|
|
68
|
+
capabilities?: string[]
|
|
69
|
+
wireFormats?: string[]
|
|
70
|
+
batchMax?: number
|
|
71
|
+
}
|
|
72
|
+
expect(parsed.agent).toBe('agent.beta')
|
|
73
|
+
expect(parsed.capabilities).toContain('math.mul2')
|
|
74
|
+
expect(parsed.wireFormats).toContain('compact')
|
|
75
|
+
expect(parsed.batchMax).toBe(1)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('negotiates shared wire capabilities from CAPS payloads', () => {
|
|
79
|
+
const local = parseAipCapabilities('{"agent":"local","capabilities":[],"wireFormats":["binary","compact"],"batchMax":16,"ackModes":["fast","receipt"]}')
|
|
80
|
+
const remote = parseAipCapabilities('{"agent":"remote","capabilities":[],"wireFormats":["compact","json"],"batchMax":8,"ackModes":["receipt"]}')
|
|
81
|
+
expect(local).not.toBeNull()
|
|
82
|
+
expect(remote).not.toBeNull()
|
|
83
|
+
|
|
84
|
+
const negotiated = negotiateAipCapabilities(local!, remote!)
|
|
85
|
+
expect(negotiated).toEqual({ wireFormat: 'compact', batchMax: 8, ackMode: 'receipt' })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('handles TASK with custom task handler', async () => {
|
|
89
|
+
const task = await alpha.createSignedIntent({
|
|
90
|
+
recipient: 'agent.beta',
|
|
91
|
+
intent: 'TASK',
|
|
92
|
+
content: 'plan route',
|
|
93
|
+
capability: 'task.plan',
|
|
94
|
+
messageId: 't-1',
|
|
95
|
+
nonce: 'n-3',
|
|
96
|
+
nowMs: 1200,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const receivedByBeta = await beta.receiveAndRespond(task, 1200)
|
|
100
|
+
expect(receivedByBeta.ok).toBe(true)
|
|
101
|
+
expect(receivedByBeta.response?.body.intent).toBe('RESULT')
|
|
102
|
+
expect(receivedByBeta.response?.body.content).toContain('task accepted:plan route')
|
|
103
|
+
expect(receivedByBeta.response?.body.correlationId).toBe('t-1')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createEnvelope, signEnvelopeHmac, type IntentKind, type ProtocolDiagnostic, type ProtocolEnvelope } from './protocol'
|
|
2
|
+
import { createAipCapabilities, encodeAipCapabilities, type AckMode } from './protocolCapabilities'
|
|
3
|
+
import { InMemoryReplayCache } from './protocolReplay'
|
|
4
|
+
import { receiveEnvelope, type WireEnvelope, type WireFormat } from './protocolTransport'
|
|
5
|
+
|
|
6
|
+
export interface TaskHandlerResult {
|
|
7
|
+
intent?: 'RESULT' | 'ERROR'
|
|
8
|
+
content: string
|
|
9
|
+
capability?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AgentSessionConfig {
|
|
13
|
+
agentId: string
|
|
14
|
+
outboundSecret: string
|
|
15
|
+
keyId?: string
|
|
16
|
+
capabilities?: string[]
|
|
17
|
+
supportedWireFormats?: WireFormat[]
|
|
18
|
+
maxBatchSize?: number
|
|
19
|
+
ackModes?: AckMode[]
|
|
20
|
+
requireSignature?: boolean
|
|
21
|
+
replayCache?: InMemoryReplayCache
|
|
22
|
+
sharedSecrets?: Record<string, string>
|
|
23
|
+
resolveInboundSecret?: (keyId: string, sender: string) => string | undefined | Promise<string | undefined>
|
|
24
|
+
onTask?: (envelope: ProtocolEnvelope) => TaskHandlerResult | Promise<TaskHandlerResult>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentReceiveResult {
|
|
28
|
+
ok: boolean
|
|
29
|
+
diagnostics: ProtocolDiagnostic[]
|
|
30
|
+
received: ProtocolEnvelope | null
|
|
31
|
+
response: ProtocolEnvelope | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class AgentSession {
|
|
35
|
+
private readonly agentId: string
|
|
36
|
+
private readonly outboundSecret: string
|
|
37
|
+
private readonly keyId: string
|
|
38
|
+
private readonly capabilities: string[]
|
|
39
|
+
private readonly supportedWireFormats: WireFormat[]
|
|
40
|
+
private readonly maxBatchSize: number
|
|
41
|
+
private readonly ackModes: AckMode[]
|
|
42
|
+
private readonly requireSignature: boolean
|
|
43
|
+
private readonly replayCache: InMemoryReplayCache
|
|
44
|
+
private readonly sharedSecrets: Record<string, string>
|
|
45
|
+
private readonly resolveInboundSecret?: (keyId: string, sender: string) => string | undefined | Promise<string | undefined>
|
|
46
|
+
private readonly onTask?: (envelope: ProtocolEnvelope) => TaskHandlerResult | Promise<TaskHandlerResult>
|
|
47
|
+
|
|
48
|
+
constructor(config: AgentSessionConfig) {
|
|
49
|
+
this.agentId = config.agentId
|
|
50
|
+
this.outboundSecret = config.outboundSecret
|
|
51
|
+
this.keyId = config.keyId ?? `${config.agentId}-k1`
|
|
52
|
+
this.capabilities = config.capabilities ?? []
|
|
53
|
+
this.supportedWireFormats = config.supportedWireFormats ?? ['compact', 'json']
|
|
54
|
+
this.maxBatchSize = config.maxBatchSize ?? 1
|
|
55
|
+
this.ackModes = config.ackModes ?? ['receipt']
|
|
56
|
+
this.requireSignature = config.requireSignature ?? true
|
|
57
|
+
this.replayCache = config.replayCache ?? new InMemoryReplayCache()
|
|
58
|
+
this.sharedSecrets = config.sharedSecrets ?? {}
|
|
59
|
+
this.resolveInboundSecret = config.resolveInboundSecret
|
|
60
|
+
this.onTask = config.onTask
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async inboundSecretResolver(keyId: string, sender: string): Promise<string | undefined> {
|
|
64
|
+
if (this.resolveInboundSecret) {
|
|
65
|
+
return this.resolveInboundSecret(keyId, sender)
|
|
66
|
+
}
|
|
67
|
+
return this.sharedSecrets[sender]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createSignedIntent(input: {
|
|
71
|
+
recipient?: string
|
|
72
|
+
intent: IntentKind
|
|
73
|
+
content: string
|
|
74
|
+
capability?: string
|
|
75
|
+
correlationId?: string
|
|
76
|
+
ttlMs?: number
|
|
77
|
+
messageId?: string
|
|
78
|
+
nonce?: string
|
|
79
|
+
nowMs?: number
|
|
80
|
+
}): Promise<ProtocolEnvelope> {
|
|
81
|
+
const unsigned = createEnvelope({
|
|
82
|
+
sender: this.agentId,
|
|
83
|
+
recipient: input.recipient,
|
|
84
|
+
intent: input.intent,
|
|
85
|
+
content: input.content,
|
|
86
|
+
capability: input.capability,
|
|
87
|
+
correlationId: input.correlationId,
|
|
88
|
+
ttlMs: input.ttlMs,
|
|
89
|
+
messageId: input.messageId,
|
|
90
|
+
nonce: input.nonce,
|
|
91
|
+
nowMs: input.nowMs,
|
|
92
|
+
})
|
|
93
|
+
return signEnvelopeHmac(unsigned, this.outboundSecret, this.keyId)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async receiveAndRespond(input: WireEnvelope | ProtocolEnvelope, nowMs = Date.now()): Promise<AgentReceiveResult> {
|
|
97
|
+
const received = await receiveEnvelope(input, {
|
|
98
|
+
nowMs,
|
|
99
|
+
requireSignature: this.requireSignature,
|
|
100
|
+
replayCache: this.replayCache,
|
|
101
|
+
secretResolver: (keyId, sender) => this.inboundSecretResolver(keyId, sender),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if (!received.ok || !received.envelope) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
diagnostics: received.diagnostics,
|
|
108
|
+
received: received.envelope,
|
|
109
|
+
response: null,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const envelope = received.envelope
|
|
114
|
+
const response = await this.buildAutoResponse(envelope, nowMs)
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
diagnostics: received.diagnostics,
|
|
118
|
+
received: envelope,
|
|
119
|
+
response,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async buildAutoResponse(envelope: ProtocolEnvelope, nowMs: number): Promise<ProtocolEnvelope | null> {
|
|
124
|
+
if (envelope.body.intent === 'PING') {
|
|
125
|
+
return this.createSignedIntent({
|
|
126
|
+
recipient: envelope.header.sender,
|
|
127
|
+
intent: 'PONG',
|
|
128
|
+
content: 'pong',
|
|
129
|
+
correlationId: envelope.header.messageId,
|
|
130
|
+
nowMs,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (envelope.body.intent === 'CAPS') {
|
|
135
|
+
return this.createSignedIntent({
|
|
136
|
+
recipient: envelope.header.sender,
|
|
137
|
+
intent: 'RESULT',
|
|
138
|
+
content: encodeAipCapabilities(
|
|
139
|
+
createAipCapabilities({
|
|
140
|
+
agent: this.agentId,
|
|
141
|
+
capabilities: this.capabilities,
|
|
142
|
+
wireFormats: this.supportedWireFormats,
|
|
143
|
+
batchMax: this.maxBatchSize,
|
|
144
|
+
ackModes: this.ackModes,
|
|
145
|
+
}),
|
|
146
|
+
),
|
|
147
|
+
capability: 'caps',
|
|
148
|
+
correlationId: envelope.header.messageId,
|
|
149
|
+
nowMs,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (envelope.body.intent === 'TASK') {
|
|
154
|
+
const taskResult = this.onTask
|
|
155
|
+
? await this.onTask(envelope)
|
|
156
|
+
: { intent: 'RESULT' as const, content: 'ACK', capability: envelope.body.capability }
|
|
157
|
+
return this.createSignedIntent({
|
|
158
|
+
recipient: envelope.header.sender,
|
|
159
|
+
intent: taskResult.intent ?? 'RESULT',
|
|
160
|
+
content: taskResult.content,
|
|
161
|
+
capability: taskResult.capability,
|
|
162
|
+
correlationId: envelope.header.messageId,
|
|
163
|
+
nowMs,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
}
|