@heddleagent/execution-host-client 7.0.0 → 8.0.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 (79) hide show
  1. package/README.md +48 -34
  2. package/dist/cli/hostctl.d.ts +11 -0
  3. package/dist/cli/hostctl.d.ts.map +1 -0
  4. package/dist/cli/hostctl.js +49 -0
  5. package/dist/cli/hostctl.js.map +1 -0
  6. package/dist/contracts/index.d.ts +2 -0
  7. package/dist/contracts/index.d.ts.map +1 -1
  8. package/dist/contracts/index.js +9 -0
  9. package/dist/contracts/index.js.map +1 -1
  10. package/dist/coordinator/contracts.d.ts +148 -5
  11. package/dist/coordinator/contracts.d.ts.map +1 -1
  12. package/dist/coordinator/contracts.js +74 -3
  13. package/dist/coordinator/contracts.js.map +1 -1
  14. package/dist/coordinator/hosted-heartbeat-execution-client.d.ts +30 -0
  15. package/dist/coordinator/hosted-heartbeat-execution-client.d.ts.map +1 -0
  16. package/dist/coordinator/hosted-heartbeat-execution-client.js +240 -0
  17. package/dist/coordinator/hosted-heartbeat-execution-client.js.map +1 -0
  18. package/dist/coordinator/hosted-heartbeat-execution-service.d.ts +14 -0
  19. package/dist/coordinator/hosted-heartbeat-execution-service.d.ts.map +1 -0
  20. package/dist/coordinator/hosted-heartbeat-execution-service.js +100 -0
  21. package/dist/coordinator/hosted-heartbeat-execution-service.js.map +1 -0
  22. package/dist/coordinator/index.d.ts +5 -5
  23. package/dist/coordinator/index.d.ts.map +1 -1
  24. package/dist/coordinator/index.js +3 -3
  25. package/dist/coordinator/index.js.map +1 -1
  26. package/dist/coordinator/node/execution-http-service.d.ts +11 -0
  27. package/dist/coordinator/node/execution-http-service.d.ts.map +1 -0
  28. package/dist/coordinator/node/{delegation-http-service.js → execution-http-service.js} +52 -39
  29. package/dist/coordinator/node/execution-http-service.js.map +1 -0
  30. package/dist/coordinator/node/index.d.ts +2 -2
  31. package/dist/coordinator/node/index.d.ts.map +1 -1
  32. package/dist/coordinator/node/index.js +1 -1
  33. package/dist/coordinator/node/index.js.map +1 -1
  34. package/dist/coordinator/node/types.d.ts +10 -9
  35. package/dist/coordinator/node/types.d.ts.map +1 -1
  36. package/dist/coordinator/node/types.js.map +1 -1
  37. package/dist/coordinator/types.d.ts +51 -13
  38. package/dist/coordinator/types.d.ts.map +1 -1
  39. package/dist/coordinator/types.js.map +1 -1
  40. package/dist/host/schemas.d.ts.map +1 -1
  41. package/dist/host/schemas.js +2 -7
  42. package/dist/host/schemas.js.map +1 -1
  43. package/dist/http-sse/direct-http-execution-host.d.ts.map +1 -1
  44. package/dist/http-sse/direct-http-execution-host.js +6 -5
  45. package/dist/http-sse/direct-http-execution-host.js.map +1 -1
  46. package/dist/http-sse/types.d.ts +2 -0
  47. package/dist/http-sse/types.d.ts.map +1 -1
  48. package/dist/http-sse/types.js.map +1 -1
  49. package/dist/node/authority-key.d.ts.map +1 -1
  50. package/dist/node/authority-key.js +24 -49
  51. package/dist/node/authority-key.js.map +1 -1
  52. package/dist/node/environment-value.d.ts +17 -0
  53. package/dist/node/environment-value.d.ts.map +1 -0
  54. package/dist/node/environment-value.js +78 -0
  55. package/dist/node/environment-value.js.map +1 -0
  56. package/dist/node/index.d.ts +4 -0
  57. package/dist/node/index.d.ts.map +1 -1
  58. package/dist/node/index.js +2 -0
  59. package/dist/node/index.js.map +1 -1
  60. package/dist/node/local-credential-bundle.d.ts +33 -0
  61. package/dist/node/local-credential-bundle.d.ts.map +1 -0
  62. package/dist/node/local-credential-bundle.js +196 -0
  63. package/dist/node/local-credential-bundle.js.map +1 -0
  64. package/dist/node/owner-only-file.d.ts +17 -0
  65. package/dist/node/owner-only-file.d.ts.map +1 -0
  66. package/dist/node/owner-only-file.js +59 -0
  67. package/dist/node/owner-only-file.js.map +1 -0
  68. package/package.json +5 -1
  69. package/dist/coordinator/hosted-heartbeat-delegation-client.d.ts +0 -21
  70. package/dist/coordinator/hosted-heartbeat-delegation-client.d.ts.map +0 -1
  71. package/dist/coordinator/hosted-heartbeat-delegation-client.js +0 -117
  72. package/dist/coordinator/hosted-heartbeat-delegation-client.js.map +0 -1
  73. package/dist/coordinator/hosted-heartbeat-delegation-service.d.ts +0 -15
  74. package/dist/coordinator/hosted-heartbeat-delegation-service.d.ts.map +0 -1
  75. package/dist/coordinator/hosted-heartbeat-delegation-service.js +0 -81
  76. package/dist/coordinator/hosted-heartbeat-delegation-service.js.map +0 -1
  77. package/dist/coordinator/node/delegation-http-service.d.ts +0 -11
  78. package/dist/coordinator/node/delegation-http-service.d.ts.map +0 -1
  79. package/dist/coordinator/node/delegation-http-service.js.map +0 -1
@@ -0,0 +1,196 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { chmod, lstat, mkdir, mkdtemp, rename, rm, } from 'node:fs/promises';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { generateExecutionAuthorityKeyFile, loadExecutionAuthorityKeyPairFromFile, } from './authority-key.js';
6
+ import { createOwnerOnlyFile, OwnerOnlyFileError, readOwnerOnlyFile, removeOneTrailingLineEnding, } from './owner-only-file.js';
7
+ const BUNDLE_KIND = 'heddle-execution-host-local-credentials';
8
+ const BUNDLE_SCHEMA_VERSION = 2;
9
+ const MAX_BUNDLE_FILE_BYTES = 16 * 1_024;
10
+ const OWNER_DIRECTORY_MODE = 0o700;
11
+ const TokenSchema = z.string().regex(/^[a-f0-9]{64}$/);
12
+ export const LOCAL_CREDENTIAL_BUNDLE_FILES = Object.freeze({
13
+ executionHostLocalToken: 'execution-host-local-token',
14
+ executionHostLocalTokenSha256: 'execution-host-local-token.sha256',
15
+ coordinatorApiToken: 'coordinator-api-token',
16
+ coordinatorProductExecutionToken: 'coordinator-product-execution-token',
17
+ executionAuthorityPrivateJwk: 'execution-authority-private.jwk',
18
+ manifest: 'manifest.json',
19
+ });
20
+ const SecretBundleFilesSchema = z.object({
21
+ executionHostLocalToken: z.literal(LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalToken),
22
+ executionHostLocalTokenSha256: z.literal(LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalTokenSha256),
23
+ coordinatorApiToken: z.literal(LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorApiToken),
24
+ coordinatorProductExecutionToken: z.literal(LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorProductExecutionToken),
25
+ executionAuthorityPrivateJwk: z.literal(LOCAL_CREDENTIAL_BUNDLE_FILES.executionAuthorityPrivateJwk),
26
+ }).strict();
27
+ const LocalCredentialBundleManifestSchema = z.object({
28
+ kind: z.literal(BUNDLE_KIND),
29
+ schemaVersion: z.literal(BUNDLE_SCHEMA_VERSION),
30
+ files: SecretBundleFilesSchema,
31
+ }).strict();
32
+ /**
33
+ * Creates one versioned, owner-only credential bundle for local hosted-service
34
+ * composition. A valid existing bundle is returned unchanged; partial or
35
+ * unmanaged directories fail closed.
36
+ */
37
+ export async function initializeLocalCredentialBundle(rawOutputDirectory) {
38
+ const outputDirectory = resolveOutputDirectory(rawOutputDirectory);
39
+ const existing = await pathExists(outputDirectory);
40
+ if (existing) {
41
+ return {
42
+ created: false,
43
+ paths: await validateLocalCredentialBundle(outputDirectory),
44
+ };
45
+ }
46
+ const parentDirectory = dirname(outputDirectory);
47
+ await mkdir(parentDirectory, { recursive: true, mode: OWNER_DIRECTORY_MODE });
48
+ const temporaryDirectory = await mkdtemp(join(parentDirectory, `.${basename(outputDirectory)}.tmp-`));
49
+ await chmod(temporaryDirectory, OWNER_DIRECTORY_MODE);
50
+ try {
51
+ await populateCredentialBundle(temporaryDirectory);
52
+ let created = true;
53
+ try {
54
+ await rename(temporaryDirectory, outputDirectory);
55
+ }
56
+ catch (error) {
57
+ if (!(await pathExists(outputDirectory))) {
58
+ throw error;
59
+ }
60
+ await rm(temporaryDirectory, { recursive: true, force: true });
61
+ created = false;
62
+ }
63
+ return {
64
+ created,
65
+ paths: await validateLocalCredentialBundle(outputDirectory),
66
+ };
67
+ }
68
+ catch (error) {
69
+ await rm(temporaryDirectory, { recursive: true, force: true });
70
+ if (error instanceof LocalCredentialBundleError) {
71
+ throw error;
72
+ }
73
+ throw new LocalCredentialBundleError('Local Execution Host credentials could not be initialized.');
74
+ }
75
+ }
76
+ /** Validates one generated bundle without returning any credential value. */
77
+ export async function validateLocalCredentialBundle(rawDirectory) {
78
+ const directory = resolveOutputDirectory(rawDirectory);
79
+ try {
80
+ await assertOwnerOnlyDirectory(directory);
81
+ const paths = resolveBundlePaths(directory);
82
+ LocalCredentialBundleManifestSchema.parse(JSON.parse(await readBundleEntry(paths.manifest)));
83
+ const [localToken, localTokenDigest, coordinatorToken, productExecutionToken,] = await Promise.all([
84
+ readBundleEntry(paths.executionHostLocalToken),
85
+ readBundleEntry(paths.executionHostLocalTokenSha256),
86
+ readBundleEntry(paths.coordinatorApiToken),
87
+ readBundleEntry(paths.coordinatorProductExecutionToken),
88
+ ]);
89
+ TokenSchema.parse(localToken);
90
+ TokenSchema.parse(localTokenDigest);
91
+ TokenSchema.parse(coordinatorToken);
92
+ TokenSchema.parse(productExecutionToken);
93
+ if (sha256(localToken) !== localTokenDigest) {
94
+ throw new LocalCredentialBundleError('Local credential bundle token verifier does not match its token.');
95
+ }
96
+ await loadExecutionAuthorityKeyPairFromFile(paths.executionAuthorityPrivateJwk);
97
+ return Object.freeze(paths);
98
+ }
99
+ catch (error) {
100
+ if (error instanceof LocalCredentialBundleError) {
101
+ throw error;
102
+ }
103
+ if (error instanceof OwnerOnlyFileError) {
104
+ throw new LocalCredentialBundleError(error.message);
105
+ }
106
+ throw new LocalCredentialBundleError('Existing local credential directory is incomplete, invalid, or unmanaged.');
107
+ }
108
+ }
109
+ async function populateCredentialBundle(directory) {
110
+ const paths = resolveBundlePaths(directory);
111
+ const localToken = generateToken();
112
+ await Promise.all([
113
+ writeBundleEntry(paths.executionHostLocalToken, localToken),
114
+ writeBundleEntry(paths.executionHostLocalTokenSha256, sha256(localToken)),
115
+ writeBundleEntry(paths.coordinatorApiToken, generateToken()),
116
+ writeBundleEntry(paths.coordinatorProductExecutionToken, generateToken()),
117
+ generateExecutionAuthorityKeyFile(paths.executionAuthorityPrivateJwk),
118
+ ]);
119
+ await writeBundleEntry(paths.manifest, JSON.stringify({
120
+ kind: BUNDLE_KIND,
121
+ schemaVersion: BUNDLE_SCHEMA_VERSION,
122
+ files: secretFileManifest(),
123
+ }, null, 2));
124
+ }
125
+ function secretFileManifest() {
126
+ return {
127
+ executionHostLocalToken: LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalToken,
128
+ executionHostLocalTokenSha256: LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalTokenSha256,
129
+ coordinatorApiToken: LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorApiToken,
130
+ coordinatorProductExecutionToken: LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorProductExecutionToken,
131
+ executionAuthorityPrivateJwk: LOCAL_CREDENTIAL_BUNDLE_FILES.executionAuthorityPrivateJwk,
132
+ };
133
+ }
134
+ function resolveBundlePaths(directory) {
135
+ return {
136
+ directory,
137
+ manifest: join(directory, LOCAL_CREDENTIAL_BUNDLE_FILES.manifest),
138
+ executionHostLocalToken: join(directory, LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalToken),
139
+ executionHostLocalTokenSha256: join(directory, LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalTokenSha256),
140
+ coordinatorApiToken: join(directory, LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorApiToken),
141
+ coordinatorProductExecutionToken: join(directory, LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorProductExecutionToken),
142
+ executionAuthorityPrivateJwk: join(directory, LOCAL_CREDENTIAL_BUNDLE_FILES.executionAuthorityPrivateJwk),
143
+ };
144
+ }
145
+ function resolveOutputDirectory(rawDirectory) {
146
+ const directory = rawDirectory.trim();
147
+ if (!directory) {
148
+ throw new LocalCredentialBundleError('A local credential output directory is required.');
149
+ }
150
+ return resolve(directory);
151
+ }
152
+ async function assertOwnerOnlyDirectory(directory) {
153
+ const metadata = await lstat(directory);
154
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
155
+ throw new LocalCredentialBundleError('Local credential bundle path must be a real directory.');
156
+ }
157
+ if (process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) {
158
+ throw new LocalCredentialBundleError('Local credential bundle directory must not be group- or world-accessible.');
159
+ }
160
+ }
161
+ async function readBundleEntry(filePath) {
162
+ return removeOneTrailingLineEnding(await readOwnerOnlyFile(filePath, {
163
+ description: 'Local credential bundle entry',
164
+ maxBytes: MAX_BUNDLE_FILE_BYTES,
165
+ }));
166
+ }
167
+ function writeBundleEntry(filePath, value) {
168
+ return createOwnerOnlyFile(filePath, `${value}\n`);
169
+ }
170
+ function generateToken() {
171
+ return randomBytes(32).toString('hex');
172
+ }
173
+ function sha256(value) {
174
+ return createHash('sha256').update(value, 'utf8').digest('hex');
175
+ }
176
+ async function pathExists(path) {
177
+ try {
178
+ await lstat(path);
179
+ return true;
180
+ }
181
+ catch (error) {
182
+ if (isErrnoCode(error, 'ENOENT')) {
183
+ return false;
184
+ }
185
+ throw new LocalCredentialBundleError('Local credential bundle path could not be inspected.');
186
+ }
187
+ }
188
+ function isErrnoCode(error, code) {
189
+ return error instanceof Error
190
+ && 'code' in error
191
+ && error.code === code;
192
+ }
193
+ export class LocalCredentialBundleError extends Error {
194
+ name = 'LocalCredentialBundleError';
195
+ }
196
+ //# sourceMappingURL=local-credential-bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-credential-bundle.js","sourceRoot":"","sources":["../../src/node/local-credential-bundle.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport {\n chmod,\n lstat,\n mkdir,\n mkdtemp,\n rename,\n rm,\n} from 'node:fs/promises';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { z } from 'zod';\nimport {\n generateExecutionAuthorityKeyFile,\n loadExecutionAuthorityKeyPairFromFile,\n} from './authority-key.js';\nimport {\n createOwnerOnlyFile,\n OwnerOnlyFileError,\n readOwnerOnlyFile,\n removeOneTrailingLineEnding,\n} from './owner-only-file.js';\n\nconst BUNDLE_KIND = 'heddle-execution-host-local-credentials';\nconst BUNDLE_SCHEMA_VERSION = 2;\nconst MAX_BUNDLE_FILE_BYTES = 16 * 1_024;\nconst OWNER_DIRECTORY_MODE = 0o700;\nconst TokenSchema = z.string().regex(/^[a-f0-9]{64}$/);\n\nexport const LOCAL_CREDENTIAL_BUNDLE_FILES = Object.freeze({\n executionHostLocalToken: 'execution-host-local-token',\n executionHostLocalTokenSha256: 'execution-host-local-token.sha256',\n coordinatorApiToken: 'coordinator-api-token',\n coordinatorProductExecutionToken: 'coordinator-product-execution-token',\n executionAuthorityPrivateJwk: 'execution-authority-private.jwk',\n manifest: 'manifest.json',\n} as const);\n\nconst SecretBundleFilesSchema = z.object({\n executionHostLocalToken: z.literal(\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalToken,\n ),\n executionHostLocalTokenSha256: z.literal(\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalTokenSha256,\n ),\n coordinatorApiToken: z.literal(\n LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorApiToken,\n ),\n coordinatorProductExecutionToken: z.literal(\n LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorProductExecutionToken,\n ),\n executionAuthorityPrivateJwk: z.literal(\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionAuthorityPrivateJwk,\n ),\n}).strict();\n\nconst LocalCredentialBundleManifestSchema = z.object({\n kind: z.literal(BUNDLE_KIND),\n schemaVersion: z.literal(BUNDLE_SCHEMA_VERSION),\n files: SecretBundleFilesSchema,\n}).strict();\n\nexport type LocalCredentialBundlePaths = {\n directory: string;\n manifest: string;\n executionHostLocalToken: string;\n executionHostLocalTokenSha256: string;\n coordinatorApiToken: string;\n coordinatorProductExecutionToken: string;\n executionAuthorityPrivateJwk: string;\n};\n\nexport type LocalCredentialBundleInitialization = {\n created: boolean;\n paths: LocalCredentialBundlePaths;\n};\n\n/**\n * Creates one versioned, owner-only credential bundle for local hosted-service\n * composition. A valid existing bundle is returned unchanged; partial or\n * unmanaged directories fail closed.\n */\nexport async function initializeLocalCredentialBundle(\n rawOutputDirectory: string,\n): Promise<LocalCredentialBundleInitialization> {\n const outputDirectory = resolveOutputDirectory(rawOutputDirectory);\n const existing = await pathExists(outputDirectory);\n if (existing) {\n return {\n created: false,\n paths: await validateLocalCredentialBundle(outputDirectory),\n };\n }\n\n const parentDirectory = dirname(outputDirectory);\n await mkdir(parentDirectory, { recursive: true, mode: OWNER_DIRECTORY_MODE });\n const temporaryDirectory = await mkdtemp(join(\n parentDirectory,\n `.${basename(outputDirectory)}.tmp-`,\n ));\n await chmod(temporaryDirectory, OWNER_DIRECTORY_MODE);\n\n try {\n await populateCredentialBundle(temporaryDirectory);\n let created = true;\n try {\n await rename(temporaryDirectory, outputDirectory);\n } catch (error) {\n if (!(await pathExists(outputDirectory))) {\n throw error;\n }\n await rm(temporaryDirectory, { recursive: true, force: true });\n created = false;\n }\n return {\n created,\n paths: await validateLocalCredentialBundle(outputDirectory),\n };\n } catch (error) {\n await rm(temporaryDirectory, { recursive: true, force: true });\n if (error instanceof LocalCredentialBundleError) {\n throw error;\n }\n throw new LocalCredentialBundleError(\n 'Local Execution Host credentials could not be initialized.',\n );\n }\n}\n\n/** Validates one generated bundle without returning any credential value. */\nexport async function validateLocalCredentialBundle(\n rawDirectory: string,\n): Promise<LocalCredentialBundlePaths> {\n const directory = resolveOutputDirectory(rawDirectory);\n try {\n await assertOwnerOnlyDirectory(directory);\n const paths = resolveBundlePaths(directory);\n LocalCredentialBundleManifestSchema.parse(\n JSON.parse(await readBundleEntry(paths.manifest)) as unknown,\n );\n\n const [\n localToken,\n localTokenDigest,\n coordinatorToken,\n productExecutionToken,\n ] = await Promise.all([\n readBundleEntry(paths.executionHostLocalToken),\n readBundleEntry(paths.executionHostLocalTokenSha256),\n readBundleEntry(paths.coordinatorApiToken),\n readBundleEntry(paths.coordinatorProductExecutionToken),\n ]);\n TokenSchema.parse(localToken);\n TokenSchema.parse(localTokenDigest);\n TokenSchema.parse(coordinatorToken);\n TokenSchema.parse(productExecutionToken);\n if (sha256(localToken) !== localTokenDigest) {\n throw new LocalCredentialBundleError(\n 'Local credential bundle token verifier does not match its token.',\n );\n }\n await loadExecutionAuthorityKeyPairFromFile(\n paths.executionAuthorityPrivateJwk,\n );\n return Object.freeze(paths);\n } catch (error) {\n if (error instanceof LocalCredentialBundleError) {\n throw error;\n }\n if (error instanceof OwnerOnlyFileError) {\n throw new LocalCredentialBundleError(error.message);\n }\n throw new LocalCredentialBundleError(\n 'Existing local credential directory is incomplete, invalid, or unmanaged.',\n );\n }\n}\n\nasync function populateCredentialBundle(directory: string): Promise<void> {\n const paths = resolveBundlePaths(directory);\n const localToken = generateToken();\n await Promise.all([\n writeBundleEntry(paths.executionHostLocalToken, localToken),\n writeBundleEntry(\n paths.executionHostLocalTokenSha256,\n sha256(localToken),\n ),\n writeBundleEntry(paths.coordinatorApiToken, generateToken()),\n writeBundleEntry(\n paths.coordinatorProductExecutionToken,\n generateToken(),\n ),\n generateExecutionAuthorityKeyFile(paths.executionAuthorityPrivateJwk),\n ]);\n await writeBundleEntry(paths.manifest, JSON.stringify({\n kind: BUNDLE_KIND,\n schemaVersion: BUNDLE_SCHEMA_VERSION,\n files: secretFileManifest(),\n }, null, 2));\n}\n\nfunction secretFileManifest() {\n return {\n executionHostLocalToken:\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalToken,\n executionHostLocalTokenSha256:\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalTokenSha256,\n coordinatorApiToken: LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorApiToken,\n coordinatorProductExecutionToken:\n LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorProductExecutionToken,\n executionAuthorityPrivateJwk:\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionAuthorityPrivateJwk,\n } as const;\n}\n\nfunction resolveBundlePaths(directory: string): LocalCredentialBundlePaths {\n return {\n directory,\n manifest: join(directory, LOCAL_CREDENTIAL_BUNDLE_FILES.manifest),\n executionHostLocalToken: join(\n directory,\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalToken,\n ),\n executionHostLocalTokenSha256: join(\n directory,\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionHostLocalTokenSha256,\n ),\n coordinatorApiToken: join(\n directory,\n LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorApiToken,\n ),\n coordinatorProductExecutionToken: join(\n directory,\n LOCAL_CREDENTIAL_BUNDLE_FILES.coordinatorProductExecutionToken,\n ),\n executionAuthorityPrivateJwk: join(\n directory,\n LOCAL_CREDENTIAL_BUNDLE_FILES.executionAuthorityPrivateJwk,\n ),\n };\n}\n\nfunction resolveOutputDirectory(rawDirectory: string): string {\n const directory = rawDirectory.trim();\n if (!directory) {\n throw new LocalCredentialBundleError(\n 'A local credential output directory is required.',\n );\n }\n return resolve(directory);\n}\n\nasync function assertOwnerOnlyDirectory(directory: string): Promise<void> {\n const metadata = await lstat(directory);\n if (!metadata.isDirectory() || metadata.isSymbolicLink()) {\n throw new LocalCredentialBundleError(\n 'Local credential bundle path must be a real directory.',\n );\n }\n if (process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) {\n throw new LocalCredentialBundleError(\n 'Local credential bundle directory must not be group- or world-accessible.',\n );\n }\n}\n\nasync function readBundleEntry(filePath: string): Promise<string> {\n return removeOneTrailingLineEnding(await readOwnerOnlyFile(filePath, {\n description: 'Local credential bundle entry',\n maxBytes: MAX_BUNDLE_FILE_BYTES,\n }));\n}\n\nfunction writeBundleEntry(filePath: string, value: string): Promise<void> {\n return createOwnerOnlyFile(filePath, `${value}\\n`);\n}\n\nfunction generateToken(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction sha256(value: string): string {\n return createHash('sha256').update(value, 'utf8').digest('hex');\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await lstat(path);\n return true;\n } catch (error) {\n if (isErrnoCode(error, 'ENOENT')) {\n return false;\n }\n throw new LocalCredentialBundleError(\n 'Local credential bundle path could not be inspected.',\n );\n }\n}\n\nfunction isErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error\n && 'code' in error\n && error.code === code;\n}\n\nexport class LocalCredentialBundleError extends Error {\n readonly name = 'LocalCredentialBundleError';\n}\n"]}
@@ -0,0 +1,17 @@
1
+ type OwnerOnlyFileReadOptions = {
2
+ description: string;
3
+ maxBytes: number;
4
+ };
5
+ /**
6
+ * Reads a bounded regular file without following its final symlink and rejects
7
+ * group- or world-accessible POSIX files.
8
+ */
9
+ export declare function readOwnerOnlyFile(filePath: string, options: OwnerOnlyFileReadOptions): Promise<string>;
10
+ /** Creates, flushes, and never overwrites one owner-only file. */
11
+ export declare function createOwnerOnlyFile(filePath: string, value: string): Promise<void>;
12
+ export declare function removeOneTrailingLineEnding(value: string): string;
13
+ export declare class OwnerOnlyFileError extends Error {
14
+ readonly name = "OwnerOnlyFileError";
15
+ }
16
+ export {};
17
+ //# sourceMappingURL=owner-only-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner-only-file.d.ts","sourceRoot":"","sources":["../../src/node/owner-only-file.ts"],"names":[],"mappings":"AASA,KAAK,wBAAwB,GAAG;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAED,kEAAkE;AAClE,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,wBAAwB;CACtC"}
@@ -0,0 +1,59 @@
1
+ import { constants } from 'node:fs';
2
+ import { open, unlink, } from 'node:fs/promises';
3
+ const OWNER_FILE_MODE = 0o600;
4
+ /**
5
+ * Reads a bounded regular file without following its final symlink and rejects
6
+ * group- or world-accessible POSIX files.
7
+ */
8
+ export async function readOwnerOnlyFile(filePath, options) {
9
+ let file;
10
+ try {
11
+ file = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
12
+ const metadata = await file.stat();
13
+ if (!metadata.isFile()
14
+ || metadata.size <= 0
15
+ || metadata.size > options.maxBytes) {
16
+ throw new OwnerOnlyFileError(`${options.description} must be a non-empty regular file no larger than ${options.maxBytes} bytes.`);
17
+ }
18
+ if (process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) {
19
+ throw new OwnerOnlyFileError(`${options.description} must not be group- or world-accessible.`);
20
+ }
21
+ return await file.readFile('utf8');
22
+ }
23
+ finally {
24
+ await file?.close().catch(() => undefined);
25
+ }
26
+ }
27
+ /** Creates, flushes, and never overwrites one owner-only file. */
28
+ export async function createOwnerOnlyFile(filePath, value) {
29
+ let file;
30
+ let created = false;
31
+ try {
32
+ file = await open(filePath, 'wx', OWNER_FILE_MODE);
33
+ created = true;
34
+ await file.writeFile(value, 'utf8');
35
+ await file.sync();
36
+ }
37
+ catch (error) {
38
+ await file?.close().catch(() => undefined);
39
+ file = undefined;
40
+ if (created) {
41
+ await unlink(filePath).catch(() => undefined);
42
+ }
43
+ throw error;
44
+ }
45
+ finally {
46
+ await file?.close().catch(() => undefined);
47
+ }
48
+ }
49
+ export function removeOneTrailingLineEnding(value) {
50
+ return value.endsWith('\r\n')
51
+ ? value.slice(0, -2)
52
+ : value.endsWith('\n')
53
+ ? value.slice(0, -1)
54
+ : value;
55
+ }
56
+ export class OwnerOnlyFileError extends Error {
57
+ name = 'OwnerOnlyFileError';
58
+ }
59
+ //# sourceMappingURL=owner-only-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner-only-file.js","sourceRoot":"","sources":["../../src/node/owner-only-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EACL,IAAI,EACJ,MAAM,GAEP,MAAM,kBAAkB,CAAC;AAE1B,MAAM,eAAe,GAAG,KAAK,CAAC;AAO9B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,QAAgB,EAChB,OAAiC;IAEjC,IAAI,IAA4B,CAAC;IACjC,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;QACvE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACnC,IACE,CAAC,QAAQ,CAAC,MAAM,EAAE;eACf,QAAQ,CAAC,IAAI,IAAI,CAAC;eAClB,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,QAAQ,EACnC,CAAC;YACD,MAAM,IAAI,kBAAkB,CAC1B,GAAG,OAAO,CAAC,WAAW,oDAAoD,OAAO,CAAC,QAAQ,SAAS,CACpG,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YAClE,MAAM,IAAI,kBAAkB,CAC1B,GAAG,OAAO,CAAC,WAAW,0CAA0C,CACjE,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,KAAa;IAEb,IAAI,IAA4B,CAAC;IACjC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC;QACnD,OAAO,GAAG,IAAI,CAAC;QACf,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACpC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,GAAG,SAAS,CAAC;QACjB,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,KAAa;IACvD,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC3B,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpB,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YACpB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACpB,CAAC,CAAC,KAAK,CAAC;AACd,CAAC;AAED,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,GAAG,oBAAoB,CAAC;CACtC","sourcesContent":["import { constants } from 'node:fs';\nimport {\n open,\n unlink,\n type FileHandle,\n} from 'node:fs/promises';\n\nconst OWNER_FILE_MODE = 0o600;\n\ntype OwnerOnlyFileReadOptions = {\n description: string;\n maxBytes: number;\n};\n\n/**\n * Reads a bounded regular file without following its final symlink and rejects\n * group- or world-accessible POSIX files.\n */\nexport async function readOwnerOnlyFile(\n filePath: string,\n options: OwnerOnlyFileReadOptions,\n): Promise<string> {\n let file: FileHandle | undefined;\n try {\n file = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);\n const metadata = await file.stat();\n if (\n !metadata.isFile()\n || metadata.size <= 0\n || metadata.size > options.maxBytes\n ) {\n throw new OwnerOnlyFileError(\n `${options.description} must be a non-empty regular file no larger than ${options.maxBytes} bytes.`,\n );\n }\n if (process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) {\n throw new OwnerOnlyFileError(\n `${options.description} must not be group- or world-accessible.`,\n );\n }\n return await file.readFile('utf8');\n } finally {\n await file?.close().catch(() => undefined);\n }\n}\n\n/** Creates, flushes, and never overwrites one owner-only file. */\nexport async function createOwnerOnlyFile(\n filePath: string,\n value: string,\n): Promise<void> {\n let file: FileHandle | undefined;\n let created = false;\n try {\n file = await open(filePath, 'wx', OWNER_FILE_MODE);\n created = true;\n await file.writeFile(value, 'utf8');\n await file.sync();\n } catch (error) {\n await file?.close().catch(() => undefined);\n file = undefined;\n if (created) {\n await unlink(filePath).catch(() => undefined);\n }\n throw error;\n } finally {\n await file?.close().catch(() => undefined);\n }\n}\n\nexport function removeOneTrailingLineEnding(value: string): string {\n return value.endsWith('\\r\\n')\n ? value.slice(0, -2)\n : value.endsWith('\\n')\n ? value.slice(0, -1)\n : value;\n}\n\nexport class OwnerOnlyFileError extends Error {\n readonly name = 'OwnerOnlyFileError';\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heddleagent/execution-host-client",
3
- "version": "7.0.0",
3
+ "version": "8.0.0",
4
4
  "description": "Contracts, execution authority, lifecycle services, and adopter clients for compatible Heddle Execution Hosts",
5
5
  "author": "Jay / Fienna Liang <roackb2@gmail.com>",
6
6
  "license": "MIT",
@@ -20,6 +20,9 @@
20
20
  },
21
21
  "main": "./dist/index.js",
22
22
  "types": "./dist/index.d.ts",
23
+ "bin": {
24
+ "heddle-hostctl": "./dist/cli/hostctl.js"
25
+ },
23
26
  "exports": {
24
27
  ".": {
25
28
  "types": "./dist/index.d.ts",
@@ -115,6 +118,7 @@
115
118
  "@aws-sdk/client-bedrock-agentcore": "^3.1107.0",
116
119
  "@modelcontextprotocol/sdk": "^1.29.0",
117
120
  "@smithy/protocol-http": "^5.5.16",
121
+ "commander": "^14.0.3",
118
122
  "dayjs": "^1.11.20",
119
123
  "eventsource-parser": "^3.1.0",
120
124
  "jose": "^6.2.8",
@@ -1,21 +0,0 @@
1
- import { type HostedHeartbeatAgentExecutionTransportInput } from '../heartbeat/index.js';
2
- import { type HostedHeartbeatDelegation, type HostedHeartbeatDelegationRequest } from './contracts.js';
3
- import type { HostedHeartbeatDelegatedExecutionTransportConfig, HostedHeartbeatDelegatedExecutionTransportPort, HostedHeartbeatDelegationClientConfig, HostedHeartbeatDelegationIssuer } from './types.js';
4
- export declare class HostedHeartbeatDelegationRequestError extends Error {
5
- readonly status: number;
6
- readonly name = "HostedHeartbeatDelegationRequestError";
7
- constructor(status: number);
8
- }
9
- /** Authenticated coordinator client for one product-issued authority bundle. */
10
- export declare class HostedHeartbeatDelegationClient implements HostedHeartbeatDelegationIssuer {
11
- #private;
12
- constructor(config: HostedHeartbeatDelegationClientConfig);
13
- issue(rawInput: HostedHeartbeatDelegationRequest, signal?: AbortSignal): Promise<HostedHeartbeatDelegation>;
14
- }
15
- /** Executes a claimed task with the exact product-issued authority bundle. */
16
- export declare class HostedHeartbeatDelegatedExecutionTransport implements HostedHeartbeatDelegatedExecutionTransportPort {
17
- #private;
18
- constructor(config: HostedHeartbeatDelegatedExecutionTransportConfig);
19
- execute(input: HostedHeartbeatAgentExecutionTransportInput): Promise<unknown>;
20
- }
21
- //# sourceMappingURL=hosted-heartbeat-delegation-client.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hosted-heartbeat-delegation-client.d.ts","sourceRoot":"","sources":["../../src/coordinator/hosted-heartbeat-delegation-client.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,2CAA2C,EACjD,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAIL,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACtC,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EACV,gDAAgD,EAChD,8CAA8C,EAC9C,qCAAqC,EACrC,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAWpB,qBAAa,qCAAsC,SAAQ,KAAK;IAGlD,QAAQ,CAAC,MAAM,EAAE,MAAM;IAFnC,QAAQ,CAAC,IAAI,2CAA2C;gBAEnC,MAAM,EAAE,MAAM;CAGpC;AAED,gFAAgF;AAChF,qBAAa,+BACb,YAAW,+BAA+B;;gBAK5B,MAAM,EAAE,qCAAqC;IAcnD,KAAK,CACT,QAAQ,EAAE,gCAAgC,EAC1C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,yBAAyB,CAAC;CA4BtC;AAED,8EAA8E;AAC9E,qBAAa,0CACb,YAAW,8CAA8C;;gBAG3C,MAAM,EAAE,gDAAgD;IAI9D,OAAO,CACX,KAAK,EAAE,2CAA2C,GACjD,OAAO,CAAC,OAAO,CAAC;CAwBpB"}
@@ -1,117 +0,0 @@
1
- import { z } from 'zod';
2
- import { HEARTBEAT_TASK_WORKFLOW, isSafeWebUrl, } from '../contracts/index.js';
3
- import { HostedHeartbeatAgentExecutionTransport, HostedHeartbeatTaskService, } from '../heartbeat/index.js';
4
- import { HOSTED_HEARTBEAT_DELEGATIONS_PATH, HostedHeartbeatDelegationRequestSchema, HostedHeartbeatDelegationSchema, } from './contracts.js';
5
- import { HostedHeartbeatServiceTokenSchema } from './service-token.js';
6
- const SafeBaseUrlSchema = z.custom((value) => value instanceof URL && isSafeWebUrl(value), 'baseUrl must be a safe HTTPS or loopback HTTP URL');
7
- const AbsolutePathSchema = z.string().min(1).max(256).regex(/^\/(?:[^?#\s]*)$/, 'must be an absolute path without query, fragment, or whitespace');
8
- export class HostedHeartbeatDelegationRequestError extends Error {
9
- status;
10
- name = 'HostedHeartbeatDelegationRequestError';
11
- constructor(status) {
12
- super(`Heartbeat delegation failed with status ${status}.`);
13
- this.status = status;
14
- }
15
- }
16
- /** Authenticated coordinator client for one product-issued authority bundle. */
17
- export class HostedHeartbeatDelegationClient {
18
- #endpoint;
19
- #authorization;
20
- #fetch;
21
- constructor(config) {
22
- const baseUrl = SafeBaseUrlSchema.parse(config.baseUrl);
23
- this.#endpoint = new URL(AbsolutePathSchema.parse(config.path ?? HOSTED_HEARTBEAT_DELEGATIONS_PATH), baseUrl);
24
- this.#authorization = `Bearer ${HostedHeartbeatServiceTokenSchema.parse(config.apiToken)}`;
25
- this.#fetch = config.fetch ?? globalThis.fetch;
26
- }
27
- async issue(rawInput, signal) {
28
- const input = HostedHeartbeatDelegationRequestSchema.parse(rawInput);
29
- const response = await this.#fetch(this.#endpoint, {
30
- method: 'POST',
31
- headers: {
32
- authorization: this.#authorization,
33
- 'content-type': 'application/json',
34
- },
35
- body: JSON.stringify(input),
36
- redirect: 'error',
37
- signal,
38
- });
39
- if (!response.ok) {
40
- throw new HostedHeartbeatDelegationRequestError(response.status);
41
- }
42
- const delegation = HostedHeartbeatDelegationSchema.parse(await response.json());
43
- if (delegation.taskId !== input.taskId
44
- || delegation.executionId !== input.executionId) {
45
- throw new Error('Product backend returned a mismatched heartbeat delegation.');
46
- }
47
- return delegation;
48
- }
49
- }
50
- /** Executes a claimed task with the exact product-issued authority bundle. */
51
- export class HostedHeartbeatDelegatedExecutionTransport {
52
- #config;
53
- constructor(config) {
54
- this.#config = config;
55
- }
56
- async execute(input) {
57
- const delegation = await this.#config.delegations.issue({
58
- schemaVersion: 1,
59
- taskId: input.request.taskId,
60
- executionId: input.request.executionId,
61
- }, input.signal);
62
- const service = new HostedHeartbeatTaskService({
63
- authority: new DelegatedExecutionAuthorityIssuer(delegation),
64
- executionHost: this.#config.executionHost,
65
- modelCredentials: this.#config.modelCredentials,
66
- mcp: {
67
- allowedTools: delegation.authority.metadata.mcp.allowedTools,
68
- },
69
- });
70
- const transport = new HostedHeartbeatAgentExecutionTransport({
71
- runner: service,
72
- resolveInvocationContext: () => ({
73
- scope: delegation.scope,
74
- runtimeSessionId: delegation.runtimeSessionId,
75
- deadlineAt: delegation.deadlineAt,
76
- }),
77
- });
78
- return await transport.execute(input);
79
- }
80
- }
81
- /** Ensures downstream execution cannot widen a delegated authority bundle. */
82
- class DelegatedExecutionAuthorityIssuer {
83
- delegation;
84
- constructor(delegation) {
85
- this.delegation = delegation;
86
- }
87
- async issue(input) {
88
- const metadata = this.delegation.authority.metadata;
89
- if (input.workflow !== HEARTBEAT_TASK_WORKFLOW
90
- || input.workflow !== metadata.workflow
91
- || input.invocationId !== metadata.invocationId
92
- || input.runtimeSessionId !== metadata.runtimeSessionId
93
- || !sameScope(input.scope, this.delegation.scope)
94
- || !sameScope(input.scope, metadata.scope)
95
- || !sameTools(input.mcp?.allowedTools, metadata.mcp.allowedTools)) {
96
- throw new Error('Heartbeat delegation does not match Heddle execution.');
97
- }
98
- const authority = this.delegation.authority;
99
- return Object.freeze({
100
- metadata: metadata,
101
- executionAssertion: () => authority.executionAssertion,
102
- mcpCapability: () => authority.mcpCapability,
103
- toJSON: () => metadata,
104
- });
105
- }
106
- }
107
- function sameScope(left, right) {
108
- return left.tenantId === right.tenantId
109
- && left.subjectId === right.subjectId
110
- && left.productSessionId === right.productSessionId;
111
- }
112
- function sameTools(left, right) {
113
- return Boolean(left
114
- && left.length === right.length
115
- && left.every((tool, index) => tool === right[index]));
116
- }
117
- //# sourceMappingURL=hosted-heartbeat-delegation-client.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hosted-heartbeat-delegation-client.js","sourceRoot":"","sources":["../../src/coordinator/hosted-heartbeat-delegation-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type {\n ExecutionAuthorityIssueInput,\n ExecutionAuthorityIssuer,\n IssuedExecutionAuthority,\n IssuedExecutionAuthorityMetadata,\n} from '../authority/index.js';\nimport {\n HEARTBEAT_TASK_WORKFLOW,\n isSafeWebUrl,\n} from '../contracts/index.js';\nimport {\n HostedHeartbeatAgentExecutionTransport,\n HostedHeartbeatTaskService,\n type HostedHeartbeatAgentExecutionTransportInput,\n} from '../heartbeat/index.js';\nimport {\n HOSTED_HEARTBEAT_DELEGATIONS_PATH,\n HostedHeartbeatDelegationRequestSchema,\n HostedHeartbeatDelegationSchema,\n type HostedHeartbeatDelegation,\n type HostedHeartbeatDelegationRequest,\n} from './contracts.js';\nimport { HostedHeartbeatServiceTokenSchema } from './service-token.js';\nimport type {\n HostedHeartbeatDelegatedExecutionTransportConfig,\n HostedHeartbeatDelegatedExecutionTransportPort,\n HostedHeartbeatDelegationClientConfig,\n HostedHeartbeatDelegationIssuer,\n} from './types.js';\n\nconst SafeBaseUrlSchema = z.custom<URL>(\n (value) => value instanceof URL && isSafeWebUrl(value),\n 'baseUrl must be a safe HTTPS or loopback HTTP URL',\n);\nconst AbsolutePathSchema = z.string().min(1).max(256).regex(\n /^\\/(?:[^?#\\s]*)$/,\n 'must be an absolute path without query, fragment, or whitespace',\n);\n\nexport class HostedHeartbeatDelegationRequestError extends Error {\n readonly name = 'HostedHeartbeatDelegationRequestError';\n\n constructor(readonly status: number) {\n super(`Heartbeat delegation failed with status ${status}.`);\n }\n}\n\n/** Authenticated coordinator client for one product-issued authority bundle. */\nexport class HostedHeartbeatDelegationClient\nimplements HostedHeartbeatDelegationIssuer {\n readonly #endpoint: URL;\n readonly #authorization: string;\n readonly #fetch: typeof globalThis.fetch;\n\n constructor(config: HostedHeartbeatDelegationClientConfig) {\n const baseUrl = SafeBaseUrlSchema.parse(config.baseUrl);\n this.#endpoint = new URL(\n AbsolutePathSchema.parse(\n config.path ?? HOSTED_HEARTBEAT_DELEGATIONS_PATH,\n ),\n baseUrl,\n );\n this.#authorization = `Bearer ${\n HostedHeartbeatServiceTokenSchema.parse(config.apiToken)\n }`;\n this.#fetch = config.fetch ?? globalThis.fetch;\n }\n\n async issue(\n rawInput: HostedHeartbeatDelegationRequest,\n signal?: AbortSignal,\n ): Promise<HostedHeartbeatDelegation> {\n const input = HostedHeartbeatDelegationRequestSchema.parse(rawInput);\n const response = await this.#fetch(this.#endpoint, {\n method: 'POST',\n headers: {\n authorization: this.#authorization,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(input),\n redirect: 'error',\n signal,\n });\n if (!response.ok) {\n throw new HostedHeartbeatDelegationRequestError(response.status);\n }\n const delegation = HostedHeartbeatDelegationSchema.parse(\n await response.json(),\n );\n if (\n delegation.taskId !== input.taskId\n || delegation.executionId !== input.executionId\n ) {\n throw new Error(\n 'Product backend returned a mismatched heartbeat delegation.',\n );\n }\n return delegation;\n }\n}\n\n/** Executes a claimed task with the exact product-issued authority bundle. */\nexport class HostedHeartbeatDelegatedExecutionTransport\nimplements HostedHeartbeatDelegatedExecutionTransportPort {\n readonly #config: HostedHeartbeatDelegatedExecutionTransportConfig;\n\n constructor(config: HostedHeartbeatDelegatedExecutionTransportConfig) {\n this.#config = config;\n }\n\n async execute(\n input: HostedHeartbeatAgentExecutionTransportInput,\n ): Promise<unknown> {\n const delegation = await this.#config.delegations.issue({\n schemaVersion: 1,\n taskId: input.request.taskId,\n executionId: input.request.executionId,\n }, input.signal);\n const service = new HostedHeartbeatTaskService({\n authority: new DelegatedExecutionAuthorityIssuer(delegation),\n executionHost: this.#config.executionHost,\n modelCredentials: this.#config.modelCredentials,\n mcp: {\n allowedTools: delegation.authority.metadata.mcp.allowedTools,\n },\n });\n const transport = new HostedHeartbeatAgentExecutionTransport({\n runner: service,\n resolveInvocationContext: () => ({\n scope: delegation.scope,\n runtimeSessionId: delegation.runtimeSessionId,\n deadlineAt: delegation.deadlineAt,\n }),\n });\n return await transport.execute(input);\n }\n}\n\n/** Ensures downstream execution cannot widen a delegated authority bundle. */\nclass DelegatedExecutionAuthorityIssuer implements ExecutionAuthorityIssuer {\n constructor(private readonly delegation: HostedHeartbeatDelegation) {}\n\n async issue(\n input: ExecutionAuthorityIssueInput,\n ): Promise<IssuedExecutionAuthority> {\n const metadata = this.delegation.authority.metadata;\n if (\n input.workflow !== HEARTBEAT_TASK_WORKFLOW\n || input.workflow !== metadata.workflow\n || input.invocationId !== metadata.invocationId\n || input.runtimeSessionId !== metadata.runtimeSessionId\n || !sameScope(input.scope, this.delegation.scope)\n || !sameScope(input.scope, metadata.scope)\n || !sameTools(input.mcp?.allowedTools, metadata.mcp.allowedTools)\n ) {\n throw new Error('Heartbeat delegation does not match Heddle execution.');\n }\n const authority = this.delegation.authority;\n return Object.freeze({\n metadata: metadata as IssuedExecutionAuthorityMetadata,\n executionAssertion: () => authority.executionAssertion,\n mcpCapability: () => authority.mcpCapability,\n toJSON: () => metadata as IssuedExecutionAuthorityMetadata,\n });\n }\n}\n\nfunction sameScope(\n left: ExecutionAuthorityIssueInput['scope'],\n right: HostedHeartbeatDelegation['scope']\n | HostedHeartbeatDelegation['authority']['metadata']['scope'],\n): boolean {\n return left.tenantId === right.tenantId\n && left.subjectId === right.subjectId\n && left.productSessionId === right.productSessionId;\n}\n\nfunction sameTools(\n left: readonly string[] | undefined,\n right: readonly string[],\n): boolean {\n return Boolean(\n left\n && left.length === right.length\n && left.every((tool, index) => tool === right[index]),\n );\n}\n"]}
@@ -1,15 +0,0 @@
1
- import { type HostedHeartbeatDelegation, type HostedHeartbeatDelegationRequest } from './contracts.js';
2
- import type { HostedHeartbeatDelegationIssuer, HostedHeartbeatDelegationServiceConfig } from './types.js';
3
- export declare class HostedHeartbeatDelegationRejectedError extends Error {
4
- readonly name = "HostedHeartbeatDelegationRejectedError";
5
- }
6
- /**
7
- * Converts one product authorization decision into a fully bound Heddle
8
- * authority bundle. Product code never constructs the delegation wire shape.
9
- */
10
- export declare class HostedHeartbeatDelegationService implements HostedHeartbeatDelegationIssuer {
11
- #private;
12
- constructor(config: HostedHeartbeatDelegationServiceConfig);
13
- issue(rawInput: HostedHeartbeatDelegationRequest, rawSignal?: AbortSignal): Promise<HostedHeartbeatDelegation>;
14
- }
15
- //# sourceMappingURL=hosted-heartbeat-delegation-service.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hosted-heartbeat-delegation-service.d.ts","sourceRoot":"","sources":["../../src/coordinator/hosted-heartbeat-delegation-service.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACtC,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EACV,+BAA+B,EAC/B,sCAAsC,EACvC,MAAM,YAAY,CAAC;AAOpB,qBAAa,sCAAuC,SAAQ,KAAK;IAC/D,QAAQ,CAAC,IAAI,4CAA4C;CAC1D;AAED;;;GAGG;AACH,qBAAa,gCACb,YAAW,+BAA+B;;gBAO5B,MAAM,EAAE,sCAAsC;IAYpD,KAAK,CACT,QAAQ,EAAE,gCAAgC,EAC1C,SAAS,CAAC,EAAE,WAAW,GACtB,OAAO,CAAC,yBAAyB,CAAC;CAqDtC"}
@@ -1,81 +0,0 @@
1
- import dayjs from 'dayjs';
2
- import { z } from 'zod';
3
- import { HEARTBEAT_TASK_WORKFLOW, OpaqueIdSchema } from '../contracts/index.js';
4
- import { HostedHeartbeatDelegationAuthorizationSchema, HostedHeartbeatDelegationRequestSchema, HostedHeartbeatDelegationSchema, } from './contracts.js';
5
- import { createHostedRuntimeSessionId } from './runtime-session.js';
6
- const DelegationServiceConfigSchema = z.object({
7
- runtimeSessionNamespace: OpaqueIdSchema,
8
- maxExecutionMs: z.number().int().min(1_000).max(Number.MAX_SAFE_INTEGER),
9
- }).strict();
10
- export class HostedHeartbeatDelegationRejectedError extends Error {
11
- name = 'HostedHeartbeatDelegationRejectedError';
12
- }
13
- /**
14
- * Converts one product authorization decision into a fully bound Heddle
15
- * authority bundle. Product code never constructs the delegation wire shape.
16
- */
17
- export class HostedHeartbeatDelegationService {
18
- #authority;
19
- #authorizer;
20
- #runtimeSessionNamespace;
21
- #maxExecutionMs;
22
- #now;
23
- constructor(config) {
24
- const parsed = DelegationServiceConfigSchema.parse({
25
- runtimeSessionNamespace: config.runtimeSessionNamespace,
26
- maxExecutionMs: config.maxExecutionMs,
27
- });
28
- this.#authority = config.authority;
29
- this.#authorizer = config.authorizer;
30
- this.#runtimeSessionNamespace = parsed.runtimeSessionNamespace;
31
- this.#maxExecutionMs = parsed.maxExecutionMs;
32
- this.#now = config.now ?? (() => new Date());
33
- }
34
- async issue(rawInput, rawSignal) {
35
- const input = HostedHeartbeatDelegationRequestSchema.parse(rawInput);
36
- const signal = rawSignal ?? new AbortController().signal;
37
- signal.throwIfAborted();
38
- const rawAuthorization = await this.#authorizer.authorize({
39
- taskId: input.taskId,
40
- executionId: input.executionId,
41
- signal,
42
- });
43
- if (!rawAuthorization) {
44
- throw new HostedHeartbeatDelegationRejectedError('The product rejected this heartbeat execution.');
45
- }
46
- const authorization = HostedHeartbeatDelegationAuthorizationSchema.parse(rawAuthorization);
47
- const runtimeSessionId = createHostedRuntimeSessionId({
48
- namespace: this.#runtimeSessionNamespace,
49
- scope: authorization.scope,
50
- });
51
- const deadlineAt = dayjs(this.#now())
52
- .add(this.#maxExecutionMs, 'millisecond')
53
- .toISOString();
54
- const issued = await this.#authority.issue({
55
- scope: authorization.scope,
56
- runtimeSessionId,
57
- invocationId: input.executionId,
58
- workflow: HEARTBEAT_TASK_WORKFLOW,
59
- mcp: { allowedTools: authorization.allowedTools },
60
- });
61
- signal.throwIfAborted();
62
- const mcpCapability = issued.mcpCapability();
63
- if (!mcpCapability || !issued.metadata.mcp) {
64
- throw new Error('Hosted heartbeat delegation requires an MCP-capable execution authority.');
65
- }
66
- return HostedHeartbeatDelegationSchema.parse({
67
- schemaVersion: 1,
68
- taskId: input.taskId,
69
- executionId: input.executionId,
70
- scope: authorization.scope,
71
- runtimeSessionId,
72
- deadlineAt,
73
- authority: {
74
- metadata: issued.metadata,
75
- executionAssertion: issued.executionAssertion(),
76
- mcpCapability,
77
- },
78
- });
79
- }
80
- }
81
- //# sourceMappingURL=hosted-heartbeat-delegation-service.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hosted-heartbeat-delegation-service.js","sourceRoot":"","sources":["../../src/coordinator/hosted-heartbeat-delegation-service.ts"],"names":[],"mappings":"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","sourcesContent":["import dayjs from 'dayjs';\nimport { z } from 'zod';\nimport { HEARTBEAT_TASK_WORKFLOW, OpaqueIdSchema } from '../contracts/index.js';\nimport {\n HostedHeartbeatDelegationAuthorizationSchema,\n HostedHeartbeatDelegationRequestSchema,\n HostedHeartbeatDelegationSchema,\n type HostedHeartbeatDelegation,\n type HostedHeartbeatDelegationRequest,\n} from './contracts.js';\nimport { createHostedRuntimeSessionId } from './runtime-session.js';\nimport type {\n HostedHeartbeatDelegationIssuer,\n HostedHeartbeatDelegationServiceConfig,\n} from './types.js';\n\nconst DelegationServiceConfigSchema = z.object({\n runtimeSessionNamespace: OpaqueIdSchema,\n maxExecutionMs: z.number().int().min(1_000).max(Number.MAX_SAFE_INTEGER),\n}).strict();\n\nexport class HostedHeartbeatDelegationRejectedError extends Error {\n readonly name = 'HostedHeartbeatDelegationRejectedError';\n}\n\n/**\n * Converts one product authorization decision into a fully bound Heddle\n * authority bundle. Product code never constructs the delegation wire shape.\n */\nexport class HostedHeartbeatDelegationService\nimplements HostedHeartbeatDelegationIssuer {\n readonly #authority: HostedHeartbeatDelegationServiceConfig['authority'];\n readonly #authorizer: HostedHeartbeatDelegationServiceConfig['authorizer'];\n readonly #runtimeSessionNamespace: string;\n readonly #maxExecutionMs: number;\n readonly #now: () => Date;\n\n constructor(config: HostedHeartbeatDelegationServiceConfig) {\n const parsed = DelegationServiceConfigSchema.parse({\n runtimeSessionNamespace: config.runtimeSessionNamespace,\n maxExecutionMs: config.maxExecutionMs,\n });\n this.#authority = config.authority;\n this.#authorizer = config.authorizer;\n this.#runtimeSessionNamespace = parsed.runtimeSessionNamespace;\n this.#maxExecutionMs = parsed.maxExecutionMs;\n this.#now = config.now ?? (() => new Date());\n }\n\n async issue(\n rawInput: HostedHeartbeatDelegationRequest,\n rawSignal?: AbortSignal,\n ): Promise<HostedHeartbeatDelegation> {\n const input = HostedHeartbeatDelegationRequestSchema.parse(rawInput);\n const signal = rawSignal ?? new AbortController().signal;\n signal.throwIfAborted();\n const rawAuthorization = await this.#authorizer.authorize({\n taskId: input.taskId,\n executionId: input.executionId,\n signal,\n });\n if (!rawAuthorization) {\n throw new HostedHeartbeatDelegationRejectedError(\n 'The product rejected this heartbeat execution.',\n );\n }\n const authorization = HostedHeartbeatDelegationAuthorizationSchema.parse(\n rawAuthorization,\n );\n const runtimeSessionId = createHostedRuntimeSessionId({\n namespace: this.#runtimeSessionNamespace,\n scope: authorization.scope,\n });\n const deadlineAt = dayjs(this.#now())\n .add(this.#maxExecutionMs, 'millisecond')\n .toISOString();\n const issued = await this.#authority.issue({\n scope: authorization.scope,\n runtimeSessionId,\n invocationId: input.executionId,\n workflow: HEARTBEAT_TASK_WORKFLOW,\n mcp: { allowedTools: authorization.allowedTools },\n });\n signal.throwIfAborted();\n const mcpCapability = issued.mcpCapability();\n if (!mcpCapability || !issued.metadata.mcp) {\n throw new Error(\n 'Hosted heartbeat delegation requires an MCP-capable execution authority.',\n );\n }\n\n return HostedHeartbeatDelegationSchema.parse({\n schemaVersion: 1,\n taskId: input.taskId,\n executionId: input.executionId,\n scope: authorization.scope,\n runtimeSessionId,\n deadlineAt,\n authority: {\n metadata: issued.metadata,\n executionAssertion: issued.executionAssertion(),\n mcpCapability,\n },\n });\n }\n}\n"]}