dev-flow-deepseek 0.8.8 → 0.8.9
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 +64 -20
- package/lib/authorization.mjs +15 -1
- package/lib/index.mjs +10 -5
- package/lib/provisioning-receipt.mjs +163 -0
- package/lib/tool-names.mjs +2 -0
- package/lib/workspace-coordinator.mjs +625 -0
- package/lib/workspace-tool.mjs +118 -0
- package/package.json +4 -1
- package/runtime/darwin-arm64/dev-flow +0 -0
- package/runtime/win32-x64/dev-flow.exe +0 -0
- package/skills/dev-flow/SKILL.md +272 -104
- package/skills/dev-flow/references/method-profiles.md +5 -4
- package/skills/dev-flow/references/node-payloads.md +31 -18
package/README.md
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
[中文](https://github.com/Innocent-children/dev-flow/blob/main/packages/deepseek/README.md) |
|
|
8
8
|
[English](https://github.com/Innocent-children/dev-flow/blob/main/docs/DEEPSEEK_en.md)
|
|
9
9
|
|
|
10
|
-
`dev-flow-deepseek` 让 DeepSeek Harness(DSH
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Recovery 判断或明确阻塞。
|
|
10
|
+
`dev-flow-deepseek` 让 DeepSeek Harness(DSH)在独立工作树中运行一个持久 Core Task。普通开发请求
|
|
11
|
+
先只读评估,不调用 Dev Flow;后续精确确认才授权创建工作树和重启会话。Core 随后通过只读 Git
|
|
12
|
+
观察计算当前 Task surface,DSH 继续负责用户授权的 fetch、branch、worktree、文件修改和命令执行。
|
|
14
13
|
|
|
15
14
|
## 支持范围
|
|
16
15
|
|
|
@@ -68,23 +67,34 @@ dsh --profile $ProfileName --dump-config
|
|
|
68
67
|
|
|
69
68
|
## 启动一个 Task
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
先把开发请求正常发给 DSH。Adapter 只读检查候选实现、调用关系、测试、配置和 Git 状态,返回
|
|
71
|
+
`small|standard|large|uncertain`、已经找到的影响面、未知项和建议;这一轮不调用 Core、不修改 Git、
|
|
72
|
+
不运行测试,也不创建 Task。即使第一条消息已经包含 `/dev-flow`,新请求也不能跳过评估和确认。
|
|
73
|
+
|
|
74
|
+
选择 Dev Flow 后,逐仓确认 remote、base branch 和新的 target branch。确认消息必须使用 Adapter
|
|
75
|
+
显示的精确形式,例如:
|
|
72
76
|
|
|
73
77
|
```text
|
|
74
|
-
/dev-flow
|
|
78
|
+
/dev-flow confirm-worktree
|
|
79
|
+
repository=primary;remote=origin;base=main;target=feature/payment-callback-signature
|
|
75
80
|
```
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
WorkspaceCoordinator 随后精确 fetch 选定 remote/base,冻结 commit,并从该 commit 创建干净、独立、
|
|
83
|
+
具名分支的 worktree。源 checkout 可以 dirty,但 staged、tracked dirty 和 untracked 内容都不会复制。
|
|
84
|
+
fetch、分支校验或 worktree 创建失败时不会创建 Core Task。
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
DSH 的 Workspace Root 在进程启动时固定,因此 Adapter 不会扩大当前 Root。它返回由 command、argv 和
|
|
87
|
+
cwd 分开的 relaunch 信息;新会话从隔离 workspace 启动,使用返回的
|
|
88
|
+
`/dev-flow resume-worktree launch=<launch_id>` 消费 receipt,复核 branch、HEAD 和 clean 状态,然后
|
|
89
|
+
才创建 Task。新 Task 保存最初请求、范围、验收条件和 method profile,不在分析前冻结最终
|
|
90
|
+
verification budget;profile 可以是 `plain`、`spec-kit` 或 `openspec`。
|
|
82
91
|
|
|
83
92
|
## 恢复已有 Task
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
回到 Task 原来绑定的同一 worktree 实例,并在当前直接用户消息中再次使用 `/dev-flow`。Adapter 会先
|
|
95
|
+
读取 Core,恢复当前阶段、revision、范围、剩余验证、Blocker 和 Recovery,不会根据聊天记录重新创建
|
|
96
|
+
进度。原 worktree 丢失或被替换时进入 `WORKSPACE_UNAVAILABLE`;同路径重建目录或同名 branch 不能冒充
|
|
97
|
+
原实例。此时只能恢复原实例,或明确 abandon Task。
|
|
88
98
|
|
|
89
99
|
如果上一次 Action 响应丢失或被截断,Adapter 先读取当前 Task 和 Recovery 判断,再按 Core 给出的
|
|
90
100
|
结果继续、恢复、阻塞或安全重试。它不会自行重复原提交。
|
|
@@ -93,6 +103,24 @@ Adapter 会先读取 Core,恢复当前阶段、revision、范围、剩余验
|
|
|
93
103
|
结果并暂停 Task。Adapter 不会自动解除;用户明确选择换方案或再试一次后,才解除 blocker,并从
|
|
94
104
|
Core 保存的原目标阶段继续。下一次仍然完全重复时会再次暂停。
|
|
95
105
|
|
|
106
|
+
## 验证投入和修改后复核
|
|
107
|
+
|
|
108
|
+
Adapter 在 TASKS 完成需求、设计、工作拆分、影响面和现有测试结构分析后,才保存初始
|
|
109
|
+
`verification_plan`,包括计划检查及理由、预计自动命令数、完整套件预期和测试代码预期。小改动先用
|
|
110
|
+
离当前 diff 最近的定向检查;剩余额度不能作为扩大到 package、module 或全仓库的理由。
|
|
111
|
+
|
|
112
|
+
额度不足时不直接结束 Task,也不先运行额外命令。Adapter 使用当前 TEST Action 的
|
|
113
|
+
`verification_budget_increased`,用 `new_impact`、`new_risk`、`verification_failure` 或
|
|
114
|
+
`verification_gap` 说明具体事实,只增加当前需要的检查、命令或权限。Core 保存原因和调整前后预算并
|
|
115
|
+
留在 TEST。为了更全面、提高信心、保险起见或“还有预算”都不能支持增加。
|
|
116
|
+
|
|
117
|
+
每次完整套件前都重新判断广泛影响、定向/包级检查是否足够、待补的具体风险和仓库当前检查点要求,
|
|
118
|
+
本次理由写入 `full_suite_reason`,小修复后不自动沿用旧理由。测试代码只为稳定行为、公共合同、重要
|
|
119
|
+
失败路径或真实回归保留;一次性 README 词语要求只做一次文本搜索。
|
|
120
|
+
|
|
121
|
+
普通实现后的复核只看当前 diff、直接或间接影响和验收所需路径。修复复核发现后只检查原问题、相关
|
|
122
|
+
回归和对应定向检查,不重启全仓库审计。显式 code review 阶段只读,交付完整发现后等待单独修复授权。
|
|
123
|
+
|
|
96
124
|
## 范围外文件先询问
|
|
97
125
|
|
|
98
126
|
Adapter 在 DSH `tools/pre-execute` 中检查 `write`、`edit` 和变更型 `str_replace_editor`。当前 direct
|
|
@@ -102,7 +130,7 @@ Repository Scope、位于 Workspace Root 且文件属于计划范围时,不因
|
|
|
102
130
|
|
|
103
131
|
计划外文件会在工具执行前暂停 Task。用户选择:`allow_once` 只允许相同写入意图,`expand_scope`
|
|
104
132
|
返回 `TASKS` 更新计划,`reject` 在当前 Task Plan revision 内继续拒绝该路径。选择与原因由 Core
|
|
105
|
-
|
|
133
|
+
保存;Core 根据 frozen base、commit、index、worktree 和 untracked 内容重新计算当前 Task surface。
|
|
106
134
|
|
|
107
135
|
该 gate 不解析 Bash、外部进程或其他工具路径;这些写入可能只能在 Core 最终检查时发现。gate
|
|
108
136
|
不可用时,支持的结构化写入保守停止。
|
|
@@ -142,16 +170,32 @@ dsh --profile "$PROFILE" --dump-config
|
|
|
142
170
|
## DeepSeek 权限与边界
|
|
143
171
|
|
|
144
172
|
- DSH 启动时的 canonical Workspace Root 是权限边界;仓库和 symlink 解析结果必须位于其中;
|
|
145
|
-
- Dev Flow 不扩大 Workspace Root
|
|
146
|
-
|
|
147
|
-
-
|
|
173
|
+
- Dev Flow 不扩大 Workspace Root,也不会通过索引发现并加入相邻仓库;需要 sibling worktree 时重启到
|
|
174
|
+
Coordinator 返回的新 Root;
|
|
175
|
+
- Core 只读观察 Git,不执行 fetch、branch、worktree、commit、push、merge、rebase、tag 或 publish;
|
|
176
|
+
- DeepSeek 负责用户确认后的 fetch、branch、worktree、文件修改和命令执行;Host gate 检查列出的
|
|
177
|
+
结构化工具,Core 计算当前 Task surface,但不会拦截每一次操作;
|
|
148
178
|
- `/dev-flow` 不绕过当前 Action、Workspace 权限、Git 写入授权或发布确认。
|
|
149
179
|
|
|
180
|
+
`DONE` 和 `CANCELLED` 只结束 Core Task并释放 claim,不会 commit、push、创建 PR、handoff 或删除
|
|
181
|
+
worktree/branch。终态会显示 remote/base/frozen commit、task branch/HEAD、路径、clean 状态、当前改动和
|
|
182
|
+
验证结果。worktree 删除与 branch 删除需要两次独立授权;active、dirty、未推送或状态不确定的资源不
|
|
183
|
+
自动清理。
|
|
184
|
+
|
|
185
|
+
DeepSeek 的清理仍由 WorkspaceCoordinator 执行。它先在不删除资源的 `prepare_cleanup` 操作中核对
|
|
186
|
+
receipt、终态 Task 和同 Git group 的源 checkout,并返回从该源 checkout 重启 DSH 的 descriptor,避免
|
|
187
|
+
原地删除当前 Workspace Root。重启后,第一次独立确认只删除 receipt 拥有、Core 已终态、clean 且
|
|
188
|
+
terminal HEAD 已精确推送的 worktree,并保留 task branch。第二次独立确认使用非 force 的
|
|
189
|
+
`git branch -d` 删除 branch;未合并、仍被 worktree 使用、HEAD/remote 不一致或 Core 读取失败都会
|
|
190
|
+
保留资源。源 checkout 路径只在调用中使用,不写入 receipt。
|
|
191
|
+
|
|
150
192
|
## 高级多仓库
|
|
151
193
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
194
|
+
当前源码支持一个主仓库和最多七个显式附加仓库。每个仓库都要分别确认 remote、base branch 和唯一
|
|
195
|
+
target branch;只有全部仓库完成 fetch、独立 worktree 创建和验证后,才一次创建一个 Core Task。
|
|
196
|
+
新 DSH 会话使用 Coordinator 返回的非 Git 共同 Workspace Root,所有 worktree 和 symlink 解析结果
|
|
197
|
+
都必须位于该 Root 内。任一仓库失败时不能只使用部分 Scope、退回共享 checkout 或留下部分 Core
|
|
198
|
+
claim。Scope 创建后不可变,系统不会扫描父目录、相邻目录、依赖或索引结果扩大范围。
|
|
155
199
|
|
|
156
200
|
使用前请阅读[项目状态](../../docs/PROJECT-STATUS.md)确认多仓库属于稳定还是源码范围。精确
|
|
157
201
|
Repository Scope、路径格式和协议规则见[架构](../../docs/ARCHITECTURE.md)与
|
package/lib/authorization.mjs
CHANGED
|
@@ -48,13 +48,27 @@ export function deriveCurrentTurn(execution) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export function currentDirectUserText(execution) {
|
|
52
|
+
const turn = deriveCurrentTurn(execution);
|
|
53
|
+
if (turn === undefined) return "";
|
|
54
|
+
const ids = new Set(turn.directUserMessageIds);
|
|
55
|
+
return execution.agent.session.events
|
|
56
|
+
.filter((event) => event?.type === "user/message"
|
|
57
|
+
&& ids.has(event.data?.id)
|
|
58
|
+
&& event.data?.source?.kind === "user")
|
|
59
|
+
.flatMap((event) => event.data.content ?? [])
|
|
60
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
61
|
+
.map((block) => block.text)
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
export function authorizeDevFlowExecution(execution, {
|
|
52
66
|
workspaceRoot = process.cwd(),
|
|
53
67
|
realpathImpl = realpathSync,
|
|
54
68
|
} = {}) {
|
|
55
69
|
if (!isDevFlowNamespaceTool(execution?.name)) return undefined;
|
|
56
70
|
if (!isExpectedDevFlowTool(execution.name)) {
|
|
57
|
-
return `${DENIAL_CODES.UNEXPECTED_TOOL}: the Dev Flow namespace permits only the
|
|
71
|
+
return `${DENIAL_CODES.UNEXPECTED_TOOL}: the Dev Flow namespace permits only the contracted Core tools.`;
|
|
58
72
|
}
|
|
59
73
|
if (execution.agent === undefined) {
|
|
60
74
|
return `${DENIAL_CODES.NO_AGENT}: ${selectorInstruction}.`;
|
package/lib/index.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveDataDirectory,
|
|
11
11
|
} from "./paths.mjs";
|
|
12
12
|
import { preflightPackagedCore, selectPackagedRuntime } from "./runtime.mjs";
|
|
13
|
+
import { registerWorkspaceCoordinator } from "./workspace-tool.mjs";
|
|
13
14
|
import {
|
|
14
15
|
DEV_FLOW_SERVER_NAME,
|
|
15
16
|
assertQualifiedToolCatalog,
|
|
@@ -18,8 +19,8 @@ import {
|
|
|
18
19
|
export const name = "dev-flow-deepseek";
|
|
19
20
|
export const inject = ["skills", "tools"];
|
|
20
21
|
|
|
21
|
-
export async function apply(ctx) {
|
|
22
|
-
await activateDeepSeekIntegration(ctx);
|
|
22
|
+
export async function apply(ctx, options = {}) {
|
|
23
|
+
await activateDeepSeekIntegration(ctx, options);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export async function activateDeepSeekIntegration(ctx, {
|
|
@@ -46,10 +47,10 @@ export async function activateDeepSeekIntegration(ctx, {
|
|
|
46
47
|
|
|
47
48
|
ctx.skills.register(Object.freeze({
|
|
48
49
|
name: "dev-flow",
|
|
49
|
-
description: "
|
|
50
|
-
whenToUse: "Use
|
|
50
|
+
description: "Assess bounded development requests, then use Dev Flow only after the developer confirms an isolated worktree launch.",
|
|
51
|
+
whenToUse: "Use for new development-request suitability assessment, explicit worktree confirmation, or an explicit Dev Flow Task resume.",
|
|
51
52
|
invocation: Object.freeze({
|
|
52
|
-
modelInvocable:
|
|
53
|
+
modelInvocable: true,
|
|
53
54
|
userInvocable: true,
|
|
54
55
|
}),
|
|
55
56
|
source: "bundled",
|
|
@@ -64,6 +65,10 @@ export async function activateDeepSeekIntegration(ctx, {
|
|
|
64
65
|
dataDirectory: dataSelection.dataDirectory,
|
|
65
66
|
workspaceRoot,
|
|
66
67
|
});
|
|
68
|
+
registerWorkspaceCoordinator(ctx, {
|
|
69
|
+
dataDirectory: dataSelection.dataDirectory,
|
|
70
|
+
workspaceRoot,
|
|
71
|
+
});
|
|
67
72
|
|
|
68
73
|
let mcpFiber;
|
|
69
74
|
let catalogCheckQueued = false;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
const MAX_RECEIPT_BYTES = 256 * 1024;
|
|
6
|
+
const receiptStatuses = new Set(["confirmed", "fetching", "fetched", "provisioning", "provisioned", "consumed", "cleaned", "failed", "uncertain"]);
|
|
7
|
+
const repositoryStatuses = new Set(["confirmed", "fetching", "fetched", "provisioning", "provisioned", "consumed", "worktree_removed", "branch_removed", "failed", "uncertain"]);
|
|
8
|
+
|
|
9
|
+
// One launch envelope groups the per-repository provisioning facts needed for
|
|
10
|
+
// all-or-nothing Core admission. It is an operation receipt, not a Task cursor.
|
|
11
|
+
|
|
12
|
+
export function provisioningReceiptPath(dataDirectory, launchID) {
|
|
13
|
+
assertLaunchID(launchID);
|
|
14
|
+
const root = provisioningReceiptDirectory(dataDirectory);
|
|
15
|
+
return containedPath(root, join(root, `${launchID}.json`), "provisioning receipt");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function writeProvisioningReceipt(dataDirectory, receipt) {
|
|
19
|
+
const validated = validateProvisioningReceipt(receipt);
|
|
20
|
+
const root = provisioningReceiptDirectory(dataDirectory);
|
|
21
|
+
await rejectSymlinkComponents(dataDirectory, root);
|
|
22
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
23
|
+
await rejectSymlinkComponents(dataDirectory, root);
|
|
24
|
+
const target = provisioningReceiptPath(dataDirectory, validated.launch_id);
|
|
25
|
+
const temporary = join(root, `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`);
|
|
26
|
+
try {
|
|
27
|
+
await writeFile(temporary, `${JSON.stringify(validated, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
28
|
+
await rename(temporary, target);
|
|
29
|
+
if (process.platform !== "win32") await chmod(target, 0o600);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
await unlink(temporary).catch(() => {});
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
return validated;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function readProvisioningReceipt(dataDirectory, launchID) {
|
|
38
|
+
const target = provisioningReceiptPath(dataDirectory, launchID);
|
|
39
|
+
let info;
|
|
40
|
+
try {
|
|
41
|
+
info = await lstat(target);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error?.code === "ENOENT") return null;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_RECEIPT_BYTES) {
|
|
47
|
+
throw new Error("provisioning receipt must be a bounded regular file");
|
|
48
|
+
}
|
|
49
|
+
if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
|
|
50
|
+
throw new Error("provisioning receipt permissions are too broad");
|
|
51
|
+
}
|
|
52
|
+
const canonical = await realpath(target);
|
|
53
|
+
if (canonical !== resolve(target)) throw new Error("provisioning receipt must be canonical");
|
|
54
|
+
let value;
|
|
55
|
+
try {
|
|
56
|
+
value = JSON.parse(await readFile(target, "utf8"));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error("provisioning receipt is invalid", { cause: error });
|
|
59
|
+
}
|
|
60
|
+
return validateProvisioningReceipt(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function validateProvisioningReceipt(value) {
|
|
64
|
+
assertExactKeys(value, [
|
|
65
|
+
"launch_id", "host", "request_digest", "profile", "workspace_root",
|
|
66
|
+
"operation_status", "repositories", "created_at", "updated_at",
|
|
67
|
+
], "provisioning receipt");
|
|
68
|
+
assertLaunchID(value.launch_id);
|
|
69
|
+
if (value.host !== "deepseek") throw new Error("provisioning receipt host is invalid");
|
|
70
|
+
assertDigest(value.request_digest, "request digest");
|
|
71
|
+
if (typeof value.profile !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(value.profile)) {
|
|
72
|
+
throw new Error("provisioning receipt profile is invalid");
|
|
73
|
+
}
|
|
74
|
+
if (typeof value.workspace_root !== "string" || !isAbsolute(value.workspace_root)) {
|
|
75
|
+
throw new Error("provisioning receipt workspace root is invalid");
|
|
76
|
+
}
|
|
77
|
+
if (!receiptStatuses.has(value.operation_status)) throw new Error("provisioning receipt status is invalid");
|
|
78
|
+
if (!Array.isArray(value.repositories) || value.repositories.length < 1 || value.repositories.length > 8) {
|
|
79
|
+
throw new Error("provisioning receipt repository count is invalid");
|
|
80
|
+
}
|
|
81
|
+
const keys = new Set();
|
|
82
|
+
const paths = new Set();
|
|
83
|
+
for (const repository of value.repositories) {
|
|
84
|
+
validateRepositoryReceipt(repository);
|
|
85
|
+
if (keys.has(repository.repository_key) || paths.has(repository.worktree_path)) {
|
|
86
|
+
throw new Error("provisioning receipt repositories must be unique");
|
|
87
|
+
}
|
|
88
|
+
keys.add(repository.repository_key);
|
|
89
|
+
paths.add(repository.worktree_path);
|
|
90
|
+
}
|
|
91
|
+
if (!Number.isFinite(Date.parse(value.created_at)) || !Number.isFinite(Date.parse(value.updated_at))) {
|
|
92
|
+
throw new Error("provisioning receipt timestamps are invalid");
|
|
93
|
+
}
|
|
94
|
+
return structuredClone(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateRepositoryReceipt(value) {
|
|
98
|
+
assertExactKeys(value, [
|
|
99
|
+
"source_repository_identity", "repository_key", "remote_name",
|
|
100
|
+
"base_branch", "target_branch", "fetched_commit", "worktree_path", "operation_status", "created_at",
|
|
101
|
+
], "provisioning repository receipt");
|
|
102
|
+
assertDigest(value.source_repository_identity, "source repository identity");
|
|
103
|
+
if (typeof value.repository_key !== "string" || !/^[a-z0-9][a-z0-9._-]{0,127}$/u.test(value.repository_key)) {
|
|
104
|
+
throw new Error("provisioning repository key is invalid");
|
|
105
|
+
}
|
|
106
|
+
for (const field of ["worktree_path"]) {
|
|
107
|
+
if (typeof value[field] !== "string" || !isAbsolute(value[field])) throw new Error(`provisioning ${field} is invalid`);
|
|
108
|
+
}
|
|
109
|
+
for (const field of ["remote_name", "base_branch", "target_branch"]) {
|
|
110
|
+
if (typeof value[field] !== "string" || value[field] === "" || value[field].length > 255 || /[\0\r\n;=]/u.test(value[field])) {
|
|
111
|
+
throw new Error(`provisioning ${field} is invalid`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (value.fetched_commit !== null && (typeof value.fetched_commit !== "string" || !/^[0-9a-f]{40,64}$/u.test(value.fetched_commit))) {
|
|
115
|
+
throw new Error("provisioning fetched commit is invalid");
|
|
116
|
+
}
|
|
117
|
+
if (!repositoryStatuses.has(value.operation_status)) throw new Error("provisioning repository status is invalid");
|
|
118
|
+
if (!Number.isFinite(Date.parse(value.created_at))) throw new Error("provisioning repository timestamp is invalid");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function provisioningReceiptDirectory(dataDirectory) {
|
|
122
|
+
const root = resolve(dataDirectory);
|
|
123
|
+
return containedPath(root, join(root, "host-operations", "deepseek", "provisioning"), "provisioning receipt directory");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertLaunchID(value) {
|
|
127
|
+
if (typeof value !== "string" || !/^[0-9a-f-]{36}$/u.test(value)) throw new Error("provisioning launch identity is invalid");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertDigest(value, label) {
|
|
131
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) throw new Error(`${label} is invalid`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertExactKeys(value, expected, label) {
|
|
135
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
136
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...expected].sort())) throw new Error(`${label} fields are invalid`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function containedPath(root, candidate, label) {
|
|
140
|
+
const canonicalRoot = resolve(root);
|
|
141
|
+
const canonicalCandidate = resolve(candidate);
|
|
142
|
+
const offset = relative(canonicalRoot, canonicalCandidate);
|
|
143
|
+
if (offset === ".." || offset.startsWith(`..${sep}`) || isAbsolute(offset)) {
|
|
144
|
+
throw new Error(`${label} escapes its owned root`);
|
|
145
|
+
}
|
|
146
|
+
return canonicalCandidate;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function rejectSymlinkComponents(root, candidate) {
|
|
150
|
+
const canonicalRoot = resolve(root);
|
|
151
|
+
const target = containedPath(canonicalRoot, candidate, "receipt path");
|
|
152
|
+
const parts = relative(canonicalRoot, target).split(sep).filter(Boolean);
|
|
153
|
+
let current = canonicalRoot;
|
|
154
|
+
for (const part of parts) {
|
|
155
|
+
current = join(current, part);
|
|
156
|
+
try {
|
|
157
|
+
if ((await lstat(current)).isSymbolicLink()) throw new Error(`receipt path contains a symbolic link: ${current}`);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error?.code === "ENOENT") return;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
package/lib/tool-names.mjs
CHANGED
|
@@ -14,9 +14,11 @@ export const DEV_FLOW_RAW_TOOL_NAMES = Object.freeze([
|
|
|
14
14
|
"dev_flow_submit_comprehension",
|
|
15
15
|
"dev_flow_submit_refactor",
|
|
16
16
|
"dev_flow_submit_delivery",
|
|
17
|
+
"dev_flow_prepare_task_relocation",
|
|
17
18
|
"dev_flow_resolve_blocker",
|
|
18
19
|
"dev_flow_recover_action",
|
|
19
20
|
"dev_flow_cancel_task",
|
|
21
|
+
"dev_flow_abandon_task",
|
|
20
22
|
]);
|
|
21
23
|
|
|
22
24
|
export const DEV_FLOW_QUALIFIED_TOOL_NAMES = Object.freeze(
|