@openscout/scout 0.2.58 → 0.2.60

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.
@@ -956,19 +956,58 @@ function findStoredCerts() {
956
956
  return null;
957
957
  }
958
958
  }
959
- function getTailscaleHostname() {
959
+ function readTailscaleStatus() {
960
960
  try {
961
961
  const output = execSync("tailscale status --self=true --peers=false --json", {
962
962
  stdio: ["pipe", "pipe", "pipe"],
963
963
  timeout: 5000
964
964
  }).toString();
965
965
  const data = JSON.parse(output);
966
- const dnsName = data?.Self?.DNSName ?? "";
967
- return dnsName.replace(/\.$/, "") || null;
966
+ const dnsName = typeof data.Self?.DNSName === "string" ? data.Self.DNSName.replace(/\.$/, "") : "";
967
+ return {
968
+ backendState: typeof data.BackendState === "string" ? data.BackendState : null,
969
+ dnsName: dnsName || null,
970
+ online: data.Self?.Online !== false,
971
+ health: Array.isArray(data.Health) ? data.Health.filter((entry) => typeof entry === "string") : []
972
+ };
968
973
  } catch {
969
974
  return null;
970
975
  }
971
976
  }
977
+ function resolveRelayEndpointForTailscaleStatus(port, tailscale) {
978
+ const backendState = tailscale?.backendState?.trim().toLowerCase() ?? "";
979
+ const hostname = tailscale?.dnsName ?? null;
980
+ const tailscaleRunning = backendState === "running" && tailscale?.online !== false && Boolean(hostname);
981
+ const tls = resolveTls(tailscaleRunning ? hostname : null);
982
+ if (tailscaleRunning && hostname && tls) {
983
+ return {
984
+ relayUrl: `wss://${hostname}:${port}`,
985
+ options: { tls }
986
+ };
987
+ }
988
+ if (tailscaleRunning && hostname) {
989
+ pairingLog.warn("relay", "tailscale is running without TLS; falling back to insecure websocket relay", {
990
+ hostname,
991
+ port
992
+ });
993
+ return {
994
+ relayUrl: `ws://${hostname}:${port}`,
995
+ options: {}
996
+ };
997
+ }
998
+ if (hostname && backendState && backendState !== "running") {
999
+ pairingLog.warn("relay", "tailscale hostname detected but tailscale is not running; using local-only relay endpoint", {
1000
+ hostname,
1001
+ backendState,
1002
+ health: tailscale?.health ?? [],
1003
+ port
1004
+ });
1005
+ }
1006
+ return {
1007
+ relayUrl: `ws://127.0.0.1:${port}`,
1008
+ options: {}
1009
+ };
1010
+ }
972
1011
  function generateTailscaleCerts(hostname) {
973
1012
  mkdirSync3(PAIRING_DIR, { recursive: true });
974
1013
  const certPath = join(PAIRING_DIR, `${hostname}.crt`);
@@ -1014,28 +1053,7 @@ function resolveTls(hostname) {
1014
1053
  return generateTailscaleCerts(hostname);
1015
1054
  }
1016
1055
  function resolveRelayEndpoint(port) {
1017
- const hostname = getTailscaleHostname();
1018
- const tls = resolveTls(hostname);
1019
- if (hostname && tls) {
1020
- return {
1021
- relayUrl: `wss://${hostname}:${port}`,
1022
- options: { tls }
1023
- };
1024
- }
1025
- if (hostname) {
1026
- pairingLog.warn("relay", "tailscale hostname detected without TLS; falling back to insecure websocket relay", {
1027
- hostname,
1028
- port
1029
- });
1030
- return {
1031
- relayUrl: `ws://${hostname}:${port}`,
1032
- options: {}
1033
- };
1034
- }
1035
- return {
1036
- relayUrl: `ws://127.0.0.1:${port}`,
1037
- options: {}
1038
- };
1056
+ return resolveRelayEndpointForTailscaleStatus(port, readTailscaleStatus());
1039
1057
  }
1040
1058
  function startManagedRelay(port = 7889) {
1041
1059
  const endpoint = resolveRelayEndpoint(port);
@@ -7869,7 +7887,73 @@ function resolveConfig() {
7869
7887
  // ../../apps/desktop/src/core/pairing/runtime/bridge/fileserver.ts
7870
7888
  import { isAbsolute } from "path";
7871
7889
  import { homedir as homedir9 } from "os";
7890
+
7891
+ // ../../apps/desktop/src/core/pairing/runtime/bridge/web-handoff.ts
7892
+ import { randomBytes as randomBytes2 } from "crypto";
7893
+ var SCOUT_WEB_HANDOFF_COOKIE = "scout_handoff";
7894
+ var WEB_HANDOFF_TTL_MS = 5 * 60 * 1000;
7895
+ var activeWebHandoffs = new Map;
7896
+ function pruneExpiredWebHandoffs(now = Date.now()) {
7897
+ for (const [token, record] of activeWebHandoffs) {
7898
+ if (record.expiresAt <= now) {
7899
+ activeWebHandoffs.delete(token);
7900
+ }
7901
+ }
7902
+ }
7903
+ function scopesMatch(left, right) {
7904
+ if (left.kind !== right.kind) {
7905
+ return false;
7906
+ }
7907
+ if (left.sessionId !== right.sessionId) {
7908
+ return false;
7909
+ }
7910
+ if (left.kind === "session") {
7911
+ return true;
7912
+ }
7913
+ return left.turnId === right.turnId && left.blockId === right.blockId;
7914
+ }
7915
+ function pathForWebHandoffScope(scope) {
7916
+ switch (scope.kind) {
7917
+ case "session":
7918
+ return `/handoff/session/${encodeURIComponent(scope.sessionId)}`;
7919
+ case "file_change":
7920
+ return `/handoff/file-change/${encodeURIComponent(scope.sessionId)}/${encodeURIComponent(scope.turnId)}/${encodeURIComponent(scope.blockId)}`;
7921
+ }
7922
+ }
7923
+ function issueWebHandoff(scope, deviceId) {
7924
+ pruneExpiredWebHandoffs();
7925
+ const token = randomBytes2(24).toString("base64url");
7926
+ const expiresAt = Date.now() + WEB_HANDOFF_TTL_MS;
7927
+ activeWebHandoffs.set(token, {
7928
+ token,
7929
+ deviceId: deviceId?.trim() || null,
7930
+ expiresAt,
7931
+ scope
7932
+ });
7933
+ return { token, expiresAt, scope };
7934
+ }
7935
+ function readAuthorizedWebHandoff(token, scope) {
7936
+ pruneExpiredWebHandoffs();
7937
+ if (!token) {
7938
+ return null;
7939
+ }
7940
+ const record = activeWebHandoffs.get(token);
7941
+ if (!record) {
7942
+ return null;
7943
+ }
7944
+ if (!scopesMatch(record.scope, scope)) {
7945
+ return null;
7946
+ }
7947
+ return record;
7948
+ }
7949
+
7950
+ // ../../apps/desktop/src/core/pairing/runtime/bridge/fileserver.ts
7872
7951
  var ALLOWED_ROOTS = [homedir9(), "/tmp"];
7952
+ var LOOPBACK_IPV4_HOST_PATTERN = /^127(?:\.\d{1,3}){3}$/;
7953
+ function isTrustedLoopbackHostname(hostname) {
7954
+ const normalized = hostname.trim().toLowerCase();
7955
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || LOOPBACK_IPV4_HOST_PATTERN.test(normalized);
7956
+ }
7873
7957
  function isAllowedPath(filePath) {
7874
7958
  if (!isAbsolute(filePath))
7875
7959
  return false;
@@ -7880,7 +7964,7 @@ function isAllowedPath(filePath) {
7880
7964
  return ALLOWED_ROOTS.some((root) => filePath.startsWith(root));
7881
7965
  }
7882
7966
  function startFileServer(options) {
7883
- const { port } = options;
7967
+ const { port, bridge } = options;
7884
7968
  let server = null;
7885
7969
  function start() {
7886
7970
  try {
@@ -7888,7 +7972,7 @@ function startFileServer(options) {
7888
7972
  port,
7889
7973
  fetch(req) {
7890
7974
  try {
7891
- return route(new URL(req.url));
7975
+ return route(req, new URL(req.url), bridge);
7892
7976
  } catch (err) {
7893
7977
  console.error(`[fileserver] ${req.url} \u2192`, err.message);
7894
7978
  return new Response("Internal error", { status: 500 });
@@ -7911,12 +7995,19 @@ function startFileServer(options) {
7911
7995
  start();
7912
7996
  return { port, stop, restart };
7913
7997
  }
7914
- function route(url) {
7998
+ function route(req, url, bridge) {
7915
7999
  const path2 = url.pathname;
7916
- if (path2 === "/file")
8000
+ if (path2 === "/file") {
8001
+ if (!isTrustedLoopbackHostname(url.hostname)) {
8002
+ return new Response("Forbidden", { status: 403 });
8003
+ }
7917
8004
  return serveFile(url);
8005
+ }
7918
8006
  if (path2 === "/health")
7919
8007
  return Response.json({ ok: true });
8008
+ if (path2.startsWith("/handoff/")) {
8009
+ return serveHandoffPage(req, url, bridge);
8010
+ }
7920
8011
  return new Response("Pairing file server", { status: 200 });
7921
8012
  }
7922
8013
  function serveFile(url) {
@@ -7930,6 +8021,520 @@ function serveFile(url) {
7930
8021
  return new Response("Not found", { status: 404 });
7931
8022
  return new Response(file);
7932
8023
  }
8024
+ function serveHandoffPage(req, url, bridge) {
8025
+ if (!bridge) {
8026
+ return new Response("Secure handoff unavailable", { status: 503 });
8027
+ }
8028
+ const scope = parseHandoffScope(url);
8029
+ if (!scope) {
8030
+ return new Response("Not found", { status: 404 });
8031
+ }
8032
+ const token = readHandoffToken(req);
8033
+ const authorized = readAuthorizedWebHandoff(token, scope);
8034
+ if (!authorized) {
8035
+ return unauthorizedHandoffResponse();
8036
+ }
8037
+ let html;
8038
+ switch (scope.kind) {
8039
+ case "session": {
8040
+ const snapshot = bridge.getSessionSnapshot(scope.sessionId);
8041
+ if (!snapshot) {
8042
+ return new Response("Session handoff expired", { status: 404 });
8043
+ }
8044
+ html = renderSessionHandoffPage(snapshot);
8045
+ break;
8046
+ }
8047
+ case "file_change": {
8048
+ const snapshot = bridge.getSessionSnapshot(scope.sessionId);
8049
+ if (!snapshot) {
8050
+ return new Response("Session handoff expired", { status: 404 });
8051
+ }
8052
+ const target = findFileChangeBlock(snapshot, scope.turnId, scope.blockId);
8053
+ if (!target) {
8054
+ return new Response("File change handoff expired", { status: 404 });
8055
+ }
8056
+ html = renderFileChangeHandoffPage(snapshot, target.turn, target.block);
8057
+ break;
8058
+ }
8059
+ }
8060
+ const headers = new Headers({
8061
+ "content-type": "text/html; charset=utf-8",
8062
+ "cache-control": "no-store",
8063
+ "x-content-type-options": "nosniff"
8064
+ });
8065
+ headers.set("set-cookie", `${SCOUT_WEB_HANDOFF_COOKIE}=${authorized.token}; Max-Age=300; Path=/handoff; HttpOnly; SameSite=Strict`);
8066
+ return new Response(html, { status: 200, headers });
8067
+ }
8068
+ function unauthorizedHandoffResponse() {
8069
+ return new Response("Secure handoff required", {
8070
+ status: 401,
8071
+ headers: {
8072
+ "cache-control": "no-store",
8073
+ "set-cookie": `${SCOUT_WEB_HANDOFF_COOKIE}=; Max-Age=0; Path=/handoff; HttpOnly; SameSite=Strict`
8074
+ }
8075
+ });
8076
+ }
8077
+ function parseHandoffScope(url) {
8078
+ const parts = url.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
8079
+ if (parts[0] !== "handoff") {
8080
+ return null;
8081
+ }
8082
+ if (parts[1] === "session" && parts[2]) {
8083
+ return { kind: "session", sessionId: parts[2] };
8084
+ }
8085
+ if (parts[1] === "file-change" && parts[2] && parts[3] && parts[4]) {
8086
+ return {
8087
+ kind: "file_change",
8088
+ sessionId: parts[2],
8089
+ turnId: parts[3],
8090
+ blockId: parts[4]
8091
+ };
8092
+ }
8093
+ return null;
8094
+ }
8095
+ function readHandoffToken(req) {
8096
+ const headerToken = req.headers.get("x-scout-handoff-token")?.trim();
8097
+ if (headerToken) {
8098
+ return headerToken;
8099
+ }
8100
+ const cookieHeader = req.headers.get("cookie");
8101
+ if (!cookieHeader) {
8102
+ return null;
8103
+ }
8104
+ for (const part of cookieHeader.split(";")) {
8105
+ const [name, ...rest] = part.trim().split("=");
8106
+ if (name === SCOUT_WEB_HANDOFF_COOKIE) {
8107
+ const value = rest.join("=").trim();
8108
+ return value || null;
8109
+ }
8110
+ }
8111
+ return null;
8112
+ }
8113
+ function findFileChangeBlock(snapshot, turnId, blockId) {
8114
+ const turn = snapshot.turns.find((candidate) => candidate.id === turnId);
8115
+ if (!turn) {
8116
+ return null;
8117
+ }
8118
+ const blockState = turn.blocks.find((candidate) => candidate.block.id === blockId);
8119
+ const block = blockState?.block;
8120
+ if (!block || block.type !== "action" || block.action.kind !== "file_change") {
8121
+ return null;
8122
+ }
8123
+ return { turn, block };
8124
+ }
8125
+ function renderPage(title, body) {
8126
+ return `<!doctype html>
8127
+ <html lang="en">
8128
+ <head>
8129
+ <meta charset="utf-8" />
8130
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8131
+ <title>${escapeHtml(title)}</title>
8132
+ <style>
8133
+ :root {
8134
+ color-scheme: dark;
8135
+ --bg: #0b0f14;
8136
+ --panel: rgba(19, 27, 39, 0.92);
8137
+ --panel-strong: rgba(12, 18, 28, 0.96);
8138
+ --line: rgba(148, 163, 184, 0.16);
8139
+ --line-strong: rgba(148, 163, 184, 0.24);
8140
+ --text: #e5ecf5;
8141
+ --muted: #90a0b4;
8142
+ --accent: #63d0ff;
8143
+ --green: #54d38a;
8144
+ --amber: #f8c36a;
8145
+ --red: #ff7b72;
8146
+ --surface-add: rgba(84, 211, 138, 0.08);
8147
+ --surface-del: rgba(255, 123, 114, 0.08);
8148
+ }
8149
+ * { box-sizing: border-box; }
8150
+ body {
8151
+ margin: 0;
8152
+ background:
8153
+ radial-gradient(circle at top left, rgba(99, 208, 255, 0.12), transparent 32%),
8154
+ linear-gradient(180deg, #090d12 0%, #0b0f14 55%, #111723 100%);
8155
+ color: var(--text);
8156
+ font: 14px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
8157
+ }
8158
+ .wrap {
8159
+ width: min(980px, calc(100vw - 24px));
8160
+ margin: 0 auto;
8161
+ padding: 20px 0 40px;
8162
+ }
8163
+ .hero, .panel {
8164
+ background: var(--panel);
8165
+ border: 1px solid var(--line);
8166
+ border-radius: 18px;
8167
+ backdrop-filter: blur(18px);
8168
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
8169
+ }
8170
+ .hero {
8171
+ padding: 18px 18px 16px;
8172
+ margin-bottom: 14px;
8173
+ }
8174
+ .eyebrow {
8175
+ display: inline-flex;
8176
+ align-items: center;
8177
+ gap: 8px;
8178
+ color: var(--muted);
8179
+ font-size: 11px;
8180
+ letter-spacing: 0.14em;
8181
+ text-transform: uppercase;
8182
+ }
8183
+ .dot {
8184
+ width: 8px;
8185
+ height: 8px;
8186
+ border-radius: 999px;
8187
+ background: var(--accent);
8188
+ box-shadow: 0 0 14px rgba(99, 208, 255, 0.65);
8189
+ }
8190
+ h1 {
8191
+ margin: 12px 0 10px;
8192
+ font-size: 24px;
8193
+ line-height: 1.15;
8194
+ }
8195
+ .meta, .stack {
8196
+ display: flex;
8197
+ flex-wrap: wrap;
8198
+ gap: 8px;
8199
+ }
8200
+ .pill {
8201
+ display: inline-flex;
8202
+ align-items: center;
8203
+ gap: 6px;
8204
+ padding: 5px 10px;
8205
+ border-radius: 999px;
8206
+ border: 1px solid var(--line-strong);
8207
+ background: rgba(255, 255, 255, 0.03);
8208
+ color: var(--muted);
8209
+ font-size: 12px;
8210
+ }
8211
+ .pill strong {
8212
+ color: var(--text);
8213
+ font-weight: 600;
8214
+ }
8215
+ .stack { flex-direction: column; }
8216
+ .panel {
8217
+ margin-top: 12px;
8218
+ overflow: hidden;
8219
+ }
8220
+ .panel-inner {
8221
+ padding: 16px;
8222
+ }
8223
+ .turn {
8224
+ border-top: 1px solid var(--line);
8225
+ }
8226
+ .turn:first-child {
8227
+ border-top: none;
8228
+ }
8229
+ .turn-head {
8230
+ padding: 14px 16px;
8231
+ display: flex;
8232
+ justify-content: space-between;
8233
+ gap: 12px;
8234
+ align-items: center;
8235
+ background: rgba(255, 255, 255, 0.02);
8236
+ }
8237
+ .turn-title {
8238
+ font-size: 13px;
8239
+ font-weight: 600;
8240
+ letter-spacing: 0.02em;
8241
+ }
8242
+ .turn-subtitle {
8243
+ color: var(--muted);
8244
+ font-size: 12px;
8245
+ margin-top: 2px;
8246
+ }
8247
+ .blocks {
8248
+ padding: 8px;
8249
+ }
8250
+ .block {
8251
+ padding: 12px;
8252
+ border-radius: 14px;
8253
+ border: 1px solid var(--line);
8254
+ background: var(--panel-strong);
8255
+ margin: 8px;
8256
+ }
8257
+ .block-head {
8258
+ display: flex;
8259
+ justify-content: space-between;
8260
+ align-items: center;
8261
+ gap: 12px;
8262
+ margin-bottom: 10px;
8263
+ }
8264
+ .block-type {
8265
+ color: var(--muted);
8266
+ text-transform: uppercase;
8267
+ letter-spacing: 0.12em;
8268
+ font-size: 11px;
8269
+ }
8270
+ .label {
8271
+ color: var(--muted);
8272
+ font-size: 12px;
8273
+ }
8274
+ .label strong {
8275
+ color: var(--text);
8276
+ }
8277
+ pre {
8278
+ margin: 0;
8279
+ white-space: pre-wrap;
8280
+ word-break: break-word;
8281
+ overflow-wrap: anywhere;
8282
+ font: 12.5px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
8283
+ }
8284
+ .code {
8285
+ padding: 12px;
8286
+ border-radius: 12px;
8287
+ border: 1px solid var(--line);
8288
+ background: rgba(255, 255, 255, 0.02);
8289
+ }
8290
+ .diff-line-add { background: var(--surface-add); color: #8ef3b7; }
8291
+ .diff-line-del { background: var(--surface-del); color: #ff9d96; }
8292
+ .details {
8293
+ margin-top: 10px;
8294
+ border: 1px solid var(--line);
8295
+ border-radius: 12px;
8296
+ overflow: hidden;
8297
+ }
8298
+ details > summary {
8299
+ list-style: none;
8300
+ cursor: pointer;
8301
+ padding: 10px 12px;
8302
+ background: rgba(255, 255, 255, 0.03);
8303
+ color: var(--muted);
8304
+ font-size: 12px;
8305
+ text-transform: uppercase;
8306
+ letter-spacing: 0.1em;
8307
+ }
8308
+ details[open] > summary {
8309
+ border-bottom: 1px solid var(--line);
8310
+ }
8311
+ .empty {
8312
+ padding: 28px 18px;
8313
+ color: var(--muted);
8314
+ }
8315
+ .grid {
8316
+ display: grid;
8317
+ gap: 10px;
8318
+ }
8319
+ .grid.two {
8320
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
8321
+ }
8322
+ a.inline {
8323
+ color: var(--accent);
8324
+ text-decoration: none;
8325
+ }
8326
+ .status-streaming { color: var(--amber); }
8327
+ .status-completed { color: var(--green); }
8328
+ .status-failed, .status-error { color: var(--red); }
8329
+ </style>
8330
+ </head>
8331
+ <body>
8332
+ <main class="wrap">
8333
+ ${body}
8334
+ </main>
8335
+ </body>
8336
+ </html>`;
8337
+ }
8338
+ function renderSessionHandoffPage(snapshot) {
8339
+ const title = snapshot.session.name || snapshot.session.id;
8340
+ const hero = `
8341
+ <section class="hero">
8342
+ <div class="eyebrow"><span class="dot"></span> Secure Proxy Session Handoff</div>
8343
+ <h1>${escapeHtml(title)}</h1>
8344
+ <div class="meta">
8345
+ <span class="pill"><strong>ID</strong> ${escapeHtml(snapshot.session.id)}</span>
8346
+ <span class="pill"><strong>Adapter</strong> ${escapeHtml(snapshot.session.adapterType)}</span>
8347
+ <span class="pill"><strong>Status</strong> ${escapeHtml(snapshot.session.status)}</span>
8348
+ ${snapshot.session.model ? `<span class="pill"><strong>Model</strong> ${escapeHtml(snapshot.session.model)}</span>` : ""}
8349
+ ${snapshot.session.cwd ? `<span class="pill"><strong>Workspace</strong> ${escapeHtml(snapshot.session.cwd)}</span>` : ""}
8350
+ </div>
8351
+ </section>
8352
+ `;
8353
+ const turns = snapshot.turns.length > 0 ? snapshot.turns.map((turn) => {
8354
+ const turnBody = turn.blocks.length > 0 ? turn.blocks.map(({ block }) => renderBlock(block)).join("") : `<div class="empty">No blocks were captured for this turn.</div>`;
8355
+ return `
8356
+ <section class="turn">
8357
+ <div class="turn-head">
8358
+ <div>
8359
+ <div class="turn-title">${escapeHtml(turn.id)}</div>
8360
+ <div class="turn-subtitle">${formatTimestamp(turn.startedAt)}${turn.endedAt ? ` to ${formatTimestamp(turn.endedAt)}` : ""}</div>
8361
+ </div>
8362
+ <span class="pill"><strong>Turn</strong> <span class="status-${escapeHtml(turn.status)}">${escapeHtml(turn.status)}</span></span>
8363
+ </div>
8364
+ <div class="blocks">${turnBody}</div>
8365
+ </section>
8366
+ `;
8367
+ }).join("") : `<div class="empty">This session has not produced any turns yet.</div>`;
8368
+ return renderPage(title, `${hero}<section class="panel">${turns}</section>`);
8369
+ }
8370
+ function renderFileChangeHandoffPage(snapshot, turn, block) {
8371
+ const action = block.action;
8372
+ const title = action.path || snapshot.session.name || snapshot.session.id;
8373
+ const hero = `
8374
+ <section class="hero">
8375
+ <div class="eyebrow"><span class="dot"></span> Secure Proxy File Change</div>
8376
+ <h1>${escapeHtml(title)}</h1>
8377
+ <div class="meta">
8378
+ <span class="pill"><strong>Session</strong> ${escapeHtml(snapshot.session.name || snapshot.session.id)}</span>
8379
+ <span class="pill"><strong>Turn</strong> ${escapeHtml(turn.id)}</span>
8380
+ <span class="pill"><strong>Status</strong> ${escapeHtml(action.status)}</span>
8381
+ </div>
8382
+ </section>
8383
+ `;
8384
+ const body = `
8385
+ <section class="panel">
8386
+ <div class="panel-inner stack">
8387
+ <div class="grid two">
8388
+ <div class="pill"><strong>Path</strong> ${escapeHtml(action.path)}</div>
8389
+ <div class="pill"><strong>Block</strong> ${escapeHtml(block.id)}</div>
8390
+ </div>
8391
+ ${action.diff ? renderDetailsSection("Diff", renderDiff(action.diff), true) : `<div class="empty">No diff was recorded for this action.</div>`}
8392
+ ${action.output ? renderDetailsSection("Output", `<div class="code"><pre>${escapeHtml(action.output)}</pre></div>`) : ""}
8393
+ </div>
8394
+ </section>
8395
+ `;
8396
+ return renderPage(title, `${hero}${body}`);
8397
+ }
8398
+ function renderBlock(block) {
8399
+ switch (block.type) {
8400
+ case "text":
8401
+ return renderTextLikeBlock("Text", block.text);
8402
+ case "reasoning":
8403
+ return renderTextLikeBlock("Reasoning", block.text);
8404
+ case "error":
8405
+ return `
8406
+ <article class="block">
8407
+ <div class="block-head">
8408
+ <div class="block-type">Error</div>
8409
+ <span class="pill status-error">${escapeHtml(block.status)}</span>
8410
+ </div>
8411
+ <div class="code"><pre>${escapeHtml(block.message)}</pre></div>
8412
+ </article>
8413
+ `;
8414
+ case "file":
8415
+ return `
8416
+ <article class="block">
8417
+ <div class="block-head">
8418
+ <div class="block-type">File</div>
8419
+ <span class="pill">${escapeHtml(block.status)}</span>
8420
+ </div>
8421
+ <div class="grid">
8422
+ ${block.name ? `<div class="label"><strong>Name</strong> ${escapeHtml(block.name)}</div>` : ""}
8423
+ <div class="label"><strong>MIME</strong> ${escapeHtml(block.mimeType)}</div>
8424
+ </div>
8425
+ </article>
8426
+ `;
8427
+ case "question":
8428
+ return renderQuestionBlock(block);
8429
+ case "action":
8430
+ return renderActionBlock(block);
8431
+ }
8432
+ }
8433
+ function renderTextLikeBlock(label, text) {
8434
+ return `
8435
+ <article class="block">
8436
+ <div class="block-head">
8437
+ <div class="block-type">${escapeHtml(label)}</div>
8438
+ </div>
8439
+ <div class="code"><pre>${escapeHtml(text)}</pre></div>
8440
+ </article>
8441
+ `;
8442
+ }
8443
+ function renderQuestionBlock(block) {
8444
+ const answer = block.answer?.length ? block.answer.join(", ") : "Awaiting answer";
8445
+ const options = block.options?.length ? `<div class="label"><strong>Options</strong> ${escapeHtml(block.options.map((option) => option.label).join(", "))}</div>` : "";
8446
+ return `
8447
+ <article class="block">
8448
+ <div class="block-head">
8449
+ <div class="block-type">Question</div>
8450
+ <span class="pill">${escapeHtml(block.questionStatus)}</span>
8451
+ </div>
8452
+ ${block.header ? `<div class="label"><strong>${escapeHtml(block.header)}</strong></div>` : ""}
8453
+ <div class="code"><pre>${escapeHtml(block.question)}</pre></div>
8454
+ <div class="stack" style="margin-top: 10px;">
8455
+ ${options}
8456
+ <div class="label"><strong>Answer</strong> ${escapeHtml(answer)}</div>
8457
+ </div>
8458
+ </article>
8459
+ `;
8460
+ }
8461
+ function renderActionBlock(block) {
8462
+ const action = block.action;
8463
+ const parts = [];
8464
+ switch (action.kind) {
8465
+ case "file_change":
8466
+ parts.push(`<div class="label"><strong>Path</strong> ${escapeHtml(action.path)}</div>`);
8467
+ if (action.diff) {
8468
+ parts.push(renderDetailsSection("Diff", renderDiff(action.diff)));
8469
+ }
8470
+ break;
8471
+ case "command":
8472
+ parts.push(`<div class="label"><strong>Command</strong></div><div class="code"><pre>${escapeHtml(action.command)}</pre></div>`);
8473
+ if (typeof action.exitCode === "number") {
8474
+ parts.push(`<div class="label"><strong>Exit Code</strong> ${escapeHtml(String(action.exitCode))}</div>`);
8475
+ }
8476
+ break;
8477
+ case "tool_call":
8478
+ parts.push(`<div class="label"><strong>Tool</strong> ${escapeHtml(action.toolName)}</div>`);
8479
+ if (action.toolCallId) {
8480
+ parts.push(`<div class="label"><strong>Call ID</strong> ${escapeHtml(action.toolCallId)}</div>`);
8481
+ }
8482
+ break;
8483
+ case "subagent":
8484
+ parts.push(`<div class="label"><strong>Agent</strong> ${escapeHtml(action.agentName || action.agentId)}</div>`);
8485
+ if (action.prompt) {
8486
+ parts.push(`<div class="code"><pre>${escapeHtml(action.prompt)}</pre></div>`);
8487
+ }
8488
+ break;
8489
+ }
8490
+ if (action.approval) {
8491
+ parts.push(`<div class="label"><strong>Approval</strong> ${escapeHtml(action.approval.risk || "medium")} risk${action.approval.description ? ` - ${escapeHtml(action.approval.description)}` : ""}</div>`);
8492
+ }
8493
+ if (action.output) {
8494
+ parts.push(renderDetailsSection("Output", `<div class="code"><pre>${escapeHtml(action.output)}</pre></div>`));
8495
+ }
8496
+ return `
8497
+ <article class="block">
8498
+ <div class="block-head">
8499
+ <div class="block-type">${escapeHtml(action.kind.replace("_", " "))}</div>
8500
+ <span class="pill">${escapeHtml(action.status)}</span>
8501
+ </div>
8502
+ <div class="stack">${parts.join("")}</div>
8503
+ </article>
8504
+ `;
8505
+ }
8506
+ function renderDetailsSection(title, innerHtml, open = false) {
8507
+ return `
8508
+ <details class="details"${open ? " open" : ""}>
8509
+ <summary>${escapeHtml(title)}</summary>
8510
+ <div class="panel-inner">${innerHtml}</div>
8511
+ </details>
8512
+ `;
8513
+ }
8514
+ function renderDiff(diff) {
8515
+ const lines = diff.split(`
8516
+ `).map((line) => {
8517
+ const className = line.startsWith("+") ? "diff-line-add" : line.startsWith("-") ? "diff-line-del" : "";
8518
+ return `<div class="${className}"><pre>${escapeHtml(line || " ")}</pre></div>`;
8519
+ }).join("");
8520
+ return `<div class="code">${lines}</div>`;
8521
+ }
8522
+ function escapeHtml(value) {
8523
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8524
+ }
8525
+ function formatTimestamp(value) {
8526
+ if (typeof value !== "number" || !Number.isFinite(value)) {
8527
+ return "Unknown time";
8528
+ }
8529
+ try {
8530
+ return new Intl.DateTimeFormat("en-US", {
8531
+ dateStyle: "medium",
8532
+ timeStyle: "short"
8533
+ }).format(value);
8534
+ } catch {
8535
+ return new Date(value).toISOString();
8536
+ }
8537
+ }
7933
8538
 
7934
8539
  // ../../apps/desktop/src/core/pairing/runtime/bridge/server.ts
7935
8540
  import { readdirSync as readdirSync3, readFileSync as readFileSync9, realpathSync, statSync as statSync3 } from "fs";
@@ -12151,8 +12756,107 @@ async function shutdownClaudeStreamJsonAgent(options, shutdownOptions = {}) {
12151
12756
 
12152
12757
  // ../runtime/src/codex-app-server.ts
12153
12758
  import { spawn as spawn3 } from "child_process";
12154
- import { access as access3, appendFile as appendFile3, constants as constants3, mkdir as mkdir5, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "fs/promises";
12759
+ import { constants as constants3 } from "fs";
12760
+ import { access as access3, appendFile as appendFile3, mkdir as mkdir5, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "fs/promises";
12155
12761
  import { delimiter as delimiter3, join as join13 } from "path";
12762
+ function normalizeCodexModelValue(value) {
12763
+ const trimmed = value?.trim();
12764
+ return trimmed ? trimmed : null;
12765
+ }
12766
+ function encodeCodexModelConfig(model) {
12767
+ return `model=${JSON.stringify(model)}`;
12768
+ }
12769
+ function parseCodexModelConfig(value) {
12770
+ const trimmed = value?.trim();
12771
+ if (!trimmed) {
12772
+ return null;
12773
+ }
12774
+ const separatorIndex = trimmed.indexOf("=");
12775
+ if (separatorIndex <= 0) {
12776
+ return null;
12777
+ }
12778
+ const key = trimmed.slice(0, separatorIndex).trim();
12779
+ if (key !== "model") {
12780
+ return null;
12781
+ }
12782
+ const rawValue = trimmed.slice(separatorIndex + 1).trim();
12783
+ if (!rawValue) {
12784
+ return null;
12785
+ }
12786
+ if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
12787
+ return rawValue.slice(1, -1) || null;
12788
+ }
12789
+ return rawValue;
12790
+ }
12791
+ function normalizeCodexAppServerLaunchArgs(launchArgs) {
12792
+ const args = Array.isArray(launchArgs) ? launchArgs.map((entry) => String(entry).trim()).filter(Boolean) : [];
12793
+ const normalized = [];
12794
+ for (let index = 0;index < args.length; index += 1) {
12795
+ const current = args[index] ?? "";
12796
+ if (current === "--model" || current === "-m") {
12797
+ const model = normalizeCodexModelValue(args[index + 1]);
12798
+ if (model) {
12799
+ normalized.push("-c", encodeCodexModelConfig(model));
12800
+ index += 1;
12801
+ continue;
12802
+ }
12803
+ normalized.push(current);
12804
+ continue;
12805
+ }
12806
+ if (current.startsWith("--model=")) {
12807
+ const model = normalizeCodexModelValue(current.slice("--model=".length));
12808
+ if (model) {
12809
+ normalized.push("-c", encodeCodexModelConfig(model));
12810
+ continue;
12811
+ }
12812
+ }
12813
+ if (current.startsWith("-m=")) {
12814
+ const model = normalizeCodexModelValue(current.slice(3));
12815
+ if (model) {
12816
+ normalized.push("-c", encodeCodexModelConfig(model));
12817
+ continue;
12818
+ }
12819
+ }
12820
+ if (current === "-c" || current === "--config") {
12821
+ const next = args[index + 1];
12822
+ if (typeof next === "string") {
12823
+ const model = parseCodexModelConfig(next);
12824
+ normalized.push(current === "--config" ? "--config" : "-c", model ? encodeCodexModelConfig(model) : next);
12825
+ index += 1;
12826
+ continue;
12827
+ }
12828
+ }
12829
+ if (current.startsWith("--config=")) {
12830
+ const value = current.slice("--config=".length);
12831
+ const model = parseCodexModelConfig(value);
12832
+ normalized.push(model ? `--config=${encodeCodexModelConfig(model)}` : current);
12833
+ continue;
12834
+ }
12835
+ normalized.push(current);
12836
+ }
12837
+ return normalized;
12838
+ }
12839
+ function readCodexAppServerModelFromLaunchArgs(launchArgs) {
12840
+ const normalized = normalizeCodexAppServerLaunchArgs(launchArgs);
12841
+ for (let index = 0;index < normalized.length; index += 1) {
12842
+ const current = normalized[index] ?? "";
12843
+ if (current === "-c" || current === "--config") {
12844
+ const model = parseCodexModelConfig(normalized[index + 1]);
12845
+ if (model) {
12846
+ return model;
12847
+ }
12848
+ index += 1;
12849
+ continue;
12850
+ }
12851
+ if (current.startsWith("--config=")) {
12852
+ const model = parseCodexModelConfig(current.slice("--config=".length));
12853
+ if (model) {
12854
+ return model;
12855
+ }
12856
+ }
12857
+ }
12858
+ return null;
12859
+ }
12156
12860
  function sessionKey2(options) {
12157
12861
  return `${options.agentName}:${options.sessionId}`;
12158
12862
  }
@@ -12261,6 +12965,13 @@ function isMissingCodexRolloutError2(error) {
12261
12965
  const message = errorMessage3(error).toLowerCase();
12262
12966
  return message.includes("no rollout found for thread id");
12263
12967
  }
12968
+ function resolveCodexCompletionGraceMs() {
12969
+ const parsed = Number.parseInt(process.env.OPENSCOUT_CODEX_COMPLETION_GRACE_MS ?? "", 10);
12970
+ if (Number.isFinite(parsed) && parsed >= 0) {
12971
+ return parsed;
12972
+ }
12973
+ return 60000;
12974
+ }
12264
12975
 
12265
12976
  class CodexAppServerSession {
12266
12977
  options;
@@ -12450,7 +13161,7 @@ class CodexAppServerSession {
12450
13161
  systemPrompt: options.systemPrompt,
12451
13162
  threadId: options.threadId ?? null,
12452
13163
  requireExistingThread: options.requireExistingThread === true,
12453
- launchArgs: Array.isArray(options.launchArgs) ? options.launchArgs : []
13164
+ launchArgs: normalizeCodexAppServerLaunchArgs(options.launchArgs)
12454
13165
  });
12455
13166
  }
12456
13167
  async ensureStarted() {
@@ -12472,6 +13183,7 @@ class CodexAppServerSession {
12472
13183
  await mkdir5(this.options.logsDirectory, { recursive: true });
12473
13184
  await writeFile5(join13(this.options.runtimeDirectory, "prompt.txt"), this.options.systemPrompt);
12474
13185
  const codexExecutable = await resolveCodexExecutable2();
13186
+ const launchArgs = normalizeCodexAppServerLaunchArgs(this.options.launchArgs);
12475
13187
  const env = buildManagedAgentEnvironment({
12476
13188
  agentName: this.options.agentName,
12477
13189
  currentDirectory: this.options.cwd,
@@ -12482,7 +13194,8 @@ class CodexAppServerSession {
12482
13194
  ...buildScoutMcpCodexLaunchArgs({
12483
13195
  currentDirectory: this.options.cwd,
12484
13196
  env
12485
- })
13197
+ }),
13198
+ ...launchArgs
12486
13199
  ], {
12487
13200
  cwd: this.options.cwd,
12488
13201
  env,
@@ -12580,6 +13293,8 @@ class CodexAppServerSession {
12580
13293
  turnId: "",
12581
13294
  startedAt: Date.now(),
12582
13295
  timer: null,
13296
+ graceTimer: null,
13297
+ timedOutAt: null,
12583
13298
  messageOrder: [],
12584
13299
  messageByItemId: new Map,
12585
13300
  resolve: resolve4,
@@ -12587,20 +13302,43 @@ class CodexAppServerSession {
12587
13302
  watchers: []
12588
13303
  };
12589
13304
  turn.timer = setTimeout(() => {
12590
- this.interrupt().catch(() => {
12591
- return;
12592
- });
12593
- if (this.activeTurn === turn) {
12594
- this.activeTurn = null;
12595
- }
12596
- for (const watcher of this.drainTurnWatchers(turn)) {
12597
- watcher.reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
12598
- }
12599
- reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
13305
+ this.scheduleTurnTimeout(turn, timeoutMs);
12600
13306
  }, timeoutMs);
12601
13307
  this.activeTurn = turn;
12602
13308
  return turn;
12603
13309
  }
13310
+ buildTurnTimeoutError(timeoutMs) {
13311
+ return new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`);
13312
+ }
13313
+ scheduleTurnTimeout(turn, timeoutMs) {
13314
+ if (turn.timedOutAt !== null) {
13315
+ return;
13316
+ }
13317
+ turn.timedOutAt = Date.now();
13318
+ this.interrupt().catch(() => {
13319
+ return;
13320
+ });
13321
+ const graceMs = resolveCodexCompletionGraceMs();
13322
+ if (graceMs <= 0) {
13323
+ this.rejectTimedOutTurn(turn, timeoutMs);
13324
+ return;
13325
+ }
13326
+ turn.graceTimer = setTimeout(() => {
13327
+ this.rejectTimedOutTurn(turn, timeoutMs);
13328
+ }, graceMs);
13329
+ }
13330
+ rejectTimedOutTurn(turn, timeoutMs) {
13331
+ if (this.activeTurn !== turn) {
13332
+ this.clearActiveTurn(turn);
13333
+ return;
13334
+ }
13335
+ this.clearActiveTurn(turn);
13336
+ const error = this.buildTurnTimeoutError(timeoutMs);
13337
+ for (const watcher of this.drainTurnWatchers(turn)) {
13338
+ watcher.reject(error);
13339
+ }
13340
+ turn.reject(error);
13341
+ }
12604
13342
  addTurnWatcher(turn, resolve4, reject, timeoutMs) {
12605
13343
  const watcher = {
12606
13344
  resolve: resolve4,
@@ -12634,6 +13372,9 @@ class CodexAppServerSession {
12634
13372
  if (turn.timer) {
12635
13373
  clearTimeout(turn.timer);
12636
13374
  }
13375
+ if (turn.graceTimer) {
13376
+ clearTimeout(turn.graceTimer);
13377
+ }
12637
13378
  if (this.activeTurn === turn) {
12638
13379
  this.activeTurn = null;
12639
13380
  }
@@ -13833,6 +14574,128 @@ function normalizeLocalAgentCapabilities(value) {
13833
14574
  function normalizeLocalAgentLaunchArgs(value) {
13834
14575
  return Array.isArray(value) ? value.map((entry) => String(entry).trim()).filter(Boolean) : [];
13835
14576
  }
14577
+ function normalizeRequestedModel(value) {
14578
+ const trimmed = value?.trim();
14579
+ return trimmed ? trimmed : undefined;
14580
+ }
14581
+ function readClaudeLaunchModel(launchArgs) {
14582
+ for (let index = 0;index < launchArgs.length; index += 1) {
14583
+ const current = launchArgs[index] ?? "";
14584
+ if (current === "--model") {
14585
+ const next = launchArgs[index + 1]?.trim();
14586
+ return next || undefined;
14587
+ }
14588
+ if (current.startsWith("--model=")) {
14589
+ const next = current.slice("--model=".length).trim();
14590
+ return next || undefined;
14591
+ }
14592
+ }
14593
+ return;
14594
+ }
14595
+ function normalizeLaunchArgsForHarness(harness, value) {
14596
+ const normalized = normalizeLocalAgentLaunchArgs(value);
14597
+ if (harness === "codex") {
14598
+ return normalizeCodexAppServerLaunchArgs(normalized);
14599
+ }
14600
+ return normalized;
14601
+ }
14602
+ function readLaunchModelForHarness(harness, launchArgs) {
14603
+ if (harness === "codex") {
14604
+ return readCodexAppServerModelFromLaunchArgs(launchArgs) ?? undefined;
14605
+ }
14606
+ if (harness === "claude") {
14607
+ return readClaudeLaunchModel(launchArgs ?? []);
14608
+ }
14609
+ return;
14610
+ }
14611
+ function stripLaunchModelForHarness(harness, launchArgs) {
14612
+ if (harness === "codex") {
14613
+ const next = [];
14614
+ const normalized = normalizeCodexAppServerLaunchArgs(launchArgs);
14615
+ for (let index = 0;index < normalized.length; index += 1) {
14616
+ const current = normalized[index] ?? "";
14617
+ if (current === "-c" || current === "--config") {
14618
+ const value = normalized[index + 1];
14619
+ if (readCodexAppServerModelFromLaunchArgs(value ? [current, value] : [current])) {
14620
+ index += 1;
14621
+ continue;
14622
+ }
14623
+ next.push(current);
14624
+ if (value) {
14625
+ next.push(value);
14626
+ index += 1;
14627
+ }
14628
+ continue;
14629
+ }
14630
+ if (current.startsWith("--config=")) {
14631
+ if (readCodexAppServerModelFromLaunchArgs([current])) {
14632
+ continue;
14633
+ }
14634
+ }
14635
+ next.push(current);
14636
+ }
14637
+ return next;
14638
+ }
14639
+ if (harness === "claude") {
14640
+ const next = [];
14641
+ const normalized = normalizeLocalAgentLaunchArgs(launchArgs);
14642
+ for (let index = 0;index < normalized.length; index += 1) {
14643
+ const current = normalized[index] ?? "";
14644
+ if (current === "--model") {
14645
+ index += 1;
14646
+ continue;
14647
+ }
14648
+ if (current.startsWith("--model=")) {
14649
+ continue;
14650
+ }
14651
+ next.push(current);
14652
+ }
14653
+ return next;
14654
+ }
14655
+ return normalizeLocalAgentLaunchArgs(launchArgs);
14656
+ }
14657
+ function buildLaunchArgsForRequestedModel(harness, model) {
14658
+ if (harness === "codex") {
14659
+ return normalizeCodexAppServerLaunchArgs(["--model", model]);
14660
+ }
14661
+ if (harness === "claude") {
14662
+ return ["--model", model];
14663
+ }
14664
+ return [];
14665
+ }
14666
+ function applyRequestedModelToLaunchArgs(harness, launchArgs, model) {
14667
+ const normalized = normalizeLaunchArgsForHarness(harness, launchArgs);
14668
+ const requestedModel = normalizeRequestedModel(model);
14669
+ if (!requestedModel) {
14670
+ return normalized;
14671
+ }
14672
+ return [
14673
+ ...stripLaunchModelForHarness(harness, normalized),
14674
+ ...buildLaunchArgsForRequestedModel(harness, requestedModel)
14675
+ ];
14676
+ }
14677
+ function defaultHarnessForOverride(override, fallback = "claude") {
14678
+ return normalizeManagedHarness2(override.defaultHarness ?? override.runtime?.harness, fallback);
14679
+ }
14680
+ function launchArgsForOverrideHarness(override, harness) {
14681
+ const profileLaunchArgs = override.harnessProfiles?.[harness]?.launchArgs;
14682
+ if (profileLaunchArgs) {
14683
+ return normalizeLaunchArgsForHarness(harness, profileLaunchArgs);
14684
+ }
14685
+ if (harness === defaultHarnessForOverride(override, harness)) {
14686
+ return normalizeLaunchArgsForHarness(harness, override.launchArgs);
14687
+ }
14688
+ return [];
14689
+ }
14690
+ function overrideHarnessProfile(override, harness) {
14691
+ const profile = override.harnessProfiles?.[harness];
14692
+ return {
14693
+ cwd: normalizeProjectPath(profile?.cwd || override.runtime?.cwd || override.projectRoot || process.cwd()),
14694
+ transport: normalizeLocalAgentTransport(profile?.transport ?? override.runtime?.transport, harness),
14695
+ sessionId: normalizeTmuxSessionName2(profile?.sessionId, `${override.agentId}-${harness}`),
14696
+ launchArgs: launchArgsForOverrideHarness(override, harness)
14697
+ };
14698
+ }
13836
14699
  function normalizeManagedHarness2(value, fallback) {
13837
14700
  return value === "codex" ? "codex" : value === "claude" ? "claude" : fallback;
13838
14701
  }
@@ -13848,7 +14711,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
13848
14711
  cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
13849
14712
  transport: normalizeLocalAgentTransport(profile.transport, harness),
13850
14713
  sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
13851
- launchArgs: normalizeLocalAgentLaunchArgs(profile.launchArgs)
14714
+ launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
13852
14715
  };
13853
14716
  }
13854
14717
  const runtimeHarness = normalizeManagedHarness2(record.harness, defaultHarness);
@@ -13857,7 +14720,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
13857
14720
  cwd: normalizeProjectPath(record.cwd || process.cwd()),
13858
14721
  transport: normalizeLocalAgentTransport(record.transport, runtimeHarness),
13859
14722
  sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${runtimeHarness}`),
13860
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
14723
+ launchArgs: normalizeLaunchArgsForHarness(runtimeHarness, record.launchArgs)
13861
14724
  };
13862
14725
  }
13863
14726
  if (!nextProfiles[defaultHarness]) {
@@ -13865,7 +14728,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
13865
14728
  cwd: normalizeProjectPath(record.cwd || process.cwd()),
13866
14729
  transport: normalizeLocalAgentTransport(record.transport, defaultHarness),
13867
14730
  sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${defaultHarness}`),
13868
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
14731
+ launchArgs: normalizeLaunchArgsForHarness(defaultHarness, record.launchArgs)
13869
14732
  };
13870
14733
  }
13871
14734
  for (const harness of ["claude", "codex"]) {
@@ -13877,7 +14740,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
13877
14740
  cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
13878
14741
  transport: normalizeLocalAgentTransport(profile.transport, harness),
13879
14742
  sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
13880
- launchArgs: normalizeLocalAgentLaunchArgs(profile.launchArgs)
14743
+ launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
13881
14744
  };
13882
14745
  }
13883
14746
  return nextProfiles;
@@ -13901,14 +14764,14 @@ function recordForHarness(record, harnessOverride) {
13901
14764
  tmuxSession: fallbackSessionId,
13902
14765
  cwd: fallbackCwd,
13903
14766
  transport: fallbackTransport,
13904
- launchArgs: normalizeLocalAgentLaunchArgs(normalized.launchArgs),
14767
+ launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs),
13905
14768
  harnessProfiles: {
13906
14769
  ...normalized.harnessProfiles,
13907
14770
  [selectedHarness]: {
13908
14771
  cwd: fallbackCwd,
13909
14772
  transport: fallbackTransport,
13910
14773
  sessionId: fallbackSessionId,
13911
- launchArgs: normalizeLocalAgentLaunchArgs(normalized.launchArgs)
14774
+ launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs)
13912
14775
  }
13913
14776
  }
13914
14777
  };
@@ -13950,7 +14813,7 @@ function normalizeLocalAgentRecord(agentId, record) {
13950
14813
  harnessProfiles,
13951
14814
  transport: activeProfile?.transport ?? normalizeLocalAgentTransport(record.transport, harness),
13952
14815
  capabilities: normalizeLocalAgentCapabilities(record.capabilities),
13953
- launchArgs: activeProfile?.launchArgs ?? normalizeLocalAgentLaunchArgs(record.launchArgs)
14816
+ launchArgs: activeProfile?.launchArgs ?? normalizeLaunchArgsForHarness(harness, record.launchArgs)
13954
14817
  };
13955
14818
  }
13956
14819
  function localAgentStatusFromRecord(agentId, record, source) {
@@ -14019,7 +14882,7 @@ function relayAgentOverrideFromLocalAgentRecord(agentId, record, existing) {
14019
14882
  source: existing?.source && existing.source !== "inferred" ? existing.source : "manual",
14020
14883
  startedAt: normalizedRecord.startedAt,
14021
14884
  systemPrompt: normalizedRecord.systemPrompt,
14022
- launchArgs: normalizeLocalAgentLaunchArgs(normalizedRecord.launchArgs),
14885
+ launchArgs: normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs),
14023
14886
  capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
14024
14887
  defaultHarness: normalizeManagedHarness2(normalizedRecord.defaultHarness, "claude"),
14025
14888
  harnessProfiles: normalizedRecord.harnessProfiles,
@@ -14040,7 +14903,7 @@ function buildCodexAgentSessionOptions(agentName, record, systemPrompt) {
14040
14903
  systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "codex_app_server" }),
14041
14904
  runtimeDirectory: relayAgentRuntimeDirectory(agentName),
14042
14905
  logsDirectory: relayAgentLogsDirectory(agentName),
14043
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
14906
+ launchArgs: normalizeLaunchArgsForHarness("codex", record.launchArgs)
14044
14907
  };
14045
14908
  }
14046
14909
  function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
@@ -14051,7 +14914,7 @@ function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
14051
14914
  systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "claude_stream_json" }),
14052
14915
  runtimeDirectory: relayAgentRuntimeDirectory(agentName),
14053
14916
  logsDirectory: relayAgentLogsDirectory(agentName),
14054
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
14917
+ launchArgs: normalizeLaunchArgsForHarness("claude", record.launchArgs)
14055
14918
  };
14056
14919
  }
14057
14920
  function isLocalAgentRecordOnline(agentName, record) {
@@ -14085,7 +14948,7 @@ function buildLocalAgentBootstrapPrompt(_harness, _systemPrompt, initialMessage)
14085
14948
  return initialMessage;
14086
14949
  }
14087
14950
  function buildLocalAgentLaunchCommand(agentName, record, projectPath, promptFile, workerScript) {
14088
- const extraArgs = shellQuoteArguments(normalizeLocalAgentLaunchArgs(record.launchArgs));
14951
+ const extraArgs = shellQuoteArguments(normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(record.harness), record.launchArgs));
14089
14952
  if (normalizeLocalAgentHarness(record.harness) === "codex") {
14090
14953
  return `exec bash ${JSON.stringify(workerScript ?? join15(relayAgentRuntimeDirectory(agentName), "codex-worker.sh"))}`;
14091
14954
  }
@@ -14351,6 +15214,7 @@ async function startLocalAgent(input) {
14351
15214
  const effectiveHarness = normalizeManagedHarness2(preferredHarness ?? configDefaultHarness, "claude");
14352
15215
  const transport = normalizeLocalAgentTransport(undefined, effectiveHarness);
14353
15216
  const sessionId = normalizeTmuxSessionName2(undefined, `${instance.id}-${effectiveHarness}`);
15217
+ const launchArgs = applyRequestedModelToLaunchArgs(effectiveHarness, [], input.model);
14354
15218
  const existingForInstance = overrides[instance.id];
14355
15219
  if (existingForInstance && normalizeProjectPath(existingForInstance.projectRoot) !== projectRoot) {
14356
15220
  throw new Error(`Another agent is already registered as ${instance.id} at ${existingForInstance.projectRoot}. ` + `Two clones of the same project on the same branch cannot both register here; ` + `rename one of the checkouts or switch its branch before running scout up.`);
@@ -14364,13 +15228,14 @@ async function startLocalAgent(input) {
14364
15228
  projectConfigPath: coldProjectConfigPath,
14365
15229
  source: "manual",
14366
15230
  startedAt: nowSeconds2(),
15231
+ launchArgs,
14367
15232
  defaultHarness: effectiveHarness,
14368
15233
  harnessProfiles: {
14369
15234
  [effectiveHarness]: {
14370
15235
  cwd: effectiveCwd ?? projectRoot,
14371
15236
  transport,
14372
15237
  sessionId,
14373
- launchArgs: []
15238
+ launchArgs
14374
15239
  }
14375
15240
  },
14376
15241
  runtime: {
@@ -14391,6 +15256,9 @@ async function startLocalAgent(input) {
14391
15256
  throw new Error(`Another agent is already registered as ${instance.id} at ${existingForInstance.projectRoot}. ` + `Two clones of the same project on the same branch cannot both register here; ` + `rename one of the checkouts or switch its branch before running scout up.`);
14392
15257
  }
14393
15258
  const resolvedHarness = preferredHarness ?? matchingOverride.runtime?.harness;
15259
+ const nextHarness = normalizeManagedHarness2(resolvedHarness, "claude");
15260
+ const nextDefaultHarness = preferredHarness ? normalizeManagedHarness2(preferredHarness, "claude") : defaultHarnessForOverride(matchingOverride, "claude");
15261
+ const nextLaunchArgs = applyRequestedModelToLaunchArgs(nextHarness, launchArgsForOverrideHarness(matchingOverride, nextHarness), input.model);
14394
15262
  overrides[instance.id] = {
14395
15263
  agentId: instance.id,
14396
15264
  definitionId: requestedDefinitionId,
@@ -14401,10 +15269,16 @@ async function startLocalAgent(input) {
14401
15269
  source: "manual",
14402
15270
  startedAt: matchingOverride.startedAt ?? nowSeconds2(),
14403
15271
  systemPrompt: matchingOverride.systemPrompt,
14404
- launchArgs: matchingOverride.launchArgs,
15272
+ launchArgs: nextHarness === nextDefaultHarness ? nextLaunchArgs : matchingOverride.launchArgs,
14405
15273
  capabilities: matchingOverride.capabilities,
14406
- defaultHarness: normalizeManagedHarness2(preferredHarness ?? matchingOverride.defaultHarness, "claude"),
14407
- harnessProfiles: matchingOverride.harnessProfiles,
15274
+ defaultHarness: nextDefaultHarness,
15275
+ harnessProfiles: {
15276
+ ...matchingOverride.harnessProfiles ?? {},
15277
+ [nextHarness]: {
15278
+ ...overrideHarnessProfile(matchingOverride, nextHarness),
15279
+ launchArgs: nextLaunchArgs
15280
+ }
15281
+ },
14408
15282
  runtime: {
14409
15283
  cwd: effectiveCwd ?? matchingOverride.runtime?.cwd ?? matchingProjectRoot,
14410
15284
  harness: resolvedHarness,
@@ -14416,6 +15290,22 @@ async function startLocalAgent(input) {
14416
15290
  await writeRelayAgentOverrides(overrides);
14417
15291
  targetAgentId = instance.id;
14418
15292
  } else {
15293
+ if (input.model) {
15294
+ const existingHarness = normalizeManagedHarness2(preferredHarness ?? matchingOverride.defaultHarness ?? matchingOverride.runtime?.harness, "claude");
15295
+ const nextLaunchArgs = applyRequestedModelToLaunchArgs(existingHarness, launchArgsForOverrideHarness(matchingOverride, existingHarness), input.model);
15296
+ overrides[matchingAgentId] = {
15297
+ ...matchingOverride,
15298
+ launchArgs: existingHarness === defaultHarnessForOverride(matchingOverride, existingHarness) ? nextLaunchArgs : matchingOverride.launchArgs,
15299
+ harnessProfiles: {
15300
+ ...matchingOverride.harnessProfiles ?? {},
15301
+ [existingHarness]: {
15302
+ ...overrideHarnessProfile(matchingOverride, existingHarness),
15303
+ launchArgs: nextLaunchArgs
15304
+ }
15305
+ }
15306
+ };
15307
+ await writeRelayAgentOverrides(overrides);
15308
+ }
14419
15309
  targetAgentId = matchingAgentId;
14420
15310
  }
14421
15311
  await ensureLocalAgentBindingOnline(targetAgentId, process.env.OPENSCOUT_NODE_ID ?? "local", {
@@ -14511,6 +15401,7 @@ function readPersistedClaudeSessionId(agentName) {
14511
15401
  }
14512
15402
  function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
14513
15403
  const normalizedRecord = normalizeLocalAgentRecord(agentId, record);
15404
+ const configuredModel = readLaunchModelForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs);
14514
15405
  const definitionId = normalizedRecord.definitionId ?? agentId;
14515
15406
  const displayName = titleCaseLocalAgentName(definitionId);
14516
15407
  const projectRoot = normalizedRecord.projectRoot ?? normalizedRecord.cwd;
@@ -14535,7 +15426,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
14535
15426
  defaultSelector: instance.defaultSelector,
14536
15427
  nodeQualifier: instance.nodeQualifier,
14537
15428
  workspaceQualifier: instance.workspaceQualifier,
14538
- branch: instance.branch
15429
+ branch: instance.branch,
15430
+ ...configuredModel ? { model: configuredModel } : {}
14539
15431
  }
14540
15432
  },
14541
15433
  agent: {
@@ -14562,7 +15454,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
14562
15454
  defaultSelector: instance.defaultSelector,
14563
15455
  nodeQualifier: instance.nodeQualifier,
14564
15456
  workspaceQualifier: instance.workspaceQualifier,
14565
- branch: instance.branch
15457
+ branch: instance.branch,
15458
+ ...configuredModel ? { model: configuredModel } : {}
14566
15459
  },
14567
15460
  agentClass: "general",
14568
15461
  capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
@@ -14595,6 +15488,7 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
14595
15488
  nodeQualifier: instance.nodeQualifier,
14596
15489
  workspaceQualifier: instance.workspaceQualifier,
14597
15490
  branch: instance.branch,
15491
+ ...configuredModel ? { model: configuredModel } : {},
14598
15492
  ...externalSessionId ? { externalSessionId } : {}
14599
15493
  }
14600
15494
  }
@@ -15971,6 +16865,20 @@ function readSyncStatus(bridge, sessionId) {
15971
16865
  oldestBufferedSeq: bridge.oldestBufferedSeq(sessionId)
15972
16866
  };
15973
16867
  }
16868
+ function resolveSyncSessionId(bridge, preferredSessionId) {
16869
+ if (preferredSessionId) {
16870
+ return preferredSessionId;
16871
+ }
16872
+ let latestSessionId = null;
16873
+ let latestActivityAt = Number.NEGATIVE_INFINITY;
16874
+ for (const session of bridge.getSessionSummaries()) {
16875
+ if (session.lastActivityAt > latestActivityAt) {
16876
+ latestActivityAt = session.lastActivityAt;
16877
+ latestSessionId = session.sessionId;
16878
+ }
16879
+ }
16880
+ return latestSessionId;
16881
+ }
15974
16882
  function sessionRegistryErrorResponse(reqId, error) {
15975
16883
  if (!isSessionRegistryError(error)) {
15976
16884
  return null;
@@ -16040,23 +16948,36 @@ async function handleRPCInner(bridge, req, deviceId) {
16040
16948
  return { id: req.id, result: { ok: true } };
16041
16949
  }
16042
16950
  case "sync/replay": {
16043
- const p = req.params;
16044
- if (!p.sessionId) {
16045
- return { id: req.id, error: { code: -32602, message: "sessionId is required" } };
16951
+ const p = req.params ?? {};
16952
+ if (typeof p.lastSeq !== "number") {
16953
+ return { id: req.id, error: { code: -32602, message: "lastSeq is required" } };
16954
+ }
16955
+ const sessionId = resolveSyncSessionId(bridge, p.sessionId);
16956
+ if (!sessionId) {
16957
+ return { id: req.id, result: { events: [] } };
16046
16958
  }
16047
- const events2 = replaySyncEvents(bridge, p.sessionId, p.lastSeq);
16959
+ const events2 = replaySyncEvents(bridge, sessionId, p.lastSeq);
16048
16960
  return { id: req.id, result: { events: events2 } };
16049
16961
  }
16050
16962
  case "sync/status": {
16051
16963
  const p = req.params ?? {};
16052
- if (!p.sessionId) {
16053
- return { id: req.id, error: { code: -32602, message: "sessionId is required" } };
16964
+ const sessionCount = bridge.listSessions().length;
16965
+ const sessionId = resolveSyncSessionId(bridge, p.sessionId);
16966
+ if (!sessionId) {
16967
+ return {
16968
+ id: req.id,
16969
+ result: {
16970
+ currentSeq: 0,
16971
+ oldestBufferedSeq: 0,
16972
+ sessionCount
16973
+ }
16974
+ };
16054
16975
  }
16055
16976
  return {
16056
16977
  id: req.id,
16057
16978
  result: {
16058
- ...readSyncStatus(bridge, p.sessionId),
16059
- sessionCount: bridge.listSessions().length
16979
+ ...readSyncStatus(bridge, sessionId),
16980
+ sessionCount
16060
16981
  }
16061
16982
  };
16062
16983
  }
@@ -34044,6 +34965,64 @@ var mobileRouter = t.router({
34044
34965
  limit: typeof input.limit === "number" ? input.limit : null
34045
34966
  }, resolveMobileCurrentDirectory2());
34046
34967
  }),
34968
+ webHandoff: procedure.input(exports_external.object({
34969
+ kind: exports_external.enum(["session", "file_change"]),
34970
+ sessionId: exports_external.string(),
34971
+ turnId: exports_external.string().optional(),
34972
+ blockId: exports_external.string().optional()
34973
+ })).mutation(({ input, ctx }) => {
34974
+ if (!ctx.deviceId) {
34975
+ throw new TRPCError({
34976
+ code: "UNAUTHORIZED",
34977
+ message: "Secure web handoff requires a paired mobile device"
34978
+ });
34979
+ }
34980
+ const snapshot = ctx.bridge.getSessionSnapshot(input.sessionId);
34981
+ if (!snapshot) {
34982
+ throw new TRPCError({
34983
+ code: "NOT_FOUND",
34984
+ message: `No session: ${input.sessionId}`
34985
+ });
34986
+ }
34987
+ let scope;
34988
+ let title = snapshot.session.name || snapshot.session.id;
34989
+ if (input.kind === "file_change") {
34990
+ if (!input.turnId || !input.blockId) {
34991
+ throw new TRPCError({
34992
+ code: "BAD_REQUEST",
34993
+ message: "turnId and blockId are required for file_change handoffs"
34994
+ });
34995
+ }
34996
+ const turn = snapshot.turns.find((candidate) => candidate.id === input.turnId);
34997
+ const block = turn?.blocks.find((candidate) => candidate.block.id === input.blockId)?.block;
34998
+ if (!turn || !block || block.type !== "action" || block.action.kind !== "file_change") {
34999
+ throw new TRPCError({
35000
+ code: "NOT_FOUND",
35001
+ message: "File change block not found"
35002
+ });
35003
+ }
35004
+ scope = {
35005
+ kind: "file_change",
35006
+ sessionId: input.sessionId,
35007
+ turnId: input.turnId,
35008
+ blockId: input.blockId
35009
+ };
35010
+ title = block.action.path || title;
35011
+ } else {
35012
+ scope = {
35013
+ kind: "session",
35014
+ sessionId: input.sessionId
35015
+ };
35016
+ }
35017
+ const issued = issueWebHandoff(scope, ctx.deviceId);
35018
+ return {
35019
+ kind: input.kind,
35020
+ path: pathForWebHandoffScope(scope),
35021
+ token: issued.token,
35022
+ expiresAt: issued.expiresAt,
35023
+ title
35024
+ };
35025
+ }),
34047
35026
  createSession: procedure.input(exports_external.object({
34048
35027
  workspaceId: exports_external.string(),
34049
35028
  harness: exports_external.string().optional(),
@@ -34257,15 +35236,42 @@ function readSyncStatus2(bridge, sessionId) {
34257
35236
  oldestBufferedSeq: bridge.oldestBufferedSeq(sessionId)
34258
35237
  };
34259
35238
  }
35239
+ function resolveSyncSessionId2(bridge, preferredSessionId) {
35240
+ if (preferredSessionId) {
35241
+ return preferredSessionId;
35242
+ }
35243
+ let latestSessionId = null;
35244
+ let latestActivityAt = Number.NEGATIVE_INFINITY;
35245
+ for (const session of bridge.getSessionSummaries()) {
35246
+ if (session.lastActivityAt > latestActivityAt) {
35247
+ latestActivityAt = session.lastActivityAt;
35248
+ latestSessionId = session.sessionId;
35249
+ }
35250
+ }
35251
+ return latestSessionId;
35252
+ }
34260
35253
  var syncRouter = t.router({
34261
- replay: procedure.input(exports_external.object({ lastSeq: exports_external.number(), sessionId: exports_external.string() })).query(({ input, ctx }) => {
34262
- const events2 = replaySyncEvents2(ctx.bridge, input.sessionId, input.lastSeq);
35254
+ replay: procedure.input(exports_external.object({ lastSeq: exports_external.number(), sessionId: exports_external.string().optional() })).query(({ input, ctx }) => {
35255
+ const sessionId = resolveSyncSessionId2(ctx.bridge, input.sessionId);
35256
+ if (!sessionId) {
35257
+ return { events: [] };
35258
+ }
35259
+ const events2 = replaySyncEvents2(ctx.bridge, sessionId, input.lastSeq);
34263
35260
  return { events: events2 };
34264
35261
  }),
34265
- status: procedure.input(exports_external.object({ sessionId: exports_external.string() })).query(({ input, ctx }) => {
35262
+ status: procedure.input(exports_external.object({ sessionId: exports_external.string().optional() }).optional()).query(({ input, ctx }) => {
35263
+ const sessionCount = ctx.bridge.listSessions().length;
35264
+ const sessionId = resolveSyncSessionId2(ctx.bridge, input?.sessionId);
35265
+ if (!sessionId) {
35266
+ return {
35267
+ currentSeq: 0,
35268
+ oldestBufferedSeq: 0,
35269
+ sessionCount
35270
+ };
35271
+ }
34266
35272
  return {
34267
- ...readSyncStatus2(ctx.bridge, input.sessionId),
34268
- sessionCount: ctx.bridge.listSessions().length
35273
+ ...readSyncStatus2(ctx.bridge, sessionId),
35274
+ sessionCount
34269
35275
  };
34270
35276
  })
34271
35277
  });
@@ -38767,7 +39773,7 @@ async function startPairingRuntime(options) {
38767
39773
  secure: config2.secure,
38768
39774
  identity: config2.secure ? identity2 : undefined
38769
39775
  });
38770
- const fileServer = startFileServer({ port: config2.port + 2 });
39776
+ const fileServer = startFileServer({ port: config2.port + 2, bridge });
38771
39777
  const relayUrl = options?.relayUrl?.trim() || config2.relay || null;
38772
39778
  if (!relayUrl) {
38773
39779
  fileServer.stop();