Compare commits

...
Author SHA1 Message Date
paseo-bot[bot]andLorenzo Fiore 3e172d18c4 test: retain real OMP ACP provider ID collision reproduction
Reproduce #3178 through configuration parsing, provider registry resolution, and real session creation. The unique omp-acp ID succeeds, while omp keeps the built-in RPC runtime despite extends: acp and times out. This is an opt-in diagnosis test, with no production behavior changes.

Co-authored-by: Lorenzo Fiore <fiorelorenzo.fl@gmail.com>
2026-10-11 23:55:40 +02:00
paseo-bot[bot]andX b3693b79d1 Find hidden agent plan files in workspace suggestions (#6641)
* fix(server): find hidden agent plan files in workspace suggestions

Workspace autocomplete skipped the editor's plan directory while traversing
other known agent directories. Include it in the existing discovery policy
and cover basename and partial-path queries through the daemon client.

Co-authored-by: X <X@16x.org>

* test(server): give plan suggestion queries explicit expectations

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: X <X@16x.org>
2026-10-11 21:54:46 +02:00
0458988825 Connect the self-hosted web app through standard-port proxies (#6638)
* fix(server): give the web UI connection hint an explicit port (#1)

injectConnectionHint built `listen` straight from the Host header, which
carries no port when the client used the scheme's default — exactly what a
browser sends to a proxy terminating TLS on 443. The app parses `listen` with
parseHostPort, which requires an explicit port, so connectionFromListen
returned null and bootstrapInitialConnectionHint bailed before probing,
silently. Boot then fell through to the localhost:6767 fallback, which is the
viewer's own machine rather than the daemon, and the app landed on the welcome
screen with no host at all.

Reaching the daemon directly never hit this: http://host:6767 puts the port in
the Host header. It only appears behind a reverse proxy on 443 or 80, such as a
Tailscale Ingress, and only in a browser profile with no host already stored.

Spell the scheme default out when the header omits it, leaving the IPv6 bracket
form intact.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(server): compute the web bootstrap port before its connection hint

Keep the default-port fix for reverse proxy bootstrap within the repository branching style.

Co-authored-by: Sian Lerk Lau <kiawin@gmail.com>

---------

Co-authored-by: Sian Lerk Lau <kiawin@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-11 20:51:51 +02:00
paseo-bot[bot] 4bd31e1a2e test(cli): accept Codex catalogs without branded model IDs (#6633)
The provider regression test rejected valid GPT catalogs solely because no ID contained codex. Keep the structural output checks without requiring that upstream branding substring.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-11 19:47:10 +02:00
6 changed files with 159 additions and 5 deletions
-4
View File
@@ -364,10 +364,6 @@ try {
ids.every((id) => id.startsWith("gpt-")),
"all codex model IDs should be from the gpt family",
);
assert(
ids.some((id) => id.includes("codex")),
"codex model list should include at least one codex-optimized model",
);
assert(
data.every((m) => m.model && m.id && m.description),
"every codex model should have model, id, and description fields",
@@ -0,0 +1,52 @@
import { mkdtemp, mkdir, rm } from "node:fs/promises";
import path from "node:path";
import { expect, test } from "vitest";
import { ProviderOverridesSchema } from "@getpaseo/protocol/provider-config";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { buildProviderRegistry } from "./provider-registry.js";
import type { AgentSession } from "./agent-sdk-types.js";
// Retained reproduction for #3178, awaiting the built-in ID configuration decision.
// Requires OMP on PATH (or OMP_COMMAND). Uses no model calls or existing provider state.
const runReproduction = process.env.OMP_PROVIDER_COLLISION_REPRO === "1";
for (const providerId of ["omp-acp", "omp"]) {
test.skipIf(!runReproduction)(
`explicit ACP configuration creates an ACP session under '${providerId}'`,
async () => {
const cwd = await mkdtemp(path.join(process.cwd(), ".provider-collision-"));
const agentDir = path.join(cwd, "agent");
await mkdir(agentDir);
const providerOverrides = ProviderOverridesSchema.parse({
[providerId]: {
extends: "acp",
label: "Oh My Pi ACP",
command: [process.env.OMP_COMMAND ?? "omp", "acp"],
enabled: true,
env: {
PI_CODING_AGENT_DIR: agentDir,
OMP_PROFILE: "default",
PI_PROFILE: "default",
XDG_CONFIG_HOME: path.join(cwd, "config"),
XDG_DATA_HOME: path.join(cwd, "data"),
XDG_CACHE_HOME: path.join(cwd, "cache"),
XDG_STATE_HOME: path.join(cwd, "state"),
},
},
});
const logger = createTestLogger();
const registry = buildProviderRegistry(logger, { providerOverrides });
const client = registry[providerId]!.createClient(logger);
let session: AgentSession | undefined;
try {
session = await client.createSession({ provider: providerId, cwd });
expect(session.provider).toBe(providerId);
} finally {
await session?.close();
await client.shutdown?.();
await rm(cwd, { recursive: true, force: true });
}
},
30_000,
);
}
@@ -134,6 +134,44 @@ describe("daemon web UI bootstrap", () => {
});
});
test("spells out the scheme default port when the Host header omits it", async () => {
const distDir = await createWebUiDist();
daemonHandle = await createTestPaseoDaemon({
mcpEnabled: false,
webUi: {
enabled: true,
distDir,
},
});
// A browser reaching a proxy that terminates TLS on 443 sends a Host header
// with no port at all. The app rejects a portless `listen`, so the daemon
// has to supply the default itself.
const httpsHint = readInjectedConnectionHint(
await fetchDaemonWebUi({
port: daemonHandle.port,
headers: { host: "paseo.example.test", "x-forwarded-proto": "https" },
}),
);
const httpHint = readInjectedConnectionHint(
await fetchDaemonWebUi({
port: daemonHandle.port,
headers: { host: "paseo.example.test" },
}),
);
const ipv6Hint = readInjectedConnectionHint(
await fetchDaemonWebUi({
port: daemonHandle.port,
headers: { host: "[::1]", "x-forwarded-proto": "https" },
}),
);
expect(httpsHint.listen).toBe("paseo.example.test:443");
expect(httpHint.listen).toBe("paseo.example.test:80");
expect(ipv6Hint.listen).toBe("[::1]:443");
});
test("ignores forwarded HTTPS when proxy trust is disabled", async () => {
const distDir = await createWebUiDist();
@@ -23,6 +23,49 @@ afterEach(async () => {
}, 60000);
describe("file explorer", () => {
test.each([
{
query: "implementation",
expectedPaths: [
".cursor/plans/implementation-plan.md",
".codex/plans/implementation-plan.md",
".claude/plans/implementation-plan.md",
],
},
{
query: ".cursor/plans/implementation",
expectedPaths: [".cursor/plans/implementation-plan.md"],
},
])(
"workspace suggestions find hidden agent plans for '$query'",
async ({ query, expectedPaths }) => {
const cwd = tmpCwd();
try {
for (const directory of [".cursor", ".codex", ".claude", ".hidden"]) {
mkdirSync(path.join(cwd, directory, "plans"), { recursive: true });
writeFileSync(path.join(cwd, directory, "plans", "implementation-plan.md"), "plan\n");
}
const response = await ctx.client.getDirectorySuggestions({
cwd,
query,
limit: 50,
includeFiles: true,
includeDirectories: true,
});
expect(response.error).toBeNull();
expect(response.entries).toHaveLength(expectedPaths.length);
expect(response.entries).toEqual(
expect.arrayContaining(
expectedPaths.map((entryPath) => ({ path: entryPath, kind: "file" })),
),
);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
},
);
test.each(["..", "..."])("supports file operations on names starting with %s", async (prefix) => {
const cwd = tmpCwd();
const fileName = `${prefix}notes.txt`;
+25 -1
View File
@@ -250,6 +250,22 @@ function serializeInlineScriptJson(value: unknown): string {
.replace(/&/g, "\\u0026");
}
/**
* A Host header carries no port when the client used the scheme's default one,
* which is what every browser sends to a proxy terminating TLS on 443. The app
* parses `listen` with parseHostPort, which requires an explicit port and
* rejects the value outright, so the hint has to spell the port out.
*/
function hasExplicitPort(host: string): boolean {
if (host.startsWith("[")) {
// IPv6 literals are bracketed: [::1] has no port, [::1]:443 does.
const end = host.indexOf("]");
return end !== -1 && /^:\d{1,5}$/.test(host.slice(end + 1));
}
const colon = host.indexOf(":");
return colon !== -1 && colon === host.lastIndexOf(":") && /^\d{1,5}$/.test(host.slice(colon + 1));
}
function injectConnectionHint(
html: string,
req: Parameters<RequestHandler>[0],
@@ -257,8 +273,16 @@ function injectConnectionHint(
): string {
const host = typeof req.headers.host === "string" ? req.headers.host : "";
const useTls = req.protocol === "https";
let listen = host;
if (host && !hasExplicitPort(host)) {
let defaultPort = 80;
if (useTls) {
defaultPort = 443;
}
listen = `${host}:${defaultPort}`;
}
const hint = {
listen: host,
listen,
useTls,
label,
};
@@ -146,6 +146,7 @@ export const WORKSPACE_SEARCH_HIDDEN_DIRECTORIES = [
".agents",
".claude",
".codex",
".cursor",
".github",
".opencode",
".paseo",