@farmslot/agent-runtime 0.21.2 → 0.22.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 (36) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/native/codex.d.ts.map +1 -1
  7. package/dist/native/codex.js +9 -1
  8. package/dist/native/codex.js.map +1 -1
  9. package/dist/native/manager.d.ts.map +1 -1
  10. package/dist/native/manager.js +14 -5
  11. package/dist/native/manager.js.map +1 -1
  12. package/dist/native/registry.d.ts +11 -0
  13. package/dist/native/registry.d.ts.map +1 -1
  14. package/dist/native/registry.js +8 -0
  15. package/dist/native/registry.js.map +1 -1
  16. package/dist/native/review-sandbox.d.ts +1 -1
  17. package/dist/native/review-sandbox.d.ts.map +1 -1
  18. package/dist/native/review-sandbox.js.map +1 -1
  19. package/dist/task-init/branch-update.d.ts +50 -0
  20. package/dist/task-init/branch-update.d.ts.map +1 -0
  21. package/dist/task-init/branch-update.js +728 -0
  22. package/dist/task-init/branch-update.js.map +1 -0
  23. package/dist/task-init/index.d.ts +1 -0
  24. package/dist/task-init/index.d.ts.map +1 -1
  25. package/dist/task-init/index.js +5 -2
  26. package/dist/task-init/index.js.map +1 -1
  27. package/dist/task-init/task-document.d.ts +3 -1
  28. package/dist/task-init/task-document.d.ts.map +1 -1
  29. package/dist/task-init/task-document.js +44 -4
  30. package/dist/task-init/task-document.js.map +1 -1
  31. package/package.json +7 -5
  32. package/scripts/branch-update.mjs +35 -0
  33. package/scripts/review-filesystem.cjs +22 -2
  34. package/scripts/review-terminal.cjs +15 -5
  35. package/scripts/toml-scan.cjs +122 -0
  36. package/scripts/workspace-trust.cjs +280 -0
@@ -0,0 +1,280 @@
1
+ // Runner-native trust stores. Callers admit only farm-created worker workspaces.
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const os = require('node:os');
5
+ const { execFileSync } = require('node:child_process');
6
+ const { tomlSectionHeaderIndexes, tomlBareStringValue } = require('./toml-scan.cjs');
7
+
8
+ const lockScript = String.raw`
9
+ import fcntl, os, subprocess, sys
10
+ with os.fdopen(os.open(sys.argv[1] + '.farmslot-review.lock', os.O_CREAT | os.O_RDWR, 0o600), 'a') as lock:
11
+ fcntl.flock(lock, fcntl.LOCK_EX)
12
+ subprocess.run(sys.argv[2:], check=True, timeout=8)
13
+ `;
14
+ const escapeToml = (value) => JSON.stringify(value).slice(1, -1);
15
+ const unescapeToml = (value) =>
16
+ value.replace(/\\(u([0-9a-fA-F]{4})|U([0-9a-fA-F]{8})|.)/g, (match, body, u4, u8) => {
17
+ if (u4 || u8) return String.fromCodePoint(parseInt(u4 || u8, 16));
18
+ return { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' }[body] ?? match;
19
+ });
20
+
21
+ function updateToml(content, namespace, folders, values) {
22
+ const header = new RegExp(
23
+ '^\\s*\\[\\s*' + namespace + '\\s*\\.\\s*"((?:[^"\\\\]|\\\\.)*)"\\s*\\]\\s*(?:#.*)?$',
24
+ );
25
+ const added = [];
26
+ for (const folder of folders) {
27
+ const lines = content.split(/(?<=\n)/);
28
+ const headers = [...tomlSectionHeaderIndexes(lines)];
29
+ const start = headers.find((index) => {
30
+ const match = header.exec(lines[index].trimEnd());
31
+ return match && unescapeToml(match[1]) === folder;
32
+ });
33
+ if (start === undefined) {
34
+ added.push(folder);
35
+ content =
36
+ content.trimEnd() +
37
+ '\n\n[' +
38
+ namespace +
39
+ '."' +
40
+ escapeToml(folder) +
41
+ '"]\n' +
42
+ Object.entries(values)
43
+ .map(([key, value]) => key + ' = ' + JSON.stringify(value) + '\n')
44
+ .join('');
45
+ continue;
46
+ }
47
+ const end = headers.find((index) => index > start) ?? lines.length;
48
+ const record = lines.slice(start, end);
49
+ for (const [key, value] of Object.entries(values)) {
50
+ const assignment = new RegExp('^\\s*' + key + '\\s*=');
51
+ const index = record.findIndex((line) => assignment.test(line));
52
+ // Grok's original decision timestamp stays stable across launch/replay.
53
+ if (
54
+ index >= 0 &&
55
+ (key === 'decided_at' || tomlBareStringValue(record[index], key) === String(value))
56
+ )
57
+ continue;
58
+ const next = key + ' = ' + JSON.stringify(value) + '\n';
59
+ if (index >= 0) record[index] = next;
60
+ else record.splice(1, 0, next);
61
+ }
62
+ content = lines.slice(0, start).concat(record, lines.slice(end)).join('');
63
+ }
64
+ return { content: content.replace(/^\n+/, ''), added };
65
+ }
66
+
67
+ function writeTrust(file, request) {
68
+ const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
69
+ let next;
70
+ let added = [];
71
+ if (request.namespace) {
72
+ const result = updateToml(content, request.namespace, request.folders, request.values);
73
+ next = result.content;
74
+ added = result.added;
75
+ } else {
76
+ let data;
77
+ try {
78
+ data = content ? JSON.parse(content) : {};
79
+ } catch {
80
+ throw new Error('Invalid runner configuration JSON');
81
+ }
82
+ if (
83
+ !data ||
84
+ typeof data !== 'object' ||
85
+ Array.isArray(data) ||
86
+ (data.projects && (typeof data.projects !== 'object' || Array.isArray(data.projects)))
87
+ )
88
+ throw new Error('Invalid runner configuration');
89
+ data.projects ??= {};
90
+ for (const folder of request.folders) {
91
+ const record = data.projects[folder] ?? {};
92
+ if (!record || typeof record !== 'object' || Array.isArray(record))
93
+ throw new Error('Invalid runner workspace configuration');
94
+ data.projects[folder] = { ...record, ...request.values };
95
+ }
96
+ next = JSON.stringify(data, null, 2) + '\n';
97
+ }
98
+ if (next === content) return added;
99
+ const temporary = file + '.' + process.pid + '.tmp';
100
+ try {
101
+ fs.writeFileSync(temporary, next, { mode: 0o600, flag: 'wx' });
102
+ fs.renameSync(temporary, file);
103
+ } finally {
104
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
105
+ }
106
+ return added;
107
+ }
108
+
109
+ function seed(file, folders, values, namespace, env) {
110
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
111
+ const target = fs.existsSync(file) ? fs.realpathSync(file) : file;
112
+ return JSON.parse(
113
+ execFileSync(
114
+ 'python3',
115
+ [
116
+ '-c',
117
+ lockScript,
118
+ target,
119
+ process.execPath,
120
+ __filename,
121
+ target,
122
+ JSON.stringify({ folders: [...new Set(folders)], values, namespace }),
123
+ ],
124
+ { env, stdio: 'pipe', timeout: 10000, encoding: 'utf8' },
125
+ ),
126
+ );
127
+ }
128
+ const home = (env) => env.HOME || os.homedir();
129
+ function trustGrokFolders(folders, env = process.env) {
130
+ const file = path.join(env.GROK_HOME || path.join(home(env), '.grok'), 'trusted_folders.toml');
131
+ const added = seed(
132
+ file,
133
+ folders,
134
+ { trusted: true, decided_at: Math.floor(Date.now() / 1000) },
135
+ 'folders',
136
+ env,
137
+ );
138
+ return { trustedPath: file, added };
139
+ }
140
+ function farmReviewWorkspace(cwd, owner) {
141
+ const source = fs.realpathSync(cwd);
142
+ const workspace = path.dirname(source);
143
+ const runs = path.dirname(workspace);
144
+ const ownerRoot = path.dirname(runs);
145
+ if (
146
+ path.basename(source) !== 'source' ||
147
+ path.basename(runs) !== 'runs' ||
148
+ path.basename(path.dirname(ownerRoot)) !== 'review-workspaces'
149
+ )
150
+ return false;
151
+ const marker = path.join(workspace, 'identity.json');
152
+ const stat = fs.lstatSync(marker);
153
+ if (
154
+ !stat.isFile() ||
155
+ stat.isSymbolicLink() ||
156
+ stat.uid !== process.getuid?.() ||
157
+ (stat.mode & 0o077) !== 0
158
+ )
159
+ throw new Error('Review workspace ownership marker is not private');
160
+ const identity = JSON.parse(fs.readFileSync(marker, 'utf8'));
161
+ if (
162
+ typeof identity.owner !== 'string' ||
163
+ identity.workspaceId !== path.basename(workspace) ||
164
+ (owner && identity.owner !== owner) ||
165
+ require('node:crypto').createHash('sha256').update(identity.owner).digest('hex') !==
166
+ path.basename(ownerRoot)
167
+ )
168
+ throw new Error('Review workspace ownership marker does not match this launch');
169
+ return true;
170
+ }
171
+ function assertGrokReviewAccount(cwd, env, launch = {}) {
172
+ const remedy =
173
+ 'Set allow_managed_hooks_only = true in the selected GROK_HOME/managed_config.toml, ' +
174
+ 'disable or remove native, plugin and compatibility hooks, and verify grok inspect --json ' +
175
+ 'reports nonManagedHooks disabled and every hook disabled; then retry. Use another runner if enforced hooks cannot be disabled.';
176
+ const executable = launch.executable || 'grok';
177
+ // Account discovery must not confuse intentionally masked PR configuration with unreadable account hooks.
178
+ const inspectionCwd = launch.inspectionCwd || cwd;
179
+ const args = ['--no-leader', '--no-auto-update', '--cwd', inspectionCwd, 'inspect', '--json'];
180
+ let inspection;
181
+ try {
182
+ inspection = JSON.parse(
183
+ execFileSync(
184
+ launch.processSandbox?.executable || executable,
185
+ launch.processSandbox ? [...launch.processSandbox.args, executable, ...args] : args,
186
+ { cwd: inspectionCwd, env, encoding: 'utf8', stdio: 'pipe', timeout: 10000 },
187
+ ),
188
+ );
189
+ } catch {
190
+ // Inspector output can include account secrets. Refuse without echoing stdout/stderr.
191
+ throw new Error('Grok review refused: account hook safety could not be verified. ' + remedy);
192
+ }
193
+ if (
194
+ !inspection ||
195
+ typeof inspection.grokVersion !== 'string' ||
196
+ !Array.isArray(inspection.hooks) ||
197
+ (inspection.hookLoadErrors !== undefined &&
198
+ (!Array.isArray(inspection.hookLoadErrors) || inspection.hookLoadErrors.length > 0))
199
+ )
200
+ throw new Error('Grok review refused: account hook safety could not be verified. ' + remedy);
201
+ // Do not classify commands as safe: a global hook can invoke any PR-supplied file.
202
+ if (inspection.hooks.some((hook) => hook?.disabled !== true))
203
+ throw new Error('Grok review refused: this account has startup hooks enabled. ' + remedy);
204
+ // The native pin also covers agent/frontmatter and notification hooks loaded after inspection.
205
+ if (
206
+ !Array.isArray(inspection.permissions?.enforced) ||
207
+ !inspection.permissions.enforced.some(
208
+ (pin) => pin?.setting === 'nonManagedHooks' && pin.enabled === false,
209
+ )
210
+ )
211
+ throw new Error(
212
+ 'Grok review refused: account startup hooks are not disabled by policy. ' + remedy,
213
+ );
214
+ }
215
+ function trustGrokWorkspace(cwd, env = process.env, owner, launch) {
216
+ if (!farmReviewWorkspace(cwd, owner)) return;
217
+ assertGrokReviewAccount(cwd, env, launch);
218
+ trustGrokFolders([path.resolve(cwd), fs.realpathSync(cwd)], env);
219
+ }
220
+ function configureCodexReviewWorkspace(cwd, env = process.env, owner) {
221
+ if (!farmReviewWorkspace(cwd, owner)) return;
222
+ seed(
223
+ path.join(env.CODEX_HOME || path.join(home(env), '.codex'), 'config.toml'),
224
+ [path.resolve(cwd), fs.realpathSync(cwd)],
225
+ { trust_level: 'untrusted' },
226
+ 'projects',
227
+ env,
228
+ );
229
+ }
230
+ function trustClaudeWorkspace(cwd, env = process.env, owner) {
231
+ if (!farmReviewWorkspace(cwd, owner)) return;
232
+ seed(
233
+ path.join(env.CLAUDE_CONFIG_DIR || home(env), '.claude.json'),
234
+ [path.resolve(cwd), fs.realpathSync(cwd)],
235
+ { hasTrustDialogAccepted: true },
236
+ undefined,
237
+ env,
238
+ );
239
+ }
240
+ function codexWorkspaceTrustConfig(cwd, trust) {
241
+ return `projects={${JSON.stringify(cwd)}={trust_level=${JSON.stringify(trust)}}}`;
242
+ }
243
+ const reviewConfigurationRoots = (cwd) =>
244
+ [
245
+ '.grok/config.toml',
246
+ '.grok/hooks',
247
+ '.grok/lsp.json',
248
+ '.grok/plugins',
249
+ '.grok/agents',
250
+ '.claude/settings.json',
251
+ '.claude/settings.local.json',
252
+ '.claude/plugins',
253
+ '.cursor/hooks.json',
254
+ '.cursor/mcp.json',
255
+ '.cursor/plugins',
256
+ '.mcp.json',
257
+ '.lsp.json',
258
+ '.envrc',
259
+ ].map((name) => path.join(fs.realpathSync(cwd), name));
260
+ const reviewConfigurationRootsByRunner = {
261
+ grok: reviewConfigurationRoots,
262
+ claude: reviewConfigurationRoots,
263
+ cursor: reviewConfigurationRoots,
264
+ };
265
+
266
+ module.exports = {
267
+ reviewConfigurationRootsByRunner,
268
+ codexWorkspaceTrustConfig,
269
+ trustGrokFolders,
270
+ trustGrokWorkspace,
271
+ configureCodexReviewWorkspace,
272
+ workspaceTrustByRunner: {
273
+ codex: configureCodexReviewWorkspace,
274
+ grok: trustGrokWorkspace,
275
+ claude: trustClaudeWorkspace,
276
+ },
277
+ trustClaudeWorkspace,
278
+ };
279
+ if (require.main === module)
280
+ console.log(JSON.stringify(writeTrust(process.argv[2], JSON.parse(process.argv[3]))));