@kelloggt/dsh-program-agents 0.0.0-stage → 0.1.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 (222) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -2
  3. package/cordis.patch.yml +5 -0
  4. package/dist/app/agents.d.ts +50 -0
  5. package/dist/app/agents.js +70 -0
  6. package/dist/app/agents.js.map +1 -0
  7. package/dist/app/conversations.d.ts +8 -0
  8. package/dist/app/conversations.js +26 -0
  9. package/dist/app/conversations.js.map +1 -0
  10. package/dist/app/endpoints.d.ts +36 -0
  11. package/dist/app/endpoints.js +162 -0
  12. package/dist/app/endpoints.js.map +1 -0
  13. package/dist/app/inbox.d.ts +61 -0
  14. package/dist/app/inbox.js +253 -0
  15. package/dist/app/inbox.js.map +1 -0
  16. package/dist/app/index.d.ts +70 -0
  17. package/dist/app/index.js +147 -0
  18. package/dist/app/index.js.map +1 -0
  19. package/dist/app/mailbox.d.ts +63 -0
  20. package/dist/app/mailbox.js +173 -0
  21. package/dist/app/mailbox.js.map +1 -0
  22. package/dist/app/models.d.ts +32 -0
  23. package/dist/app/models.js +13 -0
  24. package/dist/app/models.js.map +1 -0
  25. package/dist/app/server.d.ts +36 -0
  26. package/dist/app/server.js +233 -0
  27. package/dist/app/server.js.map +1 -0
  28. package/dist/app/stream-server.d.ts +9 -0
  29. package/dist/app/stream-server.js +77 -0
  30. package/dist/app/stream-server.js.map +1 -0
  31. package/dist/app/stream.d.ts +4 -0
  32. package/dist/app/stream.js +93 -0
  33. package/dist/app/stream.js.map +1 -0
  34. package/dist/app/tools.d.ts +16 -0
  35. package/dist/app/tools.js +24 -0
  36. package/dist/app/tools.js.map +1 -0
  37. package/dist/client/controller.d.ts +21 -0
  38. package/dist/client/controller.js +140 -0
  39. package/dist/client/controller.js.map +1 -0
  40. package/dist/client/index.d.ts +22 -0
  41. package/dist/client/index.js +14 -0
  42. package/dist/client/index.js.map +1 -0
  43. package/dist/client/model.d.ts +66 -0
  44. package/dist/client/model.js +147 -0
  45. package/dist/client/model.js.map +1 -0
  46. package/dist/client/models.d.ts +11 -0
  47. package/dist/client/models.js +27 -0
  48. package/dist/client/models.js.map +1 -0
  49. package/dist/client/transport.d.ts +64 -0
  50. package/dist/client/transport.js +141 -0
  51. package/dist/client/transport.js.map +1 -0
  52. package/dist/client/view.d.ts +19 -0
  53. package/dist/client/view.js +69 -0
  54. package/dist/client/view.js.map +1 -0
  55. package/dist/client.js +673 -0
  56. package/dist/conversations.d.ts +92 -0
  57. package/dist/conversations.js +6 -0
  58. package/dist/conversations.js.map +1 -0
  59. package/dist/endpoints.d.ts +48 -0
  60. package/dist/endpoints.js +43 -0
  61. package/dist/endpoints.js.map +1 -0
  62. package/dist/examples/dashboard.js +42 -0
  63. package/dist/examples/dynamic-server.js +27 -0
  64. package/dist/examples/inbox-worker.js +22 -0
  65. package/dist/examples/model-slots.js +19 -0
  66. package/dist/examples/standalone-supervisor.js +20 -0
  67. package/dist/host/adapter.d.ts +195 -0
  68. package/dist/host/adapter.js +630 -0
  69. package/dist/host/adapter.js.map +1 -0
  70. package/dist/host/agents.d.ts +35 -0
  71. package/dist/host/agents.js +222 -0
  72. package/dist/host/agents.js.map +1 -0
  73. package/dist/host/audit.d.ts +14 -0
  74. package/dist/host/audit.js +39 -0
  75. package/dist/host/audit.js.map +1 -0
  76. package/dist/host/catalog.d.ts +53 -0
  77. package/dist/host/catalog.js +116 -0
  78. package/dist/host/catalog.js.map +1 -0
  79. package/dist/host/contracts.d.ts +38 -0
  80. package/dist/host/contracts.js +2 -0
  81. package/dist/host/contracts.js.map +1 -0
  82. package/dist/host/conversations.d.ts +41 -0
  83. package/dist/host/conversations.js +324 -0
  84. package/dist/host/conversations.js.map +1 -0
  85. package/dist/host/events.d.ts +12 -0
  86. package/dist/host/events.js +106 -0
  87. package/dist/host/events.js.map +1 -0
  88. package/dist/host/http.d.ts +14 -0
  89. package/dist/host/http.js +30 -0
  90. package/dist/host/http.js.map +1 -0
  91. package/dist/host/index.d.ts +23 -0
  92. package/dist/host/index.js +84 -0
  93. package/dist/host/index.js.map +1 -0
  94. package/dist/host/installed-v10.js +19797 -0
  95. package/dist/host/installed-v11.js +19797 -0
  96. package/dist/host/installed-v12.js +19797 -0
  97. package/dist/host/installed-v13.js +19797 -0
  98. package/dist/host/installed-v2.js +19797 -0
  99. package/dist/host/installed-v3.js +19797 -0
  100. package/dist/host/installed-v4.js +19797 -0
  101. package/dist/host/installed-v5.js +19797 -0
  102. package/dist/host/installed-v6.js +19797 -0
  103. package/dist/host/installed-v7.js +19797 -0
  104. package/dist/host/installed-v8.js +19797 -0
  105. package/dist/host/installed-v9.js +19797 -0
  106. package/dist/host/mcp.d.ts +11 -0
  107. package/dist/host/mcp.js +42 -0
  108. package/dist/host/mcp.js.map +1 -0
  109. package/dist/host/models.d.ts +37 -0
  110. package/dist/host/models.js +141 -0
  111. package/dist/host/models.js.map +1 -0
  112. package/dist/host/operations.d.ts +22 -0
  113. package/dist/host/operations.js +72 -0
  114. package/dist/host/operations.js.map +1 -0
  115. package/dist/host/plugin.js +19797 -0
  116. package/dist/host/policies.d.ts +36 -0
  117. package/dist/host/policies.js +73 -0
  118. package/dist/host/policies.js.map +1 -0
  119. package/dist/host/requests.d.ts +57 -0
  120. package/dist/host/requests.js +353 -0
  121. package/dist/host/requests.js.map +1 -0
  122. package/dist/host/tool-client.d.ts +19 -0
  123. package/dist/host/tool-client.js +62 -0
  124. package/dist/host/tool-client.js.map +1 -0
  125. package/dist/host/validation.d.ts +5 -0
  126. package/dist/host/validation.js +42 -0
  127. package/dist/host/validation.js.map +1 -0
  128. package/dist/mcp.d.ts +7 -0
  129. package/dist/mcp.js +2 -0
  130. package/dist/mcp.js.map +1 -0
  131. package/dist/models.d.ts +34 -0
  132. package/dist/models.js +53 -0
  133. package/dist/models.js.map +1 -0
  134. package/dist/monitoring/error-buffer.d.ts +89 -0
  135. package/dist/monitoring/error-buffer.js +355 -0
  136. package/dist/monitoring/error-buffer.js.map +1 -0
  137. package/dist/monitoring/fatal.d.ts +6 -0
  138. package/dist/monitoring/fatal.js +32 -0
  139. package/dist/monitoring/fatal.js.map +1 -0
  140. package/dist/monitoring/index.d.ts +7 -0
  141. package/dist/monitoring/index.js +8 -0
  142. package/dist/monitoring/index.js.map +1 -0
  143. package/dist/monitoring/logger.d.ts +45 -0
  144. package/dist/monitoring/logger.js +174 -0
  145. package/dist/monitoring/logger.js.map +1 -0
  146. package/dist/monitoring/schemas.d.ts +34 -0
  147. package/dist/monitoring/schemas.js +132 -0
  148. package/dist/monitoring/schemas.js.map +1 -0
  149. package/dist/monitoring/standard.d.ts +51 -0
  150. package/dist/monitoring/standard.js +140 -0
  151. package/dist/monitoring/standard.js.map +1 -0
  152. package/dist/monitoring/tailer.d.ts +41 -0
  153. package/dist/monitoring/tailer.js +210 -0
  154. package/dist/monitoring/tailer.js.map +1 -0
  155. package/dist/requests.d.ts +92 -0
  156. package/dist/requests.js +120 -0
  157. package/dist/requests.js.map +1 -0
  158. package/dist/runtime/bootstrap.d.ts +1 -0
  159. package/dist/runtime/bootstrap.js +25 -0
  160. package/dist/runtime/bootstrap.js.map +1 -0
  161. package/dist/runtime/definitions.d.ts +7 -0
  162. package/dist/runtime/definitions.js +138 -0
  163. package/dist/runtime/definitions.js.map +1 -0
  164. package/dist/runtime/index.d.ts +4 -0
  165. package/dist/runtime/index.js +4 -0
  166. package/dist/runtime/index.js.map +1 -0
  167. package/dist/runtime/log-file.d.ts +24 -0
  168. package/dist/runtime/log-file.js +124 -0
  169. package/dist/runtime/log-file.js.map +1 -0
  170. package/dist/runtime/protocol.d.ts +60 -0
  171. package/dist/runtime/protocol.js +155 -0
  172. package/dist/runtime/protocol.js.map +1 -0
  173. package/dist/runtime/registry.d.ts +8 -0
  174. package/dist/runtime/registry.js +38 -0
  175. package/dist/runtime/registry.js.map +1 -0
  176. package/dist/runtime/supervisor.d.ts +74 -0
  177. package/dist/runtime/supervisor.js +881 -0
  178. package/dist/runtime/supervisor.js.map +1 -0
  179. package/dist/sdk/connection.d.ts +33 -0
  180. package/dist/sdk/connection.js +265 -0
  181. package/dist/sdk/connection.js.map +1 -0
  182. package/dist/sdk/index.d.ts +8 -0
  183. package/dist/sdk/index.js +2 -0
  184. package/dist/sdk/index.js.map +1 -0
  185. package/dist/supervision.d.ts +22 -0
  186. package/dist/supervision.js +32 -0
  187. package/dist/supervision.js.map +1 -0
  188. package/dist/types.d.ts +170 -0
  189. package/dist/types.js +2 -0
  190. package/dist/types.js.map +1 -0
  191. package/docs/installation.md +35 -0
  192. package/docs/log-format.md +102 -0
  193. package/docs/publishing.md +45 -0
  194. package/docs/supervision.md +73 -0
  195. package/examples/dashboard.ts +31 -0
  196. package/examples/dsh-web-local.patch.yml +15 -0
  197. package/examples/dynamic-server.ts +13 -0
  198. package/examples/inbox-worker.ts +17 -0
  199. package/examples/model-slots.ts +12 -0
  200. package/examples/standalone-supervisor.ts +14 -0
  201. package/examples/watch-server.ts +12 -0
  202. package/package.json +131 -3
  203. package/schemas/dsh.http.request.v1.schema.json +14 -0
  204. package/schemas/dsh.program.error-batch.v1.schema.json +24 -0
  205. package/schemas/dsh.program.error.v1.schema.json +8 -0
  206. package/schemas/standard-log-record.v1.schema.json +26 -0
  207. package/skills/program-agents/SKILL.md +124 -0
  208. package/skills/program-agents/chat-sidebar.md +23 -0
  209. package/skills/program-agents/endpoints.md +92 -0
  210. package/skills/program-agents/inboxes.md +120 -0
  211. package/skills/program-agents/log-format.md +102 -0
  212. package/skills/program-agents/models.md +59 -0
  213. package/skills/program-agents/references/chat-sidebar/AgentSidebar.tsx +181 -0
  214. package/skills/program-agents/references/chat-sidebar/README.md +181 -0
  215. package/skills/program-agents/references/chat-sidebar/agent-sidebar.css +37 -0
  216. package/skills/program-agents/references/chat-sidebar/backend.ts +184 -0
  217. package/skills/program-agents/references/chat-sidebar/protocol.ts +214 -0
  218. package/skills/program-agents/schemas/dsh.http.request.v1.schema.json +14 -0
  219. package/skills/program-agents/schemas/dsh.program.error-batch.v1.schema.json +24 -0
  220. package/skills/program-agents/schemas/dsh.program.error.v1.schema.json +8 -0
  221. package/skills/program-agents/schemas/standard-log-record.v1.schema.json +26 -0
  222. package/skills/program-agents/ui-patterns.md +39 -0
@@ -0,0 +1,881 @@
1
+ import { fork, spawn } from 'node:child_process';
2
+ import { randomBytes, randomUUID } from 'node:crypto';
3
+ import { existsSync, mkdtempSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { StringDecoder } from 'node:string_decoder';
8
+ import { endpointName, normalizeHostname } from '../endpoints.js';
9
+ import { CapabilityRegistry } from './registry.js';
10
+ import { definitionCommand, definitionResource, normalizeDefinition, normalizeResource } from './definitions.js';
11
+ import { BoundedRunLog } from './log-file.js';
12
+ import { assertJson, envelope, exactKeys, isRecord, jsonClone, matches, ProgramError, RUNTIME_LIMITS, safeError, SendQueue, validName } from './protocol.js';
13
+ const terminalStates = new Set(['completed', 'failed', 'stopped']);
14
+ function normalizeNetworking(input) {
15
+ try {
16
+ if (input !== undefined && (!isRecord(input) || !exactKeys(input, [], ['bindHost', 'publicHost', 'protocol'])))
17
+ throw new Error('Invalid endpoint networking settings');
18
+ const normalizedBindHost = normalizeHostname(input?.bindHost === undefined ? '127.0.0.1' : input.bindHost);
19
+ const bindHost = normalizedBindHost.startsWith('[') ? normalizedBindHost.slice(1, -1) : normalizedBindHost;
20
+ if (bindHost === 'browser')
21
+ throw new Error('Endpoint bindHost must be a server-local hostname');
22
+ const publicHost = input?.publicHost === undefined || input.publicHost === 'browser' ? 'browser' : normalizeHostname(input.publicHost);
23
+ if (['0.0.0.0', '[::]'].includes(publicHost))
24
+ throw new Error('Endpoint publicHost cannot be a wildcard listener address');
25
+ const protocol = input?.protocol === undefined ? 'http' : input.protocol;
26
+ if (protocol !== 'http' && protocol !== 'https')
27
+ throw new Error('Endpoint protocol must be http or https');
28
+ return { bindHost, publicHost, protocol };
29
+ }
30
+ catch (error) {
31
+ throw new ProgramError('INVALID_CONFIG', error instanceof Error ? error.message : 'Invalid endpoint networking settings');
32
+ }
33
+ }
34
+ function deferred() {
35
+ let resolve;
36
+ let reject;
37
+ const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
38
+ // Startup may fail before start() has installed its await; retain rejection safely.
39
+ void promise.catch(() => { });
40
+ return { promise, resolve, reject };
41
+ }
42
+ /** Process isolation for trusted programs, not an OS security sandbox. */
43
+ export class ProgramSupervisor {
44
+ options;
45
+ runs = new Map();
46
+ definitions = new Map();
47
+ capabilities;
48
+ networking;
49
+ maxRuns;
50
+ maxHistory;
51
+ startupTimeoutMs;
52
+ stopTimeoutMs;
53
+ callTimeoutMs;
54
+ disposed = false;
55
+ disposing;
56
+ // Includes cancelled handlers that have not cooperatively settled, even across
57
+ // restarts. A misbehaving host extension cannot leak unlimited detached work.
58
+ inflightHandlers = 0;
59
+ revokedOwners = new Set();
60
+ ownerEpochs = new Map();
61
+ logDirectory;
62
+ constructor(options) {
63
+ this.options = options;
64
+ if (process.platform === 'win32')
65
+ throw new ProgramError('UNSUPPORTED_PLATFORM', 'Program supervision requires POSIX process-group termination; Windows is not supported');
66
+ this.maxRuns = this.positive(options.maxRuns ?? 16, 'maxRuns');
67
+ this.maxHistory = this.positive(options.maxHistory ?? 32, 'maxHistory', true);
68
+ this.startupTimeoutMs = this.positive(options.startupTimeoutMs ?? 10_000, 'startupTimeoutMs');
69
+ this.stopTimeoutMs = this.positive(options.stopTimeoutMs ?? 1000, 'stopTimeoutMs');
70
+ this.callTimeoutMs = this.positive(options.callTimeoutMs ?? 30_000, 'callTimeoutMs');
71
+ this.capabilities = options.capabilities ?? new CapabilityRegistry();
72
+ this.networking = normalizeNetworking(options.networking);
73
+ if (!isRecord(options.programs) || Object.keys(options.programs).length > RUNTIME_LIMITS.definitions)
74
+ throw new ProgramError('INVALID_CONFIG', 'At most 256 program definitions may be configured');
75
+ if (options.logDirectory !== undefined) {
76
+ if (typeof options.logDirectory !== 'string' || !options.logDirectory || options.logDirectory.includes('\0'))
77
+ throw new ProgramError('INVALID_CONFIG', 'Invalid log directory');
78
+ this.logDirectory = resolve(options.logDirectory);
79
+ }
80
+ if (options.bootstrapDirectory !== undefined && (typeof options.bootstrapDirectory !== 'string' || !options.bootstrapDirectory || options.bootstrapDirectory.includes('\0')))
81
+ throw new ProgramError('INVALID_CONFIG', 'Invalid bootstrap directory');
82
+ for (const [program, definition] of Object.entries(options.programs))
83
+ this.define(program, definition);
84
+ }
85
+ /** Register or replace a trusted definition. Existing generations keep their definition. */
86
+ define(program, definition) {
87
+ this.assertAvailable();
88
+ if (!this.definitions.has(program) && this.definitions.size >= RUNTIME_LIMITS.definitions)
89
+ throw new ProgramError('LIMIT', 'Maximum configured programs reached');
90
+ this.definitions.set(program, normalizeDefinition(program, definition));
91
+ }
92
+ definitionsList() {
93
+ return [...this.definitions].map(([program, definition]) => ({
94
+ program,
95
+ name: definition.name ?? program,
96
+ description: definition.description ?? '',
97
+ command: definitionCommand(definition),
98
+ urls: (definition.urls ?? []).map((resource) => definitionResource(resource)),
99
+ ...(definition.endpoints === undefined ? {} : { endpoints: structuredClone(definition.endpoints) }),
100
+ ...(definition.logsPath === undefined ? {} : { logsPath: definition.logsPath }),
101
+ ...(definition.supervision === undefined ? {} : { supervision: structuredClone(definition.supervision) }),
102
+ }));
103
+ }
104
+ async start(request, signal) {
105
+ this.assertAvailable();
106
+ if (!isRecord(request) || !exactKeys(request, ['program', 'ownerId'], ['input', 'label']))
107
+ throw new ProgramError('INVALID_INPUT', 'Invalid program start request');
108
+ const definition = this.definitions.get(request.program);
109
+ if (!definition)
110
+ throw new ProgramError('UNKNOWN_PROGRAM', `Program is not configured: ${request.program}`);
111
+ if (typeof request.ownerId !== 'string' || !request.ownerId || request.ownerId.length > 1024)
112
+ throw new ProgramError('INVALID_OWNER', 'An opaque nonempty owner id is required');
113
+ this.assertOwner(request.ownerId);
114
+ if (request.label !== undefined && (typeof request.label !== 'string' || request.label.length > RUNTIME_LIMITS.labelLength))
115
+ throw new ProgramError('LIMIT', 'Program label is too long');
116
+ const input = request.input === undefined ? null : request.input;
117
+ assertJson(input);
118
+ if (signal?.aborted)
119
+ throw new ProgramError('ABORTED', 'Program start was cancelled');
120
+ this.assertCapacity();
121
+ const id = randomUUID();
122
+ const run = this.makeRun({ id, generation: 1, program: request.program, ownerId: request.ownerId, label: request.label ?? definition.name ?? request.program, state: 'starting', startedAt: new Date().toISOString(), logs: [], messages: [], resources: [] }, definition, jsonClone(input));
123
+ this.runs.set(id, run);
124
+ this.launch(run);
125
+ const abort = () => this.beginShutdown(run, 'stopped', 'Program start was cancelled');
126
+ signal?.addEventListener('abort', abort, { once: true });
127
+ if (signal?.aborted)
128
+ abort();
129
+ try {
130
+ return await run.hello.promise;
131
+ }
132
+ finally {
133
+ signal?.removeEventListener('abort', abort);
134
+ }
135
+ }
136
+ list(ownerId) {
137
+ return [...this.runs.values()].filter((run) => ownerId === undefined || run.snapshot.ownerId === ownerId).map((run) => this.snapshot(run));
138
+ }
139
+ get(id, ownerId) { return this.snapshot(this.lookup(id, ownerId)); }
140
+ /** Read this live generation's listener requests/reports; does not bind or probe ports. */
141
+ endpointBindings(id, ownerId) {
142
+ return jsonClone(this.endpointRun(id, ownerId).snapshot.endpoints ?? []);
143
+ }
144
+ /** The app reports only after its own OS listener succeeds (dynamic requests use port 0). */
145
+ reportEndpoint(id, ownerId, report) {
146
+ const run = this.endpointRun(id, ownerId);
147
+ if (!isRecord(report) || !exactKeys(report, ['id', 'port']) || !endpointName(report.id) || !Number.isSafeInteger(report.port) || report.port < 1 || report.port > 65535)
148
+ throw new ProgramError('INVALID_INPUT', 'Endpoint report requires a configured id and a bound port (1–65535)');
149
+ const binding = run.snapshot.endpoints?.find((endpoint) => endpoint.id === report.id);
150
+ if (!binding)
151
+ throw new ProgramError('UNKNOWN_ENDPOINT', 'Endpoint is not configured');
152
+ if (binding.requestedPort !== 0 && binding.requestedPort !== report.port)
153
+ throw new ProgramError('INVALID_INPUT', 'Reported endpoint port does not match the configured fixed port');
154
+ binding.port = report.port;
155
+ for (const resource of run.snapshot.resources) {
156
+ if (resource.endpoint !== binding.id)
157
+ continue;
158
+ resource.address = { endpoint: binding.id, host: binding.publicHost, protocol: binding.protocol, port: binding.port, path: resource.path ?? '/' };
159
+ resource.url = '';
160
+ resource.available = true;
161
+ }
162
+ this.changed(run);
163
+ return jsonClone(binding);
164
+ }
165
+ /** Conditional port fencing prevents an old watch worker revoking its successor. */
166
+ unpublishEndpoint(id, ownerId, endpointId, port) {
167
+ const run = this.endpointRun(id, ownerId);
168
+ if (!endpointName(endpointId) || (port !== undefined && (!Number.isSafeInteger(port) || port < 1 || port > 65535)))
169
+ throw new ProgramError('INVALID_INPUT', 'Invalid endpoint id or port');
170
+ const binding = run.snapshot.endpoints?.find((endpoint) => endpoint.id === endpointId);
171
+ if (!binding)
172
+ throw new ProgramError('UNKNOWN_ENDPOINT', 'Endpoint is not configured');
173
+ if (port !== undefined && binding.port !== port)
174
+ return;
175
+ delete binding.port;
176
+ for (const resource of run.snapshot.resources) {
177
+ if (resource.endpoint !== binding.id)
178
+ continue;
179
+ delete resource.address;
180
+ resource.url = '';
181
+ resource.available = false;
182
+ }
183
+ this.changed(run);
184
+ }
185
+ endpointRun(id, ownerId) {
186
+ this.assertAvailable();
187
+ // Unlike optional get/list filters, endpoint authority always requires exact owner scope.
188
+ if (typeof ownerId !== 'string' || !ownerId)
189
+ throw new ProgramError('NOT_FOUND', 'Program not found');
190
+ const run = this.lookup(id, ownerId);
191
+ this.assertOwner(ownerId);
192
+ if (!this.live(run))
193
+ throw new ProgramError('NOT_RUNNING', 'Program generation is no longer active');
194
+ return run;
195
+ }
196
+ async send(id, ownerId, body) {
197
+ this.assertAvailable();
198
+ assertJson(body);
199
+ const run = this.lookup(id, ownerId);
200
+ if (!this.live(run) || !run.helloReceived)
201
+ throw new ProgramError('NOT_RUNNING', 'Program is not running');
202
+ if (run.definition.command)
203
+ throw new ProgramError('UNSUPPORTED_OPERATION', 'Ordinary command programs have no SDK inbox');
204
+ if (run.inbox.size >= RUNTIME_LIMITS.inbox)
205
+ throw new ProgramError('BACKPRESSURE', 'Program inbox is full');
206
+ const sequence = run.nextInboxId + 1;
207
+ // Validation/preflight failures cannot consume a sequence: otherwise the next
208
+ // valid message would look like a missing or reordered inbox frame.
209
+ const message = envelope(run.identity, 'inbox', { sequence, body: jsonClone(body) });
210
+ run.queue.assertWritable();
211
+ run.nextInboxId = sequence;
212
+ const delivery = deferred();
213
+ run.inbox.set(sequence, delivery);
214
+ try {
215
+ await run.queue.send(message);
216
+ // Acknowledgment is generated only when the child consumes the message, bounding both sides.
217
+ this.recordMessage(run, 'parent', body);
218
+ }
219
+ catch (error) {
220
+ run.inbox.delete(sequence);
221
+ delivery.reject(error);
222
+ if (this.live(run))
223
+ this.beginShutdown(run, 'failed', `Inbox transport failed: ${safeError(error)}`);
224
+ throw error;
225
+ }
226
+ }
227
+ async stop(id, ownerId) {
228
+ const run = this.lookup(id, ownerId);
229
+ if (!run.closeProven) {
230
+ this.beginShutdown(run, 'stopped');
231
+ await run.closed.promise;
232
+ }
233
+ return this.snapshot(run);
234
+ }
235
+ async restart(id, ownerId) {
236
+ this.assertAvailable();
237
+ this.assertOwner(ownerId);
238
+ const ownerEpoch = this.ownerEpoch(ownerId);
239
+ const old = this.lookup(id, ownerId);
240
+ if (old.restarting)
241
+ throw new ProgramError('BUSY', 'A restart is already in progress');
242
+ old.restarting = true;
243
+ try {
244
+ if (!old.closeProven) {
245
+ this.beginShutdown(old, 'stopped');
246
+ await old.closed.promise;
247
+ }
248
+ if (!old.closeProven || this.groupExists(old))
249
+ throw new ProgramError('EXIT_UNPROVEN', 'Previous process-group exit could not be proven');
250
+ this.assertAvailable();
251
+ this.assertOwner(ownerId);
252
+ if (ownerEpoch !== this.ownerEpoch(ownerId))
253
+ throw new ProgramError('OWNER_REVOKED', 'The previous owner generation was revoked');
254
+ this.assertCapacity();
255
+ if (!Number.isSafeInteger(old.snapshot.generation + 1))
256
+ throw new ProgramError('LIMIT', 'Program generation limit reached');
257
+ const definition = this.definitions.get(old.snapshot.program);
258
+ const run = this.makeRun({ id, generation: old.snapshot.generation + 1, program: old.snapshot.program, ownerId, label: old.snapshot.label, state: 'starting', startedAt: new Date().toISOString(), logs: [], messages: [], resources: [] }, definition, old.input);
259
+ run.restarting = true;
260
+ this.runs.set(id, run);
261
+ this.launch(run);
262
+ try {
263
+ return await run.hello.promise;
264
+ }
265
+ finally {
266
+ run.restarting = false;
267
+ }
268
+ }
269
+ finally {
270
+ old.restarting = false;
271
+ }
272
+ }
273
+ /** Revoke an owner's authority until explicit host reauthorization; fence old operations. */
274
+ async revokeOwner(ownerId) {
275
+ // Retain bounded tombstones so a revoked session cannot resurrect even after
276
+ // its terminal snapshots have been evicted.
277
+ if (!this.revokedOwners.has(ownerId)) {
278
+ if (!this.ownerEpochs.has(ownerId) && this.ownerEpochs.size >= 4096) {
279
+ // Fail closed rather than forget a revocation or admit unbounded memory.
280
+ await this.dispose();
281
+ throw new ProgramError('LIMIT', 'Owner revocation history exhausted; supervisor disposed');
282
+ }
283
+ this.revokedOwners.add(ownerId);
284
+ const epoch = this.ownerEpoch(ownerId);
285
+ if (!Number.isSafeInteger(epoch + 1)) {
286
+ await this.dispose();
287
+ throw new ProgramError('LIMIT', 'Owner generation limit reached; supervisor disposed');
288
+ }
289
+ this.ownerEpochs.set(ownerId, epoch + 1);
290
+ }
291
+ const runs = [...this.runs.values()].filter((run) => run.snapshot.ownerId === ownerId && !run.closeProven);
292
+ const results = await Promise.allSettled(runs.map(async (run) => { this.beginShutdown(run, 'stopped', 'Program owner was revoked'); await run.closed.promise; }));
293
+ const failed = results.find((result) => result.status === 'rejected');
294
+ if (failed?.status === 'rejected')
295
+ throw failed.reason;
296
+ }
297
+ /** Host-only reauthorization after a replacement owner is independently verified. */
298
+ allowOwner(ownerId) {
299
+ this.assertAvailable();
300
+ if (typeof ownerId !== 'string' || !ownerId || ownerId.length > 1024)
301
+ throw new ProgramError('INVALID_OWNER', 'An opaque nonempty owner id is required');
302
+ if (!this.revokedOwners.has(ownerId))
303
+ return;
304
+ if ([...this.runs.values()].some((run) => run.snapshot.ownerId === ownerId && !run.closeProven))
305
+ throw new ProgramError('BUSY', 'Previous owner processes have not proven closure');
306
+ this.revokedOwners.delete(ownerId);
307
+ // Keep epochs so operations already awaiting old exits cannot regain authority.
308
+ }
309
+ dispose() {
310
+ if (this.disposing)
311
+ return this.disposing;
312
+ this.disposed = true;
313
+ this.disposing = (async () => {
314
+ const runs = [...this.runs.values()];
315
+ const results = await Promise.allSettled(runs.map(async (run) => {
316
+ if (!run.closeProven) {
317
+ this.beginShutdown(run, 'stopped');
318
+ await run.closed.promise;
319
+ }
320
+ }));
321
+ for (const run of runs) {
322
+ if (run.eventTimer)
323
+ clearTimeout(run.eventTimer);
324
+ run.eventTimer = undefined;
325
+ }
326
+ const failed = results.find((result) => result.status === 'rejected');
327
+ if (failed?.status === 'rejected')
328
+ throw failed.reason;
329
+ })();
330
+ return this.disposing;
331
+ }
332
+ positive(value, name, zero = false) {
333
+ if (!Number.isSafeInteger(value) || value < (zero ? 0 : 1))
334
+ throw new ProgramError('INVALID_CONFIG', `${name} must be a ${zero ? 'nonnegative' : 'positive'} integer`);
335
+ return value;
336
+ }
337
+ makeRun(snapshot, definition, input) {
338
+ snapshot.name = definition.name ?? snapshot.program;
339
+ snapshot.description = definition.description ?? '';
340
+ snapshot.command = definitionCommand(definition);
341
+ snapshot.resources = (definition.urls ?? []).map((resource) => definitionResource(resource));
342
+ if (definition.endpoints !== undefined)
343
+ snapshot.endpoints = Object.entries(definition.endpoints).map(([id, endpoint]) => ({ id, bindHost: this.networking.bindHost, requestedPort: endpoint.port === 'dynamic' ? 0 : endpoint.port, publicHost: this.networking.publicHost, protocol: this.networking.protocol }));
344
+ if (definition.logsPath !== undefined)
345
+ snapshot.logsPath = definition.logsPath;
346
+ if (definition.supervision !== undefined)
347
+ snapshot.supervision = structuredClone(definition.supervision);
348
+ return { snapshot, definition, finishing: false, input, identity: { runId: snapshot.id, generation: snapshot.generation, token: randomBytes(24).toString('hex') }, controller: new AbortController(), hello: deferred(), closed: deferred(), helloReceived: false, exitObserved: false, closeObserved: false, closeProven: false, shutdownStarted: false, calls: new Map(), lastCallId: 0, inbox: new Map(), nextInboxId: 0, protocolWindow: Date.now(), protocolCount: 0, parentWindow: Date.now(), parentCount: 0, logBytes: 0, sequence: 0, restarting: false };
349
+ }
350
+ launch(run) {
351
+ try {
352
+ // SDK input framing is validated before any process is created. Commands
353
+ // have no bootstrap, IPC, implicit shell, or readiness protocol.
354
+ const command = run.definition.command;
355
+ const init = command ? undefined : envelope(run.identity, 'init', { input: run.input, callTimeoutMs: this.callTimeoutMs });
356
+ this.logDirectory ??= mkdtempSync(join(tmpdir(), 'dsh-program-agents-'));
357
+ run.logFile = new BoundedRunLog(this.logDirectory, `${run.snapshot.id}-${run.snapshot.generation}.log`, (error) => this.log(run, 'system', `Log capture failed: ${safeError(error)}`));
358
+ run.snapshot.logsPath ??= run.logFile.path;
359
+ const env = {};
360
+ // Deliberately omit credentials, NODE_OPTIONS, and host loader/debug flags.
361
+ // HOME enables package-manager caches for ordinary trusted watched commands.
362
+ const inherited = ['PATH', 'SystemRoot', 'WINDIR', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL', 'TZ', ...(command ? ['HOME'] : [])];
363
+ for (const key of inherited)
364
+ if (process.env[key] !== undefined)
365
+ env[key] = process.env[key];
366
+ Object.assign(env, run.definition.env);
367
+ // Never inherit a previous run's credentials, even from an explicit command environment.
368
+ for (const key of Object.keys(env))
369
+ if (key.startsWith('DSH_PROGRAM_'))
370
+ delete env[key];
371
+ if (this.options.bridge) {
372
+ // Mark before invoking so partial setup failures are also revoked.
373
+ run.bridgeRegistered = true;
374
+ const bridgeEnv = this.options.bridge.environment(this.snapshot(run), structuredClone(run.definition));
375
+ if (!isRecord(bridgeEnv) || Object.keys(bridgeEnv).length > 16 || Object.entries(bridgeEnv).some(([key, value]) => !/^DSH_PROGRAM_[A-Z_]+$/.test(key) || typeof value !== 'string' || value.includes('\0') || value.length > 8192))
376
+ throw new ProgramError('INVALID_CONFIG', 'Invalid program bridge environment');
377
+ Object.assign(env, bridgeEnv);
378
+ }
379
+ // Runtime-owned requests overwrite stale explicit/bridge values. No address is
380
+ // reserved here: the app binds requestedPort (including 0) and reports it.
381
+ env.DSH_PROGRAM_ENDPOINTS_JSON = JSON.stringify(run.snapshot.endpoints ?? []);
382
+ let child;
383
+ if (command) {
384
+ child = spawn(command[0], command.slice(1), { cwd: run.definition.cwd, env, detached: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
385
+ }
386
+ else {
387
+ const directory = this.options.bootstrapDirectory;
388
+ const built = directory === undefined ? fileURLToPath(new URL('./bootstrap.js', import.meta.url)) : resolve(directory, 'bootstrap.js');
389
+ const bootstrap = existsSync(built) ? built : directory === undefined ? fileURLToPath(new URL('./bootstrap.ts', import.meta.url)) : resolve(directory, 'bootstrap.ts');
390
+ // Source execution uses tsx for .js specifiers in our TS sources. Built
391
+ // execution needs native type stripping for TS entries; Node 23 before
392
+ // 23.6 requires the flag even though Node 22.18 enables it by default.
393
+ const execArgv = bootstrap.endsWith('.ts') ? ['--import', import.meta.resolve('tsx')]
394
+ : process.features.typescript ? [] : ['--experimental-strip-types'];
395
+ child = fork(bootstrap, [run.definition.entry, ...(run.definition.args ?? [])], { cwd: run.definition.cwd, env, execArgv, detached: true, stdio: ['ignore', 'pipe', 'pipe', 'ipc'], serialization: 'json' });
396
+ run.queue = new SendQueue(child);
397
+ }
398
+ run.child = child;
399
+ run.snapshot.pid = child.pid;
400
+ this.attachLog(run, child.stdout, 'stdout');
401
+ this.attachLog(run, child.stderr, 'stderr');
402
+ if (command)
403
+ child.once('spawn', () => this.ready(run));
404
+ else
405
+ child.on('message', (message) => this.onMessage(run, message));
406
+ child.on('error', (error) => this.beginShutdown(run, 'failed', safeError(error)));
407
+ child.on('exit', (code, signal) => {
408
+ run.exitObserved = true;
409
+ if (!run.intended) {
410
+ run.intended = command && code === 0 ? 'completed' : 'failed';
411
+ if (run.intended === 'failed')
412
+ run.snapshot.error = `Program exited ${command ? '' : 'before completion '}(${signal ?? `code ${code}`})`;
413
+ }
414
+ this.revoke(run);
415
+ // Natural exits must also terminate descendants, including watcher workers.
416
+ this.signalGroup(run, 'SIGKILL');
417
+ });
418
+ child.on('close', () => {
419
+ run.closeObserved = true;
420
+ if (!run.intended) {
421
+ run.intended = 'failed';
422
+ run.snapshot.error = 'Program closed before startup';
423
+ }
424
+ this.revoke(run);
425
+ run.logClose = run.logFile?.close();
426
+ this.proveClosed(run);
427
+ });
428
+ if (!command) {
429
+ child.on('disconnect', () => {
430
+ if (!run.shutdownStarted && !run.closeObserved)
431
+ this.beginShutdown(run, 'failed', 'Program IPC disconnected before completion');
432
+ });
433
+ run.startupTimer = setTimeout(() => this.beginShutdown(run, 'failed', 'Program startup hello timed out'), this.startupTimeoutMs);
434
+ void run.queue.send(init).catch((error) => this.beginShutdown(run, 'failed', safeError(error)));
435
+ }
436
+ this.changed(run);
437
+ }
438
+ catch (error) {
439
+ if (run.child?.pid) {
440
+ this.beginShutdown(run, 'failed', safeError(error));
441
+ return;
442
+ }
443
+ run.snapshot.error = safeError(error);
444
+ run.intended = 'failed';
445
+ run.closeObserved = true;
446
+ this.finishClosed(run);
447
+ }
448
+ }
449
+ ready(run) {
450
+ if (!this.live(run) || run.helloReceived)
451
+ return;
452
+ run.helloReceived = true;
453
+ clearTimeout(run.startupTimer);
454
+ run.snapshot.state = 'running';
455
+ for (const resource of run.snapshot.resources)
456
+ resource.available = resource.endpoint === undefined || resource.address !== undefined;
457
+ this.changed(run);
458
+ run.hello.resolve(this.snapshot(run));
459
+ }
460
+ attachLog(run, stream, name) {
461
+ if (!stream)
462
+ return;
463
+ const decoder = new StringDecoder('utf8');
464
+ stream.on('data', (buffer) => {
465
+ run.logFile?.append(buffer);
466
+ const text = decoder.write(buffer);
467
+ this.output(run, text, name);
468
+ // Keep only a bounded tail of each incoming chunk, while always draining the pipe.
469
+ this.log(run, name, text);
470
+ });
471
+ stream.on('end', () => {
472
+ const text = decoder.end();
473
+ this.output(run, text, name);
474
+ this.log(run, name, text);
475
+ });
476
+ }
477
+ output(run, text, stream) {
478
+ if (!text || !this.options.onOutput)
479
+ return;
480
+ try {
481
+ this.options.onOutput(this.snapshot(run), text, stream);
482
+ }
483
+ catch { /* Output observers cannot interrupt pipe draining or process cleanup. */ }
484
+ }
485
+ log(run, stream, text) {
486
+ if (!text)
487
+ return;
488
+ let data = Buffer.from(text, 'utf8');
489
+ if (data.length > RUNTIME_LIMITS.logEntryBytes)
490
+ data = data.subarray(data.length - RUNTIME_LIMITS.logEntryBytes);
491
+ const bounded = data.toString('utf8');
492
+ const bytes = Buffer.byteLength(bounded);
493
+ run.snapshot.logs.push({ sequence: ++run.sequence, stream, text: bounded });
494
+ run.logBytes += bytes;
495
+ while (run.snapshot.logs.length > RUNTIME_LIMITS.logs || run.logBytes > RUNTIME_LIMITS.logBytes)
496
+ run.logBytes -= Buffer.byteLength(run.snapshot.logs.shift().text);
497
+ this.changed(run);
498
+ }
499
+ onMessage(run, value) {
500
+ if (!this.current(run) || !isRecord(value)) {
501
+ if (this.current(run) && !run.shutdownStarted)
502
+ this.protocolFailure(run, 'Malformed IPC envelope');
503
+ return;
504
+ }
505
+ try {
506
+ assertJson(value);
507
+ }
508
+ catch (error) {
509
+ this.protocolFailure(run, safeError(error));
510
+ return;
511
+ }
512
+ // Old/fabricated generation messages have no authority, even if their call id is new.
513
+ if (!matches(value, run.identity))
514
+ return;
515
+ if (!this.live(run))
516
+ return;
517
+ const now = Date.now();
518
+ if (now - run.protocolWindow >= 1000) {
519
+ run.protocolWindow = now;
520
+ run.protocolCount = 0;
521
+ }
522
+ if (++run.protocolCount > RUNTIME_LIMITS.protocolMessagesPerSecond) {
523
+ this.protocolFailure(run, 'IPC message rate limit exceeded');
524
+ return;
525
+ }
526
+ if (value.type === 'hello') {
527
+ if (run.helloReceived) {
528
+ this.protocolFailure(run, 'Duplicate startup hello');
529
+ return;
530
+ }
531
+ this.ready(run);
532
+ return;
533
+ }
534
+ if (!run.helloReceived) {
535
+ if (value.type === 'failed')
536
+ this.beginShutdown(run, 'failed', typeof value.error === 'string' ? value.error : 'Program import failed');
537
+ else
538
+ this.protocolFailure(run, 'IPC message before startup hello');
539
+ return;
540
+ }
541
+ if (value.type === 'call') {
542
+ if (!Number.isSafeInteger(value.callId) || value.callId <= run.lastCallId || !validName(value.method) || !('input' in value)) {
543
+ this.protocolFailure(run, 'Invalid or repeated capability call');
544
+ return;
545
+ }
546
+ run.lastCallId = value.callId;
547
+ void this.call(run, value.callId, value.method, value.input);
548
+ }
549
+ else if (value.type === 'cancel') {
550
+ if (!Number.isSafeInteger(value.callId)) {
551
+ this.protocolFailure(run, 'Invalid cancellation id');
552
+ return;
553
+ }
554
+ const pending = run.calls.get(value.callId);
555
+ if (pending) {
556
+ pending.controller.abort(new ProgramError('CANCELLED', 'Child cancelled capability call'));
557
+ clearTimeout(pending.timer);
558
+ }
559
+ }
560
+ else if (value.type === 'inboxAck') {
561
+ if (!Number.isSafeInteger(value.sequence) || !run.inbox.has(value.sequence)) {
562
+ this.protocolFailure(run, 'Invalid inbox acknowledgment');
563
+ return;
564
+ }
565
+ run.inbox.get(value.sequence).resolve();
566
+ run.inbox.delete(value.sequence);
567
+ }
568
+ else if (value.type === 'complete') {
569
+ if (!('result' in value)) {
570
+ this.protocolFailure(run, 'Missing completion result');
571
+ return;
572
+ }
573
+ run.snapshot.result = jsonClone(value.result);
574
+ this.beginShutdown(run, 'completed');
575
+ }
576
+ else if (value.type === 'failed') {
577
+ this.beginShutdown(run, 'failed', typeof value.error === 'string' ? value.error : 'Program failed');
578
+ }
579
+ else
580
+ this.protocolFailure(run, 'Unknown IPC message type');
581
+ }
582
+ async call(run, callId, method, input) {
583
+ if (run.calls.size >= RUNTIME_LIMITS.pendingCalls || this.inflightHandlers >= RUNTIME_LIMITS.pendingCalls) {
584
+ await this.reply(run, callId, false, 'Capability concurrency limit exceeded', 'LIMIT');
585
+ return;
586
+ }
587
+ this.inflightHandlers++;
588
+ const controller = new AbortController();
589
+ let timedOut = false;
590
+ const timer = setTimeout(() => {
591
+ timedOut = true;
592
+ controller.abort(new ProgramError('TIMEOUT', 'Capability deadline exceeded; effect outcome may be unknown'));
593
+ // Retain the slot until the host handler settles: cancellation is cooperative,
594
+ // and a handler ignoring AbortSignal must not allow unbounded replacement calls.
595
+ void this.reply(run, callId, false, 'Capability deadline exceeded; effect outcome may be unknown', 'TIMEOUT');
596
+ }, this.callTimeoutMs);
597
+ const pending = { controller, timer };
598
+ run.calls.set(callId, pending);
599
+ try {
600
+ const output = await this.invoke(run, method, input, controller.signal);
601
+ assertJson(output);
602
+ // Include framing overhead in result validation. Oversized host results are
603
+ // a call error, not a reason to kill an otherwise well-behaved program.
604
+ envelope(run.identity, 'response', { callId, ok: true, result: output });
605
+ if (run.calls.get(callId) === pending && !controller.signal.aborted && this.live(run))
606
+ await this.reply(run, callId, true, output);
607
+ }
608
+ catch (error) {
609
+ if (!timedOut && run.calls.get(callId) === pending && !controller.signal.aborted && this.live(run))
610
+ await this.reply(run, callId, false, safeError(error), error instanceof ProgramError ? error.code : 'CAPABILITY_FAILED');
611
+ }
612
+ finally {
613
+ this.inflightHandlers--;
614
+ clearTimeout(timer);
615
+ if (run.calls.get(callId) === pending)
616
+ run.calls.delete(callId);
617
+ }
618
+ }
619
+ async invoke(run, method, input, signal) {
620
+ if (!this.live(run) || signal.aborted)
621
+ throw new ProgramError('CANCELLED', 'Generation is no longer active');
622
+ if (method === 'capabilities.list') {
623
+ if (input !== null)
624
+ throw new ProgramError('INVALID_INPUT', 'Capability list takes null input');
625
+ const allowed = new Set(run.definition.allowedCapabilities);
626
+ const metadata = this.capabilities.list().filter((entry) => allowed.has(entry.name));
627
+ assertJson(metadata);
628
+ return metadata;
629
+ }
630
+ if (method === 'status.set') {
631
+ if (!isRecord(input) || !exactKeys(input, ['state'], ['detail']) || (input.state !== 'running' && input.state !== 'blocked') || (input.detail !== undefined && (typeof input.detail !== 'string' || input.detail.length > RUNTIME_LIMITS.statusLength)))
632
+ throw new ProgramError('INVALID_INPUT', 'Invalid status');
633
+ run.snapshot.state = input.state;
634
+ if (typeof input.detail === 'string')
635
+ run.snapshot.status = input.detail;
636
+ else
637
+ delete run.snapshot.status;
638
+ this.changed(run);
639
+ return null;
640
+ }
641
+ if (method === 'parent.send') {
642
+ const now = Date.now();
643
+ if (now - run.parentWindow >= 1000) {
644
+ run.parentWindow = now;
645
+ run.parentCount = 0;
646
+ }
647
+ if (++run.parentCount > RUNTIME_LIMITS.parentMessagesPerSecond)
648
+ throw new ProgramError('RATE_LIMIT', 'Parent notification rate limit exceeded');
649
+ if (!this.options.onParentMessage)
650
+ throw new ProgramError('UNAVAILABLE', 'Parent messaging is not configured');
651
+ await this.options.onParentMessage(run.snapshot.ownerId, jsonClone(input), this.snapshot(run), signal);
652
+ if (!this.live(run) || signal.aborted)
653
+ throw new ProgramError('CANCELLED', 'Generation ended while notifying parent; delivery outcome may be unknown');
654
+ this.recordMessage(run, 'program', input);
655
+ return null;
656
+ }
657
+ if (method === 'resources.publish') {
658
+ const resource = normalizeResource(input, true);
659
+ if (run.definition.urls?.some((configured) => configured.id === resource.id && configured.endpoint !== undefined))
660
+ throw new ProgramError('FORBIDDEN', 'Configured endpoint resources are published only through their endpoint binding');
661
+ const old = run.snapshot.resources.findIndex((other) => other.id === resource.id);
662
+ if (old === -1 && run.snapshot.resources.length >= RUNTIME_LIMITS.resources)
663
+ throw new ProgramError('LIMIT', 'Resource limit exceeded');
664
+ if (old === -1)
665
+ run.snapshot.resources.push(resource);
666
+ else
667
+ run.snapshot.resources[old] = resource;
668
+ this.changed(run);
669
+ return null;
670
+ }
671
+ if (method === 'resources.unpublish') {
672
+ if (!isRecord(input) || !exactKeys(input, ['id']) || !validName(input.id))
673
+ throw new ProgramError('INVALID_INPUT', 'Invalid resource id');
674
+ if (run.definition.urls?.some((configured) => configured.id === input.id && configured.endpoint !== undefined))
675
+ throw new ProgramError('FORBIDDEN', 'Configured endpoint resources are unpublished only through their endpoint binding');
676
+ run.snapshot.resources = run.snapshot.resources.filter((resource) => resource.id !== input.id);
677
+ this.changed(run);
678
+ return null;
679
+ }
680
+ if (!run.definition.allowedCapabilities?.includes(method))
681
+ throw new ProgramError('FORBIDDEN', `Capability is not allowlisted: ${method}`);
682
+ const capability = this.capabilities.get(method);
683
+ if (!capability)
684
+ throw new ProgramError('UNKNOWN_CAPABILITY', `Capability is not registered: ${method}`);
685
+ return capability.handler(jsonClone(input), { run: this.snapshot(run), signal });
686
+ }
687
+ async reply(run, callId, ok, value, code) {
688
+ if (!this.live(run))
689
+ return;
690
+ try {
691
+ await run.queue.send(envelope(run.identity, 'response', ok ? { callId, ok, result: value } : { callId, ok, error: value, code: code ?? 'CAPABILITY_FAILED' }));
692
+ }
693
+ catch (error) {
694
+ if (this.live(run))
695
+ this.beginShutdown(run, 'failed', safeError(error));
696
+ }
697
+ }
698
+ beginShutdown(run, intended, error) {
699
+ if (run.closeProven || run.shutdownStarted)
700
+ return;
701
+ run.shutdownStarted = true;
702
+ run.intended = intended;
703
+ if (error)
704
+ run.snapshot.error = error.slice(0, RUNTIME_LIMITS.statusLength);
705
+ run.snapshot.state = 'stopping';
706
+ clearTimeout(run.startupTimer);
707
+ run.hello.reject(new ProgramError(intended === 'stopped' ? 'ABORTED' : 'STARTUP_FAILED', error ?? 'Program stopped before startup'));
708
+ this.revoke(run);
709
+ this.changed(run);
710
+ if (!run.child?.pid) {
711
+ // A failed fork can emit close asynchronously; no process id means no group was spawned.
712
+ run.closeObserved = true;
713
+ this.finishClosed(run);
714
+ return;
715
+ }
716
+ if (run.queue)
717
+ void run.queue.send(envelope(run.identity, 'cancelRun')).catch(() => { });
718
+ this.signalGroup(run, 'SIGTERM');
719
+ run.stopTimer = setTimeout(() => this.signalGroup(run, 'SIGKILL'), this.stopTimeoutMs);
720
+ run.closeTimer = setTimeout(() => {
721
+ if (run.closeProven)
722
+ return;
723
+ this.signalGroup(run, 'SIGKILL');
724
+ run.snapshot.state = 'failed';
725
+ run.snapshot.error = 'Process-group exit could not be proven; restart is disabled';
726
+ this.changed(run);
727
+ run.closed.reject(new ProgramError('EXIT_UNPROVEN', run.snapshot.error));
728
+ }, this.stopTimeoutMs * 2 + 1000);
729
+ }
730
+ revoke(run) {
731
+ if (run.bridgeRegistered) {
732
+ run.bridgeRegistered = false;
733
+ try {
734
+ this.options.bridge?.revoke(this.snapshot(run));
735
+ }
736
+ catch { /* Revocation observers cannot obstruct mandatory process cleanup. */ }
737
+ }
738
+ if (!run.controller.signal.aborted)
739
+ run.controller.abort(new ProgramError('CANCELLED', 'Program generation ended'));
740
+ for (const pending of run.calls.values()) {
741
+ clearTimeout(pending.timer);
742
+ pending.controller.abort(new ProgramError('CANCELLED', 'Program generation ended; effect outcome may be unknown'));
743
+ }
744
+ run.calls.clear();
745
+ for (const delivery of run.inbox.values())
746
+ delivery.reject(new ProgramError('DISCONNECTED', 'Program inbox closed'));
747
+ run.inbox.clear();
748
+ for (const binding of run.snapshot.endpoints ?? [])
749
+ delete binding.port;
750
+ for (const resource of run.snapshot.resources) {
751
+ resource.available = false;
752
+ delete resource.address;
753
+ if (resource.endpoint !== undefined)
754
+ resource.url = '';
755
+ }
756
+ }
757
+ proveClosed(run) {
758
+ if (run.closeProven)
759
+ return;
760
+ if (!this.groupExists(run)) {
761
+ this.finishClosed(run);
762
+ return;
763
+ }
764
+ this.signalGroup(run, 'SIGKILL');
765
+ const deadline = Date.now() + this.stopTimeoutMs + 1000;
766
+ const check = () => {
767
+ if (run.closeProven)
768
+ return;
769
+ if (!this.groupExists(run)) {
770
+ this.finishClosed(run);
771
+ return;
772
+ }
773
+ if (Date.now() >= deadline) {
774
+ run.snapshot.state = 'failed';
775
+ run.snapshot.error = 'Process-group exit could not be proven; restart is disabled';
776
+ this.changed(run);
777
+ run.closed.reject(new ProgramError('EXIT_UNPROVEN', run.snapshot.error));
778
+ return;
779
+ }
780
+ setTimeout(check, 25);
781
+ };
782
+ setTimeout(check, 25);
783
+ }
784
+ finishClosed(run) {
785
+ if (run.closeProven || run.finishing)
786
+ return;
787
+ run.finishing = true;
788
+ clearTimeout(run.startupTimer);
789
+ clearTimeout(run.stopTimer);
790
+ clearTimeout(run.closeTimer);
791
+ run.queue?.close();
792
+ this.revoke(run);
793
+ run.logClose ??= run.logFile?.close() ?? Promise.resolve();
794
+ void run.logClose.then(() => {
795
+ run.closeProven = true;
796
+ run.closeObserved = true;
797
+ run.snapshot.state = run.intended ?? 'failed';
798
+ run.snapshot.endedAt = new Date().toISOString();
799
+ run.hello.reject(new ProgramError('STARTUP_FAILED', run.snapshot.error ?? 'Program closed before startup hello'));
800
+ this.changed(run);
801
+ run.closed.resolve();
802
+ this.prune();
803
+ });
804
+ }
805
+ groupExists(run) {
806
+ if (!run.child?.pid)
807
+ return false;
808
+ if (process.platform === 'win32')
809
+ return !run.exitObserved && !run.closeObserved;
810
+ try {
811
+ process.kill(-run.child.pid, 0);
812
+ return true;
813
+ }
814
+ catch (error) {
815
+ return error.code !== 'ESRCH';
816
+ }
817
+ }
818
+ signalGroup(run, signal) {
819
+ if (!run.child?.pid)
820
+ return;
821
+ try {
822
+ if (process.platform === 'win32')
823
+ run.child.kill(signal);
824
+ else
825
+ process.kill(-run.child.pid, signal);
826
+ }
827
+ catch (error) {
828
+ if (error.code !== 'ESRCH')
829
+ this.log(run, 'system', `Unable to signal process group: ${safeError(error)}`);
830
+ }
831
+ }
832
+ recordMessage(run, direction, body) {
833
+ run.snapshot.messages.push({ direction, body: jsonClone(body), at: new Date().toISOString() });
834
+ while (run.snapshot.messages.length > RUNTIME_LIMITS.messages)
835
+ run.snapshot.messages.shift();
836
+ this.changed(run);
837
+ }
838
+ changed(run) {
839
+ if (!this.options.onEvent || run.eventTimer || this.disposed)
840
+ return;
841
+ run.eventTimer = setTimeout(() => {
842
+ run.eventTimer = undefined;
843
+ if (!this.current(run) || this.disposed)
844
+ return;
845
+ try {
846
+ this.options.onEvent?.({ type: 'changed', run: this.snapshot(run) });
847
+ }
848
+ catch { /* Observers cannot crash or control program supervision. */ }
849
+ }, 25);
850
+ run.eventTimer.unref();
851
+ }
852
+ protocolFailure(run, message) { this.beginShutdown(run, 'failed', `Invalid IPC: ${message}`); }
853
+ current(run) { return this.runs.get(run.snapshot.id) === run; }
854
+ live(run) { return this.current(run) && !run.shutdownStarted && !run.controller.signal.aborted && !terminalStates.has(run.snapshot.state); }
855
+ snapshot(run) { return jsonClone(run.snapshot); }
856
+ lookup(id, ownerId) {
857
+ const run = this.runs.get(id);
858
+ if (!run || (ownerId !== undefined && run.snapshot.ownerId !== ownerId))
859
+ throw new ProgramError('NOT_FOUND', 'Program not found');
860
+ return run;
861
+ }
862
+ ownerEpoch(ownerId) { return this.ownerEpochs.get(ownerId) ?? 0; }
863
+ assertOwner(ownerId) { if (this.revokedOwners.has(ownerId))
864
+ throw new ProgramError('OWNER_REVOKED', 'Program owner is no longer live'); }
865
+ assertAvailable() { if (this.disposed)
866
+ throw new ProgramError('DISPOSED', 'Program supervisor is disposed'); }
867
+ assertCapacity() {
868
+ if ([...this.runs.values()].filter((run) => !run.closeProven).length >= this.maxRuns)
869
+ throw new ProgramError('LIMIT', 'Maximum concurrent programs reached');
870
+ }
871
+ prune() {
872
+ const history = [...this.runs.values()].filter((run) => run.closeProven && !run.restarting);
873
+ while (history.length > this.maxHistory) {
874
+ const old = history.shift();
875
+ this.runs.delete(old.snapshot.id);
876
+ if (old.eventTimer)
877
+ clearTimeout(old.eventTimer);
878
+ }
879
+ }
880
+ }
881
+ //# sourceMappingURL=supervisor.js.map