shadok-ai 0.17.16 → 0.17.18
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/dist/privileged.d.ts +50 -0
- package/dist/privileged.js +55 -0
- package/dist/privileged.js.map +1 -0
- package/dist/server.js +9 -20
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +12 -4
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running privileged account/filesystem commands for the multi-tenant migration
|
|
3
|
+
* and group provisioning (useradd, chown, cp over the data tree). Extracted from
|
|
4
|
+
* server.ts so the timeout behaviour — the one that broke a real migration — is
|
|
5
|
+
* unit-testable with harmless commands.
|
|
6
|
+
*
|
|
7
|
+
* The bug: every privileged command shared ONE fixed 60s timeout. That is ample
|
|
8
|
+
* for an account command (useradd/groupadd), but the migration's CORE step is a
|
|
9
|
+
* recursive `chown -R <commons> <tree>` over the WHOLE instance (workspace +
|
|
10
|
+
* every agent's worktree, `~/.claude/projects`, `~/.shadok-ai`). On a busy
|
|
11
|
+
* instance (measured: 28 running agents) that tree is hundreds of thousands of
|
|
12
|
+
* files and the chown ran past 60s, so the spawn timeout SIGTERM'd it. A killed
|
|
13
|
+
* child exits with a NULL code (`code: null, signal: 'SIGTERM'`), surfacing as
|
|
14
|
+
* `chown exited with code null` — and because the chown is a core step, the
|
|
15
|
+
* migration marker was withheld and the red "isolation did NOT complete" banner
|
|
16
|
+
* stuck. It never self-heals: the tree only grows, so every boot retry dies the
|
|
17
|
+
* same way. The fix gives the unbounded recursive operations a generous timeout
|
|
18
|
+
* (`TREE_OP_TIMEOUT_MS`) while the short account commands keep the 60s default.
|
|
19
|
+
* Safe because the migration is fire-and-forget off the boot path, so a slow run
|
|
20
|
+
* can never hang boot.
|
|
21
|
+
*/
|
|
22
|
+
/** The short account commands (useradd, groupadd, usermod, install, ln…). */
|
|
23
|
+
export declare const DEFAULT_PRIVILEGED_TIMEOUT_MS = 60000;
|
|
24
|
+
/** Recursive operations over the whole data tree (`chown -R`, `cp -r`): bounded
|
|
25
|
+
* only by the instance's size, so they need minutes, not seconds. Generous on
|
|
26
|
+
* purpose — the migration is fire-and-forget, so an over-long ceiling costs a
|
|
27
|
+
* slow migration at worst, never a hung boot. */
|
|
28
|
+
export declare const TREE_OP_TIMEOUT_MS: number;
|
|
29
|
+
/** Decide what actually gets spawned: root runs the command directly; a non-root
|
|
30
|
+
* server goes through NON-interactive sudo (`-n`) so a MISSING right fails fast
|
|
31
|
+
* with a non-zero exit instead of prompting. Pure, so the sudo decision is
|
|
32
|
+
* testable without being root. */
|
|
33
|
+
export declare function privilegedInvocation(cmd: string, argv: string[], isRoot: boolean): {
|
|
34
|
+
cmd: string;
|
|
35
|
+
argv: string[];
|
|
36
|
+
};
|
|
37
|
+
/** Spawn `spawn.cmd spawn.argv` with a timeout, rejecting with `<label> exited
|
|
38
|
+
* with code <code>` on a non-zero/null exit. `label` is the ORIGINAL command
|
|
39
|
+
* name (not `sudo`), so the message names the real failing step; a timeout kill
|
|
40
|
+
* gives `code: null` → "<label> exited with code null". */
|
|
41
|
+
export declare function runWithTimeout(spawn: {
|
|
42
|
+
cmd: string;
|
|
43
|
+
argv: string[];
|
|
44
|
+
}, label: string, timeoutMs: number): Promise<void>;
|
|
45
|
+
/** Run a privileged account/filesystem command. Pass `{ timeoutMs:
|
|
46
|
+
* TREE_OP_TIMEOUT_MS }` for an unbounded recursive operation; otherwise the 60s
|
|
47
|
+
* default applies. */
|
|
48
|
+
export declare function runPrivileged(cmd: string, argv: string[], opts?: {
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawn as spawnChild } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Running privileged account/filesystem commands for the multi-tenant migration
|
|
4
|
+
* and group provisioning (useradd, chown, cp over the data tree). Extracted from
|
|
5
|
+
* server.ts so the timeout behaviour — the one that broke a real migration — is
|
|
6
|
+
* unit-testable with harmless commands.
|
|
7
|
+
*
|
|
8
|
+
* The bug: every privileged command shared ONE fixed 60s timeout. That is ample
|
|
9
|
+
* for an account command (useradd/groupadd), but the migration's CORE step is a
|
|
10
|
+
* recursive `chown -R <commons> <tree>` over the WHOLE instance (workspace +
|
|
11
|
+
* every agent's worktree, `~/.claude/projects`, `~/.shadok-ai`). On a busy
|
|
12
|
+
* instance (measured: 28 running agents) that tree is hundreds of thousands of
|
|
13
|
+
* files and the chown ran past 60s, so the spawn timeout SIGTERM'd it. A killed
|
|
14
|
+
* child exits with a NULL code (`code: null, signal: 'SIGTERM'`), surfacing as
|
|
15
|
+
* `chown exited with code null` — and because the chown is a core step, the
|
|
16
|
+
* migration marker was withheld and the red "isolation did NOT complete" banner
|
|
17
|
+
* stuck. It never self-heals: the tree only grows, so every boot retry dies the
|
|
18
|
+
* same way. The fix gives the unbounded recursive operations a generous timeout
|
|
19
|
+
* (`TREE_OP_TIMEOUT_MS`) while the short account commands keep the 60s default.
|
|
20
|
+
* Safe because the migration is fire-and-forget off the boot path, so a slow run
|
|
21
|
+
* can never hang boot.
|
|
22
|
+
*/
|
|
23
|
+
/** The short account commands (useradd, groupadd, usermod, install, ln…). */
|
|
24
|
+
export const DEFAULT_PRIVILEGED_TIMEOUT_MS = 60_000;
|
|
25
|
+
/** Recursive operations over the whole data tree (`chown -R`, `cp -r`): bounded
|
|
26
|
+
* only by the instance's size, so they need minutes, not seconds. Generous on
|
|
27
|
+
* purpose — the migration is fire-and-forget, so an over-long ceiling costs a
|
|
28
|
+
* slow migration at worst, never a hung boot. */
|
|
29
|
+
export const TREE_OP_TIMEOUT_MS = 20 * 60_000;
|
|
30
|
+
/** Decide what actually gets spawned: root runs the command directly; a non-root
|
|
31
|
+
* server goes through NON-interactive sudo (`-n`) so a MISSING right fails fast
|
|
32
|
+
* with a non-zero exit instead of prompting. Pure, so the sudo decision is
|
|
33
|
+
* testable without being root. */
|
|
34
|
+
export function privilegedInvocation(cmd, argv, isRoot) {
|
|
35
|
+
return isRoot ? { cmd, argv } : { cmd: "sudo", argv: ["-n", cmd, ...argv] };
|
|
36
|
+
}
|
|
37
|
+
/** Spawn `spawn.cmd spawn.argv` with a timeout, rejecting with `<label> exited
|
|
38
|
+
* with code <code>` on a non-zero/null exit. `label` is the ORIGINAL command
|
|
39
|
+
* name (not `sudo`), so the message names the real failing step; a timeout kill
|
|
40
|
+
* gives `code: null` → "<label> exited with code null". */
|
|
41
|
+
export function runWithTimeout(spawn, label, timeoutMs) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const p = spawnChild(spawn.cmd, spawn.argv, { stdio: ["ignore", "inherit", "inherit"], timeout: timeoutMs });
|
|
44
|
+
p.on("error", reject);
|
|
45
|
+
p.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`${label} exited with code ${code}`))));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Run a privileged account/filesystem command. Pass `{ timeoutMs:
|
|
49
|
+
* TREE_OP_TIMEOUT_MS }` for an unbounded recursive operation; otherwise the 60s
|
|
50
|
+
* default applies. */
|
|
51
|
+
export function runPrivileged(cmd, argv, opts) {
|
|
52
|
+
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
53
|
+
return runWithTimeout(privilegedInvocation(cmd, argv, isRoot), cmd, opts?.timeoutMs ?? DEFAULT_PRIVILEGED_TIMEOUT_MS);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=privileged.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privileged.js","sourceRoot":"","sources":["../src/privileged.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC;AAEpD;;;kDAGkD;AAClD,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,MAAM,CAAC;AAE9C;;;mCAGmC;AACnC,MAAM,UAAU,oBAAoB,CAClC,GAAW,EACX,IAAc,EACd,MAAe;IAEf,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED;;;4DAG4D;AAC5D,MAAM,UAAU,cAAc,CAC5B,KAAsC,EACtC,KAAa,EACb,SAAiB;IAEjB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QAC7G,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACtB,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,KAAK,qBAAqB,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5G,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;uBAEuB;AACvB,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,IAAc,EAAE,IAA6B;IACtF,MAAM,MAAM,GAAG,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9E,OAAO,cAAc,CAAC,oBAAoB,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,IAAI,6BAA6B,CAAC,CAAC;AACxH,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -48,6 +48,7 @@ import { cancelPairingCode, issuePairingCode, pairingStatus } from "./tg-pairing
|
|
|
48
48
|
import { migrateTgBindings } from "./channels.js";
|
|
49
49
|
import { viewerFor, canResumeAs, canRestartInto, canSeeGroup, membershipVerdict, canSeeSession, listableGroups, visibleChannels, secretOwnGroupIds, ledgerGroupsFor, groupsPutFlow, privateGroupFor, newPrivateGroup, accountDeleteGroups, MAIN_GROUP_ID, } from "./groups.js";
|
|
50
50
|
import { resolveSpawnIsolation, osUserFor, homeForUser, runAsUser, provisionGroupUser, teardownGroupUser, reconcileGroupProvisioning, DEFAULT_HOME_BASE, createGroupFlow, deleteGroupFlow, SHARED_POT_GROUP, DEFAULT_SHARED_POT_PATH, } from "./osusers.js";
|
|
51
|
+
import { runPrivileged, TREE_OP_TIMEOUT_MS } from "./privileged.js";
|
|
51
52
|
import { instanceKey } from "./paths.js";
|
|
52
53
|
import { peerFileFor, loadPeers, savePeers, peerFromToken, signPeerToken, newPeer, withSalts, redeemVerdict, redeemPeer, normPeerName, removePeer, ownedByPeer, peersOutFileFor, agentInvitePrompt, peerScope, } from "./peers.js";
|
|
53
54
|
import { loadCrons, saveCrons, upsertCron, removeCron, resolveCronId, nextRunFor, nextRunAfterFailure, isTransient, CRON_MAX_RETRIES, markCronPrompt, normalizeSchedule, onceAt, stateAfterFire, scheduleLabel, cronTimeZone, defaultTimeZone, systemTimeZone, isValidTimeZone, } from "./crons.js";
|
|
@@ -2316,23 +2317,6 @@ if (DEPLOYMENT_MODE === "multi-tenant") {
|
|
|
2316
2317
|
(e instanceof Error ? e.message : String(e)));
|
|
2317
2318
|
}
|
|
2318
2319
|
}
|
|
2319
|
-
/** Run a privileged account command. Root runs it directly; otherwise go through
|
|
2320
|
-
* NON-interactive sudo (`-n`) so a MISSING right fails fast with a non-zero exit
|
|
2321
|
-
* instead of prompting — the T3 false positive (sudoers allows `true` but not
|
|
2322
|
-
* `useradd`) surfaces here and `provisionGroupUser` turns it into a refusal. */
|
|
2323
|
-
function runPrivileged(cmd, argv) {
|
|
2324
|
-
return new Promise((resolve, reject) => {
|
|
2325
|
-
let c = cmd;
|
|
2326
|
-
let a = argv;
|
|
2327
|
-
if (typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
2328
|
-
a = ["-n", c, ...a];
|
|
2329
|
-
c = "sudo";
|
|
2330
|
-
}
|
|
2331
|
-
const p = spawnChild(c, a, { stdio: ["ignore", "inherit", "inherit"], timeout: 60_000 });
|
|
2332
|
-
p.on("error", reject);
|
|
2333
|
-
p.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2336
2320
|
/**
|
|
2337
2321
|
* Make a freshly-created worktree (or self-repo clone) usable by the DROP USER.
|
|
2338
2322
|
* The server creates it as ROOT, so the checkout AND git's own worktree admin
|
|
@@ -2617,7 +2601,11 @@ async function migChownTreeToCommons() {
|
|
|
2617
2601
|
for (const t of targets) {
|
|
2618
2602
|
if (!t || !fs.existsSync(t))
|
|
2619
2603
|
continue;
|
|
2620
|
-
|
|
2604
|
+
// Unbounded: the whole workspace (every agent's worktree + node_modules),
|
|
2605
|
+
// `~/.shadok-ai` and `~/.claude`. On a busy instance this is the step that
|
|
2606
|
+
// ran past the old fixed 60s and was SIGTERM'd (`chown exited with code
|
|
2607
|
+
// null`), withholding the migration marker — see `src/privileged.ts`.
|
|
2608
|
+
await runPrivileged("chown", ["-R", owner, t], { timeoutMs: TREE_OP_TIMEOUT_MS });
|
|
2621
2609
|
}
|
|
2622
2610
|
// RELOCATE the commons agent's Claude state into its OWN HOME. Chowning
|
|
2623
2611
|
// ~/.claude alone is useless: the files stay under `/root` (0700), which `main`
|
|
@@ -2638,8 +2626,9 @@ async function migChownTreeToCommons() {
|
|
|
2638
2626
|
continue;
|
|
2639
2627
|
const d = path.join(dstClaude, sub);
|
|
2640
2628
|
await runPrivileged("install", ["-d", "-o", COMMONS_USER, "-g", COMMONS_USER, d]);
|
|
2641
|
-
|
|
2642
|
-
await runPrivileged("
|
|
2629
|
+
// `projects` holds every agent's full transcript history — also unbounded.
|
|
2630
|
+
await runPrivileged("cp", ["-rn", `${s}/.`, `${d}/`], { timeoutMs: TREE_OP_TIMEOUT_MS });
|
|
2631
|
+
await runPrivileged("chown", ["-R", owner, d], { timeoutMs: TREE_OP_TIMEOUT_MS });
|
|
2643
2632
|
}
|
|
2644
2633
|
const hist = path.join(srcClaude, "history.jsonl");
|
|
2645
2634
|
const dhist = path.join(dstClaude, "history.jsonl");
|