Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gateway-descriptor-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"grok-bot-cli": patch
---

Read Grok Bot gateway descriptor version 3 (same `entries` layout as version 2), which current Grok Bot desktop builds write. The multiple-entries safety check is unchanged.
2 changes: 1 addition & 1 deletion artifact/agent-bundle.compile-evidence.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifact/agent-bundle.manifest.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions artifact/bin/gbot-flight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25825,11 +25825,11 @@ var __webpack_modules__ = {
}
}
function encryptedPayload(wrapped) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2 && wrapped.version !== 3) {
throw new GrokBotGatewaySessionError("UNSUPPORTED_VERSION", `Unsupported Grok Bot gateway descriptor version ${wrapped.version}.`);
}
let encrypted;
if (wrapped.version === 2) {
if (wrapped.version === 2 || wrapped.version === 3) {
const entries = Object.values(wrapped.entries ?? {});
if (entries.length === 0) {
throw new GrokBotGatewaySessionError("EMPTY_ENTRIES", "Grok Bot gateway descriptor has no saved gateway entries.");
Expand Down
4 changes: 2 additions & 2 deletions artifact/bin/gbot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44254,11 +44254,11 @@ var __webpack_modules__ = {
}
}
function encryptedPayload(wrapped) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2 && wrapped.version !== 3) {
throw new GrokBotGatewaySessionError("UNSUPPORTED_VERSION", `Unsupported Grok Bot gateway descriptor version ${wrapped.version}.`);
}
let encrypted;
if (wrapped.version === 2) {
if (wrapped.version === 2 || wrapped.version === 3) {
const entries = Object.values(wrapped.entries ?? {});
if (entries.length === 0) {
throw new GrokBotGatewaySessionError("EMPTY_ENTRIES", "Grok Bot gateway descriptor has no saved gateway entries.");
Expand Down
4 changes: 2 additions & 2 deletions artifact/mcp/mcp-grok-bot-b8c2461e-flight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24532,11 +24532,11 @@ var __webpack_modules__ = {
}
}
function encryptedPayload(wrapped) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2 && wrapped.version !== 3) {
throw new GrokBotGatewaySessionError("UNSUPPORTED_VERSION", `Unsupported Grok Bot gateway descriptor version ${wrapped.version}.`);
}
let encrypted;
if (wrapped.version === 2) {
if (wrapped.version === 2 || wrapped.version === 3) {
const entries = Object.values(wrapped.entries ?? {});
if (entries.length === 0) {
throw new GrokBotGatewaySessionError("EMPTY_ENTRIES", "Grok Bot gateway descriptor has no saved gateway entries.");
Expand Down
4 changes: 2 additions & 2 deletions artifact/mcp/mcp-grok-bot-b8c2461e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61747,11 +61747,11 @@ var __webpack_modules__ = {
}
}
function encryptedPayload(wrapped) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2 && wrapped.version !== 3) {
throw new GrokBotGatewaySessionError("UNSUPPORTED_VERSION", `Unsupported Grok Bot gateway descriptor version ${wrapped.version}.`);
}
let encrypted;
if (wrapped.version === 2) {
if (wrapped.version === 2 || wrapped.version === 3) {
const entries = Object.values(wrapped.entries ?? {});
if (entries.length === 0) {
throw new GrokBotGatewaySessionError("EMPTY_ENTRIES", "Grok Bot gateway descriptor has no saved gateway entries.");
Expand Down
4 changes: 2 additions & 2 deletions artifact/scripts/gbot-relay.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21947,11 +21947,11 @@ var __webpack_modules__ = {
}
}
function encryptedPayload(wrapped) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2 && wrapped.version !== 3) {
throw new GrokBotGatewaySessionError("UNSUPPORTED_VERSION", `Unsupported Grok Bot gateway descriptor version ${wrapped.version}.`);
}
let encrypted;
if (wrapped.version === 2) {
if (wrapped.version === 2 || wrapped.version === 3) {
const entries = Object.values(wrapped.entries ?? {});
if (entries.length === 0) {
throw new GrokBotGatewaySessionError("EMPTY_ENTRIES", "Grok Bot gateway descriptor has no saved gateway entries.");
Expand Down
5 changes: 3 additions & 2 deletions src/core/app-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ class GrokBotGatewaySessionError extends Error {
}

function encryptedPayload(wrapped) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2) {
if (wrapped.version != null && wrapped.version !== 1 && wrapped.version !== 2 && wrapped.version !== 3) {
throw new GrokBotGatewaySessionError(
"UNSUPPORTED_VERSION",
`Unsupported Grok Bot gateway descriptor version ${wrapped.version}.`,
);
}
let encrypted;
if (wrapped.version === 2) {
// v3 keeps the v2 `entries` layout (adds `savedAtMs`; the payload adds `vncProxy`, unused here).
if (wrapped.version === 2 || wrapped.version === 3) {
const entries = Object.values(wrapped.entries ?? {});
if (entries.length === 0) {
throw new GrokBotGatewaySessionError(
Expand Down
126 changes: 123 additions & 3 deletions test/app-session.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ function writeWrappedDescriptor(wrapped) {
function writeWindowsAppData(
localState,
appData = mkdtempSync(join(tmpdir(), "gbot-appdata-")),
wrapped = { version: 2, entries: { primary: { encrypted: WINDOWS_ENCRYPTED_DESCRIPTOR } } },
) {
const dir = join(appData, "Grok Bot");
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, "gateway-descriptor.json"),
JSON.stringify({ version: 2, entries: { primary: { encrypted: WINDOWS_ENCRYPTED_DESCRIPTOR } } }),
JSON.stringify(wrapped),
);
writeFileSync(join(dir, "Local State"), JSON.stringify(localState));
return appData;
Expand Down Expand Up @@ -117,6 +118,44 @@ test("loads a version 2 Grok Bot gateway entry", () => {
});
});

test("loads a version 3 descriptor", () => {
const home = writeWrappedDescriptor({
version: 3,
entries: { primary: { encrypted: ENCRYPTED_DESCRIPTOR, savedAtMs: 1_791_320_229_049 } },
});

const session = loadGrokBotGatewaySession({
platform: "darwin",
home,
getKeychainPassword: () => "demo-password",
});

assert.deepEqual(session, {
gatewayUrl: "https://box.example",
gatewayToken: "gateway-token",
headers: { "x-anyrun-network-token": "route-token" },
});
});

test("rejects a version 3 descriptor with ambiguous entries", () => {
const home = writeWrappedDescriptor({
version: 3,
entries: {
first: { encrypted: ENCRYPTED_DESCRIPTOR, savedAtMs: 1 },
second: { encrypted: ENCRYPTED_DESCRIPTOR, savedAtMs: 2 },
},
});

assert.throws(
() => loadGrokBotGatewaySession({
platform: "darwin",
home,
getKeychainPassword: () => "demo-password",
}),
(error) => error.code === "AMBIGUOUS_ENTRIES",
);
});

test("rejects a version 2 descriptor with no entries", () => {
const home = writeWrappedDescriptor({ version: 2, entries: {} });

Expand Down Expand Up @@ -182,7 +221,7 @@ test("rejects a gateway entry without an encrypted payload", () => {

test("rejects an unsupported gateway descriptor version", () => {
const home = writeWrappedDescriptor({
version: 3,
version: 4,
encrypted: ENCRYPTED_DESCRIPTOR,
});

Expand All @@ -195,7 +234,7 @@ test("rejects an unsupported gateway descriptor version", () => {
(error) => {
assert.equal(error.name, "GrokBotGatewaySessionError");
assert.equal(error.code, "UNSUPPORTED_VERSION");
assert.match(error.message, /unsupported .*gateway descriptor version 3/i);
assert.match(error.message, /unsupported .*gateway descriptor version 4/i);
return true;
},
);
Expand Down Expand Up @@ -383,6 +422,87 @@ test("loads the signed-in Grok Bot gateway on Windows", () => {
});
});

test("loads a version 3 Windows descriptor through the DPAPI key provider", () => {
const appData = writeWindowsAppData(WINDOWS_LOCAL_STATE, undefined, {
version: 3,
entries: {
primary: { encrypted: WINDOWS_ENCRYPTED_DESCRIPTOR, savedAtMs: 1_791_320_229_049 },
},
});
let keyReads = 0;

const session = loadGrokBotGatewaySession({
platform: "win32",
home: "/tmp/unused",
appData,
unprotectData: (blob) => {
keyReads += 1;
return unprotectDemoKey(blob);
},
});

assert.equal(keyReads, 1);
assert.deepEqual(session, {
gatewayUrl: "https://box.example",
gatewayToken: "gateway-token",
headers: { "x-anyrun-network-token": "route-token" },
});
});

test("reports a version 3 Windows app session as usable", () => {
const appData = writeWindowsAppData(WINDOWS_LOCAL_STATE, undefined, {
version: 3,
entries: { primary: { encrypted: WINDOWS_ENCRYPTED_DESCRIPTOR, savedAtMs: 1 } },
});

assert.deepEqual(inspectGrokBotGatewaySession({
platform: "win32",
home: "/tmp/unused",
appData,
unprotectData: unprotectDemoKey,
}), { present: true, usable: true });
});

for (const [label, entries, code] of [
["empty entries", {}, "EMPTY_ENTRIES"],
["ambiguous entries", {
first: { encrypted: WINDOWS_ENCRYPTED_DESCRIPTOR, savedAtMs: 1 },
second: { encrypted: WINDOWS_ENCRYPTED_DESCRIPTOR, savedAtMs: 2 },
}, "AMBIGUOUS_ENTRIES"],
["missing encrypted payload", { primary: { savedAtMs: 1 } }, "MISSING_ENCRYPTED_PAYLOAD"],
]) {
test(`rejects version 3 Windows ${label} before unprotecting the key`, () => {
const appData = writeWindowsAppData(WINDOWS_LOCAL_STATE, undefined, { version: 3, entries });

assert.throws(() => loadGrokBotGatewaySession({
platform: "win32",
home: "/tmp/unused",
appData,
unprotectData: () => assert.fail("Invalid entries must not access the DPAPI key"),
}), (error) => error.code === code);
});
}

test("rejects a version 3 Windows descriptor with a tampered authentication tag", () => {
const tampered = Buffer.from(WINDOWS_ENCRYPTED_DESCRIPTOR, "base64");
tampered[tampered.length - 1] ^= 1;
const appData = writeWindowsAppData(WINDOWS_LOCAL_STATE, undefined, {
version: 3,
entries: { primary: { encrypted: tampered.toString("base64"), savedAtMs: 1 } },
});

const status = inspectGrokBotGatewaySession({
platform: "win32",
home: "/tmp/unused",
appData,
unprotectData: unprotectDemoKey,
});

assert.equal(status.present, true);
assert.equal(status.usable, false);
assert.equal(status.code, "UNUSABLE_SESSION");
});

test("falls back to AppData/Roaming under home when APPDATA is unset", () => {
const home = mkdtempSync(join(tmpdir(), "gbot-home-"));
writeWindowsAppData(WINDOWS_LOCAL_STATE, join(home, "AppData/Roaming"));
Expand Down
61 changes: 35 additions & 26 deletions test/doctor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,41 @@ import { grokBotGatewayDescriptorPath } from "../src/core/app-session.js";

const CLI = fileURLToPath(new URL("../dist/bin/gbot.mjs", import.meta.url));

test("doctor reports a present but unusable Grok Bot app session", {
skip: !["darwin", "linux"].includes(process.platform) && "Grok Bot app sessions are macOS/Linux-only",
}, () => {
const home = mkdtempSync(join(tmpdir(), "gbot-doctor-home-"));
const descriptorPath = grokBotGatewayDescriptorPath(home, process.platform, {});
mkdirSync(dirname(descriptorPath), { recursive: true });
writeFileSync(descriptorPath, JSON.stringify({ version: 2, entries: {} }));
const env = { ...process.env, HOME: home };
for (const name of [
"XDG_CONFIG_HOME",
"CURSOR_ACCESS_TOKEN",
"GROK_BOT_GATEWAY_URL",
"GROK_BOT_GATEWAY_TOKEN",
]) delete env[name];
for (const version of [2, 3]) {
test(`doctor reports an unusable version ${version} app session`, {
skip: !["darwin", "linux", "win32"].includes(process.platform) && "Unsupported app-session platform",
}, () => {
const fixtureHome = mkdtempSync(join(tmpdir(), "gbot-doctor-home-"));
const env = {
...process.env,
HOME: fixtureHome,
USERPROFILE: fixtureHome,
APPDATA: join(fixtureHome, "AppData/Roaming"),
};
for (const name of [
"XDG_CONFIG_HOME",
"CURSOR_ACCESS_TOKEN",
"GROK_BOT_GATEWAY_URL",
"GROK_BOT_GATEWAY_TOKEN",
]) delete env[name];
const descriptorPath = grokBotGatewayDescriptorPath(fixtureHome, process.platform, env);
mkdirSync(dirname(descriptorPath), { recursive: true });
writeFileSync(descriptorPath, JSON.stringify({ version, entries: {} }));

const result = spawnSync(process.execPath, [CLI, "doctor", "--json"], {
encoding: "utf8",
env,
});
const result = spawnSync(process.execPath, [CLI, "doctor", "--json"], {
encoding: "utf8",
env,
timeout: 20_000,
windowsHide: true,
});

assert.equal(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.deepEqual(payload.grokBotAppSession, {
present: true,
usable: false,
code: "EMPTY_ENTRIES",
error: "Grok Bot gateway descriptor has no saved gateway entries.",
assert.equal(result.status, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.deepEqual(payload.grokBotAppSession, {
present: true,
usable: false,
code: "EMPTY_ENTRIES",
error: "Grok Bot gateway descriptor has no saved gateway entries.",
});
});
});
}