@hmj-ai/cflow 1.3.2 → 1.3.3
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 +1 -0
- package/dist/src/runtime-manifest.js +1 -1
- package/dist/src/runtime.js +43 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ npm start
|
|
|
79
79
|
- Runtime 配置每次保存都会生成不可变 Profile 版本;发布 Flow 时会 pin 精确 Profile 版本,Run Ledger 记录实际执行版本。
|
|
80
80
|
- 设置页只读展示当前工作区,并提供默认 Agent 和测试最长等待时间;资源绑定按需放在折叠的高级区域。
|
|
81
81
|
- Runtime 必须返回符合 CF output contract 的 JSON,否则 Flow fail closed。
|
|
82
|
+
- Runtime 工具请求默认按 CF 的 effects 自动授权:已声明且位于工作区内的读写和命令操作无需逐次确认;未声明能力或工作区外路径会被拒绝。Flow 中显式的人工审批节点仍需单独批准。
|
|
82
83
|
- Flow/CF 草稿会保存到 SQLite;Resource Profile 可在设置中创建、编辑与删除。
|
|
83
84
|
|
|
84
85
|
非内置 Agent 可以通过 JSON manifest 接入。项目级文件放在 `<workspace>/.cflow/agents.d/*.json`,用户级文件放在 `~/.config/cflow/agents.d/*.json`;项目配置优先级更高。最小示例:
|
package/dist/src/runtime.js
CHANGED
|
@@ -15,6 +15,48 @@ export class RuntimeExecutionException extends Error {
|
|
|
15
15
|
this.details = details;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
export function isAcpToolAllowed(tool, effects, root, cwd, analysis = false) {
|
|
19
|
+
const kind = String(tool.kind ?? tool.name ?? tool.title ?? '').toLowerCase();
|
|
20
|
+
const effectType = kind.includes('read') || kind.includes('search')
|
|
21
|
+
? 'file-read'
|
|
22
|
+
: kind.includes('edit') ||
|
|
23
|
+
kind.includes('write') ||
|
|
24
|
+
kind.includes('delete') ||
|
|
25
|
+
kind.includes('move')
|
|
26
|
+
? 'file-write'
|
|
27
|
+
: kind.includes('exec') || kind.includes('command') || kind.includes('terminal')
|
|
28
|
+
? 'command'
|
|
29
|
+
: undefined;
|
|
30
|
+
const declared = effectType
|
|
31
|
+
? analysis && effectType === 'file-read'
|
|
32
|
+
? true
|
|
33
|
+
: effects.some((effect) => effect.type === effectType && effect.scope === 'workspace')
|
|
34
|
+
: false;
|
|
35
|
+
const locations = Array.isArray(tool.locations) ? [...tool.locations] : [];
|
|
36
|
+
if (tool.rawInput && typeof tool.rawInput === 'object') {
|
|
37
|
+
for (const key of ['path', 'file', 'filePath', 'filename']) {
|
|
38
|
+
const value = tool.rawInput[key];
|
|
39
|
+
if (typeof value === 'string')
|
|
40
|
+
locations.push(value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const inWorkspace = locations.every((location) => {
|
|
44
|
+
let value = typeof location === 'string' ? location : (location?.path ?? location?.uri);
|
|
45
|
+
if (typeof value === 'string' && value.startsWith('file://')) {
|
|
46
|
+
try {
|
|
47
|
+
value = new URL(value).pathname;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === 'string' && !isAbsolute(value))
|
|
54
|
+
value = resolve(cwd, value);
|
|
55
|
+
return Boolean(root) && (!value || isWithinDirectory(root, value));
|
|
56
|
+
});
|
|
57
|
+
const scopedRead = !analysis || effectType !== 'file-read' || locations.length > 0;
|
|
58
|
+
return Boolean(declared && inWorkspace && scopedRead);
|
|
59
|
+
}
|
|
18
60
|
const defaultTraits = (overrides = {}) => ({
|
|
19
61
|
backendKind: 'acp',
|
|
20
62
|
sessionMode: 'per-cf-call',
|
|
@@ -775,48 +817,8 @@ export class RuntimeManager {
|
|
|
775
817
|
return await acpClient({ name: 'CFlow' })
|
|
776
818
|
.onRequest(methods.client.session.requestPermission, async (ctx) => {
|
|
777
819
|
const tool = ctx?.params?.toolCall ?? ctx?.toolCall ?? {};
|
|
778
|
-
const kind = String(tool.kind ?? tool.name ?? tool.title ?? '').toLowerCase();
|
|
779
|
-
const effectType = kind.includes('read') || kind.includes('search')
|
|
780
|
-
? 'file-read'
|
|
781
|
-
: kind.includes('edit') ||
|
|
782
|
-
kind.includes('write') ||
|
|
783
|
-
kind.includes('delete') ||
|
|
784
|
-
kind.includes('move')
|
|
785
|
-
? 'file-write'
|
|
786
|
-
: kind.includes('exec') || kind.includes('command') || kind.includes('terminal')
|
|
787
|
-
? 'command'
|
|
788
|
-
: undefined;
|
|
789
820
|
const root = analysis?.allowedRoot ?? context?.workspaceRoot;
|
|
790
|
-
|
|
791
|
-
? analysis && effectType === 'file-read'
|
|
792
|
-
? true
|
|
793
|
-
: effects.some((effect) => effect.type === effectType && effect.scope === 'workspace')
|
|
794
|
-
: false;
|
|
795
|
-
const locations = Array.isArray(tool.locations) ? [...tool.locations] : [];
|
|
796
|
-
const raw = tool.rawInput;
|
|
797
|
-
if (raw && typeof raw === 'object') {
|
|
798
|
-
for (const key of ['path', 'file', 'filePath', 'filename']) {
|
|
799
|
-
const value = raw[key];
|
|
800
|
-
if (typeof value === 'string')
|
|
801
|
-
locations.push(value);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
const inWorkspace = locations.every((location) => {
|
|
805
|
-
let value = typeof location === 'string' ? location : (location?.path ?? location?.uri);
|
|
806
|
-
if (typeof value === 'string' && value.startsWith('file://')) {
|
|
807
|
-
try {
|
|
808
|
-
value = new URL(value).pathname;
|
|
809
|
-
}
|
|
810
|
-
catch {
|
|
811
|
-
return false;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
if (analysis && typeof value === 'string' && !isAbsolute(value))
|
|
815
|
-
value = resolve(cwd, value);
|
|
816
|
-
return Boolean(root) && (!value || isWithinDirectory(root, value));
|
|
817
|
-
});
|
|
818
|
-
const scopedRead = !analysis || effectType !== 'file-read' || locations.length > 0;
|
|
819
|
-
if (!declared || !inWorkspace || !scopedRead) {
|
|
821
|
+
if (!isAcpToolAllowed(tool, effects, root, cwd, Boolean(analysis))) {
|
|
820
822
|
permissionDenied = true;
|
|
821
823
|
const option = ctx?.params?.options?.find((o) => String(o.kind).startsWith('reject'));
|
|
822
824
|
return option
|