@volter/teams 0.2.58 → 0.2.60
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/board-process-guard.mjs +10 -3
- package/home.mjs +21 -0
- package/package.json +2 -1
- package/release-launcher.mjs +50 -0
- package/stable-release.mjs +54 -0
package/board-process-guard.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
20
20
|
import { readFileSync } from 'node:fs';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { resolve } from 'node:path';
|
|
23
|
+
import { currentRelease } from './stable-release.mjs';
|
|
23
24
|
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.';
|
|
24
25
|
export const computedKillReason = `A kill names each process by the PID this session recorded when it started it, written out as a number. A PID taken from a variable, a loop or a command's output is a pattern kill: one over a match that came out empty ended every process of a machine's user. ${processGuardReason}`;
|
|
25
26
|
export const foreignKillReason = (pid) => `PID ${pid} is not a process this session started: it is neither running under this session nor marked by it (its environment names no session of this one). Kill only what this session started, by the PID it recorded; a process another session or the person started is theirs to end, through its owning service door or by asking them.`;
|
|
@@ -192,12 +193,18 @@ export function killDecision(command, { table, session, sessionId, platform = pr
|
|
|
192
193
|
return foreign.length ? { decision: 'deny', reason: foreign.map(foreignKillReason).join(' ') } : null;
|
|
193
194
|
}
|
|
194
195
|
const quote = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
195
|
-
export function boardGuardArguments(kind, args, { cwd, enabled, node = process.execPath } = {}) {
|
|
196
|
+
export function boardGuardArguments(kind, args, { cwd, enabled, node = process.execPath, env = process.env } = {}) {
|
|
196
197
|
if (!enabled) return args;
|
|
197
|
-
|
|
198
|
+
// A session holds this command for as long as it runs, and the release that launched it is removed two installs
|
|
199
|
+
// later: the policy is named through the machine's stable launcher, which runs it out of the release installed when
|
|
200
|
+
// the hook fires (stable-release.mjs). Where the machine's stable supercode runs no front door (a checkout's home,
|
|
201
|
+
// which keeps no releases) there is no launcher, and this file is named.
|
|
202
|
+
const stable = currentRelease({ env, node });
|
|
203
|
+
const [program, ...leading] = stable ? [...stable, '@volter/teams/board-process-guard'] : [node, fileURLToPath(import.meta.url)];
|
|
204
|
+
const command = [program, ...leading].map(quote).join(' ');
|
|
198
205
|
// Claude's exec form spawns the policy with no shell on every platform (Windows included, where its shell form
|
|
199
206
|
// runs Git Bash or PowerShell and the POSIX quoting above would not parse); its PowerShell tool is guarded as Bash is.
|
|
200
|
-
const hooks = [{ matcher: '^(Bash|PowerShell)$', hooks: [{ type: 'command', command:
|
|
207
|
+
const hooks = [{ matcher: '^(Bash|PowerShell)$', hooks: [{ type: 'command', command: program, args: leading, timeout: 10 }] }];
|
|
201
208
|
if (kind === 'claude-code') {
|
|
202
209
|
const next = [...args], at = next.findIndex(arg => arg === '--settings' || arg.startsWith('--settings='));
|
|
203
210
|
if (at < 0) return [...next, '--settings', JSON.stringify({ hooks: { PreToolUse: hooks } })];
|
package/home.mjs
CHANGED
|
@@ -231,6 +231,27 @@ export class Launches {
|
|
|
231
231
|
}
|
|
232
232
|
return [...last.values()].sort((a, b) => b.t - a.t);
|
|
233
233
|
}
|
|
234
|
+
/** No launch rests at `issued` once its handler is gone (its daemon ended between that write and the next; `issuing`
|
|
235
|
+
* holds the keys a live handler still has): with its pane live in `panes` it is ready, and `ready(launch, pane)` is
|
|
236
|
+
* told; with its pane kept dead it ended as `exit(pane)` ({ failed, how }) says; with no pane it exited when its
|
|
237
|
+
* input had been confirmed, and failed otherwise. Answers the records it wrote. */
|
|
238
|
+
settleIssued(panes, { issuing = new Set(), onlyKey = null, exit, ready = () => {} }) {
|
|
239
|
+
const restarted = 'the daemon restarted before this launch became ready';
|
|
240
|
+
const settled = [];
|
|
241
|
+
for (const launch of this.all()) {
|
|
242
|
+
if (launch.status !== 'issued' || (onlyKey && launch.key !== onlyKey) || issuing.has(launch.key)) continue;
|
|
243
|
+
const { t: _previousAt, ...facts } = launch;
|
|
244
|
+
const pane = panes.find((row) => row.contextKey === launch.pane) ?? null;
|
|
245
|
+
const ended = pane?.dead ? exit(pane) : null;
|
|
246
|
+
const entry = pane && !pane.dead ? { ...facts, status: 'ready' }
|
|
247
|
+
: ended?.failed === false ? { ...facts, status: 'finished', completion: ended }
|
|
248
|
+
: ended ? { ...facts, status: 'exited', error: `${restarted}; its pane ended with ${ended.how}` }
|
|
249
|
+
: { ...facts, status: launch.inputDelivered === true ? 'exited' : 'failed', error: `${restarted}; its pane is gone` };
|
|
250
|
+
if (entry.status === 'ready') ready(launch, pane);
|
|
251
|
+
this.record(entry); settled.push(entry);
|
|
252
|
+
}
|
|
253
|
+
return settled;
|
|
254
|
+
}
|
|
234
255
|
}
|
|
235
256
|
|
|
236
257
|
/** `host:port` or `[v6]:port` → { host, port }. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.60",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"./machine-door": "./machine-door.mjs",
|
|
50
50
|
"./sender-identity": "./sender-identity.mjs",
|
|
51
51
|
"./board-process-guard": "./board-process-guard.mjs",
|
|
52
|
+
"./stable-release": "./stable-release.mjs",
|
|
52
53
|
"./server/agent-authority": "./server/agent-authority.mjs",
|
|
53
54
|
"./redact": {
|
|
54
55
|
"default": "./redact.mjs"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The machine's current release, for a command recorded outside it. A session holds the hook commands it was launched
|
|
3
|
+
// with for as long as it runs, and the release that launched it is removed two installs later: a command naming a file
|
|
4
|
+
// inside that release fails from then on. This file is kept at `<SUPERCODE_HOME>/bin/supercode-release.mjs`
|
|
5
|
+
// (stable-release.mjs), beside the machine's stable `supercode` shim, and names no release: it reads the shim each time
|
|
6
|
+
// it runs, and runs out of the release the shim names then.
|
|
7
|
+
//
|
|
8
|
+
// node supercode-release.mjs supercode <args…> that release's front door (the shim itself, for a caller
|
|
9
|
+
// that can start a program but not a `.cmd`)
|
|
10
|
+
// node supercode-release.mjs @volter/<module> <args…> a module of that release, as if started by its own path
|
|
11
|
+
//
|
|
12
|
+
// With no release to run (no shim, a shim that runs no front door, a release without the module) it says so and exits 1.
|
|
13
|
+
// A harness reads that from a hook as an error that blocks nothing, so the line says what did not happen: for the
|
|
14
|
+
// board's process guard, that the command it was asked about runs unguarded.
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const shim = join(dirname(fileURLToPath(import.meta.url)), process.platform === 'win32' ? 'supercode.cmd' : 'supercode');
|
|
21
|
+
|
|
22
|
+
/** The front door the shim runs (`exec '<node>' '<front door>' "$@"`, Windows `@"<node>" "<front door>" %*`), or null. */
|
|
23
|
+
function frontDoor() {
|
|
24
|
+
let text;
|
|
25
|
+
try { text = readFileSync(shim, 'utf8'); } catch { return null; }
|
|
26
|
+
for (const line of text.split(/\r?\n/).map((row) => row.trimEnd())) {
|
|
27
|
+
const parts = line.startsWith("exec '") && line.endsWith("' \"$@\"")
|
|
28
|
+
? line.slice(6, -6).replaceAll("'\\''", '\0').split("' '").map((part) => part.replaceAll('\0', "'"))
|
|
29
|
+
: line.startsWith('@"') && line.endsWith('" %*') ? line.slice(2, -4).split('" "') : null;
|
|
30
|
+
if (parts) return parts.length === 2 && /\.js$/i.test(parts[1]) && existsSync(parts[1]) ? parts[1] : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const [what, ...rest] = process.argv.slice(2);
|
|
36
|
+
const front = frontDoor();
|
|
37
|
+
let target = null;
|
|
38
|
+
if (front && what === 'supercode') target = front;
|
|
39
|
+
else if (front && /^@volter\//.test(what ?? '')) {
|
|
40
|
+
try { target = createRequire(front).resolve(what); } catch { /* not in that release */ }
|
|
41
|
+
}
|
|
42
|
+
if (!target) {
|
|
43
|
+
const why = front ? `the release at ${front} has no such module` : `${shim} names no installed release's front door`;
|
|
44
|
+
process.stderr.write(what === '@volter/teams/board-process-guard'
|
|
45
|
+
? `supercode: the board process guard did NOT run, and this command is running unguarded: ${what} was not run: ${why}.\n`
|
|
46
|
+
: `supercode: ${what ?? 'nothing'} was not run: ${why}.\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
process.argv.splice(1, process.argv.length - 1, target, ...rest);
|
|
50
|
+
await import(pathToFileURL(target).href);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// What a command recorded outside a release names so that it outlives the release: a session's hook commands, held in
|
|
2
|
+
// its launch flags for as long as it runs. The machine's stable `supercode` (`<SUPERCODE_HOME>/bin/supercode`, a shim
|
|
3
|
+
// claude_relay.rs ensure_stable_program points at the installed release at each install and daemon start) is such a
|
|
4
|
+
// name, but a shell script: a hook's exec form cannot start its `.cmd` on Windows, and it runs only the front door.
|
|
5
|
+
// The launcher kept beside it (release-launcher.mjs) is a file Node runs on every platform; it reads the shim when it
|
|
6
|
+
// runs and so finds the release installed then.
|
|
7
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, rmSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { supercodeHome } from './home.mjs';
|
|
11
|
+
|
|
12
|
+
const LAUNCHER = fileURLToPath(new URL('./release-launcher.mjs', import.meta.url));
|
|
13
|
+
|
|
14
|
+
/** Whether the shim at `path` runs a front door that is there: the launcher runs out of that front door's release. */
|
|
15
|
+
function runsFrontDoor(path) {
|
|
16
|
+
let text;
|
|
17
|
+
try { text = readFileSync(path, 'utf8'); } catch { return false; }
|
|
18
|
+
for (const line of text.split(/\r?\n/).map((row) => row.trimEnd())) {
|
|
19
|
+
const parts = line.startsWith("exec '") && line.endsWith("' \"$@\"")
|
|
20
|
+
? line.slice(6, -6).replaceAll("'\\''", '\0').split("' '").map((part) => part.replaceAll('\0', "'"))
|
|
21
|
+
: line.startsWith('@"') && line.endsWith('" %*') ? line.slice(2, -4).split('" "') : null;
|
|
22
|
+
if (parts) return parts.length === 2 && /\.js$/i.test(parts[1]) && parts.every((part) => existsSync(part));
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The start of a recorded command that runs the machine's current release: `[node, launcher]`, followed by `supercode`
|
|
29
|
+
* and a command line for its front door, or by a module of the release (`@volter/teams/board-process-guard`). The
|
|
30
|
+
* launcher is this release's copy, written whole when the one there differs. Null where the machine has no stable
|
|
31
|
+
* front door (a checkout's or a cargo install's home, which keeps no releases and removes none): the caller names its
|
|
32
|
+
* own file there.
|
|
33
|
+
*/
|
|
34
|
+
export function currentRelease({ env = process.env, node = process.execPath, platform = process.platform } = {}) {
|
|
35
|
+
const bin = join(supercodeHome(env), 'bin');
|
|
36
|
+
if (!runsFrontDoor(join(bin, platform === 'win32' ? 'supercode.cmd' : 'supercode'))) return null;
|
|
37
|
+
const stable = join(bin, 'supercode-release.mjs'), staged = `${stable}.${process.pid}.tmp`;
|
|
38
|
+
let current = null;
|
|
39
|
+
try { current = readFileSync(stable); } catch { /* not there yet */ }
|
|
40
|
+
try {
|
|
41
|
+
const launcher = readFileSync(LAUNCHER);
|
|
42
|
+
if (!current?.equals(launcher)) {
|
|
43
|
+
mkdirSync(dirname(stable), { recursive: true });
|
|
44
|
+
writeFileSync(staged, launcher, { mode: 0o755 });
|
|
45
|
+
renameSync(staged, stable);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// the launcher already there is kept and named: another release's copy still finds the installed release, and a
|
|
49
|
+
// path into this one would not outlive it
|
|
50
|
+
rmSync(staged, { force: true });
|
|
51
|
+
if (!current) return null;
|
|
52
|
+
}
|
|
53
|
+
return [node, stable];
|
|
54
|
+
}
|