reslop 0.3.0 → 0.3.2
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/CHANGELOG.md +29 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +166 -370
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -13
- package/lib/agents/models.js +250 -376
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/deps.js +29 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/git.js +1 -1
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +137 -31
- package/lib/input/actions.js +83 -2
- package/lib/input/editor.js +9 -0
- package/lib/input/keys.js +39 -3
- package/lib/input/line-edit.js +8 -3
- package/lib/render/agents.js +64 -36
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -4
- package/lib/render/frame.js +16 -2
- package/lib/render/log.js +5 -4
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/render.js +4 -7
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +13 -8
- package/lib/review/format.js +0 -6
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +227 -194
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -2
- package/lib/session/compose.js +20 -6
- package/lib/session/dispatch.js +43 -3
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/import.js +12 -1
- package/lib/session/lifecycle.js +70 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +3 -1
- package/lib/session/npm/npm.js +111 -13
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +17 -2
- package/lib/session/tasks.js +1 -1
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/select.js +2 -1
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/agents/allow.js
CHANGED
|
@@ -1,71 +1,160 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
3
|
const path = require('node:path');
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
4
|
+
const { isHashObject } = require('metautil');
|
|
5
|
+
const { trimText } = require('../common/utilities.js');
|
|
8
6
|
|
|
9
|
-
const {
|
|
7
|
+
const { commands: COMMANDS, filters: FILTERS } = require('./allow.json');
|
|
10
8
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
const ALLOWED = new Set([...COMMANDS, ...FILTERS]);
|
|
10
|
+
const READ_KINDS = ['read', 'search', 'think'];
|
|
11
|
+
const WRITE_KINDS = ['edit', 'delete', 'move'];
|
|
12
|
+
const SHELLS = ['sh', 'bash', 'zsh'];
|
|
13
|
+
const HEREDOC = /<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n[ \t]*\2(?=\s|\)|$)/g;
|
|
14
|
+
// Output to the terminal or nowhere is the only redirect that writes nothing
|
|
15
|
+
const QUIET_REDIRECT = /&?\d?>&\d|&?\d?>>?\s*\/dev\/null/g;
|
|
16
|
+
const SEPARATOR = /&&|\|\||\$\(|[;|&\n()`]/;
|
|
17
|
+
const QUOTE = String.fromCharCode(39);
|
|
18
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/;
|
|
19
|
+
|
|
20
|
+
const shellScript = (argv) => {
|
|
21
|
+
const parts = argv.map((part) => `${part ?? ''}`);
|
|
22
|
+
const isShell = SHELLS.includes(path.basename(parts[0] ?? ''));
|
|
23
|
+
const flag = parts.findIndex((part) => /^-[a-z]*c$/.test(part));
|
|
24
|
+
if (isShell && flag > 0 && parts[flag + 1]) return parts[flag + 1];
|
|
25
|
+
return parts.join(' ');
|
|
17
26
|
};
|
|
18
27
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (text === null) return {};
|
|
27
|
-
const data = jsonParse(text);
|
|
28
|
-
if (!isHashObject(data)) return null;
|
|
29
|
-
return data;
|
|
28
|
+
const commandOf = (call) => {
|
|
29
|
+
const input = isHashObject(call.rawInput) ? call.rawInput : {};
|
|
30
|
+
const raw = input.command ?? input.cmd;
|
|
31
|
+
if (Array.isArray(raw)) return shellScript(raw);
|
|
32
|
+
if (typeof raw === 'string' && raw.trim()) return raw;
|
|
33
|
+
return trimText(call.title).replace(/^`+|`+$/g, '');
|
|
30
34
|
};
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
// Quoted text is data, except a substitution inside double quotes
|
|
37
|
+
class ShellScan {
|
|
38
|
+
constructor(script) {
|
|
39
|
+
this.script = script;
|
|
40
|
+
this.at = 0;
|
|
41
|
+
this.code = '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
single() {
|
|
45
|
+
const end = this.script.indexOf(QUOTE, this.at + 1);
|
|
46
|
+
this.at = end < 0 ? this.script.length : end + 1;
|
|
47
|
+
this.code += '_';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
substitution(close, open) {
|
|
51
|
+
const end = this.script.indexOf(close, this.at + open.length);
|
|
52
|
+
const stop = end < 0 ? this.script.length : end;
|
|
53
|
+
this.code += ` ${this.script.slice(this.at, stop)}${close} `;
|
|
54
|
+
this.at = stop + 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
double() {
|
|
58
|
+
const { script } = this;
|
|
59
|
+
this.at += 1;
|
|
60
|
+
this.code += '_';
|
|
61
|
+
while (this.at < script.length && script[this.at] !== '"') {
|
|
62
|
+
const char = script[this.at];
|
|
63
|
+
if (char === '\\') this.at += 2;
|
|
64
|
+
else if (char === '`') this.substitution('`', '`');
|
|
65
|
+
else if (script.startsWith('$(', this.at)) this.substitution(')', '$(');
|
|
66
|
+
else this.at += 1;
|
|
67
|
+
}
|
|
68
|
+
this.at += 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
plain(char) {
|
|
72
|
+
const escaped = char === '\\';
|
|
73
|
+
this.code += escaped ? '_' : char;
|
|
74
|
+
this.at += escaped ? 2 : 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
run() {
|
|
78
|
+
const { script } = this;
|
|
79
|
+
while (this.at < script.length) {
|
|
80
|
+
const char = script[this.at];
|
|
81
|
+
if (char === QUOTE) this.single();
|
|
82
|
+
else if (char === '"') this.double();
|
|
83
|
+
else this.plain(char);
|
|
84
|
+
}
|
|
85
|
+
return this.code;
|
|
35
86
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const binaryOf = (segment) => {
|
|
90
|
+
let rest = segment.trim();
|
|
91
|
+
while (ASSIGNMENT.test(rest)) rest = rest.replace(ASSIGNMENT, '');
|
|
92
|
+
const word = rest.split(/\s+/)[0] ?? '';
|
|
93
|
+
return path.basename(word);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// An agent that may run node can run anything, so this is no sandbox: it stops
|
|
97
|
+
// a run from reaching for tools that executing a plan does not need
|
|
98
|
+
const commandAllowed = (command) => {
|
|
99
|
+
const script = `${command ?? ''}`.replace(HEREDOC, ' ');
|
|
100
|
+
const code = new ShellScan(script).run().replace(QUIET_REDIRECT, ' ');
|
|
101
|
+
if (code.includes('>')) return false;
|
|
102
|
+
const segments = code.split(SEPARATOR).map((part) => part.trim());
|
|
103
|
+
const named = segments.filter(Boolean);
|
|
104
|
+
return named.length > 0 && named.every((part) => ALLOWED.has(binaryOf(part)));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const inside = (root, file) => {
|
|
108
|
+
const rel = path.relative(root, path.resolve(root, file));
|
|
109
|
+
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
39
110
|
};
|
|
40
111
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const token = trimText(entry);
|
|
48
|
-
if (!token || allow.includes(token)) continue;
|
|
49
|
-
allow.push(token);
|
|
50
|
-
changed = true;
|
|
112
|
+
const pathsOf = (call) => {
|
|
113
|
+
const listed = Array.isArray(call.locations) ? call.locations : [];
|
|
114
|
+
const paths = [];
|
|
115
|
+
for (const location of listed) {
|
|
116
|
+
const file = isHashObject(location) ? location.path : '';
|
|
117
|
+
if (typeof file === 'string' && file) paths.push(file);
|
|
51
118
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
119
|
+
return paths;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const callAllowed = (call, root) => {
|
|
123
|
+
const kind = `${call.kind ?? ''}`;
|
|
124
|
+
if (READ_KINDS.includes(kind)) return true;
|
|
125
|
+
if (kind === 'execute') return commandAllowed(commandOf(call));
|
|
126
|
+
if (!WRITE_KINDS.includes(kind)) return false;
|
|
127
|
+
const paths = pathsOf(call);
|
|
128
|
+
return paths.length > 0 && paths.every((file) => inside(root, file));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const optionId = (options, kinds) => {
|
|
132
|
+
for (const kind of kinds) {
|
|
133
|
+
const found = options.find((option) => option.kind === kind);
|
|
134
|
+
if (found) return found.optionId;
|
|
60
135
|
}
|
|
136
|
+
return null;
|
|
61
137
|
};
|
|
62
138
|
|
|
63
|
-
|
|
64
|
-
|
|
139
|
+
// A run has nobody to ask: edits in the repository and the commands a plan
|
|
140
|
+
// needs are approved for one call, everything else is refused
|
|
141
|
+
const decidePermission = (params, root) => {
|
|
142
|
+
const call = isHashObject(params.toolCall) ? params.toolCall : {};
|
|
143
|
+
const listed = Array.isArray(params.options) ? params.options : [];
|
|
144
|
+
const options = listed.filter(isHashObject);
|
|
145
|
+
const allowed = callAllowed(call, root);
|
|
146
|
+
const kinds = allowed
|
|
147
|
+
? ['allow_once', 'allow_always']
|
|
148
|
+
: ['reject_once', 'reject_always'];
|
|
149
|
+
const id = optionId(options, kinds);
|
|
150
|
+
if (id === null) return { allowed: false, outcome: { outcome: 'cancelled' } };
|
|
151
|
+
return { allowed, outcome: { outcome: 'selected', optionId: id } };
|
|
152
|
+
};
|
|
65
153
|
|
|
66
154
|
module.exports = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
155
|
+
COMMANDS,
|
|
156
|
+
commandOf,
|
|
157
|
+
commandAllowed,
|
|
158
|
+
callAllowed,
|
|
159
|
+
decidePermission,
|
|
71
160
|
};
|
package/lib/agents/allow.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
2
|
+
"commands": ["npm", "npx", "node", "reslop", "git"],
|
|
3
|
+
"filters": [
|
|
4
|
+
"cd",
|
|
5
|
+
"cat",
|
|
6
|
+
"echo",
|
|
7
|
+
"grep",
|
|
8
|
+
"head",
|
|
9
|
+
"ls",
|
|
10
|
+
"printf",
|
|
11
|
+
"pwd",
|
|
12
|
+
"sort",
|
|
13
|
+
"tail",
|
|
14
|
+
"test",
|
|
15
|
+
"true",
|
|
16
|
+
"uniq",
|
|
17
|
+
"wc"
|
|
15
18
|
]
|
|
16
19
|
}
|