@getpaseo/server 0.1.88 → 0.1.89
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-storage.d.ts +22 -22
- package/dist/server/server/agent/create-agent/create.d.ts +2 -0
- package/dist/server/server/agent/create-agent/create.js +16 -5
- 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/mcp-server.d.ts +1 -0
- package/dist/server/server/agent/mcp-server.js +113 -70
- 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/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +6 -1
- package/dist/server/server/bootstrap.d.ts +7 -2
- package/dist/server/server/bootstrap.js +152 -115
- package/dist/server/server/config.js +41 -0
- package/dist/server/server/loop-service.d.ts +22 -22
- 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 +17 -0
- package/dist/server/server/pid-lock.d.ts +2 -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/service-proxy.d.ts +237 -0
- package/dist/server/server/service-proxy.js +714 -0
- package/dist/server/server/session.d.ts +7 -3
- package/dist/server/server/session.js +22 -10
- package/dist/server/server/websocket-server.d.ts +7 -4
- package/dist/server/server/websocket-server.js +9 -4
- package/dist/server/server/workspace-directory.js +4 -0
- 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-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 -2
- package/dist/server/utils/checkout-git.js +17 -7
- package/dist/server/utils/worktree.d.ts +17 -12
- package/dist/server/utils/worktree.js +39 -22
- package/dist/src/server/persisted-config.js +17 -0
- package/package.json +5 -5
- package/dist/server/utils/script-hostname.d.ts +0 -8
- package/dist/server/utils/script-hostname.js +0 -14
|
@@ -21,7 +21,7 @@ import type { PushNotificationSender } from "./push/notifications.js";
|
|
|
21
21
|
import type { AgentClient, AgentProvider } from "./agent/agent-sdk-types.js";
|
|
22
22
|
import type { AgentProviderRuntimeSettingsMap, ProviderOverride } from "./agent/provider-launch-config.js";
|
|
23
23
|
import type { PersistedConfig } from "./persisted-config.js";
|
|
24
|
-
import {
|
|
24
|
+
import { type ServiceProxySubsystem } from "./service-proxy.js";
|
|
25
25
|
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
|
26
26
|
import { type HostnamesConfig } from "./hostnames.js";
|
|
27
27
|
import { type DaemonAuthConfig } from "./auth.js";
|
|
@@ -49,6 +49,7 @@ export type DaemonLifecycleIntent = {
|
|
|
49
49
|
export interface PaseoDaemonConfig {
|
|
50
50
|
listen: string;
|
|
51
51
|
paseoHome: string;
|
|
52
|
+
worktreesRoot?: string;
|
|
52
53
|
corsAllowedOrigins: string[];
|
|
53
54
|
allowedHosts?: HostnamesConfig;
|
|
54
55
|
hostnames?: HostnamesConfig;
|
|
@@ -66,6 +67,10 @@ export interface PaseoDaemonConfig {
|
|
|
66
67
|
relayPublicEndpoint?: string;
|
|
67
68
|
relayUseTls?: boolean;
|
|
68
69
|
relayPublicUseTls?: boolean;
|
|
70
|
+
serviceProxy?: {
|
|
71
|
+
publicBaseUrl: string | null;
|
|
72
|
+
standaloneListen: string | null;
|
|
73
|
+
};
|
|
69
74
|
appBaseUrl?: string;
|
|
70
75
|
auth?: DaemonAuthConfig;
|
|
71
76
|
openai?: PaseoOpenAIConfig;
|
|
@@ -93,7 +98,7 @@ export interface PaseoDaemon {
|
|
|
93
98
|
agentManager: AgentManager;
|
|
94
99
|
agentStorage: AgentStorage;
|
|
95
100
|
terminalManager: TerminalManager;
|
|
96
|
-
|
|
101
|
+
serviceProxy: ServiceProxySubsystem;
|
|
97
102
|
scriptRuntimeStore: WorkspaceScriptRuntimeStore;
|
|
98
103
|
start(): Promise<void>;
|
|
99
104
|
stop(): Promise<void>;
|
|
@@ -99,7 +99,7 @@ import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js";
|
|
|
99
99
|
import { startRelayTransport } from "./relay-transport.js";
|
|
100
100
|
import { getOrCreateServerId } from "./server-id.js";
|
|
101
101
|
import { resolveDaemonVersion } from "./daemon-version.js";
|
|
102
|
-
import {
|
|
102
|
+
import { createServiceProxySubsystem } from "./service-proxy.js";
|
|
103
103
|
import { ScriptHealthMonitor } from "./script-health-monitor.js";
|
|
104
104
|
import { createScriptStatusEmitter } from "./script-status-projection.js";
|
|
105
105
|
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
|
@@ -187,30 +187,42 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
187
187
|
const app = express();
|
|
188
188
|
let boundListenTarget = null;
|
|
189
189
|
let workspaceRegistry = null;
|
|
190
|
-
const
|
|
190
|
+
const serviceProxyPublicBaseUrl = config.serviceProxy?.publicBaseUrl
|
|
191
|
+
? config.serviceProxy.publicBaseUrl
|
|
192
|
+
: null;
|
|
193
|
+
const serviceProxy = createServiceProxySubsystem({
|
|
194
|
+
logger,
|
|
195
|
+
publicBaseUrl: serviceProxyPublicBaseUrl,
|
|
196
|
+
});
|
|
191
197
|
const scriptRuntimeStore = new WorkspaceScriptRuntimeStore();
|
|
192
198
|
const configuredHostnames = config.hostnames ?? config.allowedHosts;
|
|
193
199
|
let wsServer = null;
|
|
200
|
+
let serviceProxyListenTarget = null;
|
|
194
201
|
const scriptHealthMonitor = new ScriptHealthMonitor({
|
|
195
|
-
|
|
202
|
+
serviceProxy,
|
|
196
203
|
onChange: createScriptStatusEmitter({
|
|
197
204
|
sessions: () => wsServer?.listActiveSessions().map((session) => ({
|
|
198
205
|
emit: (message) => session.emitServerMessage(message),
|
|
199
206
|
})) ?? [],
|
|
200
|
-
|
|
207
|
+
serviceProxy,
|
|
201
208
|
runtimeStore: scriptRuntimeStore,
|
|
202
209
|
daemonPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
|
|
203
210
|
resolveWorkspaceDirectory: async (workspaceId) => (await workspaceRegistry?.get(workspaceId))?.cwd ?? null,
|
|
204
211
|
logger,
|
|
212
|
+
serviceProxyPublicBaseUrl,
|
|
205
213
|
}),
|
|
206
214
|
});
|
|
207
215
|
const handleBranchChange = createBranchChangeRouteHandler({
|
|
208
|
-
|
|
216
|
+
serviceProxy,
|
|
209
217
|
onRoutesChanged: (workspaceId) => {
|
|
210
218
|
scriptHealthMonitor.invalidateWorkspace(workspaceId);
|
|
211
219
|
},
|
|
212
220
|
logger,
|
|
213
221
|
});
|
|
222
|
+
// Service proxy classifies service hosts before daemon auth/route fallthrough.
|
|
223
|
+
// Registered service hosts proxy directly; known service namespaces without a
|
|
224
|
+
// route return 404 and never reach daemon APIs.
|
|
225
|
+
app.use(serviceProxy.middleware());
|
|
214
226
|
// Host allowlist / DNS rebinding protection (vite-like semantics).
|
|
215
227
|
// For non-TCP (unix sockets), skip host validation.
|
|
216
228
|
if (listenTarget.type === "tcp") {
|
|
@@ -254,10 +266,6 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
254
266
|
app.use(createRequireBearerMiddleware(config.auth, (context) => {
|
|
255
267
|
logger.warn(context, "Rejected HTTP request with invalid daemon password");
|
|
256
268
|
}));
|
|
257
|
-
// Script proxy — intercepts requests for registered *.localhost hostnames
|
|
258
|
-
// and forwards them to the corresponding local script port. Placed after
|
|
259
|
-
// host/CORS/auth checks but before the rest of the routes.
|
|
260
|
-
app.use(createScriptProxyMiddleware({ routeStore: scriptRouteStore, logger }));
|
|
261
269
|
// Serve static files from public directory
|
|
262
270
|
app.use("/public", express.static(staticDir));
|
|
263
271
|
// Middleware
|
|
@@ -331,11 +339,10 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
331
339
|
// VoiceAssistantWebSocketServer attaches its own "upgrade" listener so that
|
|
332
340
|
// script-bound upgrades are forwarded first. The handler is a no-op for
|
|
333
341
|
// requests that don't match a registered script route.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
httpServer.on("upgrade", scriptProxyUpgradeHandler);
|
|
342
|
+
httpServer.on("upgrade", serviceProxy.upgradeHandler({ passthroughUnknown: true }));
|
|
343
|
+
if (config.serviceProxy?.standaloneListen) {
|
|
344
|
+
serviceProxyListenTarget = parseListenString(config.serviceProxy.standaloneListen);
|
|
345
|
+
}
|
|
339
346
|
const agentStorage = new AgentStorage(config.agentStoragePath, logger);
|
|
340
347
|
const projectRegistry = new FileBackedProjectRegistry(path.join(config.paseoHome, "projects", "projects.json"), logger);
|
|
341
348
|
workspaceRegistry = new FileBackedWorkspaceRegistry(path.join(config.paseoHome, "projects", "workspaces.json"), logger);
|
|
@@ -348,6 +355,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
348
355
|
const workspaceGitService = new WorkspaceGitServiceImpl({
|
|
349
356
|
logger,
|
|
350
357
|
paseoHome: config.paseoHome,
|
|
358
|
+
worktreesRoot: config.worktreesRoot,
|
|
351
359
|
deps: {
|
|
352
360
|
github,
|
|
353
361
|
},
|
|
@@ -467,6 +475,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
467
475
|
};
|
|
468
476
|
setupAutoArchiveOnMerge({
|
|
469
477
|
paseoHome: config.paseoHome,
|
|
478
|
+
worktreesRoot: config.worktreesRoot,
|
|
470
479
|
daemonConfigStore,
|
|
471
480
|
workspaceGitService,
|
|
472
481
|
github,
|
|
@@ -501,6 +510,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
501
510
|
createPaseoWorktree: async (input, serviceOptions) => {
|
|
502
511
|
return createPaseoWorktreeWorkflow({
|
|
503
512
|
paseoHome: config.paseoHome,
|
|
513
|
+
worktreesRoot: config.worktreesRoot,
|
|
504
514
|
createPaseoWorktree: async (workflowInput, workflowOptions) => {
|
|
505
515
|
return createRegisteredPaseoWorktree(workflowInput, {
|
|
506
516
|
github,
|
|
@@ -530,14 +540,16 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
530
540
|
sessionLogger: logger,
|
|
531
541
|
terminalManager,
|
|
532
542
|
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
|
533
|
-
|
|
543
|
+
serviceProxy,
|
|
534
544
|
scriptRuntimeStore,
|
|
535
545
|
getDaemonTcpPort: () => boundListenTarget?.type === "tcp" ? boundListenTarget.port : null,
|
|
536
546
|
getDaemonTcpHost: () => boundListenTarget?.type === "tcp" ? boundListenTarget.host : null,
|
|
547
|
+
serviceProxyPublicBaseUrl,
|
|
537
548
|
onScriptsChanged: null,
|
|
538
549
|
}, input, serviceOptions);
|
|
539
550
|
},
|
|
540
551
|
paseoHome: config.paseoHome,
|
|
552
|
+
worktreesRoot: config.worktreesRoot,
|
|
541
553
|
callerAgentId,
|
|
542
554
|
enableVoiceTools: false,
|
|
543
555
|
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
|
|
@@ -652,112 +664,136 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
652
664
|
logger.info({ elapsed: elapsed() }, "Speech service created");
|
|
653
665
|
logger.info({ elapsed: elapsed() }, "Bootstrap complete, ready to start listening");
|
|
654
666
|
const start = async () => {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
try {
|
|
702
|
-
config.onLifecycleIntent?.(intent);
|
|
667
|
+
let mainStarted = false;
|
|
668
|
+
try {
|
|
669
|
+
if (serviceProxyListenTarget) {
|
|
670
|
+
const boundServiceProxyTarget = await serviceProxy.startStandalone({
|
|
671
|
+
listenTarget: serviceProxyListenTarget,
|
|
672
|
+
});
|
|
673
|
+
serviceProxyListenTarget = boundServiceProxyTarget;
|
|
674
|
+
logger.info({
|
|
675
|
+
listen: formatListenTarget(serviceProxyListenTarget),
|
|
676
|
+
publicBaseUrl: serviceProxyPublicBaseUrl,
|
|
677
|
+
elapsed: elapsed(),
|
|
678
|
+
}, "Service proxy listening");
|
|
679
|
+
}
|
|
680
|
+
// Start main HTTP server
|
|
681
|
+
await new Promise((resolve, reject) => {
|
|
682
|
+
const onError = (err) => {
|
|
683
|
+
httpServer.off("listening", onListening);
|
|
684
|
+
reject(err);
|
|
685
|
+
};
|
|
686
|
+
const onListening = () => {
|
|
687
|
+
httpServer.off("error", onError);
|
|
688
|
+
mainStarted = true;
|
|
689
|
+
const logAndResolve = async () => {
|
|
690
|
+
boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer);
|
|
691
|
+
const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
|
|
692
|
+
agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
|
|
693
|
+
agentManager.setMcpBaseUrl(agentMcpBaseUrl);
|
|
694
|
+
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
|
|
695
|
+
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
|
|
696
|
+
});
|
|
697
|
+
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
|
|
698
|
+
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
|
|
699
|
+
});
|
|
700
|
+
const relayEnabled = config.relayEnabled ?? true;
|
|
701
|
+
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
|
|
702
|
+
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
|
|
703
|
+
const relayUseTls = config.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
|
|
704
|
+
const relayPublicUseTls = config.relayPublicUseTls ?? relayUseTls;
|
|
705
|
+
const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
|
|
706
|
+
if (boundListenTarget.type === "tcp") {
|
|
707
|
+
logger.info({
|
|
708
|
+
host: boundListenTarget.host,
|
|
709
|
+
port: boundListenTarget.port,
|
|
710
|
+
authRequired: !!config.auth?.password,
|
|
711
|
+
elapsed: elapsed(),
|
|
712
|
+
}, `Server listening on http://${boundListenTarget.host}:${boundListenTarget.port}`);
|
|
703
713
|
}
|
|
704
|
-
|
|
705
|
-
logger.
|
|
714
|
+
else {
|
|
715
|
+
logger.info({
|
|
716
|
+
path: boundListenTarget.path,
|
|
717
|
+
authRequired: !!config.auth?.password,
|
|
718
|
+
elapsed: elapsed(),
|
|
719
|
+
}, `Server listening on ${boundListenTarget.path}`);
|
|
706
720
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
+
if (config.auth?.password) {
|
|
722
|
+
logger.info("Daemon password authentication enabled");
|
|
723
|
+
}
|
|
724
|
+
wsServer = new VoiceAssistantWebSocketServer(httpServer, logger, serverId, agentManager, agentStorage, downloadTokenStore, config.paseoHome, daemonConfigStore, mcpBaseUrl, { allowedOrigins, hostnames: configuredHostnames }, config.auth, speechService, terminalManager, {
|
|
725
|
+
finalTimeoutMs: config.dictationFinalTimeoutMs,
|
|
726
|
+
}, daemonVersion, (intent) => {
|
|
727
|
+
try {
|
|
728
|
+
config.onLifecycleIntent?.(intent);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent");
|
|
732
|
+
}
|
|
733
|
+
}, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager, serviceProxy, scriptRuntimeStore, handleBranchChange, () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null), () => (boundListenTarget?.type === "tcp" ? boundListenTarget.host : null), (hostname) => scriptHealthMonitor.getHealthForHostname(hostname), workspaceGitService, github, config.pushNotificationSender, providerSnapshotManager, {
|
|
734
|
+
listen: formatListenTarget(boundListenTarget ?? listenTarget),
|
|
735
|
+
worktreesRoot: config.worktreesRoot,
|
|
721
736
|
relay: {
|
|
722
|
-
|
|
723
|
-
|
|
737
|
+
enabled: relayEnabled,
|
|
738
|
+
endpoint: relayEndpoint,
|
|
739
|
+
publicEndpoint: relayPublicEndpoint,
|
|
740
|
+
useTls: relayUseTls,
|
|
741
|
+
publicUseTls: relayPublicUseTls,
|
|
724
742
|
},
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
743
|
+
}, serviceProxyPublicBaseUrl);
|
|
744
|
+
if (relayEnabled) {
|
|
745
|
+
const offer = await createConnectionOfferV2({
|
|
746
|
+
serverId,
|
|
747
|
+
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
|
748
|
+
relay: {
|
|
749
|
+
endpoint: relayPublicEndpoint,
|
|
750
|
+
useTls: relayPublicUseTls,
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
|
754
|
+
relayTransport?.stop().catch(() => undefined);
|
|
755
|
+
relayTransport = startRelayTransport({
|
|
756
|
+
logger,
|
|
757
|
+
attachSocket: (ws, metadata) => {
|
|
758
|
+
if (!wsServer) {
|
|
759
|
+
throw new Error("WebSocket server not initialized");
|
|
760
|
+
}
|
|
761
|
+
return wsServer.attachExternalSocket(ws, metadata);
|
|
762
|
+
},
|
|
763
|
+
relayEndpoint,
|
|
764
|
+
relayUseTls,
|
|
765
|
+
serverId,
|
|
766
|
+
daemonKeyPair: daemonKeyPair.keyPair,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
logAndResolve().then(resolve, reject);
|
|
742
771
|
};
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (listenTarget.type === "tcp") {
|
|
748
|
-
httpServer.listen(listenTarget.port, listenTarget.host);
|
|
749
|
-
}
|
|
750
|
-
else {
|
|
751
|
-
if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
|
|
752
|
-
unlinkSync(listenTarget.path);
|
|
772
|
+
httpServer.once("error", onError);
|
|
773
|
+
httpServer.once("listening", onListening);
|
|
774
|
+
if (listenTarget.type === "tcp") {
|
|
775
|
+
httpServer.listen(listenTarget.port, listenTarget.host);
|
|
753
776
|
}
|
|
754
|
-
|
|
777
|
+
else {
|
|
778
|
+
if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
|
|
779
|
+
unlinkSync(listenTarget.path);
|
|
780
|
+
}
|
|
781
|
+
httpServer.listen(listenTarget.path);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
// Start speech service after listening so synchronous Sherpa native
|
|
785
|
+
// model loading doesn't block the server from accepting connections.
|
|
786
|
+
speechService.start();
|
|
787
|
+
scriptHealthMonitor.start();
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
await serviceProxy.stopStandalone().catch(() => undefined);
|
|
791
|
+
if (mainStarted) {
|
|
792
|
+
httpServer.closeAllConnections();
|
|
793
|
+
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
755
794
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
// model loading doesn't block the server from accepting connections.
|
|
759
|
-
speechService.start();
|
|
760
|
-
scriptHealthMonitor.start();
|
|
795
|
+
throw error;
|
|
796
|
+
}
|
|
761
797
|
};
|
|
762
798
|
const stop = async () => {
|
|
763
799
|
scriptHealthMonitor.stop();
|
|
@@ -773,6 +809,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
773
809
|
if (wsServer) {
|
|
774
810
|
await wsServer.close();
|
|
775
811
|
}
|
|
812
|
+
await serviceProxy.stopStandalone();
|
|
776
813
|
// Force-drop remaining sockets so httpServer.close() resolves promptly.
|
|
777
814
|
// We've already closed wsServer (which sent ws-layer close frames) and
|
|
778
815
|
// stopped every other service, so anything still attached is a TCP
|
|
@@ -794,7 +831,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
794
831
|
agentManager,
|
|
795
832
|
agentStorage,
|
|
796
833
|
terminalManager,
|
|
797
|
-
|
|
834
|
+
serviceProxy,
|
|
798
835
|
scriptRuntimeStore,
|
|
799
836
|
start,
|
|
800
837
|
stop,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { resolvePaseoNodeEnv } from "./paseo-env.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { expandTilde } from "../utils/path.js";
|
|
4
5
|
import { loadPersistedConfig, LogFormatSchema, LogLevelSchema, } from "./persisted-config.js";
|
|
5
6
|
import { ProviderOverrideSchema } from "./agent/provider-launch-config.js";
|
|
6
7
|
import { AgentProviderSchema } from "@getpaseo/protocol/provider-manifest";
|
|
@@ -109,6 +110,33 @@ function resolveRelayConfig(input) {
|
|
|
109
110
|
const publicUseTls = resolveTlsFromEnv(input.env.PASEO_RELAY_PUBLIC_USE_TLS, input.persisted.daemon?.relay?.publicUseTls, useTls);
|
|
110
111
|
return { enabled, endpoint, publicEndpoint, useTls, publicUseTls };
|
|
111
112
|
}
|
|
113
|
+
function resolveServiceProxyPublicBaseUrl(value) {
|
|
114
|
+
if (value === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return new URL(value).toString().replace(/\/$/, "");
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new Error(`Invalid PASEO_SERVICE_PROXY_PUBLIC_BASE_URL: ${value}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function resolveServiceProxyConfig(env, persisted) {
|
|
125
|
+
const enabledShim = parseBooleanEnv(env.PASEO_SERVICE_PROXY_ENABLED) ?? persisted.daemon?.serviceProxy?.enabled;
|
|
126
|
+
// COMPAT(serviceProxyEnabled): added 2026-06-02, remove after 2026-12-02.
|
|
127
|
+
// `enabled=false` used to disable the separate service proxy listener. Localhost
|
|
128
|
+
// service proxying is now always enabled; this only suppresses optional layers.
|
|
129
|
+
const optionalLayersEnabled = enabledShim !== false;
|
|
130
|
+
const publicBaseUrl = optionalLayersEnabled
|
|
131
|
+
? resolveServiceProxyPublicBaseUrl(env.PASEO_SERVICE_PROXY_PUBLIC_BASE_URL ??
|
|
132
|
+
persisted.daemon?.serviceProxy?.publicBaseUrl ??
|
|
133
|
+
null)
|
|
134
|
+
: null;
|
|
135
|
+
const standaloneListen = optionalLayersEnabled
|
|
136
|
+
? (env.PASEO_SERVICE_PROXY_LISTEN ?? persisted.daemon?.serviceProxy?.listen ?? null)
|
|
137
|
+
: null;
|
|
138
|
+
return { publicBaseUrl, standaloneListen };
|
|
139
|
+
}
|
|
112
140
|
function resolveVoiceLlmConfig(env, persisted) {
|
|
113
141
|
const envVoiceLlmProvider = parseOptionalVoiceLlmProvider(env.PASEO_VOICE_LLM_PROVIDER);
|
|
114
142
|
const persistedVoiceLlmProvider = parseOptionalVoiceLlmProvider(persisted.features?.voiceMode?.llm?.provider);
|
|
@@ -145,6 +173,16 @@ function resolveAuthConfig(env, persisted) {
|
|
|
145
173
|
? { password: persisted.daemon.auth.password }
|
|
146
174
|
: undefined;
|
|
147
175
|
}
|
|
176
|
+
function resolveWorktreesRoot(paseoHome, persisted) {
|
|
177
|
+
const configuredRoot = persisted.worktrees?.root?.trim();
|
|
178
|
+
if (!configuredRoot) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const expandedRoot = expandTilde(configuredRoot);
|
|
182
|
+
return path.isAbsolute(expandedRoot)
|
|
183
|
+
? path.resolve(expandedRoot)
|
|
184
|
+
: path.resolve(paseoHome, expandedRoot);
|
|
185
|
+
}
|
|
148
186
|
function resolveAppendSystemPrompt(persisted) {
|
|
149
187
|
return persisted.daemon?.appendSystemPrompt ?? "";
|
|
150
188
|
}
|
|
@@ -173,6 +211,7 @@ export function loadConfig(paseoHome, options) {
|
|
|
173
211
|
cliRelayEnabled: options?.cli?.relayEnabled,
|
|
174
212
|
cliRelayUseTls: options?.cli?.relayUseTls,
|
|
175
213
|
});
|
|
214
|
+
const serviceProxy = resolveServiceProxyConfig(env, persisted);
|
|
176
215
|
const { openai, speech } = resolveSpeechConfig({
|
|
177
216
|
paseoHome,
|
|
178
217
|
env,
|
|
@@ -183,6 +222,7 @@ export function loadConfig(paseoHome, options) {
|
|
|
183
222
|
return {
|
|
184
223
|
listen,
|
|
185
224
|
paseoHome,
|
|
225
|
+
worktreesRoot: resolveWorktreesRoot(paseoHome, persisted),
|
|
186
226
|
corsAllowedOrigins: resolveCorsAllowedOrigins(env, persisted),
|
|
187
227
|
hostnames,
|
|
188
228
|
mcpEnabled,
|
|
@@ -199,6 +239,7 @@ export function loadConfig(paseoHome, options) {
|
|
|
199
239
|
relayPublicEndpoint: relay.publicEndpoint,
|
|
200
240
|
relayUseTls: relay.useTls,
|
|
201
241
|
relayPublicUseTls: relay.publicUseTls,
|
|
242
|
+
serviceProxy,
|
|
202
243
|
appBaseUrl,
|
|
203
244
|
auth: resolveAuthConfig(env, persisted),
|
|
204
245
|
openai,
|