@heddleagent/execution-host-client 6.0.0-next.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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/dist/authority/index.d.ts +3 -0
  4. package/dist/authority/index.d.ts.map +1 -0
  5. package/dist/authority/index.js +2 -0
  6. package/dist/authority/index.js.map +1 -0
  7. package/dist/authority/jose-execution-authority.d.ts +14 -0
  8. package/dist/authority/jose-execution-authority.d.ts.map +1 -0
  9. package/dist/authority/jose-execution-authority.js +266 -0
  10. package/dist/authority/jose-execution-authority.js.map +1 -0
  11. package/dist/authority/types.d.ts +63 -0
  12. package/dist/authority/types.d.ts.map +1 -0
  13. package/dist/authority/types.js +2 -0
  14. package/dist/authority/types.js.map +1 -0
  15. package/dist/contracts/index.d.ts +163 -0
  16. package/dist/contracts/index.d.ts.map +1 -0
  17. package/dist/contracts/index.js +149 -0
  18. package/dist/contracts/index.js.map +1 -0
  19. package/dist/conversation/durable-hosted-conversation-turn-service.d.ts +23 -0
  20. package/dist/conversation/durable-hosted-conversation-turn-service.d.ts.map +1 -0
  21. package/dist/conversation/durable-hosted-conversation-turn-service.js +273 -0
  22. package/dist/conversation/durable-hosted-conversation-turn-service.js.map +1 -0
  23. package/dist/conversation/hosted-conversation-turn-service.d.ts +17 -0
  24. package/dist/conversation/hosted-conversation-turn-service.d.ts.map +1 -0
  25. package/dist/conversation/hosted-conversation-turn-service.js +60 -0
  26. package/dist/conversation/hosted-conversation-turn-service.js.map +1 -0
  27. package/dist/conversation/index.d.ts +7 -0
  28. package/dist/conversation/index.d.ts.map +1 -0
  29. package/dist/conversation/index.js +5 -0
  30. package/dist/conversation/index.js.map +1 -0
  31. package/dist/conversation/lifecycle-types.d.ts +208 -0
  32. package/dist/conversation/lifecycle-types.d.ts.map +1 -0
  33. package/dist/conversation/lifecycle-types.js +92 -0
  34. package/dist/conversation/lifecycle-types.js.map +1 -0
  35. package/dist/conversation/types.d.ts +35 -0
  36. package/dist/conversation/types.d.ts.map +1 -0
  37. package/dist/conversation/types.js +11 -0
  38. package/dist/conversation/types.js.map +1 -0
  39. package/dist/http-sse/direct-http-execution-host.d.ts +12 -0
  40. package/dist/http-sse/direct-http-execution-host.d.ts.map +1 -0
  41. package/dist/http-sse/direct-http-execution-host.js +279 -0
  42. package/dist/http-sse/direct-http-execution-host.js.map +1 -0
  43. package/dist/http-sse/errors.d.ts +19 -0
  44. package/dist/http-sse/errors.d.ts.map +1 -0
  45. package/dist/http-sse/errors.js +29 -0
  46. package/dist/http-sse/errors.js.map +1 -0
  47. package/dist/http-sse/index.d.ts +4 -0
  48. package/dist/http-sse/index.d.ts.map +1 -0
  49. package/dist/http-sse/index.js +3 -0
  50. package/dist/http-sse/index.js.map +1 -0
  51. package/dist/http-sse/types.d.ts +20 -0
  52. package/dist/http-sse/types.d.ts.map +1 -0
  53. package/dist/http-sse/types.js +2 -0
  54. package/dist/http-sse/types.js.map +1 -0
  55. package/dist/index.d.ts +6 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +6 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/mcp/errors.d.ts +11 -0
  60. package/dist/mcp/errors.d.ts.map +1 -0
  61. package/dist/mcp/errors.js +15 -0
  62. package/dist/mcp/errors.js.map +1 -0
  63. package/dist/mcp/index.d.ts +5 -0
  64. package/dist/mcp/index.d.ts.map +1 -0
  65. package/dist/mcp/index.js +3 -0
  66. package/dist/mcp/index.js.map +1 -0
  67. package/dist/mcp/jwt-capability-verifier.d.ts +12 -0
  68. package/dist/mcp/jwt-capability-verifier.d.ts.map +1 -0
  69. package/dist/mcp/jwt-capability-verifier.js +136 -0
  70. package/dist/mcp/jwt-capability-verifier.js.map +1 -0
  71. package/dist/mcp/node/index.d.ts +4 -0
  72. package/dist/mcp/node/index.d.ts.map +1 -0
  73. package/dist/mcp/node/index.js +3 -0
  74. package/dist/mcp/node/index.js.map +1 -0
  75. package/dist/mcp/node/json-toolset.d.ts +23 -0
  76. package/dist/mcp/node/json-toolset.d.ts.map +1 -0
  77. package/dist/mcp/node/json-toolset.js +94 -0
  78. package/dist/mcp/node/json-toolset.js.map +1 -0
  79. package/dist/mcp/node/streamable-http-mcp-service.d.ts +18 -0
  80. package/dist/mcp/node/streamable-http-mcp-service.d.ts.map +1 -0
  81. package/dist/mcp/node/streamable-http-mcp-service.js +229 -0
  82. package/dist/mcp/node/streamable-http-mcp-service.js.map +1 -0
  83. package/dist/mcp/node/types.d.ts +53 -0
  84. package/dist/mcp/node/types.d.ts.map +1 -0
  85. package/dist/mcp/node/types.js +2 -0
  86. package/dist/mcp/node/types.js.map +1 -0
  87. package/dist/mcp/types.d.ts +37 -0
  88. package/dist/mcp/types.d.ts.map +1 -0
  89. package/dist/mcp/types.js +2 -0
  90. package/dist/mcp/types.js.map +1 -0
  91. package/dist/node/authority-key.d.ts +14 -0
  92. package/dist/node/authority-key.d.ts.map +1 -0
  93. package/dist/node/authority-key.js +89 -0
  94. package/dist/node/authority-key.js.map +1 -0
  95. package/dist/node/direct-credentials.d.ts +21 -0
  96. package/dist/node/direct-credentials.d.ts.map +1 -0
  97. package/dist/node/direct-credentials.js +47 -0
  98. package/dist/node/direct-credentials.js.map +1 -0
  99. package/dist/node/http-service.d.ts +21 -0
  100. package/dist/node/http-service.d.ts.map +1 -0
  101. package/dist/node/http-service.js +389 -0
  102. package/dist/node/http-service.js.map +1 -0
  103. package/dist/node/index.d.ts +6 -0
  104. package/dist/node/index.d.ts.map +1 -0
  105. package/dist/node/index.js +4 -0
  106. package/dist/node/index.js.map +1 -0
  107. package/dist/node/types.d.ts +52 -0
  108. package/dist/node/types.d.ts.map +1 -0
  109. package/dist/node/types.js +2 -0
  110. package/dist/node/types.js.map +1 -0
  111. package/dist/testing/event-stream.d.ts +17 -0
  112. package/dist/testing/event-stream.d.ts.map +1 -0
  113. package/dist/testing/event-stream.js +83 -0
  114. package/dist/testing/event-stream.js.map +1 -0
  115. package/dist/testing/hosted-conversation-turn-store-conformance.d.ts +17 -0
  116. package/dist/testing/hosted-conversation-turn-store-conformance.d.ts.map +1 -0
  117. package/dist/testing/hosted-conversation-turn-store-conformance.js +137 -0
  118. package/dist/testing/hosted-conversation-turn-store-conformance.js.map +1 -0
  119. package/dist/testing/index.d.ts +5 -0
  120. package/dist/testing/index.d.ts.map +1 -0
  121. package/dist/testing/index.js +3 -0
  122. package/dist/testing/index.js.map +1 -0
  123. package/dist/testing/invocation.d.ts +20 -0
  124. package/dist/testing/invocation.d.ts.map +1 -0
  125. package/dist/testing/invocation.js +39 -0
  126. package/dist/testing/invocation.js.map +1 -0
  127. package/dist/testing/local-execution-host-contract-fixture.d.ts +21 -0
  128. package/dist/testing/local-execution-host-contract-fixture.d.ts.map +1 -0
  129. package/dist/testing/local-execution-host-contract-fixture.js +197 -0
  130. package/dist/testing/local-execution-host-contract-fixture.js.map +1 -0
  131. package/dist/testing/request.d.ts +10 -0
  132. package/dist/testing/request.d.ts.map +1 -0
  133. package/dist/testing/request.js +140 -0
  134. package/dist/testing/request.js.map +1 -0
  135. package/dist/testing/types.d.ts +42 -0
  136. package/dist/testing/types.d.ts.map +1 -0
  137. package/dist/testing/types.js +2 -0
  138. package/dist/testing/types.js.map +1 -0
  139. package/package.json +92 -0
  140. package/spec/v1/README.md +101 -0
  141. package/spec/v1/durable-hosted-conversation-lifecycle.md +139 -0
  142. package/spec/v1/fixtures/ambiguous-eof.sse +9 -0
  143. package/spec/v1/fixtures/authority.json +70 -0
  144. package/spec/v1/fixtures/cancelled.sse +9 -0
  145. package/spec/v1/fixtures/durable-conversation-lifecycle.json +890 -0
  146. package/spec/v1/fixtures/invalid-request-extra-field.json +8 -0
  147. package/spec/v1/fixtures/invalid-sequence-gap.sse +9 -0
  148. package/spec/v1/fixtures/manifest.json +53 -0
  149. package/spec/v1/fixtures/valid-request.json +7 -0
  150. package/spec/v1/fixtures/valid-result.sse +13 -0
  151. package/spec/v1/openapi.json +896 -0
  152. package/spec/v1/schema-bundle.json +1286 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jay / Fienna Liang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # `@heddleagent/execution-host-client`
2
+
3
+ `@heddleagent/execution-host-client` is the lightweight backend-side SDK for
4
+ products that invoke a separately deployed Heddle Execution Host. It lets an
5
+ adopter keep its own language stack, authentication, database, product policy,
6
+ MCP tools, and UI while reusing the security-sensitive v1 contract machinery.
7
+
8
+ > **Current availability:** version `6.0.0-next.0` is an unpublished v6
9
+ > prerelease candidate. Until it is published, the stable package remains
10
+ > `@roackb2/heddle-adopter@5.13.0`. Heddle does not currently distribute
11
+ > the compatible Execution Host implementation or offer a hosted service. The
12
+ > public surface documents and tests the adopter boundary without implying a
13
+ > generally available deployment.
14
+
15
+ After npm confirms the `next` dist-tag:
16
+
17
+ ```bash
18
+ npm install @heddleagent/execution-host-client@next
19
+ ```
20
+
21
+ The package does **not** contain Heddle's agent loop, AgentCore deployment,
22
+ Terraform, product MCP tools, or product logic. It uses the official MCP SDK,
23
+ plus `jose`, `zod`, `dayjs`, and `eventsource-parser`, for its optional
24
+ reference edges.
25
+
26
+ ## What it owns
27
+
28
+ | Import | Reusable responsibility |
29
+ | --- | --- |
30
+ | `@heddleagent/execution-host-client/contracts` | Runtime-validated v1 request, stream, identity, capability, and header contracts |
31
+ | `@heddleagent/execution-host-client/authority` | ES256 execution assertion and optional MCP capability issuance plus public JWKS projection |
32
+ | `@heddleagent/execution-host-client/conversation` | Turn orchestration across authority, model credentials, optional MCP policy, an `ExecutionHost`, and an optional adopter-implemented durable lifecycle store |
33
+ | `@heddleagent/execution-host-client/mcp` | Independent capability verification at the adopter's MCP edge |
34
+ | `@heddleagent/execution-host-client/mcp/node` | Stateless official-SDK Streamable HTTP lifecycle around adopter-defined toolsets |
35
+ | `@heddleagent/execution-host-client/http-sse` | Transport-neutral `ExecutionHost` port and strict direct-development HTTP/SSE client |
36
+ | `@heddleagent/execution-host-client/testing` | Node-only loopback v1 fixture plus durable-turn store conformance for real adapters |
37
+ | `@heddleagent/execution-host-client/node` | Optional Node JWKS/conversation HTTP edge plus safe local signing-key helpers |
38
+
39
+ Non-TypeScript adopters can consume the versioned candidate
40
+ [`spec/v1`](spec/v1/README.md) OpenAPI 3.1.1 document, JSON Schema bundle, and
41
+ golden conformance fixtures directly. The
42
+ [`Python v1 conformance reference`](conformance/reference-adopters/python-v1/README.md)
43
+ is an independent executable proof of that path, not another required service
44
+ or a separately supported SDK. It follows the contract version, not the
45
+ TypeScript source-module layout.
46
+
47
+ The adopter still owns:
48
+
49
+ - authenticating its users and mapping them to tenant, subject, and product
50
+ session IDs;
51
+ - deciding which product capabilities that identity may use;
52
+ - production signing-key storage and rotation, route placement, invocation-ID
53
+ allocation, the lifecycle-store implementation/schema/migrations, retention,
54
+ and history queries;
55
+ - implementing and hosting product MCP tools against its own APIs and data;
56
+ - choosing an AgentCore/SigV4 transport, applying results, and rendering UI.
57
+
58
+ ## Issue one invocation's authority
59
+
60
+ Load an ES256 key pair from your normal secret-management boundary, then create
61
+ one long-lived authority service at application composition:
62
+
63
+ ```ts
64
+ import { JoseExecutionAuthority } from '@heddleagent/execution-host-client/authority'
65
+
66
+ const authority = await JoseExecutionAuthority.create(
67
+ {
68
+ issuer: 'https://api.example.com',
69
+ adopterId: 'example-product',
70
+ executionAudience: 'heddle-execution-host',
71
+ keyId: 'execution-key-2026-08',
72
+ executionTtlSeconds: 300,
73
+ mcp: {
74
+ audience: 'example-product-mcp',
75
+ serverId: 'product_capabilities',
76
+ ttlSeconds: 900,
77
+ },
78
+ },
79
+ { privateKey, publicKey },
80
+ )
81
+
82
+ // Serve only this public projection from a stable JWKS URL.
83
+ const publicJwks = authority.publicJwks()
84
+
85
+ // These IDs must come from authenticated and authorized product state.
86
+ const issued = await authority.issue({
87
+ scope: {
88
+ tenantId: authenticatedTenant.id,
89
+ subjectId: authenticatedUser.id,
90
+ productSessionId: conversation.id,
91
+ },
92
+ runtimeSessionId,
93
+ invocationId,
94
+ workflow: 'conversation-turn',
95
+ mcp: { allowedTools: ['read_workspace_snapshot'] },
96
+ })
97
+ ```
98
+
99
+ An invocation without product MCP tools omits both the `mcp` deployment config
100
+ and issue input. The execution assertion remains available through
101
+ `issued.executionAssertion()`; an optional capability is available through
102
+ `issued.mcpCapability()`. JSON serialization emits only credential-free
103
+ metadata, although those identifiers still require normal logging
104
+ minimization.
105
+
106
+ ## Use the lowest-code Node path
107
+
108
+ The optional Node surface removes generic HTTP and local key-file code without
109
+ taking product decisions away from the adopter:
110
+
111
+ ```ts
112
+ import { JoseExecutionAuthority } from '@heddleagent/execution-host-client/authority'
113
+ import {
114
+ DurableHostedConversationTurnService,
115
+ HostedConversationTurnService,
116
+ } from '@heddleagent/execution-host-client/conversation'
117
+ import {
118
+ loadExecutionAuthorityKeyPairFromFile,
119
+ NodeExecutionAdopterHttpService,
120
+ } from '@heddleagent/execution-host-client/node'
121
+
122
+ const authority = await JoseExecutionAuthority.create(
123
+ authorityConfig,
124
+ await loadExecutionAuthorityKeyPairFromFile(signingJwkPath),
125
+ )
126
+ const executionTurns = new HostedConversationTurnService({
127
+ authority,
128
+ executionHost,
129
+ modelCredentials,
130
+ mcp: { allowedTools: ['read_workspace_snapshot'] },
131
+ })
132
+ const turns = new DurableHostedConversationTurnService({
133
+ turns: executionTurns,
134
+ store: productPostgresTurnStore,
135
+ })
136
+ const hostedHttp = new NodeExecutionAdopterHttpService({
137
+ authority,
138
+ authenticator: productAuthenticator,
139
+ conversations: productAdmissionService(turns),
140
+ })
141
+
142
+ // In a raw Node server, call this before the application's fallback router.
143
+ if (hostedHttp.handle(request, response)) return
144
+ ```
145
+
146
+ `productAdmissionService` is intentionally product-owned: it maps an
147
+ authenticated principal to authorized tenant, subject, product-session,
148
+ Runtime-session, and invocation IDs before calling `turns.streamTurn(...)`.
149
+ The durable wrapper owns persistence-before-event ordering, safe terminal
150
+ projection, interruption semantics, and expiry reconciliation. The supplied
151
+ store owns atomic database transitions and is certifiable through
152
+ `HostedConversationTurnStoreConformance`.
153
+ The normative behavior and cross-language scenarios are included in the
154
+ [durable v1 lifecycle profile](spec/v1/durable-hosted-conversation-lifecycle.md).
155
+ The Node service owns bounded JSON parsing, `Authorization` redaction, JWKS,
156
+ SSE framing/backpressure, disconnect cancellation, safe failures, and graceful
157
+ shutdown. Its individual `handleJwks` and `handleConversationTurn` methods are
158
+ also available when a framework already owns route matching.
159
+
160
+ For local setup, `generateExecutionAuthorityKeyFile(path)` creates a new
161
+ owner-only JWK without overwriting an existing file. The loader imports its
162
+ private key as non-exportable. Production KMS/HSM or secret-manager storage,
163
+ rotation, revocation, and Windows ACL policy still belong to deployment.
164
+
165
+ ## Verify product authority again at MCP
166
+
167
+ The adopter MCP service must independently verify the bearer. Do not trust
168
+ identity forwarded in model-controlled arguments or assume the Execution Host's
169
+ earlier check is sufficient.
170
+
171
+ ```ts
172
+ import {
173
+ JwtMcpCapabilityVerifier,
174
+ assertMcpCapabilityActive,
175
+ } from '@heddleagent/execution-host-client/mcp'
176
+
177
+ const verifier = new JwtMcpCapabilityVerifier({
178
+ issuer: 'https://api.example.com',
179
+ audience: 'example-product-mcp',
180
+ jwksUrl: new URL('https://api.example.com/.well-known/jwks.json'),
181
+ trustedAdopterId: 'example-product',
182
+ serverId: 'product_capabilities',
183
+ supportedTools: ['read_workspace_snapshot'] as const,
184
+ maxCapabilityAgeSeconds: 900,
185
+ })
186
+
187
+ const capability = await verifier.verify(bearer)
188
+ assertMcpCapabilityActive(capability)
189
+
190
+ // Resolve data only from capability.scope; tool arguments do not carry scope.
191
+ await readWorkspaceSnapshot(capability.scope)
192
+ ```
193
+
194
+ For Node adopters, the declarative JSON-tool path also removes the repetitive
195
+ allowlist, expiry, cancellation, serialization, and safe-error code:
196
+
197
+ ```ts
198
+ import {
199
+ defineNodeMcpJsonTool,
200
+ NodeMcpJsonToolset,
201
+ NodeStreamableHttpMcpService,
202
+ } from '@heddleagent/execution-host-client/mcp/node'
203
+ import { z } from 'zod'
204
+
205
+ const toolset = new NodeMcpJsonToolset({
206
+ serverInfo: { name: 'example-product', version: '1.0.0' },
207
+ tools: [defineNodeMcpJsonTool({
208
+ name: 'read_workspace_snapshot' as const,
209
+ description: 'Read the authenticated subject workspace.',
210
+ inputSchema: z.object({}).strict(),
211
+ annotations: { readOnlyHint: true },
212
+ failureMessage: 'The workspace is unavailable.',
213
+ execute: async (_input, { capability, signal }) => (
214
+ readWorkspaceSnapshot(capability.scope, signal)
215
+ ),
216
+ })],
217
+ })
218
+
219
+ const productMcp = new NodeStreamableHttpMcpService({
220
+ capabilityVerifier: verifier,
221
+ toolset,
222
+ })
223
+ ```
224
+
225
+ Use the lower-level `NodeMcpToolset` interface only when a tool needs custom MCP
226
+ content or lifecycle semantics.
227
+
228
+ ## Invoke the direct development host
229
+
230
+ The direct client is useful for local and reviewed HTTPS deployments. A
231
+ managed AgentCore deployment should supply a separate SigV4/AWS SDK transport
232
+ which implements the same `ExecutionHost` port.
233
+
234
+ ```ts
235
+ import { DirectHttpExecutionHost } from '@heddleagent/execution-host-client/http-sse'
236
+
237
+ const host = new DirectHttpExecutionHost({
238
+ baseUrl: new URL('http://127.0.0.1:8080'),
239
+ localToken: process.env.HEDDLE_EXECUTION_HOST_LOCAL_TOKEN!,
240
+ })
241
+
242
+ for await (const event of host.streamConversationTurn({
243
+ invocationId,
244
+ runtimeSessionId,
245
+ prompt: 'Summarize the relevant product state.',
246
+ executionAssertion: issued.executionAssertion(),
247
+ mcpCapability: issued.mcpCapability(),
248
+ modelApiKey,
249
+ })) {
250
+ applyExecutionEvent(event)
251
+ }
252
+ ```
253
+
254
+ The client refuses redirects, bounds parser and error bodies, validates ordered
255
+ SSE identity, streams accepted/activity events incrementally, and withholds the
256
+ terminal event until clean EOF. It never retries an ambiguous invocation.
257
+
258
+ ## Verify an adopter integration locally
259
+
260
+ The explicit `testing` subpath provides a real loopback implementation of the
261
+ v1 request/SSE boundary. Its callback can call the adopter's real local MCP
262
+ server, while the fixture supplies deterministic success, cancellation,
263
+ failure, and interrupted-EOF behavior without invoking a model or AWS.
264
+
265
+ ```ts
266
+ import {
267
+ LocalExecutionHostContractFixture,
268
+ } from '@heddleagent/execution-host-client/testing'
269
+
270
+ const fixture = await LocalExecutionHostContractFixture.start({
271
+ execute: async (invocation) => {
272
+ await callProductMcp(invocation.mcpCapability(), invocation.signal)
273
+ return { kind: 'result', result: { outcome: 'done' } }
274
+ },
275
+ })
276
+
277
+ try {
278
+ const host = fixture.createExecutionHost()
279
+ await consume(host.streamConversationTurn(input))
280
+ } finally {
281
+ await fixture.close()
282
+ }
283
+ ```
284
+
285
+ This proves the adopter-facing wire and product callback, not the Heddle loop,
286
+ real-host JWT verification, shell/filesystem behavior, tenant isolation, or
287
+ managed AgentCore behavior. See the [testing boundary](src/testing/README.md)
288
+ for the exact evidence limit.
289
+
290
+ ## Language-neutral posture
291
+
292
+ This TypeScript package is a reference implementation, not a requirement that
293
+ adopter backends use TypeScript. The wire and claim contracts are
294
+ language-neutral. The checked-in OpenAPI 3.1.1 document, JSON Schema bundle,
295
+ and golden fixtures are the canonical interoperability surface. A clean-room
296
+ Python implementation passes those fixtures without importing Heddle or the
297
+ private Execution Host code.
298
+
299
+ That is the deliberate stop line for this milestone. Heddle does not promise a
300
+ gateway, generated clients for every language, or a framework starter matrix.
301
+ Further adapters should follow a real adopter and a concrete protocol gap.
302
+
303
+ The runnable
304
+ [`node-control-plane.ts`](examples/node-control-plane.ts) example composes the
305
+ default Node path against the local fixture.
@@ -0,0 +1,3 @@
1
+ export { JoseExecutionAuthority } from './jose-execution-authority.js';
2
+ export type { ExecutionAuthority, ExecutionAuthorityConfig, ExecutionAuthorityIssueInput, ExecutionAuthorityKeyPair, ExecutionAuthorityMcpConfig, IssuedExecutionAuthority, IssuedExecutionAuthorityMetadata, IssuedMcpCapabilityMetadata, JoseExecutionAuthorityOptions, } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/authority/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,YAAY,EACV,kBAAkB,EAClB,wBAAwB,EACxB,4BAA4B,EAC5B,yBAAyB,EACzB,2BAA2B,EAC3B,wBAAwB,EACxB,gCAAgC,EAChC,2BAA2B,EAC3B,6BAA6B,GAC9B,MAAM,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { JoseExecutionAuthority } from './jose-execution-authority.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/authority/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC","sourcesContent":["export { JoseExecutionAuthority } from './jose-execution-authority.js';\nexport type {\n ExecutionAuthority,\n ExecutionAuthorityConfig,\n ExecutionAuthorityIssueInput,\n ExecutionAuthorityKeyPair,\n ExecutionAuthorityMcpConfig,\n IssuedExecutionAuthority,\n IssuedExecutionAuthorityMetadata,\n IssuedMcpCapabilityMetadata,\n JoseExecutionAuthorityOptions,\n} from './types.js';\n"]}
@@ -0,0 +1,14 @@
1
+ import { type JSONWebKeySet } from 'jose';
2
+ import type { ExecutionAuthority, ExecutionAuthorityConfig, ExecutionAuthorityIssueInput, ExecutionAuthorityKeyPair, IssuedExecutionAuthority, JoseExecutionAuthorityOptions } from './types.js';
3
+ /**
4
+ * ES256 reference adapter for issuing one invocation's admission assertion and
5
+ * optional product-MCP capability.
6
+ */
7
+ export declare class JoseExecutionAuthority implements ExecutionAuthority {
8
+ #private;
9
+ private constructor();
10
+ static create(config: ExecutionAuthorityConfig, keyPair: ExecutionAuthorityKeyPair, options?: JoseExecutionAuthorityOptions): Promise<JoseExecutionAuthority>;
11
+ issue(input: ExecutionAuthorityIssueInput): Promise<IssuedExecutionAuthority>;
12
+ publicJwks(): JSONWebKeySet;
13
+ }
14
+ //# sourceMappingURL=jose-execution-authority.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jose-execution-authority.d.ts","sourceRoot":"","sources":["../../src/authority/jose-execution-authority.ts"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,MAAM,CAAC;AAgBd,OAAO,KAAK,EACV,kBAAkB,EAClB,wBAAwB,EACxB,4BAA4B,EAC5B,yBAAyB,EACzB,wBAAwB,EAExB,6BAA6B,EAC9B,MAAM,YAAY,CAAC;AA2DpB;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,kBAAkB;;IAO/D,OAAO;WAaM,MAAM,CACjB,MAAM,EAAE,wBAAwB,EAChC,OAAO,EAAE,yBAAyB,EAClC,OAAO,GAAE,6BAAkC,GAC1C,OAAO,CAAC,sBAAsB,CAAC;IAc5B,KAAK,CACT,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,wBAAwB,CAAC;IAyCpC,UAAU,IAAI,aAAa;CAkF5B"}
@@ -0,0 +1,266 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CompactSign, SignJWT, compactVerify, exportJWK, } from 'jose';
3
+ import { z } from 'zod';
4
+ import { CONVERSATION_TURN_WORKFLOW, EXECUTION_ASSERTION_TYPE, EXECUTION_CONTRACT_VERSION, ExecutionScopeSchema, JwtAudienceSchema, JwtIssuerSchema, MCP_CAPABILITY_TYPE, McpAllowedToolsSchema, McpServerIdSchema, OpaqueIdSchema, RuntimeSessionIdSchema, } from '../contracts/index.js';
5
+ const SIGNING_ALGORITHM = 'ES256';
6
+ const ExecutionAuthorityMcpConfigSchema = z.object({
7
+ audience: JwtAudienceSchema,
8
+ serverId: McpServerIdSchema,
9
+ ttlSeconds: z.number().int().min(1).max(15 * 60),
10
+ }).strict();
11
+ const ExecutionAuthorityConfigSchema = z.object({
12
+ issuer: JwtIssuerSchema,
13
+ adopterId: OpaqueIdSchema,
14
+ executionAudience: JwtAudienceSchema,
15
+ keyId: OpaqueIdSchema,
16
+ executionTtlSeconds: z.number().int().min(1).max(15 * 60),
17
+ mcp: ExecutionAuthorityMcpConfigSchema.optional(),
18
+ }).strict().superRefine((config, context) => {
19
+ if (!config.mcp) {
20
+ return;
21
+ }
22
+ if (config.executionAudience === config.mcp.audience) {
23
+ context.addIssue({
24
+ code: 'custom',
25
+ path: ['mcp', 'audience'],
26
+ message: 'must be distinct from the execution audience',
27
+ });
28
+ }
29
+ if (config.mcp.ttlSeconds < config.executionTtlSeconds) {
30
+ context.addIssue({
31
+ code: 'custom',
32
+ path: ['mcp', 'ttlSeconds'],
33
+ message: 'must not expire before execution admission authority',
34
+ });
35
+ }
36
+ });
37
+ const ExecutionIssueScopeSchema = ExecutionScopeSchema.omit({ adopterId: true });
38
+ const ExecutionAuthorityIssueInputSchema = z.object({
39
+ scope: ExecutionIssueScopeSchema,
40
+ runtimeSessionId: RuntimeSessionIdSchema,
41
+ invocationId: OpaqueIdSchema,
42
+ workflow: z.literal(CONVERSATION_TURN_WORKFLOW),
43
+ mcp: z.object({
44
+ allowedTools: McpAllowedToolsSchema,
45
+ }).strict().optional(),
46
+ }).strict();
47
+ const PublicP256JwkSchema = z.object({
48
+ kty: z.literal('EC'),
49
+ crv: z.literal('P-256'),
50
+ x: z.string().regex(/^[A-Za-z0-9_-]+$/),
51
+ y: z.string().regex(/^[A-Za-z0-9_-]+$/),
52
+ }).passthrough().superRefine((jwk, context) => {
53
+ const privateFields = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'];
54
+ if (privateFields.some((field) => field in jwk)) {
55
+ context.addIssue({
56
+ code: 'custom',
57
+ message: 'must not contain private key material',
58
+ });
59
+ }
60
+ });
61
+ /**
62
+ * ES256 reference adapter for issuing one invocation's admission assertion and
63
+ * optional product-MCP capability.
64
+ */
65
+ export class JoseExecutionAuthority {
66
+ #config;
67
+ #privateKey;
68
+ #publicJwk;
69
+ #now;
70
+ #createCapabilityId;
71
+ constructor(config, key, options) {
72
+ this.#config = deepFreezeConfig(ExecutionAuthorityConfigSchema.parse(config));
73
+ assertPrivateSigningKey(key.privateKey);
74
+ this.#privateKey = key.privateKey;
75
+ this.#publicJwk = projectPublicJwk(key.publicJwk, this.#config.keyId);
76
+ this.#now = options.now ?? (() => new Date());
77
+ this.#createCapabilityId = options.createCapabilityId ?? randomUUID;
78
+ }
79
+ static async create(config, keyPair, options = {}) {
80
+ assertPrivateSigningKey(keyPair.privateKey);
81
+ assertPublicVerificationKey(keyPair.publicKey);
82
+ await assertMatchingKeyPair(keyPair);
83
+ return new JoseExecutionAuthority(config, {
84
+ privateKey: keyPair.privateKey,
85
+ publicJwk: await exportJWK(keyPair.publicKey),
86
+ }, options);
87
+ }
88
+ async issue(input) {
89
+ const authority = ExecutionAuthorityIssueInputSchema.parse(input);
90
+ if (authority.mcp && !this.#config.mcp) {
91
+ throw new Error('Execution authority cannot issue an MCP capability without MCP deployment configuration.');
92
+ }
93
+ const issuedAt = toEpochSeconds(this.#now());
94
+ const executionExpiresAt = issuedAt + this.#config.executionTtlSeconds;
95
+ const executionAssertion = await this.#signExecutionAssertion(authority, issuedAt, executionExpiresAt);
96
+ const mcp = authority.mcp && this.#config.mcp
97
+ ? await this.#issueMcpCapability({ ...authority, mcp: authority.mcp }, issuedAt)
98
+ : undefined;
99
+ return new ProtectedIssuedExecutionAuthority(executionAssertion, mcp?.assertion, freezeIssuedMetadata({
100
+ scope: {
101
+ adopterId: this.#config.adopterId,
102
+ ...authority.scope,
103
+ },
104
+ runtimeSessionId: authority.runtimeSessionId,
105
+ invocationId: authority.invocationId,
106
+ workflow: authority.workflow,
107
+ issuedAt: toIsoTimestamp(issuedAt),
108
+ executionExpiresAt: toIsoTimestamp(executionExpiresAt),
109
+ ...(mcp ? { mcp: mcp.metadata } : {}),
110
+ }));
111
+ }
112
+ publicJwks() {
113
+ return { keys: [{ ...this.#publicJwk }] };
114
+ }
115
+ #signExecutionAssertion(input, issuedAt, expiresAt) {
116
+ return new SignJWT({
117
+ contractVersion: EXECUTION_CONTRACT_VERSION,
118
+ adopterId: this.#config.adopterId,
119
+ tenantId: input.scope.tenantId,
120
+ productSessionId: input.scope.productSessionId,
121
+ runtimeSessionId: input.runtimeSessionId,
122
+ workflow: input.workflow,
123
+ })
124
+ .setProtectedHeader({
125
+ alg: SIGNING_ALGORITHM,
126
+ kid: this.#config.keyId,
127
+ typ: EXECUTION_ASSERTION_TYPE,
128
+ })
129
+ .setIssuer(this.#config.issuer)
130
+ .setAudience(this.#config.executionAudience)
131
+ .setSubject(input.scope.subjectId)
132
+ .setJti(input.invocationId)
133
+ .setIssuedAt(issuedAt)
134
+ .setExpirationTime(expiresAt)
135
+ .sign(this.#privateKey);
136
+ }
137
+ async #issueMcpCapability(input, issuedAt) {
138
+ const mcpConfig = this.#config.mcp;
139
+ const capabilityId = OpaqueIdSchema.parse(this.#createCapabilityId());
140
+ if (capabilityId === input.invocationId) {
141
+ throw new Error('MCP capability identity must be distinct from the invocation identity.');
142
+ }
143
+ const expiresAt = issuedAt + mcpConfig.ttlSeconds;
144
+ const assertion = await new SignJWT({
145
+ contractVersion: EXECUTION_CONTRACT_VERSION,
146
+ adopterId: this.#config.adopterId,
147
+ tenantId: input.scope.tenantId,
148
+ productSessionId: input.scope.productSessionId,
149
+ runtimeSessionId: input.runtimeSessionId,
150
+ invocationId: input.invocationId,
151
+ workflow: input.workflow,
152
+ serverId: mcpConfig.serverId,
153
+ allowedTools: [...input.mcp.allowedTools],
154
+ })
155
+ .setProtectedHeader({
156
+ alg: SIGNING_ALGORITHM,
157
+ kid: this.#config.keyId,
158
+ typ: MCP_CAPABILITY_TYPE,
159
+ })
160
+ .setIssuer(this.#config.issuer)
161
+ .setAudience(mcpConfig.audience)
162
+ .setSubject(input.scope.subjectId)
163
+ .setJti(capabilityId)
164
+ .setIssuedAt(issuedAt)
165
+ .setExpirationTime(expiresAt)
166
+ .sign(this.#privateKey);
167
+ return {
168
+ assertion,
169
+ metadata: {
170
+ capabilityId,
171
+ serverId: mcpConfig.serverId,
172
+ allowedTools: Object.freeze([...input.mcp.allowedTools]),
173
+ expiresAt: toIsoTimestamp(expiresAt),
174
+ },
175
+ };
176
+ }
177
+ }
178
+ class ProtectedIssuedExecutionAuthority {
179
+ metadata;
180
+ #executionAssertion;
181
+ #mcpCapability;
182
+ constructor(executionAssertion, mcpCapability, metadata) {
183
+ this.#executionAssertion = executionAssertion;
184
+ this.#mcpCapability = mcpCapability;
185
+ this.metadata = metadata;
186
+ }
187
+ executionAssertion() {
188
+ return this.#executionAssertion;
189
+ }
190
+ mcpCapability() {
191
+ return this.#mcpCapability;
192
+ }
193
+ toJSON() {
194
+ return this.metadata;
195
+ }
196
+ }
197
+ function projectPublicJwk(jwk, keyId) {
198
+ const parsed = PublicP256JwkSchema.parse(jwk);
199
+ return Object.freeze({
200
+ kty: parsed.kty,
201
+ crv: parsed.crv,
202
+ x: parsed.x,
203
+ y: parsed.y,
204
+ alg: SIGNING_ALGORITHM,
205
+ kid: keyId,
206
+ use: 'sig',
207
+ });
208
+ }
209
+ function assertPrivateSigningKey(key) {
210
+ const algorithm = key.algorithm;
211
+ const accepted = key.type === 'private'
212
+ && algorithm.name === 'ECDSA'
213
+ && algorithm.namedCurve === 'P-256'
214
+ && key.usages.includes('sign');
215
+ if (!accepted) {
216
+ throw new Error('Execution authority requires an ES256 private signing key.');
217
+ }
218
+ }
219
+ function assertPublicVerificationKey(key) {
220
+ const algorithm = key.algorithm;
221
+ const accepted = key.type === 'public'
222
+ && algorithm.name === 'ECDSA'
223
+ && algorithm.namedCurve === 'P-256'
224
+ && key.usages.includes('verify');
225
+ if (!accepted) {
226
+ throw new Error('Execution authority requires an ES256 public verification key.');
227
+ }
228
+ }
229
+ async function assertMatchingKeyPair(keyPair) {
230
+ try {
231
+ const probe = await new CompactSign(new TextEncoder().encode('heddle-adopter-execution-authority-key-pair'))
232
+ .setProtectedHeader({ alg: SIGNING_ALGORITHM })
233
+ .sign(keyPair.privateKey);
234
+ await compactVerify(probe, keyPair.publicKey, {
235
+ algorithms: [SIGNING_ALGORITHM],
236
+ });
237
+ }
238
+ catch {
239
+ throw new Error('Execution authority signing keys do not match.');
240
+ }
241
+ }
242
+ function toEpochSeconds(value) {
243
+ const seconds = Math.floor(value.getTime() / 1_000);
244
+ if (!Number.isSafeInteger(seconds) || seconds < 0) {
245
+ throw new Error('Execution authority could not resolve a valid issue time.');
246
+ }
247
+ return seconds;
248
+ }
249
+ function toIsoTimestamp(epochSeconds) {
250
+ return new Date(epochSeconds * 1_000).toISOString();
251
+ }
252
+ function deepFreezeConfig(config) {
253
+ const mcp = config.mcp ? Object.freeze({ ...config.mcp }) : undefined;
254
+ return Object.freeze({ ...config, ...(mcp ? { mcp } : {}) });
255
+ }
256
+ function freezeIssuedMetadata(metadata) {
257
+ const scope = Object.freeze({ ...metadata.scope });
258
+ const mcp = metadata.mcp
259
+ ? Object.freeze({
260
+ ...metadata.mcp,
261
+ allowedTools: Object.freeze([...metadata.mcp.allowedTools]),
262
+ })
263
+ : undefined;
264
+ return Object.freeze({ ...metadata, scope, ...(mcp ? { mcp } : {}) });
265
+ }
266
+ //# sourceMappingURL=jose-execution-authority.js.map