@getpaseo/server 0.1.88 → 0.1.90
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.
- package/dist/server/server/agent/agent-manager.js +4 -1
- package/dist/server/server/agent/agent-prompt.js +4 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
- package/dist/server/server/agent/agent-storage.d.ts +22 -22
- package/dist/server/server/agent/agent-storage.js +2 -9
- package/dist/server/server/agent/create-agent/create.d.ts +2 -0
- package/dist/server/server/agent/create-agent/create.js +26 -7
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
- package/dist/server/server/agent/create-agent-mode.d.ts +3 -8
- package/dist/server/server/agent/create-agent-mode.js +16 -2
- package/dist/server/server/agent/import-sessions.js +1 -1
- package/dist/server/server/agent/mcp-server.d.ts +1 -0
- package/dist/server/server/agent/mcp-server.js +113 -70
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +2 -1
- package/dist/server/server/agent/provider-snapshot-manager.js +18 -2
- package/dist/server/server/agent/providers/acp-agent.d.ts +3 -3
- package/dist/server/server/agent/providers/acp-agent.js +18 -13
- package/dist/server/server/agent/providers/codex-app-server-agent.js +16 -22
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.js +69 -2
- package/dist/server/server/agent/providers/opencode-agent.js +19 -8
- package/dist/server/server/agent/providers/pi/agent.js +13 -0
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
- package/dist/server/server/agent/timeline-projection.js +30 -1
- package/dist/server/server/atomic-file.d.ts +3 -0
- package/dist/server/server/atomic-file.js +19 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +10 -2
- package/dist/server/server/bootstrap.d.ts +7 -2
- package/dist/server/server/bootstrap.js +154 -115
- package/dist/server/server/chat/chat-service.js +2 -4
- package/dist/server/server/config.js +41 -0
- package/dist/server/server/daemon-keypair.js +2 -2
- package/dist/server/server/loop-service.d.ts +26 -22
- package/dist/server/server/loop-service.js +27 -9
- package/dist/server/server/package-version.d.ts +2 -2
- package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
- package/dist/server/server/paseo-worktree-archive-service.js +28 -9
- package/dist/server/server/persisted-config.d.ts +84 -28
- package/dist/server/server/persisted-config.js +20 -3
- package/dist/server/server/pid-lock.d.ts +2 -2
- package/dist/server/server/private-files.d.ts +0 -1
- package/dist/server/server/private-files.js +0 -5
- package/dist/server/server/schedule/service.d.ts +6 -0
- package/dist/server/server/schedule/service.js +41 -18
- package/dist/server/server/schedule/store.js +3 -2
- package/dist/server/server/script-health-monitor.d.ts +4 -4
- package/dist/server/server/script-health-monitor.js +6 -6
- package/dist/server/server/script-proxy.d.ts +2 -39
- package/dist/server/server/script-proxy.js +1 -244
- package/dist/server/server/script-route-branch-handler.d.ts +2 -2
- package/dist/server/server/script-route-branch-handler.js +3 -37
- package/dist/server/server/script-status-projection.d.ts +6 -4
- package/dist/server/server/script-status-projection.js +85 -37
- package/dist/server/server/server-id.js +3 -3
- package/dist/server/server/service-proxy.d.ts +237 -0
- package/dist/server/server/service-proxy.js +714 -0
- package/dist/server/server/session.d.ts +12 -18
- package/dist/server/server/session.js +206 -117
- package/dist/server/server/speech/providers/local/worker-client.js +1 -11
- package/dist/server/server/websocket-server.d.ts +7 -4
- package/dist/server/server/websocket-server.js +9 -4
- package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
- package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
- package/dist/server/server/workspace-directory.d.ts +8 -0
- package/dist/server/server/workspace-directory.js +141 -11
- package/dist/server/server/workspace-git-service.d.ts +3 -0
- package/dist/server/server/workspace-git-service.js +53 -12
- package/dist/server/server/workspace-registry.d.ts +2 -2
- package/dist/server/server/workspace-registry.js +2 -6
- package/dist/server/server/workspace-service-env.d.ts +1 -0
- package/dist/server/server/workspace-service-env.js +23 -18
- package/dist/server/server/worktree/commands.d.ts +2 -0
- package/dist/server/server/worktree/commands.js +4 -1
- package/dist/server/server/worktree-bootstrap.d.ts +4 -3
- package/dist/server/server/worktree-bootstrap.js +14 -13
- package/dist/server/server/worktree-core.d.ts +1 -0
- package/dist/server/server/worktree-core.js +2 -0
- package/dist/server/server/worktree-session.d.ts +6 -2
- package/dist/server/server/worktree-session.js +3 -0
- package/dist/server/services/github-service.d.ts +1 -0
- package/dist/server/services/github-service.js +7 -1
- package/dist/server/utils/checkout-git.d.ts +6 -3
- package/dist/server/utils/checkout-git.js +40 -38
- package/dist/server/utils/worktree.d.ts +17 -12
- package/dist/server/utils/worktree.js +39 -22
- package/dist/src/server/persisted-config.js +20 -3
- package/dist/src/server/private-files.js +0 -5
- package/package.json +9 -7
- package/dist/server/server/editor-targets.d.ts +0 -18
- package/dist/server/server/editor-targets.js +0 -109
- package/dist/server/utils/script-hostname.d.ts +0 -8
- package/dist/server/utils/script-hostname.js +0 -14
|
@@ -7,15 +7,15 @@ export declare const pidLockInfoSchema: z.ZodObject<{
|
|
|
7
7
|
listen: z.ZodNullable<z.ZodString>;
|
|
8
8
|
desktopManaged: z.ZodOptional<z.ZodBoolean>;
|
|
9
9
|
}, "strip", z.ZodTypeAny, {
|
|
10
|
-
uid: number;
|
|
11
10
|
hostname: string;
|
|
11
|
+
uid: number;
|
|
12
12
|
startedAt: string;
|
|
13
13
|
listen: string | null;
|
|
14
14
|
pid: number;
|
|
15
15
|
desktopManaged?: boolean | undefined;
|
|
16
16
|
}, {
|
|
17
|
-
uid: number;
|
|
18
17
|
hostname: string;
|
|
18
|
+
uid: number;
|
|
19
19
|
startedAt: string;
|
|
20
20
|
listen: string | null;
|
|
21
21
|
pid: number;
|
|
@@ -2,6 +2,5 @@ export declare const PRIVATE_DIRECTORY_MODE = 448;
|
|
|
2
2
|
export declare const PRIVATE_FILE_MODE = 384;
|
|
3
3
|
export declare function ensurePrivateDirectory(directoryPath: string): void;
|
|
4
4
|
export declare function ensurePrivateFile(filePath: string): void;
|
|
5
|
-
export declare function writePrivateFileSync(filePath: string, data: string | NodeJS.ArrayBufferView): void;
|
|
6
5
|
export declare function writePrivateFileAtomicSync(filePath: string, data: string | NodeJS.ArrayBufferView): void;
|
|
7
6
|
//# sourceMappingURL=private-files.d.ts.map
|
|
@@ -21,11 +21,6 @@ export function ensurePrivateDirectory(directoryPath) {
|
|
|
21
21
|
export function ensurePrivateFile(filePath) {
|
|
22
22
|
chmodBestEffort(filePath, PRIVATE_FILE_MODE);
|
|
23
23
|
}
|
|
24
|
-
export function writePrivateFileSync(filePath, data) {
|
|
25
|
-
ensurePrivateDirectory(path.dirname(filePath));
|
|
26
|
-
writeFileSync(filePath, data, { mode: PRIVATE_FILE_MODE });
|
|
27
|
-
ensurePrivateFile(filePath);
|
|
28
|
-
}
|
|
29
24
|
export function writePrivateFileAtomicSync(filePath, data) {
|
|
30
25
|
ensurePrivateDirectory(path.dirname(filePath));
|
|
31
26
|
const tmpPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
2
|
import { AgentManager } from "../agent/agent-manager.js";
|
|
3
3
|
import type { AgentStorage } from "../agent/agent-storage.js";
|
|
4
|
+
import type { ProviderSnapshotManager } from "../agent/provider-snapshot-manager.js";
|
|
4
5
|
import type { CreateScheduleInput, ScheduleExecutionResult, ScheduleRun, StoredSchedule, UpdateScheduleInput } from "@getpaseo/protocol/schedule/types";
|
|
6
|
+
type CreateConfigResolver = Pick<ProviderSnapshotManager, "resolveCreateConfig">;
|
|
5
7
|
export interface ScheduleServiceOptions {
|
|
6
8
|
paseoHome: string;
|
|
7
9
|
logger: Logger;
|
|
8
10
|
agentManager: AgentManager;
|
|
9
11
|
agentStorage: AgentStorage;
|
|
12
|
+
providerSnapshotManager: CreateConfigResolver;
|
|
10
13
|
now?: () => Date;
|
|
11
14
|
runner?: (schedule: StoredSchedule, runId: string) => Promise<ScheduleExecutionResult>;
|
|
12
15
|
}
|
|
@@ -15,6 +18,7 @@ export declare class ScheduleService {
|
|
|
15
18
|
private readonly logger;
|
|
16
19
|
private readonly agentManager;
|
|
17
20
|
private readonly agentStorage;
|
|
21
|
+
private readonly createConfigResolver;
|
|
18
22
|
private readonly now;
|
|
19
23
|
private readonly runner;
|
|
20
24
|
private readonly runningScheduleIds;
|
|
@@ -37,5 +41,7 @@ export declare class ScheduleService {
|
|
|
37
41
|
private runSchedule;
|
|
38
42
|
private finishRun;
|
|
39
43
|
private executeSchedule;
|
|
44
|
+
private resolveProviderCreateConfig;
|
|
40
45
|
}
|
|
46
|
+
export {};
|
|
41
47
|
//# sourceMappingURL=service.d.ts.map
|
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { curateAgentActivity } from "../agent/activity-curator.js";
|
|
4
4
|
import { ensureAgentLoaded } from "../agent/agent-loading.js";
|
|
5
5
|
import { formatSystemNotificationPrompt } from "../agent/agent-prompt.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveCreateAgentTitles } from "../agent/create-agent-title.js";
|
|
7
7
|
import { ScheduleStore } from "./store.js";
|
|
8
8
|
import { computeNextRunAt, validateScheduleCadence } from "./cron.js";
|
|
9
9
|
const SCHEDULE_TICK_INTERVAL_MS = 1000;
|
|
@@ -113,6 +113,7 @@ export class ScheduleService {
|
|
|
113
113
|
this.logger = options.logger.child({ module: "schedule-service" });
|
|
114
114
|
this.agentManager = options.agentManager;
|
|
115
115
|
this.agentStorage = options.agentStorage;
|
|
116
|
+
this.createConfigResolver = options.providerSnapshotManager;
|
|
116
117
|
this.now = options.now ?? (() => new Date());
|
|
117
118
|
this.runner = options.runner ?? ((schedule, runId) => this.executeSchedule(schedule, runId));
|
|
118
119
|
}
|
|
@@ -418,8 +419,8 @@ export class ScheduleService {
|
|
|
418
419
|
await this.store.put(updated);
|
|
419
420
|
}
|
|
420
421
|
async executeSchedule(schedule, runId) {
|
|
421
|
-
const wrappedPrompt = formatSystemNotificationPrompt(buildScheduleFireBody(schedule, runId));
|
|
422
422
|
if (schedule.target.type === "agent") {
|
|
423
|
+
const wrappedPrompt = formatSystemNotificationPrompt(buildScheduleFireBody(schedule, runId));
|
|
423
424
|
const record = await this.agentStorage.get(schedule.target.agentId);
|
|
424
425
|
if (record?.archivedAt) {
|
|
425
426
|
throw new Error(`Agent ${schedule.target.agentId} is archived`);
|
|
@@ -443,30 +444,49 @@ export class ScheduleService {
|
|
|
443
444
|
}),
|
|
444
445
|
};
|
|
445
446
|
}
|
|
447
|
+
const targetConfig = schedule.target.config;
|
|
448
|
+
const resolvedUnattendedConfig = targetConfig.modeId
|
|
449
|
+
? { modeId: targetConfig.modeId, featureValues: targetConfig.featureValues }
|
|
450
|
+
: await this.resolveProviderCreateConfig({
|
|
451
|
+
provider: targetConfig.provider,
|
|
452
|
+
cwd: targetConfig.cwd,
|
|
453
|
+
requestedMode: undefined,
|
|
454
|
+
featureValues: targetConfig.featureValues,
|
|
455
|
+
parent: null,
|
|
456
|
+
unattended: true,
|
|
457
|
+
});
|
|
446
458
|
const config = {
|
|
447
|
-
provider:
|
|
448
|
-
cwd:
|
|
449
|
-
modeId:
|
|
450
|
-
model:
|
|
451
|
-
thinkingOptionId:
|
|
452
|
-
title:
|
|
453
|
-
approvalPolicy:
|
|
454
|
-
sandboxMode:
|
|
455
|
-
networkAccess:
|
|
456
|
-
webSearch:
|
|
457
|
-
featureValues:
|
|
458
|
-
extra:
|
|
459
|
-
systemPrompt:
|
|
460
|
-
mcpServers:
|
|
459
|
+
provider: targetConfig.provider,
|
|
460
|
+
cwd: targetConfig.cwd,
|
|
461
|
+
modeId: resolvedUnattendedConfig.modeId,
|
|
462
|
+
model: targetConfig.model,
|
|
463
|
+
thinkingOptionId: targetConfig.thinkingOptionId,
|
|
464
|
+
title: targetConfig.title,
|
|
465
|
+
approvalPolicy: targetConfig.approvalPolicy,
|
|
466
|
+
sandboxMode: targetConfig.sandboxMode,
|
|
467
|
+
networkAccess: targetConfig.networkAccess,
|
|
468
|
+
webSearch: targetConfig.webSearch,
|
|
469
|
+
featureValues: resolvedUnattendedConfig.featureValues,
|
|
470
|
+
extra: targetConfig.extra,
|
|
471
|
+
systemPrompt: targetConfig.systemPrompt,
|
|
472
|
+
mcpServers: targetConfig.mcpServers,
|
|
461
473
|
};
|
|
474
|
+
const { provisionalTitle } = resolveCreateAgentTitles({
|
|
475
|
+
configTitle: config.title,
|
|
476
|
+
initialPrompt: schedule.prompt,
|
|
477
|
+
});
|
|
462
478
|
const labels = {
|
|
463
479
|
"paseo.schedule-id": schedule.id,
|
|
464
480
|
"paseo.schedule-run": runId,
|
|
465
481
|
};
|
|
466
|
-
const agent = await this.agentManager.createAgent(config, undefined, {
|
|
482
|
+
const agent = await this.agentManager.createAgent(config, undefined, {
|
|
483
|
+
labels,
|
|
484
|
+
initialPrompt: schedule.prompt,
|
|
485
|
+
initialTitle: provisionalTitle,
|
|
486
|
+
});
|
|
467
487
|
let result;
|
|
468
488
|
try {
|
|
469
|
-
result = await this.agentManager.runAgent(agent.id,
|
|
489
|
+
result = await this.agentManager.runAgent(agent.id, schedule.prompt);
|
|
470
490
|
}
|
|
471
491
|
catch (error) {
|
|
472
492
|
try {
|
|
@@ -488,5 +508,8 @@ export class ScheduleService {
|
|
|
488
508
|
}),
|
|
489
509
|
};
|
|
490
510
|
}
|
|
511
|
+
async resolveProviderCreateConfig(input) {
|
|
512
|
+
return this.createConfigResolver.resolveCreateConfig(input);
|
|
513
|
+
}
|
|
491
514
|
}
|
|
492
515
|
//# sourceMappingURL=service.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, readdir, rm
|
|
2
|
+
import { mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { StoredScheduleSchema } from "@getpaseo/protocol/schedule/types";
|
|
5
|
+
import { writeJsonFileAtomic } from "../atomic-file.js";
|
|
5
6
|
function generateScheduleId() {
|
|
6
7
|
return randomBytes(4).toString("hex");
|
|
7
8
|
}
|
|
@@ -46,7 +47,7 @@ export class ScheduleStore {
|
|
|
46
47
|
}
|
|
47
48
|
async put(schedule) {
|
|
48
49
|
await this.ensureDir();
|
|
49
|
-
await
|
|
50
|
+
await writeJsonFileAtomic(this.filePath(schedule.id), schedule);
|
|
50
51
|
}
|
|
51
52
|
async delete(id) {
|
|
52
53
|
await this.ensureDir();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
|
2
2
|
export type ScriptHealthState = "pending" | "healthy" | "unhealthy";
|
|
3
3
|
export interface ScriptHealthEntry {
|
|
4
4
|
scriptName: string;
|
|
@@ -7,7 +7,7 @@ export interface ScriptHealthEntry {
|
|
|
7
7
|
health: ScriptHealthState;
|
|
8
8
|
}
|
|
9
9
|
export declare class ScriptHealthMonitor {
|
|
10
|
-
private readonly
|
|
10
|
+
private readonly serviceProxy;
|
|
11
11
|
private readonly onChange;
|
|
12
12
|
private readonly pollIntervalMs;
|
|
13
13
|
private readonly probeTimeoutMs;
|
|
@@ -17,8 +17,8 @@ export declare class ScriptHealthMonitor {
|
|
|
17
17
|
private readonly lastEmittedSnapshots;
|
|
18
18
|
private intervalHandle;
|
|
19
19
|
private pollInFlight;
|
|
20
|
-
constructor({
|
|
21
|
-
|
|
20
|
+
constructor({ serviceProxy, onChange, pollIntervalMs, probeTimeoutMs, graceMs, failuresBeforeStopped, }: {
|
|
21
|
+
serviceProxy: ServiceProxySubsystem;
|
|
22
22
|
onChange: (workspaceId: string, scripts: ScriptHealthEntry[]) => void;
|
|
23
23
|
pollIntervalMs?: number;
|
|
24
24
|
probeTimeoutMs?: number;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
export class ScriptHealthMonitor {
|
|
3
|
-
constructor({
|
|
3
|
+
constructor({ serviceProxy, onChange, pollIntervalMs = 3000, probeTimeoutMs = 500, graceMs = 5000, failuresBeforeStopped = 2, }) {
|
|
4
4
|
this.routeStates = new Map();
|
|
5
5
|
this.lastEmittedSnapshots = new Map();
|
|
6
6
|
this.intervalHandle = null;
|
|
7
7
|
this.pollInFlight = false;
|
|
8
|
-
this.
|
|
8
|
+
this.serviceProxy = serviceProxy;
|
|
9
9
|
this.onChange = onChange;
|
|
10
10
|
this.pollIntervalMs = pollIntervalMs;
|
|
11
11
|
this.probeTimeoutMs = probeTimeoutMs;
|
|
@@ -17,7 +17,7 @@ export class ScriptHealthMonitor {
|
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
const now = Date.now();
|
|
20
|
-
for (const route of this.
|
|
20
|
+
for (const route of this.serviceProxy.getHealthCheckTargets()) {
|
|
21
21
|
this.getOrCreateState(route, now);
|
|
22
22
|
}
|
|
23
23
|
this.intervalHandle = setInterval(() => {
|
|
@@ -45,7 +45,7 @@ export class ScriptHealthMonitor {
|
|
|
45
45
|
}
|
|
46
46
|
this.pollInFlight = true;
|
|
47
47
|
try {
|
|
48
|
-
const routes = this.
|
|
48
|
+
const routes = this.serviceProxy.getHealthCheckTargets();
|
|
49
49
|
const activeHostnames = new Set(routes.map((route) => route.hostname));
|
|
50
50
|
const changedWorkspaceIds = new Set();
|
|
51
51
|
const now = Date.now();
|
|
@@ -110,7 +110,7 @@ export class ScriptHealthMonitor {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
buildWorkspaceScriptList(workspaceId) {
|
|
113
|
-
return this.
|
|
113
|
+
return this.serviceProxy.getWorkspaceHealthTargets(workspaceId).flatMap((route) => {
|
|
114
114
|
const state = this.routeStates.get(route.hostname);
|
|
115
115
|
if (!state) {
|
|
116
116
|
return [];
|
|
@@ -123,7 +123,7 @@ export class ScriptHealthMonitor {
|
|
|
123
123
|
if (state) {
|
|
124
124
|
return state.health;
|
|
125
125
|
}
|
|
126
|
-
const route = this.
|
|
126
|
+
const route = this.serviceProxy.getHealthTargetForHostname(hostname);
|
|
127
127
|
if (!route) {
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import type { Logger } from "pino";
|
|
4
|
-
import type { RequestHandler } from "express";
|
|
5
|
-
export interface ScriptRoute {
|
|
6
|
-
hostname: string;
|
|
7
|
-
port: number;
|
|
8
|
-
}
|
|
9
|
-
export interface ScriptRouteEntry extends ScriptRoute {
|
|
10
|
-
workspaceId: string;
|
|
11
|
-
projectSlug: string;
|
|
12
|
-
scriptName: string;
|
|
13
|
-
}
|
|
14
|
-
export declare class ScriptRouteStore {
|
|
15
|
-
private routes;
|
|
16
|
-
private workspaceHostnames;
|
|
17
|
-
registerRoute(entry: ScriptRouteEntry): void;
|
|
18
|
-
removeRoute(hostname: string): void;
|
|
19
|
-
removeRouteForWorkspaceScript(params: {
|
|
20
|
-
workspaceId: string;
|
|
21
|
-
scriptName: string;
|
|
22
|
-
}): void;
|
|
23
|
-
removeRoutesForPort(port: number): void;
|
|
24
|
-
findRoute(host: string): ScriptRoute | null;
|
|
25
|
-
getRouteEntry(hostname: string): ScriptRouteEntry | null;
|
|
26
|
-
listRoutes(): ScriptRouteEntry[];
|
|
27
|
-
listRoutesForWorkspace(workspaceId: string): ScriptRouteEntry[];
|
|
28
|
-
private addHostnameToWorkspaceIndex;
|
|
29
|
-
private removeHostnameFromWorkspaceIndex;
|
|
30
|
-
}
|
|
31
|
-
export declare function createScriptProxyMiddleware({ routeStore, logger, }: {
|
|
32
|
-
routeStore: ScriptRouteStore;
|
|
33
|
-
logger: Logger;
|
|
34
|
-
}): RequestHandler;
|
|
35
|
-
export declare function createScriptProxyUpgradeHandler({ routeStore, logger, }: {
|
|
36
|
-
routeStore: ScriptRouteStore;
|
|
37
|
-
logger: Logger;
|
|
38
|
-
}): (req: IncomingMessage, socket: net.Socket, head: Buffer) => void;
|
|
39
|
-
export declare function findFreePort(): Promise<number>;
|
|
1
|
+
export { createScriptProxyMiddleware, createScriptProxyUpgradeHandler, findFreePort, ScriptRouteStore, } from "./service-proxy.js";
|
|
2
|
+
export type { ScriptRoute, ScriptRouteEntry } from "./service-proxy.js";
|
|
40
3
|
//# sourceMappingURL=script-proxy.d.ts.map
|
|
@@ -1,245 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import net from "node:net";
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Hop-by-hop headers that must not be forwarded
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
const HOP_BY_HOP_HEADERS = new Set([
|
|
7
|
-
"connection",
|
|
8
|
-
"transfer-encoding",
|
|
9
|
-
"keep-alive",
|
|
10
|
-
"upgrade",
|
|
11
|
-
"proxy-connection",
|
|
12
|
-
"proxy-authenticate",
|
|
13
|
-
"proxy-authorization",
|
|
14
|
-
"te",
|
|
15
|
-
"trailer",
|
|
16
|
-
]);
|
|
17
|
-
export class ScriptRouteStore {
|
|
18
|
-
constructor() {
|
|
19
|
-
this.routes = new Map();
|
|
20
|
-
this.workspaceHostnames = new Map();
|
|
21
|
-
}
|
|
22
|
-
registerRoute(entry) {
|
|
23
|
-
const previous = this.routes.get(entry.hostname);
|
|
24
|
-
if (previous) {
|
|
25
|
-
this.removeHostnameFromWorkspaceIndex(previous.workspaceId, previous.hostname);
|
|
26
|
-
}
|
|
27
|
-
const storedEntry = { ...entry };
|
|
28
|
-
this.routes.set(storedEntry.hostname, storedEntry);
|
|
29
|
-
this.addHostnameToWorkspaceIndex(storedEntry.workspaceId, storedEntry.hostname);
|
|
30
|
-
}
|
|
31
|
-
removeRoute(hostname) {
|
|
32
|
-
const entry = this.routes.get(hostname);
|
|
33
|
-
if (!entry) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
this.routes.delete(hostname);
|
|
37
|
-
this.removeHostnameFromWorkspaceIndex(entry.workspaceId, hostname);
|
|
38
|
-
}
|
|
39
|
-
removeRouteForWorkspaceScript(params) {
|
|
40
|
-
const routes = this.listRoutesForWorkspace(params.workspaceId);
|
|
41
|
-
const route = routes.find((entry) => entry.scriptName === params.scriptName);
|
|
42
|
-
if (!route) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
this.removeRoute(route.hostname);
|
|
46
|
-
}
|
|
47
|
-
removeRoutesForPort(port) {
|
|
48
|
-
for (const [hostname, entry] of this.routes) {
|
|
49
|
-
if (entry.port === port) {
|
|
50
|
-
this.routes.delete(hostname);
|
|
51
|
-
this.removeHostnameFromWorkspaceIndex(entry.workspaceId, hostname);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
findRoute(host) {
|
|
56
|
-
// Strip port suffix from the Host header value
|
|
57
|
-
const hostname = host.replace(/:\d+$/, "");
|
|
58
|
-
// 1. Exact match
|
|
59
|
-
const exactRoute = this.routes.get(hostname);
|
|
60
|
-
if (exactRoute !== undefined) {
|
|
61
|
-
return { hostname: exactRoute.hostname, port: exactRoute.port };
|
|
62
|
-
}
|
|
63
|
-
// 2. Subdomain match — walk up the labels looking for a registered parent
|
|
64
|
-
const parts = hostname.split(".");
|
|
65
|
-
for (let i = 1; i < parts.length; i++) {
|
|
66
|
-
const candidate = parts.slice(i).join(".");
|
|
67
|
-
const candidateRoute = this.routes.get(candidate);
|
|
68
|
-
if (candidateRoute !== undefined) {
|
|
69
|
-
return { hostname: candidateRoute.hostname, port: candidateRoute.port };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
getRouteEntry(hostname) {
|
|
75
|
-
const entry = this.routes.get(hostname);
|
|
76
|
-
return entry ? { ...entry } : null;
|
|
77
|
-
}
|
|
78
|
-
listRoutes() {
|
|
79
|
-
return Array.from(this.routes.values()).map((entry) => Object.assign({}, entry));
|
|
80
|
-
}
|
|
81
|
-
listRoutesForWorkspace(workspaceId) {
|
|
82
|
-
const hostnames = this.workspaceHostnames.get(workspaceId);
|
|
83
|
-
if (!hostnames) {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
const routes = [];
|
|
87
|
-
for (const hostname of hostnames) {
|
|
88
|
-
const entry = this.routes.get(hostname);
|
|
89
|
-
if (entry) {
|
|
90
|
-
routes.push({ ...entry });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return routes;
|
|
94
|
-
}
|
|
95
|
-
addHostnameToWorkspaceIndex(workspaceId, hostname) {
|
|
96
|
-
const hostnames = this.workspaceHostnames.get(workspaceId) ?? new Set();
|
|
97
|
-
hostnames.add(hostname);
|
|
98
|
-
this.workspaceHostnames.set(workspaceId, hostnames);
|
|
99
|
-
}
|
|
100
|
-
removeHostnameFromWorkspaceIndex(workspaceId, hostname) {
|
|
101
|
-
const hostnames = this.workspaceHostnames.get(workspaceId);
|
|
102
|
-
if (!hostnames) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
hostnames.delete(hostname);
|
|
106
|
-
if (hostnames.size === 0) {
|
|
107
|
-
this.workspaceHostnames.delete(workspaceId);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// Helpers
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
function stripHopByHopHeaders(rawHeaders) {
|
|
115
|
-
const out = {};
|
|
116
|
-
for (const [key, value] of Object.entries(rawHeaders)) {
|
|
117
|
-
if (value === undefined)
|
|
118
|
-
continue;
|
|
119
|
-
if (HOP_BY_HOP_HEADERS.has(key.toLowerCase()))
|
|
120
|
-
continue;
|
|
121
|
-
out[key] = value;
|
|
122
|
-
}
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
// createScriptProxyMiddleware
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
export function createScriptProxyMiddleware({ routeStore, logger, }) {
|
|
129
|
-
return (req, res, next) => {
|
|
130
|
-
const hostHeader = req.headers.host;
|
|
131
|
-
if (!hostHeader) {
|
|
132
|
-
next();
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const route = routeStore.findRoute(hostHeader);
|
|
136
|
-
if (!route) {
|
|
137
|
-
next();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const forwardedHeaders = stripHopByHopHeaders(req.headers);
|
|
141
|
-
forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
|
|
142
|
-
forwardedHeaders["x-forwarded-host"] = hostHeader.replace(/:\d+$/, "");
|
|
143
|
-
forwardedHeaders["x-forwarded-proto"] = req.protocol;
|
|
144
|
-
const proxyReq = http.request({
|
|
145
|
-
hostname: "127.0.0.1",
|
|
146
|
-
port: route.port,
|
|
147
|
-
path: req.originalUrl,
|
|
148
|
-
method: req.method,
|
|
149
|
-
headers: forwardedHeaders,
|
|
150
|
-
}, (proxyRes) => {
|
|
151
|
-
const responseHeaders = stripHopByHopHeaders(proxyRes.headers);
|
|
152
|
-
res.writeHead(proxyRes.statusCode ?? 502, responseHeaders);
|
|
153
|
-
proxyRes.pipe(res, { end: true });
|
|
154
|
-
});
|
|
155
|
-
proxyReq.on("error", (err) => {
|
|
156
|
-
logger.warn({ err, hostname: route.hostname, port: route.port }, "Script proxy: upstream unreachable");
|
|
157
|
-
if (!res.headersSent) {
|
|
158
|
-
res.writeHead(502, { "content-type": "text/plain" });
|
|
159
|
-
res.end("502 Bad Gateway");
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
req.pipe(proxyReq, { end: true });
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// createScriptProxyUpgradeHandler
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
export function createScriptProxyUpgradeHandler({ routeStore, logger, }) {
|
|
169
|
-
return (req, socket, head) => {
|
|
170
|
-
const hostHeader = req.headers.host;
|
|
171
|
-
if (!hostHeader) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const route = routeStore.findRoute(hostHeader);
|
|
175
|
-
if (!route) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const targetSocket = net.connect({ host: "127.0.0.1", port: route.port }, () => {
|
|
179
|
-
// Reconstruct the raw HTTP upgrade request to send to the target
|
|
180
|
-
const forwardedHeaders = stripHopByHopHeaders(req.headers);
|
|
181
|
-
forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
|
|
182
|
-
forwardedHeaders["x-forwarded-host"] = hostHeader.replace(/:\d+$/, "");
|
|
183
|
-
forwardedHeaders["x-forwarded-proto"] = "http";
|
|
184
|
-
// Re-include upgrade and connection headers — they are required for
|
|
185
|
-
// WebSocket handshake even though they are hop-by-hop.
|
|
186
|
-
forwardedHeaders["connection"] = "Upgrade";
|
|
187
|
-
forwardedHeaders["upgrade"] = req.headers.upgrade ?? "websocket";
|
|
188
|
-
const headerLines = [];
|
|
189
|
-
headerLines.push(`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`);
|
|
190
|
-
for (const [key, value] of Object.entries(forwardedHeaders)) {
|
|
191
|
-
if (Array.isArray(value)) {
|
|
192
|
-
for (const v of value) {
|
|
193
|
-
headerLines.push(`${key}: ${v}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
headerLines.push(`${key}: ${value}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
headerLines.push("\r\n");
|
|
201
|
-
targetSocket.write(headerLines.join("\r\n"));
|
|
202
|
-
if (head.length > 0) {
|
|
203
|
-
targetSocket.write(head);
|
|
204
|
-
}
|
|
205
|
-
// Pipe in both directions
|
|
206
|
-
targetSocket.pipe(socket);
|
|
207
|
-
socket.pipe(targetSocket);
|
|
208
|
-
});
|
|
209
|
-
targetSocket.on("error", (err) => {
|
|
210
|
-
logger.warn({ err, hostname: route.hostname, port: route.port }, "Script proxy: WebSocket upstream unreachable");
|
|
211
|
-
socket.end();
|
|
212
|
-
});
|
|
213
|
-
socket.on("error", () => {
|
|
214
|
-
targetSocket.destroy();
|
|
215
|
-
});
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// findFreePort
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
export function findFreePort() {
|
|
222
|
-
return new Promise((resolve, reject) => {
|
|
223
|
-
const server = net.createServer();
|
|
224
|
-
server.unref();
|
|
225
|
-
server.listen(0, "127.0.0.1", () => {
|
|
226
|
-
const address = server.address();
|
|
227
|
-
if (!address || typeof address === "string") {
|
|
228
|
-
server.close();
|
|
229
|
-
reject(new Error("Failed to get assigned port"));
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const { port } = address;
|
|
233
|
-
server.close((err) => {
|
|
234
|
-
if (err) {
|
|
235
|
-
reject(err);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
resolve(port);
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
server.on("error", reject);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
1
|
+
export { createScriptProxyMiddleware, createScriptProxyUpgradeHandler, findFreePort, ScriptRouteStore, } from "./service-proxy.js";
|
|
245
2
|
//# sourceMappingURL=script-proxy.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
|
3
3
|
interface BranchChangeRouteHandlerOptions {
|
|
4
|
-
|
|
4
|
+
serviceProxy: ServiceProxySubsystem;
|
|
5
5
|
onRoutesChanged: (workspaceId: string) => void;
|
|
6
6
|
logger?: Logger;
|
|
7
7
|
}
|
|
@@ -1,44 +1,10 @@
|
|
|
1
|
-
import { buildScriptHostname } from "../utils/script-hostname.js";
|
|
2
1
|
export function createBranchChangeRouteHandler(options) {
|
|
3
2
|
return (workspaceId, _oldBranch, newBranch) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (routes.length === 0) {
|
|
3
|
+
const changed = options.serviceProxy.replaceWorkspaceBranchRoutes({ workspaceId, newBranch });
|
|
4
|
+
if (!changed) {
|
|
7
5
|
return;
|
|
8
6
|
}
|
|
9
|
-
|
|
10
|
-
for (const route of routes) {
|
|
11
|
-
const newHostname = buildScriptHostname({
|
|
12
|
-
projectSlug: route.projectSlug,
|
|
13
|
-
branchName: newBranch,
|
|
14
|
-
scriptName: route.scriptName,
|
|
15
|
-
});
|
|
16
|
-
if (newHostname !== route.hostname) {
|
|
17
|
-
updates.push({
|
|
18
|
-
oldHostname: route.hostname,
|
|
19
|
-
newHostname,
|
|
20
|
-
route,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
if (updates.length === 0) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
for (const { oldHostname, newHostname, route } of updates) {
|
|
28
|
-
options.routeStore.removeRoute(oldHostname);
|
|
29
|
-
options.routeStore.registerRoute({
|
|
30
|
-
hostname: newHostname,
|
|
31
|
-
port: route.port,
|
|
32
|
-
workspaceId: route.workspaceId,
|
|
33
|
-
projectSlug: route.projectSlug,
|
|
34
|
-
scriptName: route.scriptName,
|
|
35
|
-
});
|
|
36
|
-
options.logger?.info({
|
|
37
|
-
oldHostname,
|
|
38
|
-
newHostname,
|
|
39
|
-
scriptName: route.scriptName,
|
|
40
|
-
}, "Updated script route for branch rename");
|
|
41
|
-
}
|
|
7
|
+
options.logger?.info({ workspaceId, newBranch }, "Updated service proxy routes for branch rename");
|
|
42
8
|
options.onRoutesChanged(workspaceId);
|
|
43
9
|
};
|
|
44
10
|
}
|
|
@@ -2,7 +2,7 @@ import type { Logger } from "pino";
|
|
|
2
2
|
import type { SessionOutboundMessage, WorkspaceScriptPayload } from "@getpaseo/protocol/messages";
|
|
3
3
|
import type { PaseoConfig } from "@getpaseo/protocol/paseo-config-schema";
|
|
4
4
|
import type { ScriptHealthEntry, ScriptHealthState } from "./script-health-monitor.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
|
6
6
|
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
|
7
7
|
interface SessionEmitter {
|
|
8
8
|
emit(message: SessionOutboundMessage): void;
|
|
@@ -11,9 +11,10 @@ interface BuildWorkspaceScriptPayloadsOptions {
|
|
|
11
11
|
workspaceId: string;
|
|
12
12
|
workspaceDirectory: string;
|
|
13
13
|
paseoConfig: PaseoConfig | null;
|
|
14
|
-
|
|
14
|
+
serviceProxy: ServiceProxySubsystem;
|
|
15
15
|
runtimeStore: WorkspaceScriptRuntimeStore;
|
|
16
16
|
daemonPort: number | null;
|
|
17
|
+
serviceProxyPublicBaseUrl?: string | null;
|
|
17
18
|
gitMetadata?: {
|
|
18
19
|
projectSlug: string;
|
|
19
20
|
currentBranch: string | null;
|
|
@@ -22,11 +23,12 @@ interface BuildWorkspaceScriptPayloadsOptions {
|
|
|
22
23
|
}
|
|
23
24
|
export declare function readPaseoConfigForProjection(workspaceDirectory: string, logger: Logger): PaseoConfig | null;
|
|
24
25
|
export declare function buildWorkspaceScriptPayloads(options: BuildWorkspaceScriptPayloadsOptions): WorkspaceScriptPayload[];
|
|
25
|
-
export declare function createScriptStatusEmitter({ sessions,
|
|
26
|
+
export declare function createScriptStatusEmitter({ sessions, serviceProxy, runtimeStore, daemonPort, serviceProxyPublicBaseUrl, resolveWorkspaceDirectory, logger, }: {
|
|
26
27
|
sessions: () => SessionEmitter[];
|
|
27
|
-
|
|
28
|
+
serviceProxy: ServiceProxySubsystem;
|
|
28
29
|
runtimeStore: WorkspaceScriptRuntimeStore;
|
|
29
30
|
daemonPort: number | null | (() => number | null);
|
|
31
|
+
serviceProxyPublicBaseUrl?: string | null;
|
|
30
32
|
resolveWorkspaceDirectory: (workspaceId: string) => string | null | Promise<string | null>;
|
|
31
33
|
logger: Logger;
|
|
32
34
|
}): (workspaceId: string, scripts: ScriptHealthEntry[]) => void;
|