Three parts do the work — a multi-agent architecture that scopes who
may touch what, an appos scaffolding that refuses to guess, and a hardware
toolset that reads the board rather than the log file. Each section below ends with
what we measured and what broke.
真正干活的是三部分:一套划清"谁能碰什么"的多 agent 架构、一套拒绝猜测的
appos 脚手架,以及一套读板子而不是读日志的硬件工具集。
每一节最后都写着我们测到了什么、以及什么东西坏过。
Omnistack does not give every agent the whole toolbox. Each one is granted the tools its stage needs and denied the rest, so the boundary is expressed in configuration rather than in a paragraph of prompt asking nicely. Five primary agents ship today, and they map one-to-one onto the stages on the detail page.
Produces the framework — scenarios, devices, apps and the contracts between them — as documents plus a diagram derived from them. It has no code editor at all: every artifact goes through a publishing tool that refuses to write a stale one.
Builds and modifies an .appk and verifies it in the
simulator. It settles simple requirements itself; there is no prerequisite stage to hand
a request back from.
Puts an already-built package onto a real board from a clean state. The tool that can take down a release is deliberately not on its list.
Writes and runs click cases against the panel and reports what came back. No shell, no file writes — the plan is an argument, because the tool already accepts it.
A permission boundary that is only written in a prompt is not a boundary. Three findings from running all five agents against a real instance shaped the way they are configured.
Omnistack 不给每个 agent 一整 toolbox。每一个只被授予它那一阶段需要的工具,其余的显式拒绝—— 边界写在配置里,而不是写在提示词里求它守规矩。今天有五个 primary agent, 与技术细节页上的阶段一一对应。
产出框架——场景、设备、应用,以及它们之间的契约——形式是文档加一张由文档派生的图。 它完全没有代码编辑器:所有产物都走一个发布工具,该工具拒绝写出过期的那一份。
构建和修改 .appk 并在模拟器里验证。简单需求它自己拍板,
没有前置阶段可以把需求再交回去。
把已经构建好的包从干净状态送上真板。能拆掉一个 release 的工具,刻意不在它的清单上。
对着面板写并跑点击用例,报回来的是什么。没有 shell、没有文件写入—— 用例计划是一个参数,因为工具本来就收这个。
只写在提示词里的权限边界不是边界。把五个 agent 对着真机跑一遍,得到三个结论, 它们塑造了现在的配置方式。
Between a signed .appk and a running board there is one command, and a chain
of builders that refuse to guess at anything they were not given. The design rule is the
same everywhere: a value that cannot be derived is named, and the tool stops rather than
inventing it.
| Piece | What it does |
|---|---|
| appos | Five-layer runtime. An application is a signed, data-only package — no native code, no scripts, no device nodes. Crossing a layer is refused at build time. |
| build_nx5_base_kit.py | Assembles the base Kit every Release is built from. The component sources stay outside; this stamps each manifest from its own contents and derives the recorded digests rather than typing them. |
| build_arm_release.py | Cross-builds the runtime, and refuses a release profile it has not qualified. |
| nx5_deploy_app.py | One command from a signed package to a running release, in either application mode. |
| appos_slint_mcp.py | Reads and drives the panel's MCP endpoint for the board UI tests. |
Each refuses rather than defaults: an absent board identity, an absent base Kit and an absent cross-toolchain prefix are each reported by name, with the variable that would supply them.
One pass deployed 22 applications to the board in turn and read the result back over the device. Twenty-one came up with every process and socket flag set. One did not.
Measured on a physical PLC NX5, each application deployed and checked in sequence against the same board.
localization.supported_locales must contain 1 through 8 locales.
从一个签名的 .appk 到板子上跑起来之间只有一条命令,中间是一串拒绝猜测的构建器。
设计规则到处都一样:推不出来的值就点名报出来,工具停下来,而不是自己编一个。
| 组件 | 做什么 |
|---|---|
| appos | 五层运行时。应用是签名、纯数据的包——不含原生代码、不含脚本、不含设备节点。 跨层在构建期就被拒绝。 |
| build_nx5_base_kit.py | 装配每个 Release 都基于其上的基 Kit。 组件源码留在外面,它只把每个 manifest 按自身内容盖章,并把记录下来的摘要算出来而不是手打。 |
| build_arm_release.py | 交叉编译运行时,并且拒绝它没有认证过的 release profile。 |
| nx5_deploy_app.py | 从签名包到跑起来的 release,一条命令,两种应用模式都支持。 |
| appos_slint_mcp.py | 通过面板的 MCP 端点读取和驱动界面,供板上 UI 测试使用。 |
每一个都拒绝而不是兜底:缺失的板子身份、缺失的基 Kit、缺失的交叉工具链前缀, 都会被点名报出,并附上本该提供它的那个环境变量。
一次跑完把 22 个应用依次部署到板子上,再通过设备把结果读回来。21 个所有进程和套接字标志全部置起, 有一个没有。
实测于一块真实的 PLC NX5,各应用依次部署并在同一块板子上检查。
localization.supported_locales must contain 1 through 8 locales。The target is a PLC NX5 indoor unit. Reaching it is unglamorous work — a serial console that needs its line endings and translation mode set correctly, a read-only release over NFS, and a device that does not answer the questions you would expect it to.
On this board adb shell is unreliable, which is why the UI
tests create an adb forward instead — the forward works where the shell does
not. The console also needs -icrnl and carriage-return-plus-newline line
endings; without them the board echoes kernel log lines back as if they were commands.
A deployed release is mounted read-only over NFS from the host, and the deploy checks that it is. A managed container cannot always claim the kernel's NFS ports on the same host, so the target can declare that an external server already exports the staging root and use it as-is.
Every deploy ends by reading the board back. A green deploy is eight booleans, and they are reported individually rather than as a single verdict.
| Check | What it means |
|---|---|
| supervisor | The process that owns the runtime is up. |
| system_service | The privileged owner of anything live is up. |
| shell | The application runtime started. |
| system_service_socket | Its socket accepts. |
| platform_service_socket | The platform service's socket accepts. |
| shell_first_frame | The runtime presented a first frame. |
| active_release | The release it says is active is the one mounted. |
| release_is_read_only_nfs | It is mounted read-only over NFS. |
All eight true proves the runtime started. It does not prove the application works — that is what the board UI tests are for, and it is a separate claim.
data-app and aot-app are different runtimes, not a build setting.
In data mode the interpreter for the package lives inside the Shell's address space, so the
package's size lands directly on a 64 MB board's resident set. A package that fits in
one may not fit in the other, and the failure is a kill rather than an error message.
| The indoor unit package | data-app | aot-app |
|---|---|---|
| Result | Killed — resident set exhausted | Runs; all eight checks true |
| Resident set on the board | 42,296 kB before the kill | 18,392 kB running |
| Full deploy, warm toolchain | 26 s | 546 s |
The same signed package, deployed through the same tool, to the same board.
AOT costs about twenty times the time and buys back more than half the memory. That is the trade, stated in numbers rather than argued: iterate in data mode, qualify in AOT.
The AOT runtime was built with one codegen unit, which serialises the compile. Relaxing only that — keeping LTO on — made it 1.6× faster for 2.3% more bytes, and the board's resident set did not get worse.
Re-measured on the same machine with LTO held on, for the largest package in the catalogue. Earlier measurements relaxed LTO as well, which is why every row had looked slower and larger.
目标是一台 PLC NX5 室内机。连上它不是什么光彩的工作——一条要正确设置行尾和转换模式的串口控制台、 一份走 NFS 的只读 release,以及一台不会回答你预期那些问题的设备。
在这块板子上 adb shell 不可靠,所以 UI 测试改成建
adb forward——shell 不行的地方 forward 行。控制台还需要 -icrnl
和回车加换行的行尾;不设的话板子会把 kernel 日志回显成命令执行。
部署好的 release 从宿主机以只读方式挂载,部署检查会验证这一点。 同一台宿主机上,托管容器不一定能抢到内核的 NFS 端口, 所以 target 可以声明"已有一个外部服务器导出了 staging 根"并直接用它。
每次部署都以读回板子结束。全绿是八个布尔值,而且逐项上报,而不是给一个笼统的结论。
| 检查 | 它意味着什么 |
|---|---|
| supervisor | 持有运行时的那个进程起来了。 |
| system_service | 任何存活状态的特权归属者起来了。 |
| shell | 应用运行时启动了。 |
| system_service_socket | 它的套接字可接受连接。 |
| platform_service_socket | 平台服务的套接字可接受连接。 |
| shell_first_frame | 运行时给出了第一帧。 |
| active_release | 它自称活跃的那个 release 正是挂载着的那个。 |
| release_is_read_only_nfs | 它以只读 NFS 挂载。 |
八项全为真只证明运行时起来了,不证明应用好用——那是板上 UI 测试的事, 而且那是另一个结论。
data-app 和 aot-app 是两套不同的运行时,不是一个编译开关。
data 模式下包的解释器活在 Shell 自己的地址空间里,所以包的大小直接压在 64 MB 的板子常驻集上。
一个在一种模式下装得下的包,换一种可能装不下,而失败形式是被杀掉,不是一句报错。
| 室内机那个包 | data-app | aot-app |
|---|---|---|
| 结果 | 被杀——常驻集耗尽 | 跑起来,八项全绿 |
| 板上常驻集 | 被杀前 42,296 kB | 运行中 18,392 kB |
| 完整部署,热工具链 | 26 秒 | 546 秒 |
同一个签名包、同一个工具、同一块板子。
AOT 大约要多花二十倍的时间,换回一半以上的内存。这就是这笔交易,用数字写出来而不是靠争辩: 在 data 模式里迭代,在 AOT 模式里做认证。
AOT 运行时原本按一个 codegen unit 构建,那会把编译串行化。只放松这一项——LTO 保持不变—— 快了 1.6×,二进制大了 2.3%,而板上的常驻集没有变差。
在同一台机器上、保持 LTO 开启、对目录里最大的包重新测量。 更早那批测量同时放松了 LTO,这就是为什么每一行看上去都更慢更大。
Most embedded tooling can tell you a build succeeded. Omnistack also tells you what it did not establish, because a claim you cannot bound is not a claim you can ship on.
Measured on a physical PLC NX5 board running a deployed application.
大多数嵌入式工具只能告诉你构建成功了。Omnistack 还会告诉你它没有证明什么—— 因为一个划不出边界的结论,不是一个能拿去交付的结论。
数据来自一块真实 PLC NX5 板子上运行的已部署应用。
European two-line indoor unit. Armel target, cross-compiled and deployed over a signed release.
Bedside call unit, sharing one AppOS with the room door and declaring a disjoint capability set.
Runs the real application package before any hardware is involved, including touch and frame assertions.
Flutter targets built, installed and screenshotted on device when the deliverable is a handset app.
欧洲两线室内机。armel 目标,交叉编译,通过签名 Release 部署。
床旁呼叫单元,与病房门共用同一套 AppOS,并声明互不相交的能力集。
在碰任何硬件之前运行真实的应用包,支持触摸与帧断言。
交付物是手机应用时,用 Flutter 构建、装机并在真机上截图核对。