@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
@@ -0,0 +1,140 @@
1
+ import { createHash, timingSafeEqual, } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { AGENTCORE_RUNTIME_SESSION_HEADER, EXECUTION_ASSERTION_HEADER, EXECUTION_HOST_LOCAL_TOKEN_HEADER, ExecutionHostConversationTurnRequestSchema, MCP_CAPABILITY_HEADER, MODEL_API_KEY_HEADER, RuntimeSessionIdSchema, } from '../contracts/index.js';
4
+ const MAX_REQUEST_BODY_BYTES = 262_144;
5
+ const REDACTED_HEADER_VALUE = '[REDACTED]';
6
+ const SecretSchema = z.string().min(8).max(4_096);
7
+ const AssertionSchema = z.string().min(32).max(4_096);
8
+ const ErrorCodeSchema = z.string().min(1).max(128).regex(/^[a-z0-9_]+$/);
9
+ export async function parseLocalExecutionHostRequest(request, expectedLocalToken, signal) {
10
+ const headers = takeInvocationHeaders(request);
11
+ assertInvocationRoute(request);
12
+ assertLocalToken(headers.localToken, expectedLocalToken);
13
+ const runtimeSessionId = parseRequestValue(RuntimeSessionIdSchema, headers.runtimeSessionId, 400, 'invalid_runtime_session');
14
+ parseRequestValue(AssertionSchema, headers.executionAssertion, 401, 'invalid_execution_identity');
15
+ parseRequestValue(SecretSchema, headers.modelApiKey, 401, 'invalid_model_credential');
16
+ const mcpCapability = headers.mcpCapability === undefined
17
+ ? undefined
18
+ : parseRequestValue(AssertionSchema, headers.mcpCapability, 401, 'invalid_mcp_capability');
19
+ const parsedRequest = parseRequestValue(ExecutionHostConversationTurnRequestSchema, await readJsonBody(request, signal), 400, 'invalid_request');
20
+ return {
21
+ request: parsedRequest,
22
+ runtimeSessionId,
23
+ mcpCapability,
24
+ };
25
+ }
26
+ export function writeLocalExecutionHostRejection(response, error) {
27
+ const rejection = error instanceof LocalExecutionHostRequestError
28
+ ? { status: error.status, code: error.code }
29
+ : { status: 500, code: 'fixture_internal_error' };
30
+ const safeCode = ErrorCodeSchema.parse(rejection.code);
31
+ const body = JSON.stringify({
32
+ error: {
33
+ code: safeCode,
34
+ message: 'Local Execution Host fixture rejected the request.',
35
+ },
36
+ });
37
+ response.writeHead(rejection.status, {
38
+ 'Cache-Control': 'no-store',
39
+ 'Content-Length': Buffer.byteLength(body),
40
+ 'Content-Type': 'application/json; charset=utf-8',
41
+ 'X-Content-Type-Options': 'nosniff',
42
+ });
43
+ response.end(body);
44
+ }
45
+ class LocalExecutionHostRequestError extends Error {
46
+ status;
47
+ code;
48
+ constructor(status, code) {
49
+ super(code);
50
+ this.status = status;
51
+ this.code = code;
52
+ }
53
+ }
54
+ function takeInvocationHeaders(request) {
55
+ const extracted = {
56
+ localToken: extractSensitiveHeader(request, EXECUTION_HOST_LOCAL_TOKEN_HEADER),
57
+ runtimeSessionId: extractSensitiveHeader(request, AGENTCORE_RUNTIME_SESSION_HEADER),
58
+ executionAssertion: extractSensitiveHeader(request, EXECUTION_ASSERTION_HEADER),
59
+ mcpCapability: extractSensitiveHeader(request, MCP_CAPABILITY_HEADER),
60
+ modelApiKey: extractSensitiveHeader(request, MODEL_API_KEY_HEADER),
61
+ };
62
+ if (Object.values(extracted).some((header) => header.duplicate)) {
63
+ throw new LocalExecutionHostRequestError(400, 'duplicate_sensitive_header');
64
+ }
65
+ return Object.fromEntries(Object.entries(extracted).map(([name, header]) => [name, header.value]));
66
+ }
67
+ function extractSensitiveHeader(request, name) {
68
+ const occurrences = request.rawHeaders.reduce((count, headerName, index) => count
69
+ + (index % 2 === 0 && headerName.toLowerCase() === name ? 1 : 0), 0);
70
+ const value = request.headers[name];
71
+ request.headers[name] = value === undefined
72
+ ? undefined
73
+ : REDACTED_HEADER_VALUE;
74
+ for (let index = 0; index < request.rawHeaders.length; index += 2) {
75
+ if (request.rawHeaders[index]?.toLowerCase() === name) {
76
+ request.rawHeaders[index + 1] = REDACTED_HEADER_VALUE;
77
+ }
78
+ }
79
+ return {
80
+ value: Array.isArray(value) ? undefined : value,
81
+ duplicate: occurrences > 1 || Array.isArray(value),
82
+ };
83
+ }
84
+ function assertInvocationRoute(request) {
85
+ if (request.method !== 'POST') {
86
+ throw new LocalExecutionHostRequestError(405, 'method_not_allowed');
87
+ }
88
+ if (request.url !== '/invocations') {
89
+ throw new LocalExecutionHostRequestError(404, 'not_found');
90
+ }
91
+ const contentType = request.headers['content-type']
92
+ ?.split(';', 1)[0]
93
+ ?.trim()
94
+ .toLowerCase();
95
+ if (contentType !== 'application/json') {
96
+ throw new LocalExecutionHostRequestError(415, 'unsupported_media_type');
97
+ }
98
+ }
99
+ function assertLocalToken(actual, expected) {
100
+ if (!actual || !constantTimeEqual(actual, expected)) {
101
+ throw new LocalExecutionHostRequestError(401, 'invalid_local_token');
102
+ }
103
+ }
104
+ function constantTimeEqual(left, right) {
105
+ const leftHash = createHash('sha256').update(left).digest();
106
+ const rightHash = createHash('sha256').update(right).digest();
107
+ return timingSafeEqual(leftHash, rightHash);
108
+ }
109
+ async function readJsonBody(request, signal) {
110
+ const chunks = [];
111
+ let totalBytes = 0;
112
+ for await (const rawChunk of request) {
113
+ signal.throwIfAborted();
114
+ const chunk = Buffer.isBuffer(rawChunk)
115
+ ? rawChunk
116
+ : Buffer.from(rawChunk);
117
+ totalBytes += chunk.byteLength;
118
+ if (totalBytes > MAX_REQUEST_BODY_BYTES) {
119
+ throw new LocalExecutionHostRequestError(413, 'request_too_large');
120
+ }
121
+ chunks.push(chunk);
122
+ }
123
+ if (totalBytes === 0) {
124
+ throw new LocalExecutionHostRequestError(400, 'invalid_request');
125
+ }
126
+ try {
127
+ return JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8'));
128
+ }
129
+ catch {
130
+ throw new LocalExecutionHostRequestError(400, 'invalid_request');
131
+ }
132
+ }
133
+ function parseRequestValue(schema, value, status, code) {
134
+ const parsed = schema.safeParse(value);
135
+ if (!parsed.success) {
136
+ throw new LocalExecutionHostRequestError(status, code);
137
+ }
138
+ return parsed.data;
139
+ }
140
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/testing/request.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,eAAe,GAChB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,gCAAgC,EAChC,0BAA0B,EAC1B,iCAAiC,EACjC,0CAA0C,EAC1C,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,GAEvB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,sBAAsB,GAAG,OAAO,CAAC;AACvC,MAAM,qBAAqB,GAAG,YAAY,CAAC;AAC3C,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAClD,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACtD,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;AAQzE,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,OAAwB,EACxB,kBAA0B,EAC1B,MAAmB;IAEnB,MAAM,OAAO,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/C,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,gBAAgB,CAAC,OAAO,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;IACzD,MAAM,gBAAgB,GAAG,iBAAiB,CACxC,sBAAsB,EACtB,OAAO,CAAC,gBAAgB,EACxB,GAAG,EACH,yBAAyB,CAC1B,CAAC;IACF,iBAAiB,CACf,eAAe,EACf,OAAO,CAAC,kBAAkB,EAC1B,GAAG,EACH,4BAA4B,CAC7B,CAAC;IACF,iBAAiB,CACf,YAAY,EACZ,OAAO,CAAC,WAAW,EACnB,GAAG,EACH,0BAA0B,CAC3B,CAAC;IACF,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,KAAK,SAAS;QACvD,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,iBAAiB,CACjB,eAAe,EACf,OAAO,CAAC,aAAa,EACrB,GAAG,EACH,wBAAwB,CACzB,CAAC;IACJ,MAAM,aAAa,GAAG,iBAAiB,CACrC,0CAA0C,EAC1C,MAAM,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,EACnC,GAAG,EACH,iBAAiB,CAClB,CAAC;IACF,OAAO;QACL,OAAO,EAAE,aAAa;QACtB,gBAAgB;QAChB,aAAa;KACd,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC9C,QAAwB,EACxB,KAAc;IAEd,MAAM,SAAS,GAAG,KAAK,YAAY,8BAA8B;QAC/D,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE;QAC5C,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,wBAAwB,EAAE,CAAC;IACpD,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QAC1B,KAAK,EAAE;YACL,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,oDAAoD;SAC9D;KACF,CAAC,CAAC;IACH,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE;QACnC,eAAe,EAAE,UAAU;QAC3B,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;QACzC,cAAc,EAAE,iCAAiC;QACjD,wBAAwB,EAAE,SAAS;KACpC,CAAC,CAAC;IACH,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAUD,MAAM,8BAA+B,SAAQ,KAAK;IAErC;IACA;IAFX,YACW,MAAc,EACd,IAAY;QAErB,KAAK,CAAC,IAAI,CAAC,CAAC;QAHH,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;IAGvB,CAAC;CACF;AAED,SAAS,qBAAqB,CAAC,OAAwB;IACrD,MAAM,SAAS,GAAG;QAChB,UAAU,EAAE,sBAAsB,CAChC,OAAO,EACP,iCAAiC,CAClC;QACD,gBAAgB,EAAE,sBAAsB,CACtC,OAAO,EACP,gCAAgC,CACjC;QACD,kBAAkB,EAAE,sBAAsB,CACxC,OAAO,EACP,0BAA0B,CAC3B;QACD,aAAa,EAAE,sBAAsB,CAAC,OAAO,EAAE,qBAAqB,CAAC;QACrE,WAAW,EAAE,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,CAAC;KACnE,CAAC;IACF,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,8BAA8B,CACtC,GAAG,EACH,4BAA4B,CAC7B,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CACnD,CAAC;AACzB,CAAC;AAED,SAAS,sBAAsB,CAC7B,OAAwB,EACxB,IAAY;IAEZ,MAAM,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,CAC3C,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK;UAC/B,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAClE,CAAC,CACF,CAAC;IACF,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,KAAK,SAAS;QACzC,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,qBAAqB,CAAC;IAC1B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAClE,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC;YACtD,OAAO,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,qBAAqB,CAAC;QACxD,CAAC;IACH,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK;QAC/C,SAAS,EAAE,WAAW,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;KACnD,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAAC,OAAwB;IACrD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,IAAI,8BAA8B,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;IACtE,CAAC;IACD,IAAI,OAAO,CAAC,GAAG,KAAK,cAAc,EAAE,CAAC;QACnC,MAAM,IAAI,8BAA8B,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;QACjD,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,EAAE,IAAI,EAAE;SACP,WAAW,EAAE,CAAC;IACjB,IAAI,WAAW,KAAK,kBAAkB,EAAE,CAAC;QACvC,MAAM,IAAI,8BAA8B,CACtC,GAAG,EACH,wBAAwB,CACzB,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,MAA0B,EAAE,QAAgB;IACpE,IAAI,CAAC,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,8BAA8B,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY,EAAE,KAAa;IACpD,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;IAC5D,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;IAC9D,OAAO,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC9C,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,OAAwB,EACxB,MAAmB;IAEnB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,KAAK,EAAE,MAAM,QAAQ,IAAI,OAAO,EAAE,CAAC;QACrC,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACrC,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAsB,CAAC,CAAC;QACxC,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC;QAC/B,IAAI,UAAU,GAAG,sBAAsB,EAAE,CAAC;YACxC,MAAM,IAAI,8BAA8B,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;QACrE,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,8BAA8B,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,8BAA8B,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACnE,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CACxB,MAAoB,EACpB,KAAc,EACd,MAAc,EACd,IAAY;IAEZ,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,8BAA8B,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC","sourcesContent":["import {\n createHash,\n timingSafeEqual,\n} from 'node:crypto';\nimport type {\n IncomingMessage,\n ServerResponse,\n} from 'node:http';\nimport { z } from 'zod';\nimport {\n AGENTCORE_RUNTIME_SESSION_HEADER,\n EXECUTION_ASSERTION_HEADER,\n EXECUTION_HOST_LOCAL_TOKEN_HEADER,\n ExecutionHostConversationTurnRequestSchema,\n MCP_CAPABILITY_HEADER,\n MODEL_API_KEY_HEADER,\n RuntimeSessionIdSchema,\n type ExecutionHostConversationTurnRequest,\n} from '../contracts/index.js';\n\nconst MAX_REQUEST_BODY_BYTES = 262_144;\nconst REDACTED_HEADER_VALUE = '[REDACTED]';\nconst SecretSchema = z.string().min(8).max(4_096);\nconst AssertionSchema = z.string().min(32).max(4_096);\nconst ErrorCodeSchema = z.string().min(1).max(128).regex(/^[a-z0-9_]+$/);\n\nexport type ParsedLocalExecutionHostRequest = {\n request: ExecutionHostConversationTurnRequest;\n runtimeSessionId: string;\n mcpCapability?: string;\n};\n\nexport async function parseLocalExecutionHostRequest(\n request: IncomingMessage,\n expectedLocalToken: string,\n signal: AbortSignal,\n): Promise<ParsedLocalExecutionHostRequest> {\n const headers = takeInvocationHeaders(request);\n assertInvocationRoute(request);\n assertLocalToken(headers.localToken, expectedLocalToken);\n const runtimeSessionId = parseRequestValue(\n RuntimeSessionIdSchema,\n headers.runtimeSessionId,\n 400,\n 'invalid_runtime_session',\n );\n parseRequestValue(\n AssertionSchema,\n headers.executionAssertion,\n 401,\n 'invalid_execution_identity',\n );\n parseRequestValue(\n SecretSchema,\n headers.modelApiKey,\n 401,\n 'invalid_model_credential',\n );\n const mcpCapability = headers.mcpCapability === undefined\n ? undefined\n : parseRequestValue(\n AssertionSchema,\n headers.mcpCapability,\n 401,\n 'invalid_mcp_capability',\n );\n const parsedRequest = parseRequestValue(\n ExecutionHostConversationTurnRequestSchema,\n await readJsonBody(request, signal),\n 400,\n 'invalid_request',\n );\n return {\n request: parsedRequest,\n runtimeSessionId,\n mcpCapability,\n };\n}\n\nexport function writeLocalExecutionHostRejection(\n response: ServerResponse,\n error: unknown,\n): void {\n const rejection = error instanceof LocalExecutionHostRequestError\n ? { status: error.status, code: error.code }\n : { status: 500, code: 'fixture_internal_error' };\n const safeCode = ErrorCodeSchema.parse(rejection.code);\n const body = JSON.stringify({\n error: {\n code: safeCode,\n message: 'Local Execution Host fixture rejected the request.',\n },\n });\n response.writeHead(rejection.status, {\n 'Cache-Control': 'no-store',\n 'Content-Length': Buffer.byteLength(body),\n 'Content-Type': 'application/json; charset=utf-8',\n 'X-Content-Type-Options': 'nosniff',\n });\n response.end(body);\n}\n\ntype InvocationHeaders = {\n localToken: string | undefined;\n runtimeSessionId: string | undefined;\n executionAssertion: string | undefined;\n mcpCapability: string | undefined;\n modelApiKey: string | undefined;\n};\n\nclass LocalExecutionHostRequestError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n ) {\n super(code);\n }\n}\n\nfunction takeInvocationHeaders(request: IncomingMessage): InvocationHeaders {\n const extracted = {\n localToken: extractSensitiveHeader(\n request,\n EXECUTION_HOST_LOCAL_TOKEN_HEADER,\n ),\n runtimeSessionId: extractSensitiveHeader(\n request,\n AGENTCORE_RUNTIME_SESSION_HEADER,\n ),\n executionAssertion: extractSensitiveHeader(\n request,\n EXECUTION_ASSERTION_HEADER,\n ),\n mcpCapability: extractSensitiveHeader(request, MCP_CAPABILITY_HEADER),\n modelApiKey: extractSensitiveHeader(request, MODEL_API_KEY_HEADER),\n };\n if (Object.values(extracted).some((header) => header.duplicate)) {\n throw new LocalExecutionHostRequestError(\n 400,\n 'duplicate_sensitive_header',\n );\n }\n return Object.fromEntries(\n Object.entries(extracted).map(([name, header]) => [name, header.value]),\n ) as InvocationHeaders;\n}\n\nfunction extractSensitiveHeader(\n request: IncomingMessage,\n name: string,\n): { value: string | undefined; duplicate: boolean } {\n const occurrences = request.rawHeaders.reduce(\n (count, headerName, index) => count\n + (index % 2 === 0 && headerName.toLowerCase() === name ? 1 : 0),\n 0,\n );\n const value = request.headers[name];\n request.headers[name] = value === undefined\n ? undefined\n : REDACTED_HEADER_VALUE;\n for (let index = 0; index < request.rawHeaders.length; index += 2) {\n if (request.rawHeaders[index]?.toLowerCase() === name) {\n request.rawHeaders[index + 1] = REDACTED_HEADER_VALUE;\n }\n }\n return {\n value: Array.isArray(value) ? undefined : value,\n duplicate: occurrences > 1 || Array.isArray(value),\n };\n}\n\nfunction assertInvocationRoute(request: IncomingMessage): void {\n if (request.method !== 'POST') {\n throw new LocalExecutionHostRequestError(405, 'method_not_allowed');\n }\n if (request.url !== '/invocations') {\n throw new LocalExecutionHostRequestError(404, 'not_found');\n }\n const contentType = request.headers['content-type']\n ?.split(';', 1)[0]\n ?.trim()\n .toLowerCase();\n if (contentType !== 'application/json') {\n throw new LocalExecutionHostRequestError(\n 415,\n 'unsupported_media_type',\n );\n }\n}\n\nfunction assertLocalToken(actual: string | undefined, expected: string): void {\n if (!actual || !constantTimeEqual(actual, expected)) {\n throw new LocalExecutionHostRequestError(401, 'invalid_local_token');\n }\n}\n\nfunction constantTimeEqual(left: string, right: string): boolean {\n const leftHash = createHash('sha256').update(left).digest();\n const rightHash = createHash('sha256').update(right).digest();\n return timingSafeEqual(leftHash, rightHash);\n}\n\nasync function readJsonBody(\n request: IncomingMessage,\n signal: AbortSignal,\n): Promise<unknown> {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n for await (const rawChunk of request) {\n signal.throwIfAborted();\n const chunk = Buffer.isBuffer(rawChunk)\n ? rawChunk\n : Buffer.from(rawChunk as Uint8Array);\n totalBytes += chunk.byteLength;\n if (totalBytes > MAX_REQUEST_BODY_BYTES) {\n throw new LocalExecutionHostRequestError(413, 'request_too_large');\n }\n chunks.push(chunk);\n }\n if (totalBytes === 0) {\n throw new LocalExecutionHostRequestError(400, 'invalid_request');\n }\n try {\n return JSON.parse(Buffer.concat(chunks, totalBytes).toString('utf8'));\n } catch {\n throw new LocalExecutionHostRequestError(400, 'invalid_request');\n }\n}\n\nfunction parseRequestValue<T>(\n schema: z.ZodType<T>,\n value: unknown,\n status: number,\n code: string,\n): T {\n const parsed = schema.safeParse(value);\n if (!parsed.success) {\n throw new LocalExecutionHostRequestError(status, code);\n }\n return parsed.data;\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import type { ExecutionHostConversationTurnRequest, RuntimePublicResult } from '../contracts/index.js';
2
+ /**
3
+ * One locally admitted invocation. Credentials remain behind explicit methods
4
+ * so ordinary object inspection and JSON serialization cannot reveal them.
5
+ */
6
+ export interface LocalExecutionHostInvocation {
7
+ readonly request: ExecutionHostConversationTurnRequest;
8
+ readonly runtimeSessionId: string;
9
+ readonly signal: AbortSignal;
10
+ mcpCapability(): string | undefined;
11
+ publishActivity(activity: unknown): Promise<void>;
12
+ toJSON(): LocalExecutionHostInvocationMetadata;
13
+ }
14
+ export type LocalExecutionHostInvocationMetadata = {
15
+ schemaVersion: 1;
16
+ kind: 'conversation-turn';
17
+ invocationId: string;
18
+ runtimeSessionId: string;
19
+ };
20
+ export type LocalExecutionHostTerminal = {
21
+ kind: 'result';
22
+ result: RuntimePublicResult;
23
+ } | {
24
+ kind: 'cancelled';
25
+ reason: string;
26
+ } | {
27
+ kind: 'error';
28
+ error: {
29
+ code: string;
30
+ message: string;
31
+ };
32
+ } | {
33
+ /** End the SSE response without a terminal event to exercise recovery. */
34
+ kind: 'interrupted';
35
+ };
36
+ export type LocalExecutionHostExecutor = (invocation: LocalExecutionHostInvocation) => LocalExecutionHostTerminal | Promise<LocalExecutionHostTerminal>;
37
+ export type LocalExecutionHostContractFixtureOptions = {
38
+ execute: LocalExecutionHostExecutor;
39
+ now?: () => Date;
40
+ createRunId?: () => string;
41
+ };
42
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/testing/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oCAAoC,EACpC,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,OAAO,EAAE,oCAAoC,CAAC;IACvD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,aAAa,IAAI,MAAM,GAAG,SAAS,CAAC;IACpC,eAAe,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,MAAM,IAAI,oCAAoC,CAAC;CAChD;AAED,MAAM,MAAM,oCAAoC,GAAG;IACjD,aAAa,EAAE,CAAC,CAAC;IACjB,IAAI,EAAE,mBAAmB,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAClC;IACA,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;CAC7B,GACC;IACA,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,GACC;IACA,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,GACC;IACA,0EAA0E;IAC1E,IAAI,EAAE,aAAa,CAAC;CACrB,CAAC;AAEJ,MAAM,MAAM,0BAA0B,GAAG,CACvC,UAAU,EAAE,4BAA4B,KACrC,0BAA0B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;AAEtE,MAAM,MAAM,wCAAwC,GAAG;IACrD,OAAO,EAAE,0BAA0B,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/testing/types.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n ExecutionHostConversationTurnRequest,\n RuntimePublicResult,\n} from '../contracts/index.js';\n\n/**\n * One locally admitted invocation. Credentials remain behind explicit methods\n * so ordinary object inspection and JSON serialization cannot reveal them.\n */\nexport interface LocalExecutionHostInvocation {\n readonly request: ExecutionHostConversationTurnRequest;\n readonly runtimeSessionId: string;\n readonly signal: AbortSignal;\n mcpCapability(): string | undefined;\n publishActivity(activity: unknown): Promise<void>;\n toJSON(): LocalExecutionHostInvocationMetadata;\n}\n\nexport type LocalExecutionHostInvocationMetadata = {\n schemaVersion: 1;\n kind: 'conversation-turn';\n invocationId: string;\n runtimeSessionId: string;\n};\n\nexport type LocalExecutionHostTerminal =\n | {\n kind: 'result';\n result: RuntimePublicResult;\n }\n | {\n kind: 'cancelled';\n reason: string;\n }\n | {\n kind: 'error';\n error: {\n code: string;\n message: string;\n };\n }\n | {\n /** End the SSE response without a terminal event to exercise recovery. */\n kind: 'interrupted';\n };\n\nexport type LocalExecutionHostExecutor = (\n invocation: LocalExecutionHostInvocation,\n) => LocalExecutionHostTerminal | Promise<LocalExecutionHostTerminal>;\n\nexport type LocalExecutionHostContractFixtureOptions = {\n execute: LocalExecutionHostExecutor;\n now?: () => Date;\n createRunId?: () => string;\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@heddleagent/execution-host-client",
3
+ "version": "6.0.0-next.0",
4
+ "description": "Backend contracts, execution authority, lifecycle services, and clients for compatible Heddle Execution Hosts",
5
+ "author": "Jay / Fienna Liang <roackb2@gmail.com>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "publishConfig": {
10
+ "access": "public",
11
+ "tag": "next",
12
+ "registry": "https://registry.npmjs.org/"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/roackb2/heddle.git",
17
+ "directory": "packages/execution-host-client"
18
+ },
19
+ "homepage": "https://heddleagent.com",
20
+ "bugs": {
21
+ "url": "https://github.com/roackb2/heddle/issues"
22
+ },
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./contracts": {
34
+ "types": "./dist/contracts/index.d.ts",
35
+ "import": "./dist/contracts/index.js"
36
+ },
37
+ "./authority": {
38
+ "types": "./dist/authority/index.d.ts",
39
+ "import": "./dist/authority/index.js"
40
+ },
41
+ "./conversation": {
42
+ "types": "./dist/conversation/index.d.ts",
43
+ "import": "./dist/conversation/index.js"
44
+ },
45
+ "./mcp": {
46
+ "types": "./dist/mcp/index.d.ts",
47
+ "import": "./dist/mcp/index.js"
48
+ },
49
+ "./mcp/node": {
50
+ "types": "./dist/mcp/node/index.d.ts",
51
+ "import": "./dist/mcp/node/index.js"
52
+ },
53
+ "./http-sse": {
54
+ "types": "./dist/http-sse/index.d.ts",
55
+ "import": "./dist/http-sse/index.js"
56
+ },
57
+ "./testing": {
58
+ "types": "./dist/testing/index.d.ts",
59
+ "import": "./dist/testing/index.js"
60
+ },
61
+ "./node": {
62
+ "types": "./dist/node/index.d.ts",
63
+ "import": "./dist/node/index.js"
64
+ },
65
+ "./spec/v1/openapi.json": "./spec/v1/openapi.json",
66
+ "./spec/v1/schema-bundle.json": "./spec/v1/schema-bundle.json",
67
+ "./spec/v1/fixtures/*": "./spec/v1/fixtures/*",
68
+ "./package.json": "./package.json"
69
+ },
70
+ "files": [
71
+ "dist",
72
+ "spec",
73
+ "README.md",
74
+ "LICENSE"
75
+ ],
76
+ "keywords": [
77
+ "agent",
78
+ "ai-agent",
79
+ "backend",
80
+ "execution-host",
81
+ "mcp",
82
+ "sdk",
83
+ "streaming"
84
+ ],
85
+ "dependencies": {
86
+ "@modelcontextprotocol/sdk": "^1.29.0",
87
+ "dayjs": "^1.11.20",
88
+ "eventsource-parser": "^3.1.0",
89
+ "jose": "^6.2.8",
90
+ "zod": "^4.3.6"
91
+ }
92
+ }
@@ -0,0 +1,101 @@
1
+ # Execution Host adopter contract v1
2
+
3
+ This directory is the language-neutral contract for an adopter backend which
4
+ invokes a Heddle-compatible Execution Host. It is included in
5
+ the `@heddleagent/execution-host-client` candidate tarball, but consuming it
6
+ does not require JavaScript or TypeScript. The candidate itself is not yet
7
+ published on npm.
8
+
9
+ ## Artifacts
10
+
11
+ - `openapi.json` describes the current HTTP/SSE invocation binding.
12
+ - `schema-bundle.json` contains standalone JSON Schema Draft 2020-12
13
+ definitions for requests, events, JWT claims, protected JWT headers, results,
14
+ safe API errors, and the optional durable conversation-lifecycle profile.
15
+ - `fixtures/` contains executable examples for valid and invalid requests,
16
+ complete and interrupted streams, cancellation, sequence validation, and
17
+ execution/MCP authority. `durable-conversation-lifecycle.json` supplies the
18
+ shared TypeScript/Python terminal and store-transition vectors.
19
+ - `durable-hosted-conversation-lifecycle.md` defines persistence ordering,
20
+ transition fencing, safe projection, interruption, and reconciliation for a
21
+ product-owned lifecycle store.
22
+
23
+ Generate checked-in artifacts from Heddle's runtime schemas:
24
+
25
+ ```bash
26
+ yarn execution-host-client:contract:generate
27
+ ```
28
+
29
+ CI and package builds use the non-writing drift check:
30
+
31
+ ```bash
32
+ yarn execution-host-client:contract:verify
33
+ ```
34
+
35
+ The checked-in JSON files are the candidate interoperability artifacts. The
36
+ TypeScript Zod schemas remain the reference implementation used to generate
37
+ them. A non-TypeScript adopter should implement the JSON/SSE/JWT contract and
38
+ run the golden fixtures; it should not port Heddle's agent loop.
39
+
40
+ ## Semantic invariants
41
+
42
+ JSON Schema validates one value at a time. A conforming implementation must
43
+ also enforce these relationships:
44
+
45
+ 1. Credentials are headers, never request-body fields, prompt content, event
46
+ content, logs, or persisted product state.
47
+ 2. The first event is `accepted` with sequence `0`.
48
+ 3. Every later event has the same `invocationId` and `runId`, and sequence
49
+ numbers increase by exactly one.
50
+ 4. Exactly one `result`, `cancelled`, or `error` event is terminal and last.
51
+ 5. A terminal event is committed only after clean HTTP EOF. EOF without a
52
+ terminal is ambiguous interruption, never inferred success and never an
53
+ automatic retry.
54
+ 6. The execution assertion uses ES256 and
55
+ `typ=heddle-execution+jwt`; its `jti` equals `invocationId`.
56
+ 7. The optional MCP capability uses ES256 and
57
+ `typ=heddle-mcp-capability+jwt`; its `jti` is distinct from
58
+ `invocationId`.
59
+ 8. The host binds every duplicated adopter, tenant, subject, product session,
60
+ runtime session, invocation, and workflow claim across the request and both
61
+ credentials.
62
+ 9. The adopter MCP edge verifies the capability independently, derives scope
63
+ only from verified claims, rejects unsupported tools, and rechecks expiry
64
+ before each operation.
65
+ 10. `allowedTools` is unique, contains at most 16 collision-free names, and the
66
+ aggregate tool-name length is at most 512 characters.
67
+
68
+ The optional durable profile adds control-plane semantics around this stream.
69
+ It does not add a database endpoint to the Execution Host or make product
70
+ records part of the execution plane. See
71
+ [Durable hosted-conversation lifecycle](durable-hosted-conversation-lifecycle.md).
72
+
73
+ ## Deployment bindings
74
+
75
+ The OpenAPI document names the actual v1 headers so implementations can
76
+ interoperate with the current Execution Host. Two headers are explicitly
77
+ binding-specific:
78
+
79
+ - `X-Amzn-Bedrock-AgentCore-Runtime-Session-Id` carries the logical
80
+ `runtimeSessionId` in the AgentCore deployment. Another runtime adapter must
81
+ preserve the same logical value even if its provider uses a different wire
82
+ mechanism.
83
+ - `X-Heddle-Execution-Host-Local-Token` is only for the direct loopback
84
+ development binding. Managed AgentCore ingress uses AWS authorization and
85
+ explicitly forwarded custom headers instead.
86
+
87
+ The OpenAPI contract does not describe Terraform, image layout, SigV4 calls,
88
+ AWS account identity, or provider lifecycle. Those remain deployment concerns,
89
+ not adopter-domain contracts.
90
+
91
+ ## Compatibility and stop line
92
+
93
+ Version 1 supports `conversation-turn`. New workflows, transports, or identity
94
+ forms require an explicit contract version decision; they are not silently
95
+ added to this surface.
96
+
97
+ The repository deliberately stops after this schema, fixture set, the
98
+ TypeScript implementation, and one Python clean-room reference pass. It does
99
+ not promise a gateway, generated clients for every language, or a framework
100
+ starter matrix. Additional adapters should be driven by a real adopter and a
101
+ specific interoperability gap.
@@ -0,0 +1,139 @@
1
+ # Durable hosted-conversation lifecycle profile v1
2
+
3
+ This optional profile defines the adopter-side state machine around one
4
+ `conversation-turn` invocation. It is language-neutral even though the
5
+ supported TypeScript SDK supplies the ready-made service. Other languages can
6
+ implement the same behavior against their own database by using the versioned
7
+ schemas and `fixtures/durable-conversation-lifecycle.json`.
8
+
9
+ The profile does not move product data into Heddle. The adopter remains the
10
+ owner of the record, database, authenticated scope, retention, history query,
11
+ and UI. Heddle owns the correctness-sensitive transition and stream-ordering
12
+ rules that should not be independently reinvented by every product.
13
+
14
+ ## Participants
15
+
16
+ 1. **Product admission** authenticates the caller and selects `tenantId`,
17
+ `subjectId`, `productSessionId`, `runtimeSessionId`, a unique
18
+ `invocationId`, prompt, and optional deadline.
19
+ 2. **Lifecycle service** persists safe checkpoints around an already-composed
20
+ conversation-turn runner.
21
+ 3. **Lifecycle store** implements atomic, scope-fenced transitions in the
22
+ adopter's database.
23
+ 4. **Execution runner** issues authority, invokes the Execution Host, and
24
+ returns the validated v1 event stream.
25
+
26
+ Only the Execution Host imports the Heddle runtime in a separate-host
27
+ deployment. The adopter lifecycle service is control-plane code and never
28
+ receives the host workspace or Heddle's private trace state.
29
+
30
+ ## State machine
31
+
32
+ ```mermaid
33
+ stateDiagram-v2
34
+ [*] --> requested: create unique invocation
35
+ requested --> running: accepted persisted
36
+ requested --> failed: dependency or protocol failure
37
+ requested --> interrupted: abort or ambiguous end
38
+ running --> completed: result done
39
+ running --> max_steps: result max_steps
40
+ running --> failed: error result or failure
41
+ running --> cancelled: explicit cancelled terminal
42
+ running --> interrupted: interruption or ambiguous end
43
+ ```
44
+
45
+ `completed`, `max_steps`, `failed`, `cancelled`, and `interrupted` are terminal
46
+ and immutable. A terminal write can be repeated only when every persisted
47
+ field, including `settledAt`, is identical.
48
+
49
+ ### Store transition rules
50
+
51
+ - `createTurn` rejects every duplicate `invocationId`, including an identical
52
+ request. This prevents a repeated product request from starting execution
53
+ twice.
54
+ - Every later mutation is fenced by `invocationId` and the complete tenant,
55
+ subject, and product-session scope.
56
+ - `recordAccepted` changes `requested` to `running`. An exact repeat with the
57
+ same `runId` and `acceptedAt` is idempotent while the row is running.
58
+ - Before acceptance, only `failed` or `interrupted` can settle the row.
59
+ - After acceptance, any defined terminal status can settle the row.
60
+ - A wrong-scope, conflicting, or late transition fails atomically. It must not
61
+ silently update zero rows and report success.
62
+ - Expiry reconciliation changes only open `requested` or `running` rows in the
63
+ supplied scope whose `deadlineAt` is earlier than `expiredBefore`. It records
64
+ `interrupted/deadline_elapsed` and never overwrites a terminal row.
65
+
66
+ The candidate TypeScript conformance helper and Python reference tests execute
67
+ these rules. A real adapter must run them against its actual transactional
68
+ store, not only an in-memory substitute.
69
+
70
+ ## Persistence-before-delivery ordering
71
+
72
+ The lifecycle service observes this order:
73
+
74
+ 1. validate normalized product input;
75
+ 2. persist `requested`;
76
+ 3. start the execution runner;
77
+ 4. persist `running` before releasing `accepted`;
78
+ 5. pass through bounded public activity without persisting it;
79
+ 6. project and persist a terminal before releasing that terminal; and
80
+ 7. settle an open row as interrupted when the stream throws, ends ambiguously,
81
+ or is closed by its consumer.
82
+
83
+ If requested persistence fails, execution does not start. If accepted
84
+ persistence fails, `accepted` is not released. If terminal persistence fails,
85
+ the terminal is not released. A possibly committed write is not blindly
86
+ retried or overwritten; an open row remains eligible for later reconciliation.
87
+
88
+ The runner must emit one `accepted` event before activity or a terminal. A
89
+ terminal is trustworthy only after the underlying v1 HTTP body reaches clean
90
+ EOF. Duplicate acceptance, terminal-before-acceptance, missing terminal, and
91
+ post-terminal data are protocol failures or ambiguous interruption, never
92
+ success.
93
+
94
+ ## Terminal projection
95
+
96
+ The durable projection uses the closed statuses and failure codes in the
97
+ versioned schema bundle and golden fixture. Important distinctions are:
98
+
99
+ - only an explicit `cancelled` terminal becomes `cancelled`;
100
+ - request abort, client disconnect, server shutdown, task cancellation, and
101
+ local invocation cancellation become `interrupted/invocation_aborted`;
102
+ - clean EOF without a terminal becomes
103
+ `interrupted/stream_ended_without_terminal`;
104
+ - transport loss becomes `interrupted/stream_interrupted`;
105
+ - arbitrary host error codes and messages become the fixed durable code
106
+ `execution_error`;
107
+ - model failures map only through the fixed public model-code allowlist; and
108
+ - unknown thrown errors become `failed/execution_failed` without storing their
109
+ messages or types.
110
+
111
+ Summary bounds count Unicode code points, not UTF-8 bytes or UTF-16 code units.
112
+ The exact same bounded summary is released live and sent to the store, so a
113
+ reload cannot silently show different content from the live turn.
114
+
115
+ ## Data allowed across the store port
116
+
117
+ The lifecycle store may receive only:
118
+
119
+ - authorized tenant, subject, and product-session scope;
120
+ - invocation ID, prompt, optional deadline, and lifecycle timestamps;
121
+ - accepted run ID; and
122
+ - bounded terminal summary and a closed failure code.
123
+
124
+ It must never receive activity, tool arguments or results, hidden reasoning,
125
+ raw errors, provider-selected error text, model credentials, execution
126
+ assertions, MCP capabilities, JWTs, traces, or workspace contents.
127
+
128
+ ## Reconciliation and recovery limit
129
+
130
+ Reconciliation needs a product deadline. A turn without `deadlineAt` is not
131
+ expired by this profile because Heddle cannot invent the product's maximum run
132
+ time. Adopters that require crash convergence should always supply a deadline
133
+ and invoke scoped reconciliation while open rows are visible or from their
134
+ existing scheduler.
135
+
136
+ This profile deliberately does not define result lookup, stream reconnect,
137
+ replay, automatic retry, billing, history-list limits, or UI polling. Ambiguous
138
+ execution is recorded as interrupted and is never automatically rerun. A
139
+ future lookup/reconnect protocol requires its own explicit contract decision.
@@ -0,0 +1,9 @@
1
+ id: 0
2
+ event: accepted
3
+ data: {"schemaVersion":1,"invocationId":"invocation-001","runId":"run-001","sequence":0,"timestamp":"2026-08-10T04:00:00.000Z","kind":"accepted"}
4
+
5
+ id: 1
6
+ event: activity
7
+ data: {"schemaVersion":1,"invocationId":"invocation-001","runId":"run-001","sequence":1,"timestamp":"2026-08-10T04:00:00.000Z","kind":"activity","activity":{"type":"assistant_text_delta","text":"Working."}}
8
+
9
+ : fixture-clean-eof
@@ -0,0 +1,70 @@
1
+ {
2
+ "referenceTime": "2026-08-10T04:00:00.000Z",
3
+ "issuer": "https://api.example.test",
4
+ "keyId": "test-key-001",
5
+ "executionAudience": "urn:heddle-execution-host:example",
6
+ "mcpAudience": "urn:example:mcp",
7
+ "supportedTools": [
8
+ "read_snapshot"
9
+ ],
10
+ "expected": {
11
+ "executionClaims": {
12
+ "iss": "https://api.example.test",
13
+ "aud": "urn:heddle-execution-host:example",
14
+ "contractVersion": 1,
15
+ "adopterId": "example-adopter",
16
+ "tenantId": "tenant-a",
17
+ "productSessionId": "product-session-a",
18
+ "runtimeSessionId": "runtime-session:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
19
+ "workflow": "conversation-turn",
20
+ "sub": "subject-a",
21
+ "jti": "invocation-001",
22
+ "iat": 1786334400,
23
+ "exp": 1786334700
24
+ },
25
+ "mcpClaims": {
26
+ "iss": "https://api.example.test",
27
+ "aud": "urn:example:mcp",
28
+ "contractVersion": 1,
29
+ "adopterId": "example-adopter",
30
+ "tenantId": "tenant-a",
31
+ "productSessionId": "product-session-a",
32
+ "runtimeSessionId": "runtime-session:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
33
+ "workflow": "conversation-turn",
34
+ "sub": "subject-a",
35
+ "jti": "capability-001",
36
+ "iat": 1786334400,
37
+ "exp": 1786335000,
38
+ "invocationId": "invocation-001",
39
+ "serverId": "product_capabilities",
40
+ "allowedTools": [
41
+ "read_snapshot"
42
+ ]
43
+ }
44
+ },
45
+ "invalidMcpCases": [
46
+ {
47
+ "id": "expired",
48
+ "overrides": {
49
+ "exp": 1786334399
50
+ },
51
+ "expected": "invalid"
52
+ },
53
+ {
54
+ "id": "unsupported-tool",
55
+ "overrides": {
56
+ "allowedTools": [
57
+ "delete_workspace"
58
+ ]
59
+ },
60
+ "expected": "invalid"
61
+ },
62
+ {
63
+ "id": "swapped-runtime-session",
64
+ "overrides": {
65
+ "runtimeSessionId": "runtime-session:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
66
+ },
67
+ "expected": "host_binding_invalid"
68
+ }
69
+ ]
70
+ }
@@ -0,0 +1,9 @@
1
+ id: 0
2
+ event: accepted
3
+ data: {"schemaVersion":1,"invocationId":"invocation-001","runId":"run-001","sequence":0,"timestamp":"2026-08-10T04:00:00.000Z","kind":"accepted"}
4
+
5
+ id: 1
6
+ event: cancelled
7
+ data: {"schemaVersion":1,"invocationId":"invocation-001","runId":"run-001","sequence":1,"timestamp":"2026-08-10T04:00:00.000Z","kind":"cancelled","reason":"product_cancelled"}
8
+
9
+ : fixture-clean-eof