@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
|
@@ -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
|
},
|
|
@@ -410,6 +418,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
410
418
|
paseoHome: config.paseoHome,
|
|
411
419
|
logger,
|
|
412
420
|
agentManager,
|
|
421
|
+
providerSnapshotManager,
|
|
413
422
|
});
|
|
414
423
|
await loopService.initialize();
|
|
415
424
|
logger.info({ elapsed: elapsed() }, "Loop service initialized");
|
|
@@ -418,6 +427,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
418
427
|
logger,
|
|
419
428
|
agentManager,
|
|
420
429
|
agentStorage,
|
|
430
|
+
providerSnapshotManager,
|
|
421
431
|
});
|
|
422
432
|
await scheduleService.start();
|
|
423
433
|
agentManager.setAgentArchivedCallback(async (agentId) => {
|
|
@@ -467,6 +477,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
467
477
|
};
|
|
468
478
|
setupAutoArchiveOnMerge({
|
|
469
479
|
paseoHome: config.paseoHome,
|
|
480
|
+
worktreesRoot: config.worktreesRoot,
|
|
470
481
|
daemonConfigStore,
|
|
471
482
|
workspaceGitService,
|
|
472
483
|
github,
|
|
@@ -501,6 +512,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
501
512
|
createPaseoWorktree: async (input, serviceOptions) => {
|
|
502
513
|
return createPaseoWorktreeWorkflow({
|
|
503
514
|
paseoHome: config.paseoHome,
|
|
515
|
+
worktreesRoot: config.worktreesRoot,
|
|
504
516
|
createPaseoWorktree: async (workflowInput, workflowOptions) => {
|
|
505
517
|
return createRegisteredPaseoWorktree(workflowInput, {
|
|
506
518
|
github,
|
|
@@ -530,14 +542,16 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
530
542
|
sessionLogger: logger,
|
|
531
543
|
terminalManager,
|
|
532
544
|
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
|
533
|
-
|
|
545
|
+
serviceProxy,
|
|
534
546
|
scriptRuntimeStore,
|
|
535
547
|
getDaemonTcpPort: () => boundListenTarget?.type === "tcp" ? boundListenTarget.port : null,
|
|
536
548
|
getDaemonTcpHost: () => boundListenTarget?.type === "tcp" ? boundListenTarget.host : null,
|
|
549
|
+
serviceProxyPublicBaseUrl,
|
|
537
550
|
onScriptsChanged: null,
|
|
538
551
|
}, input, serviceOptions);
|
|
539
552
|
},
|
|
540
553
|
paseoHome: config.paseoHome,
|
|
554
|
+
worktreesRoot: config.worktreesRoot,
|
|
541
555
|
callerAgentId,
|
|
542
556
|
enableVoiceTools: false,
|
|
543
557
|
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
|
|
@@ -652,112 +666,136 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
652
666
|
logger.info({ elapsed: elapsed() }, "Speech service created");
|
|
653
667
|
logger.info({ elapsed: elapsed() }, "Bootstrap complete, ready to start listening");
|
|
654
668
|
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);
|
|
669
|
+
let mainStarted = false;
|
|
670
|
+
try {
|
|
671
|
+
if (serviceProxyListenTarget) {
|
|
672
|
+
const boundServiceProxyTarget = await serviceProxy.startStandalone({
|
|
673
|
+
listenTarget: serviceProxyListenTarget,
|
|
674
|
+
});
|
|
675
|
+
serviceProxyListenTarget = boundServiceProxyTarget;
|
|
676
|
+
logger.info({
|
|
677
|
+
listen: formatListenTarget(serviceProxyListenTarget),
|
|
678
|
+
publicBaseUrl: serviceProxyPublicBaseUrl,
|
|
679
|
+
elapsed: elapsed(),
|
|
680
|
+
}, "Service proxy listening");
|
|
681
|
+
}
|
|
682
|
+
// Start main HTTP server
|
|
683
|
+
await new Promise((resolve, reject) => {
|
|
684
|
+
const onError = (err) => {
|
|
685
|
+
httpServer.off("listening", onListening);
|
|
686
|
+
reject(err);
|
|
687
|
+
};
|
|
688
|
+
const onListening = () => {
|
|
689
|
+
httpServer.off("error", onError);
|
|
690
|
+
mainStarted = true;
|
|
691
|
+
const logAndResolve = async () => {
|
|
692
|
+
boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer);
|
|
693
|
+
const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
|
|
694
|
+
agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
|
|
695
|
+
agentManager.setMcpBaseUrl(agentMcpBaseUrl);
|
|
696
|
+
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
|
|
697
|
+
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
|
|
698
|
+
});
|
|
699
|
+
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
|
|
700
|
+
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
|
|
701
|
+
});
|
|
702
|
+
const relayEnabled = config.relayEnabled ?? true;
|
|
703
|
+
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
|
|
704
|
+
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
|
|
705
|
+
const relayUseTls = config.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
|
|
706
|
+
const relayPublicUseTls = config.relayPublicUseTls ?? relayUseTls;
|
|
707
|
+
const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
|
|
708
|
+
if (boundListenTarget.type === "tcp") {
|
|
709
|
+
logger.info({
|
|
710
|
+
host: boundListenTarget.host,
|
|
711
|
+
port: boundListenTarget.port,
|
|
712
|
+
authRequired: !!config.auth?.password,
|
|
713
|
+
elapsed: elapsed(),
|
|
714
|
+
}, `Server listening on http://${boundListenTarget.host}:${boundListenTarget.port}`);
|
|
703
715
|
}
|
|
704
|
-
|
|
705
|
-
logger.
|
|
716
|
+
else {
|
|
717
|
+
logger.info({
|
|
718
|
+
path: boundListenTarget.path,
|
|
719
|
+
authRequired: !!config.auth?.password,
|
|
720
|
+
elapsed: elapsed(),
|
|
721
|
+
}, `Server listening on ${boundListenTarget.path}`);
|
|
706
722
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
723
|
+
if (config.auth?.password) {
|
|
724
|
+
logger.info("Daemon password authentication enabled");
|
|
725
|
+
}
|
|
726
|
+
wsServer = new VoiceAssistantWebSocketServer(httpServer, logger, serverId, agentManager, agentStorage, downloadTokenStore, config.paseoHome, daemonConfigStore, mcpBaseUrl, { allowedOrigins, hostnames: configuredHostnames }, config.auth, speechService, terminalManager, {
|
|
727
|
+
finalTimeoutMs: config.dictationFinalTimeoutMs,
|
|
728
|
+
}, daemonVersion, (intent) => {
|
|
729
|
+
try {
|
|
730
|
+
config.onLifecycleIntent?.(intent);
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent");
|
|
734
|
+
}
|
|
735
|
+
}, 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, {
|
|
736
|
+
listen: formatListenTarget(boundListenTarget ?? listenTarget),
|
|
737
|
+
worktreesRoot: config.worktreesRoot,
|
|
721
738
|
relay: {
|
|
722
|
-
|
|
723
|
-
|
|
739
|
+
enabled: relayEnabled,
|
|
740
|
+
endpoint: relayEndpoint,
|
|
741
|
+
publicEndpoint: relayPublicEndpoint,
|
|
742
|
+
useTls: relayUseTls,
|
|
743
|
+
publicUseTls: relayPublicUseTls,
|
|
724
744
|
},
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
745
|
+
}, serviceProxyPublicBaseUrl);
|
|
746
|
+
if (relayEnabled) {
|
|
747
|
+
const offer = await createConnectionOfferV2({
|
|
748
|
+
serverId,
|
|
749
|
+
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
|
750
|
+
relay: {
|
|
751
|
+
endpoint: relayPublicEndpoint,
|
|
752
|
+
useTls: relayPublicUseTls,
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
|
756
|
+
relayTransport?.stop().catch(() => undefined);
|
|
757
|
+
relayTransport = startRelayTransport({
|
|
758
|
+
logger,
|
|
759
|
+
attachSocket: (ws, metadata) => {
|
|
760
|
+
if (!wsServer) {
|
|
761
|
+
throw new Error("WebSocket server not initialized");
|
|
762
|
+
}
|
|
763
|
+
return wsServer.attachExternalSocket(ws, metadata);
|
|
764
|
+
},
|
|
765
|
+
relayEndpoint,
|
|
766
|
+
relayUseTls,
|
|
767
|
+
serverId,
|
|
768
|
+
daemonKeyPair: daemonKeyPair.keyPair,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
logAndResolve().then(resolve, reject);
|
|
742
773
|
};
|
|
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);
|
|
774
|
+
httpServer.once("error", onError);
|
|
775
|
+
httpServer.once("listening", onListening);
|
|
776
|
+
if (listenTarget.type === "tcp") {
|
|
777
|
+
httpServer.listen(listenTarget.port, listenTarget.host);
|
|
753
778
|
}
|
|
754
|
-
|
|
779
|
+
else {
|
|
780
|
+
if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
|
|
781
|
+
unlinkSync(listenTarget.path);
|
|
782
|
+
}
|
|
783
|
+
httpServer.listen(listenTarget.path);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
// Start speech service after listening so synchronous Sherpa native
|
|
787
|
+
// model loading doesn't block the server from accepting connections.
|
|
788
|
+
speechService.start();
|
|
789
|
+
scriptHealthMonitor.start();
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
await serviceProxy.stopStandalone().catch(() => undefined);
|
|
793
|
+
if (mainStarted) {
|
|
794
|
+
httpServer.closeAllConnections();
|
|
795
|
+
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
755
796
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
// model loading doesn't block the server from accepting connections.
|
|
759
|
-
speechService.start();
|
|
760
|
-
scriptHealthMonitor.start();
|
|
797
|
+
throw error;
|
|
798
|
+
}
|
|
761
799
|
};
|
|
762
800
|
const stop = async () => {
|
|
763
801
|
scriptHealthMonitor.stop();
|
|
@@ -773,6 +811,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
773
811
|
if (wsServer) {
|
|
774
812
|
await wsServer.close();
|
|
775
813
|
}
|
|
814
|
+
await serviceProxy.stopStandalone();
|
|
776
815
|
// Force-drop remaining sockets so httpServer.close() resolves promptly.
|
|
777
816
|
// We've already closed wsServer (which sent ws-layer close frames) and
|
|
778
817
|
// stopped every other service, so anything still attached is a TCP
|
|
@@ -794,7 +833,7 @@ export async function createPaseoDaemon(config, rootLogger) {
|
|
|
794
833
|
agentManager,
|
|
795
834
|
agentStorage,
|
|
796
835
|
terminalManager,
|
|
797
|
-
|
|
836
|
+
serviceProxy,
|
|
798
837
|
scriptRuntimeStore,
|
|
799
838
|
start,
|
|
800
839
|
stop,
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { writeJsonFileAtomic } from "../atomic-file.js";
|
|
5
6
|
import { ChatMessageSchema, ChatRoomDetailSchema, ChatRoomSchema, } from "@getpaseo/protocol/chat/types";
|
|
6
7
|
const ChatStorePayloadSchema = z.object({
|
|
7
8
|
rooms: z.array(ChatRoomSchema),
|
|
@@ -246,10 +247,7 @@ export class FileBackedChatService {
|
|
|
246
247
|
.flat()
|
|
247
248
|
.sort((left, right) => left.createdAt.localeCompare(right.createdAt)),
|
|
248
249
|
};
|
|
249
|
-
await
|
|
250
|
-
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
251
|
-
await fs.writeFile(tempPath, JSON.stringify(payload, null, 2), "utf8");
|
|
252
|
-
await fs.rename(tempPath, this.filePath);
|
|
250
|
+
await writeJsonFileAtomic(this.filePath, payload);
|
|
253
251
|
}
|
|
254
252
|
findRoomByName(name) {
|
|
255
253
|
const normalizedName = normalizeRoomName(name);
|
|
@@ -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,
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { generateKeyPair, exportPublicKey, exportSecretKey, importPublicKey, importSecretKey, } from "@getpaseo/relay/e2ee";
|
|
5
|
-
import { ensurePrivateFile,
|
|
5
|
+
import { ensurePrivateFile, writePrivateFileAtomicSync } from "./private-files.js";
|
|
6
6
|
const KeyPairSchema = z.object({
|
|
7
7
|
v: z.literal(2),
|
|
8
8
|
publicKeyB64: z.string().min(1),
|
|
@@ -35,7 +35,7 @@ export async function loadOrCreateDaemonKeyPair(paseoHome, logger) {
|
|
|
35
35
|
publicKeyB64,
|
|
36
36
|
secretKeyB64,
|
|
37
37
|
};
|
|
38
|
-
|
|
38
|
+
writePrivateFileAtomicSync(filePath, JSON.stringify(payload, null, 2) + "\n");
|
|
39
39
|
log?.info({ filePath }, "Saved daemon keypair");
|
|
40
40
|
return { keyPair, publicKeyB64 };
|
|
41
41
|
}
|