@tea-agent/loop-agent 0.22.0 → 0.24.0
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/AGENTS.md +42 -108
- package/CHANGELOG.md +85 -0
- package/README.md +8 -5
- package/bin/agent-worker.js +0 -0
- package/dist/application/context-usage/skill-resolution-stats.js +263 -0
- package/dist/application/dag/generate-task-dag.js +17 -3
- package/dist/cli/command-definitions.js +8 -7
- package/dist/cli/program.js +17 -15
- package/dist/commands/doctor.js +269 -18
- package/dist/commands/init.js +101 -86
- package/dist/commands/stats.js +40 -11
- package/dist/executors/shell-executor.js +20 -7
- package/dist/shared/operator/capabilities.js +486 -3
- package/dist/worker/console/app-data.js +6 -0
- package/dist/worker/console/chat/artifact-card.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +495 -0
- package/dist/worker/console/chat/chat-ui-policy.js +25 -0
- package/dist/worker/console/chat/composer-draft-store.js +45 -0
- package/dist/worker/console/chat/context-panel.js +54 -0
- package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
- package/dist/worker/console/chat/explore-tools.js +299 -0
- package/dist/worker/console/chat/human-gate-card.js +37 -0
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/interview-adapter.js +136 -0
- package/dist/worker/console/chat/model-resolver.js +106 -0
- package/dist/worker/console/chat/operation-card.js +23 -0
- package/dist/worker/console/chat/pi-console-config.js +158 -0
- package/dist/worker/console/chat/pi-runtime.js +1143 -0
- package/dist/worker/console/chat/repo-browser.js +140 -0
- package/dist/worker/console/chat/repo-walk.js +116 -0
- package/dist/worker/console/chat/resource-loader.js +67 -0
- package/dist/worker/console/chat/routes.js +1646 -0
- package/dist/worker/console/chat/runtime-context.js +24 -0
- package/dist/worker/console/chat/runtime-selection.js +37 -0
- package/dist/worker/console/chat/session-store.js +437 -0
- package/dist/worker/console/chat/shortcuts.js +15 -0
- package/dist/worker/console/chat/tool-adapter.js +125 -0
- package/dist/worker/console/chat/tools.js +195 -0
- package/dist/worker/console/chat/usage.js +37 -0
- package/dist/worker/console/chat/workspace-landing.js +56 -0
- package/dist/worker/console/dag-confirmation.js +42 -8
- package/dist/worker/console/human-gate-token.js +130 -0
- package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
- package/dist/worker/console/operation-runner.js +6 -2
- package/dist/worker/console/operation-sse.js +26 -0
- package/dist/worker/console/operator-actions.js +420 -7
- package/dist/worker/console/server.js +68 -1
- package/dist/worker/console/static/assets/index-BTbrEHnO.css +1 -0
- package/dist/worker/console/static/assets/index-D9qLevoP.js +27 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/feature/profile-schema.js +1 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -5
- package/dist/workflows/dag/backend-test-result-contract.js +229 -0
- package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
- package/dist/workflows/dag/init-hybrid.js +71 -22
- package/dist/workflows/dag/node-execution.js +38 -1
- package/dist/workflows/dag/output-protocol.js +89 -0
- package/dist/workflows/dag/prompt.js +35 -1
- package/dist/workflows/dag/recovery-recommendation.js +45 -0
- package/dist/workflows/dag/report.js +28 -1
- package/dist/workflows/dag/rerun-task.js +1 -1
- package/dist/workflows/dag/scheduler.js +9 -0
- package/dist/workflows/dag/types.js +12 -0
- package/dist/workflows/dag/validate.js +55 -0
- package/docs/README.md +73 -156
- package/docs/architecture/README.md +7 -6
- package/docs/architecture/dag-execution.md +2 -2
- package/docs/architecture/evolution.md +16 -14
- package/docs/architecture/system-overview.md +1 -1
- package/docs/architecture/worker-and-feature.md +3 -3
- package/docs/governance/README.md +15 -0
- package/docs/{harness-methodology-debugging.md → governance/harness-methodology-debugging.md} +27 -3
- package/docs/init-surface.manifest.json +22 -4
- package/docs/operations/README.md +12 -0
- package/docs/{local-development-environment.md → operations/local-development-environment.md} +1 -1
- package/docs/skills/vetted-skill-registry.md +23 -3
- package/docs/templates/README.md +55 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/progress-log.md +9 -2
- package/harness.json +4 -4
- package/package.json +5 -5
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -2
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/grill-with-docs/SKILL.md +44 -52
- package/skills/grill-with-docs/adr-format.md +37 -26
- package/skills/grill-with-docs/context-format.md +18 -26
- package/skills/loop-agent/SKILL.md +28 -112
- package/skills/loop-agent/references/command-reference.md +9 -3
- package/skills/loop-agent/references/harness-policy.md +3 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
- package/skills/loop-agent/references/task-workflow.md +2 -0
- package/skills/systematic-debugging/SKILL.md +20 -4
- package/skills/test-driven-development/SKILL.md +10 -3
- package/dist/worker/console/static/assets/index-CUDke82y.js +0 -18
- package/dist/worker/console/static/assets/index-wSEksVSO.css +0 -1
- /package/docs/{harness-methodology-tdd.md → governance/harness-methodology-tdd.md} +0 -0
- /package/docs/{harness-methodology-verification.md → governance/harness-methodology-verification.md} +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, readdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { guardReadPath, isSensitivePath, scrubSecrets, } from "./explore-tools.js";
|
|
6
|
+
const TREE_LIMIT = 500;
|
|
7
|
+
const READ_LIMIT = 256 * 1024;
|
|
8
|
+
const RESPONSE_LIMIT = 128 * 1024;
|
|
9
|
+
const SKIP_DIRS = new Set([
|
|
10
|
+
".git",
|
|
11
|
+
".harness",
|
|
12
|
+
".agents",
|
|
13
|
+
"node_modules",
|
|
14
|
+
".next",
|
|
15
|
+
"dist",
|
|
16
|
+
"build",
|
|
17
|
+
"out",
|
|
18
|
+
".cache",
|
|
19
|
+
".turbo",
|
|
20
|
+
".vite",
|
|
21
|
+
"coverage",
|
|
22
|
+
]);
|
|
23
|
+
export class RepoBrowserError extends Error {
|
|
24
|
+
status;
|
|
25
|
+
code;
|
|
26
|
+
constructor(status, code, message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function guardedCode(code) {
|
|
33
|
+
return code === "PATH_OUTSIDE_REPO" ? "OUT_OF_REPO" : code;
|
|
34
|
+
}
|
|
35
|
+
export async function listRepoDirectory(repoRoot, rawPath) {
|
|
36
|
+
const root = rawPath === "";
|
|
37
|
+
const guard = await guardReadPath(repoRoot, root ? "." : rawPath);
|
|
38
|
+
if (!guard.ok)
|
|
39
|
+
throw new RepoBrowserError(403, guardedCode(guard.code), guard.message);
|
|
40
|
+
try {
|
|
41
|
+
const info = await lstat(guard.abs);
|
|
42
|
+
if (info.isSymbolicLink())
|
|
43
|
+
throw new RepoBrowserError(403, "OUT_OF_REPO", "symbolic links are not browsable");
|
|
44
|
+
if (!info.isDirectory())
|
|
45
|
+
throw new RepoBrowserError(400, "NOT_A_DIRECTORY", "path is not a directory");
|
|
46
|
+
const base = root ? "" : guard.repoRelative;
|
|
47
|
+
const entries = (await readdir(guard.abs, { withFileTypes: true }))
|
|
48
|
+
.filter((entry) => !entry.isSymbolicLink() && (entry.isDirectory() || entry.isFile()))
|
|
49
|
+
.filter((entry) => {
|
|
50
|
+
const relative = base ? `${base}/${entry.name}` : entry.name;
|
|
51
|
+
return (!isSensitivePath(relative) &&
|
|
52
|
+
!(entry.isDirectory() && SKIP_DIRS.has(entry.name)));
|
|
53
|
+
})
|
|
54
|
+
.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) ||
|
|
55
|
+
a.name.localeCompare(b.name));
|
|
56
|
+
const rechecked = await guardReadPath(repoRoot, root ? "." : rawPath);
|
|
57
|
+
if (!rechecked.ok || rechecked.abs !== guard.abs)
|
|
58
|
+
throw new RepoBrowserError(403, "OUT_OF_REPO", "directory changed while it was being listed");
|
|
59
|
+
return {
|
|
60
|
+
path: base,
|
|
61
|
+
entries: await Promise.all(entries.slice(0, TREE_LIMIT).map(async (entry) => {
|
|
62
|
+
const relative = base ? `${base}/${entry.name}` : entry.name;
|
|
63
|
+
return entry.isDirectory()
|
|
64
|
+
? { name: entry.name, path: relative, kind: "directory" }
|
|
65
|
+
: {
|
|
66
|
+
name: entry.name,
|
|
67
|
+
path: relative,
|
|
68
|
+
kind: "file",
|
|
69
|
+
size: (await lstat(path.join(guard.abs, entry.name))).size,
|
|
70
|
+
};
|
|
71
|
+
})),
|
|
72
|
+
truncated: entries.length > TREE_LIMIT,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof RepoBrowserError)
|
|
77
|
+
throw error;
|
|
78
|
+
if (error.code === "ENOENT")
|
|
79
|
+
throw new RepoBrowserError(404, "NOT_FOUND", "directory does not exist");
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function readRepoPreview(repoRoot, rawPath) {
|
|
84
|
+
const guard = await guardReadPath(repoRoot, rawPath);
|
|
85
|
+
if (!guard.ok)
|
|
86
|
+
throw new RepoBrowserError(403, guardedCode(guard.code), guard.message);
|
|
87
|
+
try {
|
|
88
|
+
const handle = await open(guard.abs, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
89
|
+
let bytes;
|
|
90
|
+
let size;
|
|
91
|
+
try {
|
|
92
|
+
const info = await handle.stat();
|
|
93
|
+
if (!info.isFile())
|
|
94
|
+
throw new RepoBrowserError(400, "NOT_A_FILE", "path is not a regular file");
|
|
95
|
+
if (info.size > READ_LIMIT)
|
|
96
|
+
throw new RepoBrowserError(413, "TOO_LARGE", `file exceeds ${READ_LIMIT} byte preview limit`);
|
|
97
|
+
const rechecked = await guardReadPath(repoRoot, rawPath);
|
|
98
|
+
if (!rechecked.ok)
|
|
99
|
+
throw new RepoBrowserError(403, guardedCode(rechecked.code), "file path changed while it was being opened");
|
|
100
|
+
const current = await lstat(rechecked.abs);
|
|
101
|
+
if (current.isSymbolicLink() ||
|
|
102
|
+
current.dev !== info.dev ||
|
|
103
|
+
current.ino !== info.ino)
|
|
104
|
+
throw new RepoBrowserError(403, "OUT_OF_REPO", "file identity changed while it was being opened");
|
|
105
|
+
size = info.size;
|
|
106
|
+
bytes = Buffer.alloc(size);
|
|
107
|
+
const bytesRead = size
|
|
108
|
+
? (await handle.read(bytes, 0, size, 0)).bytesRead
|
|
109
|
+
: 0;
|
|
110
|
+
bytes = bytes.subarray(0, bytesRead);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await handle.close();
|
|
114
|
+
}
|
|
115
|
+
if (bytes.includes(0))
|
|
116
|
+
throw new RepoBrowserError(415, "BINARY_UNSUPPORTED", "binary files cannot be previewed");
|
|
117
|
+
let end = Math.min(bytes.length, RESPONSE_LIMIT);
|
|
118
|
+
while (end > 0 && end < bytes.length && (bytes[end] & 0xc0) === 0x80)
|
|
119
|
+
end -= 1;
|
|
120
|
+
const content = scrubSecrets(bytes.subarray(0, end).toString("utf8")).scrubbed;
|
|
121
|
+
return {
|
|
122
|
+
path: guard.repoRelative,
|
|
123
|
+
kind: "text",
|
|
124
|
+
content,
|
|
125
|
+
language: path.extname(guard.repoRelative).slice(1).toLowerCase() || "text",
|
|
126
|
+
bytes: size,
|
|
127
|
+
truncated: bytes.length > RESPONSE_LIMIT,
|
|
128
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof RepoBrowserError)
|
|
133
|
+
throw error;
|
|
134
|
+
if (error.code === "ENOENT")
|
|
135
|
+
throw new RepoBrowserError(404, "NOT_FOUND", "file does not exist");
|
|
136
|
+
if (error.code === "ELOOP")
|
|
137
|
+
throw new RepoBrowserError(403, "OUT_OF_REPO", "symbolic links are not previewable");
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo file walker for the safe explore tools (roadmap M0-A).
|
|
3
|
+
*
|
|
4
|
+
* Walks the repo working tree, skipping gitignored paths (best-effort, no
|
|
5
|
+
* extra dep), the sensitive denylist, and common binary/build dirs. Returns
|
|
6
|
+
* repo-relative + absolute path pairs for bounded read/grep.
|
|
7
|
+
*/
|
|
8
|
+
import { readdir } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { isSensitivePath } from "./explore-tools.js";
|
|
11
|
+
/** Directories never descended into (build output, VCS, deps, lock state). */
|
|
12
|
+
const SKIP_DIRS = new Set([
|
|
13
|
+
".git",
|
|
14
|
+
"node_modules",
|
|
15
|
+
".next",
|
|
16
|
+
"dist",
|
|
17
|
+
"build",
|
|
18
|
+
"out",
|
|
19
|
+
".cache",
|
|
20
|
+
".turbo",
|
|
21
|
+
".vite",
|
|
22
|
+
"coverage",
|
|
23
|
+
".harness",
|
|
24
|
+
".agents",
|
|
25
|
+
]);
|
|
26
|
+
/** File extensions treated as binary (skipped by grep). */
|
|
27
|
+
const BINARY_EXTS = new Set([
|
|
28
|
+
".png",
|
|
29
|
+
".jpg",
|
|
30
|
+
".jpeg",
|
|
31
|
+
".gif",
|
|
32
|
+
".webp",
|
|
33
|
+
".bmp",
|
|
34
|
+
".ico",
|
|
35
|
+
".pdf",
|
|
36
|
+
".zip",
|
|
37
|
+
".gz",
|
|
38
|
+
".tar",
|
|
39
|
+
".tgz",
|
|
40
|
+
".7z",
|
|
41
|
+
".rar",
|
|
42
|
+
".woff",
|
|
43
|
+
".woff2",
|
|
44
|
+
".ttf",
|
|
45
|
+
".otf",
|
|
46
|
+
".eot",
|
|
47
|
+
".mp3",
|
|
48
|
+
".mp4",
|
|
49
|
+
".webm",
|
|
50
|
+
".mov",
|
|
51
|
+
".exe",
|
|
52
|
+
".dll",
|
|
53
|
+
".so",
|
|
54
|
+
".dylib",
|
|
55
|
+
".class",
|
|
56
|
+
".jar",
|
|
57
|
+
".wasm",
|
|
58
|
+
]);
|
|
59
|
+
function globToRe(glob) {
|
|
60
|
+
// Minimal glob → regex: * → [^/]*, ** → .*, ? → ., escape others.
|
|
61
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
62
|
+
const re = escaped
|
|
63
|
+
.replace(/\*\*/g, "\u0000")
|
|
64
|
+
.replace(/\*/g, "[^/]*")
|
|
65
|
+
.replace(/\u0000/g, ".*")
|
|
66
|
+
.replace(/\?/g, ".");
|
|
67
|
+
return new RegExp(`^${re}$`, "i");
|
|
68
|
+
}
|
|
69
|
+
function isBinaryExt(p) {
|
|
70
|
+
return BINARY_EXTS.has(path.extname(p).toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Walk the repo working tree under repoRoot. Returns at most maxFiles files.
|
|
74
|
+
* Skips SKIP_DIRS, binary files (by extension), and (optionally) sensitive
|
|
75
|
+
* paths. Does NOT follow symlinks (defense against escape).
|
|
76
|
+
*/
|
|
77
|
+
export async function walkRepoFiles(repoRoot, options) {
|
|
78
|
+
const skipSensitive = options?.skipSensitive ?? true;
|
|
79
|
+
const maxFiles = options?.maxFiles ?? 5000;
|
|
80
|
+
const filter = options?.glob ? globToRe(options.glob) : undefined;
|
|
81
|
+
const out = [];
|
|
82
|
+
async function walk(dir, prefix) {
|
|
83
|
+
if (out.length >= maxFiles)
|
|
84
|
+
return;
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (out.length >= maxFiles)
|
|
94
|
+
return;
|
|
95
|
+
const name = entry.name;
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
if (SKIP_DIRS.has(name))
|
|
98
|
+
continue;
|
|
99
|
+
await walk(path.join(dir, name), `${prefix}${name}/`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!entry.isFile())
|
|
103
|
+
continue; // skip symlinks, sockets, etc.
|
|
104
|
+
if (isBinaryExt(name))
|
|
105
|
+
continue;
|
|
106
|
+
const repoRelative = `${prefix}${name}`;
|
|
107
|
+
if (skipSensitive && isSensitivePath(repoRelative))
|
|
108
|
+
continue;
|
|
109
|
+
if (filter && !filter.test(repoRelative))
|
|
110
|
+
continue;
|
|
111
|
+
out.push({ abs: path.join(dir, name), repoRelative });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
await walk(repoRoot, "");
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chat — closed ResourceLoader contract (design §7.5 / V12 / plan W3).
|
|
3
|
+
*
|
|
4
|
+
* M0-A (roadmap §14 工作块 A): the bare bash/read/grep built-ins are GONE.
|
|
5
|
+
* The Chat session now exposes safe explore custom tools (safe-read /
|
|
6
|
+
* safe-grep / git-status / git-diff) which enforce a repo-relative boundary,
|
|
7
|
+
* a sensitive-file denylist, and secret scrubbing — plus find/ls. The full
|
|
8
|
+
* model-callable operator action set is still allowed; the only denied tools
|
|
9
|
+
* are file-WRITING coding tools (edit/write/apply_patch/full-tools/shell/
|
|
10
|
+
* coding-chat) AND bash/read/grep (replaced by the safe versions). hasBash is
|
|
11
|
+
* therefore false: there is no shell escape.
|
|
12
|
+
*
|
|
13
|
+
* Note: this adapter is NOT the SDK's own ResourceLoader (that is
|
|
14
|
+
* DefaultResourceLoader, created inside createAgentSessionServices with
|
|
15
|
+
* noContextFiles/noSkills/noExtensions). This adapter is the operator-chat
|
|
16
|
+
* declarative surface used for capabilities reporting and red-team checks.
|
|
17
|
+
* The three-gate authorization (authorizeOperatorChatTool) is the actual
|
|
18
|
+
* enforcement; this adapter describes what that gate admits.
|
|
19
|
+
*/
|
|
20
|
+
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, authorizeOperatorChatTool, filterOperatorChatTools, assertNoWriteToolInList, } from "./tools.js";
|
|
21
|
+
/** Built-in explore tool names that are ALLOWED in every Chat session (M0-A). */
|
|
22
|
+
export const OPERATOR_CHAT_BUILTIN_EXPLORE_TOOL_IDS = Object.freeze([
|
|
23
|
+
"find",
|
|
24
|
+
"ls",
|
|
25
|
+
]);
|
|
26
|
+
/** Safe explore custom tool names (M0-A). */
|
|
27
|
+
export const OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
|
|
28
|
+
"safe-read",
|
|
29
|
+
"safe-grep",
|
|
30
|
+
"git-status",
|
|
31
|
+
"git-diff",
|
|
32
|
+
]);
|
|
33
|
+
export function createOperatorChatResourceLoader() {
|
|
34
|
+
let active = new Set();
|
|
35
|
+
return {
|
|
36
|
+
mode: "operator-chat-closed",
|
|
37
|
+
allowedToolIds: OPERATOR_CHAT_ALLOWED_TOOLS,
|
|
38
|
+
deniedToolIds: OPERATOR_CHAT_DENIED_TOOLS,
|
|
39
|
+
deniedOperatorActions: OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS,
|
|
40
|
+
builtinExploreToolIds: OPERATOR_CHAT_BUILTIN_EXPLORE_TOOL_IDS,
|
|
41
|
+
safeExploreToolIds: OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
|
|
42
|
+
listTools: () => OPERATOR_CHAT_ALLOWED_TOOLS,
|
|
43
|
+
tryActivateTool: (toolId) => {
|
|
44
|
+
// Explore tools (bash/read/grep/find/ls) are run by the SDK's built-in
|
|
45
|
+
// tool registry, not by the operator dispatcher. tryActivateTool only
|
|
46
|
+
// classifies operator actions here, but it must NOT deny an explore
|
|
47
|
+
// tool — that would contradict the real session. Deny only write tools
|
|
48
|
+
// and report unknowns.
|
|
49
|
+
const explore = OPERATOR_CHAT_BUILTIN_EXPLORE_TOOL_IDS.includes(toolId);
|
|
50
|
+
if (explore) {
|
|
51
|
+
return { ok: true, toolId };
|
|
52
|
+
}
|
|
53
|
+
const decision = authorizeOperatorChatTool(toolId);
|
|
54
|
+
if (decision.ok)
|
|
55
|
+
active.add(decision.toolId);
|
|
56
|
+
return decision;
|
|
57
|
+
},
|
|
58
|
+
setActiveToolsByName: (names) => {
|
|
59
|
+
assertNoWriteToolInList(names);
|
|
60
|
+
const { allowed, denied } = filterOperatorChatTools(names);
|
|
61
|
+
active = new Set(allowed);
|
|
62
|
+
return { active: [...active], denied };
|
|
63
|
+
},
|
|
64
|
+
allowsEnvToolRestore: false,
|
|
65
|
+
hasBash: false,
|
|
66
|
+
};
|
|
67
|
+
}
|