Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebfae9b891 | |||
| 3ab484ddc0 |
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-status-warning/90"
|
class="text-status-warning/90"
|
||||||
>
|
>
|
||||||
UI build v2.8.46 (rebuild client to match)
|
UI build v2.8.47 (rebuild client to match)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -555,6 +555,10 @@ agenthub accounts pin team-key mia --tool codex # pins work as for Claude
|
|||||||
is used only for a pane that runs as its developer (one Linux user per
|
is used only for a pane that runs as its developer (one Linux user per
|
||||||
account): AgentHub writes the account's `auth.json` into that developer's
|
account): AgentHub writes the account's `auth.json` into that developer's
|
||||||
`~/.codex` as them (0600) just before the pane starts.
|
`~/.codex` as them (0600) just before the pane starts.
|
||||||
|
- The pane runs `codex --no-daemon` (when that Codex lists the flag in
|
||||||
|
`--help`): Codex ≥ 0.164 otherwise hands a plain session to the home's
|
||||||
|
shared app-server daemon, which outlives every pane and keeps the login
|
||||||
|
it started with — the new `auth.json` would not be used.
|
||||||
- A host pane never gets one: its `CODEX_HOME` holds the host's own login.
|
- A host pane never gets one: its `CODEX_HOME` holds the host's own login.
|
||||||
- A developer who signed Codex in themselves keeps their own login.
|
- A developer who signed Codex in themselves keeps their own login.
|
||||||
- While a pane of theirs runs on a pool account, their next pane uses the
|
- While a pane of theirs runs on a pool account, their next pane uses the
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.46",
|
"version": "2.8.47",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.46",
|
"version": "2.8.47",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.46",
|
"version": "2.8.47",
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "bin/agenthub.js"
|
"agenthub": "bin/agenthub.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -243,6 +243,29 @@ describe("codex pool: spawn", () => {
|
|||||||
assert.equal(row.account, "c");
|
assert.equal(row.account, "c");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs a ChatGPT pane without the shared daemon, only when its Codex has the flag", () => {
|
||||||
|
// The home's app-server daemon keeps the login it started with: a placed
|
||||||
|
// login reaches only a pane that runs its own app server.
|
||||||
|
const fakeCodex = (name, help) => {
|
||||||
|
const file = path.join(TMP, name);
|
||||||
|
fs.writeFileSync(file, `#!/bin/sh\necho '${help}'\n`, { mode: 0o755 });
|
||||||
|
return file;
|
||||||
|
};
|
||||||
|
const prev = process.env.AGENTHUB_CODEX_BIN;
|
||||||
|
try {
|
||||||
|
pool.addAccount("c", { auth: AUTH("acct-c") });
|
||||||
|
process.env.AGENTHUB_CODEX_BIN = fakeCodex("codex-new", " --no-daemon Run without");
|
||||||
|
assert.deepEqual(pool.forSpawn(MIA).argv, ["--no-daemon"]);
|
||||||
|
process.env.AGENTHUB_CODEX_BIN = fakeCodex("codex-old", " --oss Use a local model");
|
||||||
|
assert.deepEqual(pool.forSpawn(MIA).argv, [], "an older Codex would refuse the flag");
|
||||||
|
process.env.AGENTHUB_CODEX_BIN = path.join(TMP, "no-such-codex");
|
||||||
|
assert.deepEqual(pool.forSpawn(MIA).argv, []);
|
||||||
|
} finally {
|
||||||
|
if (prev === undefined) delete process.env.AGENTHUB_CODEX_BIN;
|
||||||
|
else process.env.AGENTHUB_CODEX_BIN = prev;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("never replaces a member's own login, and stays on the account their live pane uses", () => {
|
it("never replaces a member's own login, and stays on the account their live pane uses", () => {
|
||||||
pool.addAccount("c", { auth: AUTH("acct-c") });
|
pool.addAccount("c", { auth: AUTH("acct-c") });
|
||||||
pool.addAccount("d", { auth: AUTH("acct-d") });
|
pool.addAccount("d", { auth: AUTH("acct-d") });
|
||||||
@@ -272,7 +295,10 @@ describe("codex pool: spawn", () => {
|
|||||||
fs.writeFileSync(MIA_AUTH, JSON.stringify(own));
|
fs.writeFileSync(MIA_AUTH, JSON.stringify(own));
|
||||||
assert.equal(pool.pickAccount(MIA), null);
|
assert.equal(pool.pickAccount(MIA), null);
|
||||||
pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" });
|
pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" });
|
||||||
assert.equal(JSON.parse(fs.readFileSync(MIA_AUTH, "utf8")).tokens.refresh_token, "rt-mias-seat");
|
assert.equal(
|
||||||
|
JSON.parse(fs.readFileSync(MIA_AUTH, "utf8")).tokens.refresh_token,
|
||||||
|
"rt-mias-seat"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("copies the newest auth.json back into the pool", () => {
|
it("copies the newest auth.json back into the pool", () => {
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
* home AgentHub placed that account in, never dated in the future, and
|
* home AgentHub placed that account in, never dated in the future, and
|
||||||
* only when its id_token names the same account (`mayCopyBack`); the 2026-10-08
|
* only when its id_token names the same account (`mayCopyBack`); the 2026-10-08
|
||||||
* spike showed an old refresh token still refreshes, so panes sharing an
|
* spike showed an old refresh token still refreshes, so panes sharing an
|
||||||
* account do not log each other out.
|
* account do not log each other out. Such a pane runs `--no-daemon`
|
||||||
|
* (when its Codex has the flag): the home's shared app-server daemon
|
||||||
|
* would keep serving the login it started with.
|
||||||
*
|
*
|
||||||
* Records live in `<CLAUDE_HOME>/agenthub-codex-accounts/<name>.json` (dir
|
* Records live in `<CLAUDE_HOME>/agenthub-codex-accounts/<name>.json` (dir
|
||||||
* 0700, file 0600); no route returns a key or a token.
|
* 0700, file 0600); no route returns a key or a token.
|
||||||
@@ -424,6 +426,46 @@ function liveInHome(name, member) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Codex ≥ 0.164 hands a plain TUI (no `-c` override) to the CODEX_HOME's shared
|
||||||
|
// app-server daemon, which outlives every TUI and keeps the login it started
|
||||||
|
// with — so a ChatGPT login placed in the home reached no pane while the
|
||||||
|
// member's daemon ran on the previous one (reproduced on 0.164.0). A pool pane
|
||||||
|
// runs its own app server instead. An older Codex has neither the daemon nor
|
||||||
|
// the flag (it would refuse to start), so the flag goes only to a binary whose
|
||||||
|
// --help lists it: probed once per binary (path, size, mtime).
|
||||||
|
const noDaemonCache = new Map();
|
||||||
|
|
||||||
|
function noDaemonArgv(member) {
|
||||||
|
const bin =
|
||||||
|
process.env.AGENTHUB_CODEX_BIN ||
|
||||||
|
(member
|
||||||
|
? require("./shared-tools").sharedPath("codex")
|
||||||
|
: require("./ai-tools").detect("codex").path);
|
||||||
|
if (!bin) return [];
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
const st = fs.statSync(bin);
|
||||||
|
key = `${fs.realpathSync(bin)}:${st.size}:${st.mtimeMs}`;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!noDaemonCache.has(key)) {
|
||||||
|
let help = "";
|
||||||
|
try {
|
||||||
|
help = require("node:child_process").execFileSync(bin, ["--help"], {
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 5000,
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* no answer: leave the flag out, as before */
|
||||||
|
}
|
||||||
|
noDaemonCache.set(key, /--no-daemon\b/.test(help));
|
||||||
|
}
|
||||||
|
return noDaemonCache.get(key) ? ["--no-daemon"] : [];
|
||||||
|
}
|
||||||
|
|
||||||
/** `{name, kind}` of the account a new Codex pane would get, or null. */
|
/** `{name, kind}` of the account a new Codex pane would get, or null. */
|
||||||
function pickAccount(member = null, ownerId = null) {
|
function pickAccount(member = null, ownerId = null) {
|
||||||
const a = pick(member, ownerId);
|
const a = pick(member, ownerId);
|
||||||
@@ -449,7 +491,7 @@ function forSpawn(member = null, ownerId = null) {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: a.name,
|
name: a.name,
|
||||||
argv: [],
|
argv: noDaemonArgv(member),
|
||||||
env: {},
|
env: {},
|
||||||
place: () => {
|
place: () => {
|
||||||
syncHome(member.home); // a live pane may hold a newer login than ours
|
syncHome(member.home); // a live pane may hold a newer login than ours
|
||||||
|
|||||||
Reference in New Issue
Block a user