mindwire 0.1.15 → 0.1.19

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/client.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { Http, type FetchLike } from "./http.js";
2
2
  import { Run } from "./run.js";
3
- import { WorkspaceApi } from "./workspace.js";
3
+ import { WorkspaceApi, type ProjectAuth } from "./workspace.js";
4
4
  import { SurfacesApi } from "./surfaces.js";
5
+ import { ServiceApi } from "./service.js";
5
6
  import { type Target } from "./target/index.js";
6
7
  import type { EnsureEvent } from "./target/host.js";
7
- import type { AgentInfo, AuthMethod, AuthState, AuthStatus, Catalog, ChatSummary, CustomProvider, DeleteResult, DoctorReport, Health, MCPServer, MemoryDoc, MemoryScope, Message, ModelInfo, Notification, NotifyChannel, NotifyChannelInput, NotifyChannelTestResult, NotifyConfigInput, NotifyConfigStatus, NotifyRule, NotifyRuleInput, ProcessFrame, PromptTemplate, ResolveOptions, SetupStatus, Stats, Subagent, TurnOptions } from "./types.js";
8
+ import type { AgentInfo, AuthMethod, AuthState, AuthStatus, Catalog, ChatSummary, CustomProvider, DeleteResult, DoctorReport, Health, MCPServer, MemoryDoc, MemoryScope, Message, ModelInfo, Notification, NotifyChannel, NotifyChannelInput, NotifyChannelTestResult, NotifyConfigInput, NotifyConfigStatus, NotifyRule, NotifyRuleInput, ProcessFrame, PromptTemplate, ResolveOptions, SetupStatus, HarnessSoftware, Stats, Subagent, TurnOptions } from "./types.js";
8
9
  export interface MindwireOptions {
9
10
  /**
10
11
  * Default agent type for agent-scoped calls (e.g. `"claude-code"`). When omitted, the daemon
@@ -72,6 +73,7 @@ export declare class Mindwire {
72
73
  /** Workspace registry: saved agent profiles, projects and chat relationships. */
73
74
  readonly workspace: WorkspaceApi;
74
75
  readonly surfaces: SurfacesApi;
76
+ readonly service: ServiceApi;
75
77
  readonly http: Http;
76
78
  /** The default agent type applied to agent-scoped calls, if set. */
77
79
  readonly defaultAgent: string | undefined;
@@ -120,6 +122,10 @@ export declare class Mindwire {
120
122
  catalog(): Promise<Catalog>;
121
123
  /** `GET /agent` — capabilities + settings schema + auth methods/status for the selected agent. */
122
124
  agent(scoped?: AgentScoped): Promise<AgentInfo>;
125
+ /** `GET /agent/software` — installed version, compatibility and approved update target. */
126
+ software(opts?: AgentScoped & {
127
+ refresh?: boolean;
128
+ }): Promise<HarnessSoftware>;
123
129
  /**
124
130
  * `GET /models` — the models the selected agent can run for the configured account. An empty array
125
131
  * is valid (no credentials yet / offline). Throws a 400 {@link ApiError} for an agent whose model is
@@ -130,7 +136,7 @@ export declare class Mindwire {
130
136
  doctor(scoped?: AgentScoped): Promise<DoctorReport>;
131
137
  /** `POST /setup` — start the agent's install toolchain (background; poll {@link setupStatus}). */
132
138
  setup(scoped?: AgentScoped): Promise<SetupStatus>;
133
- /** `POST /update` — re-run the toolchain, forcing reinstall of installable steps. */
139
+ /** `POST /update` — install the catalog's newest tested version compatible with this daemon. */
134
140
  update(scoped?: AgentScoped): Promise<SetupStatus>;
135
141
  /** `GET /setup` — current toolchain install progress. */
136
142
  setupStatus(scoped?: AgentScoped): Promise<SetupStatus>;
@@ -189,6 +195,7 @@ export declare class Mindwire {
189
195
  options?: TurnOptions;
190
196
  mode?: "turn" | "resolve";
191
197
  resolve?: ResolveOptions;
198
+ gitAuth?: ProjectAuth;
192
199
  } & AgentScoped): Promise<Run>;
193
200
  /**
194
201
  * `POST /turns {mode:"resolve"}` — start a **global-resolve** run: instead of returning after one
@@ -208,6 +215,7 @@ export declare class Mindwire {
208
215
  cwd?: string;
209
216
  options?: TurnOptions;
210
217
  resolve?: ResolveOptions;
218
+ gitAuth?: ProjectAuth;
211
219
  } & AgentScoped): Promise<Run>;
212
220
  /** `GET /runs/{id}` — fetch an existing run as a {@link Run} handle. */
213
221
  run(id: string): Promise<Run>;
@@ -253,8 +261,14 @@ export declare class AuthApi {
253
261
  begin(method: string, scoped?: AgentScoped): Promise<AuthState>;
254
262
  /** `POST /auth/step` — submit fields, or poll an interactive login to completion. */
255
263
  step(input: Record<string, string>, scoped?: AgentScoped): Promise<AuthState>;
264
+ /** Read one interactive attempt, scoped by the returned AuthState.flowId. */
265
+ poll(flowId: string, scoped?: AgentScoped): Promise<AuthState>;
266
+ /** Cancel this native login without affecting a newer sign-in attempt. */
267
+ cancel(flowId: string, scoped?: AgentScoped): Promise<AuthState>;
256
268
  /** `GET /auth/status` — is the agent authenticated, and via which method. */
257
269
  status(scoped?: AgentScoped): Promise<AuthStatus>;
270
+ /** `POST /auth/logout` — disconnect this harness once its running chats finish. */
271
+ logout(scoped?: AgentScoped): Promise<AuthStatus>;
258
272
  }
259
273
  /**
260
274
  * The persistent prompt/memory surface for a scoped agent — the three layers beneath a turn's
package/dist/index.cjs CHANGED
@@ -549,12 +549,14 @@ var WorkspaceCollection = class {
549
549
  var WorkspaceApi = class {
550
550
  constructor(mw) {
551
551
  this.mw = mw;
552
+ this.git = new GitAccessApi(mw);
552
553
  this.operations = new ProjectOperationsApi(mw);
553
554
  this.agents = new WorkspaceCollection(mw, "agents");
554
555
  this.projects = new WorkspaceCollection(mw, "projects");
555
556
  this.chats = new WorkspaceCollection(mw, "chats");
556
557
  }
557
558
  mw;
559
+ git;
558
560
  operations;
559
561
  agents;
560
562
  projects;
@@ -583,6 +585,75 @@ var WorkspaceApi = class {
583
585
  return this.mw.http.request("POST", "/workspace/import", { body: records });
584
586
  }
585
587
  };
588
+ var GitAccessApi = class {
589
+ constructor(mw) {
590
+ this.mw = mw;
591
+ }
592
+ mw;
593
+ state() {
594
+ return this.mw.http.request("GET", "/workspace/git");
595
+ }
596
+ setDefault(connection, auth) {
597
+ return this.mw.http.request("PUT", "/workspace/git", { body: { connection, auth } });
598
+ }
599
+ forget(connectionId) {
600
+ return this.mw.http.request("DELETE", `/workspace/git/connections/${encodeURIComponent(connectionId)}`);
601
+ }
602
+ project(projectId) {
603
+ return this.mw.http.request("GET", `/workspace/projects/${encodeURIComponent(projectId)}/git`);
604
+ }
605
+ setProject(projectId, connection, expectedRevision, auth) {
606
+ return this.mw.http.request("PUT", `/workspace/projects/${encodeURIComponent(projectId)}/git`, {
607
+ body: { connection, expectedRevision, auth }
608
+ });
609
+ }
610
+ /** Compatibility call. Prefer start() and operation()/watch() for reconnectable writes. */
611
+ run(projectId, operation, auth) {
612
+ return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(projectId)}/git/${operation}`, { body: { auth } });
613
+ }
614
+ /** Requires health.gitOperationsVersion >= 1 (>= 2 for branch changes). Acceptance persists before Git runs;
615
+ * disconnecting only detaches the client. The same ID/intent never runs twice.
616
+ */
617
+ start(projectId, request) {
618
+ return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(projectId)}/git/operations`, { body: request });
619
+ }
620
+ async operations(projectId, activeOnly = false) {
621
+ const result = await this.mw.http.request(
622
+ "GET",
623
+ `/workspace/projects/${encodeURIComponent(projectId)}/git/operations`,
624
+ { query: { active: activeOnly, actionsVersion: 2 } }
625
+ );
626
+ return result.operations;
627
+ }
628
+ operation(id) {
629
+ return this.mw.http.request("GET", `/workspace/git/operations/${encodeURIComponent(id)}`);
630
+ }
631
+ cancel(id) {
632
+ return this.mw.http.request("POST", `/workspace/git/operations/${encodeURIComponent(id)}/cancel`);
633
+ }
634
+ /** Current snapshot followed by state changes. Aborting observation never cancels the operation. */
635
+ async *watch(id, opts = {}) {
636
+ const controller = new AbortController();
637
+ const abort = () => controller.abort();
638
+ opts.signal?.addEventListener("abort", abort, { once: true });
639
+ if (opts.signal?.aborted) controller.abort();
640
+ try {
641
+ const response = await this.mw.http.open("GET", `/workspace/git/operations/${encodeURIComponent(id)}/stream`, {
642
+ signal: controller.signal
643
+ });
644
+ let sequence = -1;
645
+ for await (const operation of readSSE(response.body, controller.signal)) {
646
+ if (operation.sequence > sequence) {
647
+ sequence = operation.sequence;
648
+ yield operation;
649
+ }
650
+ }
651
+ } finally {
652
+ controller.abort();
653
+ opts.signal?.removeEventListener("abort", abort);
654
+ }
655
+ }
656
+ };
586
657
 
587
658
  // src/surfaces.ts
588
659
  var SurfacesApi = class {
@@ -644,8 +715,27 @@ var SurfacesApi = class {
644
715
  }
645
716
  };
646
717
 
718
+ // src/service.ts
719
+ var ServiceApi = class {
720
+ constructor(client) {
721
+ this.client = client;
722
+ }
723
+ client;
724
+ updateStatus() {
725
+ return this.client.http.request("GET", "/service/update");
726
+ }
727
+ /** Download first. Acquire immediately before replacement; busy work returns 409.
728
+ * Admission stays closed until release, process exit, or the one-minute expiry. */
729
+ acquireUpdate() {
730
+ return this.client.http.request("POST", "/service/update");
731
+ }
732
+ releaseUpdate(id) {
733
+ return this.client.http.request("DELETE", `/service/update/${encodeURIComponent(id)}`);
734
+ }
735
+ };
736
+
647
737
  // src/version.ts
648
- var SDK_VERSION = "0.1.15" ;
738
+ var SDK_VERSION = "0.1.19" ;
649
739
 
650
740
  // src/daemon-binary.ts
651
741
  function supported(platform, arch) {
@@ -906,6 +996,7 @@ var Mindwire = class _Mindwire {
906
996
  /** Workspace registry: saved agent profiles, projects and chat relationships. */
907
997
  workspace;
908
998
  surfaces;
999
+ service;
909
1000
  http;
910
1001
  /** The default agent type applied to agent-scoped calls, if set. */
911
1002
  defaultAgent;
@@ -941,6 +1032,7 @@ var Mindwire = class _Mindwire {
941
1032
  this.defaultAgent = opts.agent;
942
1033
  this.workspace = new WorkspaceApi(this);
943
1034
  this.surfaces = new SurfacesApi(this);
1035
+ this.service = new ServiceApi(this);
944
1036
  this.auth = new AuthApi(this);
945
1037
  this.prompts = new PromptsApi(this);
946
1038
  this.mcp = new McpApi(this);
@@ -963,6 +1055,7 @@ var Mindwire = class _Mindwire {
963
1055
  clone.defaultAgent = agent;
964
1056
  clone.workspace = new WorkspaceApi(clone);
965
1057
  clone.surfaces = new SurfacesApi(clone);
1058
+ clone.service = new ServiceApi(clone);
966
1059
  clone.auth = new AuthApi(clone);
967
1060
  clone.prompts = new PromptsApi(clone);
968
1061
  clone.mcp = new McpApi(clone);
@@ -1011,6 +1104,12 @@ var Mindwire = class _Mindwire {
1011
1104
  agent(scoped) {
1012
1105
  return this.http.request("GET", "/agent", { query: this.agentParam(scoped) });
1013
1106
  }
1107
+ /** `GET /agent/software` — installed version, compatibility and approved update target. */
1108
+ software(opts = {}) {
1109
+ return this.http.request("GET", "/agent/software", {
1110
+ query: { ...this.agentParam(opts), ...opts.refresh ? { refresh: "true" } : {} }
1111
+ });
1112
+ }
1014
1113
  /**
1015
1114
  * `GET /models` — the models the selected agent can run for the configured account. An empty array
1016
1115
  * is valid (no credentials yet / offline). Throws a 400 {@link ApiError} for an agent whose model is
@@ -1028,7 +1127,7 @@ var Mindwire = class _Mindwire {
1028
1127
  setup(scoped) {
1029
1128
  return this.http.request("POST", "/setup", { query: this.agentParam(scoped) });
1030
1129
  }
1031
- /** `POST /update` — re-run the toolchain, forcing reinstall of installable steps. */
1130
+ /** `POST /update` — install the catalog's newest tested version compatible with this daemon. */
1032
1131
  update(scoped) {
1033
1132
  return this.http.request("POST", "/update", { query: this.agentParam(scoped) });
1034
1133
  }
@@ -1125,6 +1224,7 @@ var Mindwire = class _Mindwire {
1125
1224
  if (input.options !== void 0) body.options = input.options;
1126
1225
  if (input.mode !== void 0) body.mode = input.mode;
1127
1226
  if (input.resolve !== void 0) body.resolve = input.resolve;
1227
+ if (input.gitAuth !== void 0) body.gitAuth = input.gitAuth;
1128
1228
  const data = await this.http.request("POST", "/turns", {
1129
1229
  query: this.agentParam(input),
1130
1230
  body
@@ -1228,12 +1328,26 @@ var AuthApi = class {
1228
1328
  body: input
1229
1329
  });
1230
1330
  }
1331
+ /** Read one interactive attempt, scoped by the returned AuthState.flowId. */
1332
+ poll(flowId, scoped) {
1333
+ return this.step({ _flowId: flowId }, scoped);
1334
+ }
1335
+ /** Cancel this native login without affecting a newer sign-in attempt. */
1336
+ cancel(flowId, scoped) {
1337
+ return this.step({ _flowId: flowId, _action: "cancel" }, scoped);
1338
+ }
1231
1339
  /** `GET /auth/status` — is the agent authenticated, and via which method. */
1232
1340
  status(scoped) {
1233
1341
  return this.mw.http.request("GET", "/auth/status", {
1234
1342
  query: this.mw.agentParam(scoped)
1235
1343
  });
1236
1344
  }
1345
+ /** `POST /auth/logout` — disconnect this harness once its running chats finish. */
1346
+ logout(scoped) {
1347
+ return this.mw.http.request("POST", "/auth/logout", {
1348
+ query: this.mw.agentParam(scoped)
1349
+ });
1350
+ }
1237
1351
  };
1238
1352
  var PromptsApi = class {
1239
1353
  constructor(mw) {
@@ -1475,6 +1589,23 @@ var NotifyApi = class {
1475
1589
  };
1476
1590
 
1477
1591
  // src/target/host.ts
1592
+ function versionAtLeast(actual, desired) {
1593
+ if (actual === desired) return true;
1594
+ if (!actual || !/^\d+\.\d+\.\d+$/.test(actual) || !/^\d+\.\d+\.\d+$/.test(desired)) return false;
1595
+ const a = actual.split(".").map(Number), b = desired.split(".").map(Number);
1596
+ for (let i = 0; i < 3; i++) {
1597
+ if (a[i] !== b[i]) return a[i] > b[i];
1598
+ }
1599
+ return true;
1600
+ }
1601
+ var versionCheckScript = String.raw`version_at_least() {
1602
+ [ "$1" != "$2" ] || return 0
1603
+ [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
1604
+ local a b c x y z
1605
+ IFS=. read -r a b c <<< "$1"
1606
+ IFS=. read -r x y z <<< "$2"
1607
+ (( 10#$a > 10#$x || (10#$a == 10#$x && 10#$b > 10#$y) || (10#$a == 10#$x && 10#$b == 10#$y && 10#$c >= 10#$z) ))
1608
+ }`;
1478
1609
  function makeEmit(cfg) {
1479
1610
  const target = cfg.target ?? "sandbox";
1480
1611
  return (e) => {
@@ -1511,19 +1642,24 @@ async function ensureDaemon(host, cfg) {
1511
1642
  message: health.version ? `daemon reachable (v${health.version})` : "daemon reachable (version unknown)",
1512
1643
  version: health.version
1513
1644
  });
1514
- const upToDate = health.version !== void 0 && health.version === desired;
1645
+ const upToDate = versionAtLeast(health.version, desired);
1515
1646
  if (!cfg.forceDeploy && (upToDate || !cfg.autoUpdate)) {
1516
1647
  emit2({
1517
1648
  phase: "skip",
1518
- message: upToDate ? `daemon already at v${desired}` : "keeping the running daemon",
1649
+ message: upToDate ? `daemon already at v${health.version}` : "keeping the running daemon",
1519
1650
  version: health.version
1520
1651
  });
1521
1652
  return token;
1522
1653
  }
1654
+ if ((health.serviceUpdateVersion ?? 0) < 1) {
1655
+ 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.");
1656
+ emit2({ phase: "skip", message: "automatic update deferred: upgrade this legacy service explicitly to enable idle updates", version: health.version });
1657
+ return token;
1658
+ }
1523
1659
  } else {
1524
1660
  emit2({ phase: "probe", message: "no daemon reachable; deploying" });
1525
1661
  }
1526
- await deploy(host, cfg, emit2, token, directory);
1662
+ await deploy(host, cfg, emit2, token, directory, health.reachable);
1527
1663
  if (!cfg.token) token = await readWorkspaceToken(host, directory) ?? token;
1528
1664
  return token;
1529
1665
  } catch (err) {
@@ -1536,7 +1672,7 @@ async function readWorkspaceToken(host, directory) {
1536
1672
  const candidate = saved.stdout?.trim();
1537
1673
  return candidate && candidate.length <= 4096 && /^[\x21-\x7e]+$/.test(candidate) ? candidate : void 0;
1538
1674
  }
1539
- async function deploy(host, cfg, emit2, token, directory) {
1675
+ async function deploy(host, cfg, emit2, token, directory, requireLease) {
1540
1676
  const newPath = directory + "/mindwired.new";
1541
1677
  const BIN = shellQuote(directory + "/mindwired"), BIN_NEW = shellQuote(newPath);
1542
1678
  const STATE = shellQuote(directory + "/agent-state.json"), LOG = shellQuote(directory + "/daemon.log");
@@ -1578,6 +1714,7 @@ async function deploy(host, cfg, emit2, token, directory) {
1578
1714
  ` latest=$(curl -fsSL https://api.github.com/repos/oblien/mindwire/releases/latest | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')`,
1579
1715
  ' case "$latest" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "MINDWIRE_FAIL no matching or latest release"; exit 1 ;; esac',
1580
1716
  ' release="https://github.com/oblien/mindwire/releases/download/$latest"',
1717
+ ' mw_expected_version="${latest#v}"',
1581
1718
  ` asset="mindwired-$latest-${platform}-${arch}"`,
1582
1719
  ' download || { echo "MINDWIRE_FAIL latest release download failed"; exit 1; }',
1583
1720
  "fi",
@@ -1597,29 +1734,43 @@ async function deploy(host, cfg, emit2, token, directory) {
1597
1734
  'else echo "MINDWIRE_FAIL No supported update lock is available (flock on Linux, lockf on macOS)."; exit 1; fi',
1598
1735
  `mw_token=${shellQuote(token)}`,
1599
1736
  !cfg.token ? `if [ -s ${TOKEN} ]; then mw_token=$(cat ${TOKEN}); fi` : "",
1737
+ 'mw_lease=""',
1738
+ `mw_lease_file=${shellQuote(directory)}/update-lease-$$.json`,
1739
+ "cleanup_update() {",
1740
+ ` 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`,
1741
+ ` rm -f ${BIN_NEW} "$mw_lease_file"${stagedUpload ? " " + shellQuote(stagedUpload) : ""}`,
1742
+ "}",
1743
+ // Shared staging is only removed while this process owns the workspace lock.
1744
+ "trap cleanup_update EXIT",
1745
+ versionCheckScript,
1746
+ `mw_desired=${shellQuote(desired)}`,
1747
+ 'mw_expected_version="$mw_desired"',
1748
+ `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1600
1749
  !cfg.forceDeploy ? [
1601
- `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1602
1750
  `mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
1603
- `if [ -n "$mw_health" ] && ${cfg.autoUpdate ? `[ "$mw_version" = ${shellQuote(desired)} ]` : "true"}; then echo MINDWIRE_READY; exit 0; fi`
1751
+ `if [ -n "$mw_health" ] && ${cfg.autoUpdate ? 'version_at_least "$mw_version" "$mw_desired"' : "true"}; then echo MINDWIRE_READY; exit 0; fi`
1604
1752
  ].join("\n") : "",
1605
1753
  // macOS ships POSIX setsid in Perl, but does not include Linux's setsid executable.
1606
1754
  "if command -v setsid >/dev/null 2>&1; then mw_detach=(setsid);",
1607
1755
  `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: $!";');`,
1608
1756
  'else echo "MINDWIRE_FAIL Cannot start the Mindwire service: setsid or Perl is required."; exit 1; fi',
1609
1757
  acquire,
1610
- // Stop a prior daemon by exact process NAME, never `pkill -f <path>`: this whole script (which
1611
- // contains `${BIN}` several times) is the argv of the `bash -lc` shell running it, so a full-cmdline
1612
- // match would SIGTERM our own deploying shell before the daemon ever launches. `-x mindwired` matches
1613
- // only the daemon's comm (`bash`/`pkill` never match), leaving this shell alive.
1614
- "if command -v pkill >/dev/null 2>&1; then",
1615
- " pkill -x mindwired 2>/dev/null || true",
1616
- "else",
1617
- " for mw_proc in /proc/[0-9]*/comm; do",
1618
- ' IFS= read -r mw_name 2>/dev/null < "$mw_proc" || continue',
1619
- ' [ "$mw_name" = mindwired ] || continue',
1620
- " mw_pid=${mw_proc#/proc/}; mw_pid=${mw_pid%/comm}",
1621
- ' kill "$mw_pid" 2>/dev/null || true',
1622
- " done",
1758
+ // The service decides idleness after transfer. Never stop arbitrary processes
1759
+ // or interrupt a chat that began while the SDK was downloading its binary.
1760
+ `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1761
+ 'if [ -n "$mw_health" ]; then',
1762
+ ` 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`,
1763
+ ' case "$mw_status" in',
1764
+ " 201)",
1765
+ ` mw_lease=$(sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\\([[:alnum:]]*\\)".*/\\1/p' "$mw_lease_file")`,
1766
+ ` mw_service_pid=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p' "$mw_lease_file")`,
1767
+ ' [ -n "$mw_lease" ] && [[ "$mw_service_pid" =~ ^[0-9]+$ ]] && [ "$mw_service_pid" -gt 1 ] || { echo "MINDWIRE_FAIL invalid service update lease"; exit 1; }',
1768
+ ' kill "$mw_service_pid" ;;',
1769
+ " 409) echo MINDWIRE_UPDATE_DEFERRED; exit 0 ;;",
1770
+ ' 404) echo "MINDWIRE_FAIL this legacy service requires an explicit upgrade before idle updates"; exit 1 ;;',
1771
+ ' *) echo "MINDWIRE_FAIL could not reserve an idle service update"; exit 1 ;;',
1772
+ " esac",
1773
+ requireLease ? 'else echo "MINDWIRE_FAIL the service went offline before its idle update could be reserved"; exit 1' : "",
1623
1774
  "fi",
1624
1775
  "sleep 0.3",
1625
1776
  `mv -f ${BIN_NEW} ${BIN}`,
@@ -1629,7 +1780,10 @@ async function deploy(host, cfg, emit2, token, directory) {
1629
1780
  `"\${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>&- &`,
1630
1781
  // Health-poll from inside the VM (loopback) and emit a marker — exit codes are unreliable here.
1631
1782
  'mw_pid=$!; mw_exit=""; mw_deadline=$((SECONDS + 30))',
1632
- `while (( SECONDS < mw_deadline )); do curl -fsS --connect-timeout 2 --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz >/dev/null 2>&1 && { echo MINDWIRE_READY; exit 0; };`,
1783
+ "while (( SECONDS < mw_deadline )); do",
1784
+ ` 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)`,
1785
+ ` mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
1786
+ ' if version_at_least "$mw_version" "$mw_expected_version"; then echo MINDWIRE_READY; exit 0; fi',
1633
1787
  ' if [ -z "$mw_exit" ] && ! kill -0 "$mw_pid" 2>/dev/null; then',
1634
1788
  ' mw_exit=0; wait "$mw_pid" || mw_exit=$?; [ "$mw_exit" = 0 ] || break',
1635
1789
  " fi",
@@ -1637,13 +1791,18 @@ async function deploy(host, cfg, emit2, token, directory) {
1637
1791
  "done",
1638
1792
  `mw_tail=$(tail -n 40 ${LOG} 2>/dev/null || true)`,
1639
1793
  'if [ -n "$mw_exit" ] && [ "$mw_exit" != 0 ]; then mw_reason="Mindwire exited during startup (status $mw_exit).";',
1640
- 'else mw_reason="The Mindwire service did not become ready."; fi',
1794
+ 'else mw_reason="The Mindwire service did not become ready at v$mw_expected_version (reported: $mw_version)."; fi',
1641
1795
  '[ -z "$mw_tail" ] || mw_reason="$mw_reason $mw_tail"',
1642
1796
  'printf "MINDWIRE_FAIL %s\\n" "$mw_reason"'
1643
1797
  ].join("\n");
1644
1798
  emit2({ phase: "launch", message: "launching daemon" });
1645
1799
  const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 420 });
1646
1800
  const out = res.stdout ?? "";
1801
+ if (out.includes("MINDWIRE_UPDATE_DEFERRED")) {
1802
+ if (cfg.forceDeploy) throw new MindwireError("The workspace is busy. Wait for its operations to finish before deploying the service.");
1803
+ emit2({ phase: "skip", message: "service update deferred while workspace operations are running" });
1804
+ return;
1805
+ }
1647
1806
  if (!out.includes("MINDWIRE_READY")) {
1648
1807
  throw new MindwireError(
1649
1808
  "mindwire: the in-sandbox daemon did not become healthy after deploy.\n" + (out || res.stderr || res.error || "(no output)").trim()
@@ -1658,7 +1817,11 @@ async function probeHealth(host, port, token) {
1658
1817
  if (!body) return { reachable: false };
1659
1818
  try {
1660
1819
  const j = JSON.parse(body);
1661
- return { reachable: true, version: typeof j.version === "string" ? j.version : void 0 };
1820
+ return {
1821
+ reachable: true,
1822
+ version: typeof j.version === "string" ? j.version : void 0,
1823
+ serviceUpdateVersion: typeof j.serviceUpdateVersion === "number" ? j.serviceUpdateVersion : void 0
1824
+ };
1662
1825
  } catch {
1663
1826
  return { reachable: true };
1664
1827
  }
@@ -1759,7 +1922,7 @@ var ContainerHost = class {
1759
1922
  base;
1760
1923
  containerId;
1761
1924
  exec(argv, opts) {
1762
- return this.base.exec(["docker", "exec", this.containerId, ...argv], opts);
1925
+ return this.base.exec(["docker", "exec", "--env", "MINDWIRE_ISOLATION=container", this.containerId, ...argv], opts);
1763
1926
  }
1764
1927
  async putFile(path, data, opts) {
1765
1928
  const slash = path.lastIndexOf("/");
@@ -1859,7 +2022,7 @@ async function runContainer(host, cfg, emit2) {
1859
2022
  "mindwire: running the daemon in a container needs an image (docker.image) to create one, or an existing container (docker.container) to attach to."
1860
2023
  );
1861
2024
  }
1862
- const runArgs = ["docker", "run", "-d"];
2025
+ const runArgs = ["docker", "run", "-d", "--env", "MINDWIRE_ISOLATION=container"];
1863
2026
  if (cfg.name) runArgs.push("--name", cfg.name);
1864
2027
  runArgs.push("-p", `127.0.0.1:0:${port}`);
1865
2028
  if (cfg.createArgs?.length) runArgs.push(...cfg.createArgs);
@@ -2186,7 +2349,7 @@ var DockerHost = class {
2186
2349
  container;
2187
2350
  async exec(argv) {
2188
2351
  const { Writable } = await import('stream');
2189
- const exec = await this.container.exec({ Cmd: argv, AttachStdout: true, AttachStderr: true });
2352
+ const exec = await this.container.exec({ Cmd: argv, Env: ["MINDWIRE_ISOLATION=container"], AttachStdout: true, AttachStderr: true });
2190
2353
  const stream = await exec.start({ hijack: true, stdin: false });
2191
2354
  const out = [];
2192
2355
  const err = [];
@@ -2240,7 +2403,7 @@ async function provisionDocker(docker2, config = {}, onLog) {
2240
2403
  // The runtime image starts mindwired itself. Do not replace its command: doing so turns the
2241
2404
  // image ENTRYPOINT into `mindwired sleep infinity` and prevents the runtime from starting.
2242
2405
  Image: image,
2243
- Env: [`ADDR=:${port}`, `AGENT_TYPE=${agent}`, `AGENT_CWD=${agentCwd}`],
2406
+ Env: [`ADDR=:${port}`, `AGENT_TYPE=${agent}`, `AGENT_CWD=${agentCwd}`, "MINDWIRE_ISOLATION=container"],
2244
2407
  Tty: false,
2245
2408
  ExposedPorts: { [portKey]: {} },
2246
2409
  HostConfig: { PortBindings: { [portKey]: [{ HostPort: "0" }] } },
@@ -2558,6 +2721,7 @@ function clearCatalogCache() {
2558
2721
  exports.ApiError = ApiError;
2559
2722
  exports.AuthApi = AuthApi;
2560
2723
  exports.ContainerHost = ContainerHost;
2724
+ exports.GitAccessApi = GitAccessApi;
2561
2725
  exports.Http = Http;
2562
2726
  exports.MODELS_DEV_URL = MODELS_DEV_URL;
2563
2727
  exports.McpApi = McpApi;
@@ -2570,6 +2734,7 @@ exports.ProvidersApi = ProvidersApi;
2570
2734
  exports.Run = Run;
2571
2735
  exports.RunFailedError = RunFailedError;
2572
2736
  exports.SDK_VERSION = SDK_VERSION;
2737
+ exports.ServiceApi = ServiceApi;
2573
2738
  exports.SurfacesApi = SurfacesApi;
2574
2739
  exports.TimeoutError = TimeoutError;
2575
2740
  exports.WorkspaceApi = WorkspaceApi;