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.
- {silicon_cli-1.0.22/silicon_cli.egg-info → silicon_cli-1.0.24}/PKG-INFO +12 -3
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/README.md +11 -2
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/docker/runtime/Dockerfile +15 -4
- silicon_cli-1.0.24/docker/runtime/activate-interface-cli.py +207 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/docker/runtime/runtime-entrypoint.sh +65 -13
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/pyproject.toml +1 -1
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/config.py +14 -3
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/docker_runtime.py +292 -15
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/interface_cli.py +146 -26
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/package_manager.py +176 -13
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/process.py +138 -2
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/runtime_contract.py +3 -1
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/update.py +85 -10
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/engine.py +51 -4
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/policy.py +4 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/snapshot_adapter.py +9 -3
- {silicon_cli-1.0.22 → silicon_cli-1.0.24/silicon_cli.egg-info}/PKG-INFO +12 -3
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/SOURCES.txt +2 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_docker_runtime.py +373 -4
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_docker_update_hooks.py +176 -2
- silicon_cli-1.0.24/tests/test_interface_activation.py +205 -0
- silicon_cli-1.0.24/tests/test_package_manager.py +515 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_process_health.py +303 -1
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_runtime_contract.py +4 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_snapshot_adapter.py +25 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_transactional_updater.py +145 -1
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_update.py +77 -3
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/uv.lock +1 -1
- silicon_cli-1.0.22/tests/test_package_manager.py +0 -192
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/.github/workflows/publish-runtime.yml +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/LICENSE +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/MANIFEST.in +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/docker/runtime/Dockerfile.local +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/setup.cfg +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/__init__.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/backup_runtime.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/cli.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/glassagent.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/host_lock.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/http_transport.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/pull_transaction.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/registry.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/stemcell.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/sync.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/ui.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/__init__.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/cache.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/channel.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/checkpoint.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/fleet.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/generation.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/io.py +5 -5
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/journal.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/lock.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/maintenance.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/overlay.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/planner.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/release.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli/updater/retention.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/dependency_links.txt +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/entry_points.txt +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/requires.txt +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/silicon_cli.egg-info/top_level.txt +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_backup_runtime.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_browser_profile.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_extend_installation.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_fleet_update_journal.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_git_release_source.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_glassagent_process.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_http_transport.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_pull_args.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_pull_transaction.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_stemcell_hydration.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_sync_credentials_backup.py +0 -0
- {silicon_cli-1.0.22 → silicon_cli-1.0.24}/tests/test_update_maintenance.py +0 -0
- {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.
|
|
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` |
|
|
101
|
-
| `SILICON_INTERFACE_CLI_TARBALL` |
|
|
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` |
|
|
83
|
-
| `SILICON_INTERFACE_CLI_TARBALL` |
|
|
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
|
|
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
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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"
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
51
|
+
SILICON_INTERFACE_CLI_RELEASE_URL,
|
|
41
52
|
)
|
|
42
53
|
SILICON_INTERFACE_CLI_TARBALL = os.environ.get(
|
|
43
54
|
"SILICON_INTERFACE_CLI_TARBALL",
|
|
44
|
-
|
|
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 {
|