@tea-agent/loop-agent 0.23.1 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +1 -1
  3. package/bin/agent-worker.js +0 -0
  4. package/dist/executors/shell-executor.js +20 -7
  5. package/dist/shared/operator/capabilities.js +475 -2
  6. package/dist/worker/console/app-data.js +2 -0
  7. package/dist/worker/console/chat/artifact-card.js +23 -0
  8. package/dist/worker/console/chat/chat-event-store.js +495 -0
  9. package/dist/worker/console/chat/chat-ui-policy.js +25 -0
  10. package/dist/worker/console/chat/composer-draft-store.js +45 -0
  11. package/dist/worker/console/chat/context-panel.js +54 -0
  12. package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
  13. package/dist/worker/console/chat/explore-tools.js +299 -0
  14. package/dist/worker/console/chat/human-gate-card.js +37 -0
  15. package/dist/worker/console/chat/interview-adapter.js +136 -0
  16. package/dist/worker/console/chat/operation-card.js +23 -0
  17. package/dist/worker/console/chat/pi-console-config.js +158 -0
  18. package/dist/worker/console/chat/pi-runtime.js +581 -43
  19. package/dist/worker/console/chat/repo-browser.js +140 -0
  20. package/dist/worker/console/chat/repo-walk.js +116 -0
  21. package/dist/worker/console/chat/resource-loader.js +18 -17
  22. package/dist/worker/console/chat/routes.js +1354 -65
  23. package/dist/worker/console/chat/runtime-context.js +24 -0
  24. package/dist/worker/console/chat/runtime-selection.js +37 -0
  25. package/dist/worker/console/chat/session-store.js +210 -11
  26. package/dist/worker/console/chat/shortcuts.js +15 -0
  27. package/dist/worker/console/chat/tool-adapter.js +81 -194
  28. package/dist/worker/console/chat/tools.js +72 -48
  29. package/dist/worker/console/chat/usage.js +37 -0
  30. package/dist/worker/console/chat/workspace-landing.js +56 -0
  31. package/dist/worker/console/dag-confirmation.js +42 -8
  32. package/dist/worker/console/human-gate-token.js +130 -0
  33. package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
  34. package/dist/worker/console/operation-runner.js +6 -2
  35. package/dist/worker/console/operation-sse.js +26 -0
  36. package/dist/worker/console/operator-actions.js +420 -7
  37. package/dist/worker/console/server.js +14 -2
  38. package/dist/worker/console/static/assets/index-BTbrEHnO.css +1 -0
  39. package/dist/worker/console/static/assets/index-D9qLevoP.js +27 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -5
  42. package/dist/workflows/dag/backend-test-result-contract.js +229 -0
  43. package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
  44. package/dist/workflows/dag/init-hybrid.js +2 -1
  45. package/docs/README.md +1 -1
  46. package/docs/architecture/README.md +5 -5
  47. package/docs/architecture/evolution.md +4 -4
  48. package/docs/architecture/worker-and-feature.md +1 -1
  49. package/docs/templates/backend-test-dag.json +2 -2
  50. package/harness.json +1 -1
  51. package/package.json +1 -1
  52. package/dist/worker/console/static/assets/index-DVl7Jxt5.js +0 -25
  53. package/dist/worker/console/static/assets/index-lVcIr9Ju.css +0 -1
@@ -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
+ }
@@ -1,18 +1,14 @@
1
1
  /**
2
2
  * Operator Chat — closed ResourceLoader contract (design §7.5 / V12 / plan W3).
3
3
  *
4
- * The Chat session does NOT discover user-level / project-level extensions or
5
- * skills (its tool surface is fixed: the operator-action registry + the
6
- * built-in explore tools). This module is the typed contract describing the
7
- * DECLARED Chat tool surface it mirrors what pi-runtime.ts actually wires
8
- * into the SDK session, so that capabilities/doctor/red-team can report a
9
- * single source of truth. It is deliberately framework-agnostic (does not
10
- * import the Pi SDK at module top-level so it stays unit-testable).
11
- *
12
- * Current contract (2026-07-25 widening): bash/read/grep/find/ls are ALLOWED
13
- * explore tools; the full operator action set is allowed; the ONLY denied
14
- * tools are file-WRITING coding tools (edit/write/apply_patch/full-tools/
15
- * shell/coding-chat). hasBash therefore reflects reality: true.
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.
16
12
  *
17
13
  * Note: this adapter is NOT the SDK's own ResourceLoader (that is
18
14
  * DefaultResourceLoader, created inside createAgentSessionServices with
@@ -22,14 +18,18 @@
22
18
  * enforcement; this adapter describes what that gate admits.
23
19
  */
24
20
  import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, authorizeOperatorChatTool, filterOperatorChatTools, assertNoWriteToolInList, } from "./tools.js";
25
- /** Built-in explore tool names that are ALLOWED in every Chat session. */
21
+ /** Built-in explore tool names that are ALLOWED in every Chat session (M0-A). */
26
22
  export const OPERATOR_CHAT_BUILTIN_EXPLORE_TOOL_IDS = Object.freeze([
27
- "bash",
28
- "read",
29
- "grep",
30
23
  "find",
31
24
  "ls",
32
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
33
  export function createOperatorChatResourceLoader() {
34
34
  let active = new Set();
35
35
  return {
@@ -38,6 +38,7 @@ export function createOperatorChatResourceLoader() {
38
38
  deniedToolIds: OPERATOR_CHAT_DENIED_TOOLS,
39
39
  deniedOperatorActions: OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS,
40
40
  builtinExploreToolIds: OPERATOR_CHAT_BUILTIN_EXPLORE_TOOL_IDS,
41
+ safeExploreToolIds: OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
41
42
  listTools: () => OPERATOR_CHAT_ALLOWED_TOOLS,
42
43
  tryActivateTool: (toolId) => {
43
44
  // Explore tools (bash/read/grep/find/ls) are run by the SDK's built-in
@@ -61,6 +62,6 @@ export function createOperatorChatResourceLoader() {
61
62
  return { active: [...active], denied };
62
63
  },
63
64
  allowsEnvToolRestore: false,
64
- hasBash: true,
65
+ hasBash: false,
65
66
  };
66
67
  }