From 88b3f9e23e9b38a4c3697ae9aa653721d95083ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 1 Sep 2026 18:40:54 +0200 Subject: [PATCH] refactor(transport): move shared host mechanics --- .../0019-request-bound-platform-runtime.md | 1 + packages/host-kit/package.json | 4 + .../host-kit/src/internal/transport.test.ts | 85 +++++++++++++++++++ .../host-kit/src/internal/transport.ts | 27 ++++++ packages/host-kit/src/transport.ts | 8 ++ .../__tests__/test-file-size-ratchet.test.ts | 4 +- scripts/layering/package-boundaries.test.ts | 1 + src/__tests__/eager-closure-budgets.ts | 3 + .../platform-runtime-runtime-hints.test.ts | 2 +- .../__tests__/timeout-policy.test.ts | 2 +- src/core/runtime-transport-hints.test.ts | 65 ++++++++++++++ .../runtime-transport-hints.ts} | 0 .../__tests__/daemon-client-lifecycle.test.ts | 18 ++-- .../daemon-client-timeout-route.test.ts | 2 +- .../client}/__tests__/daemon-client.test.ts | 22 ++--- src/daemon/client/daemon-client-progress.ts | 2 +- src/daemon/client/daemon-client-transport.ts | 2 +- src/daemon/human-control-http.ts | 3 +- .../session-replay-repair-transaction.test.ts | 2 +- .../__tests__/session-replay-runtime.test.ts | 2 +- src/daemon/request-router.ts | 2 +- src/daemon/server/http-server.ts | 2 +- src/daemon/server/transport.ts | 2 +- src/daemon/session-runtime.ts | 2 +- src/daemon/upload-http.ts | 2 +- src/metro/metro-reload-endpoints.ts | 2 +- src/metro/metro.ts | 2 +- src/platform-runtime-runtime-hints.ts | 2 +- src/remote/daemon-artifacts.ts | 2 +- src/remote/daemon-proxy.ts | 3 +- src/remote/remote-request-diagnostics.ts | 2 +- src/remote/upload-stream.ts | 2 +- src/utils/line-stream.ts | 15 ---- src/utils/timing-safe-equal.ts | 12 --- 34 files changed, 234 insertions(+), 73 deletions(-) create mode 100644 packages/host-kit/src/internal/transport.test.ts rename src/utils/node-http.ts => packages/host-kit/src/internal/transport.ts (69%) create mode 100644 packages/host-kit/src/transport.ts create mode 100644 src/core/runtime-transport-hints.test.ts rename src/{utils/runtime-transport.ts => core/runtime-transport-hints.ts} (100%) rename src/{utils => daemon/client}/__tests__/daemon-client-lifecycle.test.ts (98%) rename src/{utils => daemon/client}/__tests__/daemon-client.test.ts (98%) delete mode 100644 src/utils/line-stream.ts delete mode 100644 src/utils/timing-safe-equal.ts diff --git a/docs/adr/0019-request-bound-platform-runtime.md b/docs/adr/0019-request-bound-platform-runtime.md index e4f4f67f8c..d8b97f04fe 100644 --- a/docs/adr/0019-request-bound-platform-runtime.md +++ b/docs/adr/0019-request-bound-platform-runtime.md @@ -151,6 +151,7 @@ selection, R11/R13 package enumeration, and the composite typecheck project list > commands), `process` (observing and owning host processes), `diagnostics`, `retry` > (deadline/backoff/sleep), `archive` (bounded extraction and byte limits), `file` (atomic > publishes, locks, path resolution), `request` (request-scoped cancellation and progress), +> `transport` (line framing, lazy HTTP/body mechanics, and constant-time secret comparison), > and `version` (the installed version off disk). Modules under `src/internal/` are reachable > only through a port, and a port may only hold mechanics a consumer of that capability > needs — the eager-closure row per port is what keeps that honest. diff --git a/packages/host-kit/package.json b/packages/host-kit/package.json index 9fd1bbf791..466bb556fc 100644 --- a/packages/host-kit/package.json +++ b/packages/host-kit/package.json @@ -47,6 +47,10 @@ "types": "./src/retry.ts", "default": "./src/retry.ts" }, + "./transport": { + "types": "./src/transport.ts", + "default": "./src/transport.ts" + }, "./version": { "types": "./src/version.ts", "default": "./src/version.ts" diff --git a/packages/host-kit/src/internal/transport.test.ts b/packages/host-kit/src/internal/transport.test.ts new file mode 100644 index 0000000000..3ae23547d4 --- /dev/null +++ b/packages/host-kit/src/internal/transport.test.ts @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import { EventEmitter } from 'node:events'; +import http from 'node:http'; +import type { IncomingMessage } from 'node:http'; +import https from 'node:https'; +import { Readable } from 'node:stream'; +import { test } from 'vitest'; +import { AppError } from '@agent-device/kernel/errors'; +import { + consumeTextLines, + loadNodeHttpRequester, + readNodeHttpRequestBody, + readNodeHttpResponseBody, + timingSafeStringEqual, +} from '../transport.ts'; + +test('consumeTextLines trims complete lines and retains a partial line', () => { + const first = consumeTextLines('', ' first\n\nsecond'); + assert.deepEqual(first.lines, ['first']); + assert.equal(first.buffer, 'second'); + + const second = consumeTextLines(first.buffer, '-part\n third \n'); + assert.deepEqual(second.lines, ['second-part', 'third']); + assert.equal(second.buffer, ''); +}); + +test('loadNodeHttpRequester selects the protocol module and keeps request mutable', async () => { + const httpRequester = await loadNodeHttpRequester('http:'); + const httpsRequester = await loadNodeHttpRequester('https:'); + assert.equal(httpRequester, http); + assert.equal(httpsRequester, https); + + const originalRequest = httpRequester.request; + const stub = (() => {}) as unknown as typeof originalRequest; + const mutableRequester = httpRequester as { request: typeof originalRequest }; + try { + mutableRequester.request = stub; + assert.equal(httpRequester.request, stub); + } finally { + mutableRequester.request = originalRequest; + } +}); + +test('readNodeHttpResponseBody decodes the complete response stream', async () => { + const response = Readable.from(['hello', Buffer.from(' world')]) as unknown as IncomingMessage; + assert.equal(await readNodeHttpResponseBody(response), 'hello world'); +}); + +test('readNodeHttpRequestBody returns bytes and preserves the caller error message at the limit', async () => { + const request = Readable.from([ + Buffer.from('hello '), + Buffer.from('world'), + ]) as unknown as IncomingMessage; + assert.deepEqual( + await readNodeHttpRequestBody(request, 11, 'body exceeded'), + Buffer.from('hello world'), + ); + + const oversized = Readable.from([Buffer.from('hello world!')]) as unknown as IncomingMessage; + await assert.rejects( + readNodeHttpRequestBody(oversized, 11, 'body exceeded'), + (error: unknown) => { + assert.ok(error instanceof AppError); + assert.equal(error.code, 'INVALID_ARGS'); + assert.equal(error.message, 'body exceeded'); + return true; + }, + ); +}); + +test('timingSafeStringEqual handles equal, unequal, and unequal-length secrets', () => { + assert.equal(timingSafeStringEqual('secret', 'secret'), true); + assert.equal(timingSafeStringEqual('secret', 'secreT'), false); + assert.equal(timingSafeStringEqual('secret', 'secret-longer'), false); + assert.equal(timingSafeStringEqual('', ''), true); +}); + +test('readNodeHttpResponseBody propagates response errors', async () => { + const response = new EventEmitter() as unknown as IncomingMessage; + response.setEncoding = (_encoding: BufferEncoding) => response; + const promise = readNodeHttpResponseBody(response); + const error = new Error('response failed'); + response.emit('error', error); + await assert.rejects(promise, error); +}); diff --git a/src/utils/node-http.ts b/packages/host-kit/src/internal/transport.ts similarity index 69% rename from src/utils/node-http.ts rename to packages/host-kit/src/internal/transport.ts index 5bcfa85be4..13fa052b69 100644 --- a/src/utils/node-http.ts +++ b/packages/host-kit/src/internal/transport.ts @@ -1,6 +1,22 @@ +import crypto from 'node:crypto'; import type { IncomingMessage } from 'node:http'; import { AppError } from '@agent-device/kernel/errors'; +export function consumeTextLines( + currentBuffer: string, + chunk: string | Buffer, +): { lines: string[]; buffer: string } { + const lines: string[] = []; + let buffer = currentBuffer + chunk.toString(); + let idx = buffer.indexOf('\n'); + while (idx !== -1) { + const line = buffer.slice(0, idx).trim(); + buffer = buffer.slice(idx + 1); + if (line) lines.push(line); + idx = buffer.indexOf('\n'); + } + return { lines, buffer }; +} /** The slice of `node:http` / `node:https` an outbound request needs. */ export type NodeHttpRequester = Pick; @@ -55,3 +71,14 @@ export async function readNodeHttpRequestBody( } return Buffer.concat(chunks); } + +/** + * Compares two secret strings in constant time. Hashing both inputs first + * keeps the comparison length-independent, so unequal-length tokens neither + * throw nor leak length via timing. + */ +export function timingSafeStringEqual(a: string, b: string): boolean { + const hashA = crypto.createHash('sha256').update(a).digest(); + const hashB = crypto.createHash('sha256').update(b).digest(); + return crypto.timingSafeEqual(hashA, hashB); +} diff --git a/packages/host-kit/src/transport.ts b/packages/host-kit/src/transport.ts new file mode 100644 index 0000000000..a2d250af38 --- /dev/null +++ b/packages/host-kit/src/transport.ts @@ -0,0 +1,8 @@ +export { + consumeTextLines, + loadNodeHttpRequester, + readNodeHttpRequestBody, + readNodeHttpResponseBody, + timingSafeStringEqual, + type NodeHttpRequester, +} from './internal/transport.ts'; diff --git a/scripts/__tests__/test-file-size-ratchet.test.ts b/scripts/__tests__/test-file-size-ratchet.test.ts index 8b3eed0ea0..a1f63a0e81 100644 --- a/scripts/__tests__/test-file-size-ratchet.test.ts +++ b/scripts/__tests__/test-file-size-ratchet.test.ts @@ -38,12 +38,12 @@ const PINNED_TEST_FILE_LINES: Readonly> = Object.freeze({ 'src/commands/interaction/runtime/settle.test.ts': 2359, 'src/daemon/replay/internal/__tests__/session-replay-runtime-maestro.test.ts': 1963, 'packages/platform-apple/src/runner/__tests__/runner-session.test.ts': 1957, - 'src/utils/__tests__/daemon-client.test.ts': 1873, + 'src/daemon/client/__tests__/daemon-client.test.ts': 1873, 'packages/platform-android/src/__tests__/snapshot.test.ts': 1435, 'packages/platform-apple/src/runner/__tests__/runner-client.test.ts': 1441, 'src/__tests__/client.test.ts': 1592, 'test/integration/provider-scenarios/android-lifecycle.test.ts': 1556, - 'src/utils/__tests__/daemon-client-lifecycle.test.ts': 1413, + 'src/daemon/client/__tests__/daemon-client-lifecycle.test.ts': 1409, 'packages/platform-apple/src/runner/__tests__/runner-command-retry.test.ts': 1280, 'src/__tests__/cli-client-commands.test.ts': 1304, 'src/__tests__/cli-config.test.ts': 1282, diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 104f1a77ab..f1cf43d56c 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -518,6 +518,7 @@ test('the real tree parses, declares, and passes R11', () => { '@agent-device/host-kit/process', '@agent-device/host-kit/request', '@agent-device/host-kit/retry', + '@agent-device/host-kit/transport', '@agent-device/host-kit/version', ]); assert.deepEqual([...hostKitPackage.workspaceDependencies].sort(), [ diff --git a/src/__tests__/eager-closure-budgets.ts b/src/__tests__/eager-closure-budgets.ts index 9925dd7d6f..c875099017 100644 --- a/src/__tests__/eager-closure-budgets.ts +++ b/src/__tests__/eager-closure-budgets.ts @@ -152,6 +152,9 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/host-kit/src/process.ts': 12, 'packages/host-kit/src/request.ts': 5, 'packages/host-kit/src/retry.ts': 6, + // #2139 keeps framing, lazy HTTP/body mechanics, and secret comparison behind one + // transport port without growing the CLI's eager closure. + 'packages/host-kit/src/transport.ts': 4, 'packages/host-kit/src/version.ts': 4, // --- @agent-device/provision-kit --- diff --git a/src/__tests__/platform-runtime-runtime-hints.test.ts b/src/__tests__/platform-runtime-runtime-hints.test.ts index ceeea3b8ea..b5c1c77d05 100644 --- a/src/__tests__/platform-runtime-runtime-hints.test.ts +++ b/src/__tests__/platform-runtime-runtime-hints.test.ts @@ -7,7 +7,7 @@ import { clearRuntimeHintValues, } from '../platform-runtime-runtime-hints.ts'; import { applyDeviceDefaultMetroHost, runtimeHintValues } from '../daemon/session-runtime.ts'; -import { resolveRuntimeTransportHints } from '../utils/runtime-transport.ts'; +import { resolveRuntimeTransportHints } from '../core/runtime-transport-hints.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { defaultPrefsPath, diff --git a/src/core/command-descriptor/__tests__/timeout-policy.test.ts b/src/core/command-descriptor/__tests__/timeout-policy.test.ts index 77e0b5d365..e458226571 100644 --- a/src/core/command-descriptor/__tests__/timeout-policy.test.ts +++ b/src/core/command-descriptor/__tests__/timeout-policy.test.ts @@ -16,7 +16,7 @@ import { DEFAULT_STABLE_TIMEOUT_MS } from '../../../commands/interaction/runtime // default are bounded, diffable lists — they may only change in the same PR // that updates them here. Behavioral derivation (envelope arithmetic, wait // budget parsing, flag overrides) is proven by the pre-existing oracle tests in -// src/utils/__tests__/daemon-client.test.ts, which survived this migration +// src/daemon/client/__tests__/daemon-client.test.ts, which survived this migration // unchanged. function settleObservationCommandNames(): string[] { diff --git a/src/core/runtime-transport-hints.test.ts b/src/core/runtime-transport-hints.test.ts new file mode 100644 index 0000000000..677bb7805b --- /dev/null +++ b/src/core/runtime-transport-hints.test.ts @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { AppError } from '@agent-device/kernel/errors'; +import { resolveRuntimeTransportHints, trimRuntimeValue } from './runtime-transport-hints.ts'; + +test('resolves HTTP and HTTPS bundle URLs with their default ports', () => { + assert.deepEqual( + resolveRuntimeTransportHints({ bundleUrl: 'http://metro.example.test/index.bundle' }), + { host: 'metro.example.test', port: 80, scheme: 'http' }, + ); + assert.deepEqual( + resolveRuntimeTransportHints({ bundleUrl: 'https://metro.example.test/index.bundle' }), + { host: 'metro.example.test', port: 443, scheme: 'https' }, + ); +}); + +test('explicit host and port take precedence over bundle URL values', () => { + assert.deepEqual( + resolveRuntimeTransportHints({ + metroHost: ' explicit.example.test ', + metroPort: 9090, + bundleUrl: 'https://bundle.example.test:8081/index.bundle', + }), + { host: 'explicit.example.test', port: 9090, scheme: 'https' }, + ); +}); + +test('trims values, ignores invalid ports, and returns undefined for incomplete hints', () => { + assert.equal(trimRuntimeValue(' '), undefined); + assert.equal(trimRuntimeValue(' metro.example.test '), 'metro.example.test'); + assert.equal(resolveRuntimeTransportHints(undefined), undefined); + assert.equal(resolveRuntimeTransportHints({ metroHost: 'metro.example.test' }), undefined); + assert.equal(resolveRuntimeTransportHints({ metroPort: 0 }), undefined); + assert.deepEqual( + resolveRuntimeTransportHints({ metroHost: ' metro.example.test ', metroPort: 65_536 }), + undefined, + ); +}); + +test('invalid bundle URLs retain the existing typed argument error', () => { + assert.throws( + () => resolveRuntimeTransportHints({ bundleUrl: 'not a URL' }), + (error: unknown) => { + assert.ok(error instanceof AppError); + assert.equal(error.code, 'INVALID_ARGS'); + assert.equal(error.message, 'Invalid runtime bundle URL: not a URL'); + return true; + }, + ); +}); + +test('unsupported bundle schemes do not complete an otherwise missing transport', () => { + assert.equal( + resolveRuntimeTransportHints({ bundleUrl: 'ftp://metro.example.test/index.bundle' }), + undefined, + ); + assert.deepEqual( + resolveRuntimeTransportHints({ + metroHost: 'metro.example.test', + metroPort: 8081, + bundleUrl: 'ftp://bundle.example.test/index.bundle', + }), + { host: 'metro.example.test', port: 8081, scheme: 'http' }, + ); +}); diff --git a/src/utils/runtime-transport.ts b/src/core/runtime-transport-hints.ts similarity index 100% rename from src/utils/runtime-transport.ts rename to src/core/runtime-transport-hints.ts diff --git a/src/utils/__tests__/daemon-client-lifecycle.test.ts b/src/daemon/client/__tests__/daemon-client-lifecycle.test.ts similarity index 98% rename from src/utils/__tests__/daemon-client-lifecycle.test.ts rename to src/daemon/client/__tests__/daemon-client-lifecycle.test.ts index 8c893593ac..00051aa9ea 100644 --- a/src/utils/__tests__/daemon-client-lifecycle.test.ts +++ b/src/daemon/client/__tests__/daemon-client-lifecycle.test.ts @@ -5,7 +5,7 @@ import http from 'node:http'; import net from 'node:net'; import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; vi.mock('@agent-device/host-kit/command', async (importOriginal) => ({ ...(await importOriginal()), @@ -18,20 +18,16 @@ vi.mock('@agent-device/host-kit/retry', async (importOriginal) => ({ sleep: vi.fn(async () => {}), })); -import { resolveDaemonPaths, type DaemonPaths } from '../../daemon/config.ts'; -import { - sendToDaemon, - type DaemonRequest, - type DaemonResponse, -} from '../../daemon/client/daemon-client.ts'; -import { attachActiveSessionAddressHint } from '../../daemon/client/daemon-client-lifecycle.ts'; -import { computeDaemonCodeSignature } from '../../daemon/code-signature.ts'; -import { sendRequest } from '../../daemon/client/daemon-client-transport.ts'; +import { resolveDaemonPaths, type DaemonPaths } from '../../config.ts'; +import { sendToDaemon, type DaemonRequest, type DaemonResponse } from '../daemon-client.ts'; +import { attachActiveSessionAddressHint } from '../daemon-client-lifecycle.ts'; +import { computeDaemonCodeSignature } from '../../code-signature.ts'; +import { sendRequest } from '../daemon-client-transport.ts'; import { closeLoopbackServer, listenOnLoopback, supportsLoopbackBind, -} from '../../__tests__/test-utils/loopback.ts'; +} from '../../../__tests__/test-utils/loopback.ts'; import { AppError } from '@agent-device/kernel/errors'; import { runCmdDetachedMonitored, diff --git a/src/daemon/client/__tests__/daemon-client-timeout-route.test.ts b/src/daemon/client/__tests__/daemon-client-timeout-route.test.ts index d01562e128..f6b574acf9 100644 --- a/src/daemon/client/__tests__/daemon-client-timeout-route.test.ts +++ b/src/daemon/client/__tests__/daemon-client-timeout-route.test.ts @@ -1,6 +1,6 @@ // Production-seam coverage for the real request-timeout route. // -// src/utils/__tests__/daemon-client.test.ts covers `resolveRequestTimeoutHint` +// src/daemon/client/__tests__/daemon-client.test.ts covers `resolveRequestTimeoutHint` // as a pure formatter, but a pure-formatter test cannot catch a bug in // CLEANUP ELIGIBILITY: whether `cleanupTimedOutIosRunnerBuilds` (the Apple // xcodebuild pkill sweep) actually runs. This file spies on the real diff --git a/src/utils/__tests__/daemon-client.test.ts b/src/daemon/client/__tests__/daemon-client.test.ts similarity index 98% rename from src/utils/__tests__/daemon-client.test.ts rename to src/daemon/client/__tests__/daemon-client.test.ts index be0da9b99e..53475ad39a 100644 --- a/src/utils/__tests__/daemon-client.test.ts +++ b/src/daemon/client/__tests__/daemon-client.test.ts @@ -10,7 +10,7 @@ import { closeLoopbackServer, listenOnLoopback, supportsLoopbackBind, -} from '../../__tests__/test-utils/loopback.ts'; +} from '../../../__tests__/test-utils/loopback.ts'; import { runCmdBackground } from '@agent-device/host-kit/command'; import { isProcessAlive, @@ -18,24 +18,24 @@ import { readProcessStartTime, waitForProcessExit, } from '@agent-device/host-kit/process'; -import { sendToDaemon } from '../../daemon/client/daemon-client.ts'; -import { computeDaemonCodeSignature } from '../../daemon/code-signature.ts'; -import { downloadRemoteArtifact } from '../../remote/daemon-artifacts.ts'; +import { sendToDaemon } from '../daemon-client.ts'; +import { computeDaemonCodeSignature } from '../../code-signature.ts'; +import { downloadRemoteArtifact } from '../../../remote/daemon-artifacts.ts'; import { cleanupFailedDaemonStartupMetadata, resolveDaemonStartupHint, -} from '../../daemon/client/daemon-client-metadata.ts'; -import { canConnectSocket } from '../../daemon/client/daemon-client-transport.ts'; -import { DAEMON_RPC_PROTOCOL_VERSION } from '../../daemon/http-health.ts'; +} from '../daemon-client-metadata.ts'; +import { canConnectSocket } from '../daemon-client-transport.ts'; +import { DAEMON_RPC_PROTOCOL_VERSION } from '../../http-health.ts'; import { resolveDaemonRequestTimeoutMs, resolveRequestTimeoutHint, shouldResetDaemonAfterRequestTimeout, -} from '../../daemon/client/daemon-client-timeout.ts'; -import { resolveDaemonPaths } from '../../daemon/config.ts'; -import { stopProcessForTakeover } from '../../daemon/daemon-process.ts'; +} from '../daemon-client-timeout.ts'; +import { resolveDaemonPaths } from '../../config.ts'; +import { stopProcessForTakeover } from '../../daemon-process.ts'; import { findProjectRoot, readVersion } from '@agent-device/host-kit/version'; -import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts'; +import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; // readProcessStartTime/readProcessCommand shell out to `ps` with a 1s // timeout (see host-process.ts). isAgentDeviceDaemonProcess re-reads both for diff --git a/src/daemon/client/daemon-client-progress.ts b/src/daemon/client/daemon-client-progress.ts index cc6c6d0494..b628fbaab1 100644 --- a/src/daemon/client/daemon-client-progress.ts +++ b/src/daemon/client/daemon-client-progress.ts @@ -5,7 +5,7 @@ import type http from 'node:http'; import type { Socket } from 'node:net'; import { AppError } from '@agent-device/kernel/errors'; import type { DaemonRequest, DaemonResponse } from '../types.ts'; -import { consumeTextLines } from '../../utils/line-stream.ts'; +import { consumeTextLines } from '@agent-device/host-kit/transport'; import { markDoctorProgressRendered } from './doctor-progress.ts'; import { isDaemonProgressEnvelope, diff --git a/src/daemon/client/daemon-client-transport.ts b/src/daemon/client/daemon-client-transport.ts index 9584684c9d..ecc7c13c99 100644 --- a/src/daemon/client/daemon-client-transport.ts +++ b/src/daemon/client/daemon-client-transport.ts @@ -1,7 +1,7 @@ import type { RequestProgressSink } from '@agent-device/contracts/progress'; import net from 'node:net'; import { AppError } from '@agent-device/kernel/errors'; -import { loadNodeHttpRequester, readNodeHttpResponseBody } from '../../utils/node-http.ts'; +import { loadNodeHttpRequester, readNodeHttpResponseBody } from '@agent-device/host-kit/transport'; import type { DaemonRequest, DaemonResponse } from '../types.ts'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import type { DaemonPaths, DaemonTransportPreference } from '../config.ts'; diff --git a/src/daemon/human-control-http.ts b/src/daemon/human-control-http.ts index 0f88d43ba6..218bdf0f31 100644 --- a/src/daemon/human-control-http.ts +++ b/src/daemon/human-control-http.ts @@ -1,7 +1,6 @@ import type http from 'node:http'; import { AppError, createRequestCanceledError, normalizeError } from '@agent-device/kernel/errors'; -import { readNodeHttpRequestBody } from '../utils/node-http.ts'; -import { timingSafeStringEqual } from '../utils/timing-safe-equal.ts'; +import { readNodeHttpRequestBody, timingSafeStringEqual } from '@agent-device/host-kit/transport'; import { sendRestJsonError } from './http-errors.ts'; import { HUMAN_CONTROL_HTTP_PREFIX, diff --git a/src/daemon/replay/internal/__tests__/session-replay-repair-transaction.test.ts b/src/daemon/replay/internal/__tests__/session-replay-repair-transaction.test.ts index f1f145e243..1d6426c7dd 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-repair-transaction.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-repair-transaction.test.ts @@ -6,7 +6,7 @@ * process-level keep-alive (Fix 1's daemon * teardown guard) is a different architectural layer — a client-side process * manager, not session/script state — and is covered separately in - * `src/utils/__tests__/daemon-client-lifecycle.test.ts` + * `src/daemon/client/__tests__/daemon-client-lifecycle.test.ts` * ("keeps an owned ephemeral daemon alive and hints its --state-dir..."). * * Fix 1 (session-side): a divergence never deletes the session — it stays in diff --git a/src/daemon/replay/internal/__tests__/session-replay-runtime.test.ts b/src/daemon/replay/internal/__tests__/session-replay-runtime.test.ts index 72da8b55f5..a9233616be 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-runtime.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-runtime.test.ts @@ -64,7 +64,7 @@ test('a successful replay prints one line with the step count and wall time', as // producer (`completeReplayRun`'s `sessionStore.get(sessionName)` check), not // asserted against a hand-crafted fixture — deleting that line would fail // these, unlike the client-lifecycle tests in -// `src/utils/__tests__/daemon-client-lifecycle.test.ts`, which only prove the +// `src/daemon/client/__tests__/daemon-client-lifecycle.test.ts`, which only prove the // CLIENT'S reaction to a `sessionActive` value it is handed. --- test('a close-less replay reports sessionActive: true (real producer, session still in the store)', async () => { diff --git a/src/daemon/request-router.ts b/src/daemon/request-router.ts index ba6199109f..c124296726 100644 --- a/src/daemon/request-router.ts +++ b/src/daemon/request-router.ts @@ -10,7 +10,7 @@ import { retriableForErrorCode, type DaemonError, } from '@agent-device/kernel/errors'; -import { timingSafeStringEqual } from '../utils/timing-safe-equal.ts'; +import { timingSafeStringEqual } from '@agent-device/host-kit/transport'; import type { DaemonArtifactType, ResponseCost } from '@agent-device/kernel/contracts'; import type { CloudArtifactProvider } from '@agent-device/contracts/observability'; import type { diff --git a/src/daemon/server/http-server.ts b/src/daemon/server/http-server.ts index c49e84ebbf..1d28a75651 100644 --- a/src/daemon/server/http-server.ts +++ b/src/daemon/server/http-server.ts @@ -7,7 +7,7 @@ import { type DiagnosticsRecordRef, } from '@agent-device/kernel/errors'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; -import { timingSafeStringEqual } from '../../utils/timing-safe-equal.ts'; +import { timingSafeStringEqual } from '@agent-device/host-kit/transport'; import type { CommandRpcParams, JsonRpcId, diff --git a/src/daemon/server/transport.ts b/src/daemon/server/transport.ts index 595472ce62..57025767d1 100644 --- a/src/daemon/server/transport.ts +++ b/src/daemon/server/transport.ts @@ -11,7 +11,7 @@ import { withRequestProgressSink, } from '@agent-device/host-kit/request'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; -import { consumeTextLines } from '../../utils/line-stream.ts'; +import { consumeTextLines } from '@agent-device/host-kit/transport'; import { serializeDaemonProgressEnvelope, diff --git a/src/daemon/session-runtime.ts b/src/daemon/session-runtime.ts index ba2b4b7c81..52b68b84ad 100644 --- a/src/daemon/session-runtime.ts +++ b/src/daemon/session-runtime.ts @@ -7,7 +7,7 @@ import { AppError, asAppError } from '@agent-device/kernel/errors'; import { publicPlatformString, type DeviceInfo } from '@agent-device/kernel/device'; import type { DaemonRequest, SessionRuntimeHints, SessionState } from './types.ts'; import { SessionStore } from './session-store.ts'; -import { trimRuntimeValue } from '../utils/runtime-transport.ts'; +import { trimRuntimeValue } from '../core/runtime-transport-hints.ts'; import { isAndroidEmulator, isIosSimulator } from './device-targets.ts'; import { errorResponse, type DaemonFailureResponse } from './response.ts'; diff --git a/src/daemon/upload-http.ts b/src/daemon/upload-http.ts index 5ba1e20144..130e80ae44 100644 --- a/src/daemon/upload-http.ts +++ b/src/daemon/upload-http.ts @@ -10,7 +10,7 @@ import { } from './resumable-upload.ts'; import { receiveUpload } from './upload.ts'; import { sendRestJsonError } from './http-errors.ts'; -import { readNodeHttpRequestBody } from '../utils/node-http.ts'; +import { readNodeHttpRequestBody } from '@agent-device/host-kit/transport'; const DIRECT_UPLOAD_PATH_PREFIX = '/upload/direct/'; diff --git a/src/metro/metro-reload-endpoints.ts b/src/metro/metro-reload-endpoints.ts index d7909d27ea..3801b8a6e7 100644 --- a/src/metro/metro-reload-endpoints.ts +++ b/src/metro/metro-reload-endpoints.ts @@ -3,7 +3,7 @@ import type { MetroRuntimeHints } from './metro-types.ts'; import { resolveRuntimeTransportHints, type ResolvedRuntimeTransport, -} from '../utils/runtime-transport.ts'; +} from '../core/runtime-transport-hints.ts'; const DEFAULT_METRO_HOST = 'localhost'; const DEFAULT_METRO_PORT = 8081; diff --git a/src/metro/metro.ts b/src/metro/metro.ts index 73d31203c2..fa602d8b87 100644 --- a/src/metro/metro.ts +++ b/src/metro/metro.ts @@ -1,6 +1,6 @@ import type { SessionRuntimeHints } from '@agent-device/kernel/contracts'; import { stopMetroCompanion } from './client-metro-companion.ts'; -import { resolveRuntimeTransportHints } from '../utils/runtime-transport.ts'; +import { resolveRuntimeTransportHints } from '../core/runtime-transport-hints.ts'; export type { MetroBridgeDescriptor } from './metro-types.ts'; diff --git a/src/platform-runtime-runtime-hints.ts b/src/platform-runtime-runtime-hints.ts index e9af3155d8..58887cf994 100644 --- a/src/platform-runtime-runtime-hints.ts +++ b/src/platform-runtime-runtime-hints.ts @@ -3,7 +3,7 @@ import { AppError, asAppError } from '@agent-device/kernel/errors'; import { escapeXmlTextAndAttribute } from '@agent-device/xml'; import type { RuntimeHintValues } from '@agent-device/contracts/application-lifecycle-runtime'; import { execFailureDetails, type ExecResult } from '@agent-device/host-kit/command'; -import { type ResolvedRuntimeTransport } from './utils/runtime-transport.ts'; +import { type ResolvedRuntimeTransport } from './core/runtime-transport-hints.ts'; import { loadAndroidMechanics } from './platform-runtime-android-mechanics.ts'; // React Native's PackagerConnectionSettings/DevInternalSettings read debug_http_host via diff --git a/src/remote/daemon-artifacts.ts b/src/remote/daemon-artifacts.ts index ec83c32b43..b58fd9ec61 100644 --- a/src/remote/daemon-artifacts.ts +++ b/src/remote/daemon-artifacts.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { pipeline } from 'node:stream/promises'; import { AppError } from '@agent-device/kernel/errors'; -import { loadNodeHttpRequester } from '../utils/node-http.ts'; +import { loadNodeHttpRequester } from '@agent-device/host-kit/transport'; import type { DaemonArtifact, DaemonRequest, DaemonResponse } from '../daemon/types.ts'; import { buildDaemonHttpAuthHeaders, diff --git a/src/remote/daemon-proxy.ts b/src/remote/daemon-proxy.ts index 9781a8d85c..a3548da999 100644 --- a/src/remote/daemon-proxy.ts +++ b/src/remote/daemon-proxy.ts @@ -3,8 +3,7 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { randomUUID } from 'node:crypto'; import { AppError, normalizeError } from '@agent-device/kernel/errors'; -import { readNodeHttpRequestBody } from '../utils/node-http.ts'; -import { timingSafeStringEqual } from '../utils/timing-safe-equal.ts'; +import { readNodeHttpRequestBody, timingSafeStringEqual } from '@agent-device/host-kit/transport'; import { DAEMON_HTTP_BASE_PATH, DAEMON_HTTP_NETWORK_ACCESS_HEADER, diff --git a/src/remote/remote-request-diagnostics.ts b/src/remote/remote-request-diagnostics.ts index 501956588e..8affca9078 100644 --- a/src/remote/remote-request-diagnostics.ts +++ b/src/remote/remote-request-diagnostics.ts @@ -20,7 +20,7 @@ import fs from 'node:fs'; import path from 'node:path'; import type { DaemonError, DiagnosticsRecordRef } from '@agent-device/kernel/errors'; -import { loadNodeHttpRequester } from '../utils/node-http.ts'; +import { loadNodeHttpRequester } from '@agent-device/host-kit/transport'; import { buildDaemonHttpAuthHeaders, buildDaemonHttpTenantHeaders, diff --git a/src/remote/upload-stream.ts b/src/remote/upload-stream.ts index 5f94a434b8..608b48bdd8 100644 --- a/src/remote/upload-stream.ts +++ b/src/remote/upload-stream.ts @@ -3,7 +3,7 @@ import type { IncomingHttpHeaders } from 'node:http'; import path from 'node:path'; import { pipeline } from 'node:stream/promises'; import { AppError } from '@agent-device/kernel/errors'; -import { loadNodeHttpRequester, readNodeHttpResponseBody } from '../utils/node-http.ts'; +import { loadNodeHttpRequester, readNodeHttpResponseBody } from '@agent-device/host-kit/transport'; import { createUploadProgressTransform, type UploadProgressSink, diff --git a/src/utils/line-stream.ts b/src/utils/line-stream.ts deleted file mode 100644 index e8f368fbd7..0000000000 --- a/src/utils/line-stream.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function consumeTextLines( - currentBuffer: string, - chunk: string | Buffer, -): { lines: string[]; buffer: string } { - const lines: string[] = []; - let buffer = currentBuffer + chunk.toString(); - let idx = buffer.indexOf('\n'); - while (idx !== -1) { - const line = buffer.slice(0, idx).trim(); - buffer = buffer.slice(idx + 1); - if (line) lines.push(line); - idx = buffer.indexOf('\n'); - } - return { lines, buffer }; -} diff --git a/src/utils/timing-safe-equal.ts b/src/utils/timing-safe-equal.ts deleted file mode 100644 index 74363aad33..0000000000 --- a/src/utils/timing-safe-equal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import crypto from 'node:crypto'; - -/** - * Compares two secret strings in constant time. Hashing both inputs first - * keeps the comparison length-independent, so unequal-length tokens neither - * throw nor leak length via timing. - */ -export function timingSafeStringEqual(a: string, b: string): boolean { - const hashA = crypto.createHash('sha256').update(a).digest(); - const hashB = crypto.createHash('sha256').update(b).digest(); - return crypto.timingSafeEqual(hashA, hashB); -}