chore: v2.8.16 — Oh My Pi login sharing: a pane's key is listed before it starts

omp's gateway re-reads its clients file only every 10 s, and an omp that
starts while its key is refused fails model discovery once and hangs for
good. omp-share.js now keeps four keys queued in the clients file (hashes
only; the keys in <data dir>/omp-share/tokens.json, 0600): a new pane takes
one already listed (AGENTHUB_OMP_KEY), a fresh one joins the queue, and a
gone pane's key leaves at the next sync.
This commit is contained in:
2026-10-22 10:29:55 +07:00
parent 750d74a2af
commit 9868788f86
8 changed files with 175 additions and 100 deletions
+2 -2
View File
File diff suppressed because one or more lines are too long
@@ -7239,7 +7239,7 @@ exports[`screen snapshots > Settings — about 1`] = `
<span <span
class="text-status-warning/90" class="text-status-warning/90"
> >
UI build v2.8.15 (rebuild client to match) UI build v2.8.16 (rebuild client to match)
</span> </span>
</p> </p>
</div> </div>
+10 -6
View File
@@ -553,14 +553,18 @@ omp login # e.g. anthropic
- A member's omp pane gets one provider `agenthub` in its - A member's omp pane gets one provider `agenthub` in its
`~/.omp/agent/models.yml` (written as the member; the rest of the file is `~/.omp/agent/models.yml` (written as the member; the rest of the file is
kept, a file that is not a YAML mapping is left alone), pointing at the kept, a file that is not a YAML mapping is left alone), pointing at the
gateway with `apiKey: AGENTHUB_PANE_TOKEN` — the NAME of the env var holding gateway with `apiKey: AGENTHUB_OMP_KEY` — the NAME of the env var holding
its pane token — and `OMP_SKIP_SETUP=1`. It runs on `--model agenthub/<model>`: the pane's key — and `OMP_SKIP_SETUP=1`. It runs on `--model agenthub/<model>`:
the model picked in AgentHub, else the host's own `modelRoles.default` the model picked in AgentHub, else the host's own `modelRoles.default`
(`~/.omp/agent/config.yml`) — left to itself omp would take the provider's (`~/.omp/agent/config.yml`) — left to itself omp would take the provider's
first listed model, a retired one the API answers 404 for. The clients file lists the SHA-256 first listed model, a retired one the API answers 404 for.
of each LIVE omp pane's token (rewritten every 15 s; the gateway re-reads it - The gateway re-reads its clients file only every 10 s, and an omp that
every 10 s), so a token stops working shortly after its pane ends, and starts while its key is still refused hangs for good. So AgentHub keeps four
turning sharing off empties it at once. keys QUEUED in the clients file (hashes only; the keys themselves in
`<data dir>/omp-share/tokens.json`, 0600): a new pane takes one that is
already listed, and a fresh key joins the queue. A pane's key leaves the
file at the first sync (every 15 s) after its pane is gone, and turning
sharing off empties both files at once.
- A scoped client token calls models only: it cannot read a credential, the - A scoped client token calls models only: it cannot read a credential, the
gateway's usage or credential health, and refresh tokens never leave the gateway's usage or credential health, and refresh tokens never leave the
broker. Host panes use the host's login directly and get nothing new. broker. Host panes use the host's login directly and get nothing new.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "2.8.15", "version": "2.8.16",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "agenthub", "name": "agenthub",
"version": "2.8.15", "version": "2.8.16",
"hasInstallScript": true, "hasInstallScript": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "2.8.15", "version": "2.8.16",
"bin": { "bin": {
"agenthub": "bin/agenthub.js" "agenthub": "bin/agenthub.js"
}, },
+47 -36
View File
@@ -2,9 +2,10 @@
* @file omp-share.test.js * @file omp-share.test.js
* @description Oh My Pi login sharing (server/lib/omp-share.js): a member's * @description Oh My Pi login sharing (server/lib/omp-share.js): a member's
* `models.yml` gets one `agenthub` provider pointing at the loopback gateway * `models.yml` gets one `agenthub` provider pointing at the loopback gateway
* with the pane token's env var NAME as its key (the rest of the file kept, a * with the key's env var NAME as its key (the rest of the file kept, a
* non-mapping refused); the gateway's clients file holds only LIVE omp panes' * non-mapping refused); a pane takes a key the clients file listed BEFORE it
* token hashes; a spawn is untouched while sharing is off, for a host pane, or * started (the gateway re-reads it only every 10 s), and a gone pane's key
* leaves it; a spawn is untouched while sharing is off, for a host pane, or
* without S3; and the route refuses outside S3. No omp daemon is started. * without S3; and the route refuses outside S3. No omp daemon is started.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn> * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/ */
@@ -23,13 +24,15 @@ process.env.CLAUDE_HOME = path.join(TMP, ".claude");
process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data"); process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data");
process.env.AGENTHUB_OMP_GATEWAY_PORT = "4999"; process.env.AGENTHUB_OMP_GATEWAY_PORT = "4999";
const crypto = require("node:crypto");
const yaml = require("js-yaml"); const yaml = require("js-yaml");
const sha = (t) => crypto.createHash("sha256").update(t).digest("hex");
const { createApp, startServer } = require("../index"); const { createApp, startServer } = require("../index");
const share = require("../lib/omp-share"); const share = require("../lib/omp-share");
const linux = require("../lib/linux-accounts"); const linux = require("../lib/linux-accounts");
const runAs = require("../lib/run-as"); const runAs = require("../lib/run-as");
const tmux = require("../lib/tmux"); const tmux = require("../lib/tmux");
const { db } = require("../db");
const realS3 = linux.s3Enabled; const realS3 = linux.s3Enabled;
const realLive = tmux.liveSessions; const realLive = tmux.liveSessions;
@@ -57,7 +60,7 @@ describe("mergedModels", () => {
providers: { providers: {
agenthub: { agenthub: {
baseUrl: "http://127.0.0.1:4999", baseUrl: "http://127.0.0.1:4999",
apiKey: "AGENTHUB_PANE_TOKEN", apiKey: "AGENTHUB_OMP_KEY",
api: "anthropic-messages", api: "anthropic-messages",
authHeader: true, authHeader: true,
discovery: { type: "proxy" }, discovery: { type: "proxy" },
@@ -72,7 +75,7 @@ describe("mergedModels", () => {
const doc = yaml.load(share.mergedModels(text)); const doc = yaml.load(share.mergedModels(text));
assert.equal(doc.providers.mine.baseUrl, "http://h/v1"); assert.equal(doc.providers.mine.baseUrl, "http://h/v1");
assert.deepEqual(doc.equivalence, { x: "y" }); assert.deepEqual(doc.equivalence, { x: "y" });
assert.equal(doc.providers.agenthub.apiKey, "AGENTHUB_PANE_TOKEN"); assert.equal(doc.providers.agenthub.apiKey, "AGENTHUB_OMP_KEY");
}); });
it("writes nothing when the provider is already there as is", () => { it("writes nothing when the provider is already there as is", () => {
@@ -113,17 +116,18 @@ describe("forSpawn", () => {
const member = { login: "ah-mia", home: path.join(TMP, "home-mia") }; const member = { login: "ah-mia", home: path.join(TMP, "home-mia") };
it("is null while sharing is off, for a host pane, or without S3", () => { it("is null while sharing is off, for a host pane, or without S3", () => {
assert.equal(share.forSpawn(member, "agenthub-lane-1", "h"), null); // off assert.equal(share.forSpawn(member, "agenthub-lane-1"), null); // off
fs.mkdirSync(path.dirname(flag), { recursive: true }); fs.mkdirSync(path.dirname(flag), { recursive: true });
fs.writeFileSync(flag, ""); fs.writeFileSync(flag, "");
assert.equal(share.forSpawn(member, "agenthub-lane-1", "h"), null); // no S3 assert.equal(share.forSpawn(member, "agenthub-lane-1"), null); // no S3
linux.s3Enabled = () => true; linux.s3Enabled = () => true;
assert.equal(share.forSpawn(null, "agenthub-lane-1", "h"), null); // the host's assert.equal(share.forSpawn(null, "agenthub-lane-1"), null); // the host's
assert.equal(share.forSpawn(member, "agenthub-lane-1", null), null); // no token
}); });
it("writes the member's models.yml AS them and lists the pane's token", () => { it("hands out a key the gateway already listed, and writes models.yml AS the member", () => {
turnOn(); turnOn();
share.syncClients(); // the queue, listed before any pane asks
const listedBefore = JSON.parse(fs.readFileSync(clientsFile, "utf8"));
const writes = []; const writes = [];
const real = { read: runAs.readFileSafe, mkdir: runAs.mkdirAs, write: runAs.writeAs }; const real = { read: runAs.readFileSafe, mkdir: runAs.mkdirAs, write: runAs.writeAs };
runAs.readFileSafe = () => { runAs.readFileSafe = () => {
@@ -135,17 +139,27 @@ describe("forSpawn", () => {
runAs.writeAs = (acct, file, mode, content) => writes.push([mode, acct.login, file, content]); runAs.writeAs = (acct, file, mode, content) => writes.push([mode, acct.login, file, content]);
tmux.liveSessions = () => new Set(); tmux.liveSessions = () => new Set();
try { try {
const s = share.forSpawn(member, "agenthub-lane-7", "a".repeat(64)); const s = share.forSpawn(member, "agenthub-lane-7");
assert.deepEqual(s.env, { OMP_SKIP_SETUP: "1" }); const key = s.env.AGENTHUB_OMP_KEY;
assert.match(key, /^ahomp_/);
assert.equal(s.env.OMP_SKIP_SETUP, "1");
const hash = sha(key);
assert.ok(
Object.values(listedBefore).some((c) => c.tokenSha256 === hash),
"the key was listed before the pane started"
);
const after = JSON.parse(fs.readFileSync(clientsFile, "utf8"));
assert.deepEqual(after["agenthub-lane-7"], { tokenSha256: hash });
// its place in the queue is filled again, and no plaintext reaches the clients file
assert.equal(Object.keys(after).filter((k) => k.startsWith("standby-")).length, 4);
assert.ok(!fs.readFileSync(clientsFile, "utf8").includes("ahomp_"));
assert.equal((fs.statSync(clientsFile).mode & 0o777).toString(8), "600");
s.place(); s.place();
const agentDir = path.join(member.home, ".omp", "agent"); const agentDir = path.join(member.home, ".omp", "agent");
assert.deepEqual(writes[0], ["mkdir", "ah-mia", agentDir]); assert.deepEqual(writes[0], ["mkdir", "ah-mia", agentDir]);
assert.equal(writes[1][0], "replace"); assert.equal(writes[1][0], "replace");
assert.equal(writes[1][2], path.join(agentDir, "models.yml")); assert.equal(writes[1][2], path.join(agentDir, "models.yml"));
assert.equal(yaml.load(writes[1][3]).providers.agenthub.apiKey, "AGENTHUB_PANE_TOKEN"); assert.equal(yaml.load(writes[1][3]).providers.agenthub.apiKey, "AGENTHUB_OMP_KEY");
const clients = JSON.parse(fs.readFileSync(clientsFile, "utf8"));
assert.deepEqual(clients["agenthub-lane-7"], { tokenSha256: "a".repeat(64) });
assert.equal((fs.statSync(clientsFile).mode & 0o777).toString(8), "600");
} finally { } finally {
runAs.readFileSafe = real.read; runAs.readFileSafe = real.read;
runAs.mkdirAs = real.mkdir; runAs.mkdirAs = real.mkdir;
@@ -154,29 +168,26 @@ describe("forSpawn", () => {
}); });
}); });
describe("liveClients", () => { describe("pruneTokens", () => {
before(() => { const now = 1_000_000;
const ins = db.prepare( const tokens = () => ({
`INSERT INTO dashboard_runs (id, cwd, status, tool, pane_token_hash) VALUES (?, '/x', ?, ?, ?)` standby: [],
); used: {
ins.run("agenthub-lane-21", "running", "omp", "b".repeat(64)); "agenthub-lane-21": { hash: "b", at: now - 120_000 }, // live
ins.run("agenthub-lane-22", "running", "omp", "c".repeat(64)); // pane gone "agenthub-lane-22": { hash: "c", at: now - 120_000 }, // pane gone
ins.run("agenthub-lane-23", "ended", "omp", "d".repeat(64)); "agenthub-lane-23": { hash: "d", at: now - 5_000 }, // just started
ins.run("agenthub-lane-24", "running", "claude", "e".repeat(64)); },
}); });
it("lists only live omp panes", () => { it("drops only a gone pane's key past the grace, and keeps the queue full", () => {
tmux.liveSessions = () => new Set(["agenthub-lane-21", "agenthub-lane-23", "agenthub-lane-24"]); const t = share.pruneTokens(tokens(), new Set(["agenthub-lane-21"]), now);
const c = share.liveClients(); assert.deepEqual(Object.keys(t.used).sort(), ["agenthub-lane-21", "agenthub-lane-23"]);
assert.deepEqual(c["agenthub-lane-21"], { tokenSha256: "b".repeat(64) }); assert.equal(t.standby.length, 4);
for (const id of ["agenthub-lane-22", "agenthub-lane-23", "agenthub-lane-24"]) {
assert.equal(c[id], undefined, id);
}
}); });
it("says nothing when tmux could not be asked", () => { it("drops nothing when tmux could not be asked", () => {
tmux.liveSessions = () => null; const t = share.pruneTokens(tokens(), null, now);
assert.equal(share.liveClients(), null); assert.equal(Object.keys(t.used).length, 3);
}); });
}); });
+110 -49
View File
@@ -14,13 +14,14 @@
* `/v1/messages`, `/v1/chat/completions`, `/v1/responses`, `/v1/models` * `/v1/messages`, `/v1/chat/completions`, `/v1/responses`, `/v1/models`
* with the broker's credentials put in upstream. Its own unscoped token * with the broker's credentials put in upstream. Its own unscoped token
* stays in the host's home too. * stays in the host's home too.
* A member's omp pane reaches the gateway as a SCOPED client: the clients file * A member's omp pane reaches the gateway as a SCOPED client: it is handed one
* lists the SHA-256 of each live omp pane's own pane token * key (`AGENTHUB_OMP_KEY`, through tmux -e / its launch file) that the
* (`dashboard_runs.pane_token_hash`, the same hash `auth.js` keeps), and its * clients file had already listed — by SHA-256 only — before the pane started
* `~/.omp/agent/models.yml` gets one `agenthub` provider whose `apiKey` is the * (the gateway re-reads that file only every 10 s; see `takeToken`), and its
* env var NAME `AGENTHUB_PANE_TOKEN`. A scoped client can call models but * `~/.omp/agent/models.yml` gets one `agenthub` provider whose `apiKey` is
* never read a credential or the gateway's usage, and its token stops working * that env var's NAME. A scoped client can call models but never read a
* within the gateway's 10 s re-read once its pane is gone (`syncClients`) — so * credential or the gateway's usage, and its key stops working within the
* next sync + the gateway's re-read once its pane is gone (`syncClients`) — so
* an agent can spend through the login while its pane lives, never carry it * an agent can spend through the login while its pane lives, never carry it
* away. omp's gateway serves only its built-in providers' models (a custom * away. omp's gateway serves only its built-in providers' models (a custom
* `models.yml` provider of the host, such as a 9Router, is not shared). * `models.yml` provider of the host, such as a 9Router, is not shared).
@@ -30,6 +31,7 @@
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn> * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/ */
const crypto = require("node:crypto");
const fs = require("node:fs"); const fs = require("node:fs");
const os = require("node:os"); const os = require("node:os");
const path = require("node:path"); const path = require("node:path");
@@ -37,10 +39,8 @@ const net = require("node:net");
const { spawn } = require("node:child_process"); const { spawn } = require("node:child_process");
const PROVIDER = "agenthub"; const PROVIDER = "agenthub";
const TOKEN_ENV = "AGENTHUB_PANE_TOKEN"; const TOKEN_ENV = "AGENTHUB_OMP_KEY";
const SYNC_MS = 15_000; const SYNC_MS = 15_000;
/** A spawn's token counts before its run row exists (recordRun runs after). */
const PENDING_MS = 60_000;
const port = (name, fallback) => { const port = (name, fallback) => {
const n = parseInt(process.env[name] || "", 10); const n = parseInt(process.env[name] || "", 10);
@@ -53,6 +53,7 @@ const dir = () => path.join(require("./claude-home").getDataDir(), "omp-share");
const onFlag = () => path.join(dir(), "enabled"); const onFlag = () => path.join(dir(), "enabled");
const clientsFile = () => path.join(dir(), "clients.json"); const clientsFile = () => path.join(dir(), "clients.json");
const pidsFile = () => path.join(dir(), "pids.json"); const pidsFile = () => path.join(dir(), "pids.json");
const tokensFile = () => path.join(dir(), "tokens.json");
const logFile = (name) => path.join(dir(), `${name}.log`); const logFile = (name) => path.join(dir(), `${name}.log`);
/** The host's own omp config dir, where the broker keeps its token. */ /** The host's own omp config dir, where the broker keeps its token. */
const ompDir = () => process.env.AGENTHUB_OMP_DIR || path.join(os.homedir(), ".omp"); const ompDir = () => process.env.AGENTHUB_OMP_DIR || path.join(os.homedir(), ".omp");
@@ -67,52 +68,110 @@ function ensureDir() {
fs.mkdirSync(dir(), { recursive: true, mode: 0o700 }); fs.mkdirSync(dir(), { recursive: true, mode: 0o700 });
} }
// ── the clients file ────────────────────────────────────────────────────── // ── tokens and the clients file ───────────────────────────────────────────
//
// The gateway re-reads its clients file only every 10 s, and an omp that
// starts while its key is still refused hangs for good (its model discovery
// fails once and is never retried). So a pane never gets a key minted for
// it: it takes one of STANDBY keys that have been listed for a while, and a
// fresh one takes that key's place in the queue. `tokens.json` (0600, the
// host's data dir) keeps the queue and which run holds which key, so both
// survive a restart; a key whose pane is gone leaves the file at the next sync.
const pending = new Map(); // run id → { hash, at } const STANDBY = 4;
let lastClients = null; /** A run's key stays listed this long before its pane must show up in tmux. */
const GRACE_MS = 60_000;
/** `{ <run id>: { tokenSha256 } }` for every live omp pane, or null when tmux could not say. */ const sha = (t) => crypto.createHash("sha256").update(t).digest("hex");
function liveClients() { const newToken = () => `ahomp_${crypto.randomBytes(24).toString("base64url")}`;
const live = require("./tmux").liveSessions();
if (!live) return null; function readTokens() {
const rows = require("../db") try {
.db.prepare( const j = JSON.parse(fs.readFileSync(tokensFile(), "utf8"));
`SELECT id, pane_token_hash FROM dashboard_runs return {
WHERE tool = 'omp' AND status IN ('running', 'spawning') AND pane_token_hash IS NOT NULL` standby: Array.isArray(j.standby) ? j.standby.filter((t) => typeof t === "string") : [],
) used: j.used && typeof j.used === "object" ? j.used : {},
.all(); };
} catch {
return { standby: [], used: {} };
}
}
function writeAtomic(file, text) {
ensureDir();
const tmp = `${file}.${process.pid}.tmp`;
fs.writeFileSync(tmp, text, { mode: 0o600 });
fs.renameSync(tmp, file);
}
function refill(t) {
while (t.standby.length < STANDBY) t.standby.push(newToken());
}
/** The gateway's view: every queued key and every key a run holds — hashes only. */
function clientsFor(t) {
const out = {}; const out = {};
for (const r of rows) if (live.has(r.id)) out[r.id] = { tokenSha256: r.pane_token_hash }; for (const s of t.standby) out[`standby-${sha(s).slice(0, 16)}`] = { tokenSha256: sha(s) };
const now = Date.now(); for (const [id, u] of Object.entries(t.used)) {
for (const [id, p] of pending) { if (u && typeof u.hash === "string") out[id] = { tokenSha256: u.hash };
if (now - p.at > PENDING_MS) pending.delete(id);
else out[id] = { tokenSha256: p.hash };
} }
return out; return out;
} }
function writeClients(clients) { let lastClients = null;
const text = `${JSON.stringify(clients, null, 2)}\n`;
if (text === lastClients) return; function save(t) {
ensureDir(); writeAtomic(tokensFile(), `${JSON.stringify(t)}\n`);
const tmp = `${clientsFile()}.${process.pid}.tmp`; const text = `${JSON.stringify(clientsFor(t), null, 2)}\n`;
fs.writeFileSync(tmp, text, { mode: 0o600 }); if (text === lastClients && fs.existsSync(clientsFile())) return;
fs.renameSync(tmp, clientsFile()); writeAtomic(clientsFile(), text);
lastClients = text; lastClients = text;
} }
/** Rewrite the clients file from the live omp panes (a gone pane's token drops out). */ /** A key for run `runId`, already listed with the gateway; its place in the queue is refilled. */
function takeToken(runId) {
const t = readTokens();
refill(t);
const token = t.standby.shift();
t.used[runId] = { hash: sha(token), at: Date.now() };
refill(t);
save(t);
return token;
}
/**
* Drop the keys of runs whose pane is gone (after GRACE_MS), keep the queue
* full, and rewrite both files. `live`: the tmux session names, or null when
* tmux could not say — then nothing is dropped.
*/
function pruneTokens(t, live, now = Date.now()) {
if (live) {
for (const [id, u] of Object.entries(t.used)) {
if (!live.has(id) && now - (Number(u?.at) || 0) > GRACE_MS) delete t.used[id];
}
}
refill(t);
return t;
}
/** Rewrite the clients file from the live omp panes (a gone pane's key drops out). */
function syncClients() { function syncClients() {
if (!enabled() || !available()) return; if (!enabled() || !available()) return;
try { try {
const clients = liveClients(); save(pruneTokens(readTokens(), require("./tmux").liveSessions()));
if (clients) writeClients(clients);
} catch (err) { } catch (err) {
console.warn("omp share: clients sync failed:", err.message); console.warn("omp share: clients sync failed:", err.message);
} }
} }
/** Sharing turned off: no key stays listed, queued or held. */
function revokeAll() {
lastClients = null;
if (fs.existsSync(tokensFile()) || fs.existsSync(clientsFile())) {
save({ standby: [], used: {} });
}
}
// ── a member's models.yml ───────────────────────────────────────────────── // ── a member's models.yml ─────────────────────────────────────────────────
function providerEntry() { function providerEntry() {
@@ -200,10 +259,12 @@ function shareModel(requested, hostDefault = hostDefaultModel()) {
* null when sharing is off (the pane then runs on the member's own login). * null when sharing is off (the pane then runs on the member's own login).
* Only a MEMBER's pane — the host's omp already holds the login itself. * Only a MEMBER's pane — the host's omp already holds the login itself.
*/ */
function forSpawn(member, runId, tokenHash) { function forSpawn(member, runId) {
if (!member || !tokenHash || !enabled() || !available()) return null; if (!member || !enabled() || !available()) return null;
const token = takeToken(runId);
return { return {
env: { OMP_SKIP_SETUP: "1" }, // The key itself, never in argv: tmux -e, or a member's launch file.
env: { OMP_SKIP_SETUP: "1", [TOKEN_ENV]: token },
model: (requested) => shareModel(requested), model: (requested) => shareModel(requested),
place() { place() {
try { try {
@@ -211,8 +272,6 @@ function forSpawn(member, runId, tokenHash) {
} catch (err) { } catch (err) {
console.warn(`omp share: ${member.login}'s models.yml left as is:`, err.message); console.warn(`omp share: ${member.login}'s models.yml left as is:`, err.message);
} }
pending.set(runId, { hash: tokenHash, at: Date.now() });
syncClients();
}, },
}; };
} }
@@ -328,8 +387,9 @@ async function startDaemons() {
return; return;
} }
killStale(); killStale();
// The gateway refuses to start without the clients file it names. // The gateway refuses to start without the clients file it names, and
if (!fs.existsSync(clientsFile())) writeClients({}); // must list the queued keys from its first read.
save(pruneTokens(readTokens(), null));
const bport = brokerPort(); const bport = brokerPort();
const gport = gatewayPort(); const gport = gatewayPort();
for (const p of [bport, gport]) { for (const p of [bport, gport]) {
@@ -422,9 +482,7 @@ async function setEnabled(on) {
fs.rmSync(onFlag(), { force: true }); fs.rmSync(onFlag(), { force: true });
stop(); stop();
// No pane keeps a way in once sharing is off. // No pane keeps a way in once sharing is off.
lastClients = null; revokeAll();
pending.clear();
if (fs.existsSync(clientsFile())) writeClients({});
} }
return state(); return state();
} }
@@ -458,7 +516,10 @@ module.exports = {
mergedModels, mergedModels,
providerEntry, providerEntry,
shareModel, shareModel,
liveClients, takeToken,
pruneTokens,
clientsFor,
readTokens,
PROVIDER, PROVIDER,
TOKEN_ENV, TOKEN_ENV,
}; };
+2 -3
View File
@@ -490,13 +490,12 @@ function spawnRun(args) {
: null; : null;
const paneCred = require("./auth").newPaneToken(); const paneCred = require("./auth").newPaneToken();
// A member's omp on the host's shared omp login (omp-share.js): its // A member's omp on the host's shared omp login (omp-share.js): its
// models.yml points at the loopback gateway, keyed by this pane's token. // models.yml points at the loopback gateway, with a key already listed there.
const ompShare = const ompShare =
tool === "omp" tool === "omp"
? require("./omp-share").forSpawn( ? require("./omp-share").forSpawn(
require("./run-as").accountForLane(laneId), require("./run-as").accountForLane(laneId),
runIdForLane(laneId, slot), runIdForLane(laneId, slot)
paneCred?.hash
) )
: null; : null;
if (forkSession === true) { if (forkSession === true) {