feat: unify source and skills for Dever 0.1.2 installation

This commit is contained in:
Antigravity committed 2026-10-05 02:49:17 +00:00
1 parent a2e0436ed4
commit 19f122b4b4
47 files changed
+1951 -63

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
[submodule "skills"]
path = skills
url = https://github.com/shemic/dever-main-skills.git
+2 -1
View File
@@ -24,10 +24,11 @@ Applies to AST/HIR contract metadata, typed LLVM/resource/coroutine lowering, pr
- Functions: `name(inputs) (outputs) pure recover("reason")`; modifiers optional, identical across all clauses of one signature. Source has no `async` modifier or `await(...)` form.
- Transaction functions use `transaction name(inputs) (outputs)` and infer a single physical database effect through their complete call graph; source never receives a transaction handle. Sequential `blocking` is allowed only through its existing synchronous/no-database/no-concurrency contract; detached, parallel, stream and channel work remain forbidden.
- Model files synthesize opaque `model.id`, generated `id/created_at`, a record Page type and static short CRUD symbols. Anonymous Model blocks are contextual syntax, not general Map values.
- `Program::api_snapshot() -> String`, `check_api(&str) -> Result<(), String>`, `application_warnings(entry) -> Result<Vec<Diagnostic>, String>`.
- `Program::api_snapshot() -> String`, `check_api(&str) -> Result<(), String>`, `application_errors() -> Result<Vec<Diagnostic>, String>`, `application_warnings(entry) -> Result<Vec<Diagnostic>, String>`.
- Application tests use `test/<component>/<domain>/<topic>.dever[.md]`; the filename selects the one ordinary zero-input, zero-output `<topic>() ()` entry. Test helpers/types remain private to that file. Tests call same-domain App/Domain and cross-domain App only; they cannot call Model operations, Port, Adapter, API or another test, and production cannot call tests.
- Test assertions are compiler-owned `assert(Bool)` and `assert_eq(actual, expected)`. Equality requires the same comparable type; only `null`/empty expressions that already require context receive the left type, so numeric promotion does not make unequal source types valid. Business failures continue through `result(call)` and explicit recovery.
- CLI: `check <root>`, `test <root>`, `run <root> [--tenant <id>] [-- <cmd> <json>]`, `build <root> --output <new-file>`, `tenant migrate <root> <id>`, `tenant owner <root> <id> <site> <user-id>` and `tenant component enable|disable <root> <id> <component>`. Run/build collect checked API/CMD/Job declarations into one generated entry and reject application `main.dever`. Test checks `module/ + test/` before running stable-sorted cases; check/run/build load only module/. Deployment and database settings come only from `config/setting.json`.
- CLI check and application compilation share `Program::application_errors()` before resource preparation: C016 rejects unreachable App/Domain functions and recognized trivial private forwarders or Model CRUD wrappers, not every wrapper shape. Check remains static and offline, with no Worker preparation, runtime pack, native compilation, service or database execution. Test retains its case-local roots and Port fakes without applying production reachability; core library checking still permits exports whose consumers are unknown.
- Module API files contain declarations such as `get profile = app.profile`, `post publish = app.publish`, `cmd bootstrap = app.bootstrap` and `rest model`; they do not contain handler function bodies. HTTP routes inherit the App signature and return the fixed envelope. `public` is valid only on one HTTP declaration. Recursive `api/<site-path>/<topic>.dever` supplies configured site ownership without source `site` statements.
- Authentication providers, sites, external HTTP origins, Cookie names/lifetimes and tenant routing come only from `config/setting.json`. Verify is one read-only App function from `Claims` to an App-owned record with exactly `id: Text`, `user_id: ModelId?`, and `tenant_id: ModelId?`; it cannot return permissions. A protected site's generated entry verifies its configured provider, enters trusted tenant scope, checks every reachable tenant component and authorizes the exact permission before reading business input or dispatching App. A `public` declaration ignores an ambient browser Cookie, but an explicitly supplied invalid `Authorization` credential remains an authentication error.
+13 -5
View File
@@ -30,7 +30,8 @@ Process main is separate from callable roots. Initialize Unix SIGPIPE policy bef
## 2. Signatures
- Project creation: `dever new <new-directory> [--markdown]`; `project::create(&Path, bool)` consumes the single template source under the pinned `skills/assets/template` submodule. The machine launcher selects the active core without reading nonexistent target configuration.
- 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.
- 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.
- Application target: `dever build <root> --output <new-file> [--target linux-x86_64|linux-aarch64]`; explicit dependency preparation accepts the same target on `dever lib add/install/update/doctor`. Omission selects `BuildTarget::host()`. `CompileRequest.target: BuildTarget` is required and serialized canonically; test requests must use the host.
@@ -81,15 +82,17 @@ Process main is separate from callable roots. Initialize Unix SIGPIPE policy bef
## 3. Contracts
Developer onboarding has two official repositories: `shemic/dever-main` and `shemic/dever-main-skills`. The skill directory is an independent Git repository, pinned by the compiler's submodule; it distinguishes this language from Go Dever. Author `skill` is a required list of `{source,path,sha256}` entries published as signed `skills/dever-language/<path>` artifacts, including SKILL.md; at most 256 files, 1 MiB each and 8 MiB total. New-project source bytes come from the same skill templates; Markdown documentation wraps them without a second semantic copy. Keep the generated configuration CMD-only and free of database/auth prerequisites.
Developer onboarding uses one official source repository, `ssh://git@git.dever.cc:2222/shemic/dever.git`. Its ordinary `skills/` directory is committed and versioned with the compiler, without a submodule or separate skill repository; it distinguishes this language from Go Dever. Author `skill` is a required list of `{source,path,sha256}` entries published as signed `skills/dever-language/<path>` artifacts, including SKILL.md; at most 256 files, 1 MiB each and 8 MiB total. New-project source bytes come from the same skill templates; Markdown documentation wraps them without a second semantic copy. Keep the generated configuration CMD-only and free of database/auth prerequisites.
Official `install` and `update` explicitly download from the compiled GitHub Releases origin; neither project setting.json nor environment variables choose that origin. `dever-release-v2` binds base `artifacts` and optional `extensions` in one signature. Each platform publishes `dever-<platform>.manifest.json`, `.manifest.sig`, `.tar.zst`, plus `.ext-<kind>-<target>.tar.zst`, under immutable version tags `v<major.minor.patch>`. Kind is runtime-pip/npm/go, build-pip/npm, or target; target holds nonhost native runtime/CRT and sandbox. Validate closed IDs, path namespaces, duplicate/overlapping paths, digests and size/count limits in the shared catalog owner. Base retains all four host database profiles, compiler closure, bootstrap, host sandbox and skill. Extensions cannot override base files. Do not fabricate an unsigned merged manifest.
Official `install`, `update` and extension preparation explicitly download from the compiled `https://git.dever.cc/shemic/dever/dist` origin; neither project setting.json nor environment variables choose that origin. Metadata uses `latest/` or `<major.minor.patch>/`; after authentication every payload uses the exact signed version directory, without `v` or `download/`. `dever-release-v2` binds base `artifacts` and optional `extensions` in one signature. Each platform publishes `dever-<platform>.manifest.json`, `.manifest.sig`, `.tar.zst`, plus `.ext-<kind>-<target>.tar.zst`, under immutable version directories. Kind is runtime-pip/npm/go, build-pip/npm, or target; target holds nonhost native runtime/CRT and sandbox. Validate closed IDs, path namespaces, duplicate/overlapping paths, digests and size/count limits in the shared catalog owner. Base retains all four host database profiles, compiler closure, bootstrap, host sandbox and skill. Extensions cannot override base files. Do not fabricate an unsigned merged manifest.
Authenticate the catalog before extraction. Zstandard is a single checksummed frame with window_log_max=27; verify complete compressed EOF/trailer and bounded expansion even after TAR EOF. USTAR contains only its signed ordinary artifact files, with no duplicate, escaping or extra entries; require complete terminator/padding and reject trailing content. Redirects remain on fixed official HTTPS hosts. Publication never replaces another writer's directory. First-install raw `.blob-<sha256>` assets include the launcher and every bootstrap/lib dependency, independently digest-checked before execution. The launcher owns extraction; Python must not implement a second TAR/Zstd parser or require system zstd.
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. It 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 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.
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.
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.
@@ -244,6 +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.
- 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.
- 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.
@@ -314,7 +318,7 @@ Cache miss -> compile and publish; changed inputs -> separate key; incomplete/mo
## 5. Good / Base / Bad Cases
Onboarding base: `dever new app` followed by `run app -- hello.greeting.greet '{"name":"Dever"}'` returns the standard envelope with `data:"Hello, Dever"`. Good: --markdown generates equivalent checked declarations; two projects/users share core and skill. Bad: manual main.dever scaffolding, Go service/Page JSON templates, update URLs in project setting.json, or a global skill copied once and allowed to drift from the installed compiler.
Onboarding base: `dever new app` followed by `run app -- hello.greeting.greet '{"name":"Dever"}'` returns the standard envelope with `data:"Hello, Dever"`. Good: --markdown generates equivalent checked declarations; two projects/users share core and skill. Installation good: standalone Bash installs a verified local release into an owned image, then pipe reinstall preserves it. Bad: manual main.dever scaffolding, Go service/Page JSON templates, update URLs in project setting.json, a Git login page served as install, or a global skill copied once and allowed to drift from the installed compiler.
Cross build good: an x86 core uses ARM profiles, ARM Worker assets and x86 Go tools emitting ARM objects. Base: omitted target retains native behavior. Bad: replacing global host identity, executing an ARM build tool on x86, labeling x86 native extensions ARM, or deriving target loader permissions from the host.
@@ -342,6 +346,8 @@ 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 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.
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.
Native release changes select `native_release` plus affected private pack/installation checks. Default cases cover deterministic bytes/signature, private-copy ownership, malformed configuration/path/digest/key rejection, cleanup, and concurrent no-overwrite publication. Explicit `native_release::acceptance` consumes prepared optimized archives and author executable; inspect SQLite/PostgreSQL symbols, then repeat packaging, verify signed installation, compile/run all four profiles and run independent outputs after source removal/daemon stop. Report PG-server and cross-host source rebuild evidence separately. Keep native checks and Cargo serial under constrained disk space.
@@ -400,6 +406,8 @@ 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.
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.
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.
Native authoring wrong: discover a compiler in PATH while signing, overwrite a completed release, or strip a hard-linked archive in place. Correct: separate the recorded source-build recipe from deterministic copy/verify/sign, preserve link symbols, and publish a wholly new directory with the existing signed installation contract.
@@ -0,0 +1,2 @@
{"file":".trellis/spec/backend/toolchain-and-library.md","reason":"Signed install and source contract"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"Validation boundaries and meaningful tests"}
@@ -0,0 +1,19 @@
# 设计
## 唯一安装实现
保留 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 -,参数原样转发。完整函数定义在最后调用前解析,截断的主体不执行安装。
生成器注入独立固定公钥而不是从下载包取公钥;源 skill 的普通 Python 调用仍使用配套公钥文件。Python main 接受内嵌默认公钥参数;具体嵌入使用清晰的源码封装,避免手工复制第二份算法或脆弱的源码字符串替换。
默认版本 latest;元数据为 dist/latest/dever-<platform>.manifest.json 与 .sig;验签后只从 dist/<signed-version>/ 获取基础 archive、raw blobs 和扩展。Rust ReleaseSource 与 Python 使用同一静态协议。只允许官方 HTTPS 同站重定向,禁止凭据、非 443、fragment 和外站。没有环境变量/项目配置覆盖和旧源 fallback。
## 行为与验证边界
机器布局与 bootstrap 事务不变,解压保持在 Rust owner,不增加 shell tar/zstd 解析。帮助不联网、不提权;非 root 安装使用系统 sudo,保留退出状态。沿用 --release、--trusted-key、--system-root、--no-start 做离线镜像验收。
现有真实 0.1.1 split assets 可验证新入口和安装生命周期;旧二进制内部仍指向 GitHub,不能据此声称新源线上 update 已通过。新版本构建发布属后续步骤。服务器须显式映射 /shemic/dever/install 到纯脚本;Git 页面或 raw 登录跳转不是安装脚本。
## 资源
磁盘不足 1 GiB,隔离镜像优先放 /dev/shm 自有临时目录并及时清理,仅保留报告。源码备份 target/install-before.WTVfLC。不得删除其他缓存、改变系统服务或修改 AppArmor 全局状态。仅修改安装、发行源、相关测试和必要文档,不做语言与 skill 约束的邻接改造。
@@ -0,0 +1,3 @@
{"file":".trellis/spec/backend/index.md","reason":"Rust compiler scope and checklist"}
{"file":".trellis/spec/backend/toolchain-and-library.md","reason":"Signed distribution, bootstrap and single ownership"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"Focused offline testing and temporary ownership"}
@@ -0,0 +1,52 @@
# 执行与证据
## 授权与起点
- 已在方案与「先实现 install、统一新源」说明之后授权实施并同意任务登记,不重复询问。
- 主仓库 a2e0436、skill 27ac8f2 起点均 clean。
- 备份 target/install-before.WTVfLC/core.tar.gz 与 skills.tar.gz。
## 顺序与所有权
1. [x] trellis-implement:Python 安装器、生成器、根 install、Rust ReleaseSource、根 test/ 定向测试及既有验收 harness 路由。不修改文档、spec、Git 元数据、版本号。
2. [x] 主会话:README、安装说明、SDK 发布说明与规范;真实签名包的新入口自有镜像验收,不修改全局服务。
3. [x] trellis-check:独立检查完整 diff,修复确有的安装/边界/测试缺陷,核验生成物一致性。
4. [x] 主会话:关键证据、必要复验、diff/资源清理、实现验收收口;准确说明线上端点与新包未发布。提交归档随后续迁移统一处理。
## 定向验证
- bash -n install;实际 Bash 文件/管道帮助和失败路径。
- Python onboarding installer 与新增入口定向测试。
- cargo test --offline -p dever-cli --lib toolchain::release_source;修改文件格式检查,不跑全量。
- 真实旧 0.1.1 签名基础包,通过 bash install --release ... --system-root ... --no-start 安装自有镜像。旧二进制不证明新源线上 update。
- 生成器 --check、git diff --check、skill 文档检查。
## 结果
- Python 定向 `python3 -B -m unittest discover -s test/onboarding -p 'test_install*.py' -v`:19/19 通过。包含实际 Bash、真实 Ed25519、独立公钥、字面参数与退出状态、非 root UID help 和进程内 sudo 拦截;没有实际提权/全局服务操作。
- Rust `cargo test --offline -p dever-cli --lib toolchain::release_source`:10/10 通过。实际本地 HTTP 传输断言 latest 两份元数据后从签名准确版本取 archive。复用 target/debug,未跑全量。
- 原有作者 rustfmt 对修改 Rust 文件检查通过。主会话复核生成 --check、bash -n、主/skill diff --check 和 skill quick_validate 均通过。
- `test/onboarding/verify_install_entry.py` 真实 0.1.1 签名资产验收 4/4 通过,报告 `target/install-entry-acceptance.json`:Rust 验签解包准备、独立 Bash 首装且 core/skill 对应、管道重装保持核心与选择、坏签名拒绝且原 journal/core/bootstrap 不变。自有 /dev/shm 临时目录已清理,没有启动全局服务。
- 初次验收报告保留为 `target/install-entry-acceptance-initial.json`。首装通过后,重复安装因测试误要求 active-version 字节完全相同失败;实际 `bootstrap/transaction.rs` 明确追加激活记录。仅修正验收:重复安装比较实际选中版本与负载字节,失败安装仍严格比较 journal 字节。生产安装行为未放宽,重跑完整4阶段通过。
- 根 install 为 13,938 字节,摘要 `3cab983b5093c4a7075f28707042c672a00ce754888ac3e7c9f85853d40ae7eb`;真实验收使用旧版负载,不表示新版本构建、线上安装或线上 update。
- 任务分支 `feat/install-entry`;未提交、推送、改 Git remote 或合并 skills 子仓库。后续迁移发布需新版本 core/launcher/daemon 与完整配套 skill,部署匿名 HTTPS 安装/资产路由。
- 独立 trellis-check 最终无阻塞、未修改产品文件;再次核对生成脚本与真实验收报告 SHA 完全一致,认可 bootstrap 同版本激活记录的测试边界。全部本次验收已满足,未跑全量 clippy、全量 CI 或线上安装。
## 合仓与发行阶段(2026-10-05)
用户已批准前述后续交付方案,继续使用本任务记录,不重复请求创建任务或实施批准。
- [ ] 主会话:当前源码和 Git 历史备份;skills 普通目录化;新远端核验、提交推送;同步安装/源码文档。
- [ ] 发行 worker:准备新版本精简发行,复用既有制作和验收脚本,报告实际包体;不操作 Git、全局安装或服务。
- [ ] 主会话:核对新包及匹配 skill,发布静态端点并完成范围内首装/update 验收。
- [ ] 独立检查、提交、任务证据与归档;部署缺失信息明确记录,未完成的发行项不标成通过。
初始只读证据:新 SSH 仓库 ls-remote 成功且无 refs;HTTPS /shemic/dever/install 返回 404。已询问站点部署方式,等待期间继续合仓与可离线完成的新包准备。系统盘剩约 368 MiB,/dev/shm 约 2.4 GiB,优先复用资产和缓存,不能删除其它项目或部署数据。
### 当前证据
- 备份目录 `target/migration-backup.pBIq3U`:当前源码 tar.gz(不含构建物、Git元数据、密钥与本地配置)、完整 `core.bundle` 和 `skills.bundle` 均验证通过;原 `skills/.git` 移入此目录,保留本地历史和配置。
- `skills/` 已从 gitlink 改成主仓库 22 个普通文件,删除唯一 `.gitmodules` 及本地 submodule 配置。`origin` 已切至新 SSH 地址,旧远端以 `github` 保留;没有向旧远端推送。
- 生成 install `--check`、Bash语法、skill结构和diff检查通过;同仓说明、模板和新源文档已同步。
- 此前 skill约束和CLI接线一并进入本版:`check/run/build` 共用 C016;CLI新增5项及既有1项定向回归通过,CMS普通/Markdown静态check均通过(各7条既有W001)。不扩大冗余判定、不声称所有CRUD包装都可识别。
- 发行版本设为0.1.2,core/launcher/daemon在自有tmpfs目录串行离线重建;不重编未改的runtime。为本轮发行空间,Cargo仅清理本地dever-cli dev构建产物,释放236.7MiB,可从源码恢复;旧release、作者输入和备份保留。
+38
View File
@@ -0,0 +1,38 @@
# Dever 单命令安装入口与官方源迁移
## Goal
实现自包含 Bash install,复用签名安装器并统一 git.dever.cc 下载源,完成隔离定向验证
## 背景与授权
用户要求先实现 install,再处理新 Git 的迁移发布;已收到方案后明确要求实施,并于 2026-10-05 同意任务登记。已有签名 Python 安装器、Rust 解压/bootstrap 和配套 skill 打包可以复用。新 Git SSH 已可访问,远端尚无 refs;HTTPS /install 与公开发行资产尚未部署。
## Requirements
- R1:根 install 可直接通过 Bash 文件或管道执行,无需克隆源码或先安装 skill;支持现有版本选择、本地发行目录、自有镜像和不启动服务选项。
- R2:复用唯一签名安装器和 Rust 解压/bootstrap。独立公钥、签名、摘要、长度、路径验证和失败恢复不得削弱;非 root 使用 sudo,帮助不需要管理员权限。
- R3:首次安装、机器 install/update、按需扩展统一使用 https://git.dever.cc/shemic/dever/dist;latest 只选择签名版本,负载取准确版本目录;不从业务配置或环境变量选择官方源。
- R4:普通 run/build 仍离线,机器共享布局、按需扩展、版本与 skill 同步保持原有合同。安装前检查必要 OS/ABI/工具,清晰报告不支持或未发布。
- R5:安装说明准确区分代码完成与服务器发布;源码下载源迁移不代表已有 0.1.1 二进制已更改。
## Acceptance Criteria
- [x] Bash 语法、直接/管道 help、参数转发、退出状态、非 root 提权与截断脚本行为有最小定向验证。
- [x] 生成物与现有安装器/独立公钥一致,不存在手工维护的第二套安装逻辑。
- [x] Python/Rust 新源 latest/精确版本/扩展地址一致,拒绝非法重定向。
- [x] 真实 Ed25519 校验、损坏拒绝、临时文件清理保持通过;实际签名包可通过新入口安装到自有镜像,不启动宿主服务。
- [x] 必要定向测试和 diff 检查通过,文档说明命令、发布目录、旧版迁移和线上未验证边界。
## 本次边界
先完成 install 及必需下载链路,不执行 Git 迁移/提交/push、skills 子仓库并入、全局安装、Web 服务配置、全量 CI 或正式新版本构建发布。后续必须重建包含新源的二进制,不能将旧包重新标号。源仓库访问限制不变,安装端点与资产需要匿名 HTTPS。
## 2026-10-05 后续交付授权
用户在合仓、Git 迁移、精简发行和线上安装更新验收方案后回复「可以」,本阶段继续实施以下交付。上一阶段的 Git/发布排除仅描述当时范围,不再阻止本阶段工作;仍不运行全量测试、不替换作者机全局服务或安装。
- [ ] 备份当前源码与 skills Git 历史,将 skills 作为普通目录并入主仓库,保留当前约束和模板。
- [ ] 主仓库迁移至 ssh://git@git.dever.cc:2222/shemic/dever.git,提交并推送;不覆盖远端已有历史。
- [ ] 用新版本重建 core/launcher/daemon,复用未改变的已验证 runtime 输入,制作精简基础包、匹配 skill 与可选扩展,记录体积与摘要。
- [ ] 发布安装脚本和签名资产,验证真实 HTTPS 首装与 update 的版本/skill 一致性;若部署接入缺失,保留可发布资产并明确缺失信息,不把本地验收当作上线成功。
@@ -0,0 +1,26 @@
{
"id": "install-entry",
"name": "install-entry",
"title": "Dever 单命令安装入口与官方源迁移",
"description": "实现自包含 Bash install,复用签名安装器并统一 git.dever.cc 下载源,完成隔离定向验证",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "Antigravity",
"assignee": "Antigravity",
"createdAt": "2026-10-05",
"completedAt": null,
"branch": "feat/install-entry",
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
Generated
+6 -6
View File
@@ -430,7 +430,7 @@ dependencies = [
[[package]]
name = "dever-backend-bridge"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"cc",
"dever-core",
@@ -443,7 +443,7 @@ dependencies = [
[[package]]
name = "dever-cli"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"base64 0.22.1",
"csv",
@@ -471,7 +471,7 @@ dependencies = [
[[package]]
name = "dever-core"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"dever-runtime",
"pulldown-cmark",
@@ -481,7 +481,7 @@ dependencies = [
[[package]]
name = "dever-runtime"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"argon2",
"base64 0.22.1",
@@ -514,7 +514,7 @@ dependencies = [
[[package]]
name = "dever-sandbox"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"command-fds",
"goblin",
@@ -527,7 +527,7 @@ dependencies = [
[[package]]
name = "dever-tests"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"bytes",
"dever-core",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/dever-core", "crates/dever-runtime", "crates/dever-cli", "cra
resolver = "3"
[workspace.package]
version = "0.1.1"
version = "0.1.2"
edition = "2024"
rust-version = "1.95"
publish = false
+5 -3
View File
@@ -72,8 +72,8 @@ Linux 开发环境运行构建产物:
```
- `new`:从随版本验证的模板原子创建新项目,不覆盖已有目录;`--markdown` 生成等价 Markdown 源码。
- 机器版 `update`:从官方 GitHub Releases 显式下载并校验,统一更新活动核心与对应 skill;不修改业务配置。`skill path [project-root]` 返回经验证的配套指引,`skill install <new-directory>` 安装稳定的 AI 加载入口。项目可锁定 dever.version,普通项目命令不会触发更新。
- `check`:加载全部源码并完成语法、名称、类型和分句检查。
- 机器版 `update`:从内置的 `https://git.dever.cc/shemic/dever/dist` 显式下载并校验,统一更新活动核心与对应 skill;不修改业务配置。`skill path [project-root]` 返回经验证的配套指引,`skill install <new-directory>` 安装稳定的 AI 加载入口。项目可锁定 dever.version,普通项目命令不会触发更新。
- `check`:加载生产源码,完成语法、名称、类型、分句及应用入口可达性和冗余硬检查;不执行应用或准备运行资源。
- `fmt --check`:只检查格式,不修改文件。
- `fmt`:统一格式化项目 `module/` 和存在的 `test/`;需要修复格式时单独执行 `dever fmt my_app`。
- `test`:检查生产源码与应用测试,再按名称串行运行每个隔离用例;没有 `test/` 时报告 0 个测试并成功。
@@ -1514,7 +1514,9 @@ dever check my_app
首次生成需要新路径;已存在的文件不会覆盖。`check`、`run`、`build` 自动检查项目根已有的 `dever.api`,变化时打印增删项并失败。快照记录公开名称、类型、可见字段、构造边界、约束、效果和恢复策略。正常修改私有实现不扩大公开接口。更新基线需要单独生成新文件并审查差异,不会自动“修复”基线。基线必须由项目审查或权限保护;如果允许 AI 同时任意修改源码和基线,它就不是授权边界。未提供基线时不会限制接口变化。
编译器还会建议清理有证据的重复范围判断(W002)、相同私有实现(W003)、可复用的纯调用(W004)和没有增加契约的私有转发(W005)。未使用赋值沿用 W001,不叠加第二套规则。指定应用入口时,W007 提醒没有外部使用需求的公开函数;普通库检查不推测未知调用方。建议不改变执行,不作为硬错误,也不按行数或函数个数强迫抽象。官方源码执行同样的契约检查,CLI 默认只展示当前用户源码的建议;官方硬错误不会隐藏。
`check`、`run`、`build` 共用应用硬检查 C016:拒绝从生产入口不可达的 App/Domain 函数,以及符合现有判定条件的私有空转发和 Model CRUD 包装,不能识别全部包装形态。检查先于 Worker 和运行库准备,`check` 保持静态、离线。`test` 按自己的测试入口检查,不套用生产可达性,测试调用不能使生产死代码通过 `check`。
编译器还会建议清理有证据的重复范围判断(W002)、相同私有实现(W003)、可复用的纯调用(W004)和没有增加契约的私有转发(W005)。未使用赋值沿用 W001,不叠加第二套规则。指定应用入口时,W007 提醒没有外部使用需求的公开函数;普通库检查不推测未知调用方。这些警告本身不改变执行、不作为硬错误;上述应用检查会另外拒绝其中符合 C016 判定条件的转发。检查不按行数或函数个数强迫抽象。官方源码执行同样的契约检查,CLI 默认只展示当前用户源码的建议;官方硬错误不会隐藏。
契约分析有明确的工作上限(当前每轮相应证明最多 16,384 个工作单元,名义类型嵌套最多 128 层)。超大嵌套失败结构或事实证明超过上限时报 C015,不会跳过检查后放行。共享结构使用缓存与写时复制,普通重复引用不会直接展开成指数数量的副本。
+8 -2
View File
@@ -4,7 +4,13 @@ Dever 语言编译器。基础类型、表达式、命名输出、条件分句
面向 Dever 程序开发者的完整语法、标准包和示例说明见 [Dever 语言开发指南](LANGUAGE.md);编译器的长期架构和语义实现见 [Dever 语言实现原理](IMPLEMENTATION.md)。
应用开发从 [安装指引](skills/references/install.md) 和独立 [dever-language skill](skills/SKILL.md) 开始。Skill 仓库为 `shemic/dever-main-skills`,本仓库的 `skills/` submodule 固定配套提交;源码维护者使用 `git clone --recurse-submodules git@github.com:shemic/dever-main.git`。
应用开发从 [安装指引](skills/references/install.md) 和配套 [dever-language skill](skills/SKILL.md) 开始。根目录 [install](install) 是自包含 Bash 安装入口,无需先克隆源码或安装 skill。服务器部署脚本和发行资产后,使用:
```sh
curl -fsSL https://git.dever.cc/shemic/dever/install | bash
```
源码中的入口不等于线上已发布:该 URL 需要服务器返回纯脚本,配套签名资产放在同站 `dist/`。发布与旧安装迁移步骤见 [发行说明](sdk/native-release.md#public-installer-and-static-distribution)。
```sh
dever new my_app
@@ -14,7 +20,7 @@ dever run my_app -- hello.greeting.greet '{"name":"Dever"}'
dever build my_app --output my_app/program
```
`new my_app --markdown` 生成等价 Markdown 项目,包含配置、AI 指引和应用测试。机器版 `dever update` 显式从官方 Releases 更新核心与配套 skill;`dever skill path [project-root]` 读取对应版本,`dever skill install <new-directory>` 安装持续跟随机器版本的 AI 入口。地址不写入业务 `config/setting.json`,普通 run/build 不联网。多个项目共享 `/opt/dever`,无需在每个项目复制核心;`crates/` 与 `library/` 是编译器和标准库维护目录。
`new my_app --markdown` 生成等价 Markdown 项目,包含配置、AI 指引和应用测试。机器版 `dever update` 显式从内置官方发行源更新核心与配套 skill;`dever skill path [project-root]` 读取对应版本,`dever skill install <new-directory>` 安装持续跟随机器版本的 AI 入口。地址不写入业务 `config/setting.json`,普通 run/build 不联网。多个项目共享 `/opt/dever`,无需在每个项目复制核心;`crates/` 与 `library/` 是编译器和标准库维护目录。源码统一位于 `ssh://git@git.dever.cc:2222/shemic/dever.git`,`skills/` 与核心同仓维护,无需额外克隆或初始化子模块。
[示例索引](examples/README.md) 以 CMS 为当前业务示例:[Dever 实现](examples/cms/dever/module/news/article/api/admin/manage.dever)和 [Markdown 实现](examples/cms/md/module/news/article/api/admin/manage.dever.md)同步维护。旧示例及编译拒绝反例归档在 `examples/old/`,正式回归测试保留在根目录 `test/`。
+6 -6
View File
@@ -231,6 +231,12 @@ fn execute() -> Result<ExitCode, String> {
return api::write(&program, output.as_deref());
}
api::check_baseline(&program, &root)?;
if matches!(action, Action::Check) || action.compiles_application() {
let errors = program.application_errors()?;
if !errors.is_empty() {
return Err(errors.iter().map(|error| error.render(&sources)).collect());
}
}
if matches!(action, Action::Check) {
return Ok(ExitCode::SUCCESS);
}
@@ -246,12 +252,6 @@ fn execute() -> Result<ExitCode, String> {
} else {
None
};
if action.compiles_application() {
let errors = program.application_errors()?;
if !errors.is_empty() {
return Err(errors.iter().map(|error| error.render(&sources)).collect());
}
}
match &action {
Action::Check => unreachable!("check returned before target selection"),
Action::Lib { .. } => unreachable!("lib returned before source checking"),
@@ -18,7 +18,7 @@ use super::release::{
};
use super::validate_catalog;
const OFFICIAL_RELEASES: &str = "https://github.com/shemic/dever-main/releases";
const OFFICIAL_RELEASES: &str = "https://git.dever.cc/shemic/dever/dist";
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(300);
const METADATA_LIMIT: u64 = 2 * 1024 * 1024;
static NEXT_DOWNLOAD: AtomicU64 = AtomicU64::new(0);
@@ -47,13 +47,8 @@ impl ReleaseSource {
}
fn asset(&self, requested: &str, suffix: &str) -> String {
let selection = if requested == "latest" {
"latest/download".to_owned()
} else {
format!("download/v{requested}")
};
format!(
"{}/{selection}/dever-{}.{suffix}",
"{}/{requested}/dever-{}.{suffix}",
self.base,
platform_identity()
)
@@ -152,14 +147,7 @@ fn redirect(current: &str, location: &str) -> Result<String, String> {
|| next.password().is_some()
|| next.port_or_known_default() != Some(443)
|| next.fragment().is_some()
|| !matches!(
next.host_str(),
Some(
"github.com"
| "release-assets.githubusercontent.com"
| "objects.githubusercontent.com"
)
)
|| next.host_str() != Some("git.dever.cc")
{
return Err("release redirect must remain on the official HTTPS asset hosts".into());
}
+1 -1
View File
@@ -6,7 +6,7 @@ use crate::specialize::{self, Specialization};
use crate::types::{Field, Parameter, Shape, Type};
impl Program {
/// Application entries are known only for run/build; library checks retain unused exports.
/// CLI check/run/build share production roots; core library checks retain unused exports.
pub fn application_errors(&self) -> Result<Vec<Diagnostic>, String> {
let roots = self
.api_routes
Executable
+295
View File
@@ -0,0 +1,295 @@
#!/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"
#!/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()
main(default_key='7889e0edaf5fbf1060bef4ad6e27106128485c8412a6f6cd4457dc67e112986e')
DEVER_INSTALL_PYTHON
}
dever_install "$@"
+67
View File
@@ -0,0 +1,67 @@
#!/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()
+19 -2
View File
@@ -92,16 +92,33 @@ Use a separate author root, not a Dever application's configuration. Place its e
## Versioned AI skill and public release assets
`skill` is required. Copy the complete standalone `dever-main-skills` checkout into the author inputs, excluding `.git` and temporary files, and list every skill file as `{source,path,sha256}`. Paths are relative to its skill root; the maker publishes them under `skills/dever-language/` in the signed manifest. The two entries above illustrate the shape, not the complete file list. `SKILL.md` is mandatory; at most 256 files, 1 MiB per file and 8 MiB total are allowed.
`skill` is required. Copy the complete `skills/` directory from the same main-repository revision as the compiler into the author inputs, excluding Git metadata and temporary files, and list every skill file as `{source,path,sha256}`. It is an ordinary repository directory, not a separate checkout or submodule. Paths are relative to its skill root; the maker publishes them under `skills/dever-language/` in the signed manifest. The two entries above illustrate the shape, not the complete file list. `SKILL.md` is mandatory; at most 256 files, 1 MiB per file and 8 MiB total are allowed.
`dever skill path [project-root]` validates and returns the skill belonging to the selected compiler. `dever skill install <new-directory>` installs a small AI entry that resolves the active signed skill on each task. It refuses existing directories. `dever update` and `dever use` change the one active-version journal used by both core and skill; they do not rewrite every user's AI directory. Ordinary users can read the signed skill without the install lock or administrator permissions.
Publish official versions to `shemic/dever-main` GitHub Releases with tag `v<version>`. The v2 signed root catalog separates base `artifacts` from `extensions`. Base includes core/private libraries, bootstrap, host native profiles/sandbox and skill. Each extension declares `{kind,target,artifacts}`; `kind` is `{"type":"runtime","ecosystem":"pip|npm|go"}`, `{"type":"build","ecosystem":"pip|npm"}` or `{"type":"target"}`. Target combines that target's native profiles/CRT and sandbox. Paths and identities cannot overlap. No extension has an independent unsigned manifest or can override base files.
Publish official assets under `https://git.dever.cc/shemic/dever/dist/<version>/`, independently of Git source access. Git tags may remain `v<version>`; HTTP asset directories use the exact numeric version without `v`. The v2 signed root catalog separates base `artifacts` from `extensions`. Base includes core/private libraries, bootstrap, host native profiles/sandbox and skill. Each extension declares `{kind,target,artifacts}`; `kind` is `{"type":"runtime","ecosystem":"pip|npm|go"}`, `{"type":"build","ecosystem":"pip|npm"}` or `{"type":"target"}`. Target combines that target's native profiles/CRT and sandbox. Paths and identities cannot overlap. No extension has an independent unsigned manifest or can override base files.
`python3 sdk/release-assets.py <release> --output <new-assets> --zstd /absolute/author/zstd` writes `dever-<platform>.manifest.json`, `.manifest.sig`, `.tar.zst`, plus `.ext-<runtime-pip|runtime-npm|runtime-go|build-pip|build-npm|target>-<target>.tar.zst`. Each USTAR contains exactly its signed ordinary artifact files, no directory headers or embedded root metadata. Zstandard uses a checksummed single frame with a maximum 128 MiB window. The publisher also emits `.blob-<sha256>` for bootstrap/dever and every bootstrap/lib dependency. First installation authenticates these raw helpers before executing the launcher's bounded extractor; end users do not need a system zstd executable. Report helper bytes separately from base archive size.
Only explicit install/update, target add and dependency preparation download official resources. The source address is compiled into the toolchain, not application configuration or environment variables. Extension preparation uses closed typed IPC and the existing machine service, storing immutable version/catalog-bound resources under `cache/extensions`. Multiple projects/users share one verified tree. `run/build` and resource reads never download. `update` prepares the new core/skill and the active version's already installed extension selection before publishing the active-version journal; failure keeps the old selection usable. The stable machine launcher/daemon are provisioned by bootstrap, so changes to that protocol require a matching trusted bootstrap installation.
## 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.
Configure the web server to serve these anonymous HTTPS paths:
```text
/shemic/dever/install generated plain Bash script
/shemic/dever/dist/latest/dever-<platform>.manifest.json
/shemic/dever/dist/latest/dever-<platform>.manifest.sig
/shemic/dever/dist/<version>/dever-<platform>.* all matching immutable assets
```
These are explicit static routes, not assumed Git web UI or GitHub-compatible release URLs. Do not return a login page or HTML file viewer. Source repositories can retain their existing access restrictions. Publish the complete immutable version directory first, then switch the latest catalog selection together. Installers authenticate the latest catalog and download payloads only from its signed exact version. Keep old version directories for pinned installations; do not replace a published version's bytes. Release assets are outside Git source history.
For the GitHub-to-new-origin migration, rebuild the core, launcher and daemon with the new compiled origin and a new version before publishing. Already installed old launchers still contain GitHub addresses, and ordinary update does not replace the stable bootstrap pair. Run the new `install` once to provision that pair through the existing bootstrap transaction, then use normal `dever update`. A successful test using older signed bytes proves installation behavior only; it does not prove a newly built release or online migration.
## Linux first installation
Include the stable launcher and daemon in the same signed release by adding this author field:
Submodule skills deleted from 27ac8f21c2.
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+51
View File
@@ -0,0 +1,51 @@
---
name: dever-language
description: 开发、检查、测试或维护 Dever 语言应用(.dever、.dever.md),创建新项目,安装或更新 dever 命令,编写 App、Domain、Model、API、CMD、Job、Port/Adapter,接入 Dever Package 或 Python/JS/Go Lib 时使用。也用于 Dever 应用目录、权限、租户和命名约束检查。不要用于 Go Dever 框架、dever-go 或普通 Go 组件开发。
---
# Dever 语言开发
Dever 是独立语言。应用源码使用 `.dever` 或 `.dever.md`,不沿用 Go Dever 的 service、Page JSON 或 Provider 目录;第三方语言实现经 Port/Adapter 接入。
## 先确定版本和范围
1. 读取用户项目的 `AGENTS.md`、现有目录和 `config/setting.json`。回答、分析、诊断只做只读检查;用户要求修改时再实施,不擅自安装软件、改服务或改全局命令。
2. 若已安装机器版 Dever,新项目运行 `dever skill path`,已有项目运行 `dever skill path <project-root>`,读取返回目录中的 `SKILL.md` 与本次需要的参考文件。已经在那个目录读取本 skill 时不再递归加载。它由 `dever update` 与核心一起更新;已有项目按其可选 dever.version 选择对应指引,不能把活动版本新增语法强加给旧项目。
3. 尚无 `dever` 时按 [安装与更新](references/install.md) 引导安装;不能把 `dever-go` 当作 Dever 语言命令。公开发行包缺失时如实说明,不编造成功、不自动编译一套缺少 runtime 的开发二进制。
4. 新项目使用 `dever new <new-directory>`;Markdown 项目加 `--markdown`。不要手工猜模板,不覆盖已有目录。项目模板的用途和检查命令见 [项目开发](references/development.md)。
开发、重构或审查应用代码前,必须读取 [项目开发](references/development.md) 中的文件与函数组织、配置与硬编码、禁止绕过检查、验证范围。安装或纯语法问答只加载与请求相关的参考。
## 按需要阅读
| 工作 | 先读 |
| --- | --- |
| 新项目、目录、命名、测试与交付 | [development.md](references/development.md) |
| 类型、分句、函数、集合、失败和并发 | [language.md](references/language.md) |
| HTTP/CMD、Model、权限、租户、Job | [application.md](references/application.md) |
| 标准能力、外部生态、Package | [libraries.md](references/libraries.md) |
| Markdown 源码 | [markdown.md](references/markdown.md) |
| 安装命令、安装 skill、更新 | [install.md](references/install.md) |
只加载当前任务相关参考。当前版本的编译器检查和已验证模板决定语法;参考中没有的调用先查官方对应版本源码,不能按其它语言习惯杜撰方法。
## 必须遵守的应用边界
- 配置只在 `config/setting.json`;不通过环境变量读取配置、密码、数据库 URL 或选择工具链。不要把 Dever 的官方仓库地址、更新配置写入业务项目。
- 应用目录是 `module/<component>/<domain>/<role>.dever`;role 只有 `app/domain/model/port/adapter/api/job`。复杂职责再按主题拆分,不机械地每个函数建一个文件,不建 `common/utils/helper/base/service` 杂物目录。
- 不写 `package/import/exposes/main`。应用入口由 API、CMD、Job 声明生成。App 是对外领域能力;跨领域只调用目标 App,不直接调用其私有 Model、Domain、Adapter。
- API 只声明绑定或 REST,App 编排,Domain 保存内聚的业务规则,Model 声明持久化合同,Port/Adapter 隔离外部 I/O。已有标准能力直接复用,禁止重复手写 ID 解析、分页裁剪、JSON 拼装等碎片函数。
- 函数使用命名输出和互斥、穷尽的相邻分句;没有 `if/else/switch/match/return`、任意循环或递归。不要靠分句顺序隐藏重叠规则。
- HTTP 默认有权限,匿名入口才写 `public`。不添加 `auth.require`、客户端自报身份/权限或每个 API 重复鉴权。站点按配置里的 API 目录匹配,租户按数据库隔离。
- 测试放在 `test/<component>/<domain>/<topic>.dever`,入口为文件同名 `topic() ()`。外部调用使用 case-local Port fake,不在普通测试启动真实 Lib 或访问真实业务数据库。
- `run/build` 离线。依赖准备使用显式 `dever package` / `dever lib` 命令;已有锁在新机器用 `dever lib install`,ARM 应用目标用 `dever target add linux-aarch64`。这些命令按需准备机器共享资源,不扫描宿主 Python、Node、Go 或 PATH 来充当运行环境。
## 工作方式
1. 修改前查相关领域、实际调用链、标准能力和既有实现。对非平凡改动简要说明缺少什么行为、应该由哪一层负责、哪些文件需要改变;已有实现满足要求时保留它。
2. 实施时以完整业务职责组织文件和函数。抽取须有业务含义、明确不变量、实际复用或降低复杂流程理解成本的作用;不按行数或函数个数机械拆分。只清理本次引入或废弃的重复与死代码,保留无关改动。
3. 完成后审查新增调用链、公开合同、配置与可信身份来源,检查本次引入的诊断并执行范围内验证。禁止假调用、吞错、假成功、擅自修改接口基线或弱化测试来通过检查;详见开发指引。
`fmt --check`、`check`、`test` 和 `run/build` 的覆盖范围不同。`check` 与 `run/build` 共用应用入口可达性和冗余硬检查,保持静态、离线,不执行应用;具体见 [验证范围](references/development.md#验证范围)。`dever test` 没有筛选参数,已有大项目不擅自全量测试;新模板的小套件可完整验证。已有授权覆盖的验证直接执行,启动服务、接真实数据库或更改系统安装须遵守用户的范围限制。
交付说明实际改动、验证结果与未验证边界。规则写入 skill 不等于新增编译器检查,能编译不等于业务验收、性能验收或已发布。
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "Dever 语言开发"
short_description: "安装与更新 Dever,创建项目,按语言约束开发、检查和测试应用"
default_prompt: "使用 $dever-language 创建并验证一个符合当前语言规范的 Dever 项目。"
+1
View File
@@ -0,0 +1 @@
7889e0edaf5fbf1060bef4ad6e27106128485c8412a6f6cd4457dc67e112986e
+2
View File
@@ -0,0 +1,2 @@
/data/
/hello-app
+14
View File
@@ -0,0 +1,14 @@
# Dever 应用开发
在本项目运行 `dever skill path .`,读取返回目录的 `SKILL.md` 和 `references/development.md`,使用与项目版本匹配的规则。AI 尚未接入 skill 时,可用 `dever skill install <新的AI技能目录>/dever-language` 创建稳定入口。这里使用 Dever 语言,不是 Go Dever 框架;命令缺失时按 README 的安装入口处理,不猜语法或用 dever-go 替代。
- 源码放在 `module/<组件>/<领域>/`,按职责使用 `app`、`domain`、`model`、`api`、`port`、`adapter`、`job`;只创建实际需要的角色。
- 不写 `main.dever`、`package`、`import` 或 `expose`。API/CMD/Job 是入口;跨领域调用 App。
- 部署配置只用 `config/setting.json`,不增加环境变量配置或写死真实地址、凭据。用户、租户和权限来自可信上下文,不用特殊账号 ID 判断权限;正常字面量与明确业务常量可以保留。
- 修改前先读实际调用链并查找复用点。文件按内聚业务主题组织,函数须有业务含义、不变量、实际复用或隔离复杂流程的作用;不按行数拆分,不机械地一个函数一个文件,不建 `utils/common` 等通用桶。
- 普通 CRUD 用 Model 与 REST,增加业务合同或编排才写 App,不叠加空转发层。API 只声明入口,Domain 保存业务规则,外部能力经 Port/Adapter。
- 不用假调用、吞错、假成功、修改接口基线、弱化测试或转移到第三方语言绕过检查。只清理本次引入或废弃的代码,保留无关修改。
- 保持当前项目源码格式一致。Markdown 源码只编译顶层 `dever` 围栏,声明文档必须与源码一致。
- 修改后执行范围内的 `dever fmt . --check`、`dever check .` 和适当验证。check 静态执行与 run/build 相同的应用入口可达性及冗余硬检查,不准备外部依赖或执行应用;报告实际覆盖范围,不能把 check 成功当作全部验收。
- 普通测试位于 `test/<组件>/<领域>/`,用文件同名零输入零输出函数,外部能力用 Port fake。`dever test .` 会运行全部应用测试,已有大项目不默认全量执行;新模板的小套件可完整验证。遵守用户对生产数据、服务、真实数据库和压测的限制。
- 安装、升级、依赖准备与业务运行分开;`run/build` 不隐式联网。没有用户要求,不提交或推送 Git。
+24
View File
@@ -0,0 +1,24 @@
# Dever 应用
这是无需数据库、鉴权或第三方工具的最小 CMD 项目。
在项目目录运行:
```sh
dever check .
dever fmt . --check
dever test .
dever run . -- hello.greeting.greet '{"name":"Dever"}'
dever build . --output hello-app
./hello-app hello.greeting.greet '{"name":"Dever"}'
```
问候结果为 `Hello, Dever`。输入由 `api` 的 CMD 声明解析并传给 `app.greet`;业务不读取环境变量,配置在 `config/setting.json`。
以上命令适用于这个最小模板;项目扩大后按改动选择验证,`dever test .` 会执行整个项目的应用套件。`check` 静态执行与 run/build 相同的应用入口可达性及冗余硬检查,无需准备运行资源;它不执行应用,不能代替运行或业务验收。
AI 开发规范见本项目 `AGENTS.md`。运行 `dever skill path .` 获取项目对应版本的完整 `dever-language` skill,开发前读取其中 `references/development.md`。AI 的稳定 skill 入口可用 `dever skill install <新的AI技能目录>/dever-language` 安装;核心与配套 skill 通过 `dever update` 同步更新,已锁定版本的项目仍使用自己的版本。
首次安装入口为 `https://git.dever.cc/shemic/dever/install`,源码仓库为 `ssh://git@git.dever.cc:2222/shemic/dever.git`。安装前确认该站已发布对应平台的脚本和签名包;部署完成后可用 `curl -fsSL https://git.dever.cc/shemic/dever/install | bash`,机器安装需要管理员权限。仓库可访问不代表二进制已经发布。
普通 `.dever` 与 Markdown `.dever.md` 共享语言规则。用 `dever new <新目录> --markdown` 创建 Markdown 项目。目录必须不存在,生成器不会覆盖已有项目。
@@ -0,0 +1,5 @@
{
"log": {
"level": "info"
}
}
@@ -0,0 +1 @@
cmd greet = app.greet
@@ -0,0 +1,17 @@
# 问候命令
将命令行 JSON 输入交给 App。
- 包:`hello.greeting.api`
- 公开类型:无
- 公开方法:无
- 使用:无
## greet
声明问候命令。
- 声明:`cmd greet`
```dever
{{source}}```
@@ -0,0 +1,3 @@
greet(name: Text) (message: Text) {
message = "Hello, " + name
}
@@ -0,0 +1,23 @@
# 问候
提供可复用的问候用例。
- 包:`hello.greeting.app`
- 公开类型:无
- 公开方法:
- `greet`
- 使用:
- `hello.greeting.greet(name)`
## greet
根据名字生成问候。
- 函数:`greet`
- 输入:
- `name: Text`:名字
- 输出:
- `message: Text`:问候
```dever
{{source}}```
@@ -0,0 +1,4 @@
greet() () {
message = app.greet("Dever")
assert_eq(message, "Hello, Dever")
}
@@ -0,0 +1,19 @@
# 问候测试
检查问候用例的输出。
- 包:`hello.greeting.greet`
- 公开类型:无
- 公开方法:无
- 使用:无
## greet
调用真实 App 并断言输出。
- 函数:`greet`
- 输入:无
- 输出:无
```dever
{{source}}```
+67
View File
@@ -0,0 +1,67 @@
# 应用接口、数据与后台任务
## API 和 CMD
`module/news/article/api/admin/manage.dever`:
```dever
get list = app.list
get detail = app.detail
post create = app.create
put replace = app.replace
delete remove = app.remove
post publish = app.publish
```
API 无函数体,继承 App 输入/输出签名。支持 get/post/put/delete,当前没有 patch、旧 get_ 前缀或源码内 site 声明。`cmd greet = app.greet` 生成 `component.domain.greet` 命令,参数来自单个 JSON 对象;main.dever 不存在,也不读取命令行参数。
GET/DELETE 从 query 读标量参数,POST/PUT 从 JSON body 读参数;额外、重复或缺少必填字段均拒绝。无需自己读 body 或构造 dever.http.Response。成功统一为 `{"code":0,"message":"ok","data":...}`;错误 data 为 null,code 对应状态码。接口必须绑定一个可序列化输出,输出名可自定。
`dever.api.Error` 中 Invalid/Unauthorized/Forbidden/NotFound/Conflict/TooManyRequests 对应 400/401/403/404/409/429;未知内部失败记录脱敏日志并返回安全的 500。
写 HTTP/CMD/Job 调用链由入口建立事务;GET 调用链不能写库或入队。密码 hash/verify 是例外:不能让昂贵密码运算占用入口长事务,实际写入必须放显式短 transaction,并在其中重新检查依赖状态。不能为了“默认有事务”而删除这种短事务。
## 站点、身份与权限
`sites.admin.path = "admin"` 匹配领域内 `api/admin/**`,front 同理。文件名 topic 也进入 URL;上述 publish 是 `/news/article/admin/manage/publish`。每条 HTTP 声明只能匹配一个站点,重叠或遗漏配置会失败;path 不改写 URL,也不匹配 `api/admin.dever`。
所有 HTTP 默认鉴权并校验精确权限。只有明确匿名操作写 `public post login = app.login` 等单条 public 声明;没有 public rest、public cmd、auth.require 或每个业务文件的权限注册代码。
配置的 auth provider 引用一个项目自己的 `verify` App 方法,验证账号、会话、成员和租户关系。输入是 `dever.auth.Claims`,输出 record 必须且只能包含 `id: Text`、`user_id: <UserModelId>?`、`tenant_id: <TenantModelId>?`;只能读 global Model,不能写库、拿原始 Header 或返回自报权限。
认证成功的受保护 App 可读取 `dever.auth.id()`、`session()`、`user_id()`、`tenant_id()`、`dever.site.key()`;来源是隐藏可信上下文,不能让客户端提交 user_id/tenant_id 当作身份。名义 ID 必须与站点 verify 的类型对应,可空 ID 经分句处理后才能使用。public/CMD/Job/普通测试不会自动获得身份读取能力。
核心生成 `component.domain.site.action` 权限目录,method 是元数据,不额外拼进权限键。REST 的 action 固定为 read/create/replace/delete。核心维护角色、用户角色、角色权限;项目无需重建这些表。App 用 `dever.auth.permissions/save_role/grant_role/revoke_role/disable_role` 实现后台授权入口,具体参数查当前库签名。它们只能从受保护写 API 到达。资源所有权可用 `dever.auth.owns_user(id)`,不等同于接口角色权限。
Cookie 登录/退出用 `dever.auth.issue_cookie(subject, session, tenant)` 与 `clear_cookie()`。Header/Cookie 使用 `dever.api` 读取/设置,无需显式 context 参数。密钥来自部署配置;Cookie 写入和 Cookie 认证的写请求需要精确同源校验,站点配置 origin。敏感头不能当普通 Text 读取。
## Model、CRUD 与业务编排
`module/news/article/model.dever` 的主记录应为 Article:
```dever
type Article {
title: Text(1, 200)
body: Text
}
```
自动有只读 `id` 与 `created_at`。字段支持 default、private、unique、index;范围和长度在存储边界验证。关系使用目标 Model 专有 id 字段,不用 Int 代替外键。
同领域 App 可用 `model.create({...})`、`model.get(id)`、`model.update(id,{...})`、`model.delete(id)`、`model.list({where=...,order=[created_at.desc,id.desc],page=1,size=20})`。这是编译器静态 DSL,不是动态字典 SQL;不用手写 repository。聚合、关联和插入流程仍由 App 明确编排,不能自动推测关联图级联写入。
纯粹 CRUD 可在 API 写 `rest model`,多 Model 时指定 `rest model.<topic>`。Model 中 `create = expr`、`replace = expr`、`search = expr` 控制 REST 字段来源/查询;引用 input 表示客户端同名输入转换,不引用 input 表示服务端提供且禁止客户端提交。不要改成 post/put/get 字段关键字。
`private owner user_id: ... = dever.auth.user_id()` 在创建时赋值,并将所有权条件加入全部读写 SQL;不是只在输出隐藏 user_id。这种静态 owner 隔离可由 Model 直接声明,无需仅为它增加 App。关联操作、超出字段合同的业务校验、复杂输入输出或发布状态机由 App 编排,不能藏在 API 里。App 对外用明确 View,别泄漏 Model 私有字段。
不为每个 CRUD 机械建立 App、Domain、Port 和 Adapter;Model 的数据库操作无需再套一层 Port/repository。实际新增业务合同或编排时才增加对应实现。完整反重复、反硬编码及检查边界见 [项目开发](development.md)。
## 租户、任务与部署配置
配置只有 database 租户隔离:`tenant.database` 选控制连接,`global type` 使用平台库,其余持久 Model 与 Job 使用租户库。没有 field/table/mode 配置。租户缺失、未迁移或 schema 过期明确失败,不回退平台库。
运维命令:`dever tenant migrate <root> <id>`;`dever tenant owner <root> <id> <site> <user-id>` 建立核心 Owner;`dever tenant component enable|disable <root> <id> <component>` 控制租户功能。可用租户组件从 Model/Job 推导,不手写 Profile 分组。
job.dever 声明持久任务并绑定 App;enqueue 与事务、重试、调度、权限重验使用核心机制,不另写后台死循环。实际 Job 声明从对应版本指南和 CMS publishing/schedule 示例复用,不能把第三方 Worker 当持久 Job。
部署配置的真实结构可查官方 CMS `config/setting.json`,只添加项目用到的 database、tenant、auth、sites、http、job、runtime、log。最小 CMD 项目只需模板配置。密码、token 与真实数据库连接不提交 Git。
+128
View File
@@ -0,0 +1,128 @@
# 项目开发
新建最小项目:
```sh
dever new my_app
dever fmt my_app --check
dever check my_app
dever test my_app
dever run my_app -- hello.greeting.greet '{"name":"Dever"}'
dever build my_app --output my_app/program
./my_app/program hello.greeting.greet '{"name":"Dever"}'
```
创建 Markdown 项目用 `dever new my_app --markdown`。两种模板使用相同代码和行为,Markdown 另加编译器校验的说明。目标目录必须不存在,父目录必须已存在;不接受向现有项目合并或覆盖。build 输出也必须不存在。
模板只包含 CMD、App、配置、一个测试和开发指引,无需数据库或 HTTP 服务。实际项目在它的目录合同上扩展,替换演示领域,删除不再使用的示例与测试。添加 HTTP 时须完成 [站点与身份配置](application.md),不能只把 cmd 换成 get。
```text
my_app/
AGENTS.md
README.md
config/setting.json
module/
news/article/
app.dever
domain.dever
model.dever
port.dever
adapter.dever
job.dever
api/
admin/manage.dever
front/browse.dever
test/news/article/publish.dever
```
这是可用职责位置,不是要求创建全部文件。component、domain 和文件主题使用有业务意义的 ASCII 小写蛇形命名;类型用 PascalCase,方法和字段用 snake_case。不要增加任意第三层领域目录。组件按业务能力划分,领域按一致的数据和业务边界划分。组件、领域和主题不能命名为 `common/shared/utils/helper/helpers/base/service/services` 等编译器禁止的通用桶。
| 位置 | 责任和访问规则 |
| --- | --- |
| App | 跨领域公开能力,校验与业务编排;同领域用 `app.publish`,外部用 `news.article.publish` |
| Domain | 同领域内部业务规则;纯计算优先放这里,不承接数据库/外部 I/O |
| Model | 存储字段、索引、关系、迁移、Seed、静态 SQL;同领域 App 使用 ORM |
| Port | 外部能力的类型、操作与失败合同 |
| Adapter | 实现 Port;私有,不反调 App/Domain/Model |
| API | HTTP/CMD 入口绑定与自动 REST,无业务函数体 |
| Job | 持久任务声明,绑定同领域 App |
| Test | 文件同名、零输入零输出的唯一测试入口 |
## 文件与函数组织
简单领域保留角色单文件;复杂角色可以拆为 `app/publish.dever`、`app/search.dever` 等内聚主题,调用名不带文件名。非 API topic 目录不递归。App/Domain/Port/Adapter/Job 单文件与同名目录二选一,选择目录时至少有两个内聚主题文件;Model/API 主文件与 topic 目录可按编译器合同共存,不受这个最少文件数限制。只有 `api/` 可按站点和资源继续嵌套,目录会影响 URL。
一个文件可以包含多个相关类型、函数和分句。按完整业务主题命名,不机械地一个函数一个文件,也不为尚不存在的需求创建空 App、Domain、Port 或 Adapter。
抽取函数应表达独立业务规则、维护明确的不变量、复用已有逻辑,或隔离难以理解的复杂流程。只使用一次的函数也可以有这些作用;不能只因几行代码能起名字就再加一层。职责内聚的代码不按行数强拆,也不通过把所有逻辑塞进一个长函数来规避碎片化。
| 情况 | 处理方式 |
| --- | --- |
| `parse_id` 重新手写数字解析并把失败悄悄变成 0 | 复用已有解析、名义 ID 和明确失败合同,不复制算法 |
| `trim_name` 只原样转发一次标准 trim,没有新增合同 | 在使用处调用标准能力 |
| `page_slice` 重复 Model 已有的分页规则 | 使用 Model 分页,不在各领域复制一份 |
| `publish` 组织发布条件、状态变化和后续任务 | 保留完整 App 能力,把内聚业务规则放同领域 Domain |
| 单次使用的规则需要独立表达有效期或状态不变量 | 可以抽取并按业务含义命名 |
| 两处相似代码承担不同规则、未来变化原因不同 | 保持各自归属,不为表面相似增加通用调度层 |
相同规则先复用现有所有者;只有真实共享的逻辑才抽象。跨领域共享能力由负责该业务的 App 提供,不直接调用对方私有实现,不为缩短调用名建立空转发链。普通 CRUD 直接用 Model 和 `rest model`;需要超出 Model 字段合同的输入转换、业务校验、关联操作或输出合同才增加 App 编排,具体见 [应用规则](application.md#modelcrud-与业务编排)。
## 配置与硬编码
| 数据或规则 | 归属 |
| --- | --- |
| 随部署变化的服务地址、端口、数据库连接、凭据 | `config/setting.json`,不增加环境变量配置,不提交真实秘密 |
| 当前用户、租户、权限及资源归属 | 可信认证上下文、核心授权与 Model 策略,不接受客户端自报身份 |
| 有固定业务含义的状态、范围和规则 | 类型、互斥分句、字段约束或内聚 Domain 定义,复用同一规则 |
| 需要用户或运营维护的业务数据 | 项目 Model 与相应业务流程,不把具体记录写进源码分支 |
| 测试 ID、样例地址和固定结果 | 测试夹具,不作为生产失败时的兜底 |
禁止按某个账号、租户、记录的裸 ID、名字或地址开特例,例如用 `user_id == 1` 代替管理员授权。普通查询参数可以标识目标记录,但不能因此成为操作者身份或访问权限。
避免硬编码不等于搬走所有字面量。局部的 0、1、提示文字和稳定协议常量可以保留;有业务意义、难以理解或反复出现的值由现有类型或领域定义表达。只有确需运行时变化的值才增加配置。官方发行源是工具链固定合同,不能塞进业务 setting.json。
## 禁止绕过检查
- 不添加假入口、无意义调用或仅供测试触达的调用,掩盖生产入口不可达的 App/Domain。删除本次废弃代码或接通真实业务调用链。
- 不把重复业务逻辑转移到 Adapter、Python、JS 或 Go 来避开 Dever 检查。外部实现的质量同样需要审查。
- 不吞掉失败、用空值或假数据冒充成功;有业务依据的恢复须使用明确结果合同,并验证恢复行为。
- 不为通过检查擅自改 `dever.api`、删失败测试、弱化断言或关闭权限、类型及沙箱限制。确需改变公开合同,先说明调用方影响,在用户授权范围内同步实现、基线与测试。
- 普通应用开发不修改编译器或核心库来容纳违规代码;语言或核心维护须有相应任务授权。只处理当前改动相关问题,不借机重写无关代码。
## 测试与检查
`test/hello/greeting/greet.dever`:
```dever
greet() () {
assert_eq(hello.greeting.greet("Dever"), "Hello, Dever")
}
```
模板中的实际字符串以生成文件为准。测试只执行文件同名函数,不需要 main 或注解。`assert` 接受 Bool,`assert_eq` 要求同类型。测试可访问同领域 App/Domain,跨领域仅 App,不能直接写 Model CRUD。使用 Model 时运行器使用临时 SQLite;不使用 Model 时不需要数据库。它不会读取生产 setting.json 或生产 data。对业务失败使用 `result(call)` 配合 choice 分句验证。
常用命令:`fmt --check` 查格式,`check` 查语法/类型/目录/分句/配置,`api` 输出 API 合同,`test` 运行应用套件,`run` 开发执行,`build` 生成独立程序。不要发明 `test --filter`、`run api` 或 `dever init`。
### 验证范围
| 检查 | 能证明的范围 |
| --- | --- |
| `dever fmt <root> --check` | 源码格式;不证明类型或业务正确 |
| `dever check <root>` | 静态语法、类型、目录、命名、分句、相关配置、已有 API 基线,以及应用入口可达性和冗余硬检查;不执行应用 |
| `dever test <root>` | 当前项目全部应用测试的实际断言;没有筛选参数,不证明生产入口完整 |
| `dever run/build` | 通过与 check 相同的应用硬检查,再准备本地编译资源并执行或编译;成功不等于部署或性能验收 |
| AI 审查 | 业务含义、职责拆分、复用、硬编码与验证范围,不能替代程序检查 |
`check/run/build` 共用 C016,拒绝生产入口不可达的 App/Domain,以及符合现有判定条件的私有空转发和 Model CRUD 包装;并非任意角色、所有函数或全部 CRUD 包装都能检出。`check` 不准备 Worker 或 runtime pack,不进行原生编译,不启动应用、服务或数据库。`test` 保持独立的测试入口与 Port fake 检查,不执行生产入口可达性检查;不能用测试调用掩盖生产死代码,也不能把静态检查当作运行验收。
W001–W005 等诊断包含未使用赋值、部分冗余条件、相同私有实现、重复纯调用和空转发建议,警告本身不会使命令失败。审查本次引入的诊断,修正有证据的问题;不能声称编译器能判定全部硬编码或业务价值。skill 中的开发要求不自动变成编译器硬错误。
选择与改动相符且已获授权的检查。新模板的小套件可以完整运行;已有大项目不默认运行全量 `dever test`,也不默认启动服务、连接真实数据库或压测。需要但尚未运行的检查要说明原因和剩余证据,不能伪报通过。已有 `dever.api` 时保留基线约束;`dever api <root> --output <新文件>` 可以导出待比较合同,不覆盖原文件。
源码放 module/,测试放 test/,部署配置放 config/,运行数据放 data/。生成程序从二进制同级 config/setting.json 读取配置。应用无需打包编译器;使用外部 Lib 时相关 runtime/依赖由 build 嵌入。只读 check 不证明真实数据库或部署验收。
## 核心与业务的区别
统一源码仓库地址为 `ssh://git@git.dever.cc:2222/shemic/dever.git`,迁移与发行状态见 [安装指引](install.md)。`crates/dever-core` 负责语言检查/编译,`crates/dever-runtime` 负责运行时,`crates/dever-cli` 负责命令,`library/` 是随版本提供的标准 Dever 源码。维护中的完整业务示例是 `examples/cms/dever` 与 `examples/cms/md`;`examples/old` 是历史回归,不作新项目模板。
开发普通应用无需复制或修改这些核心目录。可复用业务组件作为 Dever Package;外部 Python/JS/Go 生态称 Lib。多个项目共享机器安装的 Dever。
+79
View File
@@ -0,0 +1,79 @@
# 安装与更新
安装入口为 `https://git.dever.cc/shemic/dever/install`,官方二进制源为同站 `/shemic/dever/dist`。这些地址由发行工具固定,不写入业务项目配置,不通过环境变量选择。Git 源码可访问不代表安装端点或二进制已发布;服务器尚未部署时如实报告,不能把登录 HTML 当作脚本。
## 安装 dever
先检查 `dever --help`。机器版提供 new、install、update、target、version、skill;仅源码开发二进制不管理机器安装。不要把已有同名命令直接删掉或覆盖,先识别归属。
初次安装需要 Linux、Bash、Python 3.9 或更新版本的 `/usr/bin/python3` 和支持 Ed25519 的 `/usr/bin/openssl`;管道下载还需要 curl。非 root 安装通过 `/usr/bin/sudo` 提权,帮助不提权。这些工具用于校验和安装,生成的应用不依赖系统 Python、Node、Go 或 Rust。检查安装脚本并取得机器安装授权后执行:
还须满足所选发行包的主机 ABI 要求;当前准备的 Linux x86_64 编译器要求 glibc 2.39。ARM64 应用交叉构建可在 x86_64 主机使用,不能据此宣称 ARM64 宿主编译器包已经发布。
```sh
curl -fsSL https://git.dever.cc/shemic/dever/install | bash
```
无需先克隆源码或安装 skill。查看参数用 `bash -s -- --help`;固定版本用 `bash -s -- --version <准确版本>`,替换上面命令尾部的 `bash` 即可。本地已下载脚本可用 `bash install --help` 检查。
该命令下载当前平台最新已签名基础包。脚本包含独立固定公钥,先校验发行元数据,再验证引导程序及其私有库摘要,由引导程序解压 Zstandard 包;无需系统 zstd。安装 `/opt/dever`、`/usr/local/bin/dever`、机器共享服务和必要沙箱策略,默认启动服务。`--no-start` 可仅安装。没有对应正式资产时明确失败,不修改已安装版本。
基础包包含 Dever 编译器、本机应用编译资源、四种数据库运行库和配套 skill。Python、Node、Go、第三方原生构建工具与 ARM 应用目标按需安装,保存在机器共享资源目录;同一版本的多个用户和项目共用一份。
## 将 skill 接入 AI
安装核心后,将稳定入口装到用户选择的 AI 技能目录:
```sh
dever skill install <新的AI技能目录>/dever-language
```
目录必须尚不存在,不能覆盖已有 skill。入口每次解析与核心配套的完整 skill,重载 AI 工具的技能列表后使用。这是 Dever 语言的 `dever-language`,不是 Go 框架的 `shemic-dever`。不自动修改机器上其他用户的 AI 目录。
## 按需准备资源
首次添加或更新 Lib 时,`dever lib add/update <project-root> ...` 自动准备相应生态,只有进入源码构建流程时才准备构建工具。已有 `dever.lock` 的项目在新机器运行 `dever lib install <project-root>`,按锁恢复依赖,保持配置和锁文件不变;没有 Lib 的 Python/JS/Go Adapter 也可通过该命令准备环境。缺少锁时先显式 `dever lib update <project-root>` 生成锁。不要把 install 当作重新选版本的 update。
在 x86_64 机器准备 ARM64 应用目标:
```sh
dever target add linux-aarch64
dever lib install <project-root> --target linux-aarch64
dever build <project-root> --target linux-aarch64 --output <新文件>
```
只用 Dever、没有外部依赖的项目省略 lib install。目标 Lib 必须已有匹配目标的锁;首次生成 ARM 锁使用 lib add/update 的同名 target 参数。准备会显式联网,后续 run/build 离线;缺资源时执行提示的准备命令,不改用系统解释器。target add 准备当前活动版本,锁定其它编译器版本的项目先准备并选择对应版本。
已准备本地发行包时:
```sh
bash install --release /可信发行目录 --trusted-key /独立确认的公钥文件
```
测试或镜像制作增加 `--system-root /已有的自有镜像目录`;不会启动宿主服务。安装器生成的临时 `config/setting.json` 仅供机器 bootstrap 使用,包含本地发行路径、安装根和公钥路径;不是业务配置,不写发行仓库地址或环境变量。
公钥可公开提交;签名私钥绝不能进 skill、源码仓库或发行包。只有已独立确认的公钥才可作为 `--trusted-key`,不能从未知下载包顺手取一把钥匙作为信任依据。
已持有完整 skill 时仍可直接运行 `sudo python3 <skill目录>/scripts/install.py`,它与根 Bash 入口复用同一安装实现。
## 一条命令更新核心与 skill
```sh
sudo dever update
dever version
dever skill path
```
update 只访问内置官方发行源,先准备核心、同版本完整 skill,以及当前活动版本已安装的扩展,再切换活动版本。未使用的扩展不会一起下载;任一准备失败保留原活动版本。普通 check/test/run/build 不检查更新、不隐式联网。
从旧 GitHub 发行源迁移时,需要在新源正式包发布后执行一次新的 `install`。旧启动器和守护进程内置旧地址,普通 update 不负责替换这两个稳定程序;只修改仓库 remote 或重新发布旧包不能完成迁移。新 install 成功后再用 `dever update`。
本仓库直接安装的 skill 在开始开发时先运行 `dever skill path` 并加载该版本的完整 skill,不必每个项目 git pull。也可以用 `dever skill install <新的AI技能目录>/dever-language` 安装稳定的薄入口,之后每次自动解析当前机器版本;已有目录不会被覆盖。不要把新入口嵌套装进已经存在的完整 skill 目录。
项目可选 `config/setting.json` 的 `dever.version` 锁定准确 `major.minor.patch`,未锁定时使用机器活动版本。`dever install 版本` 准备指定版本;`dever use 版本` 显式切换活动版本。update 不改业务项目文件或数据库。锁定项目用 `dever skill path <project-root>` 读取对应指引,不能仅凭活动版指南采用新语法。
## 源码维护者
统一源码仓库为 `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`。禁止单独修改生成物中的安装算法。首个新源发行包发布前,默认下载会明确失败;使用真实签名本地包可验证安装,但不能代表线上安装或更新已经通过。
+83
View File
@@ -0,0 +1,83 @@
# 核心语法
以下片段说明语法。将它们放进正确角色文件并补完整调用链后检查,不能当作免配置 HTTP 项目。
## 类型和函数
```dever
type Greeting {
message: Text
}
greet(name: Text) (response: Greeting) {
response = Greeting { message = "Hello, " + text.trim(name) }
}
```
函数是 `name(输入) (命名输出) { 线性语句 }`,没有 function/return。调用按位置传参。零输入或零输出也写 `()`。单输出直接得到值;多输出调用返回只能按输出名访问的结果。App 的输入、输出和向外传播错误均为公开合同,不暴露 Domain/Adapter 私有类型。
函数命名表达业务行为,调用路径不附加主题文件名。相关函数、类型和同名分句可以同处一个内聚文件;拆分与复用的判断见 [文件与函数组织](development.md#文件与函数组织),不按函数长度自动增加 helper。
常用类型:`Bool`、`Int`(检查溢出)、`Float`、精确 `Decimal`、`Text`、`Bytes`、`Uuid`、`Id`、`DateTime`、`Date`、`Time`、`Duration`、`Secret`、`Json`。可空用 `T?`,集合用 `List<T>`、`Map<K,V>`。Model ID 是每个 Model 独有的名义类型,不等于任意 Int。不得把不同表的 ID 混用。
record 用字段初始化;choice 用分支构造:
```dever
type State {
Draft
Published
Rejected(reason: Text)
}
label(state: State.Published) (text: Text) {
text = "published"
}
label(state: State.Rejected(reason)) (text: Text) {
text = reason
}
label(state: other) (text: Text) {
text = "draft"
}
```
## 分支与边界
没有 if、else、switch、match、条件表达式或递归。同名、同参数数量的分句必须连续,相同输出合同,互斥且穷尽;没有先匹配谁的顺序优先级。`other` 是同一参数位置所有明确模式的补集。
```dever
display(value: Text) (text: Text) {
text = value
}
display(value: null) (text: Text) {
text = "missing"
}
grade(score: Int < 60) (text: Text) {
text = "fail"
}
grade(score: Int >= 60) (text: Text) {
text = "pass"
}
```
支持完整类型、choice 分支、null、Bool/Text/数值字面量、Int/Decimal 范围与 other 模式;多输入检查全部输入组合。任一函数都必须为命名输出赋值。不要写 `return` 提前退出,按分句表示结果与失败。
字面量分句适合明确的状态和规则,不意味着可以按某个用户 ID、租户名字或样例数据写业务特例。运行时配置与可信身份的归属见 [配置与硬编码](development.md#配置与硬编码)。
集合遍历使用语言的 `map/filter/reduce/each` 等核心操作及静态 handler,不能加入 for/while。handler 的签名和可达调用链由编译器检查,不是运行期闭包或动态插件。具体签名查当前版本 `library/` 与核心语言指南,不杜撰 lambda 语法。
## 失败和能力
业务错误通过 choice 的 `error` 分支声明,`fail` 传播,`result(call)` 显式捕获;普通调用直接得到成功值。不能用字符串/任意 JSON 代替类型化失败,不能捕获后悄悄吞掉错误。错误映射到 HTTP 的方式见 application.md。
纯 Domain 函数可声明 `pure`,编译器检查其调用链;要从 Model REST 字段来源调用的业务转换必须是合规的同领域 pure Domain。外部文件/网络 I/O 经 Port/Adapter,不把系统调用塞入普通 App 或 Domain。
Dever 推断挂起,无需 async/await。直接调用按顺序完成;`run` 显式启动结构化子任务,`wait/stop` 消费 Task,任务不能逃逸所属作用域。Group/Channel/Stream 具有静态资源约束;不能跨挂起、线程或请求随意保存句柄。`result` 不改变调度,也不绕开 effect 检查。
`Secret` 不可打印、比较或转为普通 Text。密码使用现有 crypto 能力;不要自制哈希。声明类型、分句和 effect 报错时修正真实边界,不添加无意义中转函数、泛用返回包装或吞错分句来绕过检查。
更多完整语法以官方核心仓库同版本 `LANGUAGE.md` 为准;新建项目先使用本 skill 的已验证模板。
+46
View File
@@ -0,0 +1,46 @@
# 标准能力与依赖
内置 package 不需要 import、下载或把标准库复制到 module/。先查已有能力,避免项目里堆积 parse_id、page_slice、trim 包装函数。
| 能力 | 入口 |
| --- | --- |
| 文本 | `text.trim/lower/upper/contains/starts_with/ends_with/split/replace/join` |
| 数字 | `int.parse/to_text`、`decimal.parse/round/from_int`、`float.parse`、`math` |
| 字节 | `dever.bytes.from_text/to_text/from_ints/length/at/slice/concat` |
| 时间 | `dever.time.now/parse_datetime/format_datetime/parse_date/format_date/duration/add/subtract/difference` |
| 密码与摘要 | `dever.crypto.password_hash/password_verify/token/sha256/hmac_sha256/constant_time_eq` |
| 类型化 JSON | `dever.json`,API/CMD 通常直接复用自动 wire codec |
| HTTP/网络/文件 | `dever.http`、`dever.net`、`dever.io`,业务外部 I/O 按 Port 边界使用 |
| 请求与身份 | `dever.api`、`dever.auth`、`dever.site`,受入口可达性限制 |
这是查找索引,不保证省略的参数/返回值。精确函数签名在官方核心仓库同版本 `library/`,复杂标准能力先读取对应文件和 LANGUAGE.md。`parse` 常返回可空值,不能假定失败得到 0。新版本发布前 skill 索引与库签名须一起验证。
## Dever Package
Package 是可复用 Dever 组件。通过项目 setting.json 中的 `package.registry` 与 `package.use` 声明;显式 `dever package add|list|update|remove|doctor <root> ...` 管理。`dever.lock` 固定版本、摘要和传递依赖;Package 不能覆盖本地同名组件。不要在业务源码中引入 import 或自建 vendor 查找规则。
## 外部 Lib 与普通二进制
第三方生态叫 Lib。项目 `config/setting.json` 的根 lib 数组声明 `pip:name@version`、`npm:name@version` 或 Go 依赖,使用 `dever lib add/list/install/update/remove/doctor` 管理。add/update 自动准备实际需要的运行环境与构建工具;install 按已有锁精确恢复,不改变配置或锁。run/build 只消费锁,不联网,也不读取系统 Python、Node、Go 或 PATH。
锁记录原始来源、摘要、固定构建输入和产物。新机器缺少已构建产物时,install 使用固定输入重建并核对输出摘要;第三方构建不能重现相同字节时会明确失败,不能用更新锁或偷偷重新解析版本冒充恢复。检查详细错误后,由用户决定是否执行 update 重新生成锁。
Lib 必须通过领域 Port/Adapter 使用。Port 声明类型与失败合同,Adapter 实现它;App 只调用 Port。声明的类型、返回值、业务失败、超时和 capability 使用统一 Worker 协议,各语言 SDK 不替代编译器类型检查。Python/JS/Go SDK 与可用 runtime 必须匹配 Dever 发行版本。
Dever 不检查第三方语言源码内部的函数拆分、重复实现和业务硬编码;Port 类型与 Worker 协议验证不能代替这部分审查。外部生态承接确需复用的现成能力,不能用于转移普通业务逻辑以规避 Dever 约束,开发规则仍见 [项目开发](development.md)。
普通 Linux 二进制不需要 Worker 协议。领域 `port.dever`:
```dever
execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error
```
`adapter.dever`:
```dever
external command "bin/tool" {}
```
将相应架构 ELF 放在领域的 bin/tool,App 调用 `port.execute(["--version"], null)`。结果有 code、stdout、stderr,字节显式转文本;参数不是 shell 字符串。非零退出码仍是结果,启动失败/超时/超限是错误。文件、网络、子进程能力显式声明且受配置授权,不能关闭沙箱来掩盖部署问题。
测试必须使用 Port fake,不执行真实第三方程序。跨到 ARM 时必须提供目标架构的库和二进制,Dever 不转换已有 x86 ELF。
+15
View File
@@ -0,0 +1,15 @@
# Markdown 源码
使用 `dever new <new-directory> --markdown` 获得经过验证的模板;不要只把任意 Markdown 中的代码围栏改名当作完整程序。
文件后缀 `.dever.md`;编译器只编译顶层 `dever` 或 `typescript dever` 围栏,且校验说明与代码合同。普通 `.md` 不是源码,两种源码格式的类型、目录、访问、权限约束一致。
- 每个文件一个一级标题,随后写用途、包、公开类型、公开方法、使用;空项写“无”。包说明来自路径,不是源码 package 声明。
- 每个逻辑声明有自己的二级标题、用途和唯一代码块,一级标题下不直接放源码。
- 类型写 `- 类型:` 与字段/分支说明;函数写 `- 函数:`、输入、输出,名称、类型、顺序与代码一致。每个字段说明包含非空中文用途。
- 同名同参数数量的所有分句放同一章节、同一围栏;不同签名或类型分开。
- API 写 `- 声明:cmd greet`、`post publish`、`rest model` 等,与代码一致,不虚构公开方法。
- App 自动公开其类型/函数;Domain、Adapter、API 和测试没有公开方法。调用清单隐藏 app role 和 app topic。
- 测试同样需要完整说明,唯一入口仍是文件同名零输入零输出函数。
维护源码与说明后一起跑 fmt --check、check。完整声明说明协议见官方核心仓库同版本 MARKDOWN-SYNTAX.md。
+266
View File
@@ -0,0 +1,266 @@
#!/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()
+187
View File
@@ -116,6 +116,193 @@ fn missing_pack_fails_without_host_tools_and_check_and_empty_test_need_no_pack()
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_rejects_redundancy_before_preparing_build_resources() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
write(root, "config/setting.json", "{}");
write(
root,
"module/sample/profile/domain.dever",
"normalize(value: Text) (result: Text) pure { result = text.trim(value) }\nalias(value: Text) (result: Text) pure { result = normalize(value) }\norphan() (result: Text) pure { result = \"unused\" }",
);
write(
root,
"module/sample/profile/app.dever",
"greet(value: Text) (result: Text) { result = domain.alias(value) }\nunused() (result: Text) { result = \"unused\" }",
);
write(
root,
"module/sample/profile/api.dever",
"cmd greet = app.greet",
);
let checked = cli(&compiler, root, "check", &[]);
failure(&checked, "C016");
let diagnostic = String::from_utf8_lossy(&checked.stderr);
for expected in [
"sample/profile/app.dever:2:1: error[C016]",
"function 'sample.profile.unused' is unreachable",
"sample/profile/domain.dever:2:1: error[C016]",
"only forwards unchanged inputs",
"sample/profile/domain.dever:3:1: error[C016]",
"function 'sample.profile.domain.orphan' is unreachable",
] {
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
}
// The same source fails before native packs, output publication or execution.
let executable = root.join("must-not-build");
for (action, args) in [
("run", Vec::new()),
("build", vec!["--output", executable.to_str().unwrap()]),
] {
let rejected = cli(&compiler, root, action, &args);
assert!(!rejected.status.success());
assert_eq!(rejected.stderr, checked.stderr);
}
assert!(!executable.exists());
assert!(!root.join("data").exists());
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_rejects_plain_model_get_wrapper_without_opening_database() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
write(root, "config/setting.json", "{}");
write(
root,
"module/sample/profile/model.dever",
"type Profile { name: Text }",
);
write(
root,
"module/sample/profile/app.dever",
"lookup(id: model.id) (profile: model.Profile) { profile = model.get(id) }",
);
write(
root,
"module/sample/profile/api.dever",
"cmd lookup = app.lookup",
);
let rejected = cli(&compiler, root, "check", &[]);
failure(&rejected, "C016");
let diagnostic = String::from_utf8_lossy(&rejected.stderr);
for expected in [
"sample/profile/app.dever:1:1: error[C016]",
"only forwards a Model CRUD operation; declare rest for plain CRUD",
] {
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
}
assert!(!root.join("data").exists());
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_accepts_both_templates_and_preserves_markdown_locations() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let workspace = TemporaryDirectory::new();
for markdown in [false, true] {
let root = workspace
.path()
.join(if markdown { "markdown" } else { "plain" });
dever_cli::project::create(&root, markdown).unwrap();
success(&cli(&compiler, &root, "check", &[]));
// Removing the production declaration must not make the test a production root.
let suffix = if markdown { "dever.md" } else { "dever" };
fs::remove_file(root.join(format!("module/hello/greeting/api.{suffix}"))).unwrap();
let rejected = cli(&compiler, &root, "check", &[]);
failure(&rejected, "C016");
failure(
&rejected,
"unreachable from API, CMD, or Job production entries",
);
let app = root.join(format!("module/hello/greeting/app.{suffix}"));
let source = fs::read_to_string(&app).unwrap();
let line = source
.lines()
.position(|line| line.starts_with("greet("))
.unwrap()
+ 1;
failure(&rejected, &format!("hello/greeting/app.{suffix}:{line}:1"));
assert!(!root.join("data").exists());
}
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_needs_no_database_worker_or_runtime_pack() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
write(root, "config/setting.json", "{}");
write(
root,
"module/sample/profile/model.dever",
"type Profile { name: Text }",
);
// A model-only project has no App/Domain code to reject, even without entries.
success(&cli(&compiler, root, "check", &[]));
write(
root,
"module/notification/mail/app.dever",
"read() (value: Int) { value = port.read() }",
);
write(
root,
"module/notification/mail/port.dever",
"read() (value: Int) fails dever.time.SleepResult",
);
write(
root,
"module/notification/mail/adapter.dever",
"external exec \"worker/must-not-start\" {}",
);
write(
root,
"module/notification/mail/api.dever",
"cmd read = app.read",
);
success(&cli(&compiler, root, "check", &[]));
assert!(!root.join("data").exists());
assert!(!root.join("dever.lock").exists());
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_does_not_apply_production_reachability_to_tests() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
write(
root,
"module/sample/profile/app.dever",
"greet() (value: Text) { value = \"hello\" }",
);
write(root, "config/setting.json", "{}");
failure(&cli(&compiler, root, "check", &[]), "C016");
let empty = cli(&compiler, root, "test", &[]);
success(&empty);
assert!(String::from_utf8_lossy(&empty.stdout).contains("0 passed; 0 failed"));
write(
root,
"test/sample/profile/greeting.dever",
"greeting() () { assert_eq(app.greet(), \"hello\") }",
);
// The real case reaches the test compiler's missing-pack boundary, not C016.
let tests = cli(&compiler, root, "test", &[]);
failure(&tests, "runtime pack");
assert!(!String::from_utf8_lossy(&tests.stderr).contains("C016"));
assert!(!String::from_utf8_lossy(&tests.stdout).contains("C016"));
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn cross_build_requires_the_selected_pack_even_when_host_pack_exists() {
let root = TemporaryDirectory::new();
+32 -12
View File
@@ -108,22 +108,24 @@ fn signed_catalog_paths_and_total_size_are_bounded_before_extraction() {
#[test]
fn official_redirects_reject_http_credentials_ports_and_foreign_hosts() {
let current = "https://github.com/shemic/dever-main/releases/latest/download/a";
let current = "https://git.dever.cc/shemic/dever/dist/latest/a";
assert!(
redirect(
current,
"https://release-assets.githubusercontent.com/asset?token=opaque"
"https://git.dever.cc/shemic/dever/dist/1.2.3/a?token=opaque"
)
.is_ok()
);
assert!(redirect(current, "/shemic/dever-main/releases/download/v1.2.3/a").is_ok());
assert!(redirect(current, "/shemic/dever/dist/1.2.3/a").is_ok());
for address in [
"http://github.com/a",
"http://git.dever.cc/a",
"https://evil.example/a",
"https://github.com.evil.example/a",
"https://user@github.com/a",
"https://github.com:444/a",
"https://github.com/a#fragment",
"https://git.dever.cc.evil.example/a",
"https://user@git.dever.cc/a",
"https://git.dever.cc:444/a",
"https://git.dever.cc/a#fragment",
"https://github.com/a",
"https://release-assets.githubusercontent.com/a",
] {
assert!(redirect(current, address).is_err(), "{address}");
}
@@ -135,14 +137,21 @@ fn official_asset_names_are_versioned_and_platform_specific() {
assert_eq!(
source.asset("latest", "manifest.json"),
format!(
"{OFFICIAL_RELEASES}/latest/download/dever-{}.manifest.json",
"https://git.dever.cc/shemic/dever/dist/latest/dever-{}.manifest.json",
platform_identity()
)
);
assert_eq!(
source.asset("1.2.3", "tar.zst"),
format!(
"{OFFICIAL_RELEASES}/download/v1.2.3/dever-{}.tar.zst",
"https://git.dever.cc/shemic/dever/dist/1.2.3/dever-{}.tar.zst",
platform_identity()
)
);
assert_eq!(
source.asset("1.2.3", "ext-runtime-pip-linux-x86_64.tar.zst"),
format!(
"https://git.dever.cc/shemic/dever/dist/1.2.3/dever-{}.ext-runtime-pip-linux-x86_64.tar.zst",
platform_identity()
)
);
@@ -174,7 +183,7 @@ fn server(responses: Vec<Vec<u8>>) -> (ReleaseSource, std::thread::JoinHandle<()
let base = format!("http://{}/releases", listener.local_addr().unwrap());
let worker = std::thread::spawn(move || {
let deadline = Instant::now() + Duration::from_secs(5);
for response in responses {
for (index, response) in responses.into_iter().enumerate() {
let mut stream = loop {
match listener.accept() {
Ok((stream, _)) => break stream,
@@ -194,7 +203,18 @@ fn server(responses: Vec<Vec<u8>>) -> (ReleaseSource, std::thread::JoinHandle<()
let mut reader = io::BufReader::new(stream.try_clone().unwrap());
let mut request = String::new();
reader.read_line(&mut request).unwrap();
assert!(request.starts_with("GET /releases/"));
let (selection, suffix) = [
("latest", "manifest.json"),
("latest", "manifest.sig"),
("1.2.3", "tar.zst"),
][index];
assert_eq!(
request,
format!(
"GET /releases/{selection}/dever-{}.{suffix} HTTP/1.1\r\n",
platform_identity()
)
);
loop {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
+90
View File
@@ -0,0 +1,90 @@
"""Standalone Bash contract; subprocesses never install or change services."""
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
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):
def run_bash(self, script, *arguments, **kwargs):
return subprocess.run(["/bin/bash", "-s", "--", *arguments], input=script,
env={}, text=True, capture_output=True, timeout=10, **kwargs)
def test_generated_entry_matches_source_and_independent_key(self):
expected = generator.render((ROOT / "skills/scripts/install.py").read_text(),
(ROOT / "skills/assets/release.pub").read_text())
self.assertEqual((ROOT / "install").read_text(), expected)
syntax = subprocess.run(["/bin/bash", "-n", str(ROOT / "install")], env={},
text=True, capture_output=True, timeout=10)
self.assertEqual(syntax.returncode, 0, syntax.stderr)
def test_file_and_pipe_help_do_not_need_a_checkout(self):
with tempfile.TemporaryDirectory(prefix="dever-entry-") as directory:
entry = Path(directory) / "install"
entry.write_bytes((ROOT / "install").read_bytes())
for command, contents in ((["/bin/bash", str(entry), "--help"], None),
(["/bin/bash", "-s", "--", "--help"], entry.read_text())):
result = subprocess.run(command, input=contents, cwd=directory, env={},
text=True, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("--trusted-key", result.stdout)
self.assertIn("--system-root", result.stdout)
invalid = self.run_bash((ROOT / "install").read_text(), "--unknown-argument")
self.assertEqual(invalid.returncode, 2)
def test_arguments_python_isolation_and_exit_code_are_preserved(self):
source = '''import json
import sys
def main(default_key):
print(json.dumps([sys.argv[1:], default_key, sys.flags.isolated]))
raise SystemExit(37)
'''
key = "ab" * 32
script = generator.render(source, key)
arguments = ["--release", "/a path/with 'quotes'", "", "$(not executed)", "*", "--no-start"]
result = self.run_bash(script, *arguments)
self.assertEqual(result.returncode, 37, result.stderr)
self.assertEqual(json.loads(result.stdout), [arguments, key, 1])
def test_truncated_body_does_not_execute_python_or_sudo(self):
script = generator.render('def main(default_key):\n print("EXECUTED")\n', "ab" * 32)
for end in (script.index("import sys"), script.index("DEVER_INSTALL_PYTHON\n}"),
script.index('\ndever_install "$@"')):
result = self.run_bash(script[:end])
self.assertNotIn("EXECUTED", result.stdout)
@unittest.skipUnless(os.geteuid() == 0 and Path("/usr/bin/sudo").is_file(),
"real nonroot UID fixture requires root and sudo executable")
def test_nonroot_help_skips_sudo_and_install_forwards_through_sudo(self):
# Replace only the sudo command in this owned Bash process. It records
# argv and returns without executing Python or attempting escalation.
intercept = '''function /usr/bin/sudo() {
printf '<%s>\\n' "$@"
return 43
}
'''
script = intercept + (ROOT / "install").read_text()
lowered = {"user": 65534, "group": 65534, "extra_groups": []}
help_result = self.run_bash(script, "--help", **lowered)
self.assertEqual(help_result.returncode, 0, help_result.stderr)
self.assertIn("--release", help_result.stdout)
install = self.run_bash(script, "--system-root", "/owned image", "--no-start", **lowered)
self.assertEqual(install.returncode, 43, install.stderr)
self.assertEqual(install.stdout.splitlines(), [
"<-->", "</usr/bin/python3>", "<-I>", "<->", "<--system-root>",
"</owned image>", "<--no-start>",
])
if __name__ == "__main__":
unittest.main()
+84
View File
@@ -212,6 +212,8 @@ class InstallerTests(unittest.TestCase):
calls = []
def download(address):
self.assertTrue(address.startswith(
f"https://git.dever.cc/shemic/dever/dist/1.2.3/dever-{self.platform}."))
return (assets_path / address.rsplit("/", 1)[-1]).open("rb")
def extract(command, **kwargs):
@@ -298,6 +300,88 @@ class InstallerTests(unittest.TestCase):
self.assertEqual(len(calls), 1)
self.assertEqual(list(image.iterdir()), [])
def test_metadata_uses_latest_or_exact_directory_and_never_a_github_fallback(self):
for version in ("latest", "1.2.3"):
destination = self.directory(version)
addresses = []
def download(address):
addresses.append(address)
return io.BytesIO(b"metadata")
with mock.patch.object(installer, "download", side_effect=download):
installer.metadata(destination, None, version, self.platform)
self.assertEqual(addresses, [
f"https://git.dever.cc/shemic/dever/dist/{version}/dever-{self.platform}.{suffix}"
for suffix in ("manifest.json", "manifest.sig")
])
def test_redirects_remain_on_the_official_https_site(self):
handler = installer.OfficialRedirect()
current = "https://git.dever.cc/shemic/dever/dist/latest/a"
request = installer.urllib.request.Request(current)
allowed = "https://git.dever.cc/shemic/dever/dist/1.2.3/a"
redirected = handler.redirect_request(request, None, 302, "Found", {}, allowed)
self.assertEqual(redirected.full_url, allowed)
for address in (
"http://git.dever.cc/a", "https://evil.example/a",
"https://git.dever.cc.evil.example/a", "https://user@git.dever.cc/a",
"https://git.dever.cc:444/a", "https://git.dever.cc/a#fragment",
"https://github.com/a", "https://release-assets.githubusercontent.com/a"):
with self.subTest(address=address), self.assertRaises(ValueError):
handler.redirect_request(request, None, 302, "Found", {}, address)
def test_prerequisites_reject_unsupported_hosts_before_download(self):
with mock.patch.object(installer.os, "confstr", return_value="glibc 2.39"):
installer.prerequisites("linux-x86_64")
for libc in ("glibc 2.38", "musl 1.2", ""):
with mock.patch.object(installer.os, "confstr", return_value=libc), \
self.assertRaisesRegex(ValueError, "glibc"):
installer.prerequisites("linux-x86_64")
with mock.patch.object(installer.os, "access", return_value=False), \
self.assertRaisesRegex(ValueError, "openssl"):
installer.prerequisites("linux-x86_64")
@unittest.skipUnless(installer.os.geteuid() == 0, "owned image installation requires root")
def test_standalone_bash_uses_embedded_key_and_cleans_after_signature_failure(self):
# Execute an independently signed fixture to observe the entire entry
# 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.write_text('''#!/usr/bin/python3
import json
from pathlib import Path
import sys
assert sys.argv[1] == "--dever-install"
setting = json.loads((Path(sys.argv[2]) / "config/setting.json").read_text())
assert setting["activate_service"] is False
(Path(setting["system_root"]) / "called.json").write_text(json.dumps(setting))
''')
content = launcher.read_bytes()
manifest = json.loads((self.release / "manifest.json").read_text())
for artifact in manifest["artifacts"]:
if artifact["path"] == "bootstrap/dever":
artifact.update(bytes=len(content), sha256=hashlib.sha256(content).hexdigest())
(self.release / "manifest.json").write_text(json.dumps(manifest))
self.sign_manifest()
entry = generator.render((ROOT / "skills/scripts/install.py").read_text(), self.public.read_text())
image = self.directory("image")
arguments = ["/bin/bash", "-s", "--", "--release", str(self.release),
"--system-root", str(image), "--version", "1.2.3", "--no-start"]
completed = subprocess.run(arguments, input=entry, env={}, text=True,
capture_output=True, timeout=20)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertTrue((image / "called.json").is_file())
self.assertEqual(list(image.glob(".dever-install-*")), [])
(image / "called.json").unlink()
with (self.release / "manifest.json").open("a") as stream:
stream.write(" ")
rejected = subprocess.run(arguments, input=entry, env={}, text=True,
capture_output=True, timeout=20)
self.assertNotEqual(rejected.returncode, 0)
self.assertIn("签名校验失败", rejected.stderr)
self.assertEqual(list(image.iterdir()), [])
if __name__ == "__main__":
unittest.main()
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/python3
"""Opt-in real Bash installer acceptance in an owned image, without host services."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
from verify_prepared_release import WORKSPACE, execute
def verify(args):
result = {
"status": "running", "checks": [],
"transport": "local signed assets; actual generated Bash entry and Rust bootstrap",
"online_install_and_update": "not_run",
"entry_sha256": hashlib.sha256(args.entry.read_bytes()).hexdigest(),
}
def passed(name):
result["checks"].append(name)
args.report.write_text(json.dumps(result, indent=2) + "\n")
print(f"PASS {name}", flush=True)
try:
with tempfile.TemporaryDirectory(prefix="dever-install-entry-", dir=args.scratch_parent) as directory:
scratch = Path(directory)
release = scratch / "release"
release.mkdir()
for name in ("manifest.json", "manifest.sig"):
shutil.copyfile(args.release / name, release / name)
manifest = json.loads((release / "manifest.json").read_bytes())
if manifest["platform"] != "linux-x86_64":
raise ValueError("this acceptance fixture requires a Linux x86_64 release")
result["payload_version"] = manifest["version"]
result["manifest_sha256"] = hashlib.sha256((release / "manifest.json").read_bytes()).hexdigest()
key = scratch / "release.pub"
shutil.copyfile(WORKSPACE / "skills/assets/release.pub", key)
extraction = scratch / "extract.json"
extraction.write_text(json.dumps({
"release": str(release),
"archive": str(args.assets / "dever-linux-x86_64.tar.zst"),
"trusted_key": str(key),
}))
execute([args.extractor, "--dever-extract", extraction])
passed("prepared_release_authenticated_by_existing_rust_extractor")
# A copied standalone entry has no adjacent skill or source checkout.
entry = scratch / "install"
shutil.copyfile(args.entry, entry)
image = scratch / "image"
image.mkdir()
arguments = ["--release", str(release), "--system-root", str(image), "--no-start"]
execute(["/bin/bash", entry, *arguments])
machine = image / "opt/dever"
launcher = machine / "bin/dever"
installed = machine / "versions" / manifest["version"]
if execute([launcher, "version"]) != manifest["version"]:
raise RuntimeError("installed launcher selected the wrong version")
skill = installed / "skills/dever-language"
if execute([launcher, "skill", "path"]) != str(skill):
raise RuntimeError("installed skill does not match the selected core")
if (image / "usr/local/bin/dever").readlink() != Path("/opt/dever/bin/dever"):
raise RuntimeError("public launcher symlink was not installed")
passed("standalone_bash_first_install_and_matching_skill")
tracked = [launcher, machine / "bin/deverd",
installed / "manifest.json", installed / "dever-core"]
baseline = {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in tracked}
def unchanged():
current = {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in tracked}
if current != baseline:
raise RuntimeError("existing installed state changed")
if list(image.glob(".dever-install-*")):
raise RuntimeError("installer left private staging behind")
repeated = subprocess.run(
["/bin/bash", "-s", "--", *arguments], input=entry.read_bytes(),
cwd=scratch, env={}, capture_output=True, timeout=180,
)
if repeated.returncode:
raise RuntimeError(repeated.stderr.decode(errors="replace"))
unchanged()
# Bootstrap records another activation even for the same version.
# Compare the selected version here, then freeze the journal for failure checks.
if execute([launcher, "version"]) != manifest["version"]:
raise RuntimeError("reinstall changed the selected version")
active = machine / "state/active-version"
tracked.append(active)
baseline[str(active)] = hashlib.sha256(active.read_bytes()).hexdigest()
passed("pipe_reinstall_preserves_installed_state_and_cleans_staging")
(release / "manifest.sig").write_text("00" * 64 + "\n")
rejected = subprocess.run(
["/bin/bash", entry, *arguments], cwd=scratch, env={},
capture_output=True, timeout=30,
)
if rejected.returncode == 0 or "签名校验失败" not in rejected.stderr.decode(errors="replace"):
raise RuntimeError("corrupt signature did not fail at authentication")
unchanged()
if execute([launcher, "version"]) != manifest["version"]:
raise RuntimeError("failed install changed the selected version")
passed("corrupt_signature_rejected_existing_install_preserved")
result["status"] = "passed"
except BaseException as error:
result["status"] = "failed"
result["error"] = str(error)
raise
finally:
args.report.write_text(json.dumps(result, indent=2) + "\n")
def main():
parser = argparse.ArgumentParser(description=__doc__)
for name in ("release", "assets", "extractor", "scratch-parent", "report"):
parser.add_argument(f"--{name}", required=True, type=Path)
parser.add_argument("--entry", type=Path, default=WORKSPACE / "install")
args = parser.parse_args()
if os.geteuid() != 0:
parser.error("requires root for its own image; never installs host services")
for name, value in vars(args).items():
setattr(args, name, value.absolute())
args.report.parent.mkdir(parents=True, exist_ok=True)
verify(args)
if __name__ == "__main__":
main()
+5 -5
View File
@@ -202,14 +202,14 @@ def install_assets(assets, image, manifest):
stem = f"dever-{manifest['platform']}"
base = installer.RELEASES
routes = {
f"{base}/latest/download/{stem}.manifest.json": assets / f"{stem}.manifest.json",
f"{base}/latest/download/{stem}.manifest.sig": assets / f"{stem}.manifest.sig",
f"{base}/download/v{manifest['version']}/{stem}.tar.zst": assets / f"{stem}.tar.zst",
f"{base}/latest/{stem}.manifest.json": assets / f"{stem}.manifest.json",
f"{base}/latest/{stem}.manifest.sig": assets / f"{stem}.manifest.sig",
f"{base}/{manifest['version']}/{stem}.tar.zst": assets / f"{stem}.tar.zst",
}
for artifact in manifest["artifacts"]:
if artifact["path"] == "bootstrap/dever" or artifact["path"].startswith("bootstrap/lib/"):
name = f"{stem}.blob-{artifact['sha256']}"
routes[f"{base}/download/v{manifest['version']}/{name}"] = assets / name
routes[f"{base}/{manifest['version']}/{name}"] = assets / name
installer.download = lambda address: routes[address].open("rb")
installer.install(argparse.Namespace(
version="latest", release=None, system_root=image, no_start=True,
@@ -225,7 +225,7 @@ def accept(release, assets, image, report, init, extension_helper=None):
"version": manifest["version"], "platform": manifest["platform"],
"manifest_sha256": hashlib.sha256((release / "manifest.json").read_bytes()).hexdigest(),
"checks": [], "global_service_actions": "not_run", "online_update": "not_run",
"installation_transport": "local_github_assets" if assets else "local_signed_directory",
"installation_transport": "local_official_assets" if assets else "local_signed_directory",
}
def passed(name):