silicon-cli 1.0.22__tar.gz → 1.0.24__tar.gz

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 (76) hide show
  1. {silicon_cli-1.0.22/silicon_cli.egg-info → silicon_cli-1.0.24}/PKG-INFO +12 -3
  2. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/README.md +11 -2
  3. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/docker/runtime/Dockerfile +15 -4
  4. silicon_cli-1.0.24/docker/runtime/activate-interface-cli.py +207 -0
  5. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/docker/runtime/runtime-entrypoint.sh +65 -13
  6. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/pyproject.toml +1 -1
  7. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/config.py +14 -3
  8. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/docker_runtime.py +292 -15
  9. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/interface_cli.py +146 -26
  10. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/package_manager.py +176 -13
  11. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/process.py +138 -2
  12. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/runtime_contract.py +3 -1
  13. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/update.py +85 -10
  14. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/engine.py +51 -4
  15. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/policy.py +4 -0
  16. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/snapshot_adapter.py +9 -3
  17. {silicon_cli-1.0.22 → silicon_cli-1.0.24/silicon_cli.egg-info}/PKG-INFO +12 -3
  18. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/SOURCES.txt +2 -0
  19. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_docker_runtime.py +373 -4
  20. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_docker_update_hooks.py +176 -2
  21. silicon_cli-1.0.24/tests/test_interface_activation.py +205 -0
  22. silicon_cli-1.0.24/tests/test_package_manager.py +515 -0
  23. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_process_health.py +303 -1
  24. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_runtime_contract.py +4 -0
  25. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_snapshot_adapter.py +25 -0
  26. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_transactional_updater.py +145 -1
  27. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_update.py +77 -3
  28. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/uv.lock +1 -1
  29. silicon_cli-1.0.22/tests/test_package_manager.py +0 -192
  30. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/.github/workflows/publish-runtime.yml +0 -0
  31. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/LICENSE +0 -0
  32. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/MANIFEST.in +0 -0
  33. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/docker/runtime/Dockerfile.local +0 -0
  34. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/setup.cfg +0 -0
  35. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/__init__.py +0 -0
  36. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/backup_runtime.py +0 -0
  37. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/cli.py +0 -0
  38. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/glassagent.py +0 -0
  39. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/host_lock.py +0 -0
  40. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/http_transport.py +0 -0
  41. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/pull_transaction.py +0 -0
  42. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/registry.py +0 -0
  43. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/stemcell.py +0 -0
  44. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/sync.py +0 -0
  45. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/ui.py +0 -0
  46. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/__init__.py +0 -0
  47. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/cache.py +0 -0
  48. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/channel.py +0 -0
  49. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/checkpoint.py +0 -0
  50. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/fleet.py +0 -0
  51. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/generation.py +0 -0
  52. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/io.py +5 -5
  53. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/journal.py +0 -0
  54. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/lock.py +0 -0
  55. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/maintenance.py +0 -0
  56. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/overlay.py +0 -0
  57. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/planner.py +0 -0
  58. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/release.py +0 -0
  59. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/retention.py +0 -0
  60. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/dependency_links.txt +0 -0
  61. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/entry_points.txt +0 -0
  62. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/requires.txt +0 -0
  63. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/top_level.txt +0 -0
  64. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_backup_runtime.py +0 -0
  65. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_browser_profile.py +0 -0
  66. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_extend_installation.py +0 -0
  67. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_fleet_update_journal.py +0 -0
  68. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_git_release_source.py +0 -0
  69. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_glassagent_process.py +0 -0
  70. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_http_transport.py +0 -0
  71. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_pull_args.py +0 -0
  72. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_pull_transaction.py +0 -0
  73. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_stemcell_hydration.py +0 -0
  74. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_sync_credentials_backup.py +0 -0
  75. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_update_maintenance.py +0 -0
  76. {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_update_overlay_retention.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: silicon-cli
3
- Version: 1.0.22
3
+ Version: 1.0.24
4
4
  Summary: Silicon CLI — create, run, and manage your silicon instances.
5
5
  Author: Saket
6
6
  License-Expression: MIT
@@ -97,8 +97,8 @@ and durable update jobs.
97
97
  | `GLASS_SERVER_URL` | `https://glass.teamofsilicons.com` | Glass sync server (pull/push) |
98
98
  | `SILICON_STEMCELL_REPO` | `teamofsilicons/silicon-stemcell` | GitHub repository used by `new`, `pull`, and `update` |
99
99
  | `SILICON_PYTHON` | `python3` | interpreter used to run a silicon's `main.py` |
100
- | `SILICON_INTERFACE_CLI_PACKAGE` | `@teamofsilicons/silicon-interface-cli` | npm package used to install the Silicon Interface CLI |
101
- | `SILICON_INTERFACE_CLI_TARBALL` | versioned npm tarball | fallback package URL if registry metadata is briefly unavailable |
100
+ | `SILICON_INTERFACE_CLI_PACKAGE` | immutable Interface CLI release asset | package spec used to install the Silicon Interface CLI |
101
+ | `SILICON_INTERFACE_CLI_TARBALL` | same immutable release asset | fallback package spec for an explicitly overridden primary source |
102
102
  | `SILICON_INTERFACE_CLI_SOURCE` | *(empty)* | local package dir or `silicon-interface.mjs` path for dev installs |
103
103
  | `SILICON_INTERFACE_CLI_SKIP` | *(empty)* | set to `1` to skip interface CLI setup |
104
104
  | `SILICON_INTERFACE_DAEMON_SKIP` | *(empty)* | set to `1` to install the CLI without starting its listener daemon |
@@ -301,6 +301,15 @@ The local wrappers are written to:
301
301
 
302
302
  For Glass-pulled silicons, those wrappers automatically use the folder's
303
303
  `.glass.json` (`server_url` + `api_key`) for conversation API auth.
304
+ Docker startup atomically rewrites these two small per-instance launchers on
305
+ every boot. They export the instance root and execute the absolute Interface
306
+ CLI bundled in the selected runtime image; the package itself is never copied
307
+ over durable inbox or cursor state. Selecting an older runtime image therefore
308
+ selects that image's Interface CLI as well, including the first rollback to
309
+ the immediately preceding pre-fix image because both images expose the same
310
+ absolute runtime command. Images older than the Interface runtime contract,
311
+ which do not contain `/usr/local/bin/silicon-interface`, require a supported
312
+ intermediate release rather than direct rollback.
304
313
 
305
314
  During local development, point `SILICON_INTERFACE_CLI_SOURCE` at the package:
306
315
 
@@ -79,8 +79,8 @@ and durable update jobs.
79
79
  | `GLASS_SERVER_URL` | `https://glass.teamofsilicons.com` | Glass sync server (pull/push) |
80
80
  | `SILICON_STEMCELL_REPO` | `teamofsilicons/silicon-stemcell` | GitHub repository used by `new`, `pull`, and `update` |
81
81
  | `SILICON_PYTHON` | `python3` | interpreter used to run a silicon's `main.py` |
82
- | `SILICON_INTERFACE_CLI_PACKAGE` | `@teamofsilicons/silicon-interface-cli` | npm package used to install the Silicon Interface CLI |
83
- | `SILICON_INTERFACE_CLI_TARBALL` | versioned npm tarball | fallback package URL if registry metadata is briefly unavailable |
82
+ | `SILICON_INTERFACE_CLI_PACKAGE` | immutable Interface CLI release asset | package spec used to install the Silicon Interface CLI |
83
+ | `SILICON_INTERFACE_CLI_TARBALL` | same immutable release asset | fallback package spec for an explicitly overridden primary source |
84
84
  | `SILICON_INTERFACE_CLI_SOURCE` | *(empty)* | local package dir or `silicon-interface.mjs` path for dev installs |
85
85
  | `SILICON_INTERFACE_CLI_SKIP` | *(empty)* | set to `1` to skip interface CLI setup |
86
86
  | `SILICON_INTERFACE_DAEMON_SKIP` | *(empty)* | set to `1` to install the CLI without starting its listener daemon |
@@ -283,6 +283,15 @@ The local wrappers are written to:
283
283
 
284
284
  For Glass-pulled silicons, those wrappers automatically use the folder's
285
285
  `.glass.json` (`server_url` + `api_key`) for conversation API auth.
286
+ Docker startup atomically rewrites these two small per-instance launchers on
287
+ every boot. They export the instance root and execute the absolute Interface
288
+ CLI bundled in the selected runtime image; the package itself is never copied
289
+ over durable inbox or cursor state. Selecting an older runtime image therefore
290
+ selects that image's Interface CLI as well, including the first rollback to
291
+ the immediately preceding pre-fix image because both images expose the same
292
+ absolute runtime command. Images older than the Interface runtime contract,
293
+ which do not contain `/usr/local/bin/silicon-interface`, require a supported
294
+ intermediate release rather than direct rollback.
286
295
 
287
296
  During local development, point `SILICON_INTERFACE_CLI_SOURCE` at the package:
288
297
 
@@ -4,7 +4,8 @@ FROM node:22-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca440
4
4
  ARG SILICON_CLI_SPEC=silicon-cli==1.0.22
5
5
  ARG SILICON_BROWSER_SPEC=silicon-browser==1.0.8
6
6
  ARG SILICON_EXTEND_SPEC=silicon-extend==0.1.1
7
- ARG SILICON_INTERFACE_CLI_SPEC=@teamofsilicons/silicon-interface-cli@2.0.1
7
+ ARG SILICON_INTERFACE_CLI_URL=https://github.com/teamofsilicons/silicon-interface-web/releases/download/interface-cli-v2.0.2/teamofsilicons-silicon-interface-cli-2.0.2.tgz
8
+ ARG SILICON_INTERFACE_CLI_SHA256=5f594958e8165dfaf87e19a71781a628012b5debe0482dcdc24f28b308e710b2
8
9
  ARG CLAUDE_CODE_SPEC=@anthropic-ai/claude-code@2.1.220
9
10
  ARG CODEX_SPEC=@openai/codex@0.145.0
10
11
  ARG PIP_SPEC=pip==26.1.2
@@ -47,13 +48,23 @@ RUN python3 -m venv /opt/silicon-runtime \
47
48
  "${SILICON_EXTEND_SPEC}" \
48
49
  && /opt/silicon-runtime/bin/pip check
49
50
 
50
- RUN npm install -g --no-audit --no-fund \
51
- "${SILICON_INTERFACE_CLI_SPEC}" \
51
+ RUN curl --fail --location --silent --show-error \
52
+ --output /tmp/silicon-interface-cli.tgz \
53
+ "${SILICON_INTERFACE_CLI_URL}" \
54
+ && echo "${SILICON_INTERFACE_CLI_SHA256} /tmp/silicon-interface-cli.tgz" \
55
+ | sha256sum --check --strict \
56
+ && npm install -g --no-audit --no-fund \
57
+ /tmp/silicon-interface-cli.tgz \
58
+ && rm -f /tmp/silicon-interface-cli.tgz \
59
+ && npm install -g --no-audit --no-fund \
52
60
  "${CLAUDE_CODE_SPEC}" \
53
61
  "${CODEX_SPEC}"
54
62
 
63
+ COPY docker/runtime/activate-interface-cli.py /usr/local/libexec/silicon-activate-interface-cli.py
55
64
  COPY docker/runtime/runtime-entrypoint.sh /usr/local/bin/silicon-runtime-entrypoint
56
- RUN chmod +x /usr/local/bin/silicon-runtime-entrypoint
65
+ RUN chmod 0555 \
66
+ /usr/local/libexec/silicon-activate-interface-cli.py \
67
+ /usr/local/bin/silicon-runtime-entrypoint
57
68
 
58
69
  WORKDIR /silicon
59
70
  ENTRYPOINT ["tini", "--", "/usr/local/bin/silicon-runtime-entrypoint"]
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """Atomically activate an image-owned Interface CLI for one Silicon.
3
+
4
+ Docker runtime images already contain the exact, checksum-verified Interface
5
+ package selected by the Stemcell release. Persisting another package copy
6
+ under ``.silicon-interface`` makes upgrades non-atomic and prevents an image
7
+ rollback from selecting its own older CLI. Instead, this helper atomically
8
+ writes per-instance launchers that export the durable instance root and execute
9
+ the selected image's absolute CLI path.
10
+
11
+ The launcher path is stable across runtime generations. Once an instance has
12
+ booted one fixed image, even a rollback to the immediately preceding pre-fix
13
+ image selects that image's bundled CLI without requiring the old entrypoint to
14
+ understand the new activation scheme.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import fcntl
21
+ import json
22
+ import os
23
+ import re
24
+ import stat
25
+ import subprocess
26
+ import sys
27
+ import tempfile
28
+ from pathlib import Path
29
+
30
+
31
+ VERSION_RE = re.compile(
32
+ r"(?<!\d)(\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9_.-]+)?)(?!\d)"
33
+ )
34
+ COMMAND_TIMEOUT_SECONDS = 30
35
+
36
+
37
+ class ActivationError(RuntimeError):
38
+ """The selected runtime CLI could not be activated safely."""
39
+
40
+
41
+ def _fsync_directory(path: Path) -> None:
42
+ flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
43
+ descriptor = os.open(path, flags)
44
+ try:
45
+ os.fsync(descriptor)
46
+ finally:
47
+ os.close(descriptor)
48
+
49
+
50
+ def _ensure_real_directory(path: Path, *, mode: int = 0o700) -> None:
51
+ try:
52
+ metadata = path.lstat()
53
+ except FileNotFoundError:
54
+ path.mkdir(mode=mode)
55
+ _fsync_directory(path.parent)
56
+ return
57
+ if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
58
+ raise ActivationError(f"unsafe Interface activation directory: {path}")
59
+
60
+
61
+ def _atomic_write(path: Path, payload: bytes, *, mode: int) -> None:
62
+ descriptor, temporary = tempfile.mkstemp(
63
+ prefix=f".{path.name}.",
64
+ dir=path.parent,
65
+ )
66
+ temporary_path = Path(temporary)
67
+ try:
68
+ os.fchmod(descriptor, mode)
69
+ with os.fdopen(descriptor, "wb", closefd=True) as output:
70
+ descriptor = -1
71
+ output.write(payload)
72
+ output.flush()
73
+ os.fsync(output.fileno())
74
+ os.replace(temporary_path, path)
75
+ _fsync_directory(path.parent)
76
+ finally:
77
+ if descriptor >= 0:
78
+ os.close(descriptor)
79
+ temporary_path.unlink(missing_ok=True)
80
+
81
+
82
+ def _version(command: Path, *, cwd: Path) -> str:
83
+ try:
84
+ result = subprocess.run(
85
+ [str(command), "--version"],
86
+ cwd=cwd,
87
+ capture_output=True,
88
+ text=True,
89
+ check=False,
90
+ timeout=COMMAND_TIMEOUT_SECONDS,
91
+ )
92
+ except (OSError, subprocess.SubprocessError) as exc:
93
+ raise ActivationError(
94
+ f"could not execute Silicon Interface CLI: {exc}"
95
+ ) from exc
96
+ output = "\n".join(
97
+ value.strip()
98
+ for value in (result.stdout, result.stderr)
99
+ if value.strip()
100
+ )
101
+ match = VERSION_RE.search(output)
102
+ if result.returncode != 0 or match is None:
103
+ detail = output or f"exited with {result.returncode}"
104
+ raise ActivationError(f"invalid Silicon Interface CLI: {detail}")
105
+ return match.group(1)
106
+
107
+
108
+ def _launcher(runtime_executable: Path) -> bytes:
109
+ executable = str(runtime_executable)
110
+ if "\n" in executable or "'" in executable:
111
+ raise ActivationError("runtime Interface CLI path is unsafe")
112
+ return f"""#!/bin/sh
113
+ set -eu
114
+ HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
115
+ ROOT=$(CDPATH= cd -- "$HERE/../.." && pwd)
116
+ export SILICON_INTERFACE_ROOT="$ROOT"
117
+ exec '{executable}' "$@"
118
+ """.encode("utf-8")
119
+
120
+
121
+ def _activate_locked(
122
+ root: Path,
123
+ runtime_executable: Path,
124
+ ) -> dict[str, str]:
125
+ interface_root = root / ".silicon-interface"
126
+ binaries = interface_root / "bin"
127
+ _ensure_real_directory(binaries)
128
+
129
+ expected_version = _version(runtime_executable, cwd=root)
130
+ launcher = _launcher(runtime_executable)
131
+
132
+ # Replace the secondary spelling first. The canonical `si` path used by
133
+ # Stemcell changes only after every prerequisite is durable. Each rename is
134
+ # atomic, and both the old and new launcher remain independently runnable.
135
+ _atomic_write(
136
+ binaries / "silicon-interface",
137
+ launcher,
138
+ mode=0o755,
139
+ )
140
+ _atomic_write(binaries / "si", launcher, mode=0o755)
141
+
142
+ for name in ("silicon-interface", "si"):
143
+ actual = _version(binaries / name, cwd=root)
144
+ if actual != expected_version:
145
+ raise ActivationError(
146
+ f"activated {name} does not match the runtime image"
147
+ )
148
+ return {
149
+ "version": expected_version,
150
+ "runtime_executable": str(runtime_executable),
151
+ }
152
+
153
+
154
+ def activate(
155
+ root: Path,
156
+ runtime_executable: Path,
157
+ ) -> dict[str, str]:
158
+ root = root.resolve(strict=True)
159
+ if not root.is_dir():
160
+ raise ActivationError(f"Silicon root is not a directory: {root}")
161
+ if not runtime_executable.is_absolute():
162
+ raise ActivationError("runtime Interface CLI path must be absolute")
163
+ resolved_executable = runtime_executable.resolve(strict=True)
164
+ if not resolved_executable.is_file() or not os.access(
165
+ resolved_executable, os.X_OK
166
+ ):
167
+ raise ActivationError(
168
+ "runtime Silicon Interface CLI is not executable: "
169
+ f"{runtime_executable}"
170
+ )
171
+
172
+ interface_root = root / ".silicon-interface"
173
+ _ensure_real_directory(interface_root)
174
+ lock_path = interface_root / ".activation.lock"
175
+ flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_CLOEXEC", 0)
176
+ flags |= getattr(os, "O_NOFOLLOW", 0)
177
+ descriptor = os.open(lock_path, flags, 0o600)
178
+ try:
179
+ fcntl.flock(descriptor, fcntl.LOCK_EX)
180
+ return _activate_locked(root, runtime_executable)
181
+ finally:
182
+ fcntl.flock(descriptor, fcntl.LOCK_UN)
183
+ os.close(descriptor)
184
+
185
+
186
+ def _parser() -> argparse.ArgumentParser:
187
+ parser = argparse.ArgumentParser(
188
+ description="Atomically activate the runtime's Silicon Interface CLI"
189
+ )
190
+ parser.add_argument("--root", required=True, type=Path)
191
+ parser.add_argument("--executable", required=True, type=Path)
192
+ return parser
193
+
194
+
195
+ def main(argv: list[str] | None = None) -> int:
196
+ arguments = _parser().parse_args(argv)
197
+ try:
198
+ result = activate(arguments.root, arguments.executable)
199
+ except (ActivationError, OSError) as exc:
200
+ print(f"Silicon Interface activation failed: {exc}", file=sys.stderr)
201
+ return 1
202
+ print(json.dumps(result, sort_keys=True, separators=(",", ":")))
203
+ return 0
204
+
205
+
206
+ if __name__ == "__main__":
207
+ raise SystemExit(main())
@@ -71,14 +71,12 @@ prepare_runtime() {
71
71
  log "missing mount: $SILICON_ROOT"
72
72
  exit 1
73
73
  fi
74
- if [ ! -f "$SILICON_ROOT/main.py" ]; then
75
- log "$SILICON_ROOT does not look like a Silicon instance; expected main.py"
76
- exit 1
77
- fi
78
74
 
79
75
  mkdir -p "$HOME" "$SILICON_HOME" "$SILICON_BROWSER_HOME"
80
76
  prepare_shared_auth
81
77
  cd "$SILICON_ROOT"
78
+ local runtime_python
79
+ runtime_python="$(command -v python3)"
82
80
 
83
81
  # Resolve the same atomically selected generation as silicon-cli. Generation
84
82
  # pointers are instance-relative so the host path and /silicon mount can
@@ -188,6 +186,10 @@ PY
188
186
  )
189
187
  release_root="${runtime_paths[0]}"
190
188
  environment_python="${runtime_paths[1]:-}"
189
+ if [ ! -f "$release_root/main.py" ]; then
190
+ log "$release_root does not look like a Silicon instance; expected main.py"
191
+ exit 1
192
+ fi
191
193
 
192
194
  local dependency_file=""
193
195
  local -a pip_integrity_args=()
@@ -257,14 +259,25 @@ if (
257
259
  )
258
260
  PY
259
261
 
260
- if command -v silicon-interface >/dev/null 2>&1; then
261
- if [ ! -x "$SILICON_ROOT/.silicon-interface/bin/si" ]; then
262
- log "installing Silicon Interface shim"
263
- silicon-interface install "$SILICON_ROOT" >/dev/null
264
- fi
265
- fi
266
-
267
- python - "$INSTANCE_NAME" "$SILICON_ROOT" <<'PY'
262
+ global_interface="/usr/local/bin/silicon-interface"
263
+ local_interface="$SILICON_ROOT/.silicon-interface/bin/si"
264
+ [ -x "$global_interface" ] \
265
+ || die "Silicon Interface CLI is missing from the runtime image"
266
+ global_interface_version="$("$global_interface" --version 2>/dev/null || true)"
267
+ [ -n "$global_interface_version" ] \
268
+ || die "Silicon Interface CLI in the runtime image is not executable"
269
+ log "atomically activating Silicon Interface CLI from the runtime image"
270
+ "$runtime_python" \
271
+ /usr/local/libexec/silicon-activate-interface-cli.py \
272
+ --root "$SILICON_ROOT" \
273
+ --executable "$global_interface" \
274
+ >/dev/null \
275
+ || die "Silicon Interface CLI activation failed"
276
+ local_interface_version="$("$local_interface" --version 2>/dev/null || true)"
277
+ [ "$local_interface_version" = "$global_interface_version" ] \
278
+ || die "Silicon Interface shim does not match the runtime image"
279
+
280
+ "$runtime_python" - "$INSTANCE_NAME" "$SILICON_ROOT" <<'PY'
268
281
  import sys
269
282
  from pathlib import Path
270
283
 
@@ -384,6 +397,44 @@ PY
384
397
 
385
398
  prepare_runtime
386
399
 
400
+ # The Interface daemon PID belongs to the prior container namespace. Keep this
401
+ # one-shot marker outside the protected Interface snapshot so an updater-owned
402
+ # suspended boot can restore state first; process.start consumes it immediately
403
+ # before starting Interface.
404
+ python3 - "$SILICON_ROOT" <<'PY' \
405
+ || die "could not publish the Interface daemon fresh-boot marker"
406
+ import os
407
+ import stat
408
+ import sys
409
+ from pathlib import Path
410
+
411
+ root = Path(sys.argv[1]).resolve()
412
+ state = root / ".silicon"
413
+ state.mkdir(mode=0o700, exist_ok=True)
414
+ if state.is_symlink() or not state.is_dir():
415
+ raise SystemExit("Silicon runtime state directory is unsafe")
416
+ marker = state / "interface-daemon-reset-required"
417
+ flags = os.O_CREAT | os.O_WRONLY
418
+ flags |= getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
419
+ descriptor = os.open(marker, flags, 0o600)
420
+ try:
421
+ metadata = os.fstat(descriptor)
422
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1:
423
+ raise SystemExit("Interface daemon fresh-boot marker is unsafe")
424
+ os.ftruncate(descriptor, 0)
425
+ os.fsync(descriptor)
426
+ finally:
427
+ os.close(descriptor)
428
+ directory = os.open(
429
+ state,
430
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0),
431
+ )
432
+ try:
433
+ os.fsync(directory)
434
+ finally:
435
+ os.close(directory)
436
+ PY
437
+
387
438
  # PIDs are namespaced to each container boot. Stale files from a previous
388
439
  # container can point at an unrelated new process, so remove them before start.
389
440
  rm -f \
@@ -399,7 +450,8 @@ if [ -f "$SILICON_ROOT/.silicon/docker-start-suspended" ]; then
399
450
  rm -f "$SILICON_ROOT/.silicon/docker-start-suspended"
400
451
  log "service start suspended for transactional state restoration"
401
452
  else
402
- silicon start "$INSTANCE_NAME" || log "initial start returned non-zero; container stays alive for inspection"
453
+ SILICON_INTERFACE_RESET_DAEMON_PID=1 silicon start "$INSTANCE_NAME" \
454
+ || log "initial start returned non-zero; container stays alive for inspection"
403
455
  fi
404
456
 
405
457
  while true; do
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "silicon-cli"
7
- version = "1.0.22"
7
+ version = "1.0.24"
8
8
  description = "Silicon CLI — create, run, and manage your silicon instances."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -34,14 +34,25 @@ if (
34
34
  STEMCELL_GIT_URL = f"https://github.com/{STEMCELL_REPO}.git"
35
35
 
36
36
  # Silicon Interface CLI. During local development, silicon-cli will auto-detect
37
- # a sibling silicon-interface checkout; in production this package spec is used.
37
+ # a sibling silicon-interface checkout. Production uses the immutable GitHub
38
+ # release asset because npm publishing is not part of the runtime release path.
39
+ SILICON_INTERFACE_CLI_VERSION = "2.0.2"
40
+ SILICON_INTERFACE_CLI_RELEASE_URL = (
41
+ "https://github.com/teamofsilicons/silicon-interface-web/releases/download/"
42
+ "interface-cli-v2.0.2/"
43
+ "teamofsilicons-silicon-interface-cli-2.0.2.tgz"
44
+ )
45
+ SILICON_INTERFACE_CLI_RELEASE_SHA256 = (
46
+ "5f594958e8165dfaf87e19a71781a628"
47
+ "012b5debe0482dcdc24f28b308e710b2"
48
+ )
38
49
  SILICON_INTERFACE_CLI_PACKAGE = os.environ.get(
39
50
  "SILICON_INTERFACE_CLI_PACKAGE",
40
- "@teamofsilicons/silicon-interface-cli",
51
+ SILICON_INTERFACE_CLI_RELEASE_URL,
41
52
  )
42
53
  SILICON_INTERFACE_CLI_TARBALL = os.environ.get(
43
54
  "SILICON_INTERFACE_CLI_TARBALL",
44
- "https://registry.npmjs.org/@teamofsilicons/silicon-interface-cli/-/silicon-interface-cli-2.0.1.tgz",
55
+ SILICON_INTERFACE_CLI_RELEASE_URL,
45
56
  )
46
57
  SILICON_INTERFACE_CLI_SOURCE = os.environ.get("SILICON_INTERFACE_CLI_SOURCE", "")
47
58
  SILICON_INTERFACE_CLI_SKIP = os.environ.get("SILICON_INTERFACE_CLI_SKIP", "").lower() in {