create-walle 0.9.11 → 0.9.13
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 +3 -3
- package/package.json +2 -2
- package/template/bin/dev.sh +7 -1
- package/template/bin/setup.js +53 -9
- package/template/bin/sync-images.js +53 -0
- package/template/builder-journal.md +17 -0
- package/template/claude-task-manager/api-prompts.js +98 -13
- package/template/claude-task-manager/api-reviews.js +82 -5
- package/template/claude-task-manager/db.js +32 -5
- package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
- package/template/claude-task-manager/lib/session-capture.js +421 -0
- package/template/claude-task-manager/lib/session-history.js +135 -15
- package/template/claude-task-manager/lib/session-jobs.js +10 -5
- package/template/claude-task-manager/lib/session-stream.js +87 -19
- package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
- package/template/claude-task-manager/lib/walle-session-context.js +61 -0
- package/template/claude-task-manager/lib/walle-transcript.js +176 -0
- package/template/claude-task-manager/public/css/setup.css +35 -8
- package/template/claude-task-manager/public/css/walle-session.css +56 -0
- package/template/claude-task-manager/public/css/walle.css +120 -0
- package/template/claude-task-manager/public/index.html +814 -181
- package/template/claude-task-manager/public/js/message-renderer.js +148 -19
- package/template/claude-task-manager/public/js/reviews.js +120 -62
- package/template/claude-task-manager/public/js/setup.js +75 -31
- package/template/claude-task-manager/public/js/stream-view.js +115 -55
- package/template/claude-task-manager/public/js/walle-session.js +84 -2
- package/template/claude-task-manager/public/js/walle.js +308 -54
- package/template/claude-task-manager/server.js +1092 -146
- package/template/claude-task-manager/session-integrity.js +181 -54
- package/template/claude-task-manager/session-utils.js +123 -41
- package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
- package/template/package.json +1 -1
- package/template/wall-e/adapters/ctm.js +39 -18
- package/template/wall-e/agent-runners/contract.js +17 -0
- package/template/wall-e/agent-runners/index.js +22 -0
- package/template/wall-e/agent-runtime/harness.js +212 -0
- package/template/wall-e/agent-runtime/index.js +8 -0
- package/template/wall-e/agent-runtime/registry.js +67 -0
- package/template/wall-e/agent-runtime/session-store.js +179 -0
- package/template/wall-e/agent-runtime/spawn.js +208 -0
- package/template/wall-e/api-walle.js +174 -7
- package/template/wall-e/brain.js +266 -28
- package/template/wall-e/channels/policy.js +88 -0
- package/template/wall-e/channels/registry.js +15 -1
- package/template/wall-e/channels/reply-dispatcher.js +70 -0
- package/template/wall-e/channels/session-bindings.js +51 -0
- package/template/wall-e/chat/code-review-context.js +29 -0
- package/template/wall-e/chat.js +188 -42
- package/template/wall-e/coding/acp-adapter.js +188 -0
- package/template/wall-e/coding/agent-catalog.js +129 -0
- package/template/wall-e/coding/compaction-service.js +247 -0
- package/template/wall-e/coding/execution-trace.js +3 -0
- package/template/wall-e/coding/instruction-service.js +224 -0
- package/template/wall-e/coding/model-message.js +67 -0
- package/template/wall-e/coding/permission-rules-store.js +111 -0
- package/template/wall-e/coding/permission-service.js +266 -0
- package/template/wall-e/coding/prompt-bundle.js +67 -0
- package/template/wall-e/coding/prompt-runtime.js +243 -0
- package/template/wall-e/coding/provider-transform.js +188 -0
- package/template/wall-e/coding/runtime-mode.js +132 -0
- package/template/wall-e/coding/snapshot-service.js +155 -0
- package/template/wall-e/coding/stream-processor.js +268 -0
- package/template/wall-e/coding/task-tool.js +255 -0
- package/template/wall-e/coding/tool-registry.js +361 -0
- package/template/wall-e/coding/transcript-writer.js +143 -0
- package/template/wall-e/coding/workspace-replay.js +324 -0
- package/template/wall-e/coding-context.js +4 -22
- package/template/wall-e/coding-orchestrator.js +307 -18
- package/template/wall-e/coding-prompts.js +44 -3
- package/template/wall-e/context/context-builder.js +43 -1
- package/template/wall-e/context/topic-matcher.js +1 -1
- package/template/wall-e/eval/agent-runner.js +59 -13
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
- package/template/wall-e/eval/benchmarks.js +100 -16
- package/template/wall-e/eval/eval-orchestrator.js +218 -8
- package/template/wall-e/eval/harvester.js +62 -5
- package/template/wall-e/eval/head-to-head.js +23 -2
- package/template/wall-e/eval/humaneval-adapter.js +30 -5
- package/template/wall-e/eval/livecodebench-adapter.js +29 -5
- package/template/wall-e/eval/manifest.js +186 -0
- package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
- package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
- package/template/wall-e/eval/session-transcripts.js +57 -4
- package/template/wall-e/eval/swebench-adapter.js +109 -3
- package/template/wall-e/evaluation/agent-router.js +53 -1
- package/template/wall-e/evaluation/coding-quorum.js +48 -1
- package/template/wall-e/evaluation/router.js +4 -2
- package/template/wall-e/evaluation/tier-selector.js +11 -1
- package/template/wall-e/extraction/contradiction.js +2 -2
- package/template/wall-e/extraction/indexer.js +2 -1
- package/template/wall-e/extraction/knowledge-extractor.js +2 -2
- package/template/wall-e/hooks/cli.js +92 -0
- package/template/wall-e/hooks/discovery.js +119 -0
- package/template/wall-e/hooks/index.js +7 -0
- package/template/wall-e/hooks/manifest.js +55 -0
- package/template/wall-e/hooks/runtime.js +84 -0
- package/template/wall-e/hooks/session-memory.js +225 -0
- package/template/wall-e/http/auth.js +6 -2
- package/template/wall-e/http/chat-api.js +54 -8
- package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
- package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
- package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
- package/template/wall-e/listening/calendar.js +3 -1
- package/template/wall-e/llm/client.js +64 -10
- package/template/wall-e/llm/google.js +39 -5
- package/template/wall-e/llm/ollama.js +1 -1
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/provider-availability.js +10 -0
- package/template/wall-e/llm/provider-error.js +269 -0
- package/template/wall-e/llm/tool-adapter.js +48 -12
- package/template/wall-e/loops/boot.js +2 -1
- package/template/wall-e/loops/initiative.js +2 -2
- package/template/wall-e/loops/tasks.js +8 -47
- package/template/wall-e/loops/workspace-prompts.js +20 -0
- package/template/wall-e/mcp-server.js +442 -1
- package/template/wall-e/memory/session-ingest-service.js +159 -0
- package/template/wall-e/memory/source-indexer.js +289 -0
- package/template/wall-e/plugins/discovery.js +83 -0
- package/template/wall-e/plugins/manifest-loader.js +50 -10
- package/template/wall-e/plugins/manifest-schema.js +69 -0
- package/template/wall-e/plugins/model-catalog.js +55 -0
- package/template/wall-e/prompts/coding/base.txt +2 -0
- package/template/wall-e/prompts/coding/deepseek.txt +1 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
- package/template/wall-e/prompts/coding/plan.txt +1 -0
- package/template/wall-e/runtime/execution-trace.js +220 -0
- package/template/wall-e/security/audit.js +266 -0
- package/template/wall-e/security/ssrf.js +236 -0
- package/template/wall-e/session-files.js +303 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
- package/template/wall-e/skills/internal-skill-registry.js +2 -2
- package/template/wall-e/skills/script-skill-runner.js +143 -0
- package/template/wall-e/skills/skill-executor.js +5 -6
- package/template/wall-e/skills/skill-fallback.js +3 -1
- package/template/wall-e/skills/skill-harness-registry.js +7 -8
- package/template/wall-e/skills/skill-planner.js +52 -4
- package/template/wall-e/skills/slack-ingest.js +11 -3
- package/template/wall-e/sources/base.js +90 -0
- package/template/wall-e/sources/builtin.js +33 -0
- package/template/wall-e/sources/claude-code-jsonl.js +78 -0
- package/template/wall-e/sources/codex-jsonl.js +125 -0
- package/template/wall-e/sources/coding-session-utils.js +117 -0
- package/template/wall-e/sources/contract-suite.js +59 -0
- package/template/wall-e/sources/gemini-jsonl.js +85 -0
- package/template/wall-e/sources/index.js +9 -0
- package/template/wall-e/sources/jsonl-utils.js +181 -0
- package/template/wall-e/sources/record-types.js +252 -0
- package/template/wall-e/sources/registry.js +92 -0
- package/template/wall-e/sources/transforms.js +100 -0
- package/template/wall-e/sources/walle-jsonl.js +108 -0
- package/template/wall-e/tools/coding-middleware.js +31 -1
- package/template/wall-e/tools/file-tracker.js +25 -1
- package/template/wall-e/tools/local-tools.js +75 -47
- package/template/wall-e/tools/session-sharing.js +68 -1
- package/template/wall-e/tools/shell-analyzer.js +1 -1
- package/template/wall-e/tools/shell-policy.js +47 -0
- package/template/wall-e/tools/snapshot.js +42 -0
- package/template/wall-e/training/harvester.js +62 -5
- package/template/wall-e/utils/repair.js +253 -1
- package/template/website/index.html +3 -3
- package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_INSTRUCTION_FILES = ['AGENTS.md', 'CLAUDE.md', 'CONTEXT.md'];
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
9
|
+
const _loadedByScope = new Map();
|
|
10
|
+
|
|
11
|
+
class InstructionService {
|
|
12
|
+
constructor({
|
|
13
|
+
instructionFiles = DEFAULT_INSTRUCTION_FILES,
|
|
14
|
+
configured = [],
|
|
15
|
+
fetchImpl = globalThis.fetch,
|
|
16
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
17
|
+
} = {}) {
|
|
18
|
+
this.instructionFiles = instructionFiles;
|
|
19
|
+
this.configured = configured;
|
|
20
|
+
this.fetchImpl = fetchImpl;
|
|
21
|
+
this.timeoutMs = timeoutMs;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getSystemInstructions({ projectRoot = process.cwd(), includeGlobal = true } = {}) {
|
|
25
|
+
const instructions = [];
|
|
26
|
+
const project = this._firstExisting(projectRoot, this.instructionFiles);
|
|
27
|
+
if (project) instructions.push(project);
|
|
28
|
+
|
|
29
|
+
if (includeGlobal) {
|
|
30
|
+
for (const candidate of [
|
|
31
|
+
path.join(os.homedir(), '.walle', 'instructions.md'),
|
|
32
|
+
path.join(os.homedir(), '.claude', 'CLAUDE.md'),
|
|
33
|
+
]) {
|
|
34
|
+
const loaded = readInstruction(candidate);
|
|
35
|
+
if (loaded) instructions.push(loaded);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return dedupeInstructions(instructions);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
discoverForRead(filePath, {
|
|
42
|
+
projectRoot = process.cwd(),
|
|
43
|
+
sessionId = '',
|
|
44
|
+
messageId = '',
|
|
45
|
+
dedupe = true,
|
|
46
|
+
} = {}) {
|
|
47
|
+
const scope = `${sessionId || 'global'}:${messageId || 'session'}`;
|
|
48
|
+
return this.discoverForFile(filePath, projectRoot, { scope, dedupe, skipPath: filePath });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
discoverForFile(filePath, projectRoot = process.cwd(), { scope = '', dedupe = false, skipPath = '' } = {}) {
|
|
52
|
+
const resolvedRoot = realpathBestEffort(projectRoot);
|
|
53
|
+
const resolvedFile = realpathBestEffort(filePath);
|
|
54
|
+
let dir = fs.existsSync(resolvedFile) && fs.statSync(resolvedFile).isDirectory()
|
|
55
|
+
? resolvedFile
|
|
56
|
+
: path.dirname(resolvedFile);
|
|
57
|
+
const found = [];
|
|
58
|
+
const loaded = scope ? getLoadedSet(scope) : null;
|
|
59
|
+
|
|
60
|
+
while (isWithin(resolvedRoot, dir)) {
|
|
61
|
+
for (const name of this.instructionFiles) {
|
|
62
|
+
const candidate = path.join(dir, name);
|
|
63
|
+
if (path.resolve(candidate) === path.resolve(skipPath || '')) continue;
|
|
64
|
+
const instruction = readInstruction(candidate);
|
|
65
|
+
if (!instruction) continue;
|
|
66
|
+
if (dedupe && loaded?.has(instruction.path)) continue;
|
|
67
|
+
found.push(instruction);
|
|
68
|
+
if (dedupe) loaded?.add(instruction.path);
|
|
69
|
+
}
|
|
70
|
+
const parent = path.dirname(dir);
|
|
71
|
+
if (parent === dir) break;
|
|
72
|
+
dir = parent;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
instructions: dedupeInstructions(found),
|
|
76
|
+
loadedPaths: found.map((item) => item.path),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async loadConfiguredInstructions() {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const entry of this.configured || []) {
|
|
83
|
+
if (!entry) continue;
|
|
84
|
+
if (/^https?:\/\//i.test(entry)) {
|
|
85
|
+
const loaded = await this._fetchInstruction(entry);
|
|
86
|
+
if (loaded) out.push(loaded);
|
|
87
|
+
} else if (entry.includes('*')) {
|
|
88
|
+
for (const filePath of expandGlob(entry)) {
|
|
89
|
+
const loaded = readInstruction(filePath);
|
|
90
|
+
if (loaded) out.push(loaded);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
const loaded = readInstruction(entry);
|
|
94
|
+
if (loaded) out.push(loaded);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return dedupeInstructions(out);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async _fetchInstruction(url) {
|
|
101
|
+
if (typeof this.fetchImpl !== 'function') return null;
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
104
|
+
try {
|
|
105
|
+
const response = await this.fetchImpl(url, { signal: controller.signal });
|
|
106
|
+
if (!response?.ok) return null;
|
|
107
|
+
const content = await response.text();
|
|
108
|
+
return { path: url, content };
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_firstExisting(root, names) {
|
|
117
|
+
for (const name of names) {
|
|
118
|
+
const loaded = readInstruction(path.join(root, name));
|
|
119
|
+
if (loaded) return loaded;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readInstruction(filePath) {
|
|
126
|
+
try {
|
|
127
|
+
if (!fs.existsSync(filePath)) return null;
|
|
128
|
+
const stat = fs.statSync(filePath);
|
|
129
|
+
if (!stat.isFile()) return null;
|
|
130
|
+
return {
|
|
131
|
+
path: path.resolve(filePath),
|
|
132
|
+
content: fs.readFileSync(filePath, 'utf8'),
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function dedupeInstructions(instructions) {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const item of instructions || []) {
|
|
143
|
+
if (!item?.path || seen.has(item.path)) continue;
|
|
144
|
+
seen.add(item.path);
|
|
145
|
+
out.push(item);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getLoadedSet(scope) {
|
|
151
|
+
if (!_loadedByScope.has(scope)) _loadedByScope.set(scope, new Set());
|
|
152
|
+
return _loadedByScope.get(scope);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isWithin(root, target) {
|
|
156
|
+
return target === root || target.startsWith(root + path.sep);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function realpathBestEffort(filePath) {
|
|
160
|
+
try {
|
|
161
|
+
return fs.realpathSync(filePath);
|
|
162
|
+
} catch {
|
|
163
|
+
return path.resolve(filePath);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function expandGlob(pattern) {
|
|
168
|
+
const absolutePattern = path.resolve(pattern);
|
|
169
|
+
const firstGlob = absolutePattern.search(/[*?]/);
|
|
170
|
+
const base = firstGlob === -1
|
|
171
|
+
? path.dirname(absolutePattern)
|
|
172
|
+
: nearestExistingParent(absolutePattern.slice(0, firstGlob));
|
|
173
|
+
const regex = globToRegExp(absolutePattern);
|
|
174
|
+
const matches = [];
|
|
175
|
+
walkFiles(base, (filePath) => {
|
|
176
|
+
if (regex.test(path.resolve(filePath))) matches.push(filePath);
|
|
177
|
+
});
|
|
178
|
+
return matches;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function nearestExistingParent(prefix) {
|
|
182
|
+
let dir = prefix.endsWith(path.sep) ? prefix.slice(0, -1) : path.dirname(prefix);
|
|
183
|
+
while (dir && dir !== path.dirname(dir)) {
|
|
184
|
+
if (fs.existsSync(dir)) return dir;
|
|
185
|
+
dir = path.dirname(dir);
|
|
186
|
+
}
|
|
187
|
+
return fs.existsSync(dir) ? dir : process.cwd();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function walkFiles(dir, visit) {
|
|
191
|
+
let entries;
|
|
192
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const full = path.join(dir, entry.name);
|
|
195
|
+
if (entry.isDirectory()) {
|
|
196
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
197
|
+
walkFiles(full, visit);
|
|
198
|
+
} else if (entry.isFile()) {
|
|
199
|
+
visit(full);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function globToRegExp(pattern) {
|
|
205
|
+
const escaped = path.resolve(pattern)
|
|
206
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
207
|
+
.replace(/\*\*/g, '<<GLOBSTAR>>')
|
|
208
|
+
.replace(/\*/g, '[^/]*')
|
|
209
|
+
.replace(/\?/g, '[^/]')
|
|
210
|
+
.replace(/<<GLOBSTAR>>/g, '.*');
|
|
211
|
+
return new RegExp(`^${escaped}$`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resetInstructionDedupe() {
|
|
215
|
+
_loadedByScope.clear();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
InstructionService,
|
|
220
|
+
DEFAULT_INSTRUCTION_FILES,
|
|
221
|
+
DEFAULT_TIMEOUT_MS,
|
|
222
|
+
resetInstructionDedupe,
|
|
223
|
+
expandGlob,
|
|
224
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
function newId(prefix = 'msg') {
|
|
6
|
+
return `${prefix}_${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function textFromContent(content) {
|
|
10
|
+
if (!content) return '';
|
|
11
|
+
if (typeof content === 'string') return content;
|
|
12
|
+
if (Array.isArray(content)) {
|
|
13
|
+
return content.map((part) => {
|
|
14
|
+
if (!part) return '';
|
|
15
|
+
if (typeof part === 'string') return part;
|
|
16
|
+
return part.text || part.content || '';
|
|
17
|
+
}).filter(Boolean).join('\n');
|
|
18
|
+
}
|
|
19
|
+
if (typeof content === 'object') {
|
|
20
|
+
if (content.text != null) return textFromContent(content.text);
|
|
21
|
+
if (content.content != null) return textFromContent(content.content);
|
|
22
|
+
try { return JSON.stringify(content); } catch { return String(content); }
|
|
23
|
+
}
|
|
24
|
+
return String(content);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeToolCall(call = {}, index = 0) {
|
|
28
|
+
return {
|
|
29
|
+
id: call.id || call.toolCallId || call.tool_use_id || newId(`tool_${index}`),
|
|
30
|
+
name: call.name || call.tool || call.function?.name || '',
|
|
31
|
+
input: call.input || call.arguments || call.function?.arguments || {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assistantMessageFromResponse(response = {}) {
|
|
36
|
+
const toolCalls = (response.toolCalls || []).map(normalizeToolCall);
|
|
37
|
+
const content = [];
|
|
38
|
+
if (response.reasoningContent) content.push({ type: 'reasoning', text: response.reasoningContent });
|
|
39
|
+
if (response.content) content.push({ type: 'text', text: textFromContent(response.content) });
|
|
40
|
+
for (const call of toolCalls) {
|
|
41
|
+
content.push({ type: 'tool_use', id: call.id, name: call.name, input: call.input });
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
role: 'assistant',
|
|
45
|
+
content: content.length === 1 && content[0].type === 'text' ? content[0].text : content,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toolResultMessage(toolResults = []) {
|
|
50
|
+
return {
|
|
51
|
+
role: 'user',
|
|
52
|
+
content: toolResults.map((result) => ({
|
|
53
|
+
type: 'tool_result',
|
|
54
|
+
tool_use_id: result.toolCallId || result.id,
|
|
55
|
+
content: textFromContent(result.content ?? result.result ?? result.error ?? ''),
|
|
56
|
+
is_error: Boolean(result.error),
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
newId,
|
|
63
|
+
textFromContent,
|
|
64
|
+
normalizeToolCall,
|
|
65
|
+
assistantMessageFromResponse,
|
|
66
|
+
toolResultMessage,
|
|
67
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { wildcardMatch } = require('../tools/permission-rules');
|
|
5
|
+
|
|
6
|
+
const KV_PREFIX = 'permission:always:';
|
|
7
|
+
const _memoryRules = new Map();
|
|
8
|
+
|
|
9
|
+
class PermissionRulesStore {
|
|
10
|
+
constructor({ brain = null, namespace = KV_PREFIX } = {}) {
|
|
11
|
+
this.brain = brain;
|
|
12
|
+
this.namespace = namespace;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
list(projectRoot = '') {
|
|
16
|
+
return this._load(projectRoot);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
addAlways({ projectRoot = '', permission, pattern = '*', tool = '', metadata = {} } = {}) {
|
|
20
|
+
if (!permission) throw new Error('permission is required');
|
|
21
|
+
const rules = this._load(projectRoot);
|
|
22
|
+
const normalized = {
|
|
23
|
+
permission,
|
|
24
|
+
pattern,
|
|
25
|
+
tool,
|
|
26
|
+
metadata,
|
|
27
|
+
createdAt: new Date().toISOString(),
|
|
28
|
+
};
|
|
29
|
+
const exists = rules.some((rule) =>
|
|
30
|
+
rule.permission === normalized.permission &&
|
|
31
|
+
rule.pattern === normalized.pattern &&
|
|
32
|
+
rule.tool === normalized.tool
|
|
33
|
+
);
|
|
34
|
+
if (!exists) {
|
|
35
|
+
rules.push(normalized);
|
|
36
|
+
this._save(projectRoot, rules);
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
findAlways({ projectRoot = '', permission, pattern = '*', tool = '' } = {}) {
|
|
42
|
+
const roots = uniqueRoots(projectRoot);
|
|
43
|
+
for (const root of roots) {
|
|
44
|
+
for (const rule of this._load(root)) {
|
|
45
|
+
if (tool && rule.tool && rule.tool !== tool) continue;
|
|
46
|
+
if (wildcardMatch(permission || '', rule.permission || '*') && wildcardMatch(pattern || '', rule.pattern || '*')) {
|
|
47
|
+
return rule;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_load(projectRoot = '') {
|
|
55
|
+
const key = this._key(projectRoot);
|
|
56
|
+
const raw = this._getKv(key);
|
|
57
|
+
if (!raw) return [];
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_save(projectRoot = '', rules = []) {
|
|
67
|
+
this._setKv(this._key(projectRoot), JSON.stringify(rules));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_key(projectRoot = '') {
|
|
71
|
+
return this.namespace + normalizeRoot(projectRoot || 'global');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_getKv(key) {
|
|
75
|
+
if (this.brain?.getKv) return this.brain.getKv(key);
|
|
76
|
+
try {
|
|
77
|
+
const brain = require('../brain');
|
|
78
|
+
if (brain?.getKv) return brain.getKv(key);
|
|
79
|
+
} catch {}
|
|
80
|
+
return _memoryRules.get(key) || '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_setKv(key, value) {
|
|
84
|
+
if (this.brain?.setKv) return this.brain.setKv(key, value);
|
|
85
|
+
try {
|
|
86
|
+
const brain = require('../brain');
|
|
87
|
+
if (brain?.setKv) return brain.setKv(key, value);
|
|
88
|
+
} catch {}
|
|
89
|
+
_memoryRules.set(key, value);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeRoot(projectRoot) {
|
|
95
|
+
if (!projectRoot || projectRoot === 'global') return 'global';
|
|
96
|
+
return path.resolve(projectRoot);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function uniqueRoots(projectRoot) {
|
|
100
|
+
const roots = [normalizeRoot(projectRoot), 'global'];
|
|
101
|
+
return [...new Set(roots)];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clearMemoryRules() {
|
|
105
|
+
_memoryRules.clear();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
PermissionRulesStore,
|
|
110
|
+
clearMemoryRules,
|
|
111
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { checkPermission } = require('../tools/permission-checker');
|
|
6
|
+
const { analyzeShellCommand, initParser: initShellParser } = require('../tools/shell-analyzer');
|
|
7
|
+
const { PermissionRulesStore } = require('./permission-rules-store');
|
|
8
|
+
const { inferPatchPaths } = require('./snapshot-service');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
|
+
const _sharedPending = new Map();
|
|
12
|
+
|
|
13
|
+
class PermissionService {
|
|
14
|
+
constructor({
|
|
15
|
+
events = null,
|
|
16
|
+
rulesStore = null,
|
|
17
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
18
|
+
headlessPolicy = 'reject',
|
|
19
|
+
evaluatePermission = checkPermission,
|
|
20
|
+
} = {}) {
|
|
21
|
+
this.events = events;
|
|
22
|
+
this.rulesStore = rulesStore || new PermissionRulesStore();
|
|
23
|
+
this.timeoutMs = timeoutMs;
|
|
24
|
+
this.headlessPolicy = headlessPolicy;
|
|
25
|
+
this.evaluatePermission = evaluatePermission;
|
|
26
|
+
this.pending = _sharedPending;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async authorize({ sessionId = '', tool, input = {}, cwd = '', projectRoot = '', mode = '', metadata = {}, timeoutMs, headless = false } = {}) {
|
|
30
|
+
const requests = await buildToolPermissionRequests(tool, input, {
|
|
31
|
+
cwd,
|
|
32
|
+
projectRoot: projectRoot || cwd,
|
|
33
|
+
sessionId,
|
|
34
|
+
mode,
|
|
35
|
+
metadata,
|
|
36
|
+
});
|
|
37
|
+
if (requests.length === 0) return { decision: 'allow', source: 'default', reason: 'No permission required' };
|
|
38
|
+
|
|
39
|
+
for (const request of requests) {
|
|
40
|
+
const evaluated = await this.evaluatePermission(request.check);
|
|
41
|
+
if (evaluated.decision === 'allow') continue;
|
|
42
|
+
if (evaluated.decision === 'deny') {
|
|
43
|
+
return { ...evaluated, request };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const always = this.rulesStore.findAlways({
|
|
47
|
+
projectRoot: request.projectRoot,
|
|
48
|
+
permission: request.permission,
|
|
49
|
+
pattern: request.pattern,
|
|
50
|
+
tool: request.tool,
|
|
51
|
+
});
|
|
52
|
+
if (always) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (headless || (timeoutMs ?? this.timeoutMs) === 0) {
|
|
57
|
+
return {
|
|
58
|
+
decision: this.headlessPolicy === 'allow' ? 'allow' : 'deny',
|
|
59
|
+
source: 'headless',
|
|
60
|
+
reason: `Permission required for ${request.permission}/${request.pattern}`,
|
|
61
|
+
request,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const reply = await this.ask({
|
|
66
|
+
...request,
|
|
67
|
+
sessionId,
|
|
68
|
+
metadata: { ...metadata, ...request.metadata, evaluated },
|
|
69
|
+
timeoutMs,
|
|
70
|
+
});
|
|
71
|
+
if (reply.decision !== 'allow') return { ...reply, request };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { decision: 'allow', source: 'permissions', reason: 'Permission checks passed' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
ask({ sessionId = '', permission, pattern = '*', patterns = null, tool = '', metadata = {}, timeoutMs } = {}) {
|
|
78
|
+
if (!permission) throw new Error('permission is required');
|
|
79
|
+
const id = `perm_${crypto.randomUUID().slice(0, 8)}`;
|
|
80
|
+
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
81
|
+
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const timer = effectiveTimeout > 0
|
|
84
|
+
? setTimeout(() => this._expire(id), effectiveTimeout)
|
|
85
|
+
: null;
|
|
86
|
+
const entry = {
|
|
87
|
+
id,
|
|
88
|
+
sessionId,
|
|
89
|
+
permission,
|
|
90
|
+
pattern,
|
|
91
|
+
patterns: patterns || [pattern],
|
|
92
|
+
tool,
|
|
93
|
+
metadata,
|
|
94
|
+
createdAt: Date.now(),
|
|
95
|
+
timer,
|
|
96
|
+
resolve,
|
|
97
|
+
};
|
|
98
|
+
this.pending.set(id, entry);
|
|
99
|
+
this._emit('permission.asked', publicEntry(entry));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
reply({ requestId, reply, message = '' } = {}) {
|
|
104
|
+
const entry = this.pending.get(requestId);
|
|
105
|
+
if (!entry) return false;
|
|
106
|
+
const decision = reply === 'once' || reply === 'always' ? 'allow' : 'deny';
|
|
107
|
+
this._resolveEntry(entry, { decision, reply, message, source: 'user' });
|
|
108
|
+
if (reply === 'always') {
|
|
109
|
+
const rule = this.rulesStore.addAlways({
|
|
110
|
+
projectRoot: entry.metadata.projectRoot || '',
|
|
111
|
+
permission: entry.permission,
|
|
112
|
+
pattern: entry.pattern,
|
|
113
|
+
tool: entry.tool,
|
|
114
|
+
metadata: { message },
|
|
115
|
+
});
|
|
116
|
+
this._resolveCompatibleAlways(entry, rule, message);
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
list({ sessionId = '' } = {}) {
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const entry of this.pending.values()) {
|
|
124
|
+
if (sessionId && entry.sessionId !== sessionId) continue;
|
|
125
|
+
out.push(publicEntry(entry));
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
clear({ sessionId = '' } = {}) {
|
|
131
|
+
for (const entry of [...this.pending.values()]) {
|
|
132
|
+
if (sessionId && entry.sessionId !== sessionId) continue;
|
|
133
|
+
this._resolveEntry(entry, { decision: 'deny', reply: 'reject', message: 'Permission request cleared', source: 'clear' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_resolveCompatibleAlways(sourceEntry, rule, message) {
|
|
138
|
+
for (const entry of [...this.pending.values()]) {
|
|
139
|
+
if (entry.id === sourceEntry.id) continue;
|
|
140
|
+
if (entry.permission !== sourceEntry.permission) continue;
|
|
141
|
+
if (entry.tool !== sourceEntry.tool) continue;
|
|
142
|
+
if ((entry.metadata.projectRoot || '') !== (sourceEntry.metadata.projectRoot || '')) continue;
|
|
143
|
+
if (entry.pattern !== sourceEntry.pattern) continue;
|
|
144
|
+
this._resolveEntry(entry, { decision: 'allow', reply: 'always', message, source: 'always', rule });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_resolveEntry(entry, result) {
|
|
149
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
150
|
+
this.pending.delete(entry.id);
|
|
151
|
+
this._emit('permission.replied', { ...publicEntry(entry), result });
|
|
152
|
+
entry.resolve(result);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_expire(id) {
|
|
156
|
+
const entry = this.pending.get(id);
|
|
157
|
+
if (!entry) return;
|
|
158
|
+
this._resolveEntry(entry, {
|
|
159
|
+
decision: 'deny',
|
|
160
|
+
reply: 'timeout',
|
|
161
|
+
message: 'Permission request timed out',
|
|
162
|
+
source: 'timeout',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_emit(type, payload) {
|
|
167
|
+
if (this.events?.emit) this.events.emit(type, payload);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function buildToolPermissionRequests(tool, input = {}, { cwd = '', projectRoot = '', sessionId = '', mode = '', metadata = {} } = {}) {
|
|
172
|
+
const root = path.resolve(projectRoot || cwd || process.cwd());
|
|
173
|
+
const base = { sessionId, tool, projectRoot: root, metadata: { ...metadata, projectRoot: root } };
|
|
174
|
+
if (!tool) return [];
|
|
175
|
+
|
|
176
|
+
if (tool === 'run_shell') {
|
|
177
|
+
const command = input.command || '';
|
|
178
|
+
await initShellParser();
|
|
179
|
+
const analysis = await analyzeShellCommand(command, input.cwd || cwd || root);
|
|
180
|
+
const commandTokens = analysis.commandTokens.length > 0 ? analysis.commandTokens[0] : undefined;
|
|
181
|
+
const pattern = commandTokens && commandTokens.length > 0 ? `${commandTokens[0]} *` : `${String(command).split(/\s+/)[0] || '*'} *`;
|
|
182
|
+
return [{
|
|
183
|
+
...base,
|
|
184
|
+
permission: 'bash',
|
|
185
|
+
pattern,
|
|
186
|
+
metadata: { ...base.metadata, command, commandTokens },
|
|
187
|
+
check: { tool: 'run_shell', command, commandTokens, projectPath: root, sessionId, mode },
|
|
188
|
+
}];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (tool === 'read_file') {
|
|
192
|
+
const filePath = resolveInputPath(input.file_path || input.path || '', root);
|
|
193
|
+
return [fileRequest({ ...base, permission: 'read', pattern: filePath, filePath, mode })];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (tool === 'write_file' || tool === 'edit_file' || tool === 'multi_edit') {
|
|
197
|
+
const filePath = resolveInputPath(input.file_path || input.path || '', root);
|
|
198
|
+
return [fileRequest({ ...base, tool, permission: 'edit', pattern: filePath, filePath, mode })];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (tool === 'apply_patch') {
|
|
202
|
+
const files = inferPatchPaths(input.patch_text || input.patch || '', root);
|
|
203
|
+
return files.map((filePath) => fileRequest({ ...base, tool, permission: 'edit', pattern: filePath, filePath, mode }));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (['applescript', 'claude_code', 'mail_send', 'slack_send_message'].includes(tool)) {
|
|
207
|
+
return [{
|
|
208
|
+
...base,
|
|
209
|
+
permission: tool,
|
|
210
|
+
pattern: '*',
|
|
211
|
+
check: { tool, command: '', args: [], projectPath: root, sessionId, mode },
|
|
212
|
+
}];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function fileRequest({ sessionId, tool, projectRoot, permission, pattern, filePath, mode, metadata }) {
|
|
219
|
+
const checkTool = permission === 'read' ? 'read_file' : 'edit_file';
|
|
220
|
+
return {
|
|
221
|
+
sessionId,
|
|
222
|
+
tool,
|
|
223
|
+
projectRoot,
|
|
224
|
+
permission,
|
|
225
|
+
pattern,
|
|
226
|
+
metadata: { ...metadata, filePath, projectRoot },
|
|
227
|
+
check: { tool: checkTool, command: filePath, args: [], projectPath: projectRoot, sessionId, mode },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveInputPath(filePath, root) {
|
|
232
|
+
if (!filePath) return '';
|
|
233
|
+
return path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(root, filePath);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function publicEntry(entry) {
|
|
237
|
+
return {
|
|
238
|
+
id: entry.id,
|
|
239
|
+
sessionId: entry.sessionId,
|
|
240
|
+
permission: entry.permission,
|
|
241
|
+
pattern: entry.pattern,
|
|
242
|
+
patterns: entry.patterns,
|
|
243
|
+
tool: entry.tool,
|
|
244
|
+
metadata: entry.metadata,
|
|
245
|
+
createdAt: entry.createdAt,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getDefaultPermissionService() {
|
|
250
|
+
return new PermissionService();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function clearPendingPermissions() {
|
|
254
|
+
for (const entry of _sharedPending.values()) {
|
|
255
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
256
|
+
}
|
|
257
|
+
_sharedPending.clear();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
PermissionService,
|
|
262
|
+
buildToolPermissionRequests,
|
|
263
|
+
getDefaultPermissionService,
|
|
264
|
+
clearPendingPermissions,
|
|
265
|
+
DEFAULT_TIMEOUT_MS,
|
|
266
|
+
};
|