@7h3/protocol 0.1.2 → 0.5.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 +152 -0
- package/CONTRIBUTING.md +82 -0
- package/Dockerfile +73 -0
- package/GOVERNANCE.md +62 -0
- package/README.md +1317 -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 +406 -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-github.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/banner.svg +123 -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 +121 -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/cbor.go +551 -0
- package/sdk/go/cbor_test.go +232 -0
- package/sdk/go/encryption.go +280 -0
- package/sdk/go/encryption_test.go +318 -0
- package/sdk/go/go.mod +7 -0
- package/sdk/go/go.sum +4 -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/replay.go +121 -0
- package/sdk/go/replay_test.go +149 -0
- package/sdk/go/webhook.go +136 -0
- package/sdk/pq/package-lock.json +1358 -0
- package/sdk/pq/package.json +42 -0
- package/sdk/pq/src/index.test.ts +143 -0
- package/sdk/pq/src/index.ts +166 -0
- package/sdk/pq/tsconfig.json +14 -0
- package/sdk/pq/vitest.config.ts +7 -0
- package/sdk/python/README.md +18 -0
- package/sdk/python/protocol_7h3/__init__.py +46 -0
- package/sdk/python/protocol_7h3/encryption.py +252 -0
- package/sdk/python/protocol_7h3/http.py +212 -0
- package/sdk/python/protocol_7h3/keys.py +149 -0
- package/sdk/python/protocol_7h3/pq.py +244 -0
- package/sdk/python/protocol_7h3/protocol.py +525 -0
- package/sdk/python/protocol_7h3/queue.py +118 -0
- package/sdk/python/protocol_7h3/replay.py +98 -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_encryption.py +206 -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/sdk/threshold/index.d.ts +68 -0
- package/sdk/threshold/index.d.ts.map +1 -0
- package/sdk/threshold/index.js +254 -0
- package/sdk/threshold/package-lock.json +1361 -0
- package/sdk/threshold/package.json +39 -0
- package/sdk/threshold/src/index.d.ts +68 -0
- package/sdk/threshold/src/index.d.ts.map +1 -0
- package/sdk/threshold/src/index.js +254 -0
- package/sdk/threshold/src/index.test.ts +238 -0
- package/sdk/threshold/src/index.ts +355 -0
- package/sdk/threshold/tsconfig.json +19 -0
- package/sdk/threshold/vitest.config.ts +12 -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/capability.test.ts +504 -0
- package/src/capability.ts +380 -0
- package/src/cborCodec.test.ts +263 -0
- package/src/cborCodec.ts +339 -0
- package/src/conformance.test.ts +136 -0
- package/src/conformanceVectors.ts +99 -0
- package/src/encryption.test.ts +206 -0
- package/src/encryption.ts +245 -0
- package/src/envelopeCbor.ts +140 -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 +246 -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 +189 -0
- package/src/index.ts +39 -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/otel.ts +136 -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/{protocol.d.ts → src/protocol.d.ts} +2 -1
- package/src/protocol.d.ts.map +1 -0
- package/src/protocol.js +294 -0
- package/src/protocol.test.ts +155 -0
- package/src/protocol.ts +414 -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 +273 -0
- package/src/replayStores.ts +215 -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/stream.test.ts +254 -0
- package/src/stream.ts +417 -0
- package/src/telemetry.test.ts +251 -0
- package/src/telemetry.ts +299 -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 +200 -0
- package/tsconfig.json +15 -0
- package/tsconfig.lib.json +23 -0
- package/vite.lib.config.ts +16 -0
- package/vitest.config.ts +11 -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/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,72 @@
|
|
|
1
|
+
export type PolicyRequirement = 'ed25519' | 'hmac' | 'any' | 'none'
|
|
2
|
+
|
|
3
|
+
export interface RoutePolicy {
|
|
4
|
+
path: string
|
|
5
|
+
require: PolicyRequirement
|
|
6
|
+
rateLimit?: { requests: number; windowMs: number }
|
|
7
|
+
allowedSenders?: string[]
|
|
8
|
+
signResponse?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Translate a glob pattern to a RegExp.
|
|
13
|
+
* Supported:
|
|
14
|
+
* - exact: '/health'
|
|
15
|
+
* - '?' → any single non-slash char
|
|
16
|
+
* - '*' → any chars within a single segment (no slash)
|
|
17
|
+
* - '**' → any chars at any depth (including slashes)
|
|
18
|
+
*/
|
|
19
|
+
export function matchGlob(pattern: string, path: string): boolean {
|
|
20
|
+
// Build regex from pattern, handling ** before * to avoid double-expanding
|
|
21
|
+
let regexStr = '^'
|
|
22
|
+
let i = 0
|
|
23
|
+
while (i < pattern.length) {
|
|
24
|
+
const ch = pattern[i]
|
|
25
|
+
if (ch === '*') {
|
|
26
|
+
if (pattern[i + 1] === '*') {
|
|
27
|
+
// Double star: matches anything including /
|
|
28
|
+
regexStr += '.*'
|
|
29
|
+
i += 2
|
|
30
|
+
} else {
|
|
31
|
+
// Single star: matches any non-slash sequence
|
|
32
|
+
regexStr += '[^/]*'
|
|
33
|
+
i += 1
|
|
34
|
+
}
|
|
35
|
+
} else if (ch === '?') {
|
|
36
|
+
// Single char, not slash
|
|
37
|
+
regexStr += '[^/]'
|
|
38
|
+
i += 1
|
|
39
|
+
} else {
|
|
40
|
+
// Escape regex metacharacters in literal segments
|
|
41
|
+
regexStr += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
42
|
+
i += 1
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
regexStr += '$'
|
|
46
|
+
|
|
47
|
+
return new RegExp(regexStr).test(path)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find the first matching policy for a given path.
|
|
52
|
+
* Returns null if no policy matches.
|
|
53
|
+
*/
|
|
54
|
+
export function matchPolicy(policies: RoutePolicy[], path: string): RoutePolicy | null {
|
|
55
|
+
for (const policy of policies) {
|
|
56
|
+
if (matchGlob(policy.path, path)) {
|
|
57
|
+
return policy
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check whether a sender is permitted by a policy.
|
|
65
|
+
* If allowedSenders is not set, all senders are allowed.
|
|
66
|
+
*/
|
|
67
|
+
export function isAllowedSender(policy: RoutePolicy, sender: string): boolean {
|
|
68
|
+
if (!policy.allowedSenders || policy.allowedSenders.length === 0) {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
return policy.allowedSenders.includes(sender)
|
|
72
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { loadRuntimePolicy, parseRuntimePolicyJson, validateRuntimePolicy } from './runtimePolicy'
|
|
3
|
+
|
|
4
|
+
describe('runtime policy loader', () => {
|
|
5
|
+
it('parses valid policy JSON', () => {
|
|
6
|
+
const policy = parseRuntimePolicyJson(
|
|
7
|
+
JSON.stringify({
|
|
8
|
+
version: '1.0',
|
|
9
|
+
name: 'gluv-runtime-policy',
|
|
10
|
+
status: 'active',
|
|
11
|
+
}),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
expect(policy.version).toBe('1.0')
|
|
15
|
+
expect(policy.name).toBe('gluv-runtime-policy')
|
|
16
|
+
expect(policy.status).toBe('active')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('rejects invalid JSON', () => {
|
|
20
|
+
expect(() => parseRuntimePolicyJson('{')).toThrow(/Invalid runtime policy JSON/i)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('rejects invalid root object', () => {
|
|
24
|
+
expect(() => validateRuntimePolicy('not-an-object')).toThrow(/root must be an object/i)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('rejects missing required fields', () => {
|
|
28
|
+
expect(() => validateRuntimePolicy({ name: 'x', status: 'active' })).toThrow(/version/i)
|
|
29
|
+
expect(() => validateRuntimePolicy({ version: '1.0', status: 'active' })).toThrow(/name/i)
|
|
30
|
+
expect(() => validateRuntimePolicy({ version: '1.0', name: 'x' })).toThrow(/status/i)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('loads policy using injected reader and path', async () => {
|
|
34
|
+
const policy = await loadRuntimePolicy({
|
|
35
|
+
path: '/virtual/AI_RUNTIME_POLICY.json',
|
|
36
|
+
readTextFile: async (filePath) => {
|
|
37
|
+
expect(filePath).toBe('/virtual/AI_RUNTIME_POLICY.json')
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
version: '1.0',
|
|
40
|
+
name: 'gluv-runtime-policy',
|
|
41
|
+
status: 'active',
|
|
42
|
+
owner: 'platform-ai-ops',
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(policy.owner).toBe('platform-ai-ops')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface RuntimePolicy {
|
|
2
|
+
version: string
|
|
3
|
+
name: string
|
|
4
|
+
status: string
|
|
5
|
+
owner?: string
|
|
6
|
+
updated_at?: string
|
|
7
|
+
hard_invariants?: Record<string, unknown>
|
|
8
|
+
slo_defaults?: Record<string, unknown>
|
|
9
|
+
mode_selection?: Record<string, unknown>
|
|
10
|
+
guardrails?: Record<string, unknown>
|
|
11
|
+
tuning?: Record<string, unknown>
|
|
12
|
+
adaptive_flow?: Record<string, unknown>
|
|
13
|
+
benchmark_policy?: Record<string, unknown>
|
|
14
|
+
runtime_decision_tree?: unknown[]
|
|
15
|
+
operator_handoff_required_fields?: string[]
|
|
16
|
+
[key: string]: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LoadRuntimePolicyOptions {
|
|
20
|
+
path?: string
|
|
21
|
+
readTextFile?: (filePath: string) => Promise<string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function asNonEmptyString(value: unknown, field: string): string {
|
|
29
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
30
|
+
throw new Error(`Invalid runtime policy: '${field}' must be a non-empty string`)
|
|
31
|
+
}
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateRuntimePolicy(value: unknown): RuntimePolicy {
|
|
36
|
+
if (!isRecord(value)) {
|
|
37
|
+
throw new Error('Invalid runtime policy: root must be an object')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const version = asNonEmptyString(value.version, 'version')
|
|
41
|
+
const name = asNonEmptyString(value.name, 'name')
|
|
42
|
+
const status = asNonEmptyString(value.status, 'status')
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...value,
|
|
46
|
+
version,
|
|
47
|
+
name,
|
|
48
|
+
status,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseRuntimePolicyJson(jsonText: string): RuntimePolicy {
|
|
53
|
+
let parsed: unknown
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(jsonText)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
58
|
+
throw new Error(`Invalid runtime policy JSON: ${message}`)
|
|
59
|
+
}
|
|
60
|
+
return validateRuntimePolicy(parsed)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function defaultReadTextFile(filePath: string): Promise<string> {
|
|
64
|
+
if (typeof fetch === 'function') {
|
|
65
|
+
const response = await fetch(filePath)
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Failed to fetch runtime policy at '${filePath}': ${response.status} ${response.statusText}`)
|
|
68
|
+
}
|
|
69
|
+
return response.text()
|
|
70
|
+
}
|
|
71
|
+
throw new Error(
|
|
72
|
+
"No default runtime file reader available in this environment. Provide 'readTextFile' in loadRuntimePolicy(options).",
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function loadRuntimePolicy(options: LoadRuntimePolicyOptions = {}): Promise<RuntimePolicy> {
|
|
77
|
+
const filePath = options.path ?? 'AI_RUNTIME_POLICY.json'
|
|
78
|
+
const readTextFile = options.readTextFile ?? defaultReadTextFile
|
|
79
|
+
const text = await readTextFile(filePath)
|
|
80
|
+
return parseRuntimePolicyJson(text)
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { RuntimePolicyManager } from './runtimePolicyManager'
|
|
3
|
+
|
|
4
|
+
describe('runtime policy manager', () => {
|
|
5
|
+
it('loads initial policy and exposes snapshot', async () => {
|
|
6
|
+
const manager = new RuntimePolicyManager({
|
|
7
|
+
path: '/virtual/AI_RUNTIME_POLICY.json',
|
|
8
|
+
readTextFile: async () =>
|
|
9
|
+
JSON.stringify({
|
|
10
|
+
version: '1.0',
|
|
11
|
+
name: 'gluv-runtime-policy',
|
|
12
|
+
status: 'active',
|
|
13
|
+
hard_invariants: {},
|
|
14
|
+
slo_defaults: {},
|
|
15
|
+
tuning: {
|
|
16
|
+
batch_size: { low_traffic: [4, 8], medium_traffic: [8, 32], high_traffic: [32, 64] },
|
|
17
|
+
inflight_cap: { low_traffic: [16, 64], medium_traffic: [64, 256], high_traffic: [256, 1024] },
|
|
18
|
+
retry_backoff_ms: { low_traffic: [1, 2], medium_traffic: [2, 8], high_traffic: [4, 16] },
|
|
19
|
+
max_retry_attempts: 3,
|
|
20
|
+
retriable_status_codes: [503],
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const loaded = await manager.loadInitial()
|
|
26
|
+
expect(loaded.policy.name).toBe('gluv-runtime-policy')
|
|
27
|
+
expect(manager.current().policy.version).toBe('1.0')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { bootstrapRuntimePolicyEnforcer, type BootstrapRuntimePolicyEnforcerOptions, type PolicyEnforcer } from './policyEnforcer'
|
|
2
|
+
import type { RuntimePolicy } from './runtimePolicy'
|
|
3
|
+
|
|
4
|
+
export interface RuntimePolicySnapshot {
|
|
5
|
+
policy: RuntimePolicy
|
|
6
|
+
enforcer: PolicyEnforcer
|
|
7
|
+
loadedAtMs: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RuntimePolicyManager {
|
|
11
|
+
private snapshot: RuntimePolicySnapshot | null = null
|
|
12
|
+
private readonly options: BootstrapRuntimePolicyEnforcerOptions
|
|
13
|
+
|
|
14
|
+
constructor(options: BootstrapRuntimePolicyEnforcerOptions = {}) {
|
|
15
|
+
this.options = options
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async loadInitial(): Promise<RuntimePolicySnapshot> {
|
|
19
|
+
const loaded = await bootstrapRuntimePolicyEnforcer(this.options)
|
|
20
|
+
this.snapshot = {
|
|
21
|
+
...loaded,
|
|
22
|
+
loadedAtMs: Date.now(),
|
|
23
|
+
}
|
|
24
|
+
return this.snapshot
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
current(): RuntimePolicySnapshot {
|
|
28
|
+
if (!this.snapshot) {
|
|
29
|
+
throw new Error('Runtime policy not loaded. Call loadInitial() first.')
|
|
30
|
+
}
|
|
31
|
+
return this.snapshot
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async refresh(): Promise<RuntimePolicySnapshot> {
|
|
35
|
+
const previous = this.snapshot
|
|
36
|
+
try {
|
|
37
|
+
const loaded = await bootstrapRuntimePolicyEnforcer(this.options)
|
|
38
|
+
this.snapshot = {
|
|
39
|
+
...loaded,
|
|
40
|
+
loadedAtMs: Date.now(),
|
|
41
|
+
}
|
|
42
|
+
return this.snapshot
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (previous) {
|
|
45
|
+
return previous
|
|
46
|
+
}
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RuntimeMode } from './policyEnforcer'
|
|
2
|
+
|
|
3
|
+
export type RuntimeEnvironment = 'dev' | 'staging' | 'prod'
|
|
4
|
+
|
|
5
|
+
export interface RuntimeTuningPreset {
|
|
6
|
+
environment: RuntimeEnvironment
|
|
7
|
+
mode: RuntimeMode
|
|
8
|
+
poolSize: number
|
|
9
|
+
batchSize: number
|
|
10
|
+
inflightCap: number
|
|
11
|
+
retryMaxAttempts: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PRESETS: Record<RuntimeEnvironment, RuntimeTuningPreset> = {
|
|
15
|
+
dev: {
|
|
16
|
+
environment: 'dev',
|
|
17
|
+
mode: 'ws-batch',
|
|
18
|
+
poolSize: 1,
|
|
19
|
+
batchSize: 8,
|
|
20
|
+
inflightCap: 64,
|
|
21
|
+
retryMaxAttempts: 2,
|
|
22
|
+
},
|
|
23
|
+
staging: {
|
|
24
|
+
environment: 'staging',
|
|
25
|
+
mode: 'http-binary-batch',
|
|
26
|
+
poolSize: 2,
|
|
27
|
+
batchSize: 32,
|
|
28
|
+
inflightCap: 256,
|
|
29
|
+
retryMaxAttempts: 3,
|
|
30
|
+
},
|
|
31
|
+
prod: {
|
|
32
|
+
environment: 'prod',
|
|
33
|
+
mode: 'http-binary-batch',
|
|
34
|
+
poolSize: 4,
|
|
35
|
+
batchSize: 64,
|
|
36
|
+
inflightCap: 1024,
|
|
37
|
+
retryMaxAttempts: 3,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getRuntimeTuningPreset(environment: RuntimeEnvironment): RuntimeTuningPreset {
|
|
42
|
+
return PRESETS[environment]
|
|
43
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
2
|
+
import { signResponse, verifyResponse, RESPONSE_HEADER } from './signedResponse'
|
|
3
|
+
import { generateEd25519KeypairBase64Url } from './protocol'
|
|
4
|
+
|
|
5
|
+
let keys: { publicKey: string; privateKey: string }
|
|
6
|
+
let wrongKeys: { publicKey: string; privateKey: string }
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
keys = await generateEd25519KeypairBase64Url()
|
|
10
|
+
wrongKeys = await generateEd25519KeypairBase64Url()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('signedResponse', () => {
|
|
14
|
+
it('round-trip: signs and verifies correctly', async () => {
|
|
15
|
+
const body = 'hello world'
|
|
16
|
+
const { headers } = await signResponse(body, {
|
|
17
|
+
privateKey: keys.privateKey,
|
|
18
|
+
sender: 'gateway-agent',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const result = await verifyResponse(body, headers, { publicKey: keys.publicKey })
|
|
22
|
+
expect(result.ok).toBe(true)
|
|
23
|
+
expect(result.envelope?.body.intent).toBe('RESULT')
|
|
24
|
+
expect(result.envelope?.body.content).toBe(body)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('fails when body does not match envelope content', async () => {
|
|
28
|
+
const body = 'original body'
|
|
29
|
+
const { headers } = await signResponse(body, {
|
|
30
|
+
privateKey: keys.privateKey,
|
|
31
|
+
sender: 'gateway-agent',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const result = await verifyResponse('tampered body', headers, { publicKey: keys.publicKey })
|
|
35
|
+
expect(result.ok).toBe(false)
|
|
36
|
+
expect(result.reason).toBe('content-mismatch')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('fails with wrong public key', async () => {
|
|
40
|
+
const body = 'some content'
|
|
41
|
+
const { headers } = await signResponse(body, {
|
|
42
|
+
privateKey: keys.privateKey,
|
|
43
|
+
sender: 'gateway-agent',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const result = await verifyResponse(body, headers, { publicKey: wrongKeys.publicKey })
|
|
47
|
+
expect(result.ok).toBe(false)
|
|
48
|
+
expect(result.reason).toBe('invalid-signature')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('fails when header is missing', async () => {
|
|
52
|
+
const result = await verifyResponse('body', {}, { publicKey: keys.publicKey })
|
|
53
|
+
expect(result.ok).toBe(false)
|
|
54
|
+
expect(result.reason).toBe('missing-header')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('fails when envelope TTL is expired', async () => {
|
|
58
|
+
// Sign with 1ms ttl — immediately expired
|
|
59
|
+
const body = 'ttl-test'
|
|
60
|
+
const { headers } = await signResponse(body, {
|
|
61
|
+
privateKey: keys.privateKey,
|
|
62
|
+
sender: 'gateway-agent',
|
|
63
|
+
ttlMs: 1,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Wait a tick to ensure expiry
|
|
67
|
+
await new Promise(r => setTimeout(r, 10))
|
|
68
|
+
|
|
69
|
+
const result = await verifyResponse(body, headers, { publicKey: keys.publicKey })
|
|
70
|
+
expect(result.ok).toBe(false)
|
|
71
|
+
expect(result.reason).toBe('ttl-expired')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('fails when maxAgeMs exceeded', async () => {
|
|
75
|
+
const body = 'max-age-test'
|
|
76
|
+
const { headers } = await signResponse(body, {
|
|
77
|
+
privateKey: keys.privateKey,
|
|
78
|
+
sender: 'gateway-agent',
|
|
79
|
+
ttlMs: 60_000, // long ttl
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Wait to ensure age exceeds maxAgeMs
|
|
83
|
+
await new Promise(r => setTimeout(r, 10))
|
|
84
|
+
|
|
85
|
+
const result = await verifyResponse(body, headers, {
|
|
86
|
+
publicKey: keys.publicKey,
|
|
87
|
+
maxAgeMs: 1, // tiny window
|
|
88
|
+
})
|
|
89
|
+
expect(result.ok).toBe(false)
|
|
90
|
+
expect(result.reason).toBe('ttl-expired')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('passes correlationId and recipient through envelope', async () => {
|
|
94
|
+
const body = 'corr-test'
|
|
95
|
+
const { headers } = await signResponse(body, {
|
|
96
|
+
privateKey: keys.privateKey,
|
|
97
|
+
sender: 'gateway-agent',
|
|
98
|
+
recipient: 'client-agent',
|
|
99
|
+
correlationId: 'req-123',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const result = await verifyResponse(body, headers, { publicKey: keys.publicKey })
|
|
103
|
+
expect(result.ok).toBe(true)
|
|
104
|
+
expect(result.envelope?.body.correlationId).toBe('req-123')
|
|
105
|
+
expect(result.envelope?.header.recipient).toBe('client-agent')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('RESPONSE_HEADER constant is x-7h3-response', () => {
|
|
109
|
+
expect(RESPONSE_HEADER).toBe('x-7h3-response')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEnvelope,
|
|
3
|
+
signEnvelopeEd25519,
|
|
4
|
+
verifyEnvelopeEd25519,
|
|
5
|
+
type ProtocolEnvelope,
|
|
6
|
+
} from './protocol'
|
|
7
|
+
|
|
8
|
+
export const RESPONSE_HEADER = 'x-7h3-response'
|
|
9
|
+
|
|
10
|
+
export async function signResponse(
|
|
11
|
+
body: string,
|
|
12
|
+
opts: {
|
|
13
|
+
privateKey: string
|
|
14
|
+
sender: string
|
|
15
|
+
recipient?: string
|
|
16
|
+
correlationId?: string
|
|
17
|
+
ttlMs?: number
|
|
18
|
+
},
|
|
19
|
+
): Promise<{ headers: { 'x-7h3-response': string } }> {
|
|
20
|
+
const envelope = createEnvelope({
|
|
21
|
+
sender: opts.sender,
|
|
22
|
+
recipient: opts.recipient,
|
|
23
|
+
intent: 'RESULT',
|
|
24
|
+
content: body,
|
|
25
|
+
correlationId: opts.correlationId,
|
|
26
|
+
ttlMs: opts.ttlMs ?? 60_000,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const signed = await signEnvelopeEd25519(envelope, opts.privateKey)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
headers: {
|
|
33
|
+
[RESPONSE_HEADER]: JSON.stringify(signed),
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function verifyResponse(
|
|
39
|
+
body: string,
|
|
40
|
+
headers: Record<string, string | string[] | undefined>,
|
|
41
|
+
opts: {
|
|
42
|
+
publicKey: string
|
|
43
|
+
maxAgeMs?: number
|
|
44
|
+
},
|
|
45
|
+
): Promise<{ ok: boolean; envelope?: ProtocolEnvelope; reason?: string }> {
|
|
46
|
+
const raw = headers[RESPONSE_HEADER]
|
|
47
|
+
const rawStr = Array.isArray(raw) ? raw[0] : raw
|
|
48
|
+
if (!rawStr) {
|
|
49
|
+
return { ok: false, reason: 'missing-header' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let envelope: ProtocolEnvelope
|
|
53
|
+
try {
|
|
54
|
+
envelope = JSON.parse(rawStr) as ProtocolEnvelope
|
|
55
|
+
} catch {
|
|
56
|
+
return { ok: false, reason: 'malformed-envelope' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check TTL from envelope
|
|
60
|
+
const nowMs = Date.now()
|
|
61
|
+
const { timestampMs, ttlMs } = envelope.header
|
|
62
|
+
if (timestampMs + ttlMs < nowMs) {
|
|
63
|
+
return { ok: false, reason: 'ttl-expired', envelope }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check maxAgeMs if provided
|
|
67
|
+
if (opts.maxAgeMs !== undefined && nowMs - timestampMs > opts.maxAgeMs) {
|
|
68
|
+
return { ok: false, reason: 'ttl-expired', envelope }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify signature
|
|
72
|
+
const valid = await verifyEnvelopeEd25519(envelope, opts.publicKey)
|
|
73
|
+
if (!valid) {
|
|
74
|
+
return { ok: false, reason: 'invalid-signature', envelope }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Verify body matches envelope content
|
|
78
|
+
if (envelope.body.content !== body) {
|
|
79
|
+
return { ok: false, reason: 'content-mismatch', envelope }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { ok: true, envelope }
|
|
83
|
+
}
|