Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 27 additions & 20 deletions packages/coding-agent/install-lock/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/coding-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@
"hosted-git-info": "9.0.3",
"ignore": "7.0.8",
"jiti": "2.7.0",
"minimatch": "10.2.6",
"minimatch": "11.0.1",
"proper-lockfile": "4.1.2",
"quickjs-wasi": "3.6.2",
"semver": "7.8.5",
"typebox": "1.3.27",
"undici": "8.10.2",
"undici": "8.12.2",
"yaml": "2.9.0"
},
"overrides": {
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export {
} from "./core/extensions/index.ts";
// Footer data provider (git branch + extension statuses - data not otherwise available to extensions)
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts";
export { applyHttpProxySettings, configureHttpDispatcher } from "./core/http-dispatcher.ts";
export type { RegisteredMcpServer } from "./core/mcp-servers.ts";
export { convertToLlm } from "./core/messages.ts";
export { ModelRegistry } from "./core/model-registry.ts";
Expand Down
34 changes: 34 additions & 0 deletions packages/coding-agent/test/fixtures/excessive-content-encoding.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import { createServer } from "node:net";
import { setTimeout } from "node:timers/promises";
import * as undici from "undici";
import { configureHttpDispatcher } from "../../src/core/http-dispatcher.ts";

// Run in a child process: the regression escapes fetch's rejection handler.
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "NO_PROXY", "no_proxy"]) {
delete process.env[key];
}
if (process.argv[2] === "configured-global") configureHttpDispatcher();
const fetcher = process.argv[2] === "configured-global" ? globalThis.fetch : undici.fetch;
const server = createServer((socket) => {
socket.on("error", () => {});
socket.once("data", () => {
socket.end(
"HTTP/1.1 200 OK\r\n" +
"Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip\r\n" +
"Content-Length: 100\r\nConnection: close\r\n\r\n",
);
});
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address();
assert(address && typeof address !== "string");
await assert.rejects(fetcher(`http://127.0.0.1:${address.port}/`), /fetch failed/);
// Give the prematurely closed body time to emit its asynchronous error.
await setTimeout(100);
} finally {
await undici.getGlobalDispatcher().close();
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
console.log("survived excessive content encodings");
12 changes: 12 additions & 0 deletions packages/coding-agent/test/http-dispatcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { execFileSync } from "node:child_process";
import http from "node:http";
import net from "node:net";
import tls from "node:tls";
import { fileURLToPath } from "node:url";
import * as undici from "undici";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { applyHttpProxySettings, configureHttpDispatcher } from "../src/core/http-dispatcher.ts";
Expand Down Expand Up @@ -62,6 +64,16 @@ describe("http dispatcher", () => {
const originalFetch = globalThis.fetch;
let savedProxyEnv: Record<(typeof DISPATCHER_PROXY_ENV_KEYS)[number], string | undefined>;

// Regression for seipass/hasilan-code#1 (GHSA-xjj6-f33q-929c).
it.each(["fetch", "configured-global"])("survives excessive content encodings with %s", (mode) => {
const output = execFileSync(
process.execPath,
[fileURLToPath(new URL("./fixtures/excessive-content-encoding.mjs", import.meta.url)), mode],
{ encoding: "utf8", timeout: 10_000 },
);
expect(output.trim()).toBe("survived excessive content encodings");
});

beforeEach(() => {
savedProxyEnv = Object.fromEntries(DISPATCHER_PROXY_ENV_KEYS.map((key) => [key, process.env[key]])) as Record<
(typeof DISPATCHER_PROXY_ENV_KEYS)[number],
Expand Down
11 changes: 9 additions & 2 deletions packages/web-harness/src/server/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { randomBytes } from "node:crypto";
import { join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { ModelRuntime, type ToolDefinition } from "@hasilan-code/pi-coding-agent";
import {
applyHttpProxySettings,
configureHttpDispatcher,
ModelRuntime,
type ToolDefinition,
} from "@hasilan-code/pi-coding-agent";
import { toString as qrCodeText } from "qrcode";
import { ATTACHMENT_LIMITS } from "../shared/attachments.ts";
import { CANVAS_LIMITS, canvasSaveSchema, isCanvasDocument, serializeCanvasDesign } from "../shared/canvas.ts";
Expand Down Expand Up @@ -79,14 +84,16 @@ export async function startHarness(options: CliOptions, hooks: HarnessHooks = {}
async function createHarness(options: CliOptions, hooks: HarnessHooks) {
const cwd = await resolveCliWorkspace(options);
const agentDir = getHarnessAgentDir();
const agentSettings = createHarnessSettings(cwd, agentDir);
applyHttpProxySettings(agentSettings.getGlobalSettings().httpProxy);
configureHttpDispatcher(agentSettings.getHttpIdleTimeoutMs());
const modelRuntime = await ModelRuntime.create({
authPath: join(agentDir, "auth.json"),
modelsPath: join(agentDir, "models.json"),
modelsStorePath: join(agentDir, "models-store.json"),
});
const settings = await SettingsService.open(agentDir, modelRuntime);
const uiPreferences = await UiPreferencesStore.open(agentDir);
const agentSettings = createHarnessSettings(cwd, agentDir);
const authLogin = new AuthLoginManager(modelRuntime, { getDeviceId: () => agentSettings.getOrCreateDeviceId() });
const artifacts = new ArtifactStore();
const browser = new BrowserManager(artifacts);
Expand Down
43 changes: 43 additions & 0 deletions packages/web-harness/test/fixtures/http-dispatcher-startup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { setTimeout } from "node:timers/promises";
import * as undici from "undici";
import { startHarness } from "../../src/server/main.ts";
import { WidgetData } from "../../src/server/widget-data.ts";

const root = await mkdtemp(join(tmpdir(), "hasilan-http-startup-"));
process.env.HASILAN_CODE_AGENT_DIR = join(root, "agent");
process.env.PI_OFFLINE = "1";
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "NO_PROXY", "no_proxy"]) {
delete process.env[key];
}
let harness;
const server = createServer((socket) => {
socket.on("error", () => {});
socket.once("data", () => {
socket.end(
"HTTP/1.1 200 OK\r\n" +
"Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip\r\n" +
"Content-Length: 100\r\nConnection: close\r\n\r\n",
);
});
});
try {
harness = await startHarness({ cwd: root, port: 0, noOpen: true, help: false, lan: false, development: false });
assert.equal(globalThis.fetch, undici.fetch, "Hasilan must use the patched undici fetch");
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert(address && typeof address !== "string");
const widgets = new WidgetData();
await assert.rejects(widgets.read(new URL(`http://127.0.0.1:${address.port}/`), 0, (value) => value));
await setTimeout(100);
} finally {
await harness?.shutdown();
if (server.listening) await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await undici.getGlobalDispatcher().close();
await rm(root, { recursive: true, force: true });
}
console.log("survived excessive content encodings");
14 changes: 14 additions & 0 deletions packages/web-harness/test/http-dispatcher-startup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";

// Regression for seipass/hasilan-code#1 (GHSA-xjj6-f33q-929c).
it("uses the patched HTTP implementation after the shared web and desktop startup", () => {
const resolver = new URL("../../coding-agent/src/experimental/source-resolver.ts", import.meta.url).href;
const fixture = fileURLToPath(new URL("./fixtures/http-dispatcher-startup.mjs", import.meta.url));
const output = execFileSync(process.execPath, ["--import", resolver, fixture], {
encoding: "utf8",
timeout: 30_000,
});
expect(output.trim()).toBe("survived excessive content encodings");
}, 40_000);
1 change: 1 addition & 0 deletions scripts/test-hasilan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ execFileSync(
vitest,
"--run",
"--maxWorkers=2",
"test/http-dispatcher.test.ts",
"test/suite/agent-session-context-injection.test.ts",
"test/suite/agent-session-queue.test.ts",
],
Expand Down
Loading