@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 @@
1
+ {"version":3,"file":"event-stream.js","sourceRoot":"","sources":["../../src/testing/event-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,0BAA0B,EAC1B,8BAA8B,EAC9B,cAAc,EACd,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAG/B,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;AACzE,MAAM,cAAc,GAAG,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;IAClD,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;QACzB,MAAM,EAAE,yBAAyB;KAClC,CAAC,CAAC,MAAM,EAAE;IACX,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC;QAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;KACrC,CAAC,CAAC,MAAM,EAAE;IACX,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QACxB,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;YACd,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;SACtC,CAAC,CAAC,MAAM,EAAE;KACZ,CAAC,CAAC,MAAM,EAAE;IACX,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;CACtD,CAAC,CAAC;AAOH,MAAM,OAAO,6BAA6B;IAC/B,SAAS,CAAiB;IAC1B,OAAO,CAAc;IACrB,aAAa,CAAS;IACtB,MAAM,CAAS;IACf,IAAI,CAAa;IAC1B,SAAS,GAAG,CAAC,CAAC;IAEd,YAAoB,KAMnB;QACC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC;QAChC,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC,YAAY,CAAC;QACxC,IAAI,CAAC,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC;IACxB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAMjB;QACC,MAAM,MAAM,GAAG,IAAI,6BAA6B,CAAC,KAAK,CAAC,CAAC;QACxD,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE;YAC5B,eAAe,EAAE,UAAU;YAC3B,cAAc,EAAE,kCAAkC;YAClD,wBAAwB,EAAE,SAAS;SACpC,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,eAAe,CAAC,QAAiB;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,QAAoC;QAC/C,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;QAC9B,IAAI,QAAQ,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACpC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAe;QAC1B,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,8BAA8B,CAAC,KAAK,CAAC;YACjD,aAAa,EAAE,0BAA0B;YACzC,YAAY,EAAE,IAAI,CAAC,aAAa;YAChC,KAAK,EAAE,IAAI,CAAC,MAAM;YAClB,QAAQ,EAAE,IAAI,CAAC,SAAS;YACxB,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE;YACpC,GAAG,IAAI;SACR,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;QACpB,MAAM,KAAK,GAAG;YACZ,OAAO,KAAK,CAAC,QAAQ,EAAE;YACvB,UAAU,KAAK,CAAC,IAAI,EAAE;YACtB,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;YAChC,EAAE;YACF,EAAE;SACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,+BAA+B,CAC7C,KAAc;IAEd,OAAO,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACrC,CAAC","sourcesContent":["import { once } from 'node:events';\nimport type { ServerResponse } from 'node:http';\nimport { z } from 'zod';\nimport {\n EXECUTION_CONTRACT_VERSION,\n ExecutionHostStreamEventSchema,\n OpaqueIdSchema,\n RuntimePublicResultSchema,\n} from '../contracts/index.js';\nimport type { LocalExecutionHostTerminal } from './types.js';\n\nconst ErrorCodeSchema = z.string().min(1).max(128).regex(/^[a-z0-9_]+$/);\nconst TerminalSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('result'),\n result: RuntimePublicResultSchema,\n }).strict(),\n z.object({\n kind: z.literal('cancelled'),\n reason: z.string().min(1).max(1_600),\n }).strict(),\n z.object({\n kind: z.literal('error'),\n error: z.object({\n code: ErrorCodeSchema,\n message: z.string().min(1).max(1_600),\n }).strict(),\n }).strict(),\n z.object({ kind: z.literal('interrupted') }).strict(),\n]);\n\ntype EventBody =\n | { kind: 'accepted' }\n | { kind: 'activity'; activity: unknown }\n | Exclude<LocalExecutionHostTerminal, { kind: 'interrupted' }>;\n\nexport class LocalExecutionHostEventStream {\n readonly #response: ServerResponse;\n readonly #signal: AbortSignal;\n readonly #invocationId: string;\n readonly #runId: string;\n readonly #now: () => Date;\n #sequence = 0;\n\n private constructor(input: {\n response: ServerResponse;\n signal: AbortSignal;\n invocationId: string;\n runId: string;\n now: () => Date;\n }) {\n this.#response = input.response;\n this.#signal = input.signal;\n this.#invocationId = input.invocationId;\n this.#runId = OpaqueIdSchema.parse(input.runId);\n this.#now = input.now;\n }\n\n static async open(input: {\n response: ServerResponse;\n signal: AbortSignal;\n invocationId: string;\n runId: string;\n now: () => Date;\n }): Promise<LocalExecutionHostEventStream> {\n const stream = new LocalExecutionHostEventStream(input);\n input.response.writeHead(200, {\n 'Cache-Control': 'no-store',\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'X-Content-Type-Options': 'nosniff',\n });\n await stream.#write({ kind: 'accepted' });\n return stream;\n }\n\n publishActivity(activity: unknown): Promise<void> {\n return this.#write({ kind: 'activity', activity });\n }\n\n async settle(terminal: LocalExecutionHostTerminal): Promise<void> {\n this.#signal.throwIfAborted();\n if (terminal.kind !== 'interrupted') {\n await this.#write(terminal);\n }\n this.#response.end();\n }\n\n async #write(body: EventBody): Promise<void> {\n this.#signal.throwIfAborted();\n const event = ExecutionHostStreamEventSchema.parse({\n schemaVersion: EXECUTION_CONTRACT_VERSION,\n invocationId: this.#invocationId,\n runId: this.#runId,\n sequence: this.#sequence,\n timestamp: this.#now().toISOString(),\n ...body,\n });\n this.#sequence += 1;\n const frame = [\n `id: ${event.sequence}`,\n `event: ${event.kind}`,\n `data: ${JSON.stringify(event)}`,\n '',\n '',\n ].join('\\n');\n if (!this.#response.write(frame)) {\n await once(this.#response, 'drain', { signal: this.#signal });\n }\n }\n}\n\nexport function parseLocalExecutionHostTerminal(\n value: unknown,\n): LocalExecutionHostTerminal {\n return TerminalSchema.parse(value);\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import { type HostedConversationTurnIdentity, type HostedConversationTurnLifecycleRecord, type HostedConversationTurnLifecycleStore } from '../conversation/index.js';
2
+ export type HostedConversationTurnStoreConformanceHarness = {
3
+ store: HostedConversationTurnLifecycleStore;
4
+ findTurn(identity: HostedConversationTurnIdentity): Promise<HostedConversationTurnLifecycleRecord | undefined>;
5
+ clear(): Promise<void>;
6
+ };
7
+ /**
8
+ * Certifies the atomic transition behavior required by the durable turn
9
+ * service using the same versioned vectors as non-TypeScript implementations.
10
+ */
11
+ export declare class HostedConversationTurnStoreConformance {
12
+ static verify(harness: HostedConversationTurnStoreConformanceHarness): Promise<void>;
13
+ private static verifyLifecycleAndFencing;
14
+ private static verifyPreAcceptanceFailure;
15
+ private static verifyScopedExpiry;
16
+ }
17
+ //# sourceMappingURL=hosted-conversation-turn-store-conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-conversation-turn-store-conformance.d.ts","sourceRoot":"","sources":["../../src/testing/hosted-conversation-turn-store-conformance.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,8BAA8B,EACnC,KAAK,qCAAqC,EAC1C,KAAK,oCAAoC,EAC1C,MAAM,0BAA0B,CAAC;AAkDlC,MAAM,MAAM,6CAA6C,GAAG;IAC1D,KAAK,EAAE,oCAAoC,CAAC;IAC5C,QAAQ,CACN,QAAQ,EAAE,8BAA8B,GACvC,OAAO,CAAC,qCAAqC,GAAG,SAAS,CAAC,CAAC;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAEF;;;GAGG;AACH,qBAAa,sCAAsC;WACpC,MAAM,CACjB,OAAO,EAAE,6CAA6C,GACrD,OAAO,CAAC,IAAI,CAAC;mBAoBK,yBAAyB;mBAgDzB,0BAA0B;mBAgB1B,kBAAkB;CA8CxC"}
@@ -0,0 +1,137 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { z } from 'zod';
4
+ import { HostedConversationAcceptedTurnSchema, HostedConversationExpiredTurnReconciliationSchema, HostedConversationRequestedTurnSchema, HostedConversationTurnSettlementSchema, } from '../conversation/index.js';
5
+ const fixtureUrl = new URL('../../spec/v1/fixtures/durable-conversation-lifecycle.json', import.meta.url);
6
+ const RunningRecordSchema = HostedConversationRequestedTurnSchema.extend({
7
+ status: z.literal('running'),
8
+ runId: HostedConversationAcceptedTurnSchema.shape.runId,
9
+ acceptedAt: HostedConversationAcceptedTurnSchema.shape.acceptedAt,
10
+ }).strict();
11
+ const CompletedRecordSchema = RunningRecordSchema.extend({
12
+ status: z.literal('completed'),
13
+ summary: z.string(),
14
+ settledAt: HostedConversationTurnSettlementSchema.options[0].shape.settledAt,
15
+ }).strict();
16
+ const CompletedSettlementSchema = HostedConversationTurnSettlementSchema.options[0];
17
+ const FailedSettlementSchema = HostedConversationTurnSettlementSchema.options[2];
18
+ const StoreFixtureSchema = z.object({
19
+ storeCases: z.object({
20
+ lifecycleAndFencing: z.object({
21
+ requested: HostedConversationRequestedTurnSchema,
22
+ wrongScopeAccepted: HostedConversationAcceptedTurnSchema,
23
+ accepted: HostedConversationAcceptedTurnSchema,
24
+ conflictingAccepted: HostedConversationAcceptedTurnSchema,
25
+ conflictingAcceptedAt: HostedConversationAcceptedTurnSchema,
26
+ completed: CompletedSettlementSchema,
27
+ wrongScopeCompleted: CompletedSettlementSchema,
28
+ conflictingCompleted: FailedSettlementSchema,
29
+ conflictingSettledAt: CompletedSettlementSchema,
30
+ expectedRunning: RunningRecordSchema,
31
+ expectedCompleted: CompletedRecordSchema,
32
+ }).strict(),
33
+ preAcceptanceFailure: z.object({
34
+ requested: HostedConversationRequestedTurnSchema,
35
+ settlement: FailedSettlementSchema,
36
+ }).strict(),
37
+ expiry: z.object({
38
+ expiredRequested: HostedConversationRequestedTurnSchema,
39
+ expiredRunning: HostedConversationRequestedTurnSchema,
40
+ expiredRunningAcceptance: HostedConversationAcceptedTurnSchema,
41
+ futureRequested: HostedConversationRequestedTurnSchema,
42
+ terminalRequested: HostedConversationRequestedTurnSchema,
43
+ terminalSettlement: FailedSettlementSchema,
44
+ otherScopeExpired: HostedConversationRequestedTurnSchema,
45
+ reconciliation: HostedConversationExpiredTurnReconciliationSchema,
46
+ }).strict(),
47
+ }).strict(),
48
+ }).passthrough();
49
+ /**
50
+ * Certifies the atomic transition behavior required by the durable turn
51
+ * service using the same versioned vectors as non-TypeScript implementations.
52
+ */
53
+ export class HostedConversationTurnStoreConformance {
54
+ static async verify(harness) {
55
+ const fixture = await loadStoreFixture();
56
+ await harness.clear();
57
+ try {
58
+ await this.verifyLifecycleAndFencing(harness, fixture.lifecycleAndFencing);
59
+ await harness.clear();
60
+ await this.verifyPreAcceptanceFailure(harness, fixture.preAcceptanceFailure);
61
+ await harness.clear();
62
+ await this.verifyScopedExpiry(harness, fixture.expiry);
63
+ }
64
+ finally {
65
+ await harness.clear();
66
+ }
67
+ }
68
+ static async verifyLifecycleAndFencing(harness, fixture) {
69
+ const { store } = harness;
70
+ await store.createTurn(fixture.requested);
71
+ assert.deepEqual(await harness.findTurn(fixture.requested), {
72
+ ...fixture.requested,
73
+ status: 'requested',
74
+ });
75
+ await assert.rejects(() => store.createTurn(fixture.requested));
76
+ await assert.rejects(() => store.recordAccepted(fixture.wrongScopeAccepted));
77
+ await store.recordAccepted(fixture.accepted);
78
+ await store.recordAccepted(fixture.accepted);
79
+ await assert.rejects(() => store.recordAccepted(fixture.conflictingAccepted));
80
+ await assert.rejects(() => store.recordAccepted(fixture.conflictingAcceptedAt));
81
+ assert.deepEqual(await harness.findTurn(fixture.requested), fixture.expectedRunning);
82
+ await assert.rejects(() => store.settleTurn(fixture.wrongScopeCompleted));
83
+ await store.settleTurn(fixture.completed);
84
+ await store.settleTurn(fixture.completed);
85
+ await assert.rejects(() => store.settleTurn(fixture.conflictingCompleted));
86
+ await assert.rejects(() => store.settleTurn(fixture.conflictingSettledAt));
87
+ await assert.rejects(() => store.recordAccepted(fixture.accepted));
88
+ assert.deepEqual(await harness.findTurn(fixture.requested), fixture.expectedCompleted);
89
+ }
90
+ static async verifyPreAcceptanceFailure(harness, fixture) {
91
+ await harness.store.createTurn(fixture.requested);
92
+ await harness.store.settleTurn(fixture.settlement);
93
+ assert.deepEqual(await harness.findTurn(fixture.requested), {
94
+ ...fixture.requested,
95
+ status: fixture.settlement.status,
96
+ failureCode: fixture.settlement.failureCode,
97
+ settledAt: fixture.settlement.settledAt,
98
+ });
99
+ }
100
+ static async verifyScopedExpiry(harness, fixture) {
101
+ await Promise.all([
102
+ harness.store.createTurn(fixture.expiredRequested),
103
+ harness.store.createTurn(fixture.expiredRunning),
104
+ harness.store.createTurn(fixture.futureRequested),
105
+ harness.store.createTurn(fixture.terminalRequested),
106
+ harness.store.createTurn(fixture.otherScopeExpired),
107
+ ]);
108
+ await harness.store.recordAccepted(fixture.expiredRunningAcceptance);
109
+ await harness.store.settleTurn(fixture.terminalSettlement);
110
+ await harness.store.interruptExpiredTurns(fixture.reconciliation);
111
+ for (const expired of [
112
+ fixture.expiredRequested,
113
+ fixture.expiredRunning,
114
+ ]) {
115
+ assert.deepEqual(await harness.findTurn(expired), {
116
+ ...expired,
117
+ ...(expired.invocationId === fixture.expiredRunning.invocationId
118
+ ? {
119
+ status: 'interrupted',
120
+ runId: fixture.expiredRunningAcceptance.runId,
121
+ acceptedAt: fixture.expiredRunningAcceptance.acceptedAt,
122
+ }
123
+ : { status: 'interrupted' }),
124
+ failureCode: 'deadline_elapsed',
125
+ settledAt: fixture.reconciliation.settledAt,
126
+ });
127
+ }
128
+ assert.equal((await harness.findTurn(fixture.futureRequested))?.status, 'requested');
129
+ assert.equal((await harness.findTurn(fixture.terminalRequested))?.status, 'failed');
130
+ assert.equal((await harness.findTurn(fixture.otherScopeExpired))?.status, 'requested');
131
+ }
132
+ }
133
+ async function loadStoreFixture() {
134
+ const raw = JSON.parse(await readFile(fixtureUrl, 'utf8'));
135
+ return StoreFixtureSchema.parse(raw).storeCases;
136
+ }
137
+ //# sourceMappingURL=hosted-conversation-turn-store-conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-conversation-turn-store-conformance.js","sourceRoot":"","sources":["../../src/testing/hosted-conversation-turn-store-conformance.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from 'node:assert/strict';\nimport { readFile } from 'node:fs/promises';\nimport { z } from 'zod';\nimport {\n HostedConversationAcceptedTurnSchema,\n HostedConversationExpiredTurnReconciliationSchema,\n HostedConversationRequestedTurnSchema,\n HostedConversationTurnSettlementSchema,\n type HostedConversationTurnIdentity,\n type HostedConversationTurnLifecycleRecord,\n type HostedConversationTurnLifecycleStore,\n} from '../conversation/index.js';\n\nconst fixtureUrl = new URL(\n '../../spec/v1/fixtures/durable-conversation-lifecycle.json',\n import.meta.url,\n);\nconst RunningRecordSchema = HostedConversationRequestedTurnSchema.extend({\n status: z.literal('running'),\n runId: HostedConversationAcceptedTurnSchema.shape.runId,\n acceptedAt: HostedConversationAcceptedTurnSchema.shape.acceptedAt,\n}).strict();\nconst CompletedRecordSchema = RunningRecordSchema.extend({\n status: z.literal('completed'),\n summary: z.string(),\n settledAt: HostedConversationTurnSettlementSchema.options[0].shape.settledAt,\n}).strict();\nconst CompletedSettlementSchema = HostedConversationTurnSettlementSchema.options[0];\nconst FailedSettlementSchema = HostedConversationTurnSettlementSchema.options[2];\nconst StoreFixtureSchema = z.object({\n storeCases: z.object({\n lifecycleAndFencing: z.object({\n requested: HostedConversationRequestedTurnSchema,\n wrongScopeAccepted: HostedConversationAcceptedTurnSchema,\n accepted: HostedConversationAcceptedTurnSchema,\n conflictingAccepted: HostedConversationAcceptedTurnSchema,\n conflictingAcceptedAt: HostedConversationAcceptedTurnSchema,\n completed: CompletedSettlementSchema,\n wrongScopeCompleted: CompletedSettlementSchema,\n conflictingCompleted: FailedSettlementSchema,\n conflictingSettledAt: CompletedSettlementSchema,\n expectedRunning: RunningRecordSchema,\n expectedCompleted: CompletedRecordSchema,\n }).strict(),\n preAcceptanceFailure: z.object({\n requested: HostedConversationRequestedTurnSchema,\n settlement: FailedSettlementSchema,\n }).strict(),\n expiry: z.object({\n expiredRequested: HostedConversationRequestedTurnSchema,\n expiredRunning: HostedConversationRequestedTurnSchema,\n expiredRunningAcceptance: HostedConversationAcceptedTurnSchema,\n futureRequested: HostedConversationRequestedTurnSchema,\n terminalRequested: HostedConversationRequestedTurnSchema,\n terminalSettlement: FailedSettlementSchema,\n otherScopeExpired: HostedConversationRequestedTurnSchema,\n reconciliation: HostedConversationExpiredTurnReconciliationSchema,\n }).strict(),\n }).strict(),\n}).passthrough();\n\nexport type HostedConversationTurnStoreConformanceHarness = {\n store: HostedConversationTurnLifecycleStore;\n findTurn(\n identity: HostedConversationTurnIdentity,\n ): Promise<HostedConversationTurnLifecycleRecord | undefined>;\n clear(): Promise<void>;\n};\n\n/**\n * Certifies the atomic transition behavior required by the durable turn\n * service using the same versioned vectors as non-TypeScript implementations.\n */\nexport class HostedConversationTurnStoreConformance {\n static async verify(\n harness: HostedConversationTurnStoreConformanceHarness,\n ): Promise<void> {\n const fixture = await loadStoreFixture();\n await harness.clear();\n try {\n await this.verifyLifecycleAndFencing(\n harness,\n fixture.lifecycleAndFencing,\n );\n await harness.clear();\n await this.verifyPreAcceptanceFailure(\n harness,\n fixture.preAcceptanceFailure,\n );\n await harness.clear();\n await this.verifyScopedExpiry(harness, fixture.expiry);\n } finally {\n await harness.clear();\n }\n }\n\n private static async verifyLifecycleAndFencing(\n harness: HostedConversationTurnStoreConformanceHarness,\n fixture: z.infer<typeof StoreFixtureSchema>['storeCases'][\n 'lifecycleAndFencing'\n ],\n ): Promise<void> {\n const { store } = harness;\n await store.createTurn(fixture.requested);\n assert.deepEqual(await harness.findTurn(fixture.requested), {\n ...fixture.requested,\n status: 'requested',\n });\n await assert.rejects(() => store.createTurn(fixture.requested));\n\n await assert.rejects(() => store.recordAccepted(\n fixture.wrongScopeAccepted,\n ));\n await store.recordAccepted(fixture.accepted);\n await store.recordAccepted(fixture.accepted);\n await assert.rejects(() => store.recordAccepted(\n fixture.conflictingAccepted,\n ));\n await assert.rejects(() => store.recordAccepted(\n fixture.conflictingAcceptedAt,\n ));\n assert.deepEqual(\n await harness.findTurn(fixture.requested),\n fixture.expectedRunning,\n );\n\n await assert.rejects(() => store.settleTurn(\n fixture.wrongScopeCompleted,\n ));\n await store.settleTurn(fixture.completed);\n await store.settleTurn(fixture.completed);\n await assert.rejects(() => store.settleTurn(\n fixture.conflictingCompleted,\n ));\n await assert.rejects(() => store.settleTurn(\n fixture.conflictingSettledAt,\n ));\n await assert.rejects(() => store.recordAccepted(fixture.accepted));\n assert.deepEqual(\n await harness.findTurn(fixture.requested),\n fixture.expectedCompleted,\n );\n }\n\n private static async verifyPreAcceptanceFailure(\n harness: HostedConversationTurnStoreConformanceHarness,\n fixture: z.infer<typeof StoreFixtureSchema>['storeCases'][\n 'preAcceptanceFailure'\n ],\n ): Promise<void> {\n await harness.store.createTurn(fixture.requested);\n await harness.store.settleTurn(fixture.settlement);\n assert.deepEqual(await harness.findTurn(fixture.requested), {\n ...fixture.requested,\n status: fixture.settlement.status,\n failureCode: fixture.settlement.failureCode,\n settledAt: fixture.settlement.settledAt,\n });\n }\n\n private static async verifyScopedExpiry(\n harness: HostedConversationTurnStoreConformanceHarness,\n fixture: z.infer<typeof StoreFixtureSchema>['storeCases']['expiry'],\n ): Promise<void> {\n await Promise.all([\n harness.store.createTurn(fixture.expiredRequested),\n harness.store.createTurn(fixture.expiredRunning),\n harness.store.createTurn(fixture.futureRequested),\n harness.store.createTurn(fixture.terminalRequested),\n harness.store.createTurn(fixture.otherScopeExpired),\n ]);\n await harness.store.recordAccepted(fixture.expiredRunningAcceptance);\n await harness.store.settleTurn(fixture.terminalSettlement);\n\n await harness.store.interruptExpiredTurns(fixture.reconciliation);\n\n for (const expired of [\n fixture.expiredRequested,\n fixture.expiredRunning,\n ]) {\n assert.deepEqual(await harness.findTurn(expired), {\n ...expired,\n ...(expired.invocationId === fixture.expiredRunning.invocationId\n ? {\n status: 'interrupted',\n runId: fixture.expiredRunningAcceptance.runId,\n acceptedAt: fixture.expiredRunningAcceptance.acceptedAt,\n }\n : { status: 'interrupted' }),\n failureCode: 'deadline_elapsed',\n settledAt: fixture.reconciliation.settledAt,\n });\n }\n assert.equal(\n (await harness.findTurn(fixture.futureRequested))?.status,\n 'requested',\n );\n assert.equal(\n (await harness.findTurn(fixture.terminalRequested))?.status,\n 'failed',\n );\n assert.equal(\n (await harness.findTurn(fixture.otherScopeExpired))?.status,\n 'requested',\n );\n }\n}\n\nasync function loadStoreFixture(): Promise<\n z.infer<typeof StoreFixtureSchema>['storeCases']\n> {\n const raw = JSON.parse(await readFile(fixtureUrl, 'utf8')) as unknown;\n return StoreFixtureSchema.parse(raw).storeCases;\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export { LocalExecutionHostContractFixture, } from './local-execution-host-contract-fixture.js';
2
+ export { HostedConversationTurnStoreConformance, } from './hosted-conversation-turn-store-conformance.js';
3
+ export type { HostedConversationTurnStoreConformanceHarness, } from './hosted-conversation-turn-store-conformance.js';
4
+ export type { LocalExecutionHostExecutor, LocalExecutionHostContractFixtureOptions, LocalExecutionHostInvocation, LocalExecutionHostInvocationMetadata, LocalExecutionHostTerminal, } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iCAAiC,GAClC,MAAM,4CAA4C,CAAC;AACpD,OAAO,EACL,sCAAsC,GACvC,MAAM,iDAAiD,CAAC;AACzD,YAAY,EACV,6CAA6C,GAC9C,MAAM,iDAAiD,CAAC;AACzD,YAAY,EACV,0BAA0B,EAC1B,wCAAwC,EACxC,4BAA4B,EAC5B,oCAAoC,EACpC,0BAA0B,GAC3B,MAAM,YAAY,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { LocalExecutionHostContractFixture, } from './local-execution-host-contract-fixture.js';
2
+ export { HostedConversationTurnStoreConformance, } from './hosted-conversation-turn-store-conformance.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iCAAiC,GAClC,MAAM,4CAA4C,CAAC;AACpD,OAAO,EACL,sCAAsC,GACvC,MAAM,iDAAiD,CAAC","sourcesContent":["export {\n LocalExecutionHostContractFixture,\n} from './local-execution-host-contract-fixture.js';\nexport {\n HostedConversationTurnStoreConformance,\n} from './hosted-conversation-turn-store-conformance.js';\nexport type {\n HostedConversationTurnStoreConformanceHarness,\n} from './hosted-conversation-turn-store-conformance.js';\nexport type {\n LocalExecutionHostExecutor,\n LocalExecutionHostContractFixtureOptions,\n LocalExecutionHostInvocation,\n LocalExecutionHostInvocationMetadata,\n LocalExecutionHostTerminal,\n} from './types.js';\n"]}
@@ -0,0 +1,20 @@
1
+ import type { ExecutionHostConversationTurnRequest } from '../contracts/index.js';
2
+ import type { LocalExecutionHostInvocation, LocalExecutionHostInvocationMetadata } from './types.js';
3
+ /** Keeps request-scoped authority out of enumerable object properties. */
4
+ export declare class CredentialBoundLocalInvocation implements LocalExecutionHostInvocation {
5
+ #private;
6
+ constructor(input: {
7
+ request: ExecutionHostConversationTurnRequest;
8
+ runtimeSessionId: string;
9
+ signal: AbortSignal;
10
+ mcpCapability?: string;
11
+ publishActivity: (activity: unknown) => Promise<void>;
12
+ });
13
+ get request(): ExecutionHostConversationTurnRequest;
14
+ get runtimeSessionId(): string;
15
+ get signal(): AbortSignal;
16
+ mcpCapability(): string | undefined;
17
+ publishActivity(activity: unknown): Promise<void>;
18
+ toJSON(): LocalExecutionHostInvocationMetadata;
19
+ }
20
+ //# sourceMappingURL=invocation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invocation.d.ts","sourceRoot":"","sources":["../../src/testing/invocation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,KAAK,EACV,4BAA4B,EAC5B,oCAAoC,EACrC,MAAM,YAAY,CAAC;AAEpB,0EAA0E;AAC1E,qBAAa,8BACb,YAAW,4BAA4B;;gBAOzB,KAAK,EAAE;QACjB,OAAO,EAAE,oCAAoC,CAAC;QAC9C,gBAAgB,EAAE,MAAM,CAAC;QACzB,MAAM,EAAE,WAAW,CAAC;QACpB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACvD;IAQD,IAAI,OAAO,IAAI,oCAAoC,CAElD;IAED,IAAI,gBAAgB,IAAI,MAAM,CAE7B;IAED,IAAI,MAAM,IAAI,WAAW,CAExB;IAED,aAAa,IAAI,MAAM,GAAG,SAAS;IAInC,eAAe,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjD,MAAM,IAAI,oCAAoC;CAQ/C"}
@@ -0,0 +1,39 @@
1
+ /** Keeps request-scoped authority out of enumerable object properties. */
2
+ export class CredentialBoundLocalInvocation {
3
+ #request;
4
+ #runtimeSessionId;
5
+ #signal;
6
+ #mcpCapability;
7
+ #publishActivity;
8
+ constructor(input) {
9
+ this.#request = Object.freeze({ ...input.request });
10
+ this.#runtimeSessionId = input.runtimeSessionId;
11
+ this.#signal = input.signal;
12
+ this.#mcpCapability = input.mcpCapability;
13
+ this.#publishActivity = input.publishActivity;
14
+ }
15
+ get request() {
16
+ return this.#request;
17
+ }
18
+ get runtimeSessionId() {
19
+ return this.#runtimeSessionId;
20
+ }
21
+ get signal() {
22
+ return this.#signal;
23
+ }
24
+ mcpCapability() {
25
+ return this.#mcpCapability;
26
+ }
27
+ publishActivity(activity) {
28
+ return this.#publishActivity(activity);
29
+ }
30
+ toJSON() {
31
+ return {
32
+ schemaVersion: this.#request.schemaVersion,
33
+ kind: this.#request.kind,
34
+ invocationId: this.#request.invocationId,
35
+ runtimeSessionId: this.#runtimeSessionId,
36
+ };
37
+ }
38
+ }
39
+ //# sourceMappingURL=invocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invocation.js","sourceRoot":"","sources":["../../src/testing/invocation.ts"],"names":[],"mappings":"AAMA,0EAA0E;AAC1E,MAAM,OAAO,8BAA8B;IAEhC,QAAQ,CAAuC;IAC/C,iBAAiB,CAAS;IAC1B,OAAO,CAAc;IACrB,cAAc,CAAqB;IACnC,gBAAgB,CAAuC;IAEhE,YAAY,KAMX;QACC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC,gBAAgB,CAAC;QAChD,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,aAAa,CAAC;QAC1C,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,eAAe,CAAC;IAChD,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,IAAI,gBAAgB;QAClB,OAAO,IAAI,CAAC,iBAAiB,CAAC;IAChC,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,aAAa;QACX,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED,eAAe,CAAC,QAAiB;QAC/B,OAAO,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,MAAM;QACJ,OAAO;YACL,aAAa,EAAE,IAAI,CAAC,QAAQ,CAAC,aAAa;YAC1C,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;YACxB,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,YAAY;YACxC,gBAAgB,EAAE,IAAI,CAAC,iBAAiB;SACzC,CAAC;IACJ,CAAC;CACF","sourcesContent":["import type { ExecutionHostConversationTurnRequest } from '../contracts/index.js';\nimport type {\n LocalExecutionHostInvocation,\n LocalExecutionHostInvocationMetadata,\n} from './types.js';\n\n/** Keeps request-scoped authority out of enumerable object properties. */\nexport class CredentialBoundLocalInvocation\nimplements LocalExecutionHostInvocation {\n readonly #request: ExecutionHostConversationTurnRequest;\n readonly #runtimeSessionId: string;\n readonly #signal: AbortSignal;\n readonly #mcpCapability: string | undefined;\n readonly #publishActivity: (activity: unknown) => Promise<void>;\n\n constructor(input: {\n request: ExecutionHostConversationTurnRequest;\n runtimeSessionId: string;\n signal: AbortSignal;\n mcpCapability?: string;\n publishActivity: (activity: unknown) => Promise<void>;\n }) {\n this.#request = Object.freeze({ ...input.request });\n this.#runtimeSessionId = input.runtimeSessionId;\n this.#signal = input.signal;\n this.#mcpCapability = input.mcpCapability;\n this.#publishActivity = input.publishActivity;\n }\n\n get request(): ExecutionHostConversationTurnRequest {\n return this.#request;\n }\n\n get runtimeSessionId(): string {\n return this.#runtimeSessionId;\n }\n\n get signal(): AbortSignal {\n return this.#signal;\n }\n\n mcpCapability(): string | undefined {\n return this.#mcpCapability;\n }\n\n publishActivity(activity: unknown): Promise<void> {\n return this.#publishActivity(activity);\n }\n\n toJSON(): LocalExecutionHostInvocationMetadata {\n return {\n schemaVersion: this.#request.schemaVersion,\n kind: this.#request.kind,\n invocationId: this.#request.invocationId,\n runtimeSessionId: this.#runtimeSessionId,\n };\n }\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { DirectHttpExecutionHost } from '../http-sse/index.js';
2
+ import type { LocalExecutionHostContractFixtureOptions } from './types.js';
3
+ /**
4
+ * Real loopback implementation of the Execution Host v1 HTTP/SSE boundary.
5
+ *
6
+ * It is intentionally a contract fixture, not a fake Heddle runtime. The
7
+ * supplied executor can call product APIs or MCP tools, while this class owns
8
+ * request validation, credential containment, SSE ordering, and lifecycle.
9
+ */
10
+ export declare class LocalExecutionHostContractFixture {
11
+ #private;
12
+ private constructor();
13
+ static start(options: LocalExecutionHostContractFixtureOptions): Promise<LocalExecutionHostContractFixture>;
14
+ /** Return a fresh client paired with this fixture's hidden local token. */
15
+ createExecutionHost(): DirectHttpExecutionHost;
16
+ /** A copy of the loopback URL, useful for explicit negative protocol tests. */
17
+ baseUrl(): URL;
18
+ /** Abort active invocations, close sockets, and wait for handler cleanup. */
19
+ close(): Promise<void>;
20
+ }
21
+ //# sourceMappingURL=local-execution-host-contract-fixture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-execution-host-contract-fixture.d.ts","sourceRoot":"","sources":["../../src/testing/local-execution-host-contract-fixture.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAW/D,OAAO,KAAK,EACV,wCAAwC,EAEzC,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,qBAAa,iCAAiC;;IAW5C,OAAO;WAaM,KAAK,CAChB,OAAO,EAAE,wCAAwC,GAChD,OAAO,CAAC,iCAAiC,CAAC;IAM7C,2EAA2E;IAC3E,mBAAmB,IAAI,uBAAuB;IAO9C,+EAA+E;IAC/E,OAAO,IAAI,GAAG;IAOd,6EAA6E;IAC7E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAmKvB"}
@@ -0,0 +1,197 @@
1
+ import { randomBytes, randomUUID, } from 'node:crypto';
2
+ import { once } from 'node:events';
3
+ import { createServer, } from 'node:http';
4
+ import { DirectHttpExecutionHost } from '../http-sse/index.js';
5
+ import { LocalExecutionHostEventStream, parseLocalExecutionHostTerminal, } from './event-stream.js';
6
+ import { CredentialBoundLocalInvocation } from './invocation.js';
7
+ import { parseLocalExecutionHostRequest, writeLocalExecutionHostRejection, } from './request.js';
8
+ /**
9
+ * Real loopback implementation of the Execution Host v1 HTTP/SSE boundary.
10
+ *
11
+ * It is intentionally a contract fixture, not a fake Heddle runtime. The
12
+ * supplied executor can call product APIs or MCP tools, while this class owns
13
+ * request validation, credential containment, SSE ordering, and lifecycle.
14
+ */
15
+ export class LocalExecutionHostContractFixture {
16
+ #execute;
17
+ #now;
18
+ #createRunId;
19
+ #localToken;
20
+ #server;
21
+ #activeInvocations = new Set();
22
+ #baseUrl;
23
+ #closePromise;
24
+ #closing = false;
25
+ constructor(options) {
26
+ if (typeof options.execute !== 'function') {
27
+ throw new TypeError('Local Execution Host fixture requires an executor.');
28
+ }
29
+ this.#execute = options.execute;
30
+ this.#now = options.now ?? (() => new Date());
31
+ this.#createRunId = options.createRunId ?? randomUUID;
32
+ this.#localToken = randomBytes(32).toString('base64url');
33
+ this.#server = createServer((request, response) => {
34
+ this.#dispatch(request, response);
35
+ });
36
+ }
37
+ static async start(options) {
38
+ const fixture = new LocalExecutionHostContractFixture(options);
39
+ await fixture.#listen();
40
+ return fixture;
41
+ }
42
+ /** Return a fresh client paired with this fixture's hidden local token. */
43
+ createExecutionHost() {
44
+ return new DirectHttpExecutionHost({
45
+ baseUrl: this.baseUrl(),
46
+ localToken: this.#localToken,
47
+ });
48
+ }
49
+ /** A copy of the loopback URL, useful for explicit negative protocol tests. */
50
+ baseUrl() {
51
+ if (!this.#baseUrl) {
52
+ throw new Error('Local Execution Host fixture has not started.');
53
+ }
54
+ return new URL(this.#baseUrl);
55
+ }
56
+ /** Abort active invocations, close sockets, and wait for handler cleanup. */
57
+ close() {
58
+ this.#closePromise ??= this.#close();
59
+ return this.#closePromise;
60
+ }
61
+ async #listen() {
62
+ this.#server.listen(0, '127.0.0.1');
63
+ await once(this.#server, 'listening');
64
+ const address = this.#server.address();
65
+ if (!address || typeof address === 'string') {
66
+ await this.close();
67
+ throw new Error('Local Execution Host fixture did not bind a TCP port.');
68
+ }
69
+ this.#baseUrl = new URL(`http://127.0.0.1:${address.port}/`);
70
+ }
71
+ #dispatch(request, response) {
72
+ const controller = new AbortController();
73
+ if (this.#closing) {
74
+ controller.abort();
75
+ request.destroy();
76
+ response.destroy();
77
+ return;
78
+ }
79
+ const active = {
80
+ controller,
81
+ completed: Promise.resolve(),
82
+ };
83
+ const completed = this.#handle(request, response, controller)
84
+ .catch(() => {
85
+ response.destroy();
86
+ })
87
+ .finally(() => {
88
+ this.#activeInvocations.delete(active);
89
+ });
90
+ active.completed = completed;
91
+ this.#activeInvocations.add(active);
92
+ }
93
+ async #handle(request, response, controller) {
94
+ const abort = () => controller.abort();
95
+ const abortIncompleteResponse = () => {
96
+ if (!response.writableEnded) {
97
+ abort();
98
+ }
99
+ };
100
+ request.once('aborted', abort);
101
+ response.once('close', abortIncompleteResponse);
102
+ try {
103
+ const parsed = await parseLocalExecutionHostRequest(request, this.#localToken, controller.signal);
104
+ controller.signal.throwIfAborted();
105
+ await this.#streamInvocation(parsed, response, controller);
106
+ }
107
+ catch (error) {
108
+ if (controller.signal.aborted) {
109
+ response.destroy();
110
+ return;
111
+ }
112
+ if (!response.headersSent) {
113
+ writeLocalExecutionHostRejection(response, error);
114
+ return;
115
+ }
116
+ response.destroy();
117
+ }
118
+ finally {
119
+ request.off('aborted', abort);
120
+ response.off('close', abortIncompleteResponse);
121
+ }
122
+ }
123
+ async #streamInvocation(input, response, controller) {
124
+ const stream = await LocalExecutionHostEventStream.open({
125
+ response,
126
+ signal: controller.signal,
127
+ invocationId: input.request.invocationId,
128
+ runId: this.#createRunId(),
129
+ now: this.#now,
130
+ });
131
+ let acceptingActivities = true;
132
+ let activityWrites = Promise.resolve();
133
+ let activityFailure;
134
+ const publishActivity = (activity) => {
135
+ if (!acceptingActivities) {
136
+ return Promise.reject(new Error('Cannot publish activity after local invocation settlement.'));
137
+ }
138
+ const write = activityWrites.then(() => stream.publishActivity(activity));
139
+ activityWrites = write.catch((error) => {
140
+ activityFailure ??= error;
141
+ });
142
+ return write;
143
+ };
144
+ const invocation = new CredentialBoundLocalInvocation({
145
+ ...input,
146
+ signal: controller.signal,
147
+ publishActivity,
148
+ });
149
+ let terminal;
150
+ try {
151
+ terminal = parseLocalExecutionHostTerminal(await this.#execute(invocation));
152
+ }
153
+ catch {
154
+ controller.signal.throwIfAborted();
155
+ terminal = {
156
+ kind: 'error',
157
+ error: {
158
+ code: 'fixture_execution_failed',
159
+ message: 'Local fixture execution failed.',
160
+ },
161
+ };
162
+ }
163
+ finally {
164
+ acceptingActivities = false;
165
+ }
166
+ await activityWrites;
167
+ if (activityFailure) {
168
+ throw activityFailure;
169
+ }
170
+ await stream.settle(terminal);
171
+ }
172
+ async #close() {
173
+ this.#closing = true;
174
+ for (const invocation of this.#activeInvocations) {
175
+ invocation.controller.abort();
176
+ }
177
+ const serverClosed = new Promise((resolve, reject) => {
178
+ this.#server.close((error) => {
179
+ if (error) {
180
+ reject(error);
181
+ return;
182
+ }
183
+ resolve();
184
+ });
185
+ });
186
+ this.#server.closeAllConnections();
187
+ await serverClosed;
188
+ while (this.#activeInvocations.size > 0) {
189
+ const active = [...this.#activeInvocations];
190
+ for (const invocation of active) {
191
+ invocation.controller.abort();
192
+ }
193
+ await Promise.allSettled(active.map((invocation) => invocation.completed));
194
+ }
195
+ }
196
+ }
197
+ //# sourceMappingURL=local-execution-host-contract-fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-execution-host-contract-fixture.js","sourceRoot":"","sources":["../../src/testing/local-execution-host-contract-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EACL,YAAY,GAIb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EACL,6BAA6B,EAC7B,+BAA+B,GAChC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,8BAA8B,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EACL,8BAA8B,EAC9B,gCAAgC,GAEjC,MAAM,cAAc,CAAC;AAMtB;;;;;;GAMG;AACH,MAAM,OAAO,iCAAiC;IACnC,QAAQ,CAAsD;IAC9D,IAAI,CAAa;IACjB,YAAY,CAAe;IAC3B,WAAW,CAAS;IACpB,OAAO,CAAS;IAChB,kBAAkB,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC1D,QAAQ,CAAkB;IAC1B,aAAa,CAA4B;IACzC,QAAQ,GAAG,KAAK,CAAC;IAEjB,YAAoB,OAAiD;QACnE,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YAC1C,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;QAC5E,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,WAAW,IAAI,UAAU,CAAC;QACtD,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACzD,IAAI,CAAC,OAAO,GAAG,YAAY,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;YAChD,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,KAAK,CAChB,OAAiD;QAEjD,MAAM,OAAO,GAAG,IAAI,iCAAiC,CAAC,OAAO,CAAC,CAAC;QAC/D,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACxB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,2EAA2E;IAC3E,mBAAmB;QACjB,OAAO,IAAI,uBAAuB,CAAC;YACjC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;YACvB,UAAU,EAAE,IAAI,CAAC,WAAW;SAC7B,CAAC,CAAC;IACL,CAAC;IAED,+EAA+E;IAC/E,OAAO;QACL,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAED,6EAA6E;IAC7E,KAAK;QACH,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QACrC,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;QACpC,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACvC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,oBAAoB,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;IAC/D,CAAC;IAED,SAAS,CAAC,OAAwB,EAAE,QAAwB;QAC1D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,UAAU,CAAC,KAAK,EAAE,CAAC;YACnB,OAAO,CAAC,OAAO,EAAE,CAAC;YAClB,QAAQ,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAqB;YAC/B,UAAU;YACV,SAAS,EAAE,OAAO,CAAC,OAAO,EAAE;SAC7B,CAAC;QACF,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC;aAC1D,KAAK,CAAC,GAAG,EAAE;YACV,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrB,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QACL,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;QAC7B,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,OAAO,CACX,OAAwB,EACxB,QAAwB,EACxB,UAA2B;QAE3B,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACvC,MAAM,uBAAuB,GAAG,GAAG,EAAE;YACnC,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;gBAC5B,KAAK,EAAE,CAAC;YACV,CAAC;QACH,CAAC,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QAC/B,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;QAEhD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,8BAA8B,CACjD,OAAO,EACP,IAAI,CAAC,WAAW,EAChB,UAAU,CAAC,MAAM,CAClB,CAAC;YACF,UAAU,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;YACnC,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC9B,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;gBAC1B,gCAAgC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;gBAClD,OAAO;YACT,CAAC;YACD,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrB,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YAC9B,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,iBAAiB,CACrB,KAAsC,EACtC,QAAwB,EACxB,UAA2B;QAE3B,MAAM,MAAM,GAAG,MAAM,6BAA6B,CAAC,IAAI,CAAC;YACtD,QAAQ;YACR,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,YAAY;YACxC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE;YAC1B,GAAG,EAAE,IAAI,CAAC,IAAI;SACf,CAAC,CAAC;QACH,IAAI,mBAAmB,GAAG,IAAI,CAAC;QAC/B,IAAI,cAAc,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;QACvC,IAAI,eAAwB,CAAC;QAC7B,MAAM,eAAe,GAAG,CAAC,QAAiB,EAAiB,EAAE;YAC3D,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBACzB,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAC7B,4DAA4D,CAC7D,CAAC,CAAC;YACL,CAAC;YACD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAC/B,GAAG,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CACvC,CAAC;YACF,cAAc,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBAC9C,eAAe,KAAK,KAAK,CAAC;YAC5B,CAAC,CAAC,CAAC;YACH,OAAO,KAAK,CAAC;QACf,CAAC,CAAC;QACF,MAAM,UAAU,GAAG,IAAI,8BAA8B,CAAC;YACpD,GAAG,KAAK;YACR,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,eAAe;SAChB,CAAC,CAAC;QAEH,IAAI,QAAoC,CAAC;QACzC,IAAI,CAAC;YACH,QAAQ,GAAG,+BAA+B,CACxC,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAChC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;YACnC,QAAQ,GAAG;gBACT,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE;oBACL,IAAI,EAAE,0BAA0B;oBAChC,OAAO,EAAE,iCAAiC;iBAC3C;aACF,CAAC;QACJ,CAAC;gBAAS,CAAC;YACT,mBAAmB,GAAG,KAAK,CAAC;QAC9B,CAAC;QAED,MAAM,cAAc,CAAC;QACrB,IAAI,eAAe,EAAE,CAAC;YACpB,MAAM,eAAe,CAAC;QACxB,CAAC;QACD,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACjD,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAChC,CAAC;QACD,MAAM,YAAY,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACzD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;gBAC3B,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,CAAC,KAAK,CAAC,CAAC;oBACd,OAAO;gBACT,CAAC;gBACD,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,CAAC;QACnC,MAAM,YAAY,CAAC;QACnB,OAAO,IAAI,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC;YAC5C,KAAK,MAAM,UAAU,IAAI,MAAM,EAAE,CAAC;gBAChC,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;YAChC,CAAC;YACD,MAAM,OAAO,CAAC,UAAU,CACtB,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CACjD,CAAC;QACJ,CAAC;IACH,CAAC;CACF","sourcesContent":["import {\n randomBytes,\n randomUUID,\n} from 'node:crypto';\nimport { once } from 'node:events';\nimport {\n createServer,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from 'node:http';\nimport { DirectHttpExecutionHost } from '../http-sse/index.js';\nimport {\n LocalExecutionHostEventStream,\n parseLocalExecutionHostTerminal,\n} from './event-stream.js';\nimport { CredentialBoundLocalInvocation } from './invocation.js';\nimport {\n parseLocalExecutionHostRequest,\n writeLocalExecutionHostRejection,\n type ParsedLocalExecutionHostRequest,\n} from './request.js';\nimport type {\n LocalExecutionHostContractFixtureOptions,\n LocalExecutionHostTerminal,\n} from './types.js';\n\n/**\n * Real loopback implementation of the Execution Host v1 HTTP/SSE boundary.\n *\n * It is intentionally a contract fixture, not a fake Heddle runtime. The\n * supplied executor can call product APIs or MCP tools, while this class owns\n * request validation, credential containment, SSE ordering, and lifecycle.\n */\nexport class LocalExecutionHostContractFixture {\n readonly #execute: LocalExecutionHostContractFixtureOptions['execute'];\n readonly #now: () => Date;\n readonly #createRunId: () => string;\n readonly #localToken: string;\n readonly #server: Server;\n readonly #activeInvocations = new Set<ActiveInvocation>();\n #baseUrl: URL | undefined;\n #closePromise: Promise<void> | undefined;\n #closing = false;\n\n private constructor(options: LocalExecutionHostContractFixtureOptions) {\n if (typeof options.execute !== 'function') {\n throw new TypeError('Local Execution Host fixture requires an executor.');\n }\n this.#execute = options.execute;\n this.#now = options.now ?? (() => new Date());\n this.#createRunId = options.createRunId ?? randomUUID;\n this.#localToken = randomBytes(32).toString('base64url');\n this.#server = createServer((request, response) => {\n this.#dispatch(request, response);\n });\n }\n\n static async start(\n options: LocalExecutionHostContractFixtureOptions,\n ): Promise<LocalExecutionHostContractFixture> {\n const fixture = new LocalExecutionHostContractFixture(options);\n await fixture.#listen();\n return fixture;\n }\n\n /** Return a fresh client paired with this fixture's hidden local token. */\n createExecutionHost(): DirectHttpExecutionHost {\n return new DirectHttpExecutionHost({\n baseUrl: this.baseUrl(),\n localToken: this.#localToken,\n });\n }\n\n /** A copy of the loopback URL, useful for explicit negative protocol tests. */\n baseUrl(): URL {\n if (!this.#baseUrl) {\n throw new Error('Local Execution Host fixture has not started.');\n }\n return new URL(this.#baseUrl);\n }\n\n /** Abort active invocations, close sockets, and wait for handler cleanup. */\n close(): Promise<void> {\n this.#closePromise ??= this.#close();\n return this.#closePromise;\n }\n\n async #listen(): Promise<void> {\n this.#server.listen(0, '127.0.0.1');\n await once(this.#server, 'listening');\n const address = this.#server.address();\n if (!address || typeof address === 'string') {\n await this.close();\n throw new Error('Local Execution Host fixture did not bind a TCP port.');\n }\n this.#baseUrl = new URL(`http://127.0.0.1:${address.port}/`);\n }\n\n #dispatch(request: IncomingMessage, response: ServerResponse): void {\n const controller = new AbortController();\n if (this.#closing) {\n controller.abort();\n request.destroy();\n response.destroy();\n return;\n }\n const active: ActiveInvocation = {\n controller,\n completed: Promise.resolve(),\n };\n const completed = this.#handle(request, response, controller)\n .catch(() => {\n response.destroy();\n })\n .finally(() => {\n this.#activeInvocations.delete(active);\n });\n active.completed = completed;\n this.#activeInvocations.add(active);\n }\n\n async #handle(\n request: IncomingMessage,\n response: ServerResponse,\n controller: AbortController,\n ): Promise<void> {\n const abort = () => controller.abort();\n const abortIncompleteResponse = () => {\n if (!response.writableEnded) {\n abort();\n }\n };\n request.once('aborted', abort);\n response.once('close', abortIncompleteResponse);\n\n try {\n const parsed = await parseLocalExecutionHostRequest(\n request,\n this.#localToken,\n controller.signal,\n );\n controller.signal.throwIfAborted();\n await this.#streamInvocation(parsed, response, controller);\n } catch (error) {\n if (controller.signal.aborted) {\n response.destroy();\n return;\n }\n if (!response.headersSent) {\n writeLocalExecutionHostRejection(response, error);\n return;\n }\n response.destroy();\n } finally {\n request.off('aborted', abort);\n response.off('close', abortIncompleteResponse);\n }\n }\n\n async #streamInvocation(\n input: ParsedLocalExecutionHostRequest,\n response: ServerResponse,\n controller: AbortController,\n ): Promise<void> {\n const stream = await LocalExecutionHostEventStream.open({\n response,\n signal: controller.signal,\n invocationId: input.request.invocationId,\n runId: this.#createRunId(),\n now: this.#now,\n });\n let acceptingActivities = true;\n let activityWrites = Promise.resolve();\n let activityFailure: unknown;\n const publishActivity = (activity: unknown): Promise<void> => {\n if (!acceptingActivities) {\n return Promise.reject(new Error(\n 'Cannot publish activity after local invocation settlement.',\n ));\n }\n const write = activityWrites.then(\n () => stream.publishActivity(activity),\n );\n activityWrites = write.catch((error: unknown) => {\n activityFailure ??= error;\n });\n return write;\n };\n const invocation = new CredentialBoundLocalInvocation({\n ...input,\n signal: controller.signal,\n publishActivity,\n });\n\n let terminal: LocalExecutionHostTerminal;\n try {\n terminal = parseLocalExecutionHostTerminal(\n await this.#execute(invocation),\n );\n } catch {\n controller.signal.throwIfAborted();\n terminal = {\n kind: 'error',\n error: {\n code: 'fixture_execution_failed',\n message: 'Local fixture execution failed.',\n },\n };\n } finally {\n acceptingActivities = false;\n }\n\n await activityWrites;\n if (activityFailure) {\n throw activityFailure;\n }\n await stream.settle(terminal);\n }\n\n async #close(): Promise<void> {\n this.#closing = true;\n for (const invocation of this.#activeInvocations) {\n invocation.controller.abort();\n }\n const serverClosed = new Promise<void>((resolve, reject) => {\n this.#server.close((error) => {\n if (error) {\n reject(error);\n return;\n }\n resolve();\n });\n });\n this.#server.closeAllConnections();\n await serverClosed;\n while (this.#activeInvocations.size > 0) {\n const active = [...this.#activeInvocations];\n for (const invocation of active) {\n invocation.controller.abort();\n }\n await Promise.allSettled(\n active.map((invocation) => invocation.completed),\n );\n }\n }\n}\n\ntype ActiveInvocation = {\n controller: AbortController;\n completed: Promise<void>;\n};\n"]}
@@ -0,0 +1,10 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type ExecutionHostConversationTurnRequest } from '../contracts/index.js';
3
+ export type ParsedLocalExecutionHostRequest = {
4
+ request: ExecutionHostConversationTurnRequest;
5
+ runtimeSessionId: string;
6
+ mcpCapability?: string;
7
+ };
8
+ export declare function parseLocalExecutionHostRequest(request: IncomingMessage, expectedLocalToken: string, signal: AbortSignal): Promise<ParsedLocalExecutionHostRequest>;
9
+ export declare function writeLocalExecutionHostRejection(response: ServerResponse, error: unknown): void;
10
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/testing/request.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EAQL,KAAK,oCAAoC,EAC1C,MAAM,uBAAuB,CAAC;AAQ/B,MAAM,MAAM,+BAA+B,GAAG;IAC5C,OAAO,EAAE,oCAAoC,CAAC;IAC9C,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,eAAe,EACxB,kBAAkB,EAAE,MAAM,EAC1B,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,+BAA+B,CAAC,CAyC1C;AAED,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,OAAO,GACb,IAAI,CAkBN"}