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 4eb7734ab9
8 changed files with 175 additions and 100 deletions
+47 -36
View File
@@ -2,9 +2,10 @@
* @file omp-share.test.js
* @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
* with the pane token'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'
* token hashes; a spawn is untouched while sharing is off, for a host pane, or
* with the key's env var NAME as its key (the rest of the file kept, a
* non-mapping refused); a pane takes a key the clients file listed BEFORE it
* 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.
* @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.AGENTHUB_OMP_GATEWAY_PORT = "4999";
const crypto = require("node:crypto");
const yaml = require("js-yaml");
const sha = (t) => crypto.createHash("sha256").update(t).digest("hex");
const { createApp, startServer } = require("../index");
const share = require("../lib/omp-share");
const linux = require("../lib/linux-accounts");
const runAs = require("../lib/run-as");
const tmux = require("../lib/tmux");
const { db } = require("../db");
const realS3 = linux.s3Enabled;
const realLive = tmux.liveSessions;
@@ -57,7 +60,7 @@ describe("mergedModels", () => {
providers: {
agenthub: {
baseUrl: "http://127.0.0.1:4999",
apiKey: "AGENTHUB_PANE_TOKEN",
apiKey: "AGENTHUB_OMP_KEY",
api: "anthropic-messages",
authHeader: true,
discovery: { type: "proxy" },
@@ -72,7 +75,7 @@ describe("mergedModels", () => {
const doc = yaml.load(share.mergedModels(text));
assert.equal(doc.providers.mine.baseUrl, "http://h/v1");
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", () => {
@@ -113,17 +116,18 @@ describe("forSpawn", () => {
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", () => {
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.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;
assert.equal(share.forSpawn(null, "agenthub-lane-1", "h"), null); // the host's
assert.equal(share.forSpawn(member, "agenthub-lane-1", null), null); // no token
assert.equal(share.forSpawn(null, "agenthub-lane-1"), null); // the host's
});
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();
share.syncClients(); // the queue, listed before any pane asks
const listedBefore = JSON.parse(fs.readFileSync(clientsFile, "utf8"));
const writes = [];
const real = { read: runAs.readFileSafe, mkdir: runAs.mkdirAs, write: runAs.writeAs };
runAs.readFileSafe = () => {
@@ -135,17 +139,27 @@ describe("forSpawn", () => {
runAs.writeAs = (acct, file, mode, content) => writes.push([mode, acct.login, file, content]);
tmux.liveSessions = () => new Set();
try {
const s = share.forSpawn(member, "agenthub-lane-7", "a".repeat(64));
assert.deepEqual(s.env, { OMP_SKIP_SETUP: "1" });
const s = share.forSpawn(member, "agenthub-lane-7");
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();
const agentDir = path.join(member.home, ".omp", "agent");
assert.deepEqual(writes[0], ["mkdir", "ah-mia", agentDir]);
assert.equal(writes[1][0], "replace");
assert.equal(writes[1][2], path.join(agentDir, "models.yml"));
assert.equal(yaml.load(writes[1][3]).providers.agenthub.apiKey, "AGENTHUB_PANE_TOKEN");
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");
assert.equal(yaml.load(writes[1][3]).providers.agenthub.apiKey, "AGENTHUB_OMP_KEY");
} finally {
runAs.readFileSafe = real.read;
runAs.mkdirAs = real.mkdir;
@@ -154,29 +168,26 @@ describe("forSpawn", () => {
});
});
describe("liveClients", () => {
before(() => {
const ins = db.prepare(
`INSERT INTO dashboard_runs (id, cwd, status, tool, pane_token_hash) VALUES (?, '/x', ?, ?, ?)`
);
ins.run("agenthub-lane-21", "running", "omp", "b".repeat(64));
ins.run("agenthub-lane-22", "running", "omp", "c".repeat(64)); // pane gone
ins.run("agenthub-lane-23", "ended", "omp", "d".repeat(64));
ins.run("agenthub-lane-24", "running", "claude", "e".repeat(64));
describe("pruneTokens", () => {
const now = 1_000_000;
const tokens = () => ({
standby: [],
used: {
"agenthub-lane-21": { hash: "b", at: now - 120_000 }, // live
"agenthub-lane-22": { hash: "c", at: now - 120_000 }, // pane gone
"agenthub-lane-23": { hash: "d", at: now - 5_000 }, // just started
},
});
it("lists only live omp panes", () => {
tmux.liveSessions = () => new Set(["agenthub-lane-21", "agenthub-lane-23", "agenthub-lane-24"]);
const c = share.liveClients();
assert.deepEqual(c["agenthub-lane-21"], { tokenSha256: "b".repeat(64) });
for (const id of ["agenthub-lane-22", "agenthub-lane-23", "agenthub-lane-24"]) {
assert.equal(c[id], undefined, id);
}
it("drops only a gone pane's key past the grace, and keeps the queue full", () => {
const t = share.pruneTokens(tokens(), new Set(["agenthub-lane-21"]), now);
assert.deepEqual(Object.keys(t.used).sort(), ["agenthub-lane-21", "agenthub-lane-23"]);
assert.equal(t.standby.length, 4);
});
it("says nothing when tmux could not be asked", () => {
tmux.liveSessions = () => null;
assert.equal(share.liveClients(), null);
it("drops nothing when tmux could not be asked", () => {
const t = share.pruneTokens(tokens(), null, now);
assert.equal(Object.keys(t.used).length, 3);
});
});