agent-bios 0.19.2 → 0.19.3

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.
Files changed (46) hide show
  1. package/DEPENDENCIES.md +32 -4
  2. package/README.md +54 -4
  3. package/claude/guides/cli-multi-model-workflow.md +1 -1
  4. package/claude/guides/coding-staged-workflow.md +17 -0
  5. package/claude/guides/llm-capability-boundary.md +7 -1
  6. package/claude/guides/tooling-gotchas.md +20 -1
  7. package/claude/guides/ui-design/visual-direction.md +88 -0
  8. package/claude/guides/ui-design.md +90 -0
  9. package/claude/guides/verification-discipline.md +10 -1
  10. package/claude/hooks/tooling-gotchas-hook.py +41 -0
  11. package/codex/guides/cli-multi-model-workflow.md +1 -1
  12. package/codex/guides/coding-staged-workflow.md +17 -0
  13. package/codex/guides/llm-capability-boundary.md +7 -1
  14. package/codex/guides/tooling-gotchas.md +20 -1
  15. package/codex/guides/ui-design/visual-direction.md +88 -0
  16. package/codex/guides/ui-design.md +90 -0
  17. package/codex/guides/verification-discipline.md +10 -1
  18. package/compose/app_bridge/scripts/bridge.py +23 -6
  19. package/compose/app_desktop/server.py +250 -0
  20. package/compose/domains.json +1 -0
  21. package/compose/host_platform.py +121 -0
  22. package/compose/instructions-state.py +5 -2
  23. package/compose/instructions_app.py +281 -48
  24. package/compose/instructions_catalog.py +2 -2
  25. package/compose/instructions_import.py +19 -11
  26. package/compose/instructions_install.py +77 -11
  27. package/compose/instructions_session.py +6 -6
  28. package/compose/instructions_setup.py +41 -4
  29. package/compose/instructions_setup_cli.py +31 -10
  30. package/compose/instructions_setup_i18n.py +3 -0
  31. package/compose/instructions_store.py +6 -6
  32. package/compose/instructions_transaction.py +8 -6
  33. package/compose/instructions_ui_runtime.py +2 -1
  34. package/compose/native_cli.py +52 -0
  35. package/compose/runtime_entry.py +58 -0
  36. package/compose/windows_deploy.py +719 -0
  37. package/docs/instructions.md +1 -0
  38. package/docs/releases/0.19.3.md +107 -0
  39. package/docs/session-model.md +8 -0
  40. package/docs/setup.md +36 -0
  41. package/docs/windows.md +99 -0
  42. package/install.sh +1 -0
  43. package/launch/agent-launch.py +12 -4
  44. package/launch/agent-launch.zsh +11 -1
  45. package/package.json +8 -3
  46. package/provenance.json +1 -1
@@ -0,0 +1,719 @@
1
+ """Owned Windows script deployment; private instructions remain installer-owned.
2
+
3
+ The caller authenticates the downloaded bundle before invoking this module. This
4
+ owner checks its contents, interpreter binding and destinations, stages immutable
5
+ versions, and publishes a data binding consumed by the signed static wrappers.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import contextlib
11
+ import hashlib
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ import tempfile
20
+ from typing import Any
21
+
22
+ try:
23
+ from .host_platform import redirected
24
+ from .instructions_transaction import transaction_lock, reject_symlink_ancestors
25
+ except ImportError:
26
+ from host_platform import redirected
27
+ from instructions_transaction import transaction_lock, reject_symlink_ancestors
28
+
29
+ OWNER = "agent-bios-windows-script"
30
+ SCHEMA_VERSION = 1
31
+ COMMANDS = ("agent-bios.ps1", "agent-launch.ps1")
32
+ # A directory on PATH holding only .ps1 files is reachable from PowerShell alone:
33
+ # Windows does not treat .ps1 as executable, so cmd.exe, Win+R and any program
34
+ # that spawns a command see nothing. Each command therefore also gets a cmd shim.
35
+ # The shim is generated at installation rather than shipped in the bundle because
36
+ # Authenticode cannot sign a .cmd, and a signed bundle carrying an unsignable
37
+ # member would say less than its signature appears to say. Its bytes are fixed --
38
+ # %~dp0 resolves the directory at run time -- so its digest is a stable claim.
39
+ SHIMS = {"agent-bios.cmd": "agent-bios.ps1", "agent-launch.cmd": "agent-launch.ps1"}
40
+ ENTRY = "runtime_entry.py"
41
+ PRIVATE_ENVIRONMENT = ("HOME", "AGENT_BIOS_STATE_DIR", "AGENT_BIOS_INSTRUCTIONS_DIR", "CLAUDE_CONFIG_DIR", "CODEX_HOME")
42
+
43
+
44
+ class DeploymentError(RuntimeError):
45
+ pass
46
+
47
+
48
+ def _shim(target: str) -> bytes:
49
+ """A cmd.exe entry point for a PowerShell command in the same directory.
50
+
51
+ The interpreter is named by its fixed location, not looked up on PATH, which
52
+ is what the Start menu shortcut already does. A caller whose PATH lacks the
53
+ Windows PowerShell directory is exactly the caller this shim exists for, and
54
+ a bare `powershell.exe` fails there with cmd's own 9009.
55
+ """
56
+ if target not in COMMANDS:
57
+ raise DeploymentError(f"a shim may only target an owned command: {target}")
58
+ return ("@echo off\r\n"
59
+ '"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" '
60
+ "-NoProfile -ExecutionPolicy Bypass "
61
+ f'-File "%~dp0{target}" %*\r\n'
62
+ "exit /b %ERRORLEVEL%\r\n").encode("ascii")
63
+
64
+
65
+ def _shim_inventory() -> dict[str, str]:
66
+ return {name: hashlib.sha256(_shim(target)).hexdigest() for name, target in SHIMS.items()}
67
+
68
+
69
+ def _hash(path: Path) -> str:
70
+ digest = hashlib.sha256()
71
+ with path.open("rb") as stream:
72
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
73
+ digest.update(chunk)
74
+ return digest.hexdigest()
75
+
76
+
77
+ def _canonical(data: Any) -> bytes:
78
+ return (json.dumps(data, sort_keys=True, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8")
79
+
80
+
81
+ def _atomic(path: Path, data: bytes) -> None:
82
+ reject_symlink_ancestors(path)
83
+ path.parent.mkdir(parents=True, exist_ok=True)
84
+ fd, temporary = tempfile.mkstemp(prefix=".deploy-", dir=path.parent)
85
+ try:
86
+ with os.fdopen(fd, "wb") as stream:
87
+ stream.write(data)
88
+ stream.flush()
89
+ os.fsync(stream.fileno())
90
+ os.replace(temporary, path)
91
+ finally:
92
+ with contextlib.suppress(FileNotFoundError):
93
+ os.unlink(temporary)
94
+
95
+
96
+ def _json(path: Path) -> dict[str, Any]:
97
+ reject_symlink_ancestors(path)
98
+ try:
99
+ value = json.loads(path.read_text(encoding="utf-8"))
100
+ except (OSError, ValueError) as exc:
101
+ raise DeploymentError(f"invalid deployment record: {path}") from exc
102
+ if not isinstance(value, dict):
103
+ raise DeploymentError(f"invalid deployment record: {path}")
104
+ return value
105
+
106
+
107
+ def _within(path: Path, root: Path) -> bool:
108
+ try:
109
+ path.relative_to(root)
110
+ return True
111
+ except ValueError:
112
+ return False
113
+
114
+
115
+ def _inventory(root: Path) -> dict[str, str]:
116
+ reject_symlink_ancestors(root)
117
+ if not root.is_dir():
118
+ raise DeploymentError(f"required directory is absent: {root}")
119
+ result: dict[str, str] = {}
120
+ for directory, directories, files in os.walk(root, followlinks=False):
121
+ for name in directories + files:
122
+ path = Path(directory) / name
123
+ if redirected(path):
124
+ raise DeploymentError(f"redirected bundle path: {path}")
125
+ if name in files:
126
+ if not path.is_file():
127
+ raise DeploymentError(f"non-file bundle entry: {path}")
128
+ result[path.relative_to(root).as_posix()] = _hash(path)
129
+ return dict(sorted(result.items()))
130
+
131
+
132
+ def _check_inventory(root: Path, inventory: dict[str, str], *, exact: bool = True) -> None:
133
+ if not isinstance(inventory, dict) or not inventory:
134
+ raise DeploymentError(f"empty file inventory: {root}")
135
+ for name, digest in inventory.items():
136
+ relative = Path(name)
137
+ if relative.is_absolute() or ".." in relative.parts or "\\" in name or ":" in name:
138
+ raise DeploymentError(f"unsafe inventory path: {name}")
139
+ path = root / relative
140
+ reject_symlink_ancestors(path)
141
+ if not path.is_file() or _hash(path) != digest:
142
+ raise DeploymentError(f"owned file drifted: {path}")
143
+ if exact and _inventory(root) != inventory:
144
+ raise DeploymentError(f"unexpected files in owned release: {root}")
145
+
146
+
147
+ def _version(value: str) -> tuple[int, int, int, int, tuple]:
148
+ if not isinstance(value, str):
149
+ raise DeploymentError("application version is missing")
150
+ match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?", value)
151
+ if not match:
152
+ raise DeploymentError(f"unsupported version: {value!r}")
153
+ parts = tuple((0, int(part)) if part.isdigit() else (1, part) for part in (match[4] or "").split("."))
154
+ return int(match[1]), int(match[2]), int(match[3]), int(match[4] is None), parts
155
+
156
+
157
+ def _same_path(left: str, right: str) -> bool:
158
+ left, right = os.path.expandvars(left.strip('"')), os.path.expandvars(right.strip('"'))
159
+ try:
160
+ return os.path.samefile(left, right)
161
+ except (OSError, ValueError):
162
+ return os.path.normcase(os.path.abspath(left)) == os.path.normcase(os.path.abspath(right))
163
+
164
+
165
+ class WindowsIntegration:
166
+ """HKCU PATH and per-root Start menu entries, with exact ownership receipts."""
167
+
168
+ def __init__(self):
169
+ if os.name != "nt":
170
+ raise DeploymentError("Windows deployment must run on Windows")
171
+ import winreg
172
+ self.registry = winreg
173
+
174
+ def _path(self) -> tuple[str, int]:
175
+ reg = self.registry
176
+ with reg.CreateKey(reg.HKEY_CURRENT_USER, "Environment") as key:
177
+ try:
178
+ value, kind = reg.QueryValueEx(key, "Path")
179
+ if kind not in (reg.REG_SZ, reg.REG_EXPAND_SZ):
180
+ raise DeploymentError("user PATH has an unsupported registry type")
181
+ return value, kind
182
+ except FileNotFoundError:
183
+ return "", reg.REG_EXPAND_SZ
184
+
185
+ def _set_path(self, value: str, kind: int) -> None:
186
+ reg = self.registry
187
+ with reg.CreateKey(reg.HKEY_CURRENT_USER, "Environment") as key:
188
+ reg.SetValueEx(key, "Path", 0, kind, value)
189
+ import ctypes
190
+ result = ctypes.c_size_t()
191
+ ctypes.windll.user32.SendMessageTimeoutW(0xFFFF, 0x1A, 0, "Environment", 2, 2000, ctypes.byref(result))
192
+
193
+ # Static C# source, ASCII only: it is delivered on stdin and compiled by the
194
+ # shell, so no path or user value is ever interpolated into program text.
195
+ SHORTCUT_SOURCE = """
196
+ using System;
197
+ using System.Runtime.InteropServices;
198
+ using System.Runtime.InteropServices.ComTypes;
199
+ using System.Text;
200
+ namespace AgentBiosShortcut {
201
+ [ComImport, Guid("00021401-0000-0000-C000-000000000046")]
202
+ public class ShellLink {}
203
+ [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
204
+ public interface IShellLinkW {
205
+ void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, int fFlags);
206
+ void GetIDList(out IntPtr ppidl);
207
+ void SetIDList(IntPtr pidl);
208
+ void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
209
+ void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
210
+ void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
211
+ void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
212
+ void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
213
+ void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
214
+ void GetHotkey(out short pwHotkey);
215
+ void SetHotkey(short wHotkey);
216
+ void GetShowCmd(out int piShowCmd);
217
+ void SetShowCmd(int iShowCmd);
218
+ void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
219
+ void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
220
+ void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
221
+ void Resolve(IntPtr hwnd, int fFlags);
222
+ void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
223
+ }
224
+ public static class Writer {
225
+ public static void Save(string path, string target, string arguments, string workingDirectory) {
226
+ IShellLinkW link = (IShellLinkW)new ShellLink();
227
+ link.SetPath(target);
228
+ link.SetArguments(arguments);
229
+ link.SetWorkingDirectory(workingDirectory);
230
+ ((IPersistFile)link).Save(path, false);
231
+ }
232
+ }
233
+ }
234
+ """
235
+
236
+ def _shortcut(self, target: Path, destination: Path, command: str) -> None:
237
+ # The executed PowerShell program is static; paths are data environment
238
+ # values, never interpolated into its source or encoded as a workaround.
239
+ # IShellLinkW is used directly: the WScript.Shell shortcut object cannot
240
+ # save to paths with characters outside the system ANSI code page.
241
+ script = ("$ErrorActionPreference='Stop'; $source=[Console]::In.ReadToEnd(); "
242
+ "Add-Type -TypeDefinition $source -Language CSharp; "
243
+ "[AgentBiosShortcut.Writer]::Save($env:AGENT_BIOS_SHORTCUT_DESTINATION, $env:AGENT_BIOS_SHORTCUT_SHELL, "
244
+ "('-NoProfile -File '+[char]34+$env:AGENT_BIOS_SHORTCUT_TARGET+[char]34+' '+$env:AGENT_BIOS_SHORTCUT_COMMAND), "
245
+ "$env:USERPROFILE)")
246
+ powershell = Path(os.environ["SystemRoot"]) / "System32/WindowsPowerShell/v1.0/powershell.exe"
247
+ env = dict(os.environ, AGENT_BIOS_SHORTCUT_DESTINATION=str(destination),
248
+ AGENT_BIOS_SHORTCUT_SHELL=str(powershell), AGENT_BIOS_SHORTCUT_TARGET=str(target),
249
+ AGENT_BIOS_SHORTCUT_COMMAND=command)
250
+ result = subprocess.run([str(powershell), "-NoProfile", "-NonInteractive", "-Command", script], env=env,
251
+ input=self.SHORTCUT_SOURCE, capture_output=True, text=True, encoding="utf-8",
252
+ errors="replace", timeout=180)
253
+ if result.returncode:
254
+ detail = (result.stderr or result.stdout).strip().replace("\r\n", " ")[:600]
255
+ raise DeploymentError(f"Start menu shortcut creation failed (exit {result.returncode}): {detail}")
256
+
257
+ def _group(self, root: Path) -> Path:
258
+ token = hashlib.sha256(os.path.normcase(str(root)).encode()).hexdigest()[:12]
259
+ return Path(os.environ["APPDATA"]) / "Microsoft/Windows/Start Menu/Programs" / ("agent-bios Script " + token)
260
+
261
+ def _validate(self, root: Path, receipt: dict[str, Any]) -> None:
262
+ if not receipt:
263
+ return
264
+ if receipt.get("owner") != OWNER or receipt.get("root") != str(root) or receipt.get("schema_version") != SCHEMA_VERSION:
265
+ raise DeploymentError("invalid platform ownership receipt")
266
+ if receipt.get("path_entry") not in (None, str(root / "bin")):
267
+ raise DeploymentError("invalid owned PATH entry")
268
+ group = self._group(root)
269
+ if receipt.get("shortcut_group") not in (None, str(group)):
270
+ raise DeploymentError("invalid Start menu group ownership")
271
+ for collection in (receipt.get("shortcuts", {}), receipt.get("pending_shortcuts", {})):
272
+ if not isinstance(collection, dict):
273
+ raise DeploymentError("invalid Start menu ownership inventory")
274
+ for name in collection:
275
+ path = Path(name)
276
+ if path.parent != group or path.name not in ("agent-bios.lnk", "Instructions Studio.lnk"):
277
+ raise DeploymentError("invalid Start menu ownership claim")
278
+ reject_symlink_ancestors(path)
279
+
280
+ def apply(self, root: Path, prior: dict[str, Any]) -> dict[str, Any]:
281
+ self._validate(root, prior)
282
+ receipt_path = root / "platform.json"
283
+ receipt = dict(prior)
284
+ receipt.update(schema_version=SCHEMA_VERSION, owner=OWNER, root=str(root))
285
+ group = self._group(root)
286
+ reject_symlink_ancestors(group)
287
+ owned = dict(prior.get("shortcuts", {}))
288
+ pending = dict(prior.get("pending_shortcuts", {}))
289
+ if group.exists() and prior.get("shortcut_group") != str(group) and not owned:
290
+ raise DeploymentError(f"unowned Start menu directory: {group}")
291
+ for name in ("agent-bios.lnk", "Instructions Studio.lnk"):
292
+ destination = group / name
293
+ reject_symlink_ancestors(destination)
294
+ accepted = (owned.get(str(destination)), pending.get(str(destination)))
295
+ if destination.exists() and _hash(destination) not in accepted:
296
+ raise DeploymentError(f"modified or unowned Start menu shortcut: {destination}")
297
+ current, kind = self._path()
298
+ entry = str(root / "bin")
299
+ if not any(_same_path(part, entry) for part in current.split(";") if part):
300
+ receipt["path_entry"] = entry
301
+ _atomic(receipt_path, _canonical(receipt))
302
+ self._set_path(current + (";" if current and not current.endswith(";") else "") + entry, kind)
303
+ elif not prior.get("path_entry"):
304
+ receipt["path_entry"] = None # A preexisting user's PATH entry is not ours.
305
+ receipt["shortcut_group"] = str(group)
306
+ _atomic(receipt_path, _canonical(receipt))
307
+ group.mkdir(parents=True, exist_ok=True)
308
+ for name, command in (("agent-bios.lnk", "install"), ("Instructions Studio.lnk", "instructions")):
309
+ destination = group / name
310
+ fd, temporary_name = tempfile.mkstemp(prefix=".agent-bios-", suffix=".lnk", dir=group)
311
+ os.close(fd)
312
+ temporary = Path(temporary_name)
313
+ temporary.unlink()
314
+ try:
315
+ self._shortcut(root / "bin/agent-bios.ps1", temporary, command)
316
+ pending[str(destination)] = _hash(temporary)
317
+ receipt.update(shortcuts=owned, pending_shortcuts=pending)
318
+ _atomic(receipt_path, _canonical(receipt))
319
+ os.replace(temporary, destination)
320
+ owned[str(destination)] = pending.pop(str(destination))
321
+ receipt.update(shortcuts=owned, pending_shortcuts=pending)
322
+ _atomic(receipt_path, _canonical(receipt))
323
+ finally:
324
+ temporary.unlink(missing_ok=True)
325
+ return receipt
326
+
327
+ def remove(self, root: Path, receipt: dict[str, Any]) -> list[str]:
328
+ self._validate(root, receipt)
329
+ retained: list[str] = []
330
+ entry = receipt.get("path_entry")
331
+ if entry is not None and entry != str(root / "bin"):
332
+ raise DeploymentError("invalid owned PATH entry")
333
+ if entry:
334
+ current, kind = self._path()
335
+ # Remove only the exact representation we wrote. A later user edit
336
+ # to an equivalent/short path is not permission to rewrite it.
337
+ self._set_path(";".join(part for part in current.split(";") if part != entry), kind)
338
+ group = self._group(root)
339
+ pending = receipt.get("pending_shortcuts", {})
340
+ shortcuts = receipt.get("shortcuts", {})
341
+ for name in set(shortcuts) | set(pending):
342
+ path = Path(name)
343
+ if path.parent != group or path.name not in ("agent-bios.lnk", "Instructions Studio.lnk"):
344
+ raise DeploymentError("invalid Start menu ownership claim")
345
+ reject_symlink_ancestors(path)
346
+ if path.is_file() and _hash(path) in (shortcuts.get(name), pending.get(name)):
347
+ path.unlink()
348
+ elif path.exists():
349
+ retained.append(str(path))
350
+ with contextlib.suppress(OSError):
351
+ group.rmdir()
352
+ return retained
353
+
354
+
355
+ class Deployment:
356
+ def __init__(self, root: Path, *, environ: dict[str, str] | None = None,
357
+ integration=None, installer_factory=None, probe=None):
358
+ self.root = Path(root).expanduser().absolute()
359
+ reject_symlink_ancestors(self.root)
360
+ self.root = self.root.resolve()
361
+ self.env = dict(os.environ if environ is None else environ)
362
+ legacy = self.env.get("AGENT_BIOS_CORPUS_DIR")
363
+ canonical = self.env.get("AGENT_BIOS_INSTRUCTIONS_DIR")
364
+ if legacy is not None:
365
+ if canonical is not None and not _same_path(legacy, canonical):
366
+ raise DeploymentError("conflicting AGENT_BIOS_INSTRUCTIONS_DIR and AGENT_BIOS_CORPUS_DIR")
367
+ self.env.setdefault("AGENT_BIOS_INSTRUCTIONS_DIR", legacy)
368
+ if (self.root / "owner.json").exists() or (self.root / "deployment.json").exists():
369
+ owner = _json(self.root / "owner.json")
370
+ saved = _json(self.root / "deployment.json") if (self.root / "deployment.json").exists() else owner
371
+ for record in (saved, owner):
372
+ if record.get("owner") != OWNER or record.get("schema_version") != SCHEMA_VERSION or not _same_path(record.get("root", ""), str(self.root)):
373
+ raise DeploymentError("invalid deployment ownership")
374
+ values = saved.get("private_environment", owner.get("private_environment", {}))
375
+ if not isinstance(values, dict) or set(values) - set(PRIVATE_ENVIRONMENT):
376
+ raise DeploymentError("invalid saved private environment")
377
+ values = dict(values)
378
+ if "AGENT_BIOS_STATE_DIR" not in values and owner.get("state_root"):
379
+ values["AGENT_BIOS_STATE_DIR"] = owner["state_root"]
380
+ for name, value in values.items():
381
+ if not isinstance(value, str) or not Path(value).is_absolute():
382
+ raise DeploymentError("invalid saved private environment path")
383
+ if name in self.env and not _same_path(self.env[name], value):
384
+ raise DeploymentError(f"{name} differs from the saved deployment root; explicit root relocation is not supported")
385
+ self.env.setdefault(name, value)
386
+ home = Path(self.env.get("HOME", str(Path.home()))).expanduser()
387
+ self.state_root = Path(self.env.get("AGENT_BIOS_STATE_DIR", str(home / ".local/share/agent-bios"))).resolve()
388
+ if _within(self.state_root, self.root) or _within(self.root, self.state_root):
389
+ raise DeploymentError("application root and private instructions state must be separate")
390
+ self.integration = integration if integration is not None else WindowsIntegration()
391
+ if installer_factory is None:
392
+ try:
393
+ from .instructions_install import InstructionsInstaller
394
+ except ImportError:
395
+ from instructions_install import InstructionsInstaller
396
+ installer_factory = lambda package, env: InstructionsInstaller(package, environ=env)
397
+ self.installer_factory = installer_factory
398
+ self.probe = probe or self._probe
399
+
400
+ @property
401
+ def binding_path(self) -> Path:
402
+ return self.root / "deployment.json"
403
+
404
+ def _owned(self, *, allow_absent=False) -> dict[str, Any] | None:
405
+ reject_symlink_ancestors(self.root)
406
+ if self.root.exists() and not self.root.is_dir():
407
+ raise DeploymentError(f"deployment root is not a directory: {self.root}")
408
+ if any(self.root.glob("unins*.exe")) or (self.root / "agent-bios.exe").exists():
409
+ raise DeploymentError("Inno-managed installation migration is not supported; use a distinct script installation root")
410
+ marker = self.root / "owner.json"
411
+ if not marker.exists():
412
+ if self.root.exists() and any(self.root.iterdir()):
413
+ raise DeploymentError(f"unowned nonempty deployment root: {self.root}")
414
+ if allow_absent:
415
+ return None
416
+ raise DeploymentError("no owned Windows script installation exists")
417
+ owner = _json(marker)
418
+ if owner.get("owner") != OWNER or owner.get("schema_version") != SCHEMA_VERSION or not _same_path(owner.get("root", ""), str(self.root)):
419
+ raise DeploymentError("invalid deployment ownership marker")
420
+ if owner.get("state_root") != str(self.state_root):
421
+ raise DeploymentError("private instructions state differs from the saved deployment root")
422
+ if not isinstance(owner.get("trees"), dict):
423
+ raise DeploymentError("invalid owned tree inventory")
424
+ for name, files in owner["trees"].items():
425
+ path = Path(name)
426
+ if len(path.parts) != 2 or path.parts[0] not in ("releases", "runtimes") or not re.fullmatch(r"[a-f0-9]{64}", path.name):
427
+ raise DeploymentError("invalid owned tree claim")
428
+ if not isinstance(files, dict) or not files:
429
+ raise DeploymentError("invalid owned file inventory")
430
+ reject_symlink_ancestors(self.root / path)
431
+ return owner
432
+
433
+ def _binding(self) -> dict[str, Any]:
434
+ value = _json(self.binding_path)
435
+ if value.get("owner") != OWNER or value.get("schema_version") != SCHEMA_VERSION or not _same_path(value.get("root", ""), str(self.root)):
436
+ raise DeploymentError("invalid deployment binding ownership")
437
+ release = Path(value.get("release_root", ""))
438
+ if release.parent != self.root / "releases" or not re.fullmatch(r"[a-f0-9]{64}", release.name):
439
+ raise DeploymentError("invalid deployment release root")
440
+ for field, expected in (("application_root", release / "package"), ("dependencies_root", release / "dependencies"), ("commands_root", self.root / "bin")):
441
+ if value.get(field) != str(expected):
442
+ raise DeploymentError(f"invalid deployment {field}")
443
+ if value.get("state_root") != str(self.state_root):
444
+ raise DeploymentError("private instructions state differs from the saved deployment root")
445
+ python = value.get("python")
446
+ if not isinstance(python, dict) or not Path(python.get("path", "")).is_absolute():
447
+ raise DeploymentError("invalid Python binding")
448
+ managed = python.get("managed_root")
449
+ if managed and (Path(managed).parent != self.root / "runtimes" or not _within(Path(python["path"]), Path(managed))):
450
+ raise DeploymentError("invalid managed Python ownership")
451
+ if not isinstance(value.get("command_inventory"), dict) or set(value["command_inventory"]) != set(COMMANDS) | set(SHIMS):
452
+ raise DeploymentError("invalid command ownership inventory")
453
+ if {name: value["command_inventory"][name] for name in SHIMS} != _shim_inventory():
454
+ raise DeploymentError("invalid command shim ownership inventory")
455
+ return value
456
+
457
+ def _probe(self, binding: dict[str, Any]) -> None:
458
+ python = Path(binding["python"]["path"])
459
+ if not python.is_file() or _hash(python) != binding["python"]["sha256"]:
460
+ raise DeploymentError(f"Python binding changed or is missing: {python}")
461
+ env = dict(self.env, PYTHONDONTWRITEBYTECODE="1", AGENT_BIOS_WINDOWS_BINDING=str(self.binding_path))
462
+ # The entry handles embeddable _pth isolation before native imports.
463
+ result = subprocess.run([str(python), "-I", "-B", str(Path(binding["application_root"]) / "compose" / ENTRY),
464
+ "--dependencies", binding["dependencies_root"], "--script",
465
+ str(Path(binding["application_root"]) / "compose/native_cli.py"), "--version"],
466
+ env=env, capture_output=True, text=True, timeout=60)
467
+ if result.returncode or result.stdout.strip() != binding["version"]:
468
+ raise DeploymentError(f"staged application probe failed: {(result.stderr or result.stdout).strip()}")
469
+
470
+ def _stage(self, source: Path, parent: str, owner: dict[str, Any]) -> tuple[Path, dict[str, str]]:
471
+ inventory = _inventory(source)
472
+ if not inventory:
473
+ raise DeploymentError("cannot stage an empty bundle")
474
+ digest = hashlib.sha256(_canonical(inventory)).hexdigest()
475
+ target = self.root / parent / digest
476
+ reject_symlink_ancestors(target)
477
+ claims = owner.setdefault("trees", {})
478
+ relative = target.relative_to(self.root).as_posix()
479
+ if target.exists():
480
+ if claims.get(relative) != inventory:
481
+ raise DeploymentError(f"unowned or changed immutable destination: {target}")
482
+ observed = _inventory(target)
483
+ if set(observed) - set(inventory):
484
+ raise DeploymentError(f"unexpected files in owned release: {target}")
485
+ # Repair only previously claimed bytes. Do not delete unknown files
486
+ # or replace a version directory that running workers may still use.
487
+ for name, digest in inventory.items():
488
+ if observed.get(name) != digest:
489
+ _atomic(target / name, (source / name).read_bytes())
490
+ _check_inventory(target, inventory)
491
+ return target, inventory
492
+ target.parent.mkdir(parents=True, exist_ok=True)
493
+ temporary = Path(tempfile.mkdtemp(prefix=".staging-", dir=target.parent))
494
+ try:
495
+ shutil.copytree(source, temporary, dirs_exist_ok=True)
496
+ _check_inventory(temporary, inventory)
497
+ # Claim exactly these files before publication for resumable cleanup.
498
+ claims[relative] = inventory
499
+ _atomic(self.root / "owner.json", _canonical(owner))
500
+ os.replace(temporary, target)
501
+ finally:
502
+ if temporary.exists():
503
+ shutil.rmtree(temporary)
504
+ return target, inventory
505
+
506
+ @contextlib.contextmanager
507
+ def _private_installer(self, binding: dict[str, Any]):
508
+ """Private projections and all children retain the selected entry/deps."""
509
+ runtime_env = {"AGENT_BIOS_PYTHON_ENTRY": str(Path(binding["application_root"]) / "compose" / ENTRY),
510
+ "AGENT_BIOS_PYTHON_DEPS": binding["dependencies_root"],
511
+ "AGENT_BIOS_PYTHON_EXECUTABLE": binding["python"]["path"]}
512
+ home = Path(self.env.get("HOME", str(Path.home()))).expanduser().resolve()
513
+ defaults = {"HOME": str(home), "AGENT_BIOS_STATE_DIR": str(self.state_root),
514
+ "AGENT_BIOS_INSTRUCTIONS_DIR": str(home / ".config/agent-bios/corpus"),
515
+ "CLAUDE_CONFIG_DIR": str(home / ".claude"), "CODEX_HOME": str(home / ".codex")}
516
+ private_env = {name: str(Path(self.env.get(name, default)).expanduser().resolve()) for name, default in defaults.items()}
517
+ runtime_env.update(private_env)
518
+ binding["private_environment"] = private_env
519
+ previous = {name: os.environ.get(name) for name in runtime_env}
520
+ try:
521
+ os.environ.update(runtime_env)
522
+ yield self.installer_factory(Path(binding["application_root"]), dict(self.env, **runtime_env))
523
+ finally:
524
+ for name, value in previous.items():
525
+ if value is None:
526
+ os.environ.pop(name, None)
527
+ else:
528
+ os.environ[name] = value
529
+
530
+ def _install_preflight(self, source: Path, version: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
531
+ owner = self._owned(allow_absent=True)
532
+ prior = self._binding() if self.binding_path.exists() else None
533
+ record = self.installer_factory(source / "package", self.env).record_path
534
+ reject_symlink_ancestors(record)
535
+ if record.exists() and (owner is None or prior is None):
536
+ raise DeploymentError("existing private installation requires an explicit deployment handoff; "
537
+ "a new or detached script root cannot adopt state owned by another installation")
538
+ floors = [value for value in ((owner or {}).get("last_version"), (prior or {}).get("version")) if value is not None]
539
+ for floor in floors:
540
+ if _version(version) < _version(floor):
541
+ raise DeploymentError(f"version downgrade refused: {floor} -> {version}")
542
+ return owner, prior
543
+
544
+ def install(self, source: Path, python: Path, managed_runtime: Path | None = None) -> dict[str, Any]:
545
+ source = Path(source).resolve()
546
+ python = Path(python).resolve()
547
+ if _within(source, self.root) or _within(self.root, source):
548
+ raise DeploymentError("source bundle and deployment root must be separate")
549
+ if managed_runtime is None and _within(python, source):
550
+ raise DeploymentError("a Python inside the temporary source bundle must be supplied as managed runtime")
551
+ manifest = _json(source / "package/package.json")
552
+ version = manifest.get("version")
553
+ _version(version)
554
+ if manifest.get("name") != "agent-bios":
555
+ raise DeploymentError("application bundle is not agent-bios")
556
+ for relative in ("package/compose/" + ENTRY, *("commands/" + name for name in COMMANDS)):
557
+ if not (source / relative).is_file():
558
+ raise DeploymentError(f"incomplete application bundle: {relative}")
559
+ if not (source / "dependencies").is_dir():
560
+ raise DeploymentError("incomplete application bundle: dependencies")
561
+ if not python.is_file():
562
+ raise DeploymentError(f"Python is absent: {python}")
563
+ _inventory(source) # Reject redirects before claiming or writing a root.
564
+ self._install_preflight(source, version) # Refuse foreign ownership before even creating a state lock.
565
+ with transaction_lock(self.state_root):
566
+ owner, prior = self._install_preflight(source, version)
567
+ owner = owner or {"owner": OWNER, "schema_version": SCHEMA_VERSION, "root": str(self.root),
568
+ "state_root": str(self.state_root), "trees": {}}
569
+ _atomic(self.root / "owner.json", _canonical(owner))
570
+ release, inventory = self._stage(source, "releases", owner)
571
+ managed_root = None
572
+ if managed_runtime is not None:
573
+ managed_runtime = Path(managed_runtime).resolve()
574
+ if not _within(python, managed_runtime):
575
+ raise DeploymentError("selected Python is outside the supplied managed runtime")
576
+ relative_python = python.relative_to(managed_runtime)
577
+ managed_root, _ = self._stage(managed_runtime, "runtimes", owner)
578
+ python = managed_root / relative_python
579
+ elif _within(python, self.root):
580
+ if not prior or prior["python"]["path"] != str(python) or not prior["python"].get("managed_root"):
581
+ raise DeploymentError("Python inside the deployment root must have recorded runtime ownership")
582
+ managed_root = Path(prior["python"]["managed_root"])
583
+ binding = {"owner": OWNER, "schema_version": SCHEMA_VERSION, "root": str(self.root),
584
+ "version": version, "content_id": release.name, "release_root": str(release),
585
+ "application_root": str(release / "package"), "dependencies_root": str(release / "dependencies"),
586
+ "commands_root": str(self.root / "bin"), "state_root": str(self.state_root),
587
+ "python": {"path": str(python), "sha256": _hash(python), "managed_root": str(managed_root) if managed_root else None},
588
+ "inventory": inventory,
589
+ "command_inventory": {**{name: _hash(release / "commands" / name) for name in COMMANDS},
590
+ **_shim_inventory()}}
591
+ self.probe(binding)
592
+ old_commands = (prior or {}).get("command_inventory", {})
593
+ for name in (*COMMANDS, *SHIMS):
594
+ destination = self.root / "bin" / name
595
+ reject_symlink_ancestors(destination)
596
+ if destination.exists() and _hash(destination) not in (old_commands.get(name), binding["command_inventory"][name]):
597
+ raise DeploymentError(f"modified or unowned command: {destination}")
598
+ operation = {"owner": OWNER, "schema_version": SCHEMA_VERSION, "version": version,
599
+ "phase": "prepared", "application_root": str(release / "package")}
600
+ _atomic(self.root / "operation.json", _canonical(operation))
601
+ with self._private_installer(binding) as installer:
602
+ configured = installer.record_path.exists()
603
+ if configured:
604
+ operation["phase"] = "private_update_pending"
605
+ _atomic(self.root / "operation.json", _canonical(operation))
606
+ try:
607
+ installer.install()
608
+ except Exception as exc:
609
+ operation.update(phase="private_update_needs_recovery", error=type(exc).__name__)
610
+ _atomic(self.root / "operation.json", _canonical(operation))
611
+ raise DeploymentError("private environment update failed; native recovery is required; active binding was not switched") from exc
612
+ operation["phase"] = "private_environment_updated"
613
+ _atomic(self.root / "operation.json", _canonical(operation))
614
+ for name in COMMANDS:
615
+ _atomic(self.root / "bin" / name, (release / "commands" / name).read_bytes())
616
+ for name, target in SHIMS.items():
617
+ _atomic(self.root / "bin" / name, _shim(target))
618
+ platform_path = self.root / "platform.json"
619
+ prior_platform = _json(platform_path) if platform_path.exists() else {}
620
+ if prior_platform and (prior_platform.get("owner") != OWNER or prior_platform.get("root") != str(self.root)):
621
+ raise DeploymentError("invalid platform ownership receipt")
622
+ receipt = self.integration.apply(self.root, prior_platform)
623
+ _atomic(platform_path, _canonical(receipt))
624
+ _atomic(self.binding_path, _canonical(binding))
625
+ owner.update(last_version=version, private_environment=binding["private_environment"])
626
+ _atomic(self.root / "owner.json", _canonical(owner))
627
+ operation["phase"] = "complete"
628
+ _atomic(self.root / "operation.json", _canonical(operation))
629
+ return {"application_deployed": True, "version": version, "root": str(self.root),
630
+ "commands_root": binding["commands_root"], "binding_path": str(self.binding_path),
631
+ "private_environment_updated": configured, "configuration_pending": not configured,
632
+ "activation": "unverified"}
633
+
634
+ def verify(self) -> dict[str, Any]:
635
+ self._owned()
636
+ binding = self._binding()
637
+ _check_inventory(Path(binding["release_root"]), binding["inventory"])
638
+ _check_inventory(self.root / "bin", binding["command_inventory"], exact=False)
639
+ self.probe(binding)
640
+ if (self.root / "operation.json").exists() and _json(self.root / "operation.json").get("phase") != "complete":
641
+ raise DeploymentError("Windows deployment operation is incomplete; rerun the same verified installation to recover")
642
+ with self._private_installer(binding) as installer:
643
+ configured = installer.record_path.exists()
644
+ if configured:
645
+ installer.verify()
646
+ return {"application_deployed": True, "version": binding["version"], "root": str(self.root),
647
+ "configuration_pending": not configured, "activation": "unverified"}
648
+
649
+ def uninstall(self) -> dict[str, Any]:
650
+ self._owned()
651
+ with transaction_lock(self.state_root):
652
+ owner = self._owned()
653
+ binding = self._binding()
654
+ platform = _json(self.root / "platform.json")
655
+ if platform.get("owner") != OWNER or platform.get("root") != str(self.root) or platform.get("schema_version") != SCHEMA_VERSION:
656
+ raise DeploymentError("invalid platform ownership receipt")
657
+ if hasattr(self.integration, "_validate"):
658
+ self.integration._validate(self.root, platform)
659
+ for name in (*COMMANDS, *SHIMS):
660
+ reject_symlink_ancestors(self.root / "bin" / name)
661
+ with self._private_installer(binding) as installer:
662
+ if hasattr(installer, "_active_intents") and installer._active_intents():
663
+ raise DeploymentError("active instructions activation requires completion before application removal")
664
+ installer.uninstall()
665
+ retained = self.integration.remove(self.root, platform)
666
+ for name, digest in binding["command_inventory"].items():
667
+ path = self.root / "bin" / name
668
+ reject_symlink_ancestors(path)
669
+ if path.is_file() and _hash(path) == digest:
670
+ path.unlink()
671
+ elif path.exists():
672
+ retained.append(str(path))
673
+ releases, runtimes = [], []
674
+ for relative, inventory in owner.get("trees", {}).items():
675
+ target = self.root / relative
676
+ if Path(relative).is_absolute() or ".." in Path(relative).parts or target.parent not in (self.root / "releases", self.root / "runtimes"):
677
+ raise DeploymentError("invalid owned tree claim")
678
+ reject_symlink_ancestors(target)
679
+ if not target.exists():
680
+ continue
681
+ # Historical app helpers/session resources retain absolute entry,
682
+ # dependency and Python paths beyond the active binding. Garbage
683
+ # collection is deferred until all those references are inventoried.
684
+ (runtimes if target.parent == self.root / "runtimes" else releases).append(str(target))
685
+ owner.update(last_version=binding["version"], private_environment=binding["private_environment"])
686
+ _atomic(self.root / "owner.json", _canonical(owner))
687
+ for name in ("deployment.json", "platform.json", "operation.json"):
688
+ (self.root / name).unlink(missing_ok=True)
689
+ for directory in (self.root / "bin", self.root / "releases", self.root / "runtimes", self.root):
690
+ with contextlib.suppress(OSError):
691
+ directory.rmdir()
692
+ return {"application_removed": True, "retained_paths": retained,
693
+ "retained_release_paths": releases, "retained_runtime_paths": runtimes,
694
+ "external_python_preserved": binding["python"]["managed_root"] is None,
695
+ "private_instructions_preserved": True, "sessions_preserved": True}
696
+
697
+
698
+ def main(argv=None) -> int:
699
+ parser = argparse.ArgumentParser(description=__doc__)
700
+ subcommands = parser.add_subparsers(dest="command", required=True)
701
+ install = subcommands.add_parser("install")
702
+ install.add_argument("--source", type=Path, required=True)
703
+ install.add_argument("--python", type=Path, required=True)
704
+ install.add_argument("--managed-runtime", type=Path)
705
+ for command in (install, subcommands.add_parser("verify"), subcommands.add_parser("uninstall")):
706
+ command.add_argument("--root", type=Path, required=True)
707
+ args = parser.parse_args(argv)
708
+ try:
709
+ deployment = Deployment(args.root)
710
+ result = deployment.install(args.source, args.python, args.managed_runtime) if args.command == "install" else getattr(deployment, args.command)()
711
+ print(json.dumps(result, ensure_ascii=False, indent=2))
712
+ return 0
713
+ except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as exc:
714
+ print(f"windows-deploy: {exc}", file=sys.stderr)
715
+ return 1
716
+
717
+
718
+ if __name__ == "__main__":
719
+ raise SystemExit(main())