mindwire 0.1.14 → 0.1.18

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.d.ts CHANGED
@@ -32,7 +32,9 @@ export { catalogProviders, catalogProvider, catalogModels, lookupModel, loadCata
32
32
  export type { CatalogOptions } from "./catalog/index.js";
33
33
  export { MindwireError, ApiError, RunFailedError, TimeoutError } from "./errors.js";
34
34
  export * from "./types.js";
35
- export { WorkspaceApi, WorkspaceCollection, ProjectOperationsApi } from "./workspace.js";
36
- export type { ProjectRequest, ProjectAuth, ProjectOperation, ProjectRemoveRequest } from "./workspace.js";
35
+ export { ServiceApi } from "./service.js";
36
+ export type { ServiceUpdateState, ServiceUpdateLease } from "./service.js";
37
+ export { WorkspaceApi, WorkspaceCollection, ProjectOperationsApi, GitAccessApi } from "./workspace.js";
38
+ export type { ProjectRequest, ProjectAuth, ProjectOperation, ProjectRemoveRequest, GitConnection, GitAccessState, ProjectGitState, GitAction, GitOperationRequest, GitOperation } from "./workspace.js";
37
39
  export type { WorkspaceRecord, WorkspaceAgent, WorkspaceProject, WorkspaceChat, WorkspaceKind, WorkspaceInput, WorkspaceImport, WorkspaceSnapshot } from "./workspace.js";
38
40
  export * from "./surfaces.js";
package/dist/index.js CHANGED
@@ -380,8 +380,9 @@ var Run = class _Run {
380
380
  await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/cancel`);
381
381
  }
382
382
  /**
383
- * Answer a mid-turn interaction the turn is waiting on — a permission approval, or an
384
- * AskUserQuestion / ExitPlanMode reply. Requires the agent's `respond` capability.
383
+ * Answer a pending permission, question or plan. Message-mode questions remain answerable
384
+ * after completion; the daemon steers or resumes the conversation. Read the chat's latest
385
+ * run after replying to a completed run. Requires the agent's `respond` capability.
385
386
  */
386
387
  async respond(input = {}) {
387
388
  await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/respond`, { body: input });
@@ -545,12 +546,14 @@ var WorkspaceCollection = class {
545
546
  var WorkspaceApi = class {
546
547
  constructor(mw) {
547
548
  this.mw = mw;
549
+ this.git = new GitAccessApi(mw);
548
550
  this.operations = new ProjectOperationsApi(mw);
549
551
  this.agents = new WorkspaceCollection(mw, "agents");
550
552
  this.projects = new WorkspaceCollection(mw, "projects");
551
553
  this.chats = new WorkspaceCollection(mw, "chats");
552
554
  }
553
555
  mw;
556
+ git;
554
557
  operations;
555
558
  agents;
556
559
  projects;
@@ -579,6 +582,75 @@ var WorkspaceApi = class {
579
582
  return this.mw.http.request("POST", "/workspace/import", { body: records });
580
583
  }
581
584
  };
585
+ var GitAccessApi = class {
586
+ constructor(mw) {
587
+ this.mw = mw;
588
+ }
589
+ mw;
590
+ state() {
591
+ return this.mw.http.request("GET", "/workspace/git");
592
+ }
593
+ setDefault(connection, auth) {
594
+ return this.mw.http.request("PUT", "/workspace/git", { body: { connection, auth } });
595
+ }
596
+ forget(connectionId) {
597
+ return this.mw.http.request("DELETE", `/workspace/git/connections/${encodeURIComponent(connectionId)}`);
598
+ }
599
+ project(projectId) {
600
+ return this.mw.http.request("GET", `/workspace/projects/${encodeURIComponent(projectId)}/git`);
601
+ }
602
+ setProject(projectId, connection, expectedRevision, auth) {
603
+ return this.mw.http.request("PUT", `/workspace/projects/${encodeURIComponent(projectId)}/git`, {
604
+ body: { connection, expectedRevision, auth }
605
+ });
606
+ }
607
+ /** Compatibility call. Prefer start() and operation()/watch() for reconnectable writes. */
608
+ run(projectId, operation, auth) {
609
+ return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(projectId)}/git/${operation}`, { body: { auth } });
610
+ }
611
+ /** Requires health.gitOperationsVersion >= 1. Acceptance persists before Git runs;
612
+ * disconnecting only detaches the client. The same ID/intent never runs twice.
613
+ */
614
+ start(projectId, request) {
615
+ return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(projectId)}/git/operations`, { body: request });
616
+ }
617
+ async operations(projectId, activeOnly = false) {
618
+ const result = await this.mw.http.request(
619
+ "GET",
620
+ `/workspace/projects/${encodeURIComponent(projectId)}/git/operations`,
621
+ { query: { active: activeOnly } }
622
+ );
623
+ return result.operations;
624
+ }
625
+ operation(id) {
626
+ return this.mw.http.request("GET", `/workspace/git/operations/${encodeURIComponent(id)}`);
627
+ }
628
+ cancel(id) {
629
+ return this.mw.http.request("POST", `/workspace/git/operations/${encodeURIComponent(id)}/cancel`);
630
+ }
631
+ /** Current snapshot followed by state changes. Aborting observation never cancels the operation. */
632
+ async *watch(id, opts = {}) {
633
+ const controller = new AbortController();
634
+ const abort = () => controller.abort();
635
+ opts.signal?.addEventListener("abort", abort, { once: true });
636
+ if (opts.signal?.aborted) controller.abort();
637
+ try {
638
+ const response = await this.mw.http.open("GET", `/workspace/git/operations/${encodeURIComponent(id)}/stream`, {
639
+ signal: controller.signal
640
+ });
641
+ let sequence = -1;
642
+ for await (const operation of readSSE(response.body, controller.signal)) {
643
+ if (operation.sequence > sequence) {
644
+ sequence = operation.sequence;
645
+ yield operation;
646
+ }
647
+ }
648
+ } finally {
649
+ controller.abort();
650
+ opts.signal?.removeEventListener("abort", abort);
651
+ }
652
+ }
653
+ };
582
654
 
583
655
  // src/surfaces.ts
584
656
  var SurfacesApi = class {
@@ -640,8 +712,27 @@ var SurfacesApi = class {
640
712
  }
641
713
  };
642
714
 
715
+ // src/service.ts
716
+ var ServiceApi = class {
717
+ constructor(client) {
718
+ this.client = client;
719
+ }
720
+ client;
721
+ updateStatus() {
722
+ return this.client.http.request("GET", "/service/update");
723
+ }
724
+ /** Download first. Acquire immediately before replacement; busy work returns 409.
725
+ * Admission stays closed until release, process exit, or the one-minute expiry. */
726
+ acquireUpdate() {
727
+ return this.client.http.request("POST", "/service/update");
728
+ }
729
+ releaseUpdate(id) {
730
+ return this.client.http.request("DELETE", `/service/update/${encodeURIComponent(id)}`);
731
+ }
732
+ };
733
+
643
734
  // src/version.ts
644
- var SDK_VERSION = "0.1.14" ;
735
+ var SDK_VERSION = "0.1.18" ;
645
736
 
646
737
  // src/daemon-binary.ts
647
738
  function supported(platform, arch) {
@@ -902,6 +993,7 @@ var Mindwire = class _Mindwire {
902
993
  /** Workspace registry: saved agent profiles, projects and chat relationships. */
903
994
  workspace;
904
995
  surfaces;
996
+ service;
905
997
  http;
906
998
  /** The default agent type applied to agent-scoped calls, if set. */
907
999
  defaultAgent;
@@ -937,6 +1029,7 @@ var Mindwire = class _Mindwire {
937
1029
  this.defaultAgent = opts.agent;
938
1030
  this.workspace = new WorkspaceApi(this);
939
1031
  this.surfaces = new SurfacesApi(this);
1032
+ this.service = new ServiceApi(this);
940
1033
  this.auth = new AuthApi(this);
941
1034
  this.prompts = new PromptsApi(this);
942
1035
  this.mcp = new McpApi(this);
@@ -959,6 +1052,7 @@ var Mindwire = class _Mindwire {
959
1052
  clone.defaultAgent = agent;
960
1053
  clone.workspace = new WorkspaceApi(clone);
961
1054
  clone.surfaces = new SurfacesApi(clone);
1055
+ clone.service = new ServiceApi(clone);
962
1056
  clone.auth = new AuthApi(clone);
963
1057
  clone.prompts = new PromptsApi(clone);
964
1058
  clone.mcp = new McpApi(clone);
@@ -1007,6 +1101,12 @@ var Mindwire = class _Mindwire {
1007
1101
  agent(scoped) {
1008
1102
  return this.http.request("GET", "/agent", { query: this.agentParam(scoped) });
1009
1103
  }
1104
+ /** `GET /agent/software` — installed version, compatibility and approved update target. */
1105
+ software(opts = {}) {
1106
+ return this.http.request("GET", "/agent/software", {
1107
+ query: { ...this.agentParam(opts), ...opts.refresh ? { refresh: "true" } : {} }
1108
+ });
1109
+ }
1010
1110
  /**
1011
1111
  * `GET /models` — the models the selected agent can run for the configured account. An empty array
1012
1112
  * is valid (no credentials yet / offline). Throws a 400 {@link ApiError} for an agent whose model is
@@ -1024,7 +1124,7 @@ var Mindwire = class _Mindwire {
1024
1124
  setup(scoped) {
1025
1125
  return this.http.request("POST", "/setup", { query: this.agentParam(scoped) });
1026
1126
  }
1027
- /** `POST /update` — re-run the toolchain, forcing reinstall of installable steps. */
1127
+ /** `POST /update` — install the catalog's newest tested version compatible with this daemon. */
1028
1128
  update(scoped) {
1029
1129
  return this.http.request("POST", "/update", { query: this.agentParam(scoped) });
1030
1130
  }
@@ -1121,6 +1221,7 @@ var Mindwire = class _Mindwire {
1121
1221
  if (input.options !== void 0) body.options = input.options;
1122
1222
  if (input.mode !== void 0) body.mode = input.mode;
1123
1223
  if (input.resolve !== void 0) body.resolve = input.resolve;
1224
+ if (input.gitAuth !== void 0) body.gitAuth = input.gitAuth;
1124
1225
  const data = await this.http.request("POST", "/turns", {
1125
1226
  query: this.agentParam(input),
1126
1227
  body
@@ -1224,12 +1325,26 @@ var AuthApi = class {
1224
1325
  body: input
1225
1326
  });
1226
1327
  }
1328
+ /** Read one interactive attempt, scoped by the returned AuthState.flowId. */
1329
+ poll(flowId, scoped) {
1330
+ return this.step({ _flowId: flowId }, scoped);
1331
+ }
1332
+ /** Cancel this native login without affecting a newer sign-in attempt. */
1333
+ cancel(flowId, scoped) {
1334
+ return this.step({ _flowId: flowId, _action: "cancel" }, scoped);
1335
+ }
1227
1336
  /** `GET /auth/status` — is the agent authenticated, and via which method. */
1228
1337
  status(scoped) {
1229
1338
  return this.mw.http.request("GET", "/auth/status", {
1230
1339
  query: this.mw.agentParam(scoped)
1231
1340
  });
1232
1341
  }
1342
+ /** `POST /auth/logout` — disconnect this harness once its running chats finish. */
1343
+ logout(scoped) {
1344
+ return this.mw.http.request("POST", "/auth/logout", {
1345
+ query: this.mw.agentParam(scoped)
1346
+ });
1347
+ }
1233
1348
  };
1234
1349
  var PromptsApi = class {
1235
1350
  constructor(mw) {
@@ -1471,6 +1586,23 @@ var NotifyApi = class {
1471
1586
  };
1472
1587
 
1473
1588
  // src/target/host.ts
1589
+ function versionAtLeast(actual, desired) {
1590
+ if (actual === desired) return true;
1591
+ if (!actual || !/^\d+\.\d+\.\d+$/.test(actual) || !/^\d+\.\d+\.\d+$/.test(desired)) return false;
1592
+ const a = actual.split(".").map(Number), b = desired.split(".").map(Number);
1593
+ for (let i = 0; i < 3; i++) {
1594
+ if (a[i] !== b[i]) return a[i] > b[i];
1595
+ }
1596
+ return true;
1597
+ }
1598
+ var versionCheckScript = String.raw`version_at_least() {
1599
+ [ "$1" != "$2" ] || return 0
1600
+ [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
1601
+ local a b c x y z
1602
+ IFS=. read -r a b c <<< "$1"
1603
+ IFS=. read -r x y z <<< "$2"
1604
+ (( 10#$a > 10#$x || (10#$a == 10#$x && 10#$b > 10#$y) || (10#$a == 10#$x && 10#$b == 10#$y && 10#$c >= 10#$z) ))
1605
+ }`;
1474
1606
  function makeEmit(cfg) {
1475
1607
  const target = cfg.target ?? "sandbox";
1476
1608
  return (e) => {
@@ -1507,19 +1639,24 @@ async function ensureDaemon(host, cfg) {
1507
1639
  message: health.version ? `daemon reachable (v${health.version})` : "daemon reachable (version unknown)",
1508
1640
  version: health.version
1509
1641
  });
1510
- const upToDate = health.version !== void 0 && health.version === desired;
1642
+ const upToDate = versionAtLeast(health.version, desired);
1511
1643
  if (!cfg.forceDeploy && (upToDate || !cfg.autoUpdate)) {
1512
1644
  emit2({
1513
1645
  phase: "skip",
1514
- message: upToDate ? `daemon already at v${desired}` : "keeping the running daemon",
1646
+ message: upToDate ? `daemon already at v${health.version}` : "keeping the running daemon",
1515
1647
  version: health.version
1516
1648
  });
1517
1649
  return token;
1518
1650
  }
1651
+ if ((health.serviceUpdateVersion ?? 0) < 1) {
1652
+ if (cfg.forceDeploy) throw new MindwireError("This legacy service cannot reserve an idle update. Upgrade it explicitly or stop it after its work finishes before deploying.");
1653
+ emit2({ phase: "skip", message: "automatic update deferred: upgrade this legacy service explicitly to enable idle updates", version: health.version });
1654
+ return token;
1655
+ }
1519
1656
  } else {
1520
1657
  emit2({ phase: "probe", message: "no daemon reachable; deploying" });
1521
1658
  }
1522
- await deploy(host, cfg, emit2, token, directory);
1659
+ await deploy(host, cfg, emit2, token, directory, health.reachable);
1523
1660
  if (!cfg.token) token = await readWorkspaceToken(host, directory) ?? token;
1524
1661
  return token;
1525
1662
  } catch (err) {
@@ -1532,7 +1669,7 @@ async function readWorkspaceToken(host, directory) {
1532
1669
  const candidate = saved.stdout?.trim();
1533
1670
  return candidate && candidate.length <= 4096 && /^[\x21-\x7e]+$/.test(candidate) ? candidate : void 0;
1534
1671
  }
1535
- async function deploy(host, cfg, emit2, token, directory) {
1672
+ async function deploy(host, cfg, emit2, token, directory, requireLease) {
1536
1673
  const newPath = directory + "/mindwired.new";
1537
1674
  const BIN = shellQuote(directory + "/mindwired"), BIN_NEW = shellQuote(newPath);
1538
1675
  const STATE = shellQuote(directory + "/agent-state.json"), LOG = shellQuote(directory + "/daemon.log");
@@ -1574,6 +1711,7 @@ async function deploy(host, cfg, emit2, token, directory) {
1574
1711
  ` latest=$(curl -fsSL https://api.github.com/repos/oblien/mindwire/releases/latest | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')`,
1575
1712
  ' case "$latest" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "MINDWIRE_FAIL no matching or latest release"; exit 1 ;; esac',
1576
1713
  ' release="https://github.com/oblien/mindwire/releases/download/$latest"',
1714
+ ' mw_expected_version="${latest#v}"',
1577
1715
  ` asset="mindwired-$latest-${platform}-${arch}"`,
1578
1716
  ' download || { echo "MINDWIRE_FAIL latest release download failed"; exit 1; }',
1579
1717
  "fi",
@@ -1593,43 +1731,75 @@ async function deploy(host, cfg, emit2, token, directory) {
1593
1731
  'else echo "MINDWIRE_FAIL No supported update lock is available (flock on Linux, lockf on macOS)."; exit 1; fi',
1594
1732
  `mw_token=${shellQuote(token)}`,
1595
1733
  !cfg.token ? `if [ -s ${TOKEN} ]; then mw_token=$(cat ${TOKEN}); fi` : "",
1734
+ 'mw_lease=""',
1735
+ `mw_lease_file=${shellQuote(directory)}/update-lease-$$.json`,
1736
+ "cleanup_update() {",
1737
+ ` if [ -n "$mw_lease" ]; then curl -s --connect-timeout 2 --max-time 3 -X DELETE -H "Authorization: Bearer $mw_token" "http://127.0.0.1:${cfg.port}/service/update/$mw_lease" >/dev/null 2>&1 || true; fi`,
1738
+ ` rm -f ${BIN_NEW} "$mw_lease_file"${stagedUpload ? " " + shellQuote(stagedUpload) : ""}`,
1739
+ "}",
1740
+ // Shared staging is only removed while this process owns the workspace lock.
1741
+ "trap cleanup_update EXIT",
1742
+ versionCheckScript,
1743
+ `mw_desired=${shellQuote(desired)}`,
1744
+ 'mw_expected_version="$mw_desired"',
1745
+ `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1596
1746
  !cfg.forceDeploy ? [
1597
- `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1598
1747
  `mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
1599
- `if [ -n "$mw_health" ] && ${cfg.autoUpdate ? `[ "$mw_version" = ${shellQuote(desired)} ]` : "true"}; then echo MINDWIRE_READY; exit 0; fi`
1748
+ `if [ -n "$mw_health" ] && ${cfg.autoUpdate ? 'version_at_least "$mw_version" "$mw_desired"' : "true"}; then echo MINDWIRE_READY; exit 0; fi`
1600
1749
  ].join("\n") : "",
1601
1750
  // macOS ships POSIX setsid in Perl, but does not include Linux's setsid executable.
1602
1751
  "if command -v setsid >/dev/null 2>&1; then mw_detach=(setsid);",
1603
1752
  `elif command -v perl >/dev/null 2>&1; then mw_detach=(perl -MPOSIX -e 'defined(my $sid = POSIX::setsid()) && $sid >= 0 or die "setsid: $!"; exec @ARGV; die "exec: $!";');`,
1604
1753
  'else echo "MINDWIRE_FAIL Cannot start the Mindwire service: setsid or Perl is required."; exit 1; fi',
1605
1754
  acquire,
1606
- // Stop a prior daemon by exact process NAME, never `pkill -f <path>`: this whole script (which
1607
- // contains `${BIN}` several times) is the argv of the `bash -lc` shell running it, so a full-cmdline
1608
- // match would SIGTERM our own deploying shell before the daemon ever launches. `-x mindwired` matches
1609
- // only the daemon's comm (`bash`/`pkill` never match), leaving this shell alive.
1610
- "if command -v pkill >/dev/null 2>&1; then",
1611
- " pkill -x mindwired 2>/dev/null || true",
1612
- "else",
1613
- " for mw_proc in /proc/[0-9]*/comm; do",
1614
- ' IFS= read -r mw_name 2>/dev/null < "$mw_proc" || continue',
1615
- ' [ "$mw_name" = mindwired ] || continue',
1616
- " mw_pid=${mw_proc#/proc/}; mw_pid=${mw_pid%/comm}",
1617
- ' kill "$mw_pid" 2>/dev/null || true',
1618
- " done",
1755
+ // The service decides idleness after transfer. Never stop arbitrary processes
1756
+ // or interrupt a chat that began while the SDK was downloading its binary.
1757
+ `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1758
+ 'if [ -n "$mw_health" ]; then',
1759
+ ` mw_status=$(curl -s --connect-timeout 2 --max-time 5 -X POST -H "Authorization: Bearer $mw_token" -o "$mw_lease_file" -w '%{http_code}' http://127.0.0.1:${cfg.port}/service/update) || mw_status=000`,
1760
+ ' case "$mw_status" in',
1761
+ " 201)",
1762
+ ` mw_lease=$(sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\\([[:alnum:]]*\\)".*/\\1/p' "$mw_lease_file")`,
1763
+ ` mw_service_pid=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p' "$mw_lease_file")`,
1764
+ ' [ -n "$mw_lease" ] && [[ "$mw_service_pid" =~ ^[0-9]+$ ]] && [ "$mw_service_pid" -gt 1 ] || { echo "MINDWIRE_FAIL invalid service update lease"; exit 1; }',
1765
+ ' kill "$mw_service_pid" ;;',
1766
+ " 409) echo MINDWIRE_UPDATE_DEFERRED; exit 0 ;;",
1767
+ ' 404) echo "MINDWIRE_FAIL this legacy service requires an explicit upgrade before idle updates"; exit 1 ;;',
1768
+ ' *) echo "MINDWIRE_FAIL could not reserve an idle service update"; exit 1 ;;',
1769
+ " esac",
1770
+ requireLease ? 'else echo "MINDWIRE_FAIL the service went offline before its idle update could be reserved"; exit 1' : "",
1619
1771
  "fi",
1620
1772
  "sleep 0.3",
1621
1773
  `mv -f ${BIN_NEW} ${BIN}`,
1622
1774
  `chmod +x ${BIN}`,
1623
1775
  // Detach so the daemon survives this exec's shell exiting. ADDR=":<port>" binds 0.0.0.0.
1624
- `"\${mw_detach[@]}" nohup env ADDR=":${cfg.port}" AGENT_TYPE=${shellQuote(cfg.agent)} AGENT_CWD=${shellQuote(cfg.agentCwd)} STATE_PATH=${STATE} DAEMON_TOKEN="$mw_token" ${BIN} > ${LOG} 2>&1 < /dev/null 9>&- &`,
1776
+ // Ignore HUP directly: macOS nohup can fail after setsid in a headless workspace.
1777
+ `"\${mw_detach[@]}" /bin/sh -c 'trap "" HUP; exec "$@"' mindwire-service env ADDR=":${cfg.port}" AGENT_TYPE=${shellQuote(cfg.agent)} AGENT_CWD=${shellQuote(cfg.agentCwd)} STATE_PATH=${STATE} DAEMON_TOKEN="$mw_token" ${BIN} > ${LOG} 2>&1 < /dev/null 9>&- &`,
1625
1778
  // Health-poll from inside the VM (loopback) and emit a marker — exit codes are unreliable here.
1626
- `for ((mw_attempt=0; mw_attempt<60; mw_attempt++)); do curl -fsS --max-time 2 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz >/dev/null 2>&1 && { echo MINDWIRE_READY; exit 0; }; sleep 0.25; done`,
1627
- "echo MINDWIRE_FAIL",
1628
- `tail -n 40 ${LOG} 2>/dev/null || true`
1779
+ 'mw_pid=$!; mw_exit=""; mw_deadline=$((SECONDS + 30))',
1780
+ "while (( SECONDS < mw_deadline )); do",
1781
+ ` mw_health=$(curl -fsS --connect-timeout 2 --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1782
+ ` mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
1783
+ ' if version_at_least "$mw_version" "$mw_expected_version"; then echo MINDWIRE_READY; exit 0; fi',
1784
+ ' if [ -z "$mw_exit" ] && ! kill -0 "$mw_pid" 2>/dev/null; then',
1785
+ ' mw_exit=0; wait "$mw_pid" || mw_exit=$?; [ "$mw_exit" = 0 ] || break',
1786
+ " fi",
1787
+ " sleep 0.25",
1788
+ "done",
1789
+ `mw_tail=$(tail -n 40 ${LOG} 2>/dev/null || true)`,
1790
+ 'if [ -n "$mw_exit" ] && [ "$mw_exit" != 0 ]; then mw_reason="Mindwire exited during startup (status $mw_exit).";',
1791
+ 'else mw_reason="The Mindwire service did not become ready at v$mw_expected_version (reported: $mw_version)."; fi',
1792
+ '[ -z "$mw_tail" ] || mw_reason="$mw_reason $mw_tail"',
1793
+ 'printf "MINDWIRE_FAIL %s\\n" "$mw_reason"'
1629
1794
  ].join("\n");
1630
1795
  emit2({ phase: "launch", message: "launching daemon" });
1631
1796
  const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 420 });
1632
1797
  const out = res.stdout ?? "";
1798
+ if (out.includes("MINDWIRE_UPDATE_DEFERRED")) {
1799
+ if (cfg.forceDeploy) throw new MindwireError("The workspace is busy. Wait for its operations to finish before deploying the service.");
1800
+ emit2({ phase: "skip", message: "service update deferred while workspace operations are running" });
1801
+ return;
1802
+ }
1633
1803
  if (!out.includes("MINDWIRE_READY")) {
1634
1804
  throw new MindwireError(
1635
1805
  "mindwire: the in-sandbox daemon did not become healthy after deploy.\n" + (out || res.stderr || res.error || "(no output)").trim()
@@ -1644,7 +1814,11 @@ async function probeHealth(host, port, token) {
1644
1814
  if (!body) return { reachable: false };
1645
1815
  try {
1646
1816
  const j = JSON.parse(body);
1647
- return { reachable: true, version: typeof j.version === "string" ? j.version : void 0 };
1817
+ return {
1818
+ reachable: true,
1819
+ version: typeof j.version === "string" ? j.version : void 0,
1820
+ serviceUpdateVersion: typeof j.serviceUpdateVersion === "number" ? j.serviceUpdateVersion : void 0
1821
+ };
1648
1822
  } catch {
1649
1823
  return { reachable: true };
1650
1824
  }
@@ -1745,7 +1919,7 @@ var ContainerHost = class {
1745
1919
  base;
1746
1920
  containerId;
1747
1921
  exec(argv, opts) {
1748
- return this.base.exec(["docker", "exec", this.containerId, ...argv], opts);
1922
+ return this.base.exec(["docker", "exec", "--env", "MINDWIRE_ISOLATION=container", this.containerId, ...argv], opts);
1749
1923
  }
1750
1924
  async putFile(path, data, opts) {
1751
1925
  const slash = path.lastIndexOf("/");
@@ -1845,7 +2019,7 @@ async function runContainer(host, cfg, emit2) {
1845
2019
  "mindwire: running the daemon in a container needs an image (docker.image) to create one, or an existing container (docker.container) to attach to."
1846
2020
  );
1847
2021
  }
1848
- const runArgs = ["docker", "run", "-d"];
2022
+ const runArgs = ["docker", "run", "-d", "--env", "MINDWIRE_ISOLATION=container"];
1849
2023
  if (cfg.name) runArgs.push("--name", cfg.name);
1850
2024
  runArgs.push("-p", `127.0.0.1:0:${port}`);
1851
2025
  if (cfg.createArgs?.length) runArgs.push(...cfg.createArgs);
@@ -2172,7 +2346,7 @@ var DockerHost = class {
2172
2346
  container;
2173
2347
  async exec(argv) {
2174
2348
  const { Writable } = await import('stream');
2175
- const exec = await this.container.exec({ Cmd: argv, AttachStdout: true, AttachStderr: true });
2349
+ const exec = await this.container.exec({ Cmd: argv, Env: ["MINDWIRE_ISOLATION=container"], AttachStdout: true, AttachStderr: true });
2176
2350
  const stream = await exec.start({ hijack: true, stdin: false });
2177
2351
  const out = [];
2178
2352
  const err = [];
@@ -2226,7 +2400,7 @@ async function provisionDocker(docker2, config = {}, onLog) {
2226
2400
  // The runtime image starts mindwired itself. Do not replace its command: doing so turns the
2227
2401
  // image ENTRYPOINT into `mindwired sleep infinity` and prevents the runtime from starting.
2228
2402
  Image: image,
2229
- Env: [`ADDR=:${port}`, `AGENT_TYPE=${agent}`, `AGENT_CWD=${agentCwd}`],
2403
+ Env: [`ADDR=:${port}`, `AGENT_TYPE=${agent}`, `AGENT_CWD=${agentCwd}`, "MINDWIRE_ISOLATION=container"],
2230
2404
  Tty: false,
2231
2405
  ExposedPorts: { [portKey]: {} },
2232
2406
  HostConfig: { PortBindings: { [portKey]: [{ HostPort: "0" }] } },
@@ -2541,6 +2715,6 @@ function clearCatalogCache() {
2541
2715
  inflight = null;
2542
2716
  }
2543
2717
 
2544
- export { ApiError, AuthApi, ContainerHost, Http, MODELS_DEV_URL, McpApi, Mindwire, MindwireError, NotifyApi, ProjectOperationsApi, PromptsApi, ProvidersApi, Run, RunFailedError, SDK_VERSION, SurfacesApi, TimeoutError, WorkspaceApi, WorkspaceCollection, catalogModels, catalogProvider, catalogProviders, clearCatalogCache, docker, ensureDaemon, ensureDaemonBinary, loadCatalog, local, lookupModel, oblien, provisionContainer, provisionDocker, provisionOblien, provisionSsh, provisionSshContainer, remote, resolveLinuxDaemon, ssh, startEmbedded };
2718
+ export { ApiError, AuthApi, ContainerHost, GitAccessApi, Http, MODELS_DEV_URL, McpApi, Mindwire, MindwireError, NotifyApi, ProjectOperationsApi, PromptsApi, ProvidersApi, Run, RunFailedError, SDK_VERSION, ServiceApi, SurfacesApi, TimeoutError, WorkspaceApi, WorkspaceCollection, catalogModels, catalogProvider, catalogProviders, clearCatalogCache, docker, ensureDaemon, ensureDaemonBinary, loadCatalog, local, lookupModel, oblien, provisionContainer, provisionDocker, provisionOblien, provisionSsh, provisionSshContainer, remote, resolveLinuxDaemon, ssh, startEmbedded };
2545
2719
  //# sourceMappingURL=index.js.map
2546
2720
  //# sourceMappingURL=index.js.map