Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
445 changes: 445 additions & 0 deletions .agents/docs/2026-08-30-openkal-0.10-ecosystem-plan.md

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions .agents/docs/2026-08-30-openkal-0.11-start-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# openkal 0.11 提案:把 spawn 的变体收敛成一个「怎么起」的描述

> 状态:**待拍板,一行代码都还没写。** 0.10 的六个 PR 正在合流,这份是 0.10 之后的事。

## 0. 为什么现在必须谈这个

0.10 之前,spawn 的变体是两个;0.10 加了第三个;**真实消费者的测试又要求第四和第五个**。
`process.h` 自己的注释早就警告过这条路的尽头:

> Declaring every combination is how an interface acquires four spawns and then
> eight, so the combination is declared when something needs it and not before.

⭐ 现在「something needs it」到了,而且是**两件事同时到**。所以要谈的不是「再加一个变体」,
是**这个族该长成什么样**。

## 1. 两个缺口(都由报告者的测试套件发现,不是我猜的)

### 1.1 说不出「程序在哪个目录里跑」

`kal_process_spawn(base, path, …)` 的 `base` 只**解析名字**。被起的程序的工作目录是
**实现自己的**,调用者碰不到。

实测(宿主作对照):

```
chdir(/tmp) 之后 : /tmp
被起程序的 pwd : <原目录> ← openkal-musl
被起程序的 pwd : /tmp ← 宿主
```

⚠️ **后端修不了。** `fchdir(b)` 会让 openkal-linux 那句注释成真而行为不会变好:`b` 是名字
落在哪个 preopen 就是哪个,`/usr/bin/sh` 的 `b` 就是根。**给程序起名**和**说它在哪儿跑**
是两个目录,接口只带了一个。

⚠️ 并且这**不是**「openkal 拒绝可变 cwd」那条设计的问题。那条拒绝的理由是
「a working directory that can be changed is shared mutable state between execution
contexts」——**这个理由完全成立,而且不适用于这里**:在**起程序的那一刻**说明它在哪儿跑,
是每次 spawn 各自说一次的、不可变的、不被任何两个上下文共享的东西。⇒ 该拒绝的继续拒绝,
缺的是另一件事。

### 1.2 说不出「连它起的东西一起杀」

`kal_process_terminate` 只到被起的那一个。shell 起到后台的东西没人管。

消费者的写法是标准做法(子进程 `setpgid(0,0)` 自立组,超时 `killpg` 整组),这个端口上
`setpgid` 报 EPERM、`kill(-pgid)` 报 ESRCH。他们有兜底所以**直接子进程杀得掉**,
后代杀不掉。

⚠️ 0.10 的 `KAL_PROCESS_PROP_BOUND_LIFETIME` 解决的是**另一层**:它把被起程序绑在调用者
的命上(`PR_SET_PDEATHSIG`),够不到孙子。两条不重叠。

## 2. ⭐ 提案:一个 `kal_process_start`,而不是第四、第五个 spawn

```c
/* 声明时定死,clause 5.3 */
struct kal_start {
struct kal_dir base; /* `path' 相对谁解析 */
struct kal_dir work; /* 程序在哪个目录里跑 */
const struct kal_preopen* grants; /* 交给它的目录,可为空 */
kal_uintptr grant_count;
kal_uintptr flags; /* KAL_START_BOUND_LIFETIME | KAL_START_OWN_JOB */
};

int kal_process_start(const struct kal_start*,
const char* path, kal_uintptr path_len,
const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc,
const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc,
const struct kal_spawn_streams* streams,
struct kal_process* out);
```

`KAL_START_OWN_JOB`:被起的程序**和它起的一切**构成一个单位,`kal_process_terminate`
把这个单位整个结束。Linux 是新进程组或 cgroup,Windows 是 job object,macOS 是进程组。

### 2.1 为什么是 flags 而不是再开变体

因为**两个缺口都要求「每次 spawn 各自选」**,而不是「实现要么总这样要么总不这样」:

⚠️ 新进程组会**脱离终端前台组**,于是带界面的程序里,子上下文读终端会拿到 SIGTTIN 停住。
我在 0.10 评估 `spawn_bound` 时就是因为这个排除了「让 terminate 杀进程组」——**那个理由
现在仍然成立**。三个流全是管道的调用者(跑 shell 的那种)要这个行为;交互式的调用者绝不要。

⇒ 这个区别**只有按次表达才对**。做成默认行为是错的,做成实现属性也是错的。flags 正好。

⭐ 而 flags 与 openkal 的既有风格一致:`kal_*_props` 本来就是位集,实现对做不到的那一位
答 `kal_err_not_supported`,和现在 `spawn_bound` 的约定一模一样。

### 2.2 ⚠️ 这个提案的代价,先说清楚

**它让 `spawn` / `spawn_with` / `spawn_bound` 三个都变成冗余的,而 clause 8 不许删。**
末态是四个声明,其中三个是历史。

这不好看。但另一条路的末态是**八个、然后十六个**,而且每加一个都要在五个实现里各写一遍。
⇒ 我认为「一个通用形式 + 三个历史留下的」是可辩护的末态,「十六个 spawn」不是。

**第二个代价**:`struct kal_start` 一旦声明就冻住(clause 5.3),将来再有新需求,要么进
`flags`(只够表达开关,不够表达带参数的东西),要么又开一个。⇒ flags 里能表达的将来能加,
带新参数的将来仍然会疼。这一点提案**没有**解决,只是把疼痛推远了。

## 3. 备选,以及我为什么不选它们

| 方案 | 为什么不选 |
|---|---|
| 加 `kal_process_spawn_in(work, base, …)` | 只解决 1.1;1.2 还要再来一个;组合还要再来。正是注释警告的那条路 |
| 让 `base` 同时当工作目录 | 1.1 里已经证明这不成立:`/usr/bin/sh` 的 base 是根 |
| 把工作目录塞进 `grants` 里约定一个名字(如 `"."`) | 隐式,违反「一个意思一种拼法」 |
| 加一个改 cwd 的操作 | openkal 明确拒绝过,**而且拒绝得对**——见 1.1 末段 |
| 什么都不加,让消费者自己绕 | 他们已经在绕了(`scripts/build-static.sh`),而绕不过 1.2 |

## 4. 落地顺序(若拍板要做)

1. openkal:声明 + SPEC 条目 + SURFACE + conformance 三节
2. 五个实现各自实现;做不到的位不声明,答 `kal_err_not_supported`
3. openkal-musl:`posix_spawn` 的 `addchdir` / `setpgroup` 接到 `work` / `OWN_JOB`
4. openkal-llvm-runtime 跟版本
5. 用报告者的工程验证:kaos 的三条红判据、agent-core 的三条,应当全绿

⚠️ **必须在 0.10 全部合流、发布、进 index 之后再开始**,否则两条链在同一批仓库里交叉,
版本要求又是精确的,任何一个包对不上整张图都解析不了。
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
# primitive that copies an address space and starts a context in the
# copy, and inventing one would be the simulation clause 3.1
# forbids. `--no-fork` asserts the refusal.
- { name: 'windows, gcc', os: windows-2022, toolchain: 'gcc@16.1.0', target: 'x86_64-windows-gnu', net: 'yes', fork: '--no-fork', shell: '--no-shell', abort: '--abort-terminated', dirtime: '--no-dir-time' }
- { name: 'windows, gcc', os: windows-2022, toolchain: 'gcc@16.1.0', target: 'x86_64-windows-gnu', net: 'yes', fork: '--no-fork', shell: '--no-shell', abort: '--abort-terminated', dirtime: '--dir-time' }
defaults:
run:
shell: bash
Expand Down
Loading
Loading