@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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
use protocol_7h3::keys::{
|
|
2
|
+
KeyEntry, KeyRotationManager, ManagedKeyPair, RevocationRegistry, WellKnownKeysDocument,
|
|
3
|
+
};
|
|
4
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
5
|
+
|
|
6
|
+
fn now_ms() -> u64 {
|
|
7
|
+
SystemTime::now()
|
|
8
|
+
.duration_since(UNIX_EPOCH)
|
|
9
|
+
.unwrap_or_default()
|
|
10
|
+
.as_millis() as u64
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn make_key_entry(id: &str, revoked: Option<bool>, expires: Option<u64>) -> KeyEntry {
|
|
14
|
+
KeyEntry {
|
|
15
|
+
id: id.to_string(),
|
|
16
|
+
algorithm: "Ed25519".to_string(),
|
|
17
|
+
public_key: "dGVzdC1wdWJsaWMta2V5".to_string(),
|
|
18
|
+
created: now_ms() - 1000,
|
|
19
|
+
expires,
|
|
20
|
+
revoked,
|
|
21
|
+
revoked_at: None,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 1. WellKnownKeysDocument round-trip JSON serialization
|
|
26
|
+
#[test]
|
|
27
|
+
fn test_well_known_document_json_round_trip() {
|
|
28
|
+
let entry = make_key_entry("key-1", None, None);
|
|
29
|
+
let doc = WellKnownKeysDocument::new(vec![entry]);
|
|
30
|
+
|
|
31
|
+
let json = doc.to_json().expect("serialization should succeed");
|
|
32
|
+
let restored = WellKnownKeysDocument::from_json(&json).expect("deserialization should succeed");
|
|
33
|
+
|
|
34
|
+
assert_eq!(restored.version, "7h3/0.1");
|
|
35
|
+
assert_eq!(restored.keys.len(), 1);
|
|
36
|
+
assert_eq!(restored.keys[0].id, "key-1");
|
|
37
|
+
assert_eq!(restored.keys[0].algorithm, "Ed25519");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. get_active_key finds non-revoked keys
|
|
41
|
+
#[test]
|
|
42
|
+
fn test_get_active_key_finds_active() {
|
|
43
|
+
let entry = make_key_entry("key-active", None, None);
|
|
44
|
+
let doc = WellKnownKeysDocument::new(vec![entry]);
|
|
45
|
+
|
|
46
|
+
let found = doc.get_active_key("key-active");
|
|
47
|
+
assert!(found.is_some(), "should find an active key");
|
|
48
|
+
assert_eq!(found.unwrap().id, "key-active");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. get_active_key ignores revoked keys
|
|
52
|
+
#[test]
|
|
53
|
+
fn test_get_active_key_ignores_revoked() {
|
|
54
|
+
let revoked = make_key_entry("key-revoked", Some(true), None);
|
|
55
|
+
let doc = WellKnownKeysDocument::new(vec![revoked]);
|
|
56
|
+
|
|
57
|
+
let found = doc.get_active_key("key-revoked");
|
|
58
|
+
assert!(found.is_none(), "should NOT find a revoked key");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3b. get_active_key ignores expired keys
|
|
62
|
+
#[test]
|
|
63
|
+
fn test_get_active_key_ignores_expired() {
|
|
64
|
+
// expires 10 seconds in the past
|
|
65
|
+
let expired = make_key_entry("key-expired", None, Some(now_ms() - 10_000));
|
|
66
|
+
let doc = WellKnownKeysDocument::new(vec![expired]);
|
|
67
|
+
|
|
68
|
+
let found = doc.get_active_key("key-expired");
|
|
69
|
+
assert!(found.is_none(), "should NOT find an expired key");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 4. KeyRotationManager: add_key + get_current_key
|
|
73
|
+
#[test]
|
|
74
|
+
fn test_rotation_manager_add_and_get_current() {
|
|
75
|
+
let mgr = KeyRotationManager::new(86_400_000, 3_600_000);
|
|
76
|
+
|
|
77
|
+
let pair = ManagedKeyPair {
|
|
78
|
+
id: "rot-key-1".to_string(),
|
|
79
|
+
public_key: "pub1".to_string(),
|
|
80
|
+
private_key: "priv1".to_string(),
|
|
81
|
+
created: now_ms(),
|
|
82
|
+
expires_at: None,
|
|
83
|
+
};
|
|
84
|
+
mgr.add_key(pair);
|
|
85
|
+
|
|
86
|
+
let current = mgr.get_current_key();
|
|
87
|
+
assert!(current.is_some(), "should return current key");
|
|
88
|
+
assert_eq!(current.unwrap().id, "rot-key-1");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 4b. get_current_key returns the most recent non-expired key
|
|
92
|
+
#[test]
|
|
93
|
+
fn test_rotation_manager_returns_most_recent() {
|
|
94
|
+
let mgr = KeyRotationManager::new(86_400_000, 3_600_000);
|
|
95
|
+
let base = now_ms();
|
|
96
|
+
|
|
97
|
+
mgr.add_key(ManagedKeyPair {
|
|
98
|
+
id: "older".to_string(),
|
|
99
|
+
public_key: "pub-older".to_string(),
|
|
100
|
+
private_key: "priv-older".to_string(),
|
|
101
|
+
created: base - 5000,
|
|
102
|
+
expires_at: None,
|
|
103
|
+
});
|
|
104
|
+
mgr.add_key(ManagedKeyPair {
|
|
105
|
+
id: "newer".to_string(),
|
|
106
|
+
public_key: "pub-newer".to_string(),
|
|
107
|
+
private_key: "priv-newer".to_string(),
|
|
108
|
+
created: base,
|
|
109
|
+
expires_at: None,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let current = mgr.get_current_key().expect("should have a current key");
|
|
113
|
+
assert_eq!(current.id, "newer");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4c. get_current_key excludes expired keys
|
|
117
|
+
#[test]
|
|
118
|
+
fn test_rotation_manager_excludes_expired() {
|
|
119
|
+
let mgr = KeyRotationManager::new(86_400_000, 3_600_000);
|
|
120
|
+
|
|
121
|
+
mgr.add_key(ManagedKeyPair {
|
|
122
|
+
id: "exp-key".to_string(),
|
|
123
|
+
public_key: "pub-exp".to_string(),
|
|
124
|
+
private_key: "priv-exp".to_string(),
|
|
125
|
+
created: now_ms() - 2000,
|
|
126
|
+
expires_at: Some(now_ms() - 1000), // already expired
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let current = mgr.get_current_key();
|
|
130
|
+
assert!(current.is_none(), "expired key should not be returned as current");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 5. KeyRotationManager: get_well_known_document structure
|
|
134
|
+
#[test]
|
|
135
|
+
fn test_rotation_manager_well_known_document() {
|
|
136
|
+
let mgr = KeyRotationManager::new(86_400_000, 3_600_000);
|
|
137
|
+
|
|
138
|
+
mgr.add_key(ManagedKeyPair {
|
|
139
|
+
id: "doc-key-1".to_string(),
|
|
140
|
+
public_key: "pub-doc".to_string(),
|
|
141
|
+
private_key: "priv-doc".to_string(),
|
|
142
|
+
created: now_ms(),
|
|
143
|
+
expires_at: None,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
let doc = mgr.get_well_known_document();
|
|
147
|
+
assert_eq!(doc.version, "7h3/0.1");
|
|
148
|
+
assert_eq!(doc.keys.len(), 1);
|
|
149
|
+
assert_eq!(doc.keys[0].id, "doc-key-1");
|
|
150
|
+
assert_eq!(doc.keys[0].algorithm, "Ed25519");
|
|
151
|
+
assert_eq!(doc.keys[0].public_key, "pub-doc");
|
|
152
|
+
// non-expired key should not be marked revoked
|
|
153
|
+
assert!(doc.keys[0].revoked.is_none() || !doc.keys[0].revoked.unwrap());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 6. RevocationRegistry: revoke + is_revoked
|
|
157
|
+
#[test]
|
|
158
|
+
fn test_revocation_registry_revoke_and_check() {
|
|
159
|
+
let registry = RevocationRegistry::new();
|
|
160
|
+
|
|
161
|
+
assert!(!registry.is_revoked("key-x"), "should not be revoked initially");
|
|
162
|
+
registry.revoke("key-x", Some("compromised"));
|
|
163
|
+
assert!(registry.is_revoked("key-x"), "should be revoked after revoke()");
|
|
164
|
+
assert!(!registry.is_revoked("key-y"), "unrelated key should not be revoked");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 7. RevocationRegistry: get_list contains correct fields
|
|
168
|
+
#[test]
|
|
169
|
+
fn test_revocation_registry_get_list() {
|
|
170
|
+
let registry = RevocationRegistry::new();
|
|
171
|
+
registry.revoke("key-a", Some("expired"));
|
|
172
|
+
registry.revoke("key-b", None);
|
|
173
|
+
|
|
174
|
+
let list = registry.get_list();
|
|
175
|
+
|
|
176
|
+
assert_eq!(list["version"], "7h3/0.1");
|
|
177
|
+
let revoked_keys = list["revokedKeys"].as_array().expect("revokedKeys should be array");
|
|
178
|
+
assert_eq!(revoked_keys.len(), 2);
|
|
179
|
+
|
|
180
|
+
// Find key-a in the list
|
|
181
|
+
let key_a = revoked_keys.iter().find(|e| e["id"] == "key-a").expect("key-a should be in list");
|
|
182
|
+
assert!(key_a["revokedAt"].as_u64().unwrap() > 0, "revokedAt should be a positive timestamp");
|
|
183
|
+
assert_eq!(key_a["reason"], "expired");
|
|
184
|
+
|
|
185
|
+
// Find key-b in the list — no reason
|
|
186
|
+
let key_b = revoked_keys.iter().find(|e| e["id"] == "key-b").expect("key-b should be in list");
|
|
187
|
+
assert!(key_b["revokedAt"].as_u64().unwrap() > 0);
|
|
188
|
+
assert!(key_b.get("reason").is_none() || key_b["reason"].is_null());
|
|
189
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createAipAgentAdapter } from './agentAdapter'
|
|
3
|
+
import type { WireEnvelope } from './protocolTransport'
|
|
4
|
+
|
|
5
|
+
describe('AIP agent adapter', () => {
|
|
6
|
+
it('supports raw task request/response flow', async () => {
|
|
7
|
+
const secret = 'adapter-shared-secret'
|
|
8
|
+
|
|
9
|
+
const coordinator = createAipAgentAdapter({
|
|
10
|
+
agentId: 'agent.coordinator',
|
|
11
|
+
outboundSecret: secret,
|
|
12
|
+
sharedSecrets: { 'agent.worker': secret },
|
|
13
|
+
wireFormat: 'compact',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const worker = createAipAgentAdapter({
|
|
17
|
+
agentId: 'agent.worker',
|
|
18
|
+
outboundSecret: secret,
|
|
19
|
+
sharedSecrets: { 'agent.coordinator': secret },
|
|
20
|
+
wireFormat: 'compact',
|
|
21
|
+
onTask: async (envelope) => ({
|
|
22
|
+
intent: 'RESULT',
|
|
23
|
+
content: `handled:${envelope.body.content}`,
|
|
24
|
+
capability: envelope.body.capability,
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const taskRaw = await coordinator.createRawIntent({
|
|
29
|
+
recipient: 'agent.worker',
|
|
30
|
+
intent: 'TASK',
|
|
31
|
+
content: 'optimize route',
|
|
32
|
+
capability: 'task.plan',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
let responseRaw: WireEnvelope = ''
|
|
36
|
+
const handled = await worker.handleRaw(taskRaw, async (rawResponse) => {
|
|
37
|
+
responseRaw = rawResponse
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(handled.ok).toBe(true)
|
|
41
|
+
expect(responseRaw.length).toBeGreaterThan(0)
|
|
42
|
+
|
|
43
|
+
const verification = await coordinator.receiveRaw(responseRaw)
|
|
44
|
+
expect(verification.ok).toBe(true)
|
|
45
|
+
expect(verification.received?.body.intent).toBe('RESULT')
|
|
46
|
+
expect(verification.received?.body.content).toContain('handled:optimize route')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type AgentReceiveResult, type AgentSessionConfig, AgentSession } from './protocolAgent'
|
|
2
|
+
import { type IntentKind, type ProtocolEnvelope } from './protocol'
|
|
3
|
+
import { encodeEnvelope, type WireEnvelope, type WireFormat } from './protocolTransport'
|
|
4
|
+
|
|
5
|
+
export interface AipAdapterConfig extends AgentSessionConfig {
|
|
6
|
+
wireFormat?: WireFormat
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class AipAgentAdapter {
|
|
10
|
+
private readonly session: AgentSession
|
|
11
|
+
private readonly wireFormat: WireFormat
|
|
12
|
+
|
|
13
|
+
constructor(config: AipAdapterConfig) {
|
|
14
|
+
this.session = new AgentSession(config)
|
|
15
|
+
this.wireFormat = config.wireFormat ?? 'compact'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toRaw(envelope: ProtocolEnvelope): WireEnvelope {
|
|
19
|
+
return encodeEnvelope(envelope, this.wireFormat)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async createRawIntent(input: {
|
|
23
|
+
recipient?: string
|
|
24
|
+
intent: IntentKind
|
|
25
|
+
content: string
|
|
26
|
+
capability?: string
|
|
27
|
+
correlationId?: string
|
|
28
|
+
ttlMs?: number
|
|
29
|
+
messageId?: string
|
|
30
|
+
nonce?: string
|
|
31
|
+
nowMs?: number
|
|
32
|
+
}): Promise<WireEnvelope> {
|
|
33
|
+
const envelope = await this.session.createSignedIntent(input)
|
|
34
|
+
return this.toRaw(envelope)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async receiveRaw(raw: WireEnvelope, nowMs = Date.now()): Promise<AgentReceiveResult> {
|
|
38
|
+
return this.session.receiveAndRespond(raw, nowMs)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async handleRaw(
|
|
42
|
+
raw: WireEnvelope,
|
|
43
|
+
emitResponseRaw: (rawResponse: WireEnvelope, responseEnvelope: ProtocolEnvelope) => void | Promise<void>,
|
|
44
|
+
nowMs = Date.now(),
|
|
45
|
+
): Promise<AgentReceiveResult> {
|
|
46
|
+
const result = await this.session.receiveAndRespond(raw, nowMs)
|
|
47
|
+
if (result.response) {
|
|
48
|
+
await emitResponseRaw(this.toRaw(result.response), result.response)
|
|
49
|
+
}
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createAipAgentAdapter(config: AipAdapterConfig): AipAgentAdapter {
|
|
55
|
+
return new AipAgentAdapter(config)
|
|
56
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
2
|
+
import { createAuditLog, NoopAuditLog, type AuditEntry } from './auditLog'
|
|
3
|
+
import { generateEd25519KeypairBase64Url } from './protocol'
|
|
4
|
+
|
|
5
|
+
let keys: { publicKey: string; privateKey: string }
|
|
6
|
+
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
keys = await generateEd25519KeypairBase64Url()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('InMemoryAuditLog', () => {
|
|
12
|
+
it('logs and queries entries', async () => {
|
|
13
|
+
const log = createAuditLog(keys.privateKey)
|
|
14
|
+
await log.log({ type: 'verify-ok', sender: 'agent-a', path: '/api/test', method: 'GET' })
|
|
15
|
+
await log.log({ type: 'verify-fail', sender: 'agent-b', path: '/api/test', method: 'POST', failReason: 'invalid-signature' })
|
|
16
|
+
|
|
17
|
+
const all = await log.query()
|
|
18
|
+
expect(all).toHaveLength(2)
|
|
19
|
+
expect(log.size()).toBe(2)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('query filters by type', async () => {
|
|
23
|
+
const log = createAuditLog(keys.privateKey)
|
|
24
|
+
await log.log({ type: 'verify-ok', sender: 'alice' })
|
|
25
|
+
await log.log({ type: 'verify-fail', sender: 'bob', failReason: 'expired' })
|
|
26
|
+
await log.log({ type: 'rate-limited', sender: 'alice' })
|
|
27
|
+
|
|
28
|
+
const oks = await log.query({ type: 'verify-ok' })
|
|
29
|
+
expect(oks).toHaveLength(1)
|
|
30
|
+
expect(oks[0].type).toBe('verify-ok')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('query filters by sender', async () => {
|
|
34
|
+
const log = createAuditLog(keys.privateKey)
|
|
35
|
+
await log.log({ type: 'verify-ok', sender: 'alice' })
|
|
36
|
+
await log.log({ type: 'verify-ok', sender: 'bob' })
|
|
37
|
+
await log.log({ type: 'verify-fail', sender: 'alice', failReason: 'expired' })
|
|
38
|
+
|
|
39
|
+
const aliceEntries = await log.query({ sender: 'alice' })
|
|
40
|
+
expect(aliceEntries).toHaveLength(2)
|
|
41
|
+
expect(aliceEntries.every(e => e.sender === 'alice')).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('query filters by since', async () => {
|
|
45
|
+
const log = createAuditLog(keys.privateKey)
|
|
46
|
+
const before = Date.now()
|
|
47
|
+
await log.log({ type: 'verify-ok', sender: 'alice' })
|
|
48
|
+
await new Promise(r => setTimeout(r, 5))
|
|
49
|
+
const mid = Date.now()
|
|
50
|
+
await log.log({ type: 'verify-ok', sender: 'bob' })
|
|
51
|
+
|
|
52
|
+
const recent = await log.query({ since: mid })
|
|
53
|
+
expect(recent).toHaveLength(1)
|
|
54
|
+
expect(recent[0].sender).toBe('bob')
|
|
55
|
+
|
|
56
|
+
const all = await log.query({ since: before - 1 })
|
|
57
|
+
expect(all).toHaveLength(2)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('query respects limit', async () => {
|
|
61
|
+
const log = createAuditLog(keys.privateKey)
|
|
62
|
+
for (let i = 0; i < 5; i++) {
|
|
63
|
+
await log.log({ type: 'verify-ok', sender: `agent-${i}` })
|
|
64
|
+
}
|
|
65
|
+
const limited = await log.query({ limit: 2 })
|
|
66
|
+
expect(limited).toHaveLength(2)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('verify() returns true for valid entry', async () => {
|
|
70
|
+
const log = createAuditLog(keys.privateKey)
|
|
71
|
+
await log.log({ type: 'verify-ok', sender: 'alice', path: '/api/test' })
|
|
72
|
+
|
|
73
|
+
const entries = await log.query()
|
|
74
|
+
expect(entries).toHaveLength(1)
|
|
75
|
+
const valid = await log.verify(entries[0], keys.publicKey)
|
|
76
|
+
expect(valid).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('verify() returns false for tampered entry', async () => {
|
|
80
|
+
const log = createAuditLog(keys.privateKey)
|
|
81
|
+
await log.log({ type: 'verify-ok', sender: 'alice', path: '/api/test' })
|
|
82
|
+
|
|
83
|
+
const entries = await log.query()
|
|
84
|
+
const tampered: AuditEntry = { ...entries[0], sender: 'eve' }
|
|
85
|
+
const valid = await log.verify(tampered, keys.publicKey)
|
|
86
|
+
expect(valid).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('verify() returns false with wrong public key', async () => {
|
|
90
|
+
const log = createAuditLog(keys.privateKey)
|
|
91
|
+
await log.log({ type: 'verify-ok', sender: 'alice' })
|
|
92
|
+
|
|
93
|
+
const wrongKeys = await generateEd25519KeypairBase64Url()
|
|
94
|
+
const entries = await log.query()
|
|
95
|
+
const valid = await log.verify(entries[0], wrongKeys.publicKey)
|
|
96
|
+
expect(valid).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('respects maxEntries by evicting oldest', async () => {
|
|
100
|
+
const log = createAuditLog(keys.privateKey, { maxEntries: 3 })
|
|
101
|
+
for (let i = 0; i < 5; i++) {
|
|
102
|
+
await log.log({ type: 'verify-ok', sender: `agent-${i}` })
|
|
103
|
+
}
|
|
104
|
+
expect(log.size()).toBe(3)
|
|
105
|
+
const entries = await log.query()
|
|
106
|
+
// Should have the last 3 (agents 2, 3, 4)
|
|
107
|
+
expect(entries[0].sender).toBe('agent-2')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('entries have id, timestampMs, and entrySignature', async () => {
|
|
111
|
+
const log = createAuditLog(keys.privateKey)
|
|
112
|
+
await log.log({ type: 'response-signed', upstream: 'http://upstream' })
|
|
113
|
+
|
|
114
|
+
const entries = await log.query()
|
|
115
|
+
expect(typeof entries[0].id).toBe('string')
|
|
116
|
+
expect(typeof entries[0].timestampMs).toBe('number')
|
|
117
|
+
expect(typeof entries[0].entrySignature).toBe('string')
|
|
118
|
+
expect(entries[0].entrySignature.length).toBeGreaterThan(0)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('NoopAuditLog', () => {
|
|
123
|
+
it('log does nothing', async () => {
|
|
124
|
+
const log = new NoopAuditLog()
|
|
125
|
+
await log.log({ type: 'verify-ok', sender: 'alice' })
|
|
126
|
+
expect(log.size()).toBe(0)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('query returns empty array', async () => {
|
|
130
|
+
const log = new NoopAuditLog()
|
|
131
|
+
const result = await log.query()
|
|
132
|
+
expect(result).toEqual([])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('verify always returns false', async () => {
|
|
136
|
+
const log = new NoopAuditLog()
|
|
137
|
+
const fakeEntry = {
|
|
138
|
+
id: 'x',
|
|
139
|
+
timestampMs: Date.now(),
|
|
140
|
+
type: 'verify-ok' as const,
|
|
141
|
+
entrySignature: 'fake',
|
|
142
|
+
}
|
|
143
|
+
expect(await log.verify(fakeEntry, 'key')).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
})
|
package/src/auditLog.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
signCanonicalPayloadEd25519,
|
|
3
|
+
verifyCanonicalPayloadEd25519,
|
|
4
|
+
} from './protocol'
|
|
5
|
+
|
|
6
|
+
export type AuditEventType =
|
|
7
|
+
| 'verify-ok'
|
|
8
|
+
| 'verify-fail'
|
|
9
|
+
| 'rate-limited'
|
|
10
|
+
| 'sender-denied'
|
|
11
|
+
| 'response-signed'
|
|
12
|
+
|
|
13
|
+
export interface AuditEntry {
|
|
14
|
+
id: string
|
|
15
|
+
timestampMs: number
|
|
16
|
+
type: AuditEventType
|
|
17
|
+
sender?: string
|
|
18
|
+
path?: string
|
|
19
|
+
method?: string
|
|
20
|
+
envelopeId?: string
|
|
21
|
+
failReason?: string
|
|
22
|
+
upstream?: string
|
|
23
|
+
responseStatus?: number
|
|
24
|
+
entrySignature: string // Ed25519 sig over the entry (minus entrySignature itself)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Produce a canonical payload string from an AuditEntry, excluding entrySignature.
|
|
29
|
+
* Uses explicit key order and omits undefined fields for determinism.
|
|
30
|
+
*/
|
|
31
|
+
function canonicalAuditPayload(entry: Omit<AuditEntry, 'entrySignature'> & { entrySignature?: string }): string {
|
|
32
|
+
const parts: string[] = [
|
|
33
|
+
`"id":${JSON.stringify(entry.id)}`,
|
|
34
|
+
`"timestampMs":${entry.timestampMs}`,
|
|
35
|
+
`"type":${JSON.stringify(entry.type)}`,
|
|
36
|
+
]
|
|
37
|
+
if (entry.sender !== undefined) parts.push(`"sender":${JSON.stringify(entry.sender)}`)
|
|
38
|
+
if (entry.path !== undefined) parts.push(`"path":${JSON.stringify(entry.path)}`)
|
|
39
|
+
if (entry.method !== undefined) parts.push(`"method":${JSON.stringify(entry.method)}`)
|
|
40
|
+
if (entry.envelopeId !== undefined) parts.push(`"envelopeId":${JSON.stringify(entry.envelopeId)}`)
|
|
41
|
+
if (entry.failReason !== undefined) parts.push(`"failReason":${JSON.stringify(entry.failReason)}`)
|
|
42
|
+
if (entry.upstream !== undefined) parts.push(`"upstream":${JSON.stringify(entry.upstream)}`)
|
|
43
|
+
if (entry.responseStatus !== undefined) parts.push(`"responseStatus":${entry.responseStatus}`)
|
|
44
|
+
return `{${parts.join(',')}}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AuditLogger {
|
|
48
|
+
log(event: Omit<AuditEntry, 'id' | 'timestampMs' | 'entrySignature'>): Promise<void>
|
|
49
|
+
query(opts?: { type?: AuditEventType; sender?: string; since?: number; limit?: number }): Promise<AuditEntry[]>
|
|
50
|
+
verify(entry: AuditEntry, publicKey: string): Promise<boolean>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class InMemoryAuditLog implements AuditLogger {
|
|
54
|
+
private entries: AuditEntry[] = []
|
|
55
|
+
private privateKey: string
|
|
56
|
+
private maxEntries: number
|
|
57
|
+
|
|
58
|
+
constructor(privateKey: string, maxEntries = 10_000) {
|
|
59
|
+
this.privateKey = privateKey
|
|
60
|
+
this.maxEntries = maxEntries
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async log(event: Omit<AuditEntry, 'id' | 'timestampMs' | 'entrySignature'>): Promise<void> {
|
|
64
|
+
const id = `audit-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
65
|
+
const timestampMs = Date.now()
|
|
66
|
+
|
|
67
|
+
const partial: Omit<AuditEntry, 'entrySignature'> = { id, timestampMs, ...event }
|
|
68
|
+
const payload = canonicalAuditPayload(partial)
|
|
69
|
+
const entrySignature = await signCanonicalPayloadEd25519(payload, this.privateKey)
|
|
70
|
+
|
|
71
|
+
const entry: AuditEntry = { ...partial, entrySignature }
|
|
72
|
+
|
|
73
|
+
if (this.entries.length >= this.maxEntries) {
|
|
74
|
+
this.entries.shift()
|
|
75
|
+
}
|
|
76
|
+
this.entries.push(entry)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async query(opts?: {
|
|
80
|
+
type?: AuditEventType
|
|
81
|
+
sender?: string
|
|
82
|
+
since?: number
|
|
83
|
+
limit?: number
|
|
84
|
+
}): Promise<AuditEntry[]> {
|
|
85
|
+
let results = [...this.entries]
|
|
86
|
+
|
|
87
|
+
if (opts?.type !== undefined) {
|
|
88
|
+
results = results.filter(e => e.type === opts.type)
|
|
89
|
+
}
|
|
90
|
+
if (opts?.sender !== undefined) {
|
|
91
|
+
results = results.filter(e => e.sender === opts.sender)
|
|
92
|
+
}
|
|
93
|
+
if (opts?.since !== undefined) {
|
|
94
|
+
results = results.filter(e => e.timestampMs >= opts.since!)
|
|
95
|
+
}
|
|
96
|
+
if (opts?.limit !== undefined) {
|
|
97
|
+
results = results.slice(-opts.limit)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return results
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async verify(entry: AuditEntry, publicKey: string): Promise<boolean> {
|
|
104
|
+
const partial: Omit<AuditEntry, 'entrySignature'> = {
|
|
105
|
+
id: entry.id,
|
|
106
|
+
timestampMs: entry.timestampMs,
|
|
107
|
+
type: entry.type,
|
|
108
|
+
sender: entry.sender,
|
|
109
|
+
path: entry.path,
|
|
110
|
+
method: entry.method,
|
|
111
|
+
envelopeId: entry.envelopeId,
|
|
112
|
+
failReason: entry.failReason,
|
|
113
|
+
upstream: entry.upstream,
|
|
114
|
+
responseStatus: entry.responseStatus,
|
|
115
|
+
}
|
|
116
|
+
const payload = canonicalAuditPayload(partial)
|
|
117
|
+
return verifyCanonicalPayloadEd25519(payload, entry.entrySignature, publicKey)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
size(): number {
|
|
121
|
+
return this.entries.length
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class NoopAuditLog implements AuditLogger {
|
|
126
|
+
async log(_event: Omit<AuditEntry, 'id' | 'timestampMs' | 'entrySignature'>): Promise<void> {}
|
|
127
|
+
async query(_opts?: {
|
|
128
|
+
type?: AuditEventType
|
|
129
|
+
sender?: string
|
|
130
|
+
since?: number
|
|
131
|
+
limit?: number
|
|
132
|
+
}): Promise<AuditEntry[]> {
|
|
133
|
+
return []
|
|
134
|
+
}
|
|
135
|
+
async verify(_entry: AuditEntry, _publicKey: string): Promise<boolean> {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
size(): number {
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createAuditLog(privateKey: string, opts?: { maxEntries?: number }): InMemoryAuditLog {
|
|
144
|
+
return new InMemoryAuditLog(privateKey, opts?.maxEntries)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { InMemoryAuditLog, NoopAuditLog }
|