@volter/supercode-teams 0.3.7 → 0.3.9
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/bin/teams.mjs +15 -5
- package/bin/workspace.mjs +6 -0
- package/execution.mjs +15 -3
- package/machine-events.mjs +7 -3
- package/machine.mjs +17 -3
- package/package.json +2 -3
- package/server/node-upgrade.mjs +4 -1
- package/workspaces.mjs +3 -1
- package/board-process-guard.mjs +0 -52
package/bin/teams.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// Every other verb (server, login, context, connect, disconnect, status,
|
|
30
30
|
// members, groups, sources, sync, machines, sessions) is the Teams workspace
|
|
31
31
|
// CLI in ./workspace.mjs.
|
|
32
|
-
import { createWriteStream, readFileSync, writeFileSync } from 'node:fs';
|
|
32
|
+
import { appendFileSync, createWriteStream, readFileSync, writeFileSync } from 'node:fs';
|
|
33
33
|
import { spawnSync } from 'node:child_process';
|
|
34
34
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
35
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
@@ -172,7 +172,9 @@ async function main() {
|
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
174
174
|
if (sub === 'status') { out(readJson(p.machine, { running: false })); return; }
|
|
175
|
-
|
|
175
|
+
// `machine restart [--machine M] [--reason text]`: the daemon (this one, or M's through the server) restarts under its service.
|
|
176
|
+
if (sub === 'restart') return withNode(async (rpc) => out(await rpc.call('machine.restart', { reason: flags.reason ? String(flags.reason) : null })));
|
|
177
|
+
die('machine start|status|restart');
|
|
176
178
|
}
|
|
177
179
|
case 'describe':
|
|
178
180
|
return withNode(async (rpc) => out(await rpc.call('describe')));
|
|
@@ -212,9 +214,17 @@ async function main() {
|
|
|
212
214
|
if (sub === 'stat') return withNode(async (rpc) => out(await rpc.call('files.stat', { path })));
|
|
213
215
|
if (sub === 'search') return withNode(async (rpc) => out(await rpc.call('files.search', { path, query: flags.query ?? '' })));
|
|
214
216
|
if (sub === 'get') return withNode(async (rpc) => {
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
217
|
+
// In ranges: one answer is one frame, so a file of any size is read 4 MiB at a time until it ends.
|
|
218
|
+
const { size } = await rpc.call('files.stat', { path });
|
|
219
|
+
const CHUNK = 4 * 1024 * 1024;
|
|
220
|
+
if (flags.out) writeFileSync(flags.out, Buffer.alloc(0));
|
|
221
|
+
for (let offset = 0; offset < size;) {
|
|
222
|
+
const result = await rpc.call('files.read', { path, offset, length: Math.min(CHUNK, size - offset) });
|
|
223
|
+
const bytes = Buffer.from(result.bytes, 'base64');
|
|
224
|
+
if (!bytes.length) break;
|
|
225
|
+
if (flags.out) appendFileSync(flags.out, bytes); else await new Promise((done) => process.stdout.write(bytes, done));
|
|
226
|
+
offset += bytes.length;
|
|
227
|
+
}
|
|
218
228
|
});
|
|
219
229
|
if (sub === 'put') return withNode(async (rpc) => {
|
|
220
230
|
const bytes = flags.in ? readFileSync(flags.in) : readFileSync(0);
|
package/bin/workspace.mjs
CHANGED
|
@@ -1096,6 +1096,12 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
1096
1096
|
// unreachable with no remote way back (the Mac mini sat unlinked for hours while its connector lived on): a
|
|
1097
1097
|
// daemon that died, a start that half failed or a link its backoff never recovered all end in a fresh start.
|
|
1098
1098
|
// It exits non-zero, which every service manager here restarts.
|
|
1099
|
+
// A service-run connector restarts on request through the machine's door: it exits non-zero, which every service
|
|
1100
|
+
// manager here restarts (the Windows task's minute trigger starts it whatever the exit).
|
|
1101
|
+
if (saved.service) machine.restartHandler = (reason) => {
|
|
1102
|
+
process.stderr.write(`supercode teams: restart requested through the machine door${reason ? ` (${reason})` : ''}\n`);
|
|
1103
|
+
void (async () => { await stopConnector?.(); process.exit(75); })();
|
|
1104
|
+
};
|
|
1099
1105
|
if (saved.service) {
|
|
1100
1106
|
const linkWatch = setInterval(() => {
|
|
1101
1107
|
if (linkReplaced || linkDownSince === null || Date.now() - linkDownSince < LINK_LOST_MS) return;
|
package/execution.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// so. A job ends when its stream closes, when the daemon stops, and, if the daemon died, when
|
|
9
9
|
// the next daemon reaps the record it left.
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { delimiter, join } from 'node:path';
|
|
13
13
|
import { EXECUTION_DOORS, executionDoorFor } from '@volter/supercode-harness-sdk/execution-doors';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
@@ -36,9 +36,21 @@ export function sftpServer() {
|
|
|
36
36
|
return SFTP_SERVERS.find((path) => existsSync(path)) ?? null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/** `program` as this daemon's
|
|
39
|
+
/** `program` as this daemon's own spawn (no shell) can run it from PATH, or null: an executable file, which on
|
|
40
|
+
* Windows is a `.exe` or `.com` (an npm `.cmd` shim or an extensionless sh script there does not start). */
|
|
40
41
|
function onPath(program, env) {
|
|
41
|
-
|
|
42
|
+
const names = process.platform === 'win32' ? [`${program}.exe`, `${program}.com`] : [program];
|
|
43
|
+
for (const dir of (env.PATH ?? env.Path ?? '').split(delimiter)) {
|
|
44
|
+
if (!dir) continue;
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const candidate = join(dir, name);
|
|
47
|
+
try {
|
|
48
|
+
if (!statSync(candidate).isFile()) continue;
|
|
49
|
+
if (process.platform !== 'win32') accessSync(candidate, constants.X_OK);
|
|
50
|
+
return candidate;
|
|
51
|
+
} catch { /* not here, or not runnable */ }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
42
54
|
return null;
|
|
43
55
|
}
|
|
44
56
|
|
package/machine-events.mjs
CHANGED
|
@@ -35,13 +35,17 @@ export class MachineEvents {
|
|
|
35
35
|
if(!response.ok||!response.headers.get('content-type')?.includes('text/event-stream'))throw new Error(`machine events returned HTTP ${response.status}, expected event stream`);
|
|
36
36
|
const reader=response.body.getReader();const decoder=new TextDecoder();let buffer='';
|
|
37
37
|
try{for(;;){const {value,done}=await reader.read();if(done)break;clearTimeout(deadline);deadline=setTimeout(()=>streamAbort.abort(new Error('machine event stream timed out')),45000);buffer+=decoder.decode(value,{stream:true});if(buffer.length>2*1024*1024)throw new Error('machine event exceeded stream bound');let boundary;while((boundary=buffer.indexOf('\n\n'))>=0){const frame=buffer.slice(0,boundary);buffer=buffer.slice(boundary+2);const data=frame.split('\n').filter(line=>line.startsWith('data:')).map(line=>line.slice(5).trimStart()).join('\n');if(data)await this.receive(JSON.parse(data));}}}finally{await reader.cancel().catch(()=>{});}
|
|
38
|
-
}catch(error){if(!this.abort.signal.aborted){if(this.error!==error.message)process.stderr.write(
|
|
38
|
+
}catch(error){if(!this.abort.signal.aborted){this.failingSince??=Date.now();if(this.error!==error.message)process.stderr.write(`${new Date().toISOString()} supercode teams machine events: ${error.message} (cursor ${this.state.cursor})\n`);this.error=error.message;}}
|
|
39
39
|
clearTimeout(deadline);this.connected=false;
|
|
40
40
|
if(!this.abort.signal.aborted)await new Promise(resolve=>{const done=()=>{clearTimeout(timer);this.abort.signal.removeEventListener('abort',done);resolve();};const timer=setTimeout(done,5000);this.abort.signal.addEventListener('abort',done,{once:true});});
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
async receive(event){
|
|
44
|
-
if(event.type==='machine_ready'){
|
|
44
|
+
if(event.type==='machine_ready'){
|
|
45
|
+
// A stream back after a failure says so, with how long it was down: a notice that arrives late is then traceable.
|
|
46
|
+
if(this.failingSince)process.stderr.write(`${new Date().toISOString()} supercode teams machine events: stream back at cursor ${event.cursor} after ${Math.round((Date.now()-this.failingSince)/1000)} s of: ${this.error}\n`);
|
|
47
|
+
this.connected=true;this.error=null;this.failingSince=null;return;
|
|
48
|
+
}
|
|
45
49
|
if(event.type==='access_revoked')throw new Error('machine event subscription authorization revoked');
|
|
46
50
|
if(!['machine_snapshot','machine_linked','machine_unlinked'].includes(event.type))return;
|
|
47
51
|
if(!/^\d+$/.test(event.cursor??''))throw new Error('invalid machine event cursor');
|
|
@@ -74,6 +78,6 @@ export class MachineEvents {
|
|
|
74
78
|
child.stdout.on('data',chunk=>{detail=(detail+chunk).slice(-1000);});child.stderr.on('data',chunk=>{detail=(detail+chunk).slice(-1000);});
|
|
75
79
|
child.once('error',error=>{clearTimeout(timer);reject(error);});child.once('close',code=>{clearTimeout(timer);let receipt;try{receipt=JSON.parse(detail.trim().split('\n').at(-1));}catch{}code===0&&receipt?.code===0?resolve():reject(new Error(receipt?.text||detail||`exit ${code}`));});
|
|
76
80
|
child.stdin.on('error',()=>{});
|
|
77
|
-
child.stdin.end(JSON.stringify({op:'send',from:`sc:${this.machine}:operator:teams-events`,from_name:'Teams machine events',to:item.to,id:item.id,queue:item.to.split(':')[2]==='board',
|
|
81
|
+
child.stdin.end(JSON.stringify({op:'send',from:`sc:${this.machine}:operator:teams-events`,from_name:'Teams machine events',to:item.to,id:item.id,queue:item.to.split(':')[2]==='board',notice:true,body:item.body}));
|
|
78
82
|
});}
|
|
79
83
|
}
|
package/machine.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { EventEmitter } from 'node:events';
|
|
|
30
30
|
import { appendFileSync, readFileSync, writeFileSync, writeSync, statSync, readdirSync, mkdirSync, renameSync, rmSync, existsSync, openSync, readSync, closeSync, accessSync, constants } from 'node:fs';
|
|
31
31
|
import { join, resolve, dirname } from 'node:path';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
|
-
import { boardGuardArguments } from '
|
|
33
|
+
import { boardGuardArguments } from '@volter/teams/board-process-guard';
|
|
34
34
|
import { homedir, hostname } from 'node:os';
|
|
35
35
|
import { Workspaces, inspectCheckout } from './workspaces.mjs';
|
|
36
36
|
import { WorkspaceContexts, fromContext } from '@volter/teams/context';
|
|
@@ -197,6 +197,8 @@ export class TeamsMachine extends MachineCore {
|
|
|
197
197
|
this.onTurn = onTurn;
|
|
198
198
|
this.onTurnWithdrawn = onTurnWithdrawn;
|
|
199
199
|
this.tmuxSocket = tmuxSocket ?? null;
|
|
200
|
+
// Set by a service-run connector: how `machine.restart` ends this daemon so its service manager starts it again.
|
|
201
|
+
this.restartHandler = null;
|
|
200
202
|
this.launches = new Launches(this.p.launches);
|
|
201
203
|
// Windows has no tmux: its panes live in the machine's pane server, a process of their own that outlives this
|
|
202
204
|
// daemon (a restart or an upgrade never ends a pane), as tmux panes live in tmux's server.
|
|
@@ -1215,6 +1217,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1215
1217
|
execution: executionCapabilities(this.env),
|
|
1216
1218
|
boardProcessPolicy: process.platform === 'win32' ? null : { version: 1, harnesses: ['claude-code','codex'], boundary: 'native-PreToolUse' },
|
|
1217
1219
|
runSpec: this.env.SUPERCODE_TEAMS_RUN_SPEC_SHA256 ?? null,
|
|
1220
|
+
restart: Boolean(this.restartHandler),
|
|
1218
1221
|
at: Date.now(),
|
|
1219
1222
|
};
|
|
1220
1223
|
return { ...body, signature: signBytes(this.identity.privateKeyPem, Buffer.from(canonical(body))) };
|
|
@@ -1695,6 +1698,17 @@ export class TeamsMachine extends MachineCore {
|
|
|
1695
1698
|
case 'harness.v1.machine.describe':
|
|
1696
1699
|
case 'describe':
|
|
1697
1700
|
return this.describe();
|
|
1701
|
+
case 'harness.v1.machine.restart':
|
|
1702
|
+
case 'machine.restart': {
|
|
1703
|
+
// A stale daemon (old code, a module it failed to load) is restarted from any machine its operator reaches,
|
|
1704
|
+
// never needing a person at it. Only a daemon its service manager brings back may go: one run by hand would not return.
|
|
1705
|
+
if (!operator) { this.log.append({ principal, cap: method, target: '*', outcome: 'refused' }); throw Object.assign(new Error('only an operator restarts the machine daemon'), { code: 403 }); }
|
|
1706
|
+
if (!this.restartHandler) throw Object.assign(new Error('this daemon runs without a service manager, so nothing would start it again; restart it where it runs'), { code: 'unsupported' });
|
|
1707
|
+
act(method, '*', 'ok', params.reason ? String(params.reason).slice(0, 200) : null);
|
|
1708
|
+
// The answer goes out first; the service manager starts the next daemon.
|
|
1709
|
+
setTimeout(() => this.restartHandler(params.reason ? String(params.reason) : null), 500).unref?.();
|
|
1710
|
+
return { restarting: true, pid: process.pid };
|
|
1711
|
+
}
|
|
1698
1712
|
case 'panes.present': {
|
|
1699
1713
|
// No harness callback: discovery may be the harness's own caller.
|
|
1700
1714
|
const whole = operator || holds(admitted, 'view', '*');
|
|
@@ -2292,8 +2306,8 @@ async function filesOp(method, params, to) {
|
|
|
2292
2306
|
case 'files.read': {
|
|
2293
2307
|
const fd = openSync(path, 'r');
|
|
2294
2308
|
try {
|
|
2295
|
-
// One read is at most
|
|
2296
|
-
const length = Math.min(Number(params.length) || statSync(path).size,
|
|
2309
|
+
// One read is at most 4 MiB, so its base64 answer fits the channel's 8 MiB frame; a caller asks again for more.
|
|
2310
|
+
const length = Math.min(Number(params.length) || statSync(path).size, 4 * 1024 * 1024);
|
|
2297
2311
|
const buffer = Buffer.alloc(length);
|
|
2298
2312
|
const read = readSync(fd, buffer, 0, length, params.offset ?? 0);
|
|
2299
2313
|
return { bytes: buffer.subarray(0, read).toString('base64'), read };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"./event-client": "./event-client.mjs",
|
|
14
14
|
"./bin": "./bin/teams.mjs",
|
|
15
15
|
"./fleet": "./fleet.mjs",
|
|
16
|
-
"./board-process-guard": "./board-process-guard.mjs",
|
|
17
16
|
"./bin/fleet": "./bin/fleet.mjs",
|
|
18
17
|
"./server": {
|
|
19
18
|
"types": "./server/index.d.ts",
|
|
@@ -67,7 +66,7 @@
|
|
|
67
66
|
"@volter/supercode-health": "^0.1.0",
|
|
68
67
|
"@volter/supercode-terminal": "^0.2.54",
|
|
69
68
|
"@volter/supercode-trace": "^0.1.0",
|
|
70
|
-
"@volter/teams": "0.2.
|
|
69
|
+
"@volter/teams": "0.2.6",
|
|
71
70
|
"@xterm/addon-serialize": "^0.14.0",
|
|
72
71
|
"@xterm/headless": "^6.0.0",
|
|
73
72
|
"protobufjs": "8.8.0",
|
package/server/node-upgrade.mjs
CHANGED
|
@@ -22,11 +22,14 @@ export function createMachineUpgradeHandler(api, { authenticate }) {
|
|
|
22
22
|
socket.end(`HTTP/1.1 ${status} ${code}\r\nContent-Type: application/json\r\nContent-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`);
|
|
23
23
|
};
|
|
24
24
|
const handler = async (req, socket, head) => {
|
|
25
|
+
// A caller that drops its connection mid-handshake (or later) ends only its own socket: an unheard socket error
|
|
26
|
+
// would end the whole server (measured: a client's ECONNRESET during authorization stopped the Node host).
|
|
27
|
+
socket.on('error', () => socket.destroy());
|
|
25
28
|
try {
|
|
26
29
|
const decision = await authorizeMachineUpgrade(api, { authenticate, url: req.url, headers: req.headers });
|
|
27
30
|
if (decision.status) return refuse(socket, decision.status, decision.code, decision.message);
|
|
28
31
|
return wss.handleUpgrade(req, socket, head, (ws) => {
|
|
29
|
-
ws.alive = true; ws.on('pong', () => { ws.alive = true; });
|
|
32
|
+
ws.alive = true; ws.on('pong', () => { ws.alive = true; }); ws.on('error', () => ws.terminate());
|
|
30
33
|
void acceptMachineSocket(api, decision, createWebSocketStream(ws), (reason) => ws.close(1000, String(reason ?? 'closed').slice(0, 100)));
|
|
31
34
|
});
|
|
32
35
|
} catch (error) {
|
package/workspaces.mjs
CHANGED
|
@@ -20,7 +20,9 @@ export function repositoryId(value) {
|
|
|
20
20
|
export function inspectCheckout(path) {
|
|
21
21
|
const raw = git(path, ['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-dir', '--git-common-dir']);
|
|
22
22
|
const [root, dir, common] = raw.split('\n').map(p => realpathSync(resolve(path, p)));
|
|
23
|
-
|
|
23
|
+
// The repository the checkout declares (remote.origin.url), not where a URL rewrite (insteadOf: a mirror, a proxy, a
|
|
24
|
+
// World's twin) sends its transport.
|
|
25
|
+
const remote = git(root, ['config', '--get', 'remote.origin.url'], true);
|
|
24
26
|
return { path: realpathSync(path), root, linked: dir !== common, repository: remote ? repositoryId(remote) : null, branch: git(root, ['branch', '--show-current']) || null };
|
|
25
27
|
}
|
|
26
28
|
export class Workspaces {
|
package/board-process-guard.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// The board's native PreToolUse command policy. No process lookup or signal occurs here.
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
6
|
-
export const processGuardReason = 'Pattern kills are refused for board sessions. Kill only the PID this session recorded when it started that process; use the owning service door for a service.';
|
|
7
|
-
export function patternKill(command) {
|
|
8
|
-
// Inspect shell bodies as well as nested `sh -c` / substitutions. Removing shell
|
|
9
|
-
// escapes and quotes catches split words, paths and quoted embedded commands.
|
|
10
|
-
const text = String(command).replace(/\\\r?\n/g, '').replace(/\\([a-zA-Z-])/g, '$1').replace(/["']/g, '');
|
|
11
|
-
const head = String.raw`(?:^|[;|&(\n{}]|\s-[a-zA-Z]*c\s+)\s*(?:(?:env|sudo|exec|command|xargs|nohup|do|then|else|!)\s+(?:--?[^\s;|&()]+\s+)*|[A-Za-z_][A-Za-z0-9_]*=[^\s;|&()]+\s+)*(?:[\w./-]+/)?`;
|
|
12
|
-
return new RegExp(head + String.raw`(?:pkill|killall)(?=$|[\s;|&()])`).test(text) ||
|
|
13
|
-
new RegExp(head + String.raw`pgrep\s+[^;|&\n)]*(?:--full\b|-[a-zA-Z]*f[a-zA-Z]*(?=$|\s))`).test(text);
|
|
14
|
-
}
|
|
15
|
-
const quote = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
16
|
-
export function boardGuardArguments(kind, args, { cwd, enabled, node = process.execPath } = {}) {
|
|
17
|
-
if (!enabled) return args;
|
|
18
|
-
const command = `${quote(node)} ${quote(fileURLToPath(import.meta.url))}`;
|
|
19
|
-
const hooks = [{ matcher: '^Bash$', hooks: [{ type: 'command', command, timeout: 10 }] }];
|
|
20
|
-
if (kind === 'claude-code') {
|
|
21
|
-
const next = [...args], at = next.findIndex(arg => arg === '--settings' || arg.startsWith('--settings='));
|
|
22
|
-
if (at < 0) return [...next, '--settings', JSON.stringify({ hooks: { PreToolUse: hooks } })];
|
|
23
|
-
const inline = next[at].startsWith('--settings='), value = inline ? next[at].slice(11) : next[at + 1];
|
|
24
|
-
const settings = JSON.parse(value?.trimStart().startsWith('{') ? value : readFileSync(resolve(cwd || process.cwd(), value), 'utf8'));
|
|
25
|
-
settings.hooks ??= {}; settings.hooks.PreToolUse = [...(settings.hooks.PreToolUse ?? []), ...hooks];
|
|
26
|
-
next.splice(at, inline ? 1 : 2, '--settings', JSON.stringify(settings));
|
|
27
|
-
return next;
|
|
28
|
-
}
|
|
29
|
-
if (kind === 'codex') {
|
|
30
|
-
// Native Codex rules cannot split substitutions such as kill $(pgrep -f …).
|
|
31
|
-
// Its synchronous Bash hook can deny the entire command before execution.
|
|
32
|
-
// This invocation enables and trusts the board-vetted command policy.
|
|
33
|
-
const handler = `{ matcher = "^Bash$", hooks = [{ type = "command", command = ${JSON.stringify(command)}, timeout = 10 }] }`;
|
|
34
|
-
// Codex's own flags go before any subcommand (`app-server`, `resume`) or prompt: after `app-server`, Codex
|
|
35
|
-
// 0.159 refuses --dangerously-bypass-hook-trust and the worker never starts.
|
|
36
|
-
return [...(args.includes('--dangerously-bypass-hook-trust') ? [] : ['--dangerously-bypass-hook-trust']), '-c', 'features.hooks=true', '-c', `hooks.PreToolUse=[${handler}]`, ...args];
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`board process guard is unavailable for ${kind}; no unguarded session was launched`);
|
|
39
|
-
}
|
|
40
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
41
|
-
try {
|
|
42
|
-
const input = JSON.parse(readFileSync(0, 'utf8'));
|
|
43
|
-
const command = input.tool_input?.command ?? input.tool_input?.cmd;
|
|
44
|
-
if (typeof command !== 'string') throw new Error('Bash hook input has no command');
|
|
45
|
-
if (patternKill(command)) process.stdout.write(JSON.stringify({ hookSpecificOutput: {
|
|
46
|
-
hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: processGuardReason,
|
|
47
|
-
} }) + '\n');
|
|
48
|
-
} catch (error) {
|
|
49
|
-
process.stderr.write(`Board process policy could not read the command: ${error.message}. ${processGuardReason}\n`);
|
|
50
|
-
process.exitCode = 2;
|
|
51
|
-
}
|
|
52
|
-
}
|