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.
@@ -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
- await runPrivileged("chown", ["-R", owner, t]);
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
- await runPrivileged("cp", ["-rn", `${s}/.`, `${d}/`]);
2642
- await runPrivileged("chown", ["-R", owner, d]);
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");