refactor: keep root install as the sole installer source
This commit is contained in:
1 parent
6491a766f4
commit
a46bd6e132
14 files changed
+95
-385
No files matched your search
@@ -30,7 +30,7 @@ Process main is separate from callable roots. Initialize Unix SIGPIPE policy bef
|
|||||||
|
|
||||||
## 2. Signatures
|
## 2. Signatures
|
||||||
|
|
||||||
- First installation: root `install` supports `bash install` and `curl -fsSL https://git.dever.cc/shemic/dever/install | bash`, forwarding `--version`, `--release`, `--trusted-key`, `--system-root`, and `--no-start` to the existing Python installer. `python3 sdk/install-entry.py [--check]` generates/verifies the standalone Bash from `skills/scripts/install.py` and the independent public key. Keep one installation implementation.
|
- First installation: root `install` is the sole installer source and supports `bash install` and `curl -fsSL https://git.dever.cc/shemic/dever/install | bash`. Its Bash entry forwards `--version`, `--release`, `--trusted-key`, `--system-root`, and `--no-start` to its embedded Python implementation. Maintain this file directly; skills provide guidance, not another installer, and there is no generation step.
|
||||||
- Project creation: `dever new <new-directory> [--markdown]`; `project::create(&Path, bool)` consumes the single template source under the ordinary `skills/assets/template` directory in the same repository revision. The machine launcher selects the active core without reading nonexistent target configuration.
|
- Project creation: `dever new <new-directory> [--markdown]`; `project::create(&Path, bool)` consumes the single template source under the ordinary `skills/assets/template` directory in the same repository revision. The machine launcher selects the active core without reading nonexistent target configuration.
|
||||||
- AI guidance: `dever skill path [project-root]` resolves and verifies the active or project-pinned release's `skills/dever-language`; `dever skill install <new-directory>` atomically publishes a stable loader that invokes the absolute machine launcher. Skill resolution is a public read-only operation and must not open the root-only install lock.
|
- AI guidance: `dever skill path [project-root]` resolves and verifies the active or project-pinned release's `skills/dever-language`; `dever skill install <new-directory>` atomically publishes a stable loader that invokes the absolute machine launcher. Skill resolution is a public read-only operation and must not open the root-only install lock.
|
||||||
|
|
||||||
@@ -90,9 +90,9 @@ Authenticate the catalog before extraction. Zstandard is a single checksummed fr
|
|||||||
|
|
||||||
Shared extensions live in service-writable `cache/extensions/<version>/<catalog-sha>/<id>`, outside native cache eviction and immutable core directories. IPC accepts only version and typed ID for an already authenticated installed catalog, never caller URLs/paths/keys. Prepare under the persistent install lock in private staging, verify complete bytes then publish readonly with NOREPLACE; corrupted reuse is an error. `SignedResources` owns verified read/list/root selection for Lib, daemon runtime verification and the actual compile worker. `dever target add linux-aarch64` and explicit dependency preparation may ensure resources; ordinary run/build/read cannot. Update prepares precisely the old active version's installed extension selection before switching the one core/skill active journal; any failure preserves the old selection. Stable launcher/daemon protocol changes require matching trusted bootstrap provisioning. Do not rewrite users' skill directories or add an independently switched skill pointer.
|
Shared extensions live in service-writable `cache/extensions/<version>/<catalog-sha>/<id>`, outside native cache eviction and immutable core directories. IPC accepts only version and typed ID for an already authenticated installed catalog, never caller URLs/paths/keys. Prepare under the persistent install lock in private staging, verify complete bytes then publish readonly with NOREPLACE; corrupted reuse is an error. `SignedResources` owns verified read/list/root selection for Lib, daemon runtime verification and the actual compile worker. `dever target add linux-aarch64` and explicit dependency preparation may ensure resources; ordinary run/build/read cannot. Update prepares precisely the old active version's installed extension selection before switching the one core/skill active journal; any failure preserves the old selection. Stable launcher/daemon protocol changes require matching trusted bootstrap provisioning. Do not rewrite users' skill directories or add an independently switched skill pointer.
|
||||||
|
|
||||||
The skill's first-install helper uses its independently distributed public key, verifies the full package before executing bootstrap, and generates only an owned temporary installer config. The generated Bash entry embeds that same key and passes the original Python source through a quoted heredoc to `/usr/bin/python3 -I -`; parse the complete function before invoking it. Non-root installation uses `/usr/bin/sudo`; help must neither elevate nor download. Missing Python 3.9/OpenSSL, unsupported OS/ABI, unavailable package or invalid signature is an explicit failure. Current x86_64 host packages require glibc 2.39; ARM application support does not establish an ARM compiler-host release. The installer never reads business configuration. Real-root installation explicitly starts the machine service unless --no-start is selected; image-root verification must not touch host services. Python/OpenSSL are installer prerequisites, not application runtime dependencies. `sdk/release-assets.py` uses an explicitly supplied author zstd executable and publishes deterministic base/extension/raw assets with Linux NOREPLACE directory rename; it cannot clean another writer's public output.
|
The root first-install script embeds its independently provisioned public key, verifies the full package before executing bootstrap, and generates only an owned temporary installer config. Its Bash entry passes the embedded Python source through a quoted heredoc to `/usr/bin/python3 -I -`; parse the complete function before invoking it. Non-root installation uses `/usr/bin/sudo`; help must neither elevate nor download. Missing Python 3.9/OpenSSL, unsupported OS/ABI, unavailable package or invalid signature is an explicit failure. Current x86_64 host packages require glibc 2.39; ARM application support does not establish an ARM compiler-host release. The installer never reads business configuration. Real-root installation explicitly starts the machine service unless --no-start is selected; image-root verification must not touch host services. Python/OpenSSL are installer prerequisites, not application runtime dependencies. `sdk/release-assets.py` uses an explicitly supplied author zstd executable and publishes deterministic base/extension/raw assets with Linux NOREPLACE directory rename; it cannot clean another writer's public output.
|
||||||
|
|
||||||
Web publication must explicitly serve the generated script at `/shemic/dever/install` and all assets as anonymous HTTPS, independently of source repository access. Redirects are restricted to HTTPS `git.dever.cc` with no credentials, non-443 port or fragment. Publish all immutable version assets before switching latest metadata. Source pushes and local transport tests do not prove public download readiness. Changing the official origin requires rebuilding the stable launcher/daemon and provisioning them once through the new installer; ordinary update activates version payloads but does not replace that bootstrap pair. Never relabel old binaries as a new release.
|
Web publication must explicitly serve the root script at `/shemic/dever/install` and all assets as anonymous HTTPS, independently of source repository access. Redirects are restricted to HTTPS `git.dever.cc` with no credentials, non-443 port or fragment. Publish all immutable version assets before switching latest metadata. Source pushes and local transport tests do not prove public download readiness. Changing the official origin requires rebuilding the stable launcher/daemon and provisioning them once through the new installer; ordinary update activates version payloads but does not replace that bootstrap pair. Never relabel old binaries as a new release.
|
||||||
|
|
||||||
Native author settings use `dever-native-release-input-v1`: version/target/signing_key, core and `core_libraries` inputs, required base/sqlite/postgres/both archive inputs, and ordered start/libraries/end. Input `source` is a normalized author-relative path and `sha256` is the exact lowercase digest; link inputs also specify the destination `path`. Core libraries use plain SONAME filenames and publish below `lib/`; runtime link inputs remain runtime-relative. Reuse the runtime v1 manifest types and validator. Copy into exclusively owned private staging, verify both source and copied digest, sort manifest artifacts, and sign those exact bytes with the supplied owner-private Ed25519 PKCS#8 key. Reject key content as payload, symlinks, path escape and reserved/duplicate destinations. Keep source paths, key material and timestamps out of release metadata. Linux publication uses atomic NOREPLACE; failures clean only owned staging. No tool invocation, environment configuration, downloads, input hard links or implicit trust-key installation belongs to this owner.
|
Native author settings use `dever-native-release-input-v1`: version/target/signing_key, core and `core_libraries` inputs, required base/sqlite/postgres/both archive inputs, and ordered start/libraries/end. Input `source` is a normalized author-relative path and `sha256` is the exact lowercase digest; link inputs also specify the destination `path`. Core libraries use plain SONAME filenames and publish below `lib/`; runtime link inputs remain runtime-relative. Reuse the runtime v1 manifest types and validator. Copy into exclusively owned private staging, verify both source and copied digest, sort manifest artifacts, and sign those exact bytes with the supplied owner-private Ed25519 PKCS#8 key. Reject key content as payload, symlinks, path escape and reserved/duplicate destinations. Keep source paths, key material and timestamps out of release metadata. Linux publication uses atomic NOREPLACE; failures clean only owned staging. No tool invocation, environment configuration, downloads, input hard links or implicit trust-key installation belongs to this owner.
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ Create a response-body cancellation owner only after the pending-to-committed st
|
|||||||
|
|
||||||
- New target already exists (including empty directory or dangling symlink), parent is missing, or two creators race: explicit error for the loser; preserve the existing target and remove only owned staging.
|
- New target already exists (including empty directory or dangling symlink), parent is missing, or two creators race: explicit error for the loser; preserve the existing target and remove only owned staging.
|
||||||
- Signature/payload/skill corruption, incomplete archive or absent official asset: explicit failure, no active-version switch or fallback. Publishing source code is not evidence that a downloadable binary release exists.
|
- Signature/payload/skill corruption, incomplete archive or absent official asset: explicit failure, no active-version switch or fallback. Publishing source code is not evidence that a downloadable binary release exists.
|
||||||
- Truncated Bash body: no installer or sudo execution. Missing prerequisites: fail before metadata download. Failed authentication through Bash: preserve existing active journal/core/bootstrap and clean only owned staging. Generated entry drift: `sdk/install-entry.py --check` fails. Successful same-version bootstrap may append an activation record; assert selected-version identity rather than a byte-identical journal.
|
- Truncated Bash body: no installer or sudo execution. Missing prerequisites: fail before metadata download. Failed authentication through Bash: preserve existing active journal/core/bootstrap and clean only owned staging. Tests load the embedded Python from the actual root entry and exercise Bash file/pipe execution; retain a check against the independently provisioned public key. Successful same-version bootstrap may append an activation record; assert selected-version identity rather than a byte-identical journal.
|
||||||
- Project has a pinned version: `skill path <project>` resolves that exact installed version. Foreign users must read matching guidance without obtaining the administrator install lock.
|
- Project has a pinned version: `skill path <project>` resolves that exact installed version. Foreign users must read matching guidance without obtaining the administrator install lock.
|
||||||
|
|
||||||
Cross build: unknown/duplicate/missing target arguments fail before execution; absent target packs, wrong ELF/archive machine, unsigned target leaves, incompatible lock/runtime/build receipt and changed target inputs fail before cache reuse or linking. Sandbox loader mode must survive both release packing and embedding; a hash-valid non-executable loader is not a usable Worker resource.
|
Cross build: unknown/duplicate/missing target arguments fail before execution; absent target packs, wrong ELF/archive machine, unsigned target leaves, incompatible lock/runtime/build receipt and changed target inputs fail before cache reuse or linking. Sandbox loader mode must survive both release packing and embedding; a hash-valid non-executable loader is not a usable Worker resource.
|
||||||
@@ -346,7 +346,7 @@ Managed base: a zero-Lib Python/JavaScript Adapter still locks its signed runtim
|
|||||||
|
|
||||||
For onboarding select CLI `new_project`, lib `release_source`, shared-toolchain version/skill tests and native-release skill authoring tests, plus `python3 -B -m unittest discover -s test/onboarding -v`. Assert both formats check/format, no-overwrite concurrency, real CMD/Test/build output, exact signed extraction, corrupt-download retention, project-pinned guidance and foreign-user reads. The foreign-user test lowers only an owned client UID. Bootstrap script tests mock final installation; report that separately from real native-template execution. Do not activate host services or run full release/CI suites as an onboarding check.
|
For onboarding select CLI `new_project`, lib `release_source`, shared-toolchain version/skill tests and native-release skill authoring tests, plus `python3 -B -m unittest discover -s test/onboarding -v`. Assert both formats check/format, no-overwrite concurrency, real CMD/Test/build output, exact signed extraction, corrupt-download retention, project-pinned guidance and foreign-user reads. The foreign-user test lowers only an owned client UID. Bootstrap script tests mock final installation; report that separately from real native-template execution. Do not activate host services or run full release/CI suites as an onboarding check.
|
||||||
|
|
||||||
For installer-only changes select `test_install*.py`, `toolchain::release_source` and generator `--check`. Assert actual Bash file/pipe help, isolated Python, literal argument/exit-status preservation, non-root sudo dispatch, independent-key authentication, new origin paths and rejected redirects. `verify_install_entry.py` is an opt-in actual signed-payload check: source-free entry, image-only first install, repeat install and corrupt-signature preservation. It uses an existing authenticated release/extractor and no host services; old payloads do not establish new-binary or online update acceptance.
|
For installer-only changes select `test_install*.py` and `bash -n install`; also run `toolchain::release_source` when its Rust download owner changes. Assert actual Bash file/pipe help, isolated Python, literal argument/exit-status preservation, non-root sudo dispatch, independent-key authentication, new origin paths and rejected redirects. `verify_install_entry.py` is an opt-in actual signed-payload check: source-free entry, image-only first install, repeat install and corrupt-signature preservation. It uses an existing authenticated release/extractor and no host services; old payloads do not establish new-binary or online update acceptance.
|
||||||
|
|
||||||
Cross-target changes cover `build_arguments`, native-release target/ELF/mode cases, CompileRequest validation, cache isolation and Lib/Worker target selection. The opt-in `signed_host_compiler_builds_arm_profiles_and_runs_standalone_under_qemu` and `signed_arm_ecosystems_build_offline_standalone_apps_for_system_guest` require explicit author inputs. Run exported Workers/HTTP with `test/ecosystem-release/arm64/simulate.py` under a complete ARM kernel; assert each successful output, source-located expected failure and child cleanup. Do not substitute QEMU user emulation for namespace/seccomp execution or infer physical ARM performance.
|
Cross-target changes cover `build_arguments`, native-release target/ELF/mode cases, CompileRequest validation, cache isolation and Lib/Worker target selection. The opt-in `signed_host_compiler_builds_arm_profiles_and_runs_standalone_under_qemu` and `signed_arm_ecosystems_build_offline_standalone_apps_for_system_guest` require explicit author inputs. Run exported Workers/HTTP with `test/ecosystem-release/arm64/simulate.py` under a complete ARM kernel; assert each successful output, source-located expected failure and child cleanup. Do not substitute QEMU user emulation for namespace/seccomp execution or infer physical ARM performance.
|
||||||
|
|
||||||
@@ -406,7 +406,7 @@ Command execution observes guard exec through a private CLOEXEC pipe. Bind the P
|
|||||||
|
|
||||||
Wrong: update a skill symlink separately after changing the active core, treat a cached older version as a successful failed update, or delete a public output tree after a publication race. Correct: resolve core and signed skill from one active journal, return the original download failure, and publish exclusively owned staging with NOREPLACE.
|
Wrong: update a skill symlink separately after changing the active core, treat a cached older version as a successful failed update, or delete a public output tree after a publication race. Correct: resolve core and signed skill from one active journal, return the original download failure, and publish exclusively owned staging with NOREPLACE.
|
||||||
|
|
||||||
Installer wrong: hand-edit generated install logic, take its key from the downloaded package, or assume changing Git remote changes an old launcher. Correct: regenerate from the canonical installer and independent key, then migrate the stable bootstrap pair with a newly built signed release.
|
Installer wrong: recreate a separate skill installer or generator, take its key from the downloaded package, or assume changing Git remote changes an old launcher. Correct: maintain the sole root install source with its independent key, verify its actual Bash behavior, and migrate changed stable bootstrap binaries through a newly built signed release.
|
||||||
|
|
||||||
Cross-target wrong: check ELF headers but use the host loader name to assign executable mode. Correct: validate target architecture and independently preserve each declared executable's mode across signed packaging, embedding and extraction; exercise the resulting Worker bootstrap under the target kernel.
|
Cross-target wrong: check ELF headers but use the host loader name to assign executable mode. Correct: validate target architecture and independently preserve each declared executable's mode across signed packaging, embedding and extraction; exercise the resulting Worker bootstrap under the target kernel.
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
## 唯一安装实现
|
## 唯一安装实现
|
||||||
|
|
||||||
保留 skills/scripts/install.py 的下载、签名校验、raw launcher 校验与 Rust 解压/bootstrap 调用链。sdk/install-entry.py 从该源码和 skills/assets/release.pub 生成根 install。Bash 只负责 Python 前提检查、必要的 sudo 和把完整 Python 程序通过引用 heredoc 交给 /usr/bin/python3 -I -,参数原样转发。完整函数定义在最后调用前解析,截断的主体不执行安装。
|
2026-10-05 用户在合仓后明确要求只保留根 install,取代之前的 skill安装器加生成器方案。根 install 直接维护既有下载、签名校验、raw launcher 校验与 Rust 解压/bootstrap 调用链。Bash 只负责 Python 前提检查、必要的 sudo 和把内嵌 Python 程序通过引用 heredoc 交给 /usr/bin/python3 -I -,参数原样转发。完整函数定义在最后调用前解析,截断的主体不执行安装。
|
||||||
|
|
||||||
生成器注入独立固定公钥而不是从下载包取公钥;源 skill 的普通 Python 调用仍使用配套公钥文件。Python main 接受内嵌默认公钥参数;具体嵌入使用清晰的源码封装,避免手工复制第二份算法或脆弱的源码字符串替换。
|
根脚本内嵌独立固定公钥,不从下载包取公钥;显式 --trusted-key 仍支持已独立确认的自有签名包。删除双入口专用的生成包装和skill相对路径。普通测试与发行验收统一消费根 install;测试用加载器只读取实际内嵌源码,不保存第二份安装实现、不成为生产依赖。
|
||||||
|
|
||||||
默认版本 latest;元数据为 dist/latest/dever-<platform>.manifest.json 与 .sig;验签后只从 dist/<signed-version>/ 获取基础 archive、raw blobs 和扩展。Rust ReleaseSource 与 Python 使用同一静态协议。只允许官方 HTTPS 同站重定向,禁止凭据、非 443、fragment 和外站。没有环境变量/项目配置覆盖和旧源 fallback。
|
默认版本 latest;元数据为 dist/latest/dever-<platform>.manifest.json 与 .sig;验签后只从 dist/<signed-version>/ 获取基础 archive、raw blobs 和扩展。Rust ReleaseSource 与 Python 使用同一静态协议。只允许官方 HTTPS 同站重定向,禁止凭据、非 443、fragment 和外站。没有环境变量/项目配置覆盖和旧源 fallback。
|
||||||
|
|
||||||
|
|||||||
@@ -68,3 +68,16 @@
|
|||||||
### 待上线的唯一交付阻塞
|
### 待上线的唯一交付阻塞
|
||||||
|
|
||||||
2026-10-05最后复核,`https://git.dever.cc/shemic/dever/install` 与 `/shemic/dever/dist/latest/dever-linux-x86_64.manifest.json` 均为HTTP404。已向用户询问SSH主机、端口、账号与静态目录,或既有部署配置位置;目前仅有Git SSH访问,不能据此推定服务器shell/路由配置。发布时将上述public目录映射至 `/shemic/dever/`,然后在自有隔离安装中验收真实HTTPS首装与update。部署完成前不归档、不标为全部交付完成。
|
2026-10-05最后复核,`https://git.dever.cc/shemic/dever/install` 与 `/shemic/dever/dist/latest/dever-linux-x86_64.manifest.json` 均为HTTP404。已向用户询问SSH主机、端口、账号与静态目录,或既有部署配置位置;目前仅有Git SSH访问,不能据此推定服务器shell/路由配置。发布时将上述public目录映射至 `/shemic/dever/`,然后在自有隔离安装中验收真实HTTPS首装与update。部署完成前不归档、不标为全部交付完成。
|
||||||
|
|
||||||
|
### 合仓后的安装入口清理(用户新增授权)
|
||||||
|
|
||||||
|
用户确认只保留根install。旧设计中skill安装器作为源码、根入口作为生成物的约定被替代;前述发行验收是历史证据,不能据此宣称本次改动已验证。
|
||||||
|
|
||||||
|
- 实现边界:根install成为唯一安装源码,删除 `skills/scripts/install.py` 和 `sdk/install-entry.py`,迁移根test/onboarding内的测试和发行验收调用,更新当前安装指引与规范。
|
||||||
|
- 保持固定下载源、独立公钥、验签/路径/摘要校验、Rust解压及bootstrap、失败清理、Bash参数传递/提权/截断保护。仅移除双入口专用逻辑,不修改Rust或已签名0.1.2发行资产,不运行全量或全局服务验证。
|
||||||
|
- trellis-implement负责安装脚本和测试;主会话负责指引、规范、任务记录,完成后独立检查实际diff与定向证据。
|
||||||
|
- 原19项安装定向测试19/19通过,无跳过;保留文件/管道help、字面参数与退出码、隔离Python、真实非root UID的sudo截获、截断不执行、默认官方公钥拒绝自签包、显式可信key接受及篡改拒绝/清理。三个发行验收脚本的导入/help、Bash语法、skill结构、diff和旧活动引用扫描通过。
|
||||||
|
- 主会话补验真实Bash签名/清理1/1通过。首次补验因根盘满在fixture创建阶段失败,改为仅本测试进程的 `/dev/shm` 临时目录后通过,未修改产品环境或增加环境变量配置。
|
||||||
|
- 删除旧安装器的Python缓存及空 `skills/scripts/` 目录;源码可从Git历史恢复。为提交空间删除本轮 `target/release-0.1.2/tools/dever-core` 编译器副本(16,956,160字节,摘要已核对);完整同字节核心仍保存在签名基础包,可由已有解包器恢复,其余工具与签名资产保留。
|
||||||
|
- 新根install为13,632字节,SHA256 `2b07829c7c7c7d76b8fd257239b4ab44816e064951f66e33ffd6e2bdcfce92ef`。已制作的0.1.2版本/tag及签名包仍固定在原产品提交19f122b;本次未重新发行,不把旧安装验收报告当成新脚本的验收报告。
|
||||||
|
- 待部署的 `target/release-0.1.2/public/install` 已同步新根入口并逐字节核对;dist签名资产未改。独立检查确认下载/验签等函数与原版本AST一致、Bash包装参数及尾部调用保持,未引入第二套生产安装实现;已清理规范中最后一处generator检查要求。
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- [x] Bash 语法、直接/管道 help、参数转发、退出状态、非 root 提权与截断脚本行为有最小定向验证。
|
- [x] Bash 语法、直接/管道 help、参数转发、退出状态、非 root 提权与截断脚本行为有最小定向验证。
|
||||||
- [x] 生成物与现有安装器/独立公钥一致,不存在手工维护的第二套安装逻辑。
|
- [x] 根 install 是唯一安装源码;删除skill安装器与生成器、迁移相关测试及验收调用,保留独立固定公钥和既有安装行为。(2026-10-05 合仓后用户修订)
|
||||||
- [x] Python/Rust 新源 latest/精确版本/扩展地址一致,拒绝非法重定向。
|
- [x] Python/Rust 新源 latest/精确版本/扩展地址一致,拒绝非法重定向。
|
||||||
- [x] 真实 Ed25519 校验、损坏拒绝、临时文件清理保持通过;实际签名包可通过新入口安装到自有镜像,不启动宿主服务。
|
- [x] 真实 Ed25519 校验、损坏拒绝、临时文件清理保持通过;实际签名包可通过新入口安装到自有镜像,不启动宿主服务。
|
||||||
- [x] 必要定向测试和 diff 检查通过,文档说明命令、发布目录、旧版迁移和线上未验证边界。
|
- [x] 必要定向测试和 diff 检查通过,文档说明命令、发布目录、旧版迁移和线上未验证边界。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Generated by sdk/install-entry.py; edit skills/scripts/install.py instead.
|
# Canonical installation source; the embedded Python shares this entry's lifecycle.
|
||||||
# The complete body must parse before any installation command can execute.
|
# The complete body must parse before any installation command can execute.
|
||||||
dever_install() {
|
dever_install() {
|
||||||
if [[ ! -x /usr/bin/python3 ]]; then
|
if [[ ! -x /usr/bin/python3 ]]; then
|
||||||
@@ -20,9 +20,6 @@ dever_install() {
|
|||||||
command=(/usr/bin/sudo -- "${command[@]}")
|
command=(/usr/bin/sudo -- "${command[@]}")
|
||||||
fi
|
fi
|
||||||
"${command[@]}" "$@" <<'DEVER_INSTALL_PYTHON'
|
"${command[@]}" "$@" <<'DEVER_INSTALL_PYTHON'
|
||||||
# Preserve the source entry guard without rewriting the installer program.
|
|
||||||
__name__ = "dever_embedded_installer"
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Install an authenticated Dever release; no application configuration is read."""
|
"""Install an authenticated Dever release; no application configuration is read."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -43,6 +40,7 @@ RELEASES = "https://git.dever.cc/shemic/dever/dist"
|
|||||||
MAX_METADATA = 2 * 1024 * 1024
|
MAX_METADATA = 2 * 1024 * 1024
|
||||||
MAX_RELEASE = 2 * 1024 * 1024 * 1024
|
MAX_RELEASE = 2 * 1024 * 1024 * 1024
|
||||||
ASSET_HOSTS = {"git.dever.cc"}
|
ASSET_HOSTS = {"git.dever.cc"}
|
||||||
|
DEFAULT_PUBLIC_KEY = "7889e0edaf5fbf1060bef4ad6e27106128485c8412a6f6cd4457dc67e112986e"
|
||||||
|
|
||||||
|
|
||||||
class OfficialRedirect(urllib.request.HTTPRedirectHandler):
|
class OfficialRedirect(urllib.request.HTTPRedirectHandler):
|
||||||
@@ -232,9 +230,9 @@ def download_archive(address, destination):
|
|||||||
output.write(contents)
|
output.write(contents)
|
||||||
|
|
||||||
|
|
||||||
def install(arguments, default_key=None):
|
def install(arguments):
|
||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
raise ValueError("机器安装需要管理员权限;先检查脚本,再用 sudo python3 执行")
|
raise ValueError("机器安装需要管理员权限;请通过根 install 入口执行")
|
||||||
platform = platform_name()
|
platform = platform_name()
|
||||||
prerequisites(platform)
|
prerequisites(platform)
|
||||||
if arguments.version != "latest" and not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", arguments.version):
|
if arguments.version != "latest" and not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", arguments.version):
|
||||||
@@ -254,7 +252,7 @@ def install(arguments, default_key=None):
|
|||||||
key_path = arguments.trusted_key
|
key_path = arguments.trusted_key
|
||||||
if key_path is None:
|
if key_path is None:
|
||||||
key_path = scratch / "independent-release.pub"
|
key_path = scratch / "independent-release.pub"
|
||||||
key_path.write_text(default_key)
|
key_path.write_text(DEFAULT_PUBLIC_KEY)
|
||||||
version, catalog, key = authenticate(release, key_path, scratch, platform, arguments.version)
|
version, catalog, key = authenticate(release, key_path, scratch, platform, arguments.version)
|
||||||
trusted = scratch / "release.pub"
|
trusted = scratch / "release.pub"
|
||||||
trusted.write_text(key.hex() + "\n")
|
trusted.write_text(key.hex() + "\n")
|
||||||
@@ -271,17 +269,16 @@ def install(arguments, default_key=None):
|
|||||||
print(f"Dever {version} 已安装到 {system_root / 'opt/dever'}")
|
print(f"Dever {version} 已安装到 {system_root / 'opt/dever'}")
|
||||||
|
|
||||||
|
|
||||||
def main(default_key=None):
|
def main():
|
||||||
parser = argparse.ArgumentParser(prog="dever install", description=__doc__)
|
parser = argparse.ArgumentParser(prog="dever install", description=__doc__)
|
||||||
parser.add_argument("--version", default="latest")
|
parser.add_argument("--version", default="latest")
|
||||||
parser.add_argument("--release", type=Path, help="使用本地已签名发行目录,不下载")
|
parser.add_argument("--release", type=Path, help="使用本地已签名发行目录,不下载")
|
||||||
key_path = None if default_key is not None else Path(__file__).resolve().parents[1] / "assets/release.pub"
|
parser.add_argument("--trusted-key", type=Path)
|
||||||
parser.add_argument("--trusted-key", type=Path, default=key_path)
|
|
||||||
parser.add_argument("--system-root", type=Path, default=Path("/"), help="已有的自有安装镜像目录;默认安装当前机器")
|
parser.add_argument("--system-root", type=Path, default=Path("/"), help="已有的自有安装镜像目录;默认安装当前机器")
|
||||||
parser.add_argument("--no-start", action="store_true", help="不启动当前机器 systemd 服务")
|
parser.add_argument("--no-start", action="store_true", help="不启动当前机器 systemd 服务")
|
||||||
arguments = parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
install(arguments, default_key)
|
install(arguments)
|
||||||
except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as error:
|
except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as error:
|
||||||
parser.exit(1, f"Dever 安装失败:{error}\n")
|
parser.exit(1, f"Dever 安装失败:{error}\n")
|
||||||
|
|
||||||
@@ -289,7 +286,6 @@ def main(default_key=None):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
main(default_key='7889e0edaf5fbf1060bef4ad6e27106128485c8412a6f6cd4457dc67e112986e')
|
|
||||||
DEVER_INSTALL_PYTHON
|
DEVER_INSTALL_PYTHON
|
||||||
}
|
}
|
||||||
dever_install "$@"
|
dever_install "$@"
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate the standalone Bash entry from the canonical signed installer."""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
HEADER = '''#!/usr/bin/env bash
|
|
||||||
# Generated by sdk/install-entry.py; edit skills/scripts/install.py instead.
|
|
||||||
# The complete body must parse before any installation command can execute.
|
|
||||||
dever_install() {
|
|
||||||
if [[ ! -x /usr/bin/python3 ]]; then
|
|
||||||
printf '%s\\n' 'Dever 安装需要 /usr/bin/python3 3.9 或更新版本' >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
/usr/bin/python3 -I -c 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else "Dever 安装需要 Python 3.9 或更新版本")' || return "$?"
|
|
||||||
local argument help=0
|
|
||||||
for argument in "$@"; do
|
|
||||||
case "$argument" in -h|--help) help=1 ;; esac
|
|
||||||
done
|
|
||||||
local -a command=(/usr/bin/python3 -I -)
|
|
||||||
if (( EUID != 0 && help == 0 )); then
|
|
||||||
if [[ ! -x /usr/bin/sudo ]]; then
|
|
||||||
printf '%s\\n' 'Dever 机器安装需要管理员权限;请安装 sudo 或由管理员执行此脚本' >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
command=(/usr/bin/sudo -- "${command[@]}")
|
|
||||||
fi
|
|
||||||
"${command[@]}" "$@" <<'DEVER_INSTALL_PYTHON'
|
|
||||||
# Preserve the source entry guard without rewriting the installer program.
|
|
||||||
__name__ = "dever_embedded_installer"
|
|
||||||
'''
|
|
||||||
FOOTER = '''DEVER_INSTALL_PYTHON
|
|
||||||
}
|
|
||||||
dever_install "$@"
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def render(source, public_key):
|
|
||||||
public_key = public_key.strip()
|
|
||||||
if not re.fullmatch(r"[0-9a-f]{64}", public_key):
|
|
||||||
raise ValueError("release.pub must contain one Ed25519 public key")
|
|
||||||
if "DEVER_INSTALL_PYTHON" in source.splitlines():
|
|
||||||
raise ValueError("installer source collides with the shell heredoc delimiter")
|
|
||||||
compile(source, "skills/scripts/install.py", "exec")
|
|
||||||
return HEADER + source + f"\nmain(default_key={public_key!r})\n" + FOOTER
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("--check", action="store_true", help="verify the checked-in entry without writing")
|
|
||||||
arguments = parser.parse_args()
|
|
||||||
expected = render((ROOT / "skills/scripts/install.py").read_text(),
|
|
||||||
(ROOT / "skills/assets/release.pub").read_text())
|
|
||||||
destination = ROOT / "install"
|
|
||||||
if arguments.check:
|
|
||||||
if not destination.is_file() or destination.read_text() != expected:
|
|
||||||
parser.exit(1, "install is stale; run python3 sdk/install-entry.py\n")
|
|
||||||
else:
|
|
||||||
destination.write_text(expected)
|
|
||||||
destination.chmod(0o755)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -104,12 +104,12 @@ Only explicit install/update, target add and dependency preparation download off
|
|||||||
|
|
||||||
## Public installer and static distribution
|
## Public installer and static distribution
|
||||||
|
|
||||||
`python3 sdk/install-entry.py` generates the root `install` from the canonical `skills/scripts/install.py` and the independently provisioned `skills/assets/release.pub`. `--check` verifies that the committed entry is current. The generated Bash contains the complete installer, so `curl -fsSL https://git.dever.cc/shemic/dever/install | bash` does not need a Git checkout or a separate skill download. Keep download, authentication and bootstrap behavior in the Python/Rust owners; do not maintain a second implementation inside the generated file.
|
Root `install` is the sole installer source. Maintain its Bash entry and embedded Python directly; there is no generator or separate skill installer. It embeds the independently provisioned public key, so `curl -fsSL https://git.dever.cc/shemic/dever/install | bash` needs neither a Git checkout nor a separate skill download. Keep archive extraction and machine activation in their existing Rust owners. Validate changes with `bash -n install` and the focused `python3 -B -m unittest discover -s test/onboarding -p 'test_install*.py' -v` suite; the tests load the actual root entry instead of copying its implementation.
|
||||||
|
|
||||||
Configure the web server to serve these anonymous HTTPS paths:
|
Configure the web server to serve these anonymous HTTPS paths:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/shemic/dever/install generated plain Bash script
|
/shemic/dever/install root install Bash script
|
||||||
/shemic/dever/dist/latest/dever-<platform>.manifest.json
|
/shemic/dever/dist/latest/dever-<platform>.manifest.json
|
||||||
/shemic/dever/dist/latest/dever-<platform>.manifest.sig
|
/shemic/dever/dist/latest/dever-<platform>.manifest.sig
|
||||||
/shemic/dever/dist/<version>/dever-<platform>.* all matching immutable assets
|
/shemic/dever/dist/<version>/dever-<platform>.* all matching immutable assets
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ bash install --release /可信发行目录 --trusted-key /独立确认的公钥
|
|||||||
|
|
||||||
公钥可公开提交;签名私钥绝不能进 skill、源码仓库或发行包。只有已独立确认的公钥才可作为 `--trusted-key`,不能从未知下载包顺手取一把钥匙作为信任依据。
|
公钥可公开提交;签名私钥绝不能进 skill、源码仓库或发行包。只有已独立确认的公钥才可作为 `--trusted-key`,不能从未知下载包顺手取一把钥匙作为信任依据。
|
||||||
|
|
||||||
已持有完整 skill 时仍可直接运行 `sudo python3 <skill目录>/scripts/install.py`,它与根 Bash 入口复用同一安装实现。
|
skills 只提供安装指引,不携带独立安装器;首次安装统一使用上述 `install` 入口。
|
||||||
|
|
||||||
## 一条命令更新核心与 skill
|
## 一条命令更新核心与 skill
|
||||||
|
|
||||||
@@ -76,4 +76,4 @@ update 只访问内置官方发行源,先准备核心、同版本完整 skill
|
|||||||
|
|
||||||
统一源码仓库为 `ssh://git@git.dever.cc:2222/shemic/dever.git`。`skills/` 是主仓库中的普通目录,与核心、模板和安装器一起提交、校验及发行;无需初始化 submodule 或另行克隆 skill 仓库。普通应用开发者无需克隆核心或编译 Rust。
|
统一源码仓库为 `ssh://git@git.dever.cc:2222/shemic/dever.git`。`skills/` 是主仓库中的普通目录,与核心、模板和安装器一起提交、校验及发行;无需初始化 submodule 或另行克隆 skill 仓库。普通应用开发者无需克隆核心或编译 Rust。
|
||||||
|
|
||||||
根 `install` 从 `skills/scripts/install.py` 与 `skills/assets/release.pub` 生成;修改安装器后运行 `python3 sdk/install-entry.py`,交付前运行 `python3 sdk/install-entry.py --check`。禁止单独修改生成物中的安装算法。首个新源发行包发布前,默认下载会明确失败;使用真实签名本地包可验证安装,但不能代表线上安装或更新已经通过。
|
根 `install` 是唯一安装源码,直接维护其中的 Bash 入口和内嵌 Python 实现,无需生成步骤。独立固定公钥不得来自下载包;修改后检查 `bash -n install`,并运行 `python3 -B -m unittest discover -s test/onboarding -p 'test_install*.py' -v` 验证参数传递、验签和失败清理。首个新源发行包发布前,默认下载会明确失败;使用真实签名本地包可验证安装,但不能代表线上安装或更新已经通过。
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Install an authenticated Dever release; no application configuration is read."""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path, PurePosixPath
|
|
||||||
import re
|
|
||||||
import ssl
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
RELEASES = "https://git.dever.cc/shemic/dever/dist"
|
|
||||||
MAX_METADATA = 2 * 1024 * 1024
|
|
||||||
MAX_RELEASE = 2 * 1024 * 1024 * 1024
|
|
||||||
ASSET_HOSTS = {"git.dever.cc"}
|
|
||||||
|
|
||||||
|
|
||||||
class OfficialRedirect(urllib.request.HTTPRedirectHandler):
|
|
||||||
def redirect_request(self, request, stream, code, message, headers, address):
|
|
||||||
target = urllib.parse.urlsplit(address)
|
|
||||||
if (target.scheme != "https" or target.hostname not in ASSET_HOSTS
|
|
||||||
or target.username or target.password or target.port not in (None, 443)
|
|
||||||
or target.fragment):
|
|
||||||
raise ValueError("发行下载重定向离开官方 HTTPS 站点")
|
|
||||||
return super().redirect_request(request, stream, code, message, headers, address)
|
|
||||||
|
|
||||||
|
|
||||||
def download(address):
|
|
||||||
# Installation does not inherit HTTP proxy/environment configuration.
|
|
||||||
client = urllib.request.build_opener(
|
|
||||||
urllib.request.ProxyHandler({}), OfficialRedirect(),
|
|
||||||
urllib.request.HTTPSHandler(context=ssl.create_default_context()),
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return client.open(address, timeout=60)
|
|
||||||
except urllib.error.HTTPError as error:
|
|
||||||
if error.code == 404:
|
|
||||||
raise ValueError("该版本/平台尚无官方发行包,未修改已安装的 Dever") from error
|
|
||||||
raise ValueError(f"官方发行下载失败:HTTP {error.code}") from error
|
|
||||||
|
|
||||||
|
|
||||||
def platform_name():
|
|
||||||
host = os.uname()
|
|
||||||
names = {"x86_64": "linux-x86_64", "aarch64": "linux-aarch64"}
|
|
||||||
if host.sysname != "Linux" or host.machine not in names:
|
|
||||||
raise ValueError("安装器当前只支持 Linux x86_64/aarch64,且需对应平台已发布工具包")
|
|
||||||
return names[host.machine]
|
|
||||||
|
|
||||||
|
|
||||||
def prerequisites(platform):
|
|
||||||
if sys.version_info < (3, 9):
|
|
||||||
raise ValueError("安装需要 /usr/bin/python3 3.9 或更新版本")
|
|
||||||
if not os.access("/usr/bin/openssl", os.X_OK):
|
|
||||||
raise ValueError("安装需要 /usr/bin/openssl(支持 Ed25519)")
|
|
||||||
try:
|
|
||||||
libc = os.confstr("CS_GNU_LIBC_VERSION") or ""
|
|
||||||
except (OSError, ValueError):
|
|
||||||
libc = ""
|
|
||||||
match = re.fullmatch(r"glibc ([0-9]+)\.([0-9]+)", libc)
|
|
||||||
if not match:
|
|
||||||
raise ValueError("官方 Linux 工具包需要 glibc,当前系统 ABI 不支持")
|
|
||||||
if platform == "linux-x86_64" and tuple(map(int, match.groups())) < (2, 39):
|
|
||||||
raise ValueError("当前 Linux x86_64 工具包需要 glibc 2.39 或更新版本")
|
|
||||||
|
|
||||||
|
|
||||||
def read_bounded(path, limit):
|
|
||||||
with path.open("rb") as stream:
|
|
||||||
contents = stream.read(limit + 1)
|
|
||||||
if len(contents) > limit:
|
|
||||||
raise ValueError("发行元数据超过大小限制")
|
|
||||||
return contents
|
|
||||||
|
|
||||||
|
|
||||||
def metadata(release, source, selection, platform):
|
|
||||||
for filename in ("manifest.json", "manifest.sig"):
|
|
||||||
if source:
|
|
||||||
contents = read_bounded(source / filename, MAX_METADATA)
|
|
||||||
else:
|
|
||||||
with download(f"{RELEASES}/{selection}/dever-{platform}.{filename}") as stream:
|
|
||||||
contents = stream.read(MAX_METADATA + 1)
|
|
||||||
if len(contents) > MAX_METADATA:
|
|
||||||
raise ValueError("发行元数据超过大小限制")
|
|
||||||
(release / filename).write_bytes(contents)
|
|
||||||
|
|
||||||
|
|
||||||
def authenticate(release, key_path, scratch, platform, requested):
|
|
||||||
key = bytes.fromhex(read_bounded(key_path, 256).decode().strip())
|
|
||||||
signature = bytes.fromhex(read_bounded(release / "manifest.sig", 256).decode().strip())
|
|
||||||
if len(key) != 32 or len(signature) != 64:
|
|
||||||
raise ValueError("发行公钥或签名长度错误")
|
|
||||||
public_der = scratch / "public.der"
|
|
||||||
public_der.write_bytes(bytes.fromhex("302a300506032b6570032100") + key)
|
|
||||||
signature_file = scratch / "signature.bin"
|
|
||||||
signature_file.write_bytes(signature)
|
|
||||||
verified = subprocess.run(
|
|
||||||
["/usr/bin/openssl", "pkeyutl", "-verify", "-pubin", "-keyform", "DER",
|
|
||||||
"-inkey", str(public_der), "-rawin", "-in", str(release / "manifest.json"),
|
|
||||||
"-sigfile", str(signature_file)],
|
|
||||||
env={}, capture_output=True, timeout=15, check=False,
|
|
||||||
)
|
|
||||||
if verified.returncode:
|
|
||||||
raise ValueError("发行签名校验失败,未执行安装程序")
|
|
||||||
manifest = json.loads((release / "manifest.json").read_text())
|
|
||||||
if (set(manifest) != {"format", "version", "platform", "artifacts", "extensions"}
|
|
||||||
or manifest.get("format") != "dever-release-v2" or manifest.get("platform") != platform
|
|
||||||
or not isinstance(manifest["extensions"], list) or len(manifest["extensions"]) > 12):
|
|
||||||
raise ValueError("发行格式或平台不匹配")
|
|
||||||
version = manifest.get("version", "")
|
|
||||||
if not re.fullmatch(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", version):
|
|
||||||
raise ValueError("发行版本不是准确的 major.minor.patch")
|
|
||||||
if requested != "latest" and version != requested:
|
|
||||||
raise ValueError("发行版本与请求不匹配")
|
|
||||||
artifacts = manifest.get("artifacts", [])
|
|
||||||
if not 1 <= len(artifacts) <= 4096:
|
|
||||||
raise ValueError("发行文件数超限")
|
|
||||||
catalog = {}
|
|
||||||
total = 0
|
|
||||||
for artifact in artifacts:
|
|
||||||
name, size, digest = artifact["path"], artifact["bytes"], artifact["sha256"]
|
|
||||||
if (set(artifact) != {"path", "bytes", "sha256"}
|
|
||||||
or not name or name.startswith("/")
|
|
||||||
or any(part in ("", ".", "..") for part in name.split("/"))
|
|
||||||
or any(character in name for character in ("\\", ":", "\0"))
|
|
||||||
or str(PurePosixPath(name)) != name
|
|
||||||
or name in catalog or name in ("manifest.json", "manifest.sig")):
|
|
||||||
raise ValueError("发行包含不安全或重复路径")
|
|
||||||
if type(size) is not int or size < 0 or not re.fullmatch(r"[0-9a-f]{64}", digest):
|
|
||||||
raise ValueError("发行文件长度或摘要错误")
|
|
||||||
total += size
|
|
||||||
if total > MAX_RELEASE:
|
|
||||||
raise ValueError("发行解压长度超过 2 GiB")
|
|
||||||
catalog[name] = artifact
|
|
||||||
if not {"dever-core", "bootstrap/dever", "skills/dever-language/SKILL.md"} <= catalog.keys():
|
|
||||||
raise ValueError("首装包缺少核心、bootstrap 启动器或配套 skill")
|
|
||||||
return version, catalog, key
|
|
||||||
|
|
||||||
|
|
||||||
def copy_artifact(stream, destination, artifact):
|
|
||||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
remaining = artifact["bytes"]
|
|
||||||
digest = hashlib.sha256()
|
|
||||||
with destination.open("xb") as output:
|
|
||||||
while remaining:
|
|
||||||
contents = stream.read(min(remaining, 1024 * 1024))
|
|
||||||
if not contents:
|
|
||||||
raise ValueError("发行文件被截断")
|
|
||||||
output.write(contents)
|
|
||||||
digest.update(contents)
|
|
||||||
remaining -= len(contents)
|
|
||||||
if digest.hexdigest() != artifact["sha256"]:
|
|
||||||
raise ValueError("发行文件摘要校验失败")
|
|
||||||
executable = artifact["path"] in {"dever-core", "bootstrap/dever", "bootstrap/deverd"}
|
|
||||||
destination.chmod(0o755 if executable else 0o644)
|
|
||||||
|
|
||||||
|
|
||||||
def payload(release, source, version, platform, catalog, trusted_key=None):
|
|
||||||
if source:
|
|
||||||
for name, artifact in catalog.items():
|
|
||||||
path = source / name
|
|
||||||
for ancestor in (path, *path.parents):
|
|
||||||
if ancestor.is_symlink():
|
|
||||||
raise ValueError("本地发行文件不能经过符号链接")
|
|
||||||
if ancestor == source:
|
|
||||||
break
|
|
||||||
if not path.is_file() or path.stat().st_size != artifact["bytes"]:
|
|
||||||
raise ValueError("本地发行文件类型或长度错误")
|
|
||||||
with path.open("rb") as stream:
|
|
||||||
copy_artifact(stream, release / name, artifact)
|
|
||||||
return
|
|
||||||
if trusted_key is None:
|
|
||||||
raise ValueError("网络发行解压需要独立信任公钥")
|
|
||||||
# Only signed bytes are executable. Keep the helper separate so Rust can
|
|
||||||
# create_new every archive member and enforce one complete codec/TAR contract.
|
|
||||||
with tempfile.TemporaryDirectory(prefix=".extract-", dir=release.parent) as directory:
|
|
||||||
scratch = Path(directory)
|
|
||||||
helper = scratch / "helper"
|
|
||||||
base = f"{RELEASES}/{version}/dever-{platform}"
|
|
||||||
for name, artifact in catalog.items():
|
|
||||||
if name == "bootstrap/dever" or name.startswith("bootstrap/lib/"):
|
|
||||||
with download(f"{base}.blob-{artifact['sha256']}") as stream:
|
|
||||||
copy_artifact(stream, helper / name, artifact)
|
|
||||||
if stream.read(1):
|
|
||||||
raise ValueError("bootstrap 下载超过签名长度")
|
|
||||||
compressed = scratch / "base.tar.zst"
|
|
||||||
download_archive(f"{base}.tar.zst", compressed)
|
|
||||||
configuration = scratch / "extract.json"
|
|
||||||
configuration.write_text(json.dumps({
|
|
||||||
"release": str(release), "archive": str(compressed),
|
|
||||||
"trusted_key": str(trusted_key),
|
|
||||||
}))
|
|
||||||
subprocess.run([str(helper / "bootstrap/dever"), "--dever-extract", str(configuration)],
|
|
||||||
env={}, check=True, timeout=180)
|
|
||||||
|
|
||||||
|
|
||||||
def download_archive(address, destination):
|
|
||||||
remaining = MAX_RELEASE
|
|
||||||
with download(address) as stream, destination.open("xb") as output:
|
|
||||||
while contents := stream.read(min(remaining + 1, 1024 * 1024)):
|
|
||||||
remaining -= len(contents)
|
|
||||||
if remaining < 0:
|
|
||||||
raise ValueError("发行下载超过 2 GiB")
|
|
||||||
output.write(contents)
|
|
||||||
|
|
||||||
|
|
||||||
def install(arguments, default_key=None):
|
|
||||||
if os.geteuid() != 0:
|
|
||||||
raise ValueError("机器安装需要管理员权限;先检查脚本,再用 sudo python3 执行")
|
|
||||||
platform = platform_name()
|
|
||||||
prerequisites(platform)
|
|
||||||
if arguments.version != "latest" and not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", arguments.version):
|
|
||||||
raise ValueError("--version 需要 latest 或准确版本")
|
|
||||||
system_root = arguments.system_root.absolute()
|
|
||||||
if not system_root.is_dir() or system_root.is_symlink():
|
|
||||||
raise ValueError("--system-root 必须是已存在的真实目录")
|
|
||||||
source = arguments.release.absolute() if arguments.release else None
|
|
||||||
if source and (not source.is_dir() or source.is_symlink()):
|
|
||||||
raise ValueError("--release 必须是已存在的真实发行目录")
|
|
||||||
# The root-owned staging also keeps the pinned key outside the downloaded release.
|
|
||||||
with tempfile.TemporaryDirectory(prefix=".dever-install-", dir=system_root) as directory:
|
|
||||||
scratch = Path(directory)
|
|
||||||
release = scratch / "release"
|
|
||||||
release.mkdir()
|
|
||||||
metadata(release, source, arguments.version, platform)
|
|
||||||
key_path = arguments.trusted_key
|
|
||||||
if key_path is None:
|
|
||||||
key_path = scratch / "independent-release.pub"
|
|
||||||
key_path.write_text(default_key)
|
|
||||||
version, catalog, key = authenticate(release, key_path, scratch, platform, arguments.version)
|
|
||||||
trusted = scratch / "release.pub"
|
|
||||||
trusted.write_text(key.hex() + "\n")
|
|
||||||
payload(release, source, version, platform, catalog, trusted)
|
|
||||||
configuration = scratch / "config"
|
|
||||||
configuration.mkdir()
|
|
||||||
(configuration / "setting.json").write_text(json.dumps({
|
|
||||||
"release": str(release), "system_root": str(system_root),
|
|
||||||
"trusted_key": str(trusted),
|
|
||||||
"activate_service": system_root == Path("/") and not arguments.no_start,
|
|
||||||
}))
|
|
||||||
subprocess.run([str(release / "bootstrap/dever"), "--dever-install", str(scratch)],
|
|
||||||
env={}, check=True, timeout=180)
|
|
||||||
print(f"Dever {version} 已安装到 {system_root / 'opt/dever'}")
|
|
||||||
|
|
||||||
|
|
||||||
def main(default_key=None):
|
|
||||||
parser = argparse.ArgumentParser(prog="dever install", description=__doc__)
|
|
||||||
parser.add_argument("--version", default="latest")
|
|
||||||
parser.add_argument("--release", type=Path, help="使用本地已签名发行目录,不下载")
|
|
||||||
key_path = None if default_key is not None else Path(__file__).resolve().parents[1] / "assets/release.pub"
|
|
||||||
parser.add_argument("--trusted-key", type=Path, default=key_path)
|
|
||||||
parser.add_argument("--system-root", type=Path, default=Path("/"), help="已有的自有安装镜像目录;默认安装当前机器")
|
|
||||||
parser.add_argument("--no-start", action="store_true", help="不启动当前机器 systemd 服务")
|
|
||||||
arguments = parser.parse_args()
|
|
||||||
try:
|
|
||||||
install(arguments, default_key)
|
|
||||||
except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as error:
|
|
||||||
parser.exit(1, f"Dever 安装失败:{error}\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Read the real installer for focused tests; no installation logic is copied."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
ENTRY = ROOT / "install"
|
||||||
|
|
||||||
|
|
||||||
|
def entry_parts():
|
||||||
|
entry = ENTRY.read_text()
|
||||||
|
opening = "<<'DEVER_INSTALL_PYTHON'\n"
|
||||||
|
closing = "\nDEVER_INSTALL_PYTHON\n"
|
||||||
|
if entry.count(opening) != 1 or entry.count(closing) != 1:
|
||||||
|
raise ValueError("root install must contain exactly one Python heredoc")
|
||||||
|
before, remainder = entry.split(opening)
|
||||||
|
source, after = remainder.split(closing)
|
||||||
|
return before + opening, source, closing + after
|
||||||
|
|
||||||
|
|
||||||
|
def load_installer():
|
||||||
|
_, source, _ = entry_parts()
|
||||||
|
module = ModuleType("dever_installer")
|
||||||
|
exec(compile(source, str(ENTRY) + "::python", "exec"), module.__dict__)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def entry_with_python(probe):
|
||||||
|
"""Keep the actual shell wrapper while a harmless probe observes its contract."""
|
||||||
|
before, _, after = entry_parts()
|
||||||
|
return before + probe.rstrip("\n") + after
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Standalone Bash contract; subprocesses never install or change services."""
|
"""Standalone Bash contract; subprocesses never install or change services."""
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -8,11 +7,10 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from install_support import entry_with_python, load_installer
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
spec = importlib.util.spec_from_file_location("dever_install_entry", ROOT / "sdk/install-entry.py")
|
|
||||||
generator = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(generator)
|
|
||||||
|
|
||||||
|
|
||||||
class InstallEntryTests(unittest.TestCase):
|
class InstallEntryTests(unittest.TestCase):
|
||||||
@@ -20,10 +18,10 @@ class InstallEntryTests(unittest.TestCase):
|
|||||||
return subprocess.run(["/bin/bash", "-s", "--", *arguments], input=script,
|
return subprocess.run(["/bin/bash", "-s", "--", *arguments], input=script,
|
||||||
env={}, text=True, capture_output=True, timeout=10, **kwargs)
|
env={}, text=True, capture_output=True, timeout=10, **kwargs)
|
||||||
|
|
||||||
def test_generated_entry_matches_source_and_independent_key(self):
|
def test_root_entry_pins_the_independent_key_and_has_valid_shell_syntax(self):
|
||||||
expected = generator.render((ROOT / "skills/scripts/install.py").read_text(),
|
installer = load_installer()
|
||||||
(ROOT / "skills/assets/release.pub").read_text())
|
self.assertEqual(installer.DEFAULT_PUBLIC_KEY,
|
||||||
self.assertEqual((ROOT / "install").read_text(), expected)
|
(ROOT / "skills/assets/release.pub").read_text().strip())
|
||||||
syntax = subprocess.run(["/bin/bash", "-n", str(ROOT / "install")], env={},
|
syntax = subprocess.run(["/bin/bash", "-n", str(ROOT / "install")], env={},
|
||||||
text=True, capture_output=True, timeout=10)
|
text=True, capture_output=True, timeout=10)
|
||||||
self.assertEqual(syntax.returncode, 0, syntax.stderr)
|
self.assertEqual(syntax.returncode, 0, syntax.stderr)
|
||||||
@@ -45,19 +43,17 @@ class InstallEntryTests(unittest.TestCase):
|
|||||||
def test_arguments_python_isolation_and_exit_code_are_preserved(self):
|
def test_arguments_python_isolation_and_exit_code_are_preserved(self):
|
||||||
source = '''import json
|
source = '''import json
|
||||||
import sys
|
import sys
|
||||||
def main(default_key):
|
print(json.dumps([sys.argv[1:], sys.flags.isolated]))
|
||||||
print(json.dumps([sys.argv[1:], default_key, sys.flags.isolated]))
|
raise SystemExit(37)
|
||||||
raise SystemExit(37)
|
|
||||||
'''
|
'''
|
||||||
key = "ab" * 32
|
script = entry_with_python(source)
|
||||||
script = generator.render(source, key)
|
|
||||||
arguments = ["--release", "/a path/with 'quotes'", "", "$(not executed)", "*", "--no-start"]
|
arguments = ["--release", "/a path/with 'quotes'", "", "$(not executed)", "*", "--no-start"]
|
||||||
result = self.run_bash(script, *arguments)
|
result = self.run_bash(script, *arguments)
|
||||||
self.assertEqual(result.returncode, 37, result.stderr)
|
self.assertEqual(result.returncode, 37, result.stderr)
|
||||||
self.assertEqual(json.loads(result.stdout), [arguments, key, 1])
|
self.assertEqual(json.loads(result.stdout), [arguments, 1])
|
||||||
|
|
||||||
def test_truncated_body_does_not_execute_python_or_sudo(self):
|
def test_truncated_body_does_not_execute_python_or_sudo(self):
|
||||||
script = generator.render('def main(default_key):\n print("EXECUTED")\n', "ab" * 32)
|
script = entry_with_python('print("EXECUTED")\n')
|
||||||
for end in (script.index("import sys"), script.index("DEVER_INSTALL_PYTHON\n}"),
|
for end in (script.index("import sys"), script.index("DEVER_INSTALL_PYTHON\n}"),
|
||||||
script.index('\ndever_install "$@"')):
|
script.index('\ndever_install "$@"')):
|
||||||
result = self.run_bash(script[:end])
|
result = self.run_bash(script[:end])
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from install_support import load_installer
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ def load_module(name, path):
|
|||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
installer = load_module("dever_installer", ROOT / "skills/scripts/install.py")
|
installer = load_installer()
|
||||||
assets = load_module("dever_release_assets", ROOT / "sdk/release-assets.py")
|
assets = load_module("dever_release_assets", ROOT / "sdk/release-assets.py")
|
||||||
|
|
||||||
|
|
||||||
@@ -343,10 +345,9 @@ class InstallerTests(unittest.TestCase):
|
|||||||
installer.prerequisites("linux-x86_64")
|
installer.prerequisites("linux-x86_64")
|
||||||
|
|
||||||
@unittest.skipUnless(installer.os.geteuid() == 0, "owned image installation requires root")
|
@unittest.skipUnless(installer.os.geteuid() == 0, "owned image installation requires root")
|
||||||
def test_standalone_bash_uses_embedded_key_and_cleans_after_signature_failure(self):
|
def test_standalone_bash_requires_trusted_key_and_cleans_after_signature_failure(self):
|
||||||
# Execute an independently signed fixture to observe the entire entry
|
# Execute an independently signed fixture to observe the entire entry
|
||||||
# and bootstrap call chain, without installing a machine or service.
|
# and bootstrap call chain, without installing a machine or service.
|
||||||
generator = load_module("dever_install_entry", ROOT / "sdk/install-entry.py")
|
|
||||||
launcher = self.release / "bootstrap/dever"
|
launcher = self.release / "bootstrap/dever"
|
||||||
launcher.write_text('''#!/usr/bin/python3
|
launcher.write_text('''#!/usr/bin/python3
|
||||||
import json
|
import json
|
||||||
@@ -364,10 +365,16 @@ assert setting["activate_service"] is False
|
|||||||
artifact.update(bytes=len(content), sha256=hashlib.sha256(content).hexdigest())
|
artifact.update(bytes=len(content), sha256=hashlib.sha256(content).hexdigest())
|
||||||
(self.release / "manifest.json").write_text(json.dumps(manifest))
|
(self.release / "manifest.json").write_text(json.dumps(manifest))
|
||||||
self.sign_manifest()
|
self.sign_manifest()
|
||||||
entry = generator.render((ROOT / "skills/scripts/install.py").read_text(), self.public.read_text())
|
entry = (ROOT / "install").read_text()
|
||||||
image = self.directory("image")
|
image = self.directory("image")
|
||||||
arguments = ["/bin/bash", "-s", "--", "--release", str(self.release),
|
arguments = ["/bin/bash", "-s", "--", "--release", str(self.release),
|
||||||
"--system-root", str(image), "--version", "1.2.3", "--no-start"]
|
"--system-root", str(image), "--version", "1.2.3", "--no-start"]
|
||||||
|
untrusted = subprocess.run(arguments, input=entry, env={}, text=True,
|
||||||
|
capture_output=True, timeout=20)
|
||||||
|
self.assertNotEqual(untrusted.returncode, 0)
|
||||||
|
self.assertIn("签名校验失败", untrusted.stderr)
|
||||||
|
self.assertEqual(list(image.iterdir()), [])
|
||||||
|
arguments.extend(["--trusted-key", str(self.public)])
|
||||||
completed = subprocess.run(arguments, input=entry, env={}, text=True,
|
completed = subprocess.run(arguments, input=entry, env={}, text=True,
|
||||||
capture_output=True, timeout=20)
|
capture_output=True, timeout=20)
|
||||||
self.assertEqual(completed.returncode, 0, completed.stderr)
|
self.assertEqual(completed.returncode, 0, completed.stderr)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
"""Opt-in real Bash installer acceptance in an owned image, without host services."""
|
"""Opt-in root install acceptance in an owned image, without host services."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -16,7 +16,7 @@ from verify_prepared_release import WORKSPACE, execute
|
|||||||
def verify(args):
|
def verify(args):
|
||||||
result = {
|
result = {
|
||||||
"status": "running", "checks": [],
|
"status": "running", "checks": [],
|
||||||
"transport": "local signed assets; actual generated Bash entry and Rust bootstrap",
|
"transport": "local signed assets; actual root Bash entry and Rust bootstrap",
|
||||||
"online_install_and_update": "not_run",
|
"online_install_and_update": "not_run",
|
||||||
"entry_sha256": hashlib.sha256(args.entry.read_bytes()).hexdigest(),
|
"entry_sha256": hashlib.sha256(args.entry.read_bytes()).hexdigest(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import importlib.util
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -13,6 +12,9 @@ import signal
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from install_support import load_installer
|
||||||
|
|
||||||
|
|
||||||
WORKSPACE = Path(__file__).resolve().parents[2]
|
WORKSPACE = Path(__file__).resolve().parents[2]
|
||||||
OS_LIBRARIES = (
|
OS_LIBRARIES = (
|
||||||
"libc.so.6", "libdl.so.2", "libm.so.6", "libpthread.so.0", "librt.so.1",
|
"libc.so.6", "libdl.so.2", "libm.so.6", "libpthread.so.0", "librt.so.1",
|
||||||
@@ -195,10 +197,7 @@ def ordinary_uid_target_reuse(image):
|
|||||||
def install_assets(assets, image, manifest):
|
def install_assets(assets, image, manifest):
|
||||||
# Only transport is local: signature verification, full tar extraction and
|
# Only transport is local: signature verification, full tar extraction and
|
||||||
# the final bootstrap executable are the real first-install implementation.
|
# the final bootstrap executable are the real first-install implementation.
|
||||||
script = WORKSPACE / "skills/scripts/install.py"
|
installer = load_installer()
|
||||||
specification = importlib.util.spec_from_file_location("dever_release_installer", script)
|
|
||||||
installer = importlib.util.module_from_spec(specification)
|
|
||||||
specification.loader.exec_module(installer)
|
|
||||||
stem = f"dever-{manifest['platform']}"
|
stem = f"dever-{manifest['platform']}"
|
||||||
base = installer.RELEASES
|
base = installer.RELEASES
|
||||||
routes = {
|
routes = {
|
||||||
@@ -239,7 +238,7 @@ def accept(release, assets, image, report, init, extension_helper=None):
|
|||||||
install_assets(assets, image, manifest)
|
install_assets(assets, image, manifest)
|
||||||
else:
|
else:
|
||||||
execute([
|
execute([
|
||||||
"/usr/bin/python3", WORKSPACE / "skills/scripts/install.py",
|
"/bin/bash", WORKSPACE / "install",
|
||||||
"--release", release, "--version", manifest["version"],
|
"--release", release, "--version", manifest["version"],
|
||||||
"--system-root", image,
|
"--system-root", image,
|
||||||
])
|
])
|
||||||
|
|||||||
Reference in new issue
Block a user