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
@@ -6,6 +6,10 @@ activation path: installed instructions are stored and verifiable, while a launc
6
6
  activation creates the per-session snapshot and pin.
7
7
  """
8
8
  from __future__ import annotations
9
+ try:
10
+ from host_platform import WINDOWS, runtime_environment
11
+ except ImportError:
12
+ from .host_platform import WINDOWS, runtime_environment
9
13
 
10
14
  import argparse
11
15
  import base64
@@ -482,7 +486,7 @@ class InstructionsInstaller:
482
486
  def _projection_matches(self, entry: dict[str, Any]) -> bool:
483
487
  path, after = Path(entry["path"]), entry["after"]
484
488
  return self._matches_version(path, after) and (
485
- not after.get("exists") or path.stat().st_mode & 0o777 == int(entry["mode"]))
489
+ not after.get("exists") or WINDOWS or path.stat().st_mode & 0o777 == int(entry["mode"]))
486
490
 
487
491
  def _old_record(self) -> dict[str, Any] | None:
488
492
  return _read_json(self.record_path) if self.record_path.exists() else None
@@ -500,8 +504,8 @@ class InstructionsInstaller:
500
504
  if current.is_symlink():
501
505
  raise InstallError(f"owned destination has symlink ancestor: {current}")
502
506
 
503
- def _expected_owned(self, release: Path) -> dict[str, str]:
504
- expected = {str(self.bin_root / "agent-launch"): hashlib.sha256(self._launcher_body(release)).hexdigest(),
507
+ def _expected_owned(self, release: Path, launcher_runtime=None) -> dict[str, str]:
508
+ expected = {str(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch")): hashlib.sha256(self._launcher_body(release, launcher_runtime=launcher_runtime, current=False)).hexdigest(),
505
509
  str(self.launch_root / "profiles.toml"): _sha256(release / "launch" / "agent-launch.toml")}
506
510
  i18n = release / "launch" / "i18n"
507
511
  for source in sorted(i18n.glob("*.toml")):
@@ -509,11 +513,11 @@ class InstructionsInstaller:
509
513
  return expected
510
514
 
511
515
  def _validate_owned_record(self, record: dict[str, Any], release: Path) -> None:
512
- expected = self._expected_owned(release)
516
+ expected = self._expected_owned(release, record.get("launcher_runtime"))
513
517
  launcher = record.get("launcher")
514
518
  if launcher is not None:
515
- if not isinstance(launcher, dict) or launcher.get("path") != str(self.bin_root / "agent-launch") \
516
- or launcher.get("sha256") != expected[str(self.bin_root / "agent-launch")]:
519
+ if not isinstance(launcher, dict) or launcher.get("path") != str(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch")) \
520
+ or launcher.get("sha256") != expected[str(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch"))]:
517
521
  raise InstallError("private install record has an invalid launcher ownership claim")
518
522
  self._safe_owned_path(Path(launcher["path"]), self.bin_root)
519
523
  seen: set[str] = set()
@@ -535,7 +539,7 @@ class InstructionsInstaller:
535
539
  return None
536
540
 
537
541
  def _write_owned(self, path: Path, content: bytes, old: dict[str, Any] | None, mode: int) -> dict[str, str] | None:
538
- root = self.bin_root if path == self.bin_root / "agent-launch" else self.launch_root
542
+ root = self.bin_root if path == self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch") else self.launch_root
539
543
  self._safe_owned_path(path, root)
540
544
  expected = self._owned_hash(old, path)
541
545
  if path.exists() and (path.is_symlink() or (expected is None and _sha256(path) != hashlib.sha256(content).hexdigest())
@@ -544,7 +548,35 @@ class InstructionsInstaller:
544
548
  _atomic_bytes(path, content, mode)
545
549
  return {"path": str(path), "sha256": hashlib.sha256(content).hexdigest()}
546
550
 
547
- def _launcher_body(self, package_root: Path) -> bytes:
551
+ def _current_launcher_runtime(self):
552
+ if not WINDOWS:
553
+ return None
554
+ env = runtime_environment(self.env)
555
+ return {'schema_version': 1, 'python': env.get('AGENT_BIOS_PYTHON_EXECUTABLE', str(Path(sys.executable).resolve())),
556
+ 'environment': env}
557
+
558
+ def _launcher_body(self, package_root: Path, *, launcher_runtime=None, current=True) -> bytes:
559
+ if WINDOWS:
560
+ spec = self._current_launcher_runtime() if current else launcher_runtime
561
+ if spec is None:
562
+ # Exact pre-binding Windows source format, for existing receipts.
563
+ return ('@echo off\r\n"' + sys.executable + '" "' + str(package_root / "compose/native_cli.py") + '" launch %*\r\n').encode("utf-8")
564
+ if (not isinstance(spec, dict) or set(spec) != {'schema_version', 'python', 'environment'}
565
+ or spec['schema_version'] != 1 or not isinstance(spec['python'], str)
566
+ or not Path(spec['python']).is_absolute() or not isinstance(spec['environment'], dict)):
567
+ raise InstallError('invalid recorded launcher runtime')
568
+ env = spec['environment']
569
+ allowed = {'AGENT_BIOS_PYTHON_ENTRY', 'AGENT_BIOS_PYTHON_DEPS', 'AGENT_BIOS_PYTHON_EXECUTABLE'}
570
+ if env and (set(env) != allowed or any(not isinstance(v, str) or not Path(v).is_absolute() for v in env.values())):
571
+ raise InstallError('invalid recorded launcher dependency binding')
572
+ argv = [spec['python']]
573
+ if env:
574
+ argv += ['-I', '-X', 'utf8', env['AGENT_BIOS_PYTHON_ENTRY'], '--dependencies', env['AGENT_BIOS_PYTHON_DEPS'], '--script']
575
+ argv += [str(package_root / 'compose/native_cli.py'), 'launch']
576
+ if any(any(c in value for c in '\r\n\0"') for value in argv):
577
+ raise InstallError('unsafe launcher runtime argument')
578
+ command = ' '.join('"' + value.replace('%', '%%') + '"' for value in argv)
579
+ return ('@echo off\r\nsetlocal DisableDelayedExpansion\r\nchcp 65001 >nul\r\n' + command + ' %*\r\n').encode('utf-8')
548
580
  quoted = shlex.quote(str(package_root))
549
581
  # Verify historical ownership using the exact released launcher vocabulary.
550
582
  canonical = (package_root / "compose/instructions_store.py").is_file()
@@ -564,6 +596,36 @@ class InstructionsInstaller:
564
596
  spec.loader.exec_module(module)
565
597
  return module.ShellIntegration(self.env, self.repo)
566
598
 
599
+ def launcher_reachability(self) -> dict[str, Any]:
600
+ """Report whether the launcher this install deposits can actually be typed.
601
+
602
+ Both answers are known here and invisible in the installed tree: install
603
+ writes the launcher into a directory the caller's PATH may not carry, and
604
+ the shell connection that reaches it for bare host commands is opt-in. A
605
+ completion summary that omits them reports a success the caller cannot use.
606
+ Read-only, and never raises: a summary is not worth failing an install
607
+ that succeeded, and reporting must not become a reason to write.
608
+ """
609
+ launcher = self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch")
610
+ connected: bool | None = None
611
+ if not WINDOWS:
612
+ # zsh is the only shell this connection speaks; elsewhere the question
613
+ # has no answer rather than a negative one.
614
+ try:
615
+ connected = bool(self._shell_manager().status().get("enabled"))
616
+ except (InstallError, OSError, RuntimeError, ValueError):
617
+ connected = False
618
+ try:
619
+ target = os.path.normcase(os.path.realpath(self.bin_root))
620
+ # No expanduser: a shell does not tilde-expand PATH entries when it
621
+ # searches, so a literal ~ there really does not find the command.
622
+ on_path = any(os.path.normcase(os.path.realpath(part)) == target
623
+ for part in self.env.get("PATH", "").split(os.pathsep) if part)
624
+ except (OSError, ValueError):
625
+ on_path = True # Undecidable; do not tell the caller PATH is wrong on a guess.
626
+ return {"path": str(launcher), "directory": str(self.bin_root),
627
+ "on_path": on_path, "shell_connected": connected}
628
+
567
629
  def setup_catalog(self) -> dict[str, Any]:
568
630
  return self._catalog(self.repo)
569
631
 
@@ -650,6 +712,8 @@ class InstructionsInstaller:
650
712
  return result
651
713
 
652
714
  def _shell_paths(self, action: str) -> list[dict[str, Any]]:
715
+ if WINDOWS:
716
+ return []
653
717
  try:
654
718
  manager = self._shell_manager()
655
719
  # A receipt-less managed script may survive an interrupted opt-in.
@@ -736,7 +800,7 @@ class InstructionsInstaller:
736
800
  if not isinstance(baseline_ref, str):
737
801
  raise InstallError("private instructions store candidate has no baseline ref")
738
802
  owned: list[tuple[Path, bytes, int]] = [
739
- (self.bin_root / "agent-launch", self._launcher_body(release), 0o755),
803
+ (self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch"), self._launcher_body(release), 0o755),
740
804
  (self.launch_root / "profiles.toml", (release / "launch" / "agent-launch.toml").read_bytes(), 0o644),
741
805
  ]
742
806
  owned.extend((self.launch_root / "i18n" / source.name, source.read_bytes(), 0o644)
@@ -759,6 +823,8 @@ class InstructionsInstaller:
759
823
  "launcher": launcher, "config_files": config, "created_at": int(time.time()),
760
824
  "mode": "private-session-scoped", "needs_action": [],
761
825
  }
826
+ if WINDOWS:
827
+ record["launcher_runtime"] = self._current_launcher_runtime()
762
828
  if selection_mode is not None:
763
829
  record["selection_mode"] = selection_mode
764
830
  paths = [{"path": str(path), "before": self._file_version(path),
@@ -990,7 +1056,7 @@ class InstructionsInstaller:
990
1056
  local = [self.launch_root / name for name in ("presets.local.toml", "review-methods.local.toml", "launcher.local.toml")]
991
1057
  connections = self.launch_root.parent / "agent-bios"
992
1058
  cleanup = [*local, connections / "ingest-url", self.user_root / "understand" / "state.json"]
993
- owned = [(self.bin_root / "agent-launch", self._launcher_body(release), 0o755),
1059
+ owned = [(self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch"), self._launcher_body(release, launcher_runtime=record.get("launcher_runtime"), current=False), 0o755),
994
1060
  (self.launch_root / "profiles.toml", (release / "launch" / "agent-launch.toml").read_bytes(), 0o644)]
995
1061
  owned.extend((self.launch_root / "i18n" / p.name, p.read_bytes(), 0o644)
996
1062
  for p in sorted((release / "launch" / "i18n").glob("*.toml")))
@@ -1190,7 +1256,7 @@ class InstructionsInstaller:
1190
1256
  ("wrappers/codex-run.sh", self.codex_root / "bin" / "codex-run"),
1191
1257
  ("wrappers/codex-helm.sh", self.codex_root / "bin" / "codex-helm"),
1192
1258
  ("wrappers/claude-run.sh", self.claude_root / "bin" / "claude-run"),
1193
- ("launch/agent-launch.py", self.bin_root / "agent-launch"),
1259
+ ("launch/agent-launch.py", self.bin_root / ("agent-launch.cmd" if WINDOWS else "agent-launch")),
1194
1260
  ("launch/agent-launch.toml", self.launch_root / "profiles.toml"),
1195
1261
  ("launch/agent-launch.zsh", self.launch_root / "shell.zsh"),
1196
1262
  )
@@ -4,6 +4,10 @@ Native homes stay native. The only files this module writes are private activati
4
4
  records and pins; no global instructions, auth, or discovery registration is copied.
5
5
  """
6
6
  from __future__ import annotations
7
+ try:
8
+ from host_platform import sync_directory, cli_argv
9
+ except ImportError:
10
+ from .host_platform import sync_directory, cli_argv
7
11
 
8
12
  import hashlib
9
13
  import json
@@ -144,18 +148,14 @@ def atomic_json(path, data):
144
148
  raise SessionError(f"refusing symlink: {path}")
145
149
  tmp = path.with_name(path.name + '.' + uuid.uuid4().hex + '.tmp')
146
150
  try:
147
- with open(tmp, 'x', encoding='utf-8') as out:
151
+ with open(tmp, 'x', encoding='utf-8', newline='\n') as out:
148
152
  os.chmod(tmp, 0o600)
149
153
  json.dump(data, out, ensure_ascii=False, indent=2)
150
154
  out.write('\n')
151
155
  out.flush()
152
156
  os.fsync(out.fileno())
153
157
  os.replace(tmp, path)
154
- fd = os.open(path.parent, os.O_RDONLY)
155
- try:
156
- os.fsync(fd)
157
- finally:
158
- os.close(fd)
158
+ sync_directory(path.parent)
159
159
  finally:
160
160
  tmp.unlink(missing_ok=True)
161
161
 
@@ -6,6 +6,11 @@ import json
6
6
  import os
7
7
  from pathlib import Path
8
8
  import platform
9
+
10
+ try:
11
+ from host_platform import runtime_environment
12
+ except ImportError:
13
+ from .host_platform import runtime_environment
9
14
  import re
10
15
  import shlex
11
16
  import shutil
@@ -44,7 +49,7 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
44
49
  env["PYTHONDONTWRITEBYTECODE"] = "1"
45
50
  home = Path(env.get("HOME", str(Path.home())))
46
51
  system = system or platform.system()
47
- supported = system in {"Darwin", "Linux"}
52
+ supported = system in {"Darwin", "Linux", "Windows"}
48
53
  paths = {name: which(name, path=env.get("PATH", os.defpath))
49
54
  for name in ("bash", "python3", "node", "npm", "brew", "git", "zsh", "claude", "codex", "cp", "mktemp")}
50
55
 
@@ -76,7 +81,7 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
76
81
  brew_row = add("homebrew", "Homebrew", "optional package manager", "Offers local package installation recipes when available.",
77
82
  path=paths["brew"], argv=[paths["brew"], "--version"] if paths["brew"] else None,
78
83
  reason="Optional: use an existing operating-system package manager or the official host installer.")
79
- brew = paths["brew"] if brew_row["status"] == "available" else None
84
+ brew = paths["brew"] if brew_row["status"] == "available" and system != "Windows" else None
80
85
 
81
86
  def formula(name: str) -> list[str] | None:
82
87
  return [brew, "install", name] if brew else None
@@ -84,7 +89,7 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
84
89
  add("bash", "Bash", "runtime", "Runs the package entry point and bundled shell tools.",
85
90
  path=paths["bash"], argv=[paths["bash"], "--version"] if paths["bash"] else None,
86
91
  action=formula("bash"), scope="Homebrew prefix", reason="Install Bash using your operating system package manager.")
87
- py = paths["python3"]
92
+ py = sys.executable if system == "Windows" else paths["python3"]
88
93
  add("python3", "Python 3.11+", "runtime", "Runs installation, instructions storage, and terminal interfaces.", path=py,
89
94
  argv=[py, "-c", "import sys; print(sys.version.split()[0]); raise SystemExit(sys.version_info < (3,11))"] if py else None,
90
95
  action=formula("python"), scope="Homebrew prefix", reason="Install Python 3.11+ and ensure python3 resolves to it.")
@@ -139,12 +144,16 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
139
144
  code += "; from jsonschema import Draft202012Validator"
140
145
  if managed and name in pins:
141
146
  code += f"; import sys; raise SystemExit(sys.version_info < (3, 11) or actual != {pins[name]!r})"
147
+ binding = runtime_environment(env)
148
+ if binding and name == "jsonschema" and not managed:
149
+ return [interpreter, "-I", "-X", "utf8", binding['AGENT_BIOS_PYTHON_ENTRY'],
150
+ '--dependencies', binding['AGENT_BIOS_PYTHON_DEPS'], '--probe', name]
142
151
  return [interpreter, "-c", code]
143
152
 
144
153
  bootstrap = add("python-venv", "Python venv / pip bootstrap", "managed dependency prerequisite", "Creates the managed environment using AGENT_LAUNCH_PYTHON when set, otherwise python3.", path=bootstrap_python,
145
154
  argv=[bootstrap_python, "-c", "import sys, venv, ensurepip; print('venv; bundled pip ' + ensurepip.version()); raise SystemExit(sys.version_info < (3, 11))"] if bootstrap_python else None,
146
155
  reason="Install venv/ensurepip for Python 3.11+; check AGENT_LAUNCH_PYTHON if configured. An existing managed environment does not need this bootstrap.")
147
- can_provision = bool(paths["bash"] and provisioner.is_file() and (vpy.is_file() or bootstrap["status"] == "available"))
156
+ can_provision = system != "Windows" and bool(paths["bash"] and provisioner.is_file() and (vpy.is_file() or bootstrap["status"] == "available"))
148
157
  try:
149
158
  from .instructions_ui_runtime import runtime_inventory
150
159
  except ImportError:
@@ -181,6 +190,8 @@ def dependency_inventory(repo: Path, environ: dict[str, str] | None = None, *,
181
190
  row = add(identifier, title, "optional job / personal integration", purpose, present=False,
182
191
  reason="Configure this only for a workflow that requires it; setup cannot choose your job environment or account.")
183
192
  row["status"] = "not assessed"
193
+ if system == "Windows":
194
+ rows = [row for row in rows if row["id"] not in {"homebrew", "bash", "zsh", "cp", "mktemp", "python-bootstrap", "python-venv"}]
184
195
  return rows
185
196
 
186
197
 
@@ -226,6 +237,25 @@ def format_setup_result(result: dict[str, Any], language: str = "en", *, interfa
226
237
  def recovery_hint() -> str:
227
238
  return t("Use the returned review_id with agent-bios setup status or agent-bios setup resume before continuing.")
228
239
 
240
+ def reachability_lines() -> list[str]:
241
+ """Name the two ways a finished install still cannot be reached.
242
+
243
+ install() leaves a launcher in a directory the caller's PATH may not
244
+ carry, and the shell connection that reaches it for bare host commands is
245
+ opt-in. Neither is visible in the installed tree, so a summary that omits
246
+ them reports a success the caller cannot use. Absent facts say nothing.
247
+ """
248
+ reach = result.get("launcher") or {}
249
+ lines: list[str] = []
250
+ if reach.get("on_path") is False:
251
+ lines.append(t("The launcher is installed at {path}, but {directory} is not on this shell's PATH.",
252
+ path=str(reach.get("path")), directory=str(reach.get("directory"))))
253
+ lines.append(t("Add {directory} to the PATH in your shell startup file, or run the launcher by its full path.",
254
+ directory=str(reach.get("directory"))))
255
+ if reach.get("shell_connected") is False:
256
+ lines.append(t("Typing claude or codex alone does not open the launcher: the optional shell connection is off. Turn it on with agent-bios shell restore."))
257
+ return lines
258
+
229
259
  if result.get("cancelled"):
230
260
  if result.get("cancelled_after_start"):
231
261
  lines = [t("Setup stopped after the current operation finished.")]
@@ -262,6 +292,7 @@ def format_setup_result(result: dict[str, Any], language: str = "en", *, interfa
262
292
  lines.append(t("The app command registration is retained; instruction capture did not complete."))
263
293
  if completed:
264
294
  lines.append(t("Dependencies installed: {dependencies}.", dependencies=", ".join(completed)))
295
+ lines.extend(reachability_lines())
265
296
  lines.append(recovery_hint() if interface == "conversation" else t("Open Instructions Studio: agent-bios instructions (terminal or Codex app terminal panel)."))
266
297
  return "\n".join(lines)
267
298
  if not result.get("applied"):
@@ -301,6 +332,7 @@ def format_setup_result(result: dict[str, Any], language: str = "en", *, interfa
301
332
  if request == f"Use $agent-bios to import capture {captured['capture_id']}":
302
333
  request = t("Use $agent-bios to import capture {capture_id}", capture_id=captured["capture_id"])
303
334
  lines.append(t("In Codex: {request}", request=request))
335
+ lines.extend(reachability_lines())
304
336
  lines.append(t("Continue in the conversation using the returned verified entrypoint.") if interface == "conversation"
305
337
  else t("Open Instructions Studio: agent-bios instructions (terminal or Codex app terminal panel)."))
306
338
  return "\n".join(lines)
@@ -516,6 +548,11 @@ class SetupController:
516
548
  except (OSError, RuntimeError, ValueError) as exc:
517
549
  result["installation_error"] = str(exc)
518
550
  return result
551
+ # Read once the installation is durable, so the extras failure path reports
552
+ # the same reachability as the clean one: both leave a launcher behind.
553
+ reachability = getattr(self.installer, "launcher_reachability", None)
554
+ if callable(reachability):
555
+ result["launcher"] = reachability()
519
556
  if cancelled():
520
557
  return result
521
558
  try:
@@ -20,6 +20,11 @@ from typing import Any, Callable
20
20
  if __name__ == "__main__":
21
21
  sys.dont_write_bytecode = True
22
22
 
23
+ try:
24
+ from host_platform import sync_directory, cli_argv, python_argv, runtime_environment
25
+ except ImportError:
26
+ from .host_platform import sync_directory, cli_argv, python_argv, runtime_environment
27
+
23
28
  try:
24
29
  from instructions_install import InstructionsInstaller
25
30
  from instructions_setup import SetupController, SetupError, format_setup_result, review_summary
@@ -91,6 +96,24 @@ def _identity() -> dict[str, Any]:
91
96
 
92
97
 
93
98
  def _process_identity(pid: int) -> str | None:
99
+ if os.name == "nt":
100
+ import ctypes
101
+ from ctypes import wintypes
102
+ kernel = ctypes.WinDLL("kernel32", use_last_error=True)
103
+ kernel.OpenProcess.restype = wintypes.HANDLE
104
+ kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
105
+ kernel.GetProcessTimes.argtypes = [wintypes.HANDLE] + [ctypes.POINTER(wintypes.FILETIME)] * 4
106
+ kernel.CloseHandle.argtypes = [wintypes.HANDLE]
107
+ handle = kernel.OpenProcess(0x1000, False, pid)
108
+ if not handle:
109
+ return None
110
+ try:
111
+ times = [wintypes.FILETIME() for _ in range(4)]
112
+ if not kernel.GetProcessTimes(handle, *(ctypes.byref(t) for t in times)):
113
+ return None
114
+ return str((times[0].dwHighDateTime << 32) | times[0].dwLowDateTime)
115
+ finally:
116
+ kernel.CloseHandle(handle)
94
117
  if sys.platform.startswith("linux"):
95
118
  try:
96
119
  fields = (Path("/proc") / str(pid) / "stat").read_text().rsplit(")", 1)[1].split()
@@ -131,11 +154,7 @@ def _write_json(path: Path, value: dict[str, Any]) -> None:
131
154
  os.fsync(output.fileno())
132
155
  temporary.chmod(0o600)
133
156
  os.replace(temporary, path)
134
- directory = os.open(path.parent, os.O_RDONLY)
135
- try:
136
- os.fsync(directory)
137
- finally:
138
- os.close(directory)
157
+ sync_directory(path.parent)
139
158
  finally:
140
159
  temporary.unlink(missing_ok=True)
141
160
 
@@ -191,7 +210,7 @@ class SetupService:
191
210
 
192
211
  def start(self, language: str | None = None) -> dict[str, Any]:
193
212
  package = Path(self.installer.repo).resolve()
194
- cli = ["/bin/bash", str(package / "install.sh")]
213
+ cli = cli_argv(package)
195
214
  return {"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-start",
196
215
  "languages": [{"label": label, "id": identifier} for label, identifier in LANGUAGES],
197
216
  "suggested_language": detect_language(self.installer.env), "language": self._language(language),
@@ -302,6 +321,7 @@ class SetupService:
302
321
  "AGENT_BIOS_INSTRUCTIONS_DIR": context["user_dir"], "CODEX_HOME": context["codex_dir"],
303
322
  "AGENT_BIOS_CORPUS_DIR": context["user_dir"],
304
323
  "CLAUDE_CONFIG_DIR": context["claude_dir"]}, "needs_action": []}
324
+ result["environment"].update(runtime_environment())
305
325
  try:
306
326
  with try_transaction_lock(Path(context["state_dir"])) as acquired:
307
327
  if not acquired:
@@ -312,7 +332,8 @@ class SetupService:
312
332
  return self._verified_handoff(context, result)
313
333
  except (OSError, RuntimeError, ValueError) as exc:
314
334
  result["needs_action"].append(str(exc))
315
- return result
335
+ result["environment"].update(runtime_environment())
336
+ return result
316
337
 
317
338
  def _verified_handoff(self, context: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
318
339
  record = Path(context["state_dir"]) / "runtime/private-install.json"
@@ -322,8 +343,8 @@ class SetupService:
322
343
  reject_symlink_ancestors(record)
323
344
  release = confirmed_release(Path(context["state_dir"]))
324
345
  result.update(package_verified=True, package_root=str(release), release_digest=release.name,
325
- cli_argv=["/bin/bash", str(release / "install.sh")],
326
- setup_argv=["/bin/bash", str(release / "install.sh"), "setup"], guide_path=str(release / "compose/setup/START.md"))
346
+ cli_argv=cli_argv(release),
347
+ setup_argv=cli_argv(release, "setup"), guide_path=str(release / "compose/setup/START.md"))
327
348
  result["environment"].update(AGENT_BIOS_PACKAGE_ROOT=str(release), AGENT_BIOS_PRIVATE_INSTRUCTIONS="1", AGENT_BIOS_PRIVATE_CORPUS="1", AGENT_BIOS_LEGACY_INSTALL="0")
328
349
  except (OSError, RuntimeError, ValueError) as exc:
329
350
  result["needs_action"].append(str(exc))
@@ -348,7 +369,7 @@ class SetupService:
348
369
  result["needs_action"].extend(bridge.get("needs_action", []))
349
370
  if bridge.get("registered") and not bridge.get("needs_action"):
350
371
  helper = manager.target / "scripts/bridge.py"
351
- result.update(helper_verified=True, helper_argv=[sys.executable, str(helper)])
372
+ result.update(helper_verified=True, helper_argv=python_argv(helper))
352
373
  if "AGENT_LAUNCH_VENV" in self.installer.env:
353
374
  result["environment"]["AGENT_LAUNCH_VENV"] = self.installer.env["AGENT_LAUNCH_VENV"]
354
375
  except (OSError, RuntimeError, ValueError) as exc:
@@ -172,6 +172,9 @@ MESSAGES: dict[str, tuple[str, str]] = {
172
172
  "Open Instructions Studio: agent-bios instructions (terminal or Codex app terminal panel).": ("Instructions Studio 열기: 터미널 또는 Codex 앱 터미널 패널에서 agent-bios instructions를 실행하세요.", "Instructions Studioを開くには、ターミナルまたはCodexアプリのターミナルパネルでagent-bios instructionsを実行してください。"),
173
173
  "Setup did not complete.": ("설정을 완료하지 못했습니다.", "セットアップを完了できませんでした。"),
174
174
  "Setup complete. Private runtime installed.": ("설정을 완료하고 개인 실행 환경을 설치했습니다.", "セットアップが完了し、専用の実行環境をインストールしました。"),
175
+ "The launcher is installed at {path}, but {directory} is not on this shell's PATH.": ("실행기를 {path}에 설치했지만, 현재 셸의 PATH에 {directory} 경로가 없습니다.", "ランチャーを{path}にインストールしましたが、{directory}は現在のシェルのPATHにありません。"),
176
+ "Add {directory} to the PATH in your shell startup file, or run the launcher by its full path.": ("셸 시작 파일의 PATH에 {directory} 경로를 추가하거나, 실행기를 전체 경로로 실행하세요.", "シェルの起動ファイルのPATHに{directory}を追加するか、ランチャーをフルパスで実行してください。"),
177
+ "Typing claude or codex alone does not open the launcher: the optional shell connection is off. Turn it on with agent-bios shell restore.": ("claude 또는 codex만 입력해도 실행기가 열리지 않습니다. 선택적 셸 연결이 꺼져 있습니다. agent-bios shell restore로 켜세요.", "claudeまたはcodexだけを入力してもランチャーは開きません。任意のシェル連携が無効です。agent-bios shell restoreで有効にしてください。"),
175
178
  "Instructions for future activated sessions: none.": ("앞으로 활성화하는 세션에서는 지침을 사용하지 않습니다.", "今後有効にするセッションでは指示を使用しません。"),
176
179
  "Instructions for future activated sessions: {instructions}.": ("앞으로 활성화하는 세션에서 사용할 지침: {instructions}.", "今後有効にするセッションで使用する指示:{instructions}。"),
177
180
  "Instructions policy: core, infrastructure and personal instructions.": ("지침 정책: 핵심·기반·개인 지침을 사용합니다.", "指示ポリシー:コア・基盤・個人指示を使用します。"),
@@ -6,6 +6,10 @@ does not alter a host configuration or claim that a snapshot was loaded by a
6
6
  host; that is the launch adapter's job.
7
7
  """
8
8
  from __future__ import annotations
9
+ try:
10
+ from host_platform import sync_directory, cli_argv
11
+ except ImportError:
12
+ from .host_platform import sync_directory, cli_argv
9
13
 
10
14
  import copy
11
15
  import contextlib
@@ -126,11 +130,7 @@ def _atomic_write(path: Path, value: Any) -> None:
126
130
  tmp.chmod(path.stat().st_mode & 0o777)
127
131
  os.replace(tmp, path)
128
132
  # Persist the directory entry as well as the file bytes.
129
- directory = os.open(path.parent, os.O_RDONLY)
130
- try:
131
- os.fsync(directory)
132
- finally:
133
- os.close(directory)
133
+ sync_directory(path.parent)
134
134
  finally:
135
135
  with contextlib.suppress(FileNotFoundError):
136
136
  tmp.unlink()
@@ -151,7 +151,7 @@ def _rewrite_staged_paths(staging: Path, destination: Path) -> None:
151
151
  except UnicodeDecodeError as exc:
152
152
  raise InstructionsStoreError(f"snapshot compiler emitted non-text member: {path}") from exc
153
153
  if old in text:
154
- path.write_text(text.replace(old, new), encoding="utf-8")
154
+ path.write_text(text.replace(old, new), encoding="utf-8", newline="\n")
155
155
 
156
156
 
157
157
  def _snapshot_relative_paths(files: Any) -> list[str]:
@@ -9,7 +9,10 @@ from __future__ import annotations
9
9
 
10
10
  import contextlib
11
11
  import base64
12
- import fcntl
12
+ try:
13
+ from host_platform import file_locks as fcntl, redirected
14
+ except ImportError:
15
+ from .host_platform import file_locks as fcntl, redirected
13
16
  import hashlib
14
17
  import json
15
18
  import os
@@ -57,7 +60,7 @@ def reject_symlink_ancestors(path: Path) -> None:
57
60
  a same-named link elsewhere, or a user-owned link.
58
61
  """
59
62
  for ancestor in (path, *path.parents):
60
- if not ancestor.is_symlink():
63
+ if not redirected(ancestor):
61
64
  continue
62
65
  system_alias = (sys.platform == "darwin" and ancestor != path
63
66
  and ancestor in {Path("/var"), Path("/tmp"), Path("/etc")}
@@ -82,10 +85,9 @@ def _depths(name: str) -> dict[str, int]:
82
85
  @contextlib.contextmanager
83
86
  def _transaction_lock(state_root: Path, *, readonly: bool) -> Iterator[bool]:
84
87
  root = Path(state_root).expanduser()
85
- if root.is_symlink():
88
+ if redirected(root):
86
89
  raise TransactionError(f"unsafe instructions state root: {root}")
87
- if readonly:
88
- reject_symlink_ancestors(root)
90
+ reject_symlink_ancestors(root)
89
91
  key = _key(root)
90
92
  depths = _depths("lock_depths")
91
93
  if depths.get(key, 0):
@@ -101,7 +103,7 @@ def _transaction_lock(state_root: Path, *, readonly: bool) -> Iterator[bool]:
101
103
  lock = root / ".corpus-store.lock"
102
104
  if lock.is_symlink():
103
105
  raise TransactionError(f"unsafe instructions transaction lock: {lock}")
104
- flags = (os.O_RDONLY | os.O_NONBLOCK if readonly else os.O_CREAT | os.O_RDWR) | getattr(os, "O_NOFOLLOW", 0)
106
+ flags = ((os.O_RDWR if os.name == "nt" else os.O_RDONLY) | getattr(os, "O_NONBLOCK", 0) if readonly else os.O_CREAT | os.O_RDWR) | getattr(os, "O_NOFOLLOW", 0)
105
107
  try:
106
108
  descriptor = os.open(lock, flags, 0o600)
107
109
  except FileNotFoundError:
@@ -7,6 +7,7 @@ import hashlib
7
7
  import importlib
8
8
  import io
9
9
  import json
10
+ import os
10
11
  from pathlib import Path, PurePosixPath
11
12
  import re
12
13
  import stat
@@ -255,7 +256,7 @@ def activate_ui_runtime(repo: Path) -> Path:
255
256
  return active
256
257
  parent = Path(tempfile.gettempdir()).resolve()
257
258
  home = Path.home().resolve()
258
- if parent == home or home in parent.parents:
259
+ if os.name != "nt" and (parent == home or home in parent.parents):
259
260
  parent = Path("/tmp").resolve()
260
261
  temporary = tempfile.TemporaryDirectory(prefix="agent-bios-ui-", dir=parent)
261
262
  target = Path(temporary.name)
@@ -0,0 +1,52 @@
1
+ """Bash-free entrypoint for the installed Windows application and direct Python use."""
2
+ from __future__ import annotations
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import runpy
7
+ import sys
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+ sys.path.insert(0, str(ROOT / 'compose'))
11
+ sys.dont_write_bytecode = True
12
+
13
+
14
+ def main(args=None):
15
+ args = list(sys.argv[1:] if args is None else args)
16
+ os.environ.setdefault('HOME', str(Path.home()))
17
+ os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
18
+ if args and args[0] in ('--version', '-V'):
19
+ print(json.loads((ROOT / 'package.json').read_text(encoding='utf-8'))['version'])
20
+ return 0
21
+ command = args.pop(0) if args else ('instructions' if (Path(os.environ.get('AGENT_BIOS_STATE_DIR', str(Path.home()/'.local/share/agent-bios'))) / 'runtime/private-install.json').exists() else 'install')
22
+ if command in ('help', '--help', '-h'):
23
+ print('agent-bios: install | instructions | setup | app | import | learn | understand | launch | verify | status | uninstall | reset')
24
+ return 0
25
+ modules = {'instructions':'instructions', 'corpus':'instructions', 'setup':'instructions_setup_cli',
26
+ 'app':'instructions_app', 'import':'instructions_import', 'understand':'instructions_understand'}
27
+ if command in ('install', 'onboard', 'verify', 'status', 'uninstall', 'reset'):
28
+ import instructions_install
29
+ return instructions_install.main(['--repo', str(ROOT), command, *args])
30
+ if command in modules:
31
+ import importlib
32
+ module = importlib.import_module(modules[command])
33
+ if command in ('instructions', 'corpus'):
34
+ return module.main(['--repo', str(ROOT), *args], bundled_ui=True)
35
+ return module.main(['--repo', str(ROOT), *args])
36
+ if command == 'launch':
37
+ os.environ['AGENT_BIOS_PACKAGE_ROOT'] = str(ROOT)
38
+ os.environ['AGENT_BIOS_PRIVATE_INSTRUCTIONS'] = '1'
39
+ os.environ.pop('AGENT_BIOS_PRIVATE_CORPUS', None)
40
+ sys.argv = [str(ROOT / 'launch/agent-launch.py'), *args]
41
+ runpy.run_path(sys.argv[0], run_name='__main__')
42
+ return 0
43
+ if command == 'learn':
44
+ sys.argv = [str(ROOT / 'learn/collect-learning.py'), *args]
45
+ runpy.run_path(sys.argv[0], run_name='__main__')
46
+ return 0
47
+ print(f'Unknown or unsupported native command: {command}', file=sys.stderr)
48
+ return 2
49
+
50
+
51
+ if __name__ == '__main__':
52
+ raise SystemExit(main())
@@ -0,0 +1,58 @@
1
+ """Application dependency entry for ordinary and isolated embeddable Python."""
2
+ from __future__ import annotations
3
+ import argparse
4
+ import importlib
5
+ import importlib.metadata
6
+ import os
7
+ from pathlib import Path
8
+ import runpy
9
+ import sys
10
+
11
+
12
+ def main(argv=None):
13
+ # A Windows PowerShell 5.1 caller may prefix piped input with a UTF-8 BOM.
14
+ for stream, encoding in ((sys.stdin, "utf-8-sig"), (sys.stdout, "utf-8"), (sys.stderr, "utf-8")):
15
+ if hasattr(stream, "reconfigure"):
16
+ stream.reconfigure(encoding=encoding, errors="strict")
17
+ argv = list(sys.argv[1:] if argv is None else argv)
18
+ parser = argparse.ArgumentParser(description=__doc__)
19
+ parser.add_argument('--dependencies', required=True, type=Path)
20
+ parser.add_argument('--probe', choices=['jsonschema'])
21
+ if '--script' in argv:
22
+ index = argv.index('--script')
23
+ if len(argv) <= index + 1:
24
+ parser.error('--script requires a file')
25
+ args = parser.parse_args(argv[:index])
26
+ script, tail = Path(argv[index+1]), argv[index+2:]
27
+ else:
28
+ args = parser.parse_args(argv)
29
+ script, tail = None, []
30
+ dependencies = args.dependencies.resolve(strict=True)
31
+ if not dependencies.is_dir():
32
+ parser.error('dependencies must be a directory')
33
+ sys.dont_write_bytecode = True
34
+ sys.path.insert(0, str(dependencies))
35
+ os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
36
+ os.environ['AGENT_BIOS_PYTHON_EXECUTABLE'] = str(Path(sys.executable).resolve())
37
+ os.environ['AGENT_BIOS_PYTHON_ENTRY'] = str(Path(__file__).resolve())
38
+ os.environ['AGENT_BIOS_PYTHON_DEPS'] = str(dependencies)
39
+ if args.probe:
40
+ module = importlib.import_module(args.probe)
41
+ getattr(module, 'Draft202012Validator')
42
+ print(importlib.metadata.version(args.probe))
43
+ return 0
44
+ if script is None:
45
+ parser.error('a script or probe is required')
46
+ script = script.resolve(strict=True)
47
+ if not script.is_file() or script.suffix != '.py':
48
+ parser.error('script must be a Python file')
49
+ # Isolated Python does not add the script directory to sys.path. Do this only
50
+ # for the explicitly selected application/helper, not a user's working directory.
51
+ sys.path.insert(0, str(script.parent))
52
+ sys.argv = [str(script), *tail]
53
+ runpy.run_path(str(script), run_name='__main__')
54
+ return 0
55
+
56
+
57
+ if __name__ == '__main__':
58
+ raise SystemExit(main())