探索

不拿出来的东西守住了应用 — 每个服务器都是应用,以及之后的故事

作者: makemind · 2026年7月23日

2026 年 4 月,这本杂志刊出了《每一个服务器都是应用》。它把应用从图标和商店上剥下来,还原成画面、工具、数据三样,而服务器早就握着其中两样,缺的只是一层画面。

那篇文章真正的主题不是那个等号,而是等号之后。服务器变成应用,不是所有能力都暴露到画面上,而是决定什么放到表面、什么锁在里面。 它用一个场景来画:咖啡馆的服务器开放点单和库存,把销售汇总只放在店主的画面上,而数据库删除则根本不作为任何工具拿出来。

然后文章这样收尾 —「在接下来的文章里看。」

现在我们来还这个承诺。问题在于,当时那篇文章脚下踩着的全是叙述。「不存在的按钮无法被攻破」听着有理,但只有你把工具清单确实锁着这件事拿出来看,它才成立。 不拿出来,那就是主张,不是安全。

所以这篇文章不讲新愿景。它把此前立下的主张,放到真正跑起来的东西上确认一遍。

工具清单就是表面

先从最简单的确认开始。表面是什么,问服务器就出来了。

接上桌上的 STM32H723 开发板,调 tools/list,它这样回答。

{"tools":[
  {"name":"led.set","description":"Turn the on-board LED on or off",
   "inputSchema":{"type":"object","properties":{"on":{"type":"boolean"}},"required":["on"]}},
  {"name":"sys.info","description":"Report LED state and uptime",
   "inputSchema":{"type":"object","properties":{}}}]}

只有两个。这块板能做的远不止这些 — 擦 Flash、改时钟、跳进 bootloader。那些能力在板子里,却不在清单上。 而不在清单上,就没有办法调用。画面定义再怎么摆弄,不存在的名字也不会被调起。

这就是《每一个服务器都是应用》所说的第二道边界最精瘦的形态。它不是防御代码。是根本没有修那条路。

(这段回应来自真实开发板。整条链条在开发板递出自己的画面。)

支付路径 — 没做的东西反而成了卖点的地方

更有意思的是那个选择变成优点而不是弱点的时候。

我们做了一个把 POS 画面搭到刷卡终端上的样例。门店服务器接单、算钱、发起支付。而支付处理器做的事,全部就这么多。

final amount = unpaid.fold<int>(0, (sum, o) => sum + (o['price'] as int));
final result = await terminal.call('terminal.authorize', {'amount': amount});

它不读卡。不判断是否授权。不连支付网络。 它数出金额、去问、把答案记下来。中间发生的一切都发生在通过认证的硬件里,而这段代码一次都没打开过那里面。

为了不止于主张,把适配器和终端实际来回的行原样贴上。

=> {"id":2,"tool":"terminal.authorize","args":{"amount":11000}}
<= {"id":2,"ok":true,"result":{"approved":true,"approvalCode":"A4101","last4":"4242","brand":"SIM","amount":11000}}

请看响应里没有的东西。 没有卡号。没有磁道数据。只有授权码和后四位。因为终端那一端并没有能递出更多东西的工具 — 而真实的支付设备也不会递出更多。

这里露出了 4 月那篇没看见的一件事。它把「不拿出来」描述成降低风险的选择。真做出来才发现那只是一半。对终端厂商来说这是卖点。 不打开已认证的安全区域,意味着加上画面这根轴之后不必重走认证。不是因为没做所以安全,而是因为没做所以卖得动。

(完整链条与代码在接到一块芯片上,接上的设备就成了终端。)

把判断放在工具之外 — 正是 4 月预告的那一条

4 月那篇在最后一段预告了两个案例。一个是一帧视频都不碰的行车记录仪,另一个是「把诊断留给人、只把诊疗流程整理到画面上的医生的工具 — 把判断的权限放在工具之外的案例」

我们把那个形状做成了工厂设备。为什么不是医疗,写在后面。

这是设备服务器递出机器状态的处理器。

// The server states facts and how those facts compare to limits.
// The machine never says "fine" — that word belongs to the person
// holding the checklist.
final overdue = (m['runHours'] as int) > (m['serviceEveryHours'] as int);
final vibrationOver =
    (m['vibrationMm'] as num) > (m['vibrationLimitMm'] as num);
return _json({
  'id': id, ...m,
  'serviceOverdue': overdue,
  'vibrationOverLimit': vibrationOver,
});

serviceOverdue: true 是事实。safe: false 是判断。服务器只出前者,而且没有能出后者的工具。

我们在上面搭了一个 LLM。这里是 4 月无法想象的地方 — 当一个会挑工具的东西接上来之后,「没做的东西」是否依然没被做出来。

答案在日志里。

Q: how is CONV-03 doing?
   grounded=true calls=1 equipment.read(id: CONV-03) -> overdue=true vibrationOver=true
   A: CONV-03 needs attention — service is overdue (9310 h against a 8000 h interval)
      and vibration is above limit (5.2 mm against 4.5 mm).

Q: what is the weather like?
   grounded=false calls=0
   A: I can look up machines, their current readings, and the plant checklist
      for a machine type. Ask me about one of those.

第二条是核心。对于工厂没有工具可回答的问题,工具调用 0 次。它没有凭空造出不存在的能力。而且这个事实会显示在画面上 — 依据为 0 的回答会以另一副面孔呈现。

这把 4 月那篇的命题往前推了一格。「不存在的按钮无法被攻破」现在必须加上「也无法被凭空造出来」。 工具清单定义表面,不只在人去按的时候,模型去选的时候也一样。

(那套接线与依据显示结构在把依据放在答案旁边。)

边界不只从上面划

4 月那篇把边界分成两道。接收侧(运行时不信任定义,只允许既定的组合)和暴露侧(一开始就不做成工具)。

做出来之后发现还有第三道。 硬件自己。

这是温室继电器节点的 C 代码。

/* The relay clamps its own range. Whatever the rule upstream
 * decided, the hardware still refuses to exceed itself. */
if (v < 0) v = 0;
if (v > 100) v = 100;
n->value = v;

不管上面的规则决定了什么,继电器都不越过自己。支付终端的模拟器在同一个位置也有同样的东西。

if (!json_num(line, "amount", &amount) || amount <= 0) {
    printf("{\"id\":%ld,\"ok\":false,\"error\":\"amount must be positive\"}\n", rid);
}

这为什么是另一道边界?前两道依赖软件的善意 — 依赖运行时好好校验,依赖服务器开发者不去做危险的工具。第三道不依赖。设备自己守住自己的物理极限。 服务器被攻破也好,运行时被骗也好,继电器不会超过 100。

安全必须是继电器的性质,而不是规则的善意。我们给 4 月那篇的边界二分法加上这一行。

(温室的链条在换掉传感器,控制照旧。)

表面与能力分岔的地方

做包应用的时候,偶然得到了最清晰的一张图。

我们用四个 JSON 做了无人店铺的店主应用,然后改那些 JSON 把它变成洗衣店。标签变了 —LAUNDRY — 24HMachines running。可是商品依然是冰淇淋Cone vanillaBar mintTub 474ml

一开始看着像 demo 的瑕疵。再看一遍,那是这篇文章的论点被烙成了一张图。包里握着的是表面,能力握在服务器手里。 画面怎么改商品都不变,这不是失败,而是边界正在生效的证据。

4 月那篇把这个分岔表述为「你把哪些能力选成工具,决定了它的脸」。一个半变成洗衣店的画面,展示了那句话的反面 —换了脸,能力不会跟过来。

(包到底是什么,见一个文件夹就是应用。)

该从 4 月那篇里收回的东西

同一篇文章再刊一次,若把当时写错的原样留着,那就不是重做。

删掉「会写服务器就已经懂了那门语言的八成」这句。 一个从没量过的数字。真做下来,服务器开发者在画面定义里确实会遇到第一次见的东西 — 状态绑定、每页各自的初始值、动作调用工具的写法。不难,但不是「已经懂了」。我们不拿没量过的比例当作自信的依据。

说出「约定好的语言」的名字。 4 月那篇一路留作无名。一篇主张「不开放就不成立」的文章,如果把那个开放协议所指模糊掉,就没法验证。那个协议是 MCP(Model Context Protocol),而这个系列的每个样例都说它。上面贴的 tools/list 响应和 initialize 往返,就是这个协议的真实字节。

明确标注咖啡馆的场景是举例。 4 月那篇的那个场景是以「假设」开头的思想实验,却因为写得具体而读起来像观测。在这篇里顶替咖啡馆位置的那些 — 门店服务器、设备服务器、温室、开发板 — 全部是真跑过的,并逐个用链接标明来自哪里。我们不把想象和观测排成同一种字体。

改写「在过去几卷里反复看到」。 4 月那篇说,在面板、芯片、仪器几篇里看到了服务器定义画面。那几篇当时既没有实渲染也没有运行日志。不是看到了,只是那样写了。 现在不同。这篇引用的全部是带着运行日志和实渲染截图的文章。

这个样例没有做的事

4 月那篇的三条诚实注记依然有效。等号不是自动的,不是所有东西都需要画面,既有系统不会立刻变身。做出来之后全都成立。

在此加上这次学到的。

这篇验证的只到「没做出来的能力够不着」。 那是「工具清单即表面」这一结构的结果,上面的日志展示了它。但「所以它是安全的」是更大的命题,这篇没有证明。 认证、权限、传输安全、服务器自身被攻破,全都在这篇之外。「不在清单上的东西不会被调用」和「系统是安全的」,是两个不同尺寸的主张。

为什么避开医疗。 我们把 4 月预告的「医生的工具」换成工厂设备来做。把判断权限放在工具之外这个结构是一样的,但临床判断出错时的分量不同,而这个系列没有做能承担那份分量的验证。结构相同,不等于领域相同。 医疗案例等具备那份验证之后另行处理。

行车记录仪的案例仍未偿还。 4 月预告的两个里,视频那一侧 — 一帧都不碰而只开放日志和设置 — 这篇没有处理。支付路径的案例形状相同,但不是同一个案例。记为未回收。

而且这篇没有新样例。 这里引用的日志和代码全部产自此前的五篇。horizon 类文章的工作不是新盖,而是踩在已经盖好的东西上,而 4 月那篇的问题正是脚下没有却装作在踩。

再问一次:什么不拿出来

重写 4 月那篇的最后一段。当时是这样收的 —「在接下来的文章里,我们会看到『不把什么做成工具』变成与『做什么』同等重要的设计决定的地方。」

看到了。整理一下看到的,是这样。

  • 工具清单就是表面。 开发板只拿出两个,其余能力连可调用的名字都没有。
  • 没做的东西成了买单的理由。 不打开支付安全路径,对厂商意味着不必重走认证。
  • 接上模型,不存在的工具也不会冒出来。 对没有工具可答的问题调用 0 次,而且这个事实显示在画面上。
  • 边界有三道。 接收侧的运行时、暴露侧的设计,以及自己守住自己极限的硬件
  • 换了表面,能力不会跟过来。 半变成洗衣店的画面把那条线画了出来。

服务器变成应用之后,不该碰的东西照旧不碰 — 4 月是这么写的。现在可以再加一行。「照旧不碰」这件事,可以用工具清单和通信日志摆出来看。

看不见的边界不是边界,是承诺。承诺可能不被遵守,而如果没有办法确认它有没有被遵守,你也就不能说它被遵守了。


makemind.dev 「探索」— 当时推迟的承诺,用运行日志与真实开发板的回应还上了。

Twitter