@abelxiaoxing/cadence 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/config/.env.example +6 -0
- package/package.json +1 -1
- package/prompts/abel-design.md +6 -0
- package/prompts/abel-diagnose.md +6 -0
- package/prompts/abel-implement.md +6 -0
- package/src/apply-transaction.ts +28 -8
- package/src/build-windows-job.mjs +95 -0
- package/src/candidate-workspace.ts +6 -0
- package/src/change-verification.ts +6 -2
- package/src/durable-contracts.ts +2 -0
- package/src/durable-workflow.ts +8 -2
- package/src/execution-profile.ts +28 -2
- package/src/execution-retention.ts +77 -0
- package/src/index.ts +200 -27
- package/src/isolation-backend.ts +2 -1
- package/src/operator-cli.mjs +652 -97
- package/src/operator-tools.ts +7 -1
- package/src/package-state.ts +11 -0
- package/src/package-verification.ts +49 -17
- package/src/package-workflow.ts +3 -0
- package/src/phase-execution.ts +11 -6
- package/src/remove-path.ts +32 -0
- package/src/verification-capability.ts +1 -1
- package/src/verification-environment-io.ts +123 -14
- package/src/verification-environment.ts +31 -2
- package/src/windows-job-backend.ts +419 -0
- package/src/windows-job.c +149 -0
- package/src/workspace-io-entry.mjs +2 -2
- package/src/workspace-io-worker.mjs +207 -110
package/README.md
CHANGED
|
@@ -10,10 +10,27 @@
|
|
|
10
10
|
|
|
11
11
|
1. 安装 Node.js >=22.13、Git、Pi,以及 `@fission-ai/openspec@1.5.0`。
|
|
12
12
|
2. 在 Pi 中安装:`pi install npm:@abelxiaoxing/cadence`。
|
|
13
|
-
3. 默认 Implement
|
|
13
|
+
3. 默认 Implement 使用 Linux Bubblewrap;Windows x64 可信项目可显式选择 `host-trusted`,构建与验收见 [Windows 原生执行](docs/windows-host-trusted.md)。
|
|
14
14
|
4. 首次进入项目先运行 `/abel-init`,然后 `/abel-design <需求>`,确认集中呈现的目标与方案,设计完成后运行 `/abel-implement <change>`。
|
|
15
15
|
5. 暂停时先查看原因和建议,不要靠反复 resume 或重启“碰运气”;未完成的工作会保留。
|
|
16
16
|
|
|
17
|
+
### Pi / Web UI 阶段启动约定
|
|
18
|
+
|
|
19
|
+
阶段命令需要 Cadence 扩展与包内 prompts 同时加载;只有提示正文可见,不代表 `abel_dispatch` 已注册或已激活。
|
|
20
|
+
Web UI / SDK 集成应在会话空闲时提交原始命令:`session.prompt("/abel-design <需求>", { source: "rpc" })`。
|
|
21
|
+
不要在客户端预展开模板,也不要通过 `steer()`、`followUp()` 或运行中的排队请求启动阶段。
|
|
22
|
+
运行中的显式阶段命令会被拒绝并报告 `abel-stage-requires-idle`,等待当前请求结束后重新提交即可;原有阶段不会因此被退出或丢弃。
|
|
23
|
+
|
|
24
|
+
启动失败有明确错误码:`abel-stage-provenance-invalid`(包来源不匹配)、`abel-stage-unverified-input`(缺少可验证的原始调用)和 `abel-stage-tools-unavailable`(工具未暴露或 Design 工具边界设置失败)。
|
|
25
|
+
扩展会阻止失败请求的工具执行;不能改用 bash、subagent 或 terminal 工具绕过。
|
|
26
|
+
空闲时正常启动会在首次模型请求前启用 `abel_dispatch` 并限制 Design 工具集合,显式退出后恢复进入前的工具集合。
|
|
27
|
+
恢复会话或重载不会从历史提示自动激活阶段。
|
|
28
|
+
|
|
29
|
+
如果扩展根本没有加载,扩展自身无法执行检查;提示中的缺失工具说明只是模型侧兜底,不是宿主级拦截。
|
|
30
|
+
Web UI 必须展示资源加载器的 `getExtensions().errors`,核实当前会话已绑定扩展,并在发送阶段提示前检查 `session.getAllTools()` 中的 `abel_dispatch`;不存在时直接报告配置错误,不发送阶段正文。
|
|
31
|
+
请检查包资源过滤是否禁用了 extensions、实际运行的 Node / Pi 版本,以及是否恢复了旧的工具清单。
|
|
32
|
+
诊断时只记录工具名、命令来源和脱敏错误,不记录密钥或完整需求。
|
|
33
|
+
|
|
17
34
|
### 先检查环境,不先花模型调用
|
|
18
35
|
|
|
19
36
|
在 Cadence 开发仓库中可用:
|
|
@@ -50,7 +67,9 @@ export ABEL_VERIFICATION_TIMEOUT_MS=900000
|
|
|
50
67
|
可信模式仍在独立候选目录执行,并复制依赖以避免普通缓存写入修改 consumer 的依赖,但**不是沙箱**:测试可访问宿主文件、网络及显式继承的环境变量,产生的外部副作用不能回滚。
|
|
51
68
|
HOME、临时目录和报告仍独立;PATH 复用宿主,仓库内 workspace 链接映射到候选代码。
|
|
52
69
|
该模式仍要求 Linux Bubblewrap 的 PID namespace 管理后代进程(包括 detached 后代),但不隔离宿主文件和网络;缺少该能力时暂停,不回退到普通进程组。
|
|
53
|
-
|
|
70
|
+
`local-trusted` 仅用于 Linux。
|
|
71
|
+
Windows x64 使用独立的 `host-trusted` 后端,见 [配置、构建与验收说明](docs/windows-host-trusted.md);macOS 暂不支持。
|
|
72
|
+
包含原生 helper 的 Windows x64 安装包在 [GitHub Release](https://github.com/abelxiaoxing/cadence/releases/tag/v1.8.0) 下载;npm 通用包可显式构建 helper 后配置使用。
|
|
54
73
|
依赖副本准备、替换与清理在 I/O Worker 中执行,复制支持取消并等待线程退出;大型依赖复制仍有额外成本;需要安装新依赖、宿主浏览器缓存或服务启动编排时仍需配置/准备,不会自动下载。
|
|
55
74
|
|
|
56
75
|
可选宿主参数还包括 `ABEL_BWRAP_PATH`、`ABEL_FIRST_PROGRESS_MS` 和 `ABEL_STREAM_IDLE_MS`。
|
|
@@ -339,7 +358,9 @@ inspection 不可用时不再级联误报 traceability 输入缺失,且绝不
|
|
|
339
358
|
CLI 的其他版本必须满足相同 JSON 协议;新增支持版本应加入契约测试。
|
|
340
359
|
|
|
341
360
|
**此矩阵不代表原生 Implement 隔离已全平台可用。**
|
|
342
|
-
默认隔离后端仍为 Linux Bubblewrap
|
|
361
|
+
默认隔离后端仍为 Linux Bubblewrap。
|
|
362
|
+
Windows x64 的显式 `host-trusted` 使用 Job Object 管理验证进程,不提供文件或网络安全隔离;真实原生支持以 Windows CI 验收结果为准。
|
|
363
|
+
缺少所选后端时保持暂停。
|
|
343
364
|
|
|
344
365
|
目录持久化在 Windows 上单独处理:仅当已打开的目录句柄执行 `fsync` 返回 `EPERM` 时,标记为不支持目录刷新屏障。
|
|
345
366
|
普通文件仍须在发布前成功 `fsync`;目录打开/关闭、权限、其他 I/O 及 POSIX 目录刷新错误不会被忽略。
|
package/config/.env.example
CHANGED
|
@@ -19,3 +19,9 @@ GROK_API_KEY=
|
|
|
19
19
|
# TAVILY_ENABLED=true
|
|
20
20
|
# TAVILY_API_URL=https://api.tavily.com
|
|
21
21
|
# TAVILY_API_KEY=
|
|
22
|
+
|
|
23
|
+
# Explicit Windows x64 trusted host verification (not a filesystem/network sandbox).
|
|
24
|
+
# ABEL_EXECUTION_MODE=host-trusted
|
|
25
|
+
# ABEL_WINDOWS_JOB_HELPER=C:\CadenceNative\windows-job.exe
|
|
26
|
+
# ABEL_VERIFICATION_TIMEOUT_MS=600000
|
|
27
|
+
# ABEL_VERIFICATION_ENV=DATABASE_URL,NODE_ENV
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Four explicit Abel workflow prompts with stage-isolated design and diagnosis, durable resumable implementation, and three package-owned professional Agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/prompts/abel-design.md
CHANGED
|
@@ -16,6 +16,12 @@ $ARGUMENTS
|
|
|
16
16
|
|
|
17
17
|
<!-- ABEL:PROMPT:abel-design -->
|
|
18
18
|
|
|
19
|
+
Before following this procedure, check that `abel_dispatch` is actually callable in this model request.
|
|
20
|
+
If it is missing, stop and report `Cadence configuration error: abel-stage-tools-unavailable`.
|
|
21
|
+
Do not substitute bash, subagent, or terminal tools, and do not claim the stage started.
|
|
22
|
+
Ask the operator to enable the Cadence extension together with its package prompts, inspect extension-load errors and tool filters, then resubmit the original slash command while the session is idle.
|
|
23
|
+
A prompt marker or historical active/inactive message is not evidence that the tool is available.
|
|
24
|
+
|
|
19
25
|
This stage is scoped to the invoked task.
|
|
20
26
|
Direct answers and same-task continuations stay in this stage.
|
|
21
27
|
If the user ends the workflow or requests an unrelated task, first send `{"action":"finish"}` to `abel_dispatch`, then handle the new task normally with the restored tools.
|
package/prompts/abel-diagnose.md
CHANGED
|
@@ -13,6 +13,12 @@ $ARGUMENTS
|
|
|
13
13
|
|
|
14
14
|
<!-- ABEL:PROMPT:abel-diagnose -->
|
|
15
15
|
|
|
16
|
+
Before following this procedure, check that `abel_dispatch` is actually callable in this model request.
|
|
17
|
+
If it is missing, stop and report `Cadence configuration error: abel-stage-tools-unavailable`.
|
|
18
|
+
Do not substitute bash, subagent, or terminal tools, and do not claim the stage started.
|
|
19
|
+
Ask the operator to enable the Cadence extension together with its package prompts, inspect extension-load errors and tool filters, then resubmit the original slash command while the session is idle.
|
|
20
|
+
A prompt marker or historical active/inactive message is not evidence that the tool is available.
|
|
21
|
+
|
|
16
22
|
This stage is scoped to the invoked task.
|
|
17
23
|
Direct answers and same-task continuations stay in this stage.
|
|
18
24
|
If the user ends the workflow or requests an unrelated task, first send `{"action":"finish"}` to `abel_dispatch`, then handle the new task normally with the restored tools.
|
|
@@ -16,6 +16,12 @@ $ARGUMENTS
|
|
|
16
16
|
|
|
17
17
|
<!-- ABEL:PROMPT:abel-implement -->
|
|
18
18
|
|
|
19
|
+
Before following this procedure, check that `abel_dispatch` is actually callable in this model request.
|
|
20
|
+
If it is missing, stop and report `Cadence configuration error: abel-stage-tools-unavailable`.
|
|
21
|
+
Do not substitute bash, subagent, or terminal tools, and do not claim the stage started.
|
|
22
|
+
Ask the operator to enable the Cadence extension together with its package prompts, inspect extension-load errors and tool filters, then resubmit the original slash command while the session is idle.
|
|
23
|
+
A prompt marker or historical active/inactive message is not evidence that the tool is available.
|
|
24
|
+
|
|
19
25
|
This stage is scoped to the invoked task.
|
|
20
26
|
Direct answers and same-task continuations stay in this stage.
|
|
21
27
|
If the user ends the workflow or requests an unrelated task, first send `{"action":"finish"}` to `abel_dispatch`, then handle the new task normally with the restored tools.
|
package/src/apply-transaction.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
realpathSync,
|
|
14
14
|
renameSync,
|
|
15
15
|
rmdirSync,
|
|
16
|
-
rmSync,
|
|
17
16
|
unlinkSync,
|
|
18
17
|
writeFileSync,
|
|
19
18
|
} from "node:fs";
|
|
@@ -24,6 +23,8 @@ import type { ArtifactStore } from "./artifact-store.ts";
|
|
|
24
23
|
import { compareCanonicalStrings } from "./canonical.ts";
|
|
25
24
|
import { isValidRelativePath } from "./contracts.ts";
|
|
26
25
|
import { syncDirectory } from "./directory-sync.ts";
|
|
26
|
+
import { isExecutionRetained } from "./execution-retention.ts";
|
|
27
|
+
import { removePathSync } from "./remove-path.ts";
|
|
27
28
|
import { observeSafePath } from "./safe-path.ts";
|
|
28
29
|
import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
|
|
29
30
|
import { APPLY_SCHEMA } from "./storage-schema.ts";
|
|
@@ -161,6 +162,8 @@ export interface ApplyTransactionHooks {
|
|
|
161
162
|
path: string;
|
|
162
163
|
index: number;
|
|
163
164
|
}) => void | Promise<void>;
|
|
165
|
+
/** Production executors settle their process scope before cancellation returns. */
|
|
166
|
+
awaitPostApplySettlement?: boolean;
|
|
164
167
|
postApply?: (input: {
|
|
165
168
|
transactionId: string;
|
|
166
169
|
root: string;
|
|
@@ -412,10 +415,10 @@ function writeExclusiveFile(
|
|
|
412
415
|
syncDirectory(directory);
|
|
413
416
|
return true;
|
|
414
417
|
} catch (error) {
|
|
415
|
-
|
|
418
|
+
removePathSync(temporary, { force: true });
|
|
416
419
|
throw error;
|
|
417
420
|
} finally {
|
|
418
|
-
|
|
421
|
+
removePathSync(temporary, { force: true });
|
|
419
422
|
}
|
|
420
423
|
}
|
|
421
424
|
|
|
@@ -587,7 +590,8 @@ export async function verifyCumulativeRevision(
|
|
|
587
590
|
verificationId: input.verificationId,
|
|
588
591
|
};
|
|
589
592
|
} finally {
|
|
590
|
-
|
|
593
|
+
if (!isExecutionRetained(temporary))
|
|
594
|
+
removePathSync(temporary, { recursive: true, force: true });
|
|
591
595
|
}
|
|
592
596
|
}
|
|
593
597
|
|
|
@@ -1247,11 +1251,13 @@ export class ApplyTransaction {
|
|
|
1247
1251
|
}),
|
|
1248
1252
|
);
|
|
1249
1253
|
void execution.catch(() => undefined);
|
|
1250
|
-
postApply =
|
|
1251
|
-
|
|
1252
|
-
|
|
1254
|
+
postApply =
|
|
1255
|
+
signal && !this.#hooks.awaitPostApplySettlement
|
|
1256
|
+
? await raceWithAbort(execution, signal)
|
|
1257
|
+
: await execution;
|
|
1253
1258
|
} finally {
|
|
1254
|
-
|
|
1259
|
+
if (!isExecutionRetained(isolatedRoot))
|
|
1260
|
+
removePathSync(isolatedRoot, { recursive: true, force: true });
|
|
1255
1261
|
}
|
|
1256
1262
|
}
|
|
1257
1263
|
} catch (error) {
|
|
@@ -1260,6 +1266,13 @@ export class ApplyTransaction {
|
|
|
1260
1266
|
if (settled) return settled;
|
|
1261
1267
|
if (!journal.pendingIntent && !signal?.aborted) throw error;
|
|
1262
1268
|
}
|
|
1269
|
+
if (isExecutionRetained(this.root)) {
|
|
1270
|
+
journal = this.#load(transactionId);
|
|
1271
|
+
journal.state = "recovering";
|
|
1272
|
+
journal.code = "isolation-termination-unconfirmed";
|
|
1273
|
+
this.#save(journal);
|
|
1274
|
+
return { ok: false, transactionId, state: "paused", code: journal.code };
|
|
1275
|
+
}
|
|
1263
1276
|
journal = this.#load(transactionId);
|
|
1264
1277
|
const settled = this.#settledOutcome(journal);
|
|
1265
1278
|
if (settled) return settled;
|
|
@@ -1350,6 +1363,13 @@ export class ApplyTransaction {
|
|
|
1350
1363
|
): Promise<ApplyTransactionOutcome> {
|
|
1351
1364
|
const existing = this.#active.get(transactionId);
|
|
1352
1365
|
if (existing) return existing.promise;
|
|
1366
|
+
if (isExecutionRetained(this.root))
|
|
1367
|
+
return Promise.resolve({
|
|
1368
|
+
ok: false,
|
|
1369
|
+
transactionId,
|
|
1370
|
+
state: "paused",
|
|
1371
|
+
code: "isolation-termination-unconfirmed",
|
|
1372
|
+
});
|
|
1353
1373
|
const controller = new AbortController();
|
|
1354
1374
|
const forwardAbort = () => {
|
|
1355
1375
|
if (!controller.signal.aborted) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Explicit operator/release build only. Never invoked on import, install or verification.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
function main() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
if (
|
|
11
|
+
process.platform !== "win32" ||
|
|
12
|
+
process.arch !== "x64" ||
|
|
13
|
+
args.length !== 2 ||
|
|
14
|
+
args[0] !== "--output"
|
|
15
|
+
)
|
|
16
|
+
throw new Error(
|
|
17
|
+
"Windows x64 required; usage: node build-windows-job.mjs --output <directory>",
|
|
18
|
+
);
|
|
19
|
+
const output = path.resolve(args[1]);
|
|
20
|
+
const compiler = path.join(
|
|
21
|
+
process.env.VCToolsInstallDir ?? "",
|
|
22
|
+
"bin/Hostx64/x64/cl.exe",
|
|
23
|
+
);
|
|
24
|
+
if (!path.isAbsolute(compiler))
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Select the installed MSVC x64 developer environment first",
|
|
27
|
+
);
|
|
28
|
+
const sdkVersion = process.env.WindowsSDKVersion?.replace(/[\\/]+$/u, "");
|
|
29
|
+
if (!sdkVersion || !/^10\.0\.\d+\.0$/u.test(sdkVersion))
|
|
30
|
+
throw new Error("Windows 10 SDK required");
|
|
31
|
+
const version = spawnSync(compiler, [], {
|
|
32
|
+
shell: false,
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
timeout: 10000,
|
|
35
|
+
maxBuffer: 16384,
|
|
36
|
+
});
|
|
37
|
+
const compilerVersion =
|
|
38
|
+
`${version.stdout ?? ""}${version.stderr ?? ""}`.match(
|
|
39
|
+
/19\.(?:3\d|4\d)\.\d+/u,
|
|
40
|
+
)?.[0];
|
|
41
|
+
if (!compilerVersion) throw new Error("MSVC 19.3x/19.4x required");
|
|
42
|
+
mkdirSync(output, { recursive: true });
|
|
43
|
+
const source = fileURLToPath(new URL("./windows-job.c", import.meta.url));
|
|
44
|
+
const helper = path.join(output, "windows-job.exe");
|
|
45
|
+
rmSync(`${helper}.json`, { force: true });
|
|
46
|
+
rmSync(helper, { force: true });
|
|
47
|
+
const result = spawnSync(
|
|
48
|
+
compiler,
|
|
49
|
+
[
|
|
50
|
+
"/nologo",
|
|
51
|
+
"/W4",
|
|
52
|
+
"/WX",
|
|
53
|
+
"/O2",
|
|
54
|
+
"/MT",
|
|
55
|
+
"/utf-8",
|
|
56
|
+
"/DUNICODE",
|
|
57
|
+
"/D_UNICODE",
|
|
58
|
+
source,
|
|
59
|
+
`/Fe:${helper}`,
|
|
60
|
+
`/Fo:${path.join(output, "windows-job.obj")}`,
|
|
61
|
+
],
|
|
62
|
+
{
|
|
63
|
+
shell: false,
|
|
64
|
+
cwd: output,
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
timeout: 60000,
|
|
67
|
+
maxBuffer: 65536,
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
writeFileSync(
|
|
71
|
+
path.join(output, "build.log"),
|
|
72
|
+
`${result.stdout ?? ""}${result.stderr ?? ""}`,
|
|
73
|
+
);
|
|
74
|
+
if (result.error || result.status !== 0)
|
|
75
|
+
throw new Error("Native build failed; inspect build.log");
|
|
76
|
+
const hash = (file) =>
|
|
77
|
+
createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
78
|
+
writeFileSync(
|
|
79
|
+
`${helper}.json`,
|
|
80
|
+
`${JSON.stringify({ version: 2, arch: "x64", sourceSha256: hash(source), helperSha256: hash(helper), compilerSha256: hash(compiler), compilerVersion, sdkVersion }, null, 2)}\n`,
|
|
81
|
+
{ flag: "wx" },
|
|
82
|
+
);
|
|
83
|
+
console.log(`Built ${helper}`);
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
process.argv[1] &&
|
|
87
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
88
|
+
) {
|
|
89
|
+
try {
|
|
90
|
+
main();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(error.message);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -21,6 +21,12 @@ export function runGitApply(
|
|
|
21
21
|
const child = spawn(
|
|
22
22
|
"git",
|
|
23
23
|
[
|
|
24
|
+
// Candidate patches bind exact bytes, independent of host checkout
|
|
25
|
+
// preferences. Git's Windows autocrlf default must not rewrite them.
|
|
26
|
+
"-c",
|
|
27
|
+
"core.autocrlf=false",
|
|
28
|
+
"-c",
|
|
29
|
+
"core.eol=lf",
|
|
24
30
|
"apply",
|
|
25
31
|
...(checkOnly ? ["--check"] : []),
|
|
26
32
|
"--recount",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdtempSync
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { verifyCumulativeRevision } from "./apply-transaction.ts";
|
|
5
5
|
import type { ArtifactStore } from "./artifact-store.ts";
|
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
DurableVerificationObservation,
|
|
22
22
|
DurableWorkflowEngineOptions,
|
|
23
23
|
} from "./durable-contracts.ts";
|
|
24
|
+
import { isExecutionRetained } from "./execution-retention.ts";
|
|
25
|
+
import { removePathSync } from "./remove-path.ts";
|
|
24
26
|
|
|
25
27
|
import { observeSafePath } from "./safe-path.ts";
|
|
26
28
|
|
|
@@ -72,6 +74,7 @@ const BASELINE_CAPABILITY_CODES = new Set([
|
|
|
72
74
|
"dependency-path-unsafe",
|
|
73
75
|
"isolation-backend-launch-failed",
|
|
74
76
|
"isolation-backend-unavailable",
|
|
77
|
+
"isolation-termination-unconfirmed",
|
|
75
78
|
"local-executable-missing",
|
|
76
79
|
"runner-missing",
|
|
77
80
|
"sandbox-runtime-unavailable",
|
|
@@ -527,7 +530,8 @@ export class ChangeVerification {
|
|
|
527
530
|
},
|
|
528
531
|
};
|
|
529
532
|
} finally {
|
|
530
|
-
|
|
533
|
+
if (!isExecutionRetained(root))
|
|
534
|
+
removePathSync(root, { recursive: true, force: true });
|
|
531
535
|
}
|
|
532
536
|
}
|
|
533
537
|
|
package/src/durable-contracts.ts
CHANGED
|
@@ -173,6 +173,8 @@ export interface DurableWorkflowEngineOptions {
|
|
|
173
173
|
leaseTtlMs?: number;
|
|
174
174
|
workHardLimit?: number;
|
|
175
175
|
verificationPolicy?: string;
|
|
176
|
+
/** Real process executors must settle cancellation before post-apply cleanup. */
|
|
177
|
+
awaitPostApplySettlement?: boolean;
|
|
176
178
|
verificationEnvironment?(
|
|
177
179
|
plan: ImplementPlan,
|
|
178
180
|
signal: AbortSignal,
|
package/src/durable-workflow.ts
CHANGED
|
@@ -5,7 +5,9 @@ import type {
|
|
|
5
5
|
DurableResourceInput,
|
|
6
6
|
DurableWorkflowEngineOptions,
|
|
7
7
|
} from "./durable-contracts.ts";
|
|
8
|
+
import { assertExecutionsSettled } from "./execution-retention.ts";
|
|
8
9
|
import { PhaseExecution } from "./phase-execution.ts";
|
|
10
|
+
import { removePathSync } from "./remove-path.ts";
|
|
9
11
|
|
|
10
12
|
export type {
|
|
11
13
|
DurableChangeVerificationResult,
|
|
@@ -18,7 +20,7 @@ interface DurableRunResources extends DurableExecutionResources {
|
|
|
18
20
|
closed: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
import { lstatSync, mkdirSync, readFileSync
|
|
23
|
+
import { lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
22
24
|
import path from "node:path";
|
|
23
25
|
import { DatabaseSync } from "node:sqlite";
|
|
24
26
|
import { ApplyTransaction } from "./apply-transaction.ts";
|
|
@@ -276,6 +278,7 @@ class DurableWorkflowComposition
|
|
|
276
278
|
signal: AbortSignal,
|
|
277
279
|
): Promise<DurableRunResources> {
|
|
278
280
|
signal.throwIfAborted();
|
|
281
|
+
assertExecutionsSettled(this.#options.stateRoot.rootDir);
|
|
279
282
|
const pending = this.#initializing.get(input.runId);
|
|
280
283
|
if (pending) return pending;
|
|
281
284
|
if (
|
|
@@ -325,6 +328,7 @@ class DurableWorkflowComposition
|
|
|
325
328
|
baselineRevisionId?: string;
|
|
326
329
|
currentWorkspaceRevisionId?: string;
|
|
327
330
|
}): DurableRunResources {
|
|
331
|
+
assertExecutionsSettled(this.#options.stateRoot.rootDir);
|
|
328
332
|
const existing = this.#runs.get(input.runId);
|
|
329
333
|
if (existing) {
|
|
330
334
|
if (
|
|
@@ -370,6 +374,7 @@ class DurableWorkflowComposition
|
|
|
370
374
|
artifacts,
|
|
371
375
|
workspaces,
|
|
372
376
|
hooks: {
|
|
377
|
+
awaitPostApplySettlement: this.#options.awaitPostApplySettlement,
|
|
373
378
|
postApply: ({ transactionId, root: verificationRoot, signal }) =>
|
|
374
379
|
this.#verification.verifyPostApply(
|
|
375
380
|
resources,
|
|
@@ -893,12 +898,13 @@ class DurableWorkflowComposition
|
|
|
893
898
|
}
|
|
894
899
|
|
|
895
900
|
cleanup(runId: string): void {
|
|
901
|
+
assertExecutionsSettled(this.#options.stateRoot.rootDir, true);
|
|
896
902
|
const resources = this.#runs.get(runId);
|
|
897
903
|
if (resources) {
|
|
898
904
|
this.#closeResources(resources);
|
|
899
905
|
this.#runs.delete(runId);
|
|
900
906
|
}
|
|
901
|
-
|
|
907
|
+
removePathSync(this.#runRoot(runId), { recursive: true, force: true });
|
|
902
908
|
}
|
|
903
909
|
|
|
904
910
|
close(): void {
|
package/src/execution-profile.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
1
4
|
export interface ExecutionProfile {
|
|
2
|
-
mode: "isolated" | "local-trusted";
|
|
5
|
+
mode: "isolated" | "local-trusted" | "host-trusted";
|
|
3
6
|
timeoutMs: number;
|
|
4
7
|
bwrapPath: string;
|
|
5
8
|
inheritEnvironment: string[];
|
|
9
|
+
windowsHelperPath?: string;
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
/** Operator configuration only. Unknown modes never silently disable isolation. */
|
|
@@ -10,7 +14,11 @@ export function executionProfile(
|
|
|
10
14
|
env: NodeJS.ProcessEnv = process.env,
|
|
11
15
|
): ExecutionProfile {
|
|
12
16
|
const mode = env.ABEL_EXECUTION_MODE ?? "isolated";
|
|
13
|
-
if (
|
|
17
|
+
if (
|
|
18
|
+
mode !== "isolated" &&
|
|
19
|
+
mode !== "local-trusted" &&
|
|
20
|
+
mode !== "host-trusted"
|
|
21
|
+
)
|
|
14
22
|
throw new Error("execution-mode-invalid");
|
|
15
23
|
const timeoutMs = Number(env.ABEL_VERIFICATION_TIMEOUT_MS ?? 600_000);
|
|
16
24
|
if (
|
|
@@ -29,10 +37,28 @@ export function executionProfile(
|
|
|
29
37
|
throw new Error("verification-environment-names-invalid");
|
|
30
38
|
if (mode === "isolated" && inheritEnvironment.length)
|
|
31
39
|
throw new Error("verification-environment-requires-trusted-mode");
|
|
40
|
+
if (
|
|
41
|
+
mode === "host-trusted" &&
|
|
42
|
+
env.ABEL_WINDOWS_JOB_HELPER !== undefined &&
|
|
43
|
+
(!env.ABEL_WINDOWS_JOB_HELPER ||
|
|
44
|
+
env.ABEL_WINDOWS_JOB_HELPER.includes("\0") ||
|
|
45
|
+
!(
|
|
46
|
+
path.isAbsolute(env.ABEL_WINDOWS_JOB_HELPER) ||
|
|
47
|
+
path.win32.isAbsolute(env.ABEL_WINDOWS_JOB_HELPER)
|
|
48
|
+
))
|
|
49
|
+
)
|
|
50
|
+
throw new Error("windows-job-helper-path-invalid");
|
|
32
51
|
return {
|
|
33
52
|
mode,
|
|
34
53
|
timeoutMs,
|
|
35
54
|
bwrapPath: env.ABEL_BWRAP_PATH ?? "/usr/bin/bwrap",
|
|
36
55
|
inheritEnvironment,
|
|
56
|
+
...(mode === "host-trusted"
|
|
57
|
+
? {
|
|
58
|
+
windowsHelperPath:
|
|
59
|
+
env.ABEL_WINDOWS_JOB_HELPER ??
|
|
60
|
+
fileURLToPath(new URL("./windows-job.exe", import.meta.url)),
|
|
61
|
+
}
|
|
62
|
+
: {}),
|
|
37
63
|
};
|
|
38
64
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
fsyncSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { syncDirectory } from "./directory-sync.ts";
|
|
13
|
+
|
|
14
|
+
const prefix = ".cadence-execution-";
|
|
15
|
+
const active = new Set<string>();
|
|
16
|
+
const retained = new Map<string, readonly string[]>();
|
|
17
|
+
/** Written before launching a host process. Absence of a settlement receipt
|
|
18
|
+
* survives host loss; reopening must not infer ownership from a recycled PID. */
|
|
19
|
+
export function retainExecution(ownerRoot: string, roots: readonly string[]) {
|
|
20
|
+
assertExecutionsSettled(ownerRoot);
|
|
21
|
+
const file = path.join(ownerRoot, `${prefix}${randomUUID()}.json`);
|
|
22
|
+
const fd = openSync(file, "wx", 0o600);
|
|
23
|
+
try {
|
|
24
|
+
writeFileSync(
|
|
25
|
+
fd,
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
version: 1,
|
|
28
|
+
roots: roots.map((root) => path.resolve(root)),
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
fsyncSync(fd);
|
|
32
|
+
} finally {
|
|
33
|
+
closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
syncDirectory(ownerRoot);
|
|
36
|
+
active.add(file);
|
|
37
|
+
retained.set(file, roots);
|
|
38
|
+
return {
|
|
39
|
+
settled() {
|
|
40
|
+
unlinkSync(file);
|
|
41
|
+
syncDirectory(ownerRoot);
|
|
42
|
+
active.delete(file);
|
|
43
|
+
retained.delete(file);
|
|
44
|
+
},
|
|
45
|
+
uncertain() {
|
|
46
|
+
active.delete(file);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function assertExecutionsSettled(
|
|
51
|
+
ownerRoot: string,
|
|
52
|
+
includeActive = false,
|
|
53
|
+
): void {
|
|
54
|
+
if (!lstatSync(ownerRoot, { throwIfNoEntry: false })) return;
|
|
55
|
+
if (
|
|
56
|
+
readdirSync(ownerRoot).some(
|
|
57
|
+
(name) =>
|
|
58
|
+
name.startsWith(prefix) &&
|
|
59
|
+
(includeActive || !active.has(path.join(ownerRoot, name))),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
throw new Error("isolation-termination-unconfirmed");
|
|
63
|
+
}
|
|
64
|
+
export function isExecutionRetained(root: string): boolean {
|
|
65
|
+
const resolved = path.resolve(root);
|
|
66
|
+
return [...retained.values()].some((roots) =>
|
|
67
|
+
roots.some((candidate) => {
|
|
68
|
+
const relative = path.relative(resolved, path.resolve(candidate));
|
|
69
|
+
return (
|
|
70
|
+
relative === "" ||
|
|
71
|
+
(relative !== ".." &&
|
|
72
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
73
|
+
!path.isAbsolute(relative))
|
|
74
|
+
);
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|