@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.
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/authority/index.d.ts +3 -0
- package/dist/authority/index.d.ts.map +1 -0
- package/dist/authority/index.js +2 -0
- package/dist/authority/index.js.map +1 -0
- package/dist/authority/jose-execution-authority.d.ts +14 -0
- package/dist/authority/jose-execution-authority.d.ts.map +1 -0
- package/dist/authority/jose-execution-authority.js +266 -0
- package/dist/authority/jose-execution-authority.js.map +1 -0
- package/dist/authority/types.d.ts +63 -0
- package/dist/authority/types.d.ts.map +1 -0
- package/dist/authority/types.js +2 -0
- package/dist/authority/types.js.map +1 -0
- package/dist/contracts/index.d.ts +163 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +149 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/conversation/durable-hosted-conversation-turn-service.d.ts +23 -0
- package/dist/conversation/durable-hosted-conversation-turn-service.d.ts.map +1 -0
- package/dist/conversation/durable-hosted-conversation-turn-service.js +273 -0
- package/dist/conversation/durable-hosted-conversation-turn-service.js.map +1 -0
- package/dist/conversation/hosted-conversation-turn-service.d.ts +17 -0
- package/dist/conversation/hosted-conversation-turn-service.d.ts.map +1 -0
- package/dist/conversation/hosted-conversation-turn-service.js +60 -0
- package/dist/conversation/hosted-conversation-turn-service.js.map +1 -0
- package/dist/conversation/index.d.ts +7 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/index.js +5 -0
- package/dist/conversation/index.js.map +1 -0
- package/dist/conversation/lifecycle-types.d.ts +208 -0
- package/dist/conversation/lifecycle-types.d.ts.map +1 -0
- package/dist/conversation/lifecycle-types.js +92 -0
- package/dist/conversation/lifecycle-types.js.map +1 -0
- package/dist/conversation/types.d.ts +35 -0
- package/dist/conversation/types.d.ts.map +1 -0
- package/dist/conversation/types.js +11 -0
- package/dist/conversation/types.js.map +1 -0
- package/dist/http-sse/direct-http-execution-host.d.ts +12 -0
- package/dist/http-sse/direct-http-execution-host.d.ts.map +1 -0
- package/dist/http-sse/direct-http-execution-host.js +279 -0
- package/dist/http-sse/direct-http-execution-host.js.map +1 -0
- package/dist/http-sse/errors.d.ts +19 -0
- package/dist/http-sse/errors.d.ts.map +1 -0
- package/dist/http-sse/errors.js +29 -0
- package/dist/http-sse/errors.js.map +1 -0
- package/dist/http-sse/index.d.ts +4 -0
- package/dist/http-sse/index.d.ts.map +1 -0
- package/dist/http-sse/index.js +3 -0
- package/dist/http-sse/index.js.map +1 -0
- package/dist/http-sse/types.d.ts +20 -0
- package/dist/http-sse/types.d.ts.map +1 -0
- package/dist/http-sse/types.js +2 -0
- package/dist/http-sse/types.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/errors.d.ts +11 -0
- package/dist/mcp/errors.d.ts.map +1 -0
- package/dist/mcp/errors.js +15 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/jwt-capability-verifier.d.ts +12 -0
- package/dist/mcp/jwt-capability-verifier.d.ts.map +1 -0
- package/dist/mcp/jwt-capability-verifier.js +136 -0
- package/dist/mcp/jwt-capability-verifier.js.map +1 -0
- package/dist/mcp/node/index.d.ts +4 -0
- package/dist/mcp/node/index.d.ts.map +1 -0
- package/dist/mcp/node/index.js +3 -0
- package/dist/mcp/node/index.js.map +1 -0
- package/dist/mcp/node/json-toolset.d.ts +23 -0
- package/dist/mcp/node/json-toolset.d.ts.map +1 -0
- package/dist/mcp/node/json-toolset.js +94 -0
- package/dist/mcp/node/json-toolset.js.map +1 -0
- package/dist/mcp/node/streamable-http-mcp-service.d.ts +18 -0
- package/dist/mcp/node/streamable-http-mcp-service.d.ts.map +1 -0
- package/dist/mcp/node/streamable-http-mcp-service.js +229 -0
- package/dist/mcp/node/streamable-http-mcp-service.js.map +1 -0
- package/dist/mcp/node/types.d.ts +53 -0
- package/dist/mcp/node/types.d.ts.map +1 -0
- package/dist/mcp/node/types.js +2 -0
- package/dist/mcp/node/types.js.map +1 -0
- package/dist/mcp/types.d.ts +37 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/node/authority-key.d.ts +14 -0
- package/dist/node/authority-key.d.ts.map +1 -0
- package/dist/node/authority-key.js +89 -0
- package/dist/node/authority-key.js.map +1 -0
- package/dist/node/direct-credentials.d.ts +21 -0
- package/dist/node/direct-credentials.d.ts.map +1 -0
- package/dist/node/direct-credentials.js +47 -0
- package/dist/node/direct-credentials.js.map +1 -0
- package/dist/node/http-service.d.ts +21 -0
- package/dist/node/http-service.d.ts.map +1 -0
- package/dist/node/http-service.js +389 -0
- package/dist/node/http-service.js.map +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +4 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/types.d.ts +52 -0
- package/dist/node/types.d.ts.map +1 -0
- package/dist/node/types.js +2 -0
- package/dist/node/types.js.map +1 -0
- package/dist/testing/event-stream.d.ts +17 -0
- package/dist/testing/event-stream.d.ts.map +1 -0
- package/dist/testing/event-stream.js +83 -0
- package/dist/testing/event-stream.js.map +1 -0
- package/dist/testing/hosted-conversation-turn-store-conformance.d.ts +17 -0
- package/dist/testing/hosted-conversation-turn-store-conformance.d.ts.map +1 -0
- package/dist/testing/hosted-conversation-turn-store-conformance.js +137 -0
- package/dist/testing/hosted-conversation-turn-store-conformance.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/invocation.d.ts +20 -0
- package/dist/testing/invocation.d.ts.map +1 -0
- package/dist/testing/invocation.js +39 -0
- package/dist/testing/invocation.js.map +1 -0
- package/dist/testing/local-execution-host-contract-fixture.d.ts +21 -0
- package/dist/testing/local-execution-host-contract-fixture.d.ts.map +1 -0
- package/dist/testing/local-execution-host-contract-fixture.js +197 -0
- package/dist/testing/local-execution-host-contract-fixture.js.map +1 -0
- package/dist/testing/request.d.ts +10 -0
- package/dist/testing/request.d.ts.map +1 -0
- package/dist/testing/request.js +140 -0
- package/dist/testing/request.js.map +1 -0
- package/dist/testing/types.d.ts +42 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/package.json +92 -0
- package/spec/v1/README.md +101 -0
- package/spec/v1/durable-hosted-conversation-lifecycle.md +139 -0
- package/spec/v1/fixtures/ambiguous-eof.sse +9 -0
- package/spec/v1/fixtures/authority.json +70 -0
- package/spec/v1/fixtures/cancelled.sse +9 -0
- package/spec/v1/fixtures/durable-conversation-lifecycle.json +890 -0
- package/spec/v1/fixtures/invalid-request-extra-field.json +8 -0
- package/spec/v1/fixtures/invalid-sequence-gap.sse +9 -0
- package/spec/v1/fixtures/manifest.json +53 -0
- package/spec/v1/fixtures/valid-request.json +7 -0
- package/spec/v1/fixtures/valid-result.sse +13 -0
- package/spec/v1/openapi.json +896 -0
- 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":"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","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 @@
|
|
|
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
|