conductor-remote 1.53.0 → 1.53.2

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/index.html CHANGED
@@ -13,7 +13,7 @@
13
13
  <title>Conductor Remote</title>
14
14
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
15
15
  <script src="/self-heal.js"></script>
16
- <script type="module" crossorigin src="/assets/index-DeKtykCg.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-CQuR_IG3.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-CNdG7lFR.css">
18
18
  <link rel="manifest" href="/manifest.webmanifest"></head>
19
19
  <body>
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"202ce98b69f7471208f90362c8f3f4bd"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DeKtykCg.js",revision:null},{url:"assets/index-CNdG7lFR.css",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"6fd3f4c8b836fbae3791d6d273888b16"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CQuR_IG3.js",revision:null},{url:"assets/index-CNdG7lFR.css",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  const SOURCE_EXTENSIONS = new Set([
2
3
  '.bash',
3
4
  '.c',
@@ -40,6 +41,22 @@ function sourceExtension(filePath) {
40
41
  export function isPreviewableSource(filePath) {
41
42
  return SOURCE_EXTENSIONS.has(sourceExtension(filePath));
42
43
  }
44
+ /** True only for a descendant. A prefix test would let `/workspaces-old` escape `/workspaces`. */
45
+ function insideDirectory(filePath, root) {
46
+ const relative = path.relative(root, filePath);
47
+ return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
48
+ }
49
+ /**
50
+ * Public Funnel clients may preview source under Conductor's workspaces only.
51
+ * A tailnet relay is limited to trusted devices, so it can also show project
52
+ * notes and supporting files from the signed-in user's home directory.
53
+ *
54
+ * Call this only with real paths. That ensures a symlink cannot point outside
55
+ * an allowed directory after this check.
56
+ */
57
+ export function isAllowedPreviewPath(filePath, workspaceRoot, homeRoot, exposeMode) {
58
+ return insideDirectory(filePath, workspaceRoot) || (exposeMode === 'tailnet' && insideDirectory(filePath, homeRoot));
59
+ }
43
60
  /**
44
61
  * Parse a source link without treating ordinary PWA routes as file references.
45
62
  * The extension allowlist keeps links such as `/w/a-workspace` in the browser,
@@ -8,11 +8,11 @@
8
8
  * Accessibility, no AppleScript, no window has to exist. A Conductor update can
9
9
  * rename every UI string and notifications keep working.
10
10
  *
11
- * What counts as news is deliberately narrow `sessions.status` is the only
12
- * signal Conductor records, and it holds exactly three values:
13
- * - `working → idle` the turn ended. This is *the* event: it covers "done",
14
- * "asked you a question" and "hit a permission prompt" alike, because all
15
- * three end the turn.
11
+ * What counts as news is deliberately narrow. `sessions.status` is the only
12
+ * signal Conductor records. A normal turn ends in one of three states:
13
+ * - `working → idle | needs_plan_response | needs_user_input` means the turn ended.
14
+ * This is *the* event: it covers "done", "asked you a question" and "plan ready"
15
+ * alike, because all three end the turn.
16
16
  * - `→ error` — the agent stopped badly.
17
17
  * There is no permission-request table to watch (verified against the schema), so
18
18
  * don't go looking for a finer-grained trigger; there isn't one.
@@ -247,6 +247,10 @@ function askedBy(state) {
247
247
  return '';
248
248
  return `${state.turnStartedAt ?? ''}|${state.lastUserMessageAt ?? ''}`;
249
249
  }
250
+ /** Statuses that keep a completed turn open until the person responds. */
251
+ function turnEnded(status) {
252
+ return status === 'idle' || status === 'needs_plan_response' || status === 'needs_user_input';
253
+ }
250
254
  /**
251
255
  * The status state machine, kept apart from the store and the network so it can be
252
256
  * driven a tick at a time by `scripts/check-notify.ts`. Every rule in it is a rule
@@ -307,7 +311,7 @@ export class TurnWatcher {
307
311
  if (pendingKind) {
308
312
  this.armed.delete(state.sessionId);
309
313
  // Confirmed only if the new status held for a second step; a flap just drops the arm.
310
- const held = (pendingKind === 'done' && now === 'idle') || (pendingKind === 'error' && now === 'error');
314
+ const held = (pendingKind === 'done' && turnEnded(now)) || (pendingKind === 'error' && now === 'error');
311
315
  if (held && !(pendingKind === 'done' && this.selfScheduled(state))) {
312
316
  const asked = askedBy(state);
313
317
  if (pendingKind === 'done' && asked)
@@ -320,7 +324,7 @@ export class TurnWatcher {
320
324
  // contributes its status to the snapshot but is never itself news.
321
325
  if (before === undefined)
322
326
  continue;
323
- if (before === 'working' && now === 'idle')
327
+ if (before === 'working' && turnEnded(now))
324
328
  this.armed.set(state.sessionId, 'done');
325
329
  else if (before !== 'error' && now === 'error')
326
330
  this.armed.set(state.sessionId, 'error');
@@ -8,7 +8,7 @@ import { writeAttachment } from "./attachments.js";
8
8
  import { startAutoUpdate, updateStatus } from "./autoupdate.js";
9
9
  import { loadConfig, stateDir } from "./config.js";
10
10
  import { ConductorDb } from "./db.js";
11
- import { parseFileReference } from "./file-preview.js";
11
+ import { isAllowedPreviewPath, parseFileReference } from "./file-preview.js";
12
12
  import { FirstPromptQueue } from "./firstprompt.js";
13
13
  import { startFunnelWatchdog } from "./funnel-watchdog.js";
14
14
  import { workspaceDiff } from "./git.js";
@@ -25,7 +25,7 @@ import { foldHits, queryTokens, SearchIndex } from "./search.js";
25
25
  import { SendOnce } from "./sendonce.js";
26
26
  import { readSettings, writeSettings } from "./settings.js";
27
27
  import { discardStagedAttachment, materializeStagedAttachments, stageAttachment, stagedAttachments } from "./staged-attachments.js";
28
- import { driftWarningLines, tailscaleBin } from "./tailscale.js";
28
+ import { driftWarningLines, readExposeMode, tailscaleBin } from "./tailscale.js";
29
29
  import { renderTranscript } from "./transcript.js";
30
30
  import { autoJoinHotspotMode, currentSsid, looksLikeHotspot, preferredNetworks } from "./wifi.js";
31
31
  import { createWorkspace, describeActuator, EFFORT_LABELS, listAgentModels, lockBlocked, newChat, pickActuator, retryWontHelp, screenLocked, setAgentOptions, setRestartGuard, setWorkspaceStatus, stopTurn, UiBusyError, uiQueueDepth, WORKSPACE_STATUS_LABELS, withUiPriority } from "./writes.js";
@@ -452,16 +452,11 @@ async function serveLocalImage(req, res, requestedPath) {
452
452
  const FILE_PREVIEW_MAX_BYTES = 512 * 1024;
453
453
  const FILE_PREVIEW_CONTEXT_LINES = 100;
454
454
  const FILE_PREVIEW_FIRST_LINES = 500;
455
- /** True only for a descendant. A prefix test would let `/workspaces-old` escape `/workspaces`. */
456
- function insideDirectory(filePath, root) {
457
- const relative = path.relative(root, filePath);
458
- return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
459
- }
460
455
  /**
461
456
  * Serve source that an agent linked in its Markdown. The link format comes from
462
- * coding-agent file references, but its path still arrives from an internet-facing
463
- * client. Resolve both sides and accept only regular source files below the
464
- * configured Conductor workspace root.
457
+ * coding-agent file references, but its path still arrives from a remote client.
458
+ * Public Funnel clients stay within Conductor workspaces. Tailnet-only relays may
459
+ * also read supporting source files from the signed-in user's home directory.
465
460
  */
466
461
  async function serveFilePreview(req, res, reference) {
467
462
  const target = parseFileReference(reference);
@@ -469,15 +464,18 @@ async function serveFilePreview(req, res, reference) {
469
464
  return json(req, res, 404, { error: 'source file not found' });
470
465
  let filePath;
471
466
  let workspaceRoot;
467
+ let homeRoot;
472
468
  let size;
473
469
  try {
474
470
  ;
475
- [filePath, workspaceRoot] = await Promise.all([
471
+ [filePath, workspaceRoot, homeRoot] = await Promise.all([
476
472
  fs.promises.realpath(target.path),
477
- fs.promises.realpath(cfg.workspacesRoot)
473
+ fs.promises.realpath(cfg.workspacesRoot),
474
+ fs.promises.realpath(os.homedir())
478
475
  ]);
479
- if (!insideDirectory(filePath, workspaceRoot))
476
+ if (!isAllowedPreviewPath(filePath, workspaceRoot, homeRoot, readExposeMode())) {
480
477
  return json(req, res, 404, { error: 'source file not found' });
478
+ }
481
479
  const info = await fs.promises.stat(filePath);
482
480
  if (!info.isFile())
483
481
  return json(req, res, 404, { error: 'source file not found' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.53.0",
3
+ "version": "1.53.2",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",