shadok-ai 0.17.1 → 0.17.2

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/README.md CHANGED
@@ -476,10 +476,16 @@ in to Claude, which happens after boot; the banner stays up until then and the
476
476
  migration **finishes on its own the moment you sign in** (no restart needed).
477
477
  **One-time provisioning:** isolated-group agents authenticate with a
478
478
  subscription token injected into their environment, not a shared credential
479
- file. Run `claude setup-token` once and store the result as the vault secret
480
- `MT_CLAUDE_OAUTH_TOKEN`. Without it, group spawns are refused with a message
481
- naming this step. The token is valid for about a year; re-mint it by hand
482
- (`claude setup-token`, then overwrite the secret) before it expires.
479
+ file. Until it is set, group spawns are refused with a message naming this step,
480
+ and the cockpit shows a red "no subscription token" banner. Provide it from the
481
+ cockpit itself — **no shell needed**: as an admin, click **Provide token** on
482
+ that banner (or **Replace subscription token…** in the version menu to rotate
483
+ it), authorise on your Claude subscription in the browser, and paste the code
484
+ back; the server mints and stores it. From Telegram, `/mttoken` then
485
+ `/mtcode <code>` do the same. The token is captured and vaulted server-side and
486
+ is never shown. It is valid for about a year; re-run the card (or `/mttoken`)
487
+ before it expires. The shell path still works as a fallback: `claude setup-token`
488
+ and store the result as the vault secret `MT_CLAUDE_OAUTH_TOKEN`.
483
489
 
484
490
  Telegram stays single-owner, and there are no
485
491
  disk quotas yet. Design: `docs/superpowers/specs/2026-10-29-multi-tenant-scoped-isolation-design.md`.
package/dist/server.js CHANGED
@@ -20,7 +20,8 @@ import { findTransientErrors, newTransientErrors, RETRY_DELAYS_MS } from "./retr
20
20
  import { backgroundTasks, screenShowsWork, moveToOption } from "./detect.js";
21
21
  import { PtyPilot } from "./session.js";
22
22
  import { ensureClaudeHome, ensureProjectTrusted, getDefaultModel, setDefaultModel, parseClaudeVersion, seedPlan, settingsPlan, } from "./claude-home.js";
23
- import { authStatus, cancelLogin, startLogin, submitLoginCode } from "./claude-auth.js";
23
+ import { authStatus, cancelLogin, invalidateAuthStatus, startLogin, submitLoginCode } from "./claude-auth.js";
24
+ import { startSetupToken, submitSetupTokenCode, cancelSetupToken } from "./setup-token.js";
24
25
  import { readTokenExpiry } from "./claude-creds.js";
25
26
  import { starCount } from "./stars.js";
26
27
  import { announceFirstAgent, countMainChannels, ensureFirstAgent, firstAgentStatus, settleFirstAgent, } from "./first-agent.js";
@@ -290,7 +291,7 @@ async function startFirstAgent() {
290
291
  }
291
292
  function restartTelegram() {
292
293
  tgBridge.stop();
293
- tgBridge = startTelegram(boundPort, tgCookie());
294
+ tgBridge = startTelegram(boundPort, tgCookie(), DEPLOYMENT_MODE === "multi-tenant");
294
295
  }
295
296
  // ── Cron scheduler ───────────────────────────────────────────────────────────
296
297
  // Scheduled per-channel prompts (monitoring/reporting). The server drives the
@@ -1641,7 +1642,19 @@ app.get("/auth", async (_req, res) => res.json({
1641
1642
  ...readTokenExpiry(),
1642
1643
  ...longLivedTokenInfo(loadVault(), DEPLOYMENT_MODE),
1643
1644
  ...customBackendFields(),
1645
+ ...mtTokenFields(),
1644
1646
  }));
1647
+ /**
1648
+ * Whether this is a multi-tenant instance and, if so, whether its group agents'
1649
+ * subscription token is still missing — the signal the cockpit uses to offer the
1650
+ * in-cockpit provisioning card (and the blocking banner). Mono-tenant → both
1651
+ * false; it needs no token.
1652
+ */
1653
+ function mtTokenFields() {
1654
+ const multiTenant = DEPLOYMENT_MODE === "multi-tenant";
1655
+ const present = !!secretsFor([MT_CLAUDE_OAUTH_TOKEN_NAME], MAIN_GROUP_ID)[MT_CLAUDE_OAUTH_TOKEN_NAME];
1656
+ return { multiTenant, mtTokenMissing: multiTenant && !present };
1657
+ }
1645
1658
  function customBackendFields() {
1646
1659
  const b = instanceBackend();
1647
1660
  return b.kind === "custom" ? { backend: "custom", backendModel: b.model } : {};
@@ -1679,6 +1692,59 @@ app.delete("/auth/login", (_req, res) => {
1679
1692
  cancelLogin();
1680
1693
  res.json({ ok: true });
1681
1694
  });
1695
+ // --- Subscription-token provisioning (src/setup-token.ts) -----------------
1696
+ // A multi-tenant instance injects the long-lived subscription token
1697
+ // (MT_CLAUDE_OAUTH_TOKEN) into its privilege-dropped agents; these two routes
1698
+ // let an ADMIN mint/rotate it from the cockpit instead of a hand-run shell
1699
+ // `claude setup-token`. Browser-origin + admin, exactly like the guardrail
1700
+ // writes — and server-owned: the token is captured and stored server-side, and
1701
+ // the response never carries it. Mono-tenant needs no token, so the routes are
1702
+ // inert there (the UI hides them too).
1703
+ function mtTokenGate(req, res) {
1704
+ if (DEPLOYMENT_MODE !== "multi-tenant") {
1705
+ res.status(400).json({ error: "not applicable: this instance is single-tenant" });
1706
+ return false;
1707
+ }
1708
+ if (!requestFromBrowser(req)) {
1709
+ res.status(403).json({ error: "browser only" });
1710
+ return false;
1711
+ }
1712
+ if (currentAccount(req)?.role !== "admin") {
1713
+ res.status(403).json({ error: "admins only" });
1714
+ return false;
1715
+ }
1716
+ return true;
1717
+ }
1718
+ app.post("/mt-token/start", async (req, res) => {
1719
+ if (!mtTokenGate(req, res))
1720
+ return;
1721
+ const r = await startSetupToken();
1722
+ if ("error" in r)
1723
+ return res.status(502).json(r);
1724
+ res.json(r); // {url}
1725
+ });
1726
+ app.post("/mt-token/code", async (req, res) => {
1727
+ if (!mtTokenGate(req, res))
1728
+ return;
1729
+ const code = typeof req.body?.code === "string" ? req.body.code : "";
1730
+ // The token is captured and written to the vault HERE and never leaves the
1731
+ // server: `store` is the only thing that ever touches the value.
1732
+ const r = await submitSetupTokenCode(code, (token) => setSecret(MT_CLAUDE_OAUTH_TOKEN_NAME, token, MAIN_GROUP_ID, "operator:setup-token"));
1733
+ if (r.ok) {
1734
+ mtTokenWarned = false; // a future absence gets warned again
1735
+ invalidateAuthStatus(); // so /auth re-reports longLived immediately
1736
+ }
1737
+ res.status(r.ok ? 200 : 400).json(r); // {ok} — never the token
1738
+ });
1739
+ app.delete("/mt-token", (req, res) => {
1740
+ // Same gate as start/code: a cancel is still a write on a credential flow, so
1741
+ // a non-admin/non-browser caller must not be able to kill an admin's live
1742
+ // mint mid-rotation.
1743
+ if (!mtTokenGate(req, res))
1744
+ return;
1745
+ cancelSetupToken();
1746
+ res.json({ ok: true });
1747
+ });
1682
1748
  // --- Model backend (src/backend.ts) ---------------------------------------
1683
1749
  // The instance-wide source for every agent's backend variables: the backend
1684
1750
  // chosen in the cockpit if any, else shadok's own environment. Read on every
@@ -7223,7 +7289,7 @@ server.listen(port, HOST, () => {
7223
7289
  // the internal auth cookie so it gets through the GUI password gate. The
7224
7290
  // handle + bound port let /telegram restart it hot when the config changes.
7225
7291
  boundPort = port;
7226
- tgBridge = startTelegram(port, tgCookie());
7292
+ tgBridge = startTelegram(port, tgCookie(), DEPLOYMENT_MODE === "multi-tenant");
7227
7293
  if (GUI_PASSWORD)
7228
7294
  console.log("gui: password protection ON");
7229
7295
  // Cron scheduler: prime nextRun then tick every 30s (unref'd so it never