preflite 1.0.1

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 (138) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +144 -0
  3. package/dist/adapter-spi/artifact/index.js +1 -0
  4. package/dist/adapter-spi/command/index.js +1 -0
  5. package/dist/adapter-spi/install/index.js +1 -0
  6. package/dist/adapter-spi/lifecycle/index.js +1 -0
  7. package/dist/adapter-spi/resource/index.js +1 -0
  8. package/dist/adapter-spi/snapshot/index.js +1 -0
  9. package/dist/application/agent/AgentCommandPollLoop.js +48 -0
  10. package/dist/application/agent/AgentRuntimeService.js +27 -0
  11. package/dist/application/app-package/AppPackageApplicationService.js +97 -0
  12. package/dist/application/artifact/ArtifactApplicationService.js +13 -0
  13. package/dist/application/debug/DebugApplicationService.js +117 -0
  14. package/dist/application/health/HealthMetricsService.js +47 -0
  15. package/dist/application/lease/LeaseApplicationService.js +79 -0
  16. package/dist/application/query/ObservationQueryService.js +48 -0
  17. package/dist/application/reporter/ReporterApplicationService.js +41 -0
  18. package/dist/application/resource/ResourceOccupationReleaseService.js +49 -0
  19. package/dist/application/resource/ResourceRegistryService.js +113 -0
  20. package/dist/application/session/SessionApplicationService.js +39 -0
  21. package/dist/application/task/TaskApplicationService.js +378 -0
  22. package/dist/client/agentAppPackageClient.js +91 -0
  23. package/dist/domain/agent/AgentNode.js +12 -0
  24. package/dist/domain/agent/AgentRuntime.js +6 -0
  25. package/dist/domain/artifact/ArtifactPipeline.js +6 -0
  26. package/dist/domain/artifact/ArtifactRef.js +12 -0
  27. package/dist/domain/event/AgentEvent.js +10 -0
  28. package/dist/domain/event/Reporter.js +6 -0
  29. package/dist/domain/health/HealthMetrics.js +8 -0
  30. package/dist/domain/lease/Lease.js +28 -0
  31. package/dist/domain/lease/LeaseManager.js +6 -0
  32. package/dist/domain/repositories/index.js +1 -0
  33. package/dist/domain/resource/DeviceDetails.js +23 -0
  34. package/dist/domain/resource/DeviceResource.js +16 -0
  35. package/dist/domain/resource/ResourceRegistry.js +6 -0
  36. package/dist/domain/runtime/interfaces.js +1 -0
  37. package/dist/domain/session/BaseSession.js +16 -0
  38. package/dist/domain/session/DebugSession.js +3 -0
  39. package/dist/domain/session/ExecutionSession.js +3 -0
  40. package/dist/domain/session/SessionManager.js +8 -0
  41. package/dist/domain/task/TaskRecord.js +14 -0
  42. package/dist/domain/task/TaskSpec.js +12 -0
  43. package/dist/infrastructure/adapters/AdapterRegistry.js +10 -0
  44. package/dist/infrastructure/adapters/BridgeAdapters.js +6 -0
  45. package/dist/infrastructure/adapters/android/AndroidResourceAdapter.js +51 -0
  46. package/dist/infrastructure/adapters/deviceDetailsProbe.js +229 -0
  47. package/dist/infrastructure/adapters/harmony/HarmonyResourceAdapter.js +40 -0
  48. package/dist/infrastructure/adapters/ios/IOSResourceAdapter.js +182 -0
  49. package/dist/infrastructure/adapters/real/ShellCommandAndSnapshot.js +41 -0
  50. package/dist/infrastructure/airtest/AirtestRuntime.js +168 -0
  51. package/dist/infrastructure/app-package/AppPackageUrlCache.js +191 -0
  52. package/dist/infrastructure/app-package/appPackageDownloadDir.js +13 -0
  53. package/dist/infrastructure/bootstrap/BuildRuntimeContext.js +88 -0
  54. package/dist/infrastructure/cache/DirCapacityWatchdog.js +150 -0
  55. package/dist/infrastructure/config/agentConfigFile.js +96 -0
  56. package/dist/infrastructure/device/DeviceAppPackageOps.js +146 -0
  57. package/dist/infrastructure/ios/IOSWdaWatchdog.js +207 -0
  58. package/dist/infrastructure/live-debug/LiveDebugSessionManager.js +74 -0
  59. package/dist/infrastructure/live-debug/RuntimeLiveDebugAdapters.js +19 -0
  60. package/dist/infrastructure/midscene/DebugRuntimeImpl.js +533 -0
  61. package/dist/infrastructure/midscene/MidsceneRuntimeMock.js +22 -0
  62. package/dist/infrastructure/midscene/MidsceneRuntimeReal.js +552 -0
  63. package/dist/infrastructure/midscene/executionDumpWatcher.js +219 -0
  64. package/dist/infrastructure/midscene/videoRecorder.js +365 -0
  65. package/dist/infrastructure/midscene/zipReportDir.js +36 -0
  66. package/dist/infrastructure/persistence/InMemoryRepositories.js +94 -0
  67. package/dist/infrastructure/resilience/DeliveryIdDeduper.js +26 -0
  68. package/dist/infrastructure/system/CommandRunner.js +128 -0
  69. package/dist/infrastructure/transport/http/AgentEventHttpIngestClient.js +52 -0
  70. package/dist/infrastructure/transport/http/CallbackOutboxStore.js +106 -0
  71. package/dist/infrastructure/transport/http/PlatformCallbackClient.js +113 -0
  72. package/dist/infrastructure/transport/http/PlatformCommandPollClient.js +89 -0
  73. package/dist/infrastructure/transport/http/ResilientPlatformCallbackClient.js +117 -0
  74. package/dist/infrastructure/transport/midscenePaths.js +28 -0
  75. package/dist/infrastructure/transport/ws/ResilientWsOrHttpEventPublisher.js +29 -0
  76. package/dist/infrastructure/transport/ws/WsClient.js +182 -0
  77. package/dist/infrastructure/transport/ws/WsEventPublisher.js +36 -0
  78. package/dist/interfaces/http/HttpServer.js +227 -0
  79. package/dist/interfaces/websocket/AgentWsGateway.js +227 -0
  80. package/dist/main.js +368 -0
  81. package/dist/mcp/agentHttpClient.js +82 -0
  82. package/dist/mcp/agentRuntime.js +184 -0
  83. package/dist/mcp/cli.js +36 -0
  84. package/dist/mcp/doctor.js +124 -0
  85. package/dist/mcp/evidence.js +57 -0
  86. package/dist/mcp/exploration/index.js +129 -0
  87. package/dist/mcp/exploration/sessionManager.js +122 -0
  88. package/dist/mcp/exploration/tools-atomic.js +34 -0
  89. package/dist/mcp/exploration/tools-intelligent.js +33 -0
  90. package/dist/mcp/exploration/tools-session.js +276 -0
  91. package/dist/mcp/exploration/types.js +1 -0
  92. package/dist/mcp/flowStepEvents.js +114 -0
  93. package/dist/mcp/liveViewer.js +156 -0
  94. package/dist/mcp/reportReader.js +157 -0
  95. package/dist/mcp/runManager.js +161 -0
  96. package/dist/mcp/runSummary.js +72 -0
  97. package/dist/mcp/runtimeInstall.js +44 -0
  98. package/dist/mcp/server.js +260 -0
  99. package/dist/mcp/setup.js +185 -0
  100. package/dist/mcp/types.js +1 -0
  101. package/dist/mcp/userConfig.js +45 -0
  102. package/dist/mcp/visual-flow/codegen.js +576 -0
  103. package/dist/mcp/visual-flow/index.js +14 -0
  104. package/dist/mcp/visual-flow/types.js +5 -0
  105. package/dist/mcp/visual-flow/validate.js +617 -0
  106. package/dist/protocol-contracts/commands/envelope.js +24 -0
  107. package/dist/protocol-contracts/commands/index.js +1 -0
  108. package/dist/protocol-contracts/dto/index.js +1 -0
  109. package/dist/protocol-contracts/events/index.js +1 -0
  110. package/dist/protocol-contracts/queries/index.js +1 -0
  111. package/dist/shared-kernel/enums/index.js +70 -0
  112. package/dist/shared-kernel/errors/index.js +21 -0
  113. package/dist/shared-kernel/ids/index.js +18 -0
  114. package/dist/shared-kernel/time/index.js +3 -0
  115. package/dist/shared-kernel/value-objects/index.js +1 -0
  116. package/dist/utils/appPackageLocalPath.js +75 -0
  117. package/dist/utils/deviceResourceRouting.js +24 -0
  118. package/dist/utils/harmonyAgentDebugDevice.js +60 -0
  119. package/dist/utils/harmonyHdcDeviceId.js +134 -0
  120. package/dist/utils/iosAgentDebugDevice.js +72 -0
  121. package/dist/utils/iosMjpegCapture.js +90 -0
  122. package/dist/utils/liveDebugForegroundParse.js +71 -0
  123. package/dist/utils/midscene-device-session.js +353 -0
  124. package/dist/utils/midscene-task-cache-env.js +15 -0
  125. package/dist/utils/midsceneReportConstants.js +49 -0
  126. package/dist/utils/seedMidsceneTaskCache.js +61 -0
  127. package/dist/utils/task-runners/context/androidTaskRunnerContext.js +1 -0
  128. package/dist/utils/task-runners/context/harmonyTaskRunnerContext.js +1 -0
  129. package/dist/utils/task-runners/context/iosTaskRunnerContext.js +1 -0
  130. package/dist/utils/task-runners/runAndroidNativeAppTask.js +29 -0
  131. package/dist/utils/task-runners/runHarmonyNativeAppTask.js +36 -0
  132. package/dist/utils/task-runners/runIosNativeAppTask.js +30 -0
  133. package/dist/utils/task-runners/taskAppPackage.js +20 -0
  134. package/dist/utils/wrapper/resolveTaskRunnerImport.js +11 -0
  135. package/dist/utils/wrapper/wrapAndroidTaskScript.js +38 -0
  136. package/dist/utils/wrapper/wrapHarmonyTaskScript.js +42 -0
  137. package/dist/utils/wrapper/wrapIosTaskScript.js +30 -0
  138. package/package.json +46 -0
@@ -0,0 +1,117 @@
1
+ import { isCallbackNotFoundError, } from "./PlatformCallbackClient.js";
2
+ /**
3
+ * HTTP 回调:主地址失败时可尝试备用地址;仍失败则落盘,可在网络恢复后 {@link flushOutbox} 重放。
4
+ */
5
+ export class ResilientPlatformCallbackClient {
6
+ inner;
7
+ fallback;
8
+ outbox;
9
+ constructor(inner, fallback, outbox) {
10
+ this.inner = inner;
11
+ this.fallback = fallback;
12
+ this.outbox = outbox;
13
+ }
14
+ async flushOutbox() {
15
+ await this.outbox.processWith(this.inner, 100);
16
+ }
17
+ async pushTaskStatus(taskId, payload) {
18
+ let primaryErr;
19
+ try {
20
+ await this.inner.pushTaskStatus(taskId, payload);
21
+ return;
22
+ }
23
+ catch (e) {
24
+ primaryErr = e;
25
+ }
26
+ if (this.fallback) {
27
+ try {
28
+ await this.fallback.pushTaskStatus(taskId, payload);
29
+ return;
30
+ }
31
+ catch (e) {
32
+ if (isCallbackNotFoundError(primaryErr) && isCallbackNotFoundError(e)) {
33
+ console.warn(`[ResilientPlatformCallbackClient] callback 404 on all URLs; skip outbox (taskId=${taskId})`);
34
+ return;
35
+ }
36
+ await this.outbox.enqueue({ kind: "status", taskId, payload });
37
+ return;
38
+ }
39
+ }
40
+ if (isCallbackNotFoundError(primaryErr)) {
41
+ console.warn(`[ResilientPlatformCallbackClient] callback 404; skip outbox (taskId=${taskId})`);
42
+ return;
43
+ }
44
+ await this.outbox.enqueue({ kind: "status", taskId, payload });
45
+ }
46
+ async pushTaskLog(taskId, payload) {
47
+ let primaryErr;
48
+ try {
49
+ await this.inner.pushTaskLog(taskId, payload);
50
+ return;
51
+ }
52
+ catch (e) {
53
+ primaryErr = e;
54
+ }
55
+ if (this.fallback) {
56
+ try {
57
+ await this.fallback.pushTaskLog(taskId, payload);
58
+ return;
59
+ }
60
+ catch (e) {
61
+ if (isCallbackNotFoundError(primaryErr) && isCallbackNotFoundError(e)) {
62
+ console.warn(`[ResilientPlatformCallbackClient] callback 404 on all URLs; skip outbox (taskId=${taskId})`);
63
+ return;
64
+ }
65
+ await this.outbox.enqueue({ kind: "log", taskId, payload });
66
+ return;
67
+ }
68
+ }
69
+ if (isCallbackNotFoundError(primaryErr)) {
70
+ console.warn(`[ResilientPlatformCallbackClient] callback 404; skip outbox (taskId=${taskId})`);
71
+ return;
72
+ }
73
+ await this.outbox.enqueue({ kind: "log", taskId, payload });
74
+ }
75
+ async pushTaskReport(taskId, payload) {
76
+ let primaryErr;
77
+ try {
78
+ await this.inner.pushTaskReport(taskId, payload);
79
+ return;
80
+ }
81
+ catch (e) {
82
+ primaryErr = e;
83
+ }
84
+ const b64 = payload.reportBundleBase64;
85
+ if (b64 && b64.length > 4_000_000) {
86
+ console.warn("[ResilientPlatformCallbackClient] report too large to enqueue; drop outbox (taskId=" + taskId + ")");
87
+ return;
88
+ }
89
+ const dumpLen = payload.executionDumpJson?.length ?? 0;
90
+ const assetsLen = payload.reportAssetFiles?.reduce((acc, f) => acc + (typeof f.base64 === "string" ? f.base64.length : 0), 0) ?? 0;
91
+ if (dumpLen + assetsLen > 8_000_000) {
92
+ console.warn("[ResilientPlatformCallbackClient] execution dump payload too large to enqueue; drop outbox (taskId=" +
93
+ taskId +
94
+ ")");
95
+ return;
96
+ }
97
+ if (this.fallback) {
98
+ try {
99
+ await this.fallback.pushTaskReport(taskId, payload);
100
+ return;
101
+ }
102
+ catch (e) {
103
+ if (isCallbackNotFoundError(primaryErr) && isCallbackNotFoundError(e)) {
104
+ console.warn(`[ResilientPlatformCallbackClient] callback 404 on all URLs; skip outbox (taskId=${taskId})`);
105
+ return;
106
+ }
107
+ await this.outbox.enqueue({ kind: "report", taskId, payload });
108
+ return;
109
+ }
110
+ }
111
+ if (isCallbackNotFoundError(primaryErr)) {
112
+ console.warn(`[ResilientPlatformCallbackClient] callback 404; skip outbox (taskId=${taskId})`);
113
+ return;
114
+ }
115
+ await this.outbox.enqueue({ kind: "report", taskId, payload });
116
+ }
117
+ }
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+ export function resolveMidsceneRunDir(cwd, env = process.env) {
3
+ const base = env.MIDSCENE_RUN_DIR?.trim() || "midscene_run";
4
+ if (path.isAbsolute(base))
5
+ return base;
6
+ const agentHome = env.AGENT_HOME?.trim();
7
+ if (agentHome)
8
+ return path.resolve(agentHome, base);
9
+ return path.resolve(cwd, base);
10
+ }
11
+ /** 与 @midscene/shared 默认一致:run dir 下 `report`。 */
12
+ export function getMidsceneReportRootDir(cwd, env = process.env) {
13
+ return path.join(resolveMidsceneRunDir(cwd, env), "report");
14
+ }
15
+ export function resolveTaskReportFilePaths(reportRoot, reportStem, outputFormat) {
16
+ if (outputFormat === "html-and-external-assets") {
17
+ const bundleDir = path.join(reportRoot, reportStem);
18
+ return {
19
+ reportName: reportStem,
20
+ reportHtmlPath: path.join(bundleDir, "index.html"),
21
+ bundleDir,
22
+ };
23
+ }
24
+ return {
25
+ reportName: `${reportStem}.html`,
26
+ reportHtmlPath: path.join(reportRoot, `${reportStem}.html`),
27
+ };
28
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * WS 连通时仅走 WS;断开且配置了 HTTP ingest 时优先 POST,失败再回落到 WS 待发队列。
3
+ */
4
+ export class ResilientWsOrHttpEventPublisher {
5
+ wsClient;
6
+ inner;
7
+ httpIngest;
8
+ constructor(wsClient, inner, httpIngest) {
9
+ this.wsClient = wsClient;
10
+ this.inner = inner;
11
+ this.httpIngest = httpIngest;
12
+ }
13
+ async publish(event) {
14
+ if (this.httpIngest.isEnabled() && !this.wsClient.isOpen()) {
15
+ try {
16
+ await this.httpIngest.postEvent(event);
17
+ return;
18
+ }
19
+ catch (error) {
20
+ const msg = error instanceof Error ? error.message : String(error);
21
+ console.warn(`[ResilientWsOrHttpEventPublisher] HTTP ingest failed: ${msg}; buffering via WS`);
22
+ }
23
+ }
24
+ await this.inner.publish(event);
25
+ }
26
+ async publishLiveDebugFrame(event, frame) {
27
+ await this.inner.publishLiveDebugFrame(event, frame);
28
+ }
29
+ }
@@ -0,0 +1,182 @@
1
+ import WebSocket from "ws";
2
+ function isBrokenPipeError(error) {
3
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
4
+ return code === "EPIPE" || code === "ECONNRESET" || code === "EHOSTUNREACH" || code === "ETIMEDOUT";
5
+ }
6
+ export class WsClient {
7
+ options;
8
+ socket = null;
9
+ heartbeatTimer = null;
10
+ reconnectTimer = null;
11
+ messageHandlers = [];
12
+ pendingSends = [];
13
+ reconnectAttempt = 0;
14
+ state = "disconnected";
15
+ constructor(options) {
16
+ this.options = options;
17
+ }
18
+ getConnectionState() {
19
+ return this.state;
20
+ }
21
+ isOpen() {
22
+ return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
23
+ }
24
+ getPendingSendDepth() {
25
+ return this.pendingSends.length;
26
+ }
27
+ getReconnectAttempt() {
28
+ return this.reconnectAttempt;
29
+ }
30
+ setState(next) {
31
+ if (this.state === next)
32
+ return;
33
+ this.state = next;
34
+ this.options.onConnectionState?.(next);
35
+ }
36
+ onMessage(handler) {
37
+ this.messageHandlers.push(handler);
38
+ }
39
+ connect() {
40
+ if (this.reconnectTimer) {
41
+ clearTimeout(this.reconnectTimer);
42
+ this.reconnectTimer = null;
43
+ }
44
+ if (this.socket) {
45
+ try {
46
+ this.socket.removeAllListeners();
47
+ this.socket.close();
48
+ }
49
+ catch {
50
+ /* ignore */
51
+ }
52
+ this.socket = null;
53
+ }
54
+ this.setState("connecting");
55
+ this.socket = new WebSocket(this.options.endpoint, {
56
+ ...(this.options.authToken
57
+ ? { headers: { Authorization: `Bearer ${this.options.authToken}` } }
58
+ : {}),
59
+ });
60
+ this.socket.on("open", () => {
61
+ this.reconnectAttempt = 0;
62
+ this.setState("open");
63
+ this.startHeartbeat();
64
+ void this.flushPending();
65
+ this.options.onOpen?.();
66
+ });
67
+ this.socket.on("message", async (data) => {
68
+ const text = data.toString();
69
+ for (const handler of this.messageHandlers) {
70
+ await handler(text);
71
+ }
72
+ });
73
+ this.socket.on("close", () => this.scheduleReconnect());
74
+ this.socket.on("error", () => this.scheduleReconnect());
75
+ }
76
+ /**
77
+ * 发往平台 WS;对端已断开时常见 EPIPE/ECONNRESET。
78
+ * 此处永不 reject,避免业务层未捕获导致进程退出;失败时入队并关闭连接以触发重连。
79
+ */
80
+ async send(data) {
81
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
82
+ this.pendingSends.push(data);
83
+ this.enforcePendingLimit();
84
+ this.options.onPendingDepth?.(this.pendingSends.length);
85
+ return;
86
+ }
87
+ const sock = this.socket;
88
+ await new Promise((resolve) => {
89
+ sock.send(data, (error) => {
90
+ if (error) {
91
+ const code = error.code ?? "";
92
+ const hint = isBrokenPipeError(error) ? " (peer likely closed connection)" : "";
93
+ console.warn(`[WsClient] send failed code=${code}${hint}: ${error.message}`);
94
+ this.pendingSends.unshift(data);
95
+ this.enforcePendingLimit();
96
+ this.options.onPendingDepth?.(this.pendingSends.length);
97
+ try {
98
+ sock.close();
99
+ }
100
+ catch {
101
+ try {
102
+ sock.terminate();
103
+ }
104
+ catch {
105
+ /* ignore */
106
+ }
107
+ }
108
+ resolve();
109
+ return;
110
+ }
111
+ resolve();
112
+ });
113
+ });
114
+ }
115
+ enforcePendingLimit() {
116
+ const max = this.options.pendingQueueMaxItems ?? 10_000;
117
+ if (max <= 0 || this.pendingSends.length <= max)
118
+ return;
119
+ let dropped = 0;
120
+ while (this.pendingSends.length > max) {
121
+ this.pendingSends.shift();
122
+ dropped += 1;
123
+ }
124
+ if (dropped > 0) {
125
+ console.warn(`[WsClient] dropped ${dropped} pending send(s), maxQueue=${max}`);
126
+ this.options.onPendingDropped?.(dropped);
127
+ }
128
+ }
129
+ async flushPending() {
130
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
131
+ return;
132
+ const queue = [...this.pendingSends];
133
+ this.pendingSends = [];
134
+ this.options.onPendingDepth?.(0);
135
+ for (const data of queue) {
136
+ await this.send(data);
137
+ }
138
+ }
139
+ startHeartbeat() {
140
+ if (this.heartbeatTimer)
141
+ clearInterval(this.heartbeatTimer);
142
+ this.heartbeatTimer = setInterval(() => {
143
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
144
+ return;
145
+ }
146
+ try {
147
+ this.socket.ping();
148
+ }
149
+ catch (error) {
150
+ const message = error instanceof Error ? error.message : String(error);
151
+ console.warn(`[WsClient] ping failed: ${message}`);
152
+ }
153
+ }, this.options.heartbeatMs);
154
+ }
155
+ backoffDelayMs() {
156
+ const min = this.options.reconnectBackoffMinMs ??
157
+ (Number.isFinite(this.options.reconnectMs) && this.options.reconnectMs > 0 ? this.options.reconnectMs : 2000);
158
+ const maxCap = this.options.reconnectBackoffMaxMs ?? Math.max(min * 16, 60_000);
159
+ const exp = min * Math.pow(2, this.reconnectAttempt);
160
+ const capped = Math.min(maxCap, exp);
161
+ const jitter = Math.floor(Math.random() * Math.min(capped * 0.2, 5000));
162
+ return Math.min(maxCap, capped + jitter);
163
+ }
164
+ scheduleReconnect() {
165
+ if (this.heartbeatTimer) {
166
+ clearInterval(this.heartbeatTimer);
167
+ this.heartbeatTimer = null;
168
+ }
169
+ // close / error / send 失败可能连续触发,合并为一次退避,避免 reconnectAttempt 连加
170
+ if (this.reconnectTimer) {
171
+ return;
172
+ }
173
+ this.setState("disconnected");
174
+ this.reconnectAttempt += 1;
175
+ const delayMs = this.backoffDelayMs();
176
+ this.options.onReconnectScheduled?.(delayMs, this.reconnectAttempt);
177
+ this.reconnectTimer = setTimeout(() => {
178
+ this.reconnectTimer = null;
179
+ this.connect();
180
+ }, delayMs);
181
+ }
182
+ }
@@ -0,0 +1,36 @@
1
+ const LIVE_DEBUG_BINARY_MAGIC = "LDBG1";
2
+ function encodeLiveDebugBinaryPacket(params) {
3
+ const header = Buffer.from(JSON.stringify({
4
+ kind: "LiveDebugFrameBinary",
5
+ sessionId: params.sessionId,
6
+ resourceId: params.resourceId,
7
+ mimeType: params.mimeType,
8
+ capturedAt: params.capturedAt,
9
+ byteLength: params.data.byteLength,
10
+ ...(params.foregroundApp ? { foregroundApp: params.foregroundApp } : {}),
11
+ }), "utf8");
12
+ const magic = Buffer.from(LIVE_DEBUG_BINARY_MAGIC, "utf8");
13
+ const headerLength = Buffer.allocUnsafe(4);
14
+ headerLength.writeUInt32BE(header.byteLength, 0);
15
+ return Buffer.concat([magic, headerLength, header, params.data]);
16
+ }
17
+ export class WsEventPublisher {
18
+ wsClient;
19
+ constructor(wsClient) {
20
+ this.wsClient = wsClient;
21
+ }
22
+ async publish(event) {
23
+ await this.wsClient.send(JSON.stringify(event));
24
+ }
25
+ async publishLiveDebugFrame(_event, frame) {
26
+ const packet = encodeLiveDebugBinaryPacket({
27
+ sessionId: frame.sessionId,
28
+ resourceId: frame.resourceId,
29
+ mimeType: frame.mimeType,
30
+ capturedAt: frame.capturedAt,
31
+ data: frame.data,
32
+ ...(frame.foregroundApp ? { foregroundApp: frame.foregroundApp } : {}),
33
+ });
34
+ await this.wsClient.send(packet);
35
+ }
36
+ }
@@ -0,0 +1,227 @@
1
+ import { createServer } from "node:http";
2
+ import { LeaseConflictError } from "../../shared-kernel/errors/index.js";
3
+ import { OwnerType } from "../../shared-kernel/enums/index.js";
4
+ import { asLeaseId, asTaskId } from "../../shared-kernel/ids/index.js";
5
+ export class HttpServer {
6
+ resourceService;
7
+ observationQueryService;
8
+ healthService;
9
+ port;
10
+ authToken;
11
+ agentId;
12
+ leaseService;
13
+ taskService;
14
+ appPackageService;
15
+ occupationRelease;
16
+ appPackageUrlCache;
17
+ constructor(resourceService, observationQueryService, healthService, port, authToken, agentId, leaseService, taskService, appPackageService, occupationRelease, appPackageUrlCache) {
18
+ this.resourceService = resourceService;
19
+ this.observationQueryService = observationQueryService;
20
+ this.healthService = healthService;
21
+ this.port = port;
22
+ this.authToken = authToken;
23
+ this.agentId = agentId;
24
+ this.leaseService = leaseService;
25
+ this.taskService = taskService;
26
+ this.appPackageService = appPackageService;
27
+ this.occupationRelease = occupationRelease;
28
+ this.appPackageUrlCache = appPackageUrlCache;
29
+ }
30
+ start() {
31
+ const server = createServer(async (req, res) => {
32
+ if (!req.url)
33
+ return this.writeJson(res, 404, { message: "not found" });
34
+ if (this.authToken) {
35
+ const auth = req.headers.authorization ?? "";
36
+ const expected = `Bearer ${this.authToken}`;
37
+ if (auth !== expected) {
38
+ return this.writeJson(res, 401, { message: "unauthorized" });
39
+ }
40
+ }
41
+ const url = new URL(req.url, "http://127.0.0.1");
42
+ if (url.pathname === "/platform/commands" && req.method === "POST") {
43
+ return this.handlePlatformCommand(req, res);
44
+ }
45
+ if (url.pathname === "/health")
46
+ return this.writeJson(res, 200, this.healthService.snapshot());
47
+ if (url.pathname === "/metrics")
48
+ return this.writeJson(res, 200, this.healthService.snapshot());
49
+ if (url.pathname === "/cached-app-packages" && req.method === "GET") {
50
+ if (!this.appPackageUrlCache) {
51
+ return this.writeJson(res, 503, { message: "app package url cache unavailable" });
52
+ }
53
+ const snap = await this.appPackageUrlCache.snapshotForHttp();
54
+ return this.writeJson(res, 200, snap);
55
+ }
56
+ if (url.pathname === "/resources") {
57
+ const forceRefresh = url.searchParams.get("refresh") !== "0";
58
+ if (forceRefresh) {
59
+ await this.resourceService.refresh();
60
+ }
61
+ const resources = await this.resourceService.list();
62
+ if (!this.agentId)
63
+ return this.writeJson(res, 200, resources);
64
+ return this.writeJson(res, 200, resources.map((item) => ({ ...item, agentId: this.agentId })));
65
+ }
66
+ const taskMatch = url.pathname.match(/^\/tasks\/([^/]+)$/);
67
+ if (taskMatch) {
68
+ const task = await this.observationQueryService.getTask(decodeURIComponent(taskMatch[1]));
69
+ if (!task)
70
+ return this.writeJson(res, 404, { message: "task not found" });
71
+ return this.writeJson(res, 200, task);
72
+ }
73
+ const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
74
+ if (sessionMatch) {
75
+ const session = await this.observationQueryService.getSession(decodeURIComponent(sessionMatch[1]));
76
+ if (!session)
77
+ return this.writeJson(res, 404, { message: "session not found" });
78
+ return this.writeJson(res, 200, session);
79
+ }
80
+ const leaseMatch = url.pathname.match(/^\/leases\/([^/]+)$/);
81
+ if (leaseMatch) {
82
+ const lease = await this.observationQueryService.getLease(decodeURIComponent(leaseMatch[1]));
83
+ if (!lease)
84
+ return this.writeJson(res, 404, { message: "lease not found" });
85
+ return this.writeJson(res, 200, lease);
86
+ }
87
+ if (url.pathname === "/artifacts") {
88
+ const taskId = url.searchParams.get("taskId");
89
+ if (!taskId)
90
+ return this.writeJson(res, 400, { message: "taskId is required" });
91
+ return this.writeJson(res, 200, await this.observationQueryService.listArtifacts(taskId));
92
+ }
93
+ if (url.pathname === "/events") {
94
+ const taskId = url.searchParams.get("taskId") ?? undefined;
95
+ const type = url.searchParams.get("type") ?? undefined;
96
+ const limit = Number(url.searchParams.get("limit") ?? "0");
97
+ return this.writeJson(res, 200, await this.observationQueryService.listEvents({
98
+ taskId,
99
+ type,
100
+ limit: Number.isFinite(limit) ? limit : 0,
101
+ }));
102
+ }
103
+ return this.writeJson(res, 404, { message: "not found" });
104
+ });
105
+ server.listen(this.port);
106
+ return server;
107
+ }
108
+ readRequestBody(req) {
109
+ return new Promise((resolve, reject) => {
110
+ const chunks = [];
111
+ req.on("data", (c) => chunks.push(Buffer.from(c)));
112
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
113
+ req.on("error", reject);
114
+ });
115
+ }
116
+ async handlePlatformCommand(req, res) {
117
+ let raw = "";
118
+ try {
119
+ raw = await this.readRequestBody(req);
120
+ }
121
+ catch {
122
+ return this.writeJson(res, 400, { message: "invalid body" });
123
+ }
124
+ let parsed;
125
+ try {
126
+ parsed = raw.trim() ? JSON.parse(raw) : {};
127
+ }
128
+ catch {
129
+ return this.writeJson(res, 400, { message: "invalid json" });
130
+ }
131
+ const cmd = parsed && typeof parsed === "object" && "command" in parsed
132
+ ? parsed.command
133
+ : parsed;
134
+ if (!cmd || typeof cmd !== "object" || typeof cmd.type !== "string") {
135
+ return this.writeJson(res, 400, { message: "missing command.type" });
136
+ }
137
+ const cmdType = cmd.type;
138
+ const needsLeaseSvc = cmdType === "AcquireLeaseCommand" ||
139
+ cmdType === "ReleaseLeaseCommand" ||
140
+ cmdType === "RenewLeaseCommand" ||
141
+ cmdType === "RevokeLeaseByResourceCommand";
142
+ const needsTaskSvc = cmdType === "CreateTaskCommand" || cmdType === "CancelTaskCommand";
143
+ const needsAppPkgSvc = cmdType === "InstallAppCommand" || cmdType === "UninstallAppCommand";
144
+ if (needsLeaseSvc && !this.leaseService) {
145
+ return this.writeJson(res, 503, { message: "lease service unavailable" });
146
+ }
147
+ if (needsTaskSvc && !this.taskService) {
148
+ return this.writeJson(res, 503, { message: "task service unavailable" });
149
+ }
150
+ if (needsAppPkgSvc && !this.appPackageService) {
151
+ return this.writeJson(res, 503, { message: "app package service unavailable" });
152
+ }
153
+ try {
154
+ switch (cmd.type) {
155
+ case "AcquireLeaseCommand": {
156
+ const c = cmd;
157
+ const ot = c.ownerType;
158
+ if (!Object.values(OwnerType).includes(ot)) {
159
+ return this.writeJson(res, 400, { message: "invalid ownerType" });
160
+ }
161
+ await this.leaseService.acquire(c.leaseId, c.resourceId, c.ownerId, ot, c.ttlSeconds, {
162
+ username: c.occupantUsername,
163
+ displayName: c.occupantDisplayName,
164
+ });
165
+ return this.writeJson(res, 200, { ok: true });
166
+ }
167
+ case "ReleaseLeaseCommand": {
168
+ const c = cmd;
169
+ await this.leaseService.release(asLeaseId(c.leaseId));
170
+ return this.writeJson(res, 200, { ok: true });
171
+ }
172
+ case "RenewLeaseCommand": {
173
+ const c = cmd;
174
+ await this.leaseService.renew(asLeaseId(c.leaseId), c.ttlSeconds);
175
+ return this.writeJson(res, 200, { ok: true });
176
+ }
177
+ case "RevokeLeaseByResourceCommand": {
178
+ const c = cmd;
179
+ if (this.occupationRelease) {
180
+ await this.occupationRelease.forceRelease(c.resourceId);
181
+ }
182
+ else {
183
+ await this.leaseService.revokeByResourceId(c.resourceId);
184
+ }
185
+ return this.writeJson(res, 200, { ok: true });
186
+ }
187
+ case "CreateTaskCommand": {
188
+ const c = cmd;
189
+ void this.taskService.dispatch(c).catch((error) => {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ console.error(`[HttpServer] CreateTaskCommand dispatch failed: ${message}`);
192
+ });
193
+ return this.writeJson(res, 202, { ok: true, accepted: true });
194
+ }
195
+ case "CancelTaskCommand": {
196
+ const c = cmd;
197
+ await this.taskService.cancel(asTaskId(c.taskId));
198
+ return this.writeJson(res, 200, { ok: true });
199
+ }
200
+ case "InstallAppCommand": {
201
+ const c = cmd;
202
+ await this.appPackageService.install(c.resourceId, c.appRef);
203
+ return this.writeJson(res, 200, { ok: true });
204
+ }
205
+ case "UninstallAppCommand": {
206
+ const c = cmd;
207
+ await this.appPackageService.uninstall(c.resourceId, c.bundleId);
208
+ return this.writeJson(res, 200, { ok: true });
209
+ }
210
+ default:
211
+ return this.writeJson(res, 400, { message: `unsupported command: ${cmd.type}` });
212
+ }
213
+ }
214
+ catch (error) {
215
+ if (error instanceof LeaseConflictError) {
216
+ return this.writeJson(res, 409, { ok: false, message: error.message, resourceId: error.resourceId });
217
+ }
218
+ const message = error instanceof Error ? error.message : String(error);
219
+ return this.writeJson(res, 500, { ok: false, message });
220
+ }
221
+ }
222
+ writeJson(res, statusCode, data) {
223
+ res.statusCode = statusCode;
224
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
225
+ res.end(JSON.stringify(data));
226
+ }
227
+ }