grok-bot-cli 0.4.2 → 0.5.0
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/CHANGELOG.md +30 -0
- package/README.md +13 -9
- package/dist/.claude-plugin/marketplace.json +1 -1
- package/dist/.claude-plugin/plugin.json +1 -1
- package/dist/.codex-plugin/plugin.json +1 -1
- package/dist/.cursor-plugin/plugin.json +1 -1
- package/dist/INSTALL.md +1 -1
- package/dist/README.md +13 -9
- package/dist/agent-bundle.compile-evidence.json +1 -1
- package/dist/agent-bundle.manifest.json +1 -1
- package/dist/bin/gbot-flight.mjs +207 -112
- package/dist/bin/gbot.mjs +209 -116
- package/dist/install.mjs +1 -1
- package/dist/mcp/mcp-grok-bot-b8c2461e-flight.mjs +25 -22
- package/dist/mcp/mcp-grok-bot-b8c2461e.mjs +26 -23
- package/dist/package.json +2 -2
- package/dist/plugin.json +1 -1
- package/dist/skills/talk-to-grok-bot/SKILL.md +3 -1
- package/package.json +2 -2
package/dist/bin/gbot-flight.mjs
CHANGED
|
@@ -15422,9 +15422,6 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
15422
15422
|
|
|
15423
15423
|
|
|
15424
15424
|
const config = {
|
|
15425
|
-
aliases: [
|
|
15426
|
-
'chat'
|
|
15427
|
-
],
|
|
15428
15425
|
description: 'Read the most recent messages in a Grok Bot bot or group thread.',
|
|
15429
15426
|
inputJsonSchema: {
|
|
15430
15427
|
additionalProperties: false,
|
|
@@ -15574,7 +15571,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
15574
15571
|
|
|
15575
15572
|
// Hosts and the CLI print thrown error text (and stack), and a fetch or proxy failure can echo
|
|
15576
15573
|
// a credential. Rewrap with a redacted message; keep `name` and own fields such as `reason`,
|
|
15577
|
-
// `delivery`, `mode`, and
|
|
15574
|
+
// `delivery`, `mode`, and correlation ids so outcome documents still classify the failure.
|
|
15578
15575
|
const withRedactedErrors = async (run)=>{
|
|
15579
15576
|
try {
|
|
15580
15577
|
return await run();
|
|
@@ -26410,6 +26407,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
26410
26407
|
/* import */ var node_child_process__rspack_import_5 = __webpack_require__("node:child_process");
|
|
26411
26408
|
/* import */ var node_module__rspack_import_6 = __webpack_require__("node:module");
|
|
26412
26409
|
/* import */ var _codex_contract_js__rspack_import_7 = __webpack_require__("./src/core/codex/contract.js");
|
|
26410
|
+
/* import */ var _desktop_shim_js__rspack_import_8 = __webpack_require__("./src/core/desktop-shim.js");
|
|
26411
|
+
|
|
26413
26412
|
|
|
26414
26413
|
|
|
26415
26414
|
|
|
@@ -26572,15 +26571,27 @@ class CodexRpcError extends Error {
|
|
|
26572
26571
|
}
|
|
26573
26572
|
}
|
|
26574
26573
|
class CodexSendError extends Error {
|
|
26575
|
-
constructor(message, { delivery,
|
|
26574
|
+
constructor(message, { correlationId, delivery, hop, messageId, reason, refused, threadId, turnId } = {}){
|
|
26576
26575
|
super(message);
|
|
26577
26576
|
this.name = "CodexSendError";
|
|
26578
26577
|
this.delivery = delivery;
|
|
26578
|
+
if (correlationId !== undefined) this.correlationId = correlationId;
|
|
26579
|
+
if (hop !== undefined) this.hop = hop;
|
|
26580
|
+
if (messageId !== undefined) this.messageId = messageId;
|
|
26579
26581
|
if (reason !== undefined) this.reason = reason;
|
|
26582
|
+
if (refused !== undefined) this.refused = refused;
|
|
26580
26583
|
if (threadId !== undefined) this.threadId = threadId;
|
|
26581
26584
|
if (turnId !== undefined) this.turnId = turnId;
|
|
26582
|
-
|
|
26583
|
-
|
|
26585
|
+
}
|
|
26586
|
+
}
|
|
26587
|
+
function attachEnvelope(error, envelope) {
|
|
26588
|
+
if (!error || typeof error !== "object") return;
|
|
26589
|
+
for (const key of [
|
|
26590
|
+
"messageId",
|
|
26591
|
+
"correlationId",
|
|
26592
|
+
"hop"
|
|
26593
|
+
]){
|
|
26594
|
+
if (error[key] === undefined && envelope[key] !== undefined) error[key] = envelope[key];
|
|
26584
26595
|
}
|
|
26585
26596
|
}
|
|
26586
26597
|
/** The route to the app-server is unavailable; `mode` is a stable machine-readable state. */ class CodexRouteError extends Error {
|
|
@@ -26668,11 +26679,42 @@ function isObject(value) {
|
|
|
26668
26679
|
const client = {
|
|
26669
26680
|
refused,
|
|
26670
26681
|
// Server-initiated requests are only *answered* once our own turn/start
|
|
26671
|
-
// is in flight
|
|
26672
|
-
//
|
|
26673
|
-
//
|
|
26674
|
-
//
|
|
26682
|
+
// is in flight, and then only when they name our own thread/turn:
|
|
26683
|
+
// anything earlier — or naming another thread or Desktop turn — belongs
|
|
26684
|
+
// to another client and must never be rejected on their behalf. Early
|
|
26685
|
+
// and foreign requests are still recorded; the connection closes right
|
|
26686
|
+
// after, so a pending request simply dies with it instead of denying
|
|
26687
|
+
// someone's approval.
|
|
26675
26688
|
answerServerRequests: false,
|
|
26689
|
+
expectedThreadId: null,
|
|
26690
|
+
expectedTurnId: null,
|
|
26691
|
+
// Approvals that name a turn while our own turn id is still unknown
|
|
26692
|
+
// (arriving before — or in the same socket batch as — the turn/start
|
|
26693
|
+
// acknowledgment) wait here instead of being classified foreign: once
|
|
26694
|
+
// the acknowledgment supplies the turn id, matching ones are answered
|
|
26695
|
+
// and the rest stay silent.
|
|
26696
|
+
deferred: [],
|
|
26697
|
+
// Adopt the acknowledged turn id, answering deferred requests that
|
|
26698
|
+
// prove to be ours. Runs inside the connection closure so it can send.
|
|
26699
|
+
_adoptTurn (turnId) {
|
|
26700
|
+
client.expectedTurnId = turnId;
|
|
26701
|
+
for (const entry of client.deferred.splice(0)){
|
|
26702
|
+
const params = entry.params && typeof entry.params === "object" && !Array.isArray(entry.params) ? entry.params : null;
|
|
26703
|
+
const threadId = params ? params.threadId ?? params.thread_id : null;
|
|
26704
|
+
const namedTurnId = params ? params.turnId ?? params.turn_id ?? (params.turn && params.turn.id) : null;
|
|
26705
|
+
if (threadId === client.expectedThreadId && namedTurnId === turnId) {
|
|
26706
|
+
entry.answered = true;
|
|
26707
|
+
sendJson({
|
|
26708
|
+
jsonrpc: "2.0",
|
|
26709
|
+
id: entry.id,
|
|
26710
|
+
error: {
|
|
26711
|
+
code: -32601,
|
|
26712
|
+
message: "gbot codex does not answer " + entry.method + "; configure approval_policy on the daemon"
|
|
26713
|
+
}
|
|
26714
|
+
});
|
|
26715
|
+
}
|
|
26716
|
+
}
|
|
26717
|
+
},
|
|
26676
26718
|
request (method, params) {
|
|
26677
26719
|
const id = nextId++;
|
|
26678
26720
|
return new Promise((res, rej)=>{
|
|
@@ -26736,13 +26778,36 @@ function isObject(value) {
|
|
|
26736
26778
|
};
|
|
26737
26779
|
const onMessage = (msg)=>{
|
|
26738
26780
|
if (msg.id != null && msg.method) {
|
|
26739
|
-
|
|
26740
|
-
|
|
26781
|
+
let answered = client.answerServerRequests;
|
|
26782
|
+
let defer = false;
|
|
26783
|
+
// Approval ownership: only answer requests for the turn gbot itself
|
|
26784
|
+
// started. A request naming another thread — or naming a turn that is
|
|
26785
|
+
// not ours — is recorded foreign-silent, never rejected. A request
|
|
26786
|
+
// naming a turn while our turn id is still unknown is deferred, not
|
|
26787
|
+
// classified: the turn/start acknowledgment decides it.
|
|
26788
|
+
if (answered) {
|
|
26789
|
+
const params = msg.params && typeof msg.params === "object" && !Array.isArray(msg.params) ? msg.params : null;
|
|
26790
|
+
if (params) {
|
|
26791
|
+
const threadId = params.threadId ?? params.thread_id;
|
|
26792
|
+
const turnId = params.turnId ?? params.turn_id ?? (params.turn && params.turn.id);
|
|
26793
|
+
if (threadId !== client.expectedThreadId || turnId == null) answered = false;
|
|
26794
|
+
else if (client.expectedTurnId == null) {
|
|
26795
|
+
answered = false;
|
|
26796
|
+
defer = true;
|
|
26797
|
+
} else if (turnId !== client.expectedTurnId) answered = false;
|
|
26798
|
+
} else answered = false;
|
|
26799
|
+
}
|
|
26800
|
+
const entry = {
|
|
26741
26801
|
id: msg.id,
|
|
26742
26802
|
method: msg.method,
|
|
26743
26803
|
params: msg.params,
|
|
26744
26804
|
answered
|
|
26745
|
-
}
|
|
26805
|
+
};
|
|
26806
|
+
refused.push(entry);
|
|
26807
|
+
if (defer) {
|
|
26808
|
+
client.deferred.push(entry);
|
|
26809
|
+
return;
|
|
26810
|
+
}
|
|
26746
26811
|
if (!answered) return;
|
|
26747
26812
|
sendJson({
|
|
26748
26813
|
jsonrpc: "2.0",
|
|
@@ -26987,17 +27052,30 @@ function listDesktopProcesses(timeoutMs = DESKTOP_PROCESS_LIST_TIMEOUT_MS) {
|
|
|
26987
27052
|
/**
|
|
26988
27053
|
* Status contract: `reachable` is endpoint reachability only. `schema.compatibility` is
|
|
26989
27054
|
* `exact` when the daemon reports the pinned version, otherwise `unverified` (methods
|
|
26990
|
-
* usually survive upgrades) or `unknown`. `
|
|
26991
|
-
* Desktop-
|
|
26992
|
-
*
|
|
26993
|
-
*
|
|
27055
|
+
* usually survive upgrades) or `unknown`. `desktopShimConfigured` records an installed
|
|
27056
|
+
* wrapper selected by Desktop-facing CODEX_CLI_PATH (the GUI domain on Darwin).
|
|
27057
|
+
* Configuration does not prove a running Desktop inherited it or connected.
|
|
27058
|
+
* `desktopAttached` is `"private-stdio"` when a Desktop-bundled app-server process
|
|
27059
|
+
* is visible, otherwise `"unknown"`. Managed attachment is not observable here.
|
|
26994
27060
|
*
|
|
26995
27061
|
* @param {NodeJS.ProcessEnv} [env]
|
|
26996
|
-
* @param {{ listProcesses?: string | string[] }} [opts]
|
|
26997
|
-
* defaults to a live `ps` snapshot
|
|
26998
|
-
|
|
27062
|
+
* @param {{ listProcesses?: string | string[], shim?: { installed?: boolean, wrapperPointsAtShim?: boolean } | null }} [opts]
|
|
27063
|
+
* injected process list and shim state for tests; defaults to a live `ps` snapshot and
|
|
27064
|
+
* `desktopShimStatus`. Never reads pipes or connects to Desktop.
|
|
27065
|
+
*/ async function codexStatus(env = process.env, { listProcesses, shim } = {}) {
|
|
26999
27066
|
const path = codexSocketPath(env);
|
|
27000
27067
|
const cli = probeLocalCodexVersion();
|
|
27068
|
+
let desktopShimConfigured = Boolean(shim && shim.installed && shim.wrapperPointsAtShim);
|
|
27069
|
+
if (shim === undefined) {
|
|
27070
|
+
try {
|
|
27071
|
+
const live = (0,_desktop_shim_js__rspack_import_8/* .desktopShimStatus */.em)({
|
|
27072
|
+
env
|
|
27073
|
+
});
|
|
27074
|
+
desktopShimConfigured = Boolean(live.installed && live.wrapperPointsAtShim);
|
|
27075
|
+
} catch {
|
|
27076
|
+
desktopShimConfigured = false;
|
|
27077
|
+
}
|
|
27078
|
+
}
|
|
27001
27079
|
const desktopAttached = detectDesktopPrivateAppServer({
|
|
27002
27080
|
platform: process.platform,
|
|
27003
27081
|
listProcesses: listProcesses ?? listDesktopProcesses()
|
|
@@ -27008,7 +27086,8 @@ function listDesktopProcesses(timeoutMs = DESKTOP_PROCESS_LIST_TIMEOUT_MS) {
|
|
|
27008
27086
|
pinnedVersion: PINNED_CODEX_VERSION,
|
|
27009
27087
|
cliVersion: cli.version,
|
|
27010
27088
|
cliVersionProbe: cli.probe,
|
|
27011
|
-
desktopAttached
|
|
27089
|
+
desktopAttached,
|
|
27090
|
+
desktopShimConfigured
|
|
27012
27091
|
};
|
|
27013
27092
|
let session;
|
|
27014
27093
|
try {
|
|
@@ -27167,7 +27246,7 @@ const ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
|
27167
27246
|
throw new CodexSendError("Refusing to send: hop " + hopCount + " reaches the relay bound " + bound + " (GROK_BOT_MAX_HOPS). " + "This message is an agent-to-agent relay that has already been forwarded too many times.", {
|
|
27168
27247
|
delivery: "rejected",
|
|
27169
27248
|
reason: "hop-limit",
|
|
27170
|
-
|
|
27249
|
+
...out
|
|
27171
27250
|
});
|
|
27172
27251
|
}
|
|
27173
27252
|
return out;
|
|
@@ -27255,20 +27334,20 @@ function unsupportedOrRpc(err, method, threadId, envelope) {
|
|
|
27255
27334
|
delivery: "rejected",
|
|
27256
27335
|
reason: "unsupported",
|
|
27257
27336
|
threadId,
|
|
27258
|
-
envelope
|
|
27337
|
+
...envelope
|
|
27259
27338
|
});
|
|
27260
27339
|
}
|
|
27261
27340
|
if (err instanceof CodexRpcError) return new CodexSendError(err.message, {
|
|
27262
27341
|
delivery: "rejected",
|
|
27263
27342
|
reason: "rejected",
|
|
27264
27343
|
threadId,
|
|
27265
|
-
envelope
|
|
27344
|
+
...envelope
|
|
27266
27345
|
});
|
|
27267
27346
|
return new CodexSendError("Lost the Codex " + method + " response for thread " + threadId + ": " + (err && err.message || err) + ". Delivery is unknown; list the queue before resending.", {
|
|
27268
27347
|
delivery: err && err.delivery || "unknown",
|
|
27269
27348
|
reason: "transport",
|
|
27270
27349
|
threadId,
|
|
27271
|
-
envelope
|
|
27350
|
+
...envelope
|
|
27272
27351
|
});
|
|
27273
27352
|
}
|
|
27274
27353
|
/** Read the daemon's queue for one thread (experimental `thread/queue/list`). */ async function listCodexQueue(threadId, { env = process.env, limit = 50, cursor } = {}) {
|
|
@@ -27319,7 +27398,7 @@ function unsupportedOrRpc(err, method, threadId, envelope) {
|
|
|
27319
27398
|
return (0,_codex_contract_js__rspack_import_7/* .outcomeFromReceipt */.Dz)(receipt);
|
|
27320
27399
|
} catch (err) {
|
|
27321
27400
|
// Every receipt names the message, including refusals that never reached the daemon.
|
|
27322
|
-
if (
|
|
27401
|
+
if (err instanceof CodexSendError || err instanceof CodexRouteError || err instanceof CodexProtocolError) attachEnvelope(err, envelope);
|
|
27323
27402
|
return (0,_codex_contract_js__rspack_import_7/* .outcomeFromError */.DG)(err);
|
|
27324
27403
|
}
|
|
27325
27404
|
}
|
|
@@ -27344,7 +27423,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
27344
27423
|
delivery: err instanceof CodexRpcError ? "rejected" : err && err.delivery || "unknown",
|
|
27345
27424
|
reason: err instanceof CodexRpcError ? /no rollout found|thread not found/i.test(String(err.rpc && err.rpc.message)) ? "unknown-thread" : /active writer/i.test(String(err.rpc && err.rpc.message)) ? "external-owner" : "rejected" : "transport",
|
|
27346
27425
|
threadId,
|
|
27347
|
-
envelope
|
|
27426
|
+
...envelope
|
|
27348
27427
|
});
|
|
27349
27428
|
}
|
|
27350
27429
|
if (!isObject(resumed) || !isObject(resumed.thread) || typeof resumed.thread.id !== "string") {
|
|
@@ -27371,7 +27450,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
27371
27450
|
delivery: "rejected",
|
|
27372
27451
|
reason: "busy",
|
|
27373
27452
|
threadId,
|
|
27374
|
-
envelope
|
|
27453
|
+
...envelope
|
|
27375
27454
|
});
|
|
27376
27455
|
}
|
|
27377
27456
|
if (state.busy) {
|
|
@@ -27396,7 +27475,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
27396
27475
|
delivery: "unknown",
|
|
27397
27476
|
reason: "bad-response",
|
|
27398
27477
|
threadId,
|
|
27399
|
-
envelope
|
|
27478
|
+
...envelope
|
|
27400
27479
|
});
|
|
27401
27480
|
}
|
|
27402
27481
|
return {
|
|
@@ -27410,11 +27489,15 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
27410
27489
|
// the daemon exposes no compare-and-start request. Upgrade path: thread/queue/add + thread/queue/start once stable.
|
|
27411
27490
|
// Scope refusals to this turn: server requests from earlier calls belong to another context.
|
|
27412
27491
|
const seenRefused = client.refused.length;
|
|
27413
|
-
// Arm refusal replies only now that our own turn/start is in flight
|
|
27414
|
-
//
|
|
27415
|
-
//
|
|
27416
|
-
// never rejects another client's approval.
|
|
27417
|
-
//
|
|
27492
|
+
// Arm refusal replies only now that our own turn/start is in flight, scoped
|
|
27493
|
+
// to our own thread/turn: anything recorded earlier (resume, busy reject,
|
|
27494
|
+
// queue) — or naming another thread or Desktop turn — stays unanswered so
|
|
27495
|
+
// gbot never rejects another client's approval. Requests naming a turn
|
|
27496
|
+
// that arrives before the acknowledgment supplies our turn id wait in
|
|
27497
|
+
// client.deferred and are adopted only on a match. Requests missing
|
|
27498
|
+
// thread or turn IDs remain unanswered because ownership is unknown.
|
|
27499
|
+
client.expectedThreadId = threadId;
|
|
27500
|
+
client.expectedTurnId = null;
|
|
27418
27501
|
client.answerServerRequests = true;
|
|
27419
27502
|
let turn;
|
|
27420
27503
|
try {
|
|
@@ -27438,7 +27521,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
27438
27521
|
delivery,
|
|
27439
27522
|
reason: delivery === "rejected" ? "rejected" : "transport",
|
|
27440
27523
|
threadId,
|
|
27441
|
-
envelope
|
|
27524
|
+
...envelope
|
|
27442
27525
|
});
|
|
27443
27526
|
}
|
|
27444
27527
|
const turnId = turn && turn.turn && typeof turn.turn.id === "string" && turn.turn.id ? turn.turn.id : null;
|
|
@@ -27447,10 +27530,20 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
27447
27530
|
delivery: "unknown",
|
|
27448
27531
|
reason: "bad-response",
|
|
27449
27532
|
threadId,
|
|
27450
|
-
envelope
|
|
27533
|
+
...envelope
|
|
27451
27534
|
});
|
|
27452
27535
|
}
|
|
27453
|
-
|
|
27536
|
+
client._adoptTurn(turnId);
|
|
27537
|
+
const freshRefused = client.refused.slice(seenRefused).filter((r)=>{
|
|
27538
|
+
if (!r.answered) return false;
|
|
27539
|
+
const params = r.params && typeof r.params === "object" && !Array.isArray(r.params) ? r.params : null;
|
|
27540
|
+
if (!params) return true;
|
|
27541
|
+
const refusedThreadId = params.threadId ?? params.thread_id;
|
|
27542
|
+
const refusedTurnId = params.turnId ?? params.turn_id ?? (params.turn && params.turn.id);
|
|
27543
|
+
if (refusedThreadId != null && refusedThreadId !== threadId) return false;
|
|
27544
|
+
if (refusedTurnId != null && refusedTurnId !== turnId) return false;
|
|
27545
|
+
return true;
|
|
27546
|
+
});
|
|
27454
27547
|
if (freshRefused.length) {
|
|
27455
27548
|
const methods = freshRefused.map((r)=>r.method).join(", ");
|
|
27456
27549
|
throw new CodexSendError("Turn " + turnId + " started on thread " + threadId + " but Codex asked for " + methods + ", which gbot refused. " + "Answer it in a Codex client, or set `approval_policy = \"never\"` in the daemon's config.toml for unattended sends.", {
|
|
@@ -27459,7 +27552,7 @@ async function sendToCodexThreadInner(threadId, text, { env, envelope, whenBusy
|
|
|
27459
27552
|
threadId,
|
|
27460
27553
|
turnId,
|
|
27461
27554
|
refused: freshRefused.map((r)=>r.method),
|
|
27462
|
-
envelope
|
|
27555
|
+
...envelope
|
|
27463
27556
|
});
|
|
27464
27557
|
}
|
|
27465
27558
|
return {
|
|
@@ -27480,8 +27573,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
27480
27573
|
X$: () => (listCodexThreads),
|
|
27481
27574
|
ZS: () => (sendToCodexThread),
|
|
27482
27575
|
nq: () => (buildEnvelope),
|
|
27483
|
-
pF: () => (singleLine)
|
|
27484
|
-
ql: () => (codexSocketPath)
|
|
27576
|
+
pF: () => (singleLine)
|
|
27485
27577
|
});
|
|
27486
27578
|
|
|
27487
27579
|
|
|
@@ -27546,8 +27638,6 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
27546
27638
|
}
|
|
27547
27639
|
/**
|
|
27548
27640
|
* Flatten a thrown error into the #46 send/failure document (plus exitCode).
|
|
27549
|
-
* Preserves the legacy failure-document fields, then adds exitCode for the
|
|
27550
|
-
* generated CLI's `exitCode: 'result'` policy.
|
|
27551
27641
|
*
|
|
27552
27642
|
* @param {unknown} error
|
|
27553
27643
|
* @param {{ isUsage?: (err: unknown) => boolean }} [opts]
|
|
@@ -27574,12 +27664,6 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
27574
27664
|
if (error[key] !== undefined) out[key] = error[key];
|
|
27575
27665
|
}
|
|
27576
27666
|
if (out.reason === undefined && isUsage(error)) out.reason = "usage";
|
|
27577
|
-
const envelope = /** @type {{ messageId?: string, correlationId?: string, hop?: number }} */ error.envelope;
|
|
27578
|
-
if (envelope && typeof envelope === "object") {
|
|
27579
|
-
if (envelope.messageId !== undefined) out.messageId = envelope.messageId;
|
|
27580
|
-
if (envelope.correlationId !== undefined) out.correlationId = envelope.correlationId;
|
|
27581
|
-
if (envelope.hop !== undefined) out.hop = envelope.hop;
|
|
27582
|
-
}
|
|
27583
27667
|
}
|
|
27584
27668
|
if (out.delivery === undefined) out.delivery = "rejected";
|
|
27585
27669
|
return /** @type {CodexSendOutcome & { error: string, exitCode: 1 }} */ {
|
|
@@ -27596,14 +27680,6 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
27596
27680
|
...receipt
|
|
27597
27681
|
};
|
|
27598
27682
|
if (base.delivery === undefined) base.delivery = "accepted";
|
|
27599
|
-
// Flatten envelope onto the document when present (matches fail() + json print).
|
|
27600
|
-
const envelope = base.envelope;
|
|
27601
|
-
if (envelope && typeof envelope === "object") {
|
|
27602
|
-
const e = /** @type {Record<string, unknown>} */ envelope;
|
|
27603
|
-
if (base.messageId === undefined && e.messageId !== undefined) base.messageId = e.messageId;
|
|
27604
|
-
if (base.correlationId === undefined && e.correlationId !== undefined) base.correlationId = e.correlationId;
|
|
27605
|
-
if (base.hop === undefined && e.hop !== undefined) base.hop = e.hop;
|
|
27606
|
-
}
|
|
27607
27683
|
const exitCode = sendExitCode(/** @type {{ delivery: CodexDelivery, reason?: string, error?: string }} */ base);
|
|
27608
27684
|
if (exitCode === 1 && base.error === undefined && base.reason === "approval-refused") {
|
|
27609
27685
|
base.error = typeof base.message === "string" ? base.message : "Codex refused one or more approvals.";
|
|
@@ -27652,7 +27728,7 @@ async function openBackend(opts) {
|
|
|
27652
27728
|
setGroupMembers: (group, members)=>_gateway_js__rspack_import_1/* .setGroupMembers */.Td(session, group, members),
|
|
27653
27729
|
addGroupMember: (group, bot)=>_gateway_js__rspack_import_1/* .addGroupMember */.zr(session, group, bot),
|
|
27654
27730
|
removeGroupMember: (group, bot)=>_gateway_js__rspack_import_1/* .removeGroupMember */.A0(session, group, bot),
|
|
27655
|
-
send: (ref, prompt
|
|
27731
|
+
send: (ref, prompt)=>_gateway_js__rspack_import_1/* .sendPrompt */.LV(session, ref, prompt),
|
|
27656
27732
|
transcript: (ref, limit)=>_gateway_js__rspack_import_1/* .getTranscriptTail */.bA(session, ref, limit),
|
|
27657
27733
|
thread: (ref, rootId)=>_gateway_js__rspack_import_1/* .getThread */.fG(session, ref, rootId)
|
|
27658
27734
|
};
|
|
@@ -27693,24 +27769,36 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
27693
27769
|
// Codex app-server control socket. Vendored from uploads/codex-stdio-to-daemon-ws.py
|
|
27694
27770
|
// (ChatGPT Desktop -> managed daemon shim) with fail-open deltas, marked below:
|
|
27695
27771
|
// (1) ws_connect enforces one absolute monotonic deadline for connect + HTTP
|
|
27696
|
-
// upgrade (CODEX_BRIDGE_CONNECT_TIMEOUT, default 10s),
|
|
27697
|
-
//
|
|
27698
|
-
//
|
|
27772
|
+
// upgrade (CODEX_BRIDGE_CONNECT_TIMEOUT, default 10s), requires HTTP/1.1 101
|
|
27773
|
+
// plus a matching Sec-WebSocket-Accept (a 200 fails even with a valid hash),
|
|
27774
|
+
// and retains bytes coalesced after the upgrade headers instead of discarding
|
|
27775
|
+
// the first data frame.
|
|
27699
27776
|
// (2) read_message assembles continuation frames; control frames answer inline;
|
|
27700
|
-
// socket
|
|
27701
|
-
//
|
|
27702
|
-
//
|
|
27703
|
-
//
|
|
27704
|
-
// daemon
|
|
27705
|
-
//
|
|
27706
|
-
// (
|
|
27707
|
-
//
|
|
27708
|
-
//
|
|
27709
|
-
//
|
|
27777
|
+
// every socket send/lock shares one monotonic budget (remaining first-RPC
|
|
27778
|
+
// budget, else the mid-session write budget) so a non-reading peer cannot
|
|
27779
|
+
// wedge pong writes or timeout cleanup; timeouts close and exit. Reads wait
|
|
27780
|
+
// with no timeout after the handshake so healthy idle sessions survive.
|
|
27781
|
+
// The wrapper never starts the daemon on the spawn path, so no daemon lock can
|
|
27782
|
+
// block pre-stdin Desktop.
|
|
27783
|
+
// (3) A first-response deadline (CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT, default
|
|
27784
|
+
// 30s) bounds connect-to-matching-daemon-response covering the first RPC;
|
|
27785
|
+
// unrelated notifications and foreign responses never satisfy it, only the
|
|
27786
|
+
// matching response/error (or timeout) clears it. The preflight (3s) +
|
|
27787
|
+
// upgrade (10s) + first RPC (30s) budget stays far below a daemon-lock hang.
|
|
27788
|
+
// Exits are 0 served, 1 pre-stdio (wrapper falls back to real Codex on pristine
|
|
27789
|
+
// stdio+stdout), 2 mid-session (wrapper exits so Desktop reconnects).
|
|
27790
|
+
// (4) Byte-precise commit on input OR output, committed BEFORE the stdout
|
|
27791
|
+
// write: os.read into a userspace line buffer counts every stdin byte (even
|
|
27792
|
+
// an unforwarded blank line), and any daemon payload marks stdout used before
|
|
27793
|
+
// it is written via a bounded stdout write, so the fallback only ever runs
|
|
27794
|
+
// on truly pristine stdio+stdout and never spawns a second Codex after used
|
|
27795
|
+
// stdout. Buffered EOF-tail data flushes before the WS Close, which drains
|
|
27796
|
+
// via shutdown+close with a bounded join. The reader signals a done event so
|
|
27797
|
+
// WS close wakes the select loop; no path leaves a thread hung.
|
|
27710
27798
|
// Desktop stdio hangs with stock `codex app-server proxy`, so this custom
|
|
27711
27799
|
// bridge is required. Install writes it to the CODEX_HOME bin dir. Never reads
|
|
27712
27800
|
// Desktop private pipes (CODEX_APP_TOOLS_PIPE_PATH, /tmp/codex-browser-use/).
|
|
27713
|
-
const BRIDGE_SOURCE = "#!/usr/bin/env python3\n\"\"\"Stdio JSONL (Desktop) <-> WebSocket-over-unix (managed app-server daemon).\nReversible: remove CODEX_CLI_PATH wrapper. Does not read Desktop private pipes.\n\"\"\"\nfrom __future__ import annotations\nimport base64, hashlib, json, os, select, socket, struct, sys, threading, time\n\nSOCK = os.environ.get(\n \"CODEX_APP_SERVER_SOCK\",\n os.path.expanduser(\"~/.codex/app-server-control/app-server-control.sock\"),\n)\nLOG = os.environ.get(\"CODEX_STDIO_BRIDGE_LOG\", os.path.expanduser(\"~/.codex/bin/codex-stdio-to-daemon-ws.log\"))\nWS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n\n# Wrapper contract: 0 served the session, 1 failed before any stdin was\n# consumed (wrapper falls back to real Codex on pristine stdio), 2 failed\n# mid-session (wrapper exits promptly so Desktop reconnects; the fallback\n# must never run on half-consumed stdin).\nEXIT_SERVED = 0\nEXIT_PRE_STDIO = 1\nEXIT_MID_SESSION = 2\n\nCONNECT_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_CONNECT_TIMEOUT\", \"10\"))\n# Bound from connect to the first daemon message: a daemon that completes the\n# upgrade but never answers the first RPC must not hold Desktop past this.\nFIRST_MESSAGE_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT\", \"30\"))\n\ndef log(msg: str) -> None:\n try:\n with open(LOG, \"a\") as f:\n f.write(time.strftime(\"%Y-%m-%dT%H:%M:%SZ\", time.gmtime()) + \" \" + msg + \"\\n\")\n except OSError:\n pass\n\ndef ws_connect(path: str, timeout: float = CONNECT_TIMEOUT_S):\n \"\"\"Connect + HTTP upgrade under one absolute monotonic deadline.\n\n Returns (socket, trailing) where trailing holds bytes coalesced after the\n upgrade headers (TCP may deliver the 101 and the first WS frame together;\n discarding them would lose the first message). Validates the 101 status\n and Sec-WebSocket-Accept so a non-WebSocket listener fails fast.\n \"\"\"\n deadline = time.monotonic() + timeout\n s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n s.settimeout(timeout)\n try:\n s.connect(path)\n key = base64.b64encode(os.urandom(16)).decode()\n expected = base64.b64encode(hashlib.sha1((key + WS_GUID).encode()).digest()).decode()\n req = (\n f\"GET /rpc HTTP/1.1\\r\\nHost: localhost\\r\\nUpgrade: websocket\\r\\n\"\n f\"Connection: Upgrade\\r\\nSec-WebSocket-Key: {key}\\r\\nSec-WebSocket-Version: 13\\r\\n\\r\\n\"\n ).encode()\n s.sendall(req)\n buf = b\"\"\n while True:\n if b\"\\r\\n\\r\\n\" in buf:\n break\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"WebSocket upgrade timed out\")\n if len(buf) > 65536:\n raise ConnectionError(\"WS upgrade headers too large\")\n s.settimeout(remaining)\n chunk = s.recv(4096)\n if not chunk:\n raise ConnectionError(\"daemon closed during WS upgrade\")\n buf += chunk\n head, _, trailing = buf.partition(b\"\\r\\n\\r\\n\")\n lines = head.split(b\"\\r\\n\")\n if b\"101\" not in lines[0]:\n raise ConnectionError(f\"WS upgrade failed: {head[:200]!r}\")\n accept = None\n for line in lines[1:]:\n name, sep, value = line.partition(b\":\")\n if sep and name.strip().lower() == b\"sec-websocket-accept\":\n accept = value.strip().decode(\"latin-1\")\n if accept != expected:\n raise ConnectionError(\"WS upgrade accept mismatch\")\n except Exception:\n try:\n s.close()\n except OSError:\n pass\n raise\n s.settimeout(None)\n return s, trailing\n\ndef mask_send(sock: socket.socket, payload: bytes, opcode: int = 1) -> None:\n mask = os.urandom(4)\n ln = len(payload)\n if ln < 126:\n hdr = bytes([0x80 | opcode, 0x80 | ln])\n elif ln < 65536:\n hdr = bytes([0x80 | opcode, 0x80 | 126]) + struct.pack(\"!H\", ln)\n else:\n hdr = bytes([0x80 | opcode, 0x80 | 127]) + struct.pack(\"!Q\", ln)\n sock.sendall(hdr + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))\n\ndef locked_send(sock: socket.socket, lock: threading.Lock, payload: bytes, opcode: int = 1) -> None:\n with lock:\n mask_send(sock, payload, opcode=opcode)\n\ndef _buffered_reader(sock: socket.socket, initial: bytes = b\"\"):\n buf = bytearray(initial)\n def recv(n):\n while len(buf) < n:\n chunk = sock.recv(n - len(buf))\n if not chunk:\n raise ConnectionError(\"eof\")\n buf.extend(chunk)\n out = bytes(buf[:n])\n del buf[:n]\n return out\n return recv\n\ndef read_frame(recv):\n hdr = recv(2)\n fin = bool(hdr[0] & 0x80)\n opcode = hdr[0] & 0x0F\n masked = bool(hdr[1] & 0x80)\n ln = hdr[1] & 0x7F\n if ln == 126:\n ln = struct.unpack(\"!H\", recv(2))[0]\n elif ln == 127:\n ln = struct.unpack(\"!Q\", recv(8))[0]\n mask = recv(4) if masked else b\"\"\n payload = recv(ln)\n if masked:\n payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))\n return fin, opcode, payload\n\ndef read_message(recv, send_pong):\n \"\"\"Next complete data message, assembling continuation frames.\n\n Control frames are handled inline (pong answered, close reported) so a\n fragmented message split around a ping still reassembles.\n \"\"\"\n opcode = None\n parts = []\n while True:\n fin, op, payload = read_frame(recv)\n if op == 0x8:\n return (\"close\", None, b\"\")\n if op == 0x9:\n send_pong(payload)\n continue\n if op == 0xA:\n continue\n if op in (0x1, 0x2):\n if opcode is not None:\n raise ConnectionError(\"data frame inside fragmented message\")\n if fin:\n return (\"data\", op, payload)\n opcode = op\n parts.append(payload)\n continue\n if op == 0x0:\n if opcode is None:\n raise ConnectionError(\"stray continuation frame\")\n parts.append(payload)\n if fin:\n return (\"data\", opcode, b\"\".join(parts))\n continue\n raise ConnectionError(f\"unsupported opcode {op}\")\n\ndef stdout_writer(sock: socket.socket, recv, send_pong, done: threading.Event, first_msg: threading.Event) -> None:\n out = sys.stdout.buffer\n try:\n while True:\n kind, opcode, payload = read_message(recv, send_pong)\n if kind == \"close\":\n log(\"daemon WS close\")\n return\n if opcode == 0x1:\n # Desktop StdioConnection expects newline-delimited JSON text\n if not payload.endswith(b\"\\n\"):\n payload += b\"\\n\"\n out.write(payload)\n out.flush()\n else:\n out.write(payload)\n out.flush()\n first_msg.set()\n except Exception as e:\n log(f\"stdout_writer exit: {e}\")\n finally:\n done.set()\n\ndef main() -> int:\n log(f\"start sock={SOCK}\")\n try:\n sock, pending = ws_connect(SOCK)\n except Exception as e:\n log(f\"connect failed: {e}\")\n sys.stderr.write(f\"codex-stdio-to-daemon-ws: {e}\\n\")\n return EXIT_PRE_STDIO\n log(\"connected\")\n connected_at = time.monotonic()\n send_lock = threading.Lock()\n def send_pong(payload: bytes) -> None:\n locked_send(sock, send_lock, payload, opcode=0xA)\n recv = _buffered_reader(sock, pending)\n done = threading.Event()\n first_msg = threading.Event()\n t = threading.Thread(target=stdout_writer, args=(sock, recv, send_pong, done, first_msg), daemon=True)\n t.start()\n # Byte-precise commit point: the fallback may run only while zero stdin\n # bytes have left the pipe. Reads use os.read into a userspace line buffer\n # (buffered readline could readahead past the commit point), and every\n # byte read counts — even a blank line. first_msg bounds the first RPC.\n stdin_bytes = 0\n pending_in = b\"\"\n clean_eof = False\n try:\n fd = sys.stdin.fileno()\n while not done.is_set():\n if not first_msg.is_set() and time.monotonic() - connected_at > FIRST_MESSAGE_TIMEOUT_S:\n log(\"first daemon message timed out\")\n break\n ready, _, _ = select.select([fd], [], [], 0.5)\n if not ready:\n continue\n chunk = os.read(fd, 65536)\n if not chunk:\n clean_eof = True\n break\n stdin_bytes += len(chunk)\n pending_in += chunk\n while b\"\\n\" in pending_in:\n line, _, pending_in = pending_in.partition(b\"\\n\")\n line = line.strip()\n if not line:\n continue\n locked_send(sock, send_lock, line)\n except Exception as e:\n log(f\"stdin loop exit: {e}\")\n try:\n locked_send(sock, send_lock, b\"\", opcode=0x8)\n except Exception:\n pass\n if clean_eof:\n tail = pending_in.strip()\n if tail and not done.is_set():\n try:\n locked_send(sock, send_lock, tail)\n except Exception as e:\n log(f\"eof flush failed: {e}\")\n log(\"exit\")\n return EXIT_SERVED\n # Reader died, first RPC timed out, or stdin broke: fail open only if no\n # stdin byte was ever consumed.\n rc = EXIT_MID_SESSION if stdin_bytes else EXIT_PRE_STDIO\n log(f\"exit rc={rc}\")\n return rc\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n";
|
|
27801
|
+
const BRIDGE_SOURCE = "#!/usr/bin/env python3\n\"\"\"Stdio JSONL (Desktop) <-> WebSocket-over-unix (managed app-server daemon).\nReversible: remove CODEX_CLI_PATH wrapper. Does not read Desktop private pipes.\n\"\"\"\nfrom __future__ import annotations\nimport base64, hashlib, json, os, select, socket, struct, sys, threading, time\n\nSOCK = os.environ.get(\n \"CODEX_APP_SERVER_SOCK\",\n os.path.expanduser(\"~/.codex/app-server-control/app-server-control.sock\"),\n)\nLOG = os.environ.get(\"CODEX_STDIO_BRIDGE_LOG\", os.path.expanduser(\"~/.codex/bin/codex-stdio-to-daemon-ws.log\"))\nWS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n\n# Wrapper contract: 0 served the session, 1 failed while stdio was still\n# pristine -- no stdin byte consumed and no stdout frame committed (wrapper\n# falls back to real Codex), 2 failed mid-session (wrapper exits promptly so\n# Desktop reconnects; the fallback must never run on half-consumed stdin or\n# dirty stdout).\nEXIT_SERVED = 0\nEXIT_PRE_STDIO = 1\nEXIT_MID_SESSION = 2\n\nCONNECT_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_CONNECT_TIMEOUT\", \"10\"))\n# Bound from connect to the matching first daemon response: a daemon that\n# completes the upgrade but never answers the first RPC must not hold Desktop\n# past this. Only the matching initialize response/error (or the deadline\n# itself) clears it; notifications never do.\nFIRST_MESSAGE_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT\", \"30\"))\n# Bound on every mid-session socket send and stdout write: a wedged or\n# non-reading peer must not hang Desktop past this. Reads after a successful\n# init block with no timeout so healthy idle sessions survive silence.\nIO_TIMEOUT_S = float(os.environ.get(\"CODEX_BRIDGE_IO_TIMEOUT\", \"30\"))\n\ndef log(msg: str) -> None:\n try:\n with open(LOG, \"a\") as f:\n f.write(time.strftime(\"%Y-%m-%dT%H:%M:%SZ\", time.gmtime()) + \" \" + msg + \"\\n\")\n except OSError:\n pass\n\ndef ws_connect(path: str, timeout: float = CONNECT_TIMEOUT_S):\n \"\"\"Connect + HTTP upgrade under one absolute monotonic deadline.\n\n Returns (socket, trailing) where trailing holds bytes coalesced after the\n upgrade headers (TCP may deliver the 101 and the first WS frame together;\n discarding them would lose the first message). Requires HTTP/1.1 101 plus\n a matching Sec-WebSocket-Accept so a 200 (or any non-101) fails fast even\n when the accept hash happens to be valid.\n \"\"\"\n deadline = time.monotonic() + timeout\n s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n s.settimeout(timeout)\n try:\n s.connect(path)\n key = base64.b64encode(os.urandom(16)).decode()\n expected = base64.b64encode(hashlib.sha1((key + WS_GUID).encode()).digest()).decode()\n req = (\n f\"GET /rpc HTTP/1.1\\r\\nHost: localhost\\r\\nUpgrade: websocket\\r\\n\"\n f\"Connection: Upgrade\\r\\nSec-WebSocket-Key: {key}\\r\\nSec-WebSocket-Version: 13\\r\\n\\r\\n\"\n ).encode()\n s.settimeout(max(0.0, deadline - time.monotonic()))\n s.sendall(req)\n buf = b\"\"\n while True:\n if b\"\\r\\n\\r\\n\" in buf:\n break\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"WebSocket upgrade timed out\")\n if len(buf) > 65536:\n raise ConnectionError(\"WS upgrade headers too large\")\n s.settimeout(remaining)\n chunk = s.recv(4096)\n if not chunk:\n raise ConnectionError(\"daemon closed during WS upgrade\")\n buf += chunk\n head, _, trailing = buf.partition(b\"\\r\\n\\r\\n\")\n lines = head.split(b\"\\r\\n\")\n if not lines or lines[0].split(b\" \", 2)[:2] != [b\"HTTP/1.1\", b\"101\"]:\n raise ConnectionError(f\"WS upgrade failed: {head[:200]!r}\")\n accept = None\n for line in lines[1:]:\n name, sep, value = line.partition(b\":\")\n if sep and name.strip().lower() == b\"sec-websocket-accept\":\n accept = value.strip().decode(\"latin-1\")\n if accept != expected:\n raise ConnectionError(\"WS upgrade accept mismatch\")\n except Exception:\n try:\n s.close()\n except OSError:\n pass\n raise\n # Keep one socket mode across reader and writer threads. Reads wait\n # indefinitely in select; sends use their own absolute deadline.\n s.setblocking(False)\n return s, trailing\n\ndef mask_send(sock: socket.socket, payload: bytes, opcode: int = 1, deadline=None) -> None:\n mask = os.urandom(4)\n ln = len(payload)\n if ln < 126:\n hdr = bytes([0x80 | opcode, 0x80 | ln])\n elif ln < 65536:\n hdr = bytes([0x80 | opcode, 0x80 | 126]) + struct.pack(\"!H\", ln)\n else:\n hdr = bytes([0x80 | opcode, 0x80 | 127]) + struct.pack(\"!Q\", ln)\n view = memoryview(hdr + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))\n while view:\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"socket write timed out\")\n _, writable, _ = select.select([], [sock], [], remaining)\n if not writable:\n raise TimeoutError(\"socket write timed out\")\n try:\n n = sock.send(view)\n except BlockingIOError:\n continue\n if not n:\n raise ConnectionError(\"socket closed during write\")\n view = view[n:]\n\ndef locked_send(sock: socket.socket, lock: threading.Lock, payload: bytes, opcode: int = 1, timeout: float = CONNECT_TIMEOUT_S) -> None:\n \"\"\"Serialized send sharing one monotonic budget across lock wait + send.\n\n A non-reading peer must never wedge pong writes or timeout cleanup: on\n expiry raise TimeoutError so the caller closes and exits without hanging.\n \"\"\"\n if timeout is None:\n timeout = CONNECT_TIMEOUT_S\n deadline = time.monotonic() + timeout\n if timeout <= 0:\n raise TimeoutError(\"send budget expired\")\n if not lock.acquire(timeout=timeout):\n raise TimeoutError(\"send lock timed out\")\n try:\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"send budget expired\")\n mask_send(sock, payload, opcode=opcode, deadline=deadline)\n finally:\n lock.release()\n\ndef _buffered_reader(sock: socket.socket, initial: bytes = b\"\"):\n buf = bytearray(initial)\n def recv(n):\n while len(buf) < n:\n select.select([sock], [], [])\n try:\n chunk = sock.recv(n - len(buf))\n except BlockingIOError:\n continue\n if not chunk:\n raise ConnectionError(\"eof\")\n buf.extend(chunk)\n out = bytes(buf[:n])\n del buf[:n]\n return out\n return recv\n\ndef read_frame(recv):\n hdr = recv(2)\n fin = bool(hdr[0] & 0x80)\n opcode = hdr[0] & 0x0F\n masked = bool(hdr[1] & 0x80)\n ln = hdr[1] & 0x7F\n if ln == 126:\n ln = struct.unpack(\"!H\", recv(2))[0]\n elif ln == 127:\n ln = struct.unpack(\"!Q\", recv(8))[0]\n mask = recv(4) if masked else b\"\"\n payload = recv(ln)\n if masked:\n payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))\n return fin, opcode, payload\n\ndef read_message(recv, send_pong):\n \"\"\"Next complete data message, assembling continuation frames.\n\n Control frames are handled inline (pong answered, close reported) so a\n fragmented message split around a ping still reassembles.\n \"\"\"\n opcode = None\n parts = []\n while True:\n fin, op, payload = read_frame(recv)\n if op == 0x8:\n return (\"close\", None, b\"\")\n if op == 0x9:\n send_pong(payload)\n continue\n if op == 0xA:\n continue\n if op in (0x1, 0x2):\n if opcode is not None:\n raise ConnectionError(\"data frame inside fragmented message\")\n if fin:\n return (\"data\", op, payload)\n opcode = op\n parts.append(payload)\n continue\n if op == 0x0:\n if opcode is None:\n raise ConnectionError(\"stray continuation frame\")\n parts.append(payload)\n if fin:\n return (\"data\", opcode, b\"\".join(parts))\n continue\n raise ConnectionError(f\"unsupported opcode {op}\")\n\ndef _is_daemon_response(payload: bytes, expect_id=None, known_ids=None) -> bool:\n \"\"\"True only for the matching JSON-RPC response/error, never a notification.\n\n The first-RPC timer must survive unrelated daemon notifications and foreign\n responses: only the response/error whose id matches the client's initialize\n (or, before it is known, any id the client already sent) proves the first\n RPC got an answer. Timeouts clear it via the deadline, never via data.\n \"\"\"\n try:\n text = payload.decode(\"utf-8\").strip()\n msg = json.loads(text)\n except Exception:\n return False\n if not isinstance(msg, dict):\n return False\n rid = msg.get(\"id\")\n if rid is None or \"method\" in msg or (\"result\" not in msg and \"error\" not in msg):\n return False\n if expect_id is not None:\n return rid == expect_id\n if known_ids:\n return rid in known_ids\n return False\n\ndef _bounded_stdout_write(data: bytes, timeout: float = IO_TIMEOUT_S) -> None:\n \"\"\"Write one daemon frame to stdout without hanging on a non-reading peer.\n\n Waits for writability under the mid-session write budget and writes via\n os.write in a loop, so a Desktop that stopped reading cannot hang the\n bridge and then exit 1 on dirty stdout.\n \"\"\"\n if timeout is None:\n timeout = IO_TIMEOUT_S\n if timeout <= 0:\n raise TimeoutError(\"stdout budget expired\")\n fd = sys.stdout.fileno()\n deadline = time.monotonic() + timeout\n was_blocking = os.get_blocking(fd)\n os.set_blocking(fd, False)\n try:\n view = memoryview(data)\n while view:\n remaining = deadline - time.monotonic()\n if remaining <= 0:\n raise TimeoutError(\"stdout write timed out\")\n _, writable, _ = select.select([], [fd], [], remaining)\n if not writable:\n raise TimeoutError(\"stdout write timed out\")\n try:\n n = os.write(fd, view)\n except BlockingIOError:\n continue\n view = view[n:]\n finally:\n os.set_blocking(fd, was_blocking)\n\ndef stdout_writer(sock: socket.socket, recv, send_pong, done: threading.Event, first_msg: threading.Event, output_forwarded: threading.Event, client_ids=None, init_state=None) -> None:\n try:\n while True:\n kind, opcode, payload = read_message(recv, send_pong)\n if kind == \"close\":\n log(\"daemon WS close\")\n return\n if opcode == 0x1:\n # Desktop StdioConnection expects newline-delimited JSON text\n if not payload.endswith(b\"\\n\"):\n payload += b\"\\n\"\n # Commit BEFORE writing: once a daemon byte is about to hit\n # stdout, the session is dirty and the fallback must never run,\n # even if the write itself blocks or fails.\n output_forwarded.set()\n _bounded_stdout_write(payload)\n if opcode == 0x1:\n expect = (init_state or {}).get(\"initialize_id\") if isinstance(init_state, dict) else None\n if _is_daemon_response(payload, expect, client_ids or ()):\n first_msg.set()\n except Exception as e:\n log(f\"stdout_writer exit: {e}\")\n finally:\n done.set()\n\ndef main() -> int:\n log(f\"start sock={SOCK}\")\n try:\n sock, pending = ws_connect(SOCK)\n except Exception as e:\n log(f\"connect failed: {e}\")\n sys.stderr.write(f\"codex-stdio-to-daemon-ws: {e}\\n\")\n return EXIT_PRE_STDIO\n log(\"connected\")\n connected_at = time.monotonic()\n first_deadline = connected_at + FIRST_MESSAGE_TIMEOUT_S\n send_lock = threading.Lock()\n done = threading.Event()\n first_msg = threading.Event()\n output_forwarded = threading.Event()\n client_ids = set()\n init_state = {\"initialize_id\": None}\n\n def _send_timeout() -> float:\n # Before the first matching daemon response the remaining first-RPC\n # budget bounds every send; after it each send gets the mid-session\n # write budget. Idle reads are never timed out.\n if not first_msg.is_set():\n return max(0.0, first_deadline - time.monotonic())\n return IO_TIMEOUT_S\n\n def _cleanup_timeout() -> float:\n # Timeout cleanup stays within the expired session budget: once the\n # first-RPC deadline has passed, close directly instead of opening\n # fresh windows that let a short budget take several timeout periods.\n if not first_msg.is_set():\n return max(0.0, first_deadline - time.monotonic())\n return CONNECT_TIMEOUT_S\n\n def send_pong(payload: bytes) -> None:\n locked_send(sock, send_lock, payload, opcode=0xA, timeout=_send_timeout())\n recv = _buffered_reader(sock, pending)\n t = threading.Thread(target=stdout_writer, args=(sock, recv, send_pong, done, first_msg, output_forwarded, client_ids, init_state), daemon=True)\n t.start()\n # Byte-precise commit point: the fallback may run only while zero stdin\n # bytes have left the pipe and no daemon payload was committed to stdout.\n # Reads use os.read into a userspace line buffer (buffered readline could\n # readahead past the commit point), and every byte read counts -- even a\n # blank line. first_msg tracks the matching daemon response/error, never\n # a notification.\n stdin_bytes = 0\n pending_in = b\"\"\n clean_eof = False\n try:\n fd = sys.stdin.fileno()\n while not done.is_set():\n if not first_msg.is_set() and time.monotonic() >= first_deadline:\n log(\"first daemon message timed out\")\n break\n ready, _, _ = select.select([fd], [], [], 0.5)\n if not ready:\n continue\n chunk = os.read(fd, 65536)\n if not chunk:\n clean_eof = True\n break\n stdin_bytes += len(chunk)\n pending_in += chunk\n while b\"\\n\" in pending_in:\n line, _, pending_in = pending_in.partition(b\"\\n\")\n line = line.strip()\n if not line:\n continue\n try:\n try:\n obj = json.loads(line.decode(\"utf-8\"))\n except Exception:\n obj = None\n if isinstance(obj, dict) and obj.get(\"id\") is not None:\n client_ids.add(obj.get(\"id\"))\n if obj.get(\"method\") == \"initialize\" and init_state[\"initialize_id\"] is None:\n init_state[\"initialize_id\"] = obj.get(\"id\")\n except Exception:\n pass\n try:\n locked_send(sock, send_lock, line, timeout=_send_timeout())\n except TimeoutError as e:\n log(f\"stdin forward timed out: {e}\")\n break\n except Exception as e:\n log(f\"stdin forward failed: {e}\")\n break\n else:\n continue\n break\n except Exception as e:\n log(f\"stdin loop exit: {e}\")\n # Flush any buffered EOF-tail data BEFORE the WS Close: sending Close\n # first would let the daemon discard the tail.\n if clean_eof:\n tail = pending_in.strip()\n if tail and not done.is_set():\n try:\n locked_send(sock, send_lock, tail, timeout=_cleanup_timeout())\n except Exception as e:\n log(f\"eof flush failed: {e}\")\n try:\n locked_send(sock, send_lock, b\"\", opcode=0x8, timeout=_cleanup_timeout())\n except Exception as e:\n log(f\"close send failed: {e}\")\n try:\n try:\n sock.shutdown(socket.SHUT_RDWR)\n except OSError:\n pass\n sock.close()\n except OSError:\n pass\n # Bound termination: wake the reader so no thread is left hung on recv.\n t.join(timeout=0.5)\n # A reader still exiting could commit output after this check. In that\n # case pristine stdio cannot be proven, so never permit fallback.\n if t.is_alive():\n output_forwarded.set()\n if clean_eof:\n log(\"exit\")\n return EXIT_SERVED\n # Reader died, first RPC timed out, post-upgrade I/O timed out, or stdin\n # broke: fail open only while stdio is still pristine -- no stdin byte\n # ever consumed AND no daemon payload ever committed to stdout. Committed\n # output forces a mid-session exit so Desktop reconnects instead of\n # falling back to stock Codex on a corrupted stream.\n rc = EXIT_MID_SESSION if (stdin_bytes or output_forwarded.is_set()) else EXIT_PRE_STDIO\n log(f\"exit rc={rc}\")\n return rc\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n";
|
|
27714
27802
|
|
|
27715
27803
|
__webpack_require__.d(__webpack_exports__, {
|
|
27716
27804
|
}, {
|
|
@@ -27724,9 +27812,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
27724
27812
|
/* import */ var node_fs__rspack_import_1 = __webpack_require__("node:fs");
|
|
27725
27813
|
/* import */ var node_os__rspack_import_2 = __webpack_require__("node:os");
|
|
27726
27814
|
/* import */ var node_path__rspack_import_3 = __webpack_require__("node:path");
|
|
27727
|
-
/* import */ var
|
|
27728
|
-
/* import */ var _codex_bridge_js__rspack_import_4 = __webpack_require__("./src/core/codex-bridge.js");
|
|
27729
|
-
|
|
27815
|
+
/* import */ var _desktop_shim_bridge_js__rspack_import_4 = __webpack_require__("./src/core/desktop-shim-bridge.js");
|
|
27730
27816
|
|
|
27731
27817
|
|
|
27732
27818
|
|
|
@@ -27804,11 +27890,12 @@ function renderWrapperScript({ realPath, bridgePath, wrapperLogPath, codexHome,
|
|
|
27804
27890
|
# daemon's control socket. Reversible: \`gbot codex desktop-shim uninstall\`.
|
|
27805
27891
|
# The daemon is kept up by the LaunchAgent login script, not here: this hot path
|
|
27806
27892
|
# never starts the daemon itself, so a wedged daemon lock cannot hang
|
|
27807
|
-
# Desktop. Fail-open runs ONLY
|
|
27808
|
-
#
|
|
27893
|
+
# Desktop. Fail-open runs ONLY while stdio is still pristine (no stdin
|
|
27894
|
+
# consumed, no stdout written): an absent socket fails the preflight and a
|
|
27895
|
+
# pre-session bridge failure (exit 1) falls through
|
|
27809
27896
|
# to the real standalone codex on pristine stdio. A mid-session bridge failure
|
|
27810
27897
|
# (exit 2+) exits promptly so Desktop reconnects; the fallback never runs on
|
|
27811
|
-
# half-consumed stdin. Bridge exit 0 means it served the session. Never touches
|
|
27898
|
+
# half-consumed stdin or dirty stdout. Bridge exit 0 means it served the session. Never touches
|
|
27812
27899
|
# Desktop binaries or its private tool pipe.
|
|
27813
27900
|
set -u
|
|
27814
27901
|
REAL="\${CODEX_DESKTOP_WRAPPER_REAL:-${realPath}}"
|
|
@@ -27825,7 +27912,11 @@ echo "$(ts) argv: $*" >>"$LOG" 2>/dev/null || true
|
|
|
27825
27912
|
if [[ ! -x "$REAL" ]]; then
|
|
27826
27913
|
FALLBACK="$(command -v codex 2>/dev/null || true)"
|
|
27827
27914
|
if [[ -n "$FALLBACK" && -x "$FALLBACK" ]]; then
|
|
27828
|
-
|
|
27915
|
+
if [[ "$FALLBACK" -ef "$0" ]] 2>/dev/null || { [[ -n "\${CODEX_CLI_PATH:-}" ]] && [[ "$FALLBACK" -ef "\${CODEX_CLI_PATH}" ]] 2>/dev/null; }; then
|
|
27916
|
+
echo "$(ts) refusing self fallback $FALLBACK" >>"$LOG" 2>/dev/null || true
|
|
27917
|
+
else
|
|
27918
|
+
REAL="$FALLBACK"
|
|
27919
|
+
fi
|
|
27829
27920
|
fi
|
|
27830
27921
|
fi
|
|
27831
27922
|
|
|
@@ -27846,7 +27937,7 @@ preflight() {
|
|
|
27846
27937
|
}
|
|
27847
27938
|
|
|
27848
27939
|
if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 ]]; then
|
|
27849
|
-
if [[ -
|
|
27940
|
+
if [[ -f "$BRIDGE" ]] && command -v python3 >/dev/null 2>&1; then
|
|
27850
27941
|
if preflight; then
|
|
27851
27942
|
echo "$(ts) rewrite -> stdio-ws bridge" >>"$LOG" 2>/dev/null || true
|
|
27852
27943
|
python3 "$BRIDGE"
|
|
@@ -27996,7 +28087,7 @@ function fileExists(path) {
|
|
|
27996
28087
|
recursive: true
|
|
27997
28088
|
});
|
|
27998
28089
|
writeExecutable(paths.wrapperPath, renderWrapperScript(paths));
|
|
27999
|
-
(0,node_fs__rspack_import_1.writeFileSync)(paths.bridgePath,
|
|
28090
|
+
(0,node_fs__rspack_import_1.writeFileSync)(paths.bridgePath, _desktop_shim_bridge_js__rspack_import_4/* .BRIDGE_SOURCE.endsWith */.w.endsWith("\n") ? _desktop_shim_bridge_js__rspack_import_4/* .BRIDGE_SOURCE */.w : `${_desktop_shim_bridge_js__rspack_import_4/* .BRIDGE_SOURCE */.w}\n`, {
|
|
28000
28091
|
mode: 420
|
|
28001
28092
|
});
|
|
28002
28093
|
writeExecutable(paths.envScriptPath, renderEnvScript(paths));
|
|
@@ -28076,11 +28167,12 @@ function fileExists(path) {
|
|
|
28076
28167
|
paths.bridgePath,
|
|
28077
28168
|
paths.envScriptPath
|
|
28078
28169
|
]){
|
|
28170
|
+
const existed = fileExists(path);
|
|
28079
28171
|
try {
|
|
28080
28172
|
if ((0,node_fs__rspack_import_1.rmSync)(path, {
|
|
28081
28173
|
force: true
|
|
28082
28174
|
}) === undefined && fileExists(path)) warnings.push(`could not remove ${path}`);
|
|
28083
|
-
else if (!fileExists(path)) removed.push(path);
|
|
28175
|
+
else if (existed && !fileExists(path)) removed.push(path);
|
|
28084
28176
|
} catch (error) {
|
|
28085
28177
|
warnings.push(`could not remove ${path} (${error instanceof Error ? error.message : String(error)})`);
|
|
28086
28178
|
}
|
|
@@ -28174,12 +28266,10 @@ function socketStateOf(path) {
|
|
|
28174
28266
|
// Desktop-facing value: the GUI domain on macOS, the caller env elsewhere.
|
|
28175
28267
|
const desktopCliPath = platform === "darwin" ? guiCliPath : cliPath;
|
|
28176
28268
|
const installed = wrapperPresent && isExecutable(paths.wrapperPath) && bridgePresent;
|
|
28177
|
-
// Effective socket, resolved exactly like gbot's own client
|
|
28178
|
-
// send/queue/status never
|
|
28179
|
-
|
|
28180
|
-
|
|
28181
|
-
CODEX_HOME: paths.codexHome
|
|
28182
|
-
});
|
|
28269
|
+
// Effective socket, resolved exactly like gbot's own client (CODEX_APP_SERVER_SOCK
|
|
28270
|
+
// wins, else the install-time CODEX_HOME) so status and send/queue/status never
|
|
28271
|
+
// disagree. Inlined here so codex-bridge can reuse desktopShimStatus without a cycle.
|
|
28272
|
+
const socketPath = env.CODEX_APP_SERVER_SOCK || (0,node_path__rspack_import_3.join)(paths.codexHome, "app-server-control", "app-server-control.sock");
|
|
28183
28273
|
return {
|
|
28184
28274
|
action: "status",
|
|
28185
28275
|
bridgePath: paths.bridgePath,
|
|
@@ -28274,7 +28364,7 @@ function formatTranscript(out, { full = false } = {}) {
|
|
|
28274
28364
|
for (const e of entries){
|
|
28275
28365
|
const role = e.role || e.kind || e.sender || e.type || "msg";
|
|
28276
28366
|
const text = (0,_transcript_js__rspack_import_0/* .entryText */.u0)(e);
|
|
28277
|
-
const id = e.id ||
|
|
28367
|
+
const id = e.id || "";
|
|
28278
28368
|
lines.push("[" + role + (id ? " " + id : "") + "] " + (full ? text : truncateCliText(text)));
|
|
28279
28369
|
}
|
|
28280
28370
|
return lines.join("\n");
|
|
@@ -28287,6 +28377,7 @@ function formatCodexStatus(s) {
|
|
|
28287
28377
|
const lines = [
|
|
28288
28378
|
"socket: " + s.socketPath + " (" + s.socketState + ")"
|
|
28289
28379
|
];
|
|
28380
|
+
if (s.desktopShimConfigured) lines.push("desktop shim: configured (managed attachment unverified)");
|
|
28290
28381
|
if (!s.reachable) return lines.concat("reachable: no (" + s.mode + ")", s.message).join("\n");
|
|
28291
28382
|
if (s.mode !== "daemon") {
|
|
28292
28383
|
return lines.concat("reachable: yes, but unusable (" + s.mode + ")", s.message).join("\n");
|
|
@@ -28294,7 +28385,7 @@ function formatCodexStatus(s) {
|
|
|
28294
28385
|
lines.push("reachable: yes (daemon)");
|
|
28295
28386
|
const cli = s.cliVersion ?? (s.cliVersionProbe === "ok" ? "unknown" : "unknown, probe " + s.cliVersionProbe);
|
|
28296
28387
|
lines.push("daemon version: " + (s.daemonVersion ?? "unknown") + " cli version: " + cli + " pinned schema: " + s.pinnedVersion + " (" + s.schema.compatibility + ")");
|
|
28297
|
-
lines.push(s.desktopAttached === "private-stdio" ? "desktop attached: private-stdio (
|
|
28388
|
+
lines.push(s.desktopAttached === "private-stdio" ? "desktop attached: private-stdio (Desktop-bundled app-server process observed; managed attachment unverified)" : "desktop attached: unknown (not observable from the socket)");
|
|
28298
28389
|
if (s.versionMismatch) {
|
|
28299
28390
|
lines.push("warning: daemon and CLI versions differ; `codex app-server daemon restart` picks up the installed CLI");
|
|
28300
28391
|
}
|
|
@@ -28309,6 +28400,9 @@ function formatCodexThread(t) {
|
|
|
28309
28400
|
const preview = t.preview ? "\n " + stripTerminalControls(String(t.preview).replace(/\s+/g, " ")).slice(0, 200) : "";
|
|
28310
28401
|
return stripTerminalControls(t.id) + " " + stripTerminalControls(t.status) + title + "\n " + stripTerminalControls(t.cwd ?? "") + preview;
|
|
28311
28402
|
}
|
|
28403
|
+
/** Shell-safe single-quoting for copy-pasteable export lines (spaces, quotes, $). */ function shellQuote(value) {
|
|
28404
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
28405
|
+
}
|
|
28312
28406
|
function formatDesktopShimStatus(s) {
|
|
28313
28407
|
const mark = (ok)=>ok ? "yes" : "no";
|
|
28314
28408
|
const cliLine = s.platform === "darwin" ? "CODEX_CLI_PATH: GUI domain " + (s.guiCliPath ?? "(unset)") + " / this shell " + (s.cliPath ?? "(unset)") + (s.wrapperPointsAtShim ? " (Desktop-facing value points at shim)" : "") : "CODEX_CLI_PATH: " + (s.cliPath ?? "(unset)") + (s.wrapperPointsAtShim ? " (points at shim)" : "");
|
|
@@ -28323,7 +28417,7 @@ function formatDesktopShimStatus(s) {
|
|
|
28323
28417
|
if (!s.installed) {
|
|
28324
28418
|
lines.push("shim: not installed — run `gbot codex desktop-shim install` (Desktop keeps stock behavior until then)");
|
|
28325
28419
|
} else if (!s.wrapperPointsAtShim) {
|
|
28326
|
-
lines.push("shim: installed but CODEX_CLI_PATH does not point at it — reinstall or relaunch ChatGPT.app after login");
|
|
28420
|
+
lines.push(s.platform === "darwin" ? "shim: installed but CODEX_CLI_PATH does not point at it — reinstall or relaunch ChatGPT.app after login" : "shim: installed but CODEX_CLI_PATH does not point at it — export CODEX_CLI_PATH=" + shellQuote(s.wrapperPath));
|
|
28327
28421
|
} else {
|
|
28328
28422
|
lines.push("shim: active — Desktop app-server spawns bridge onto the managed daemon");
|
|
28329
28423
|
}
|
|
@@ -28366,19 +28460,17 @@ class GatewayError extends Error {
|
|
|
28366
28460
|
const GATEWAY_TIMEOUT_MS = 30000;
|
|
28367
28461
|
const GATEWAY_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
28368
28462
|
function backendBase() {
|
|
28369
|
-
return (process.env.
|
|
28463
|
+
return (process.env.CURSOR_API_BASE_URL || "https://api2.cursor.sh").replace(/\/$/, "");
|
|
28370
28464
|
}
|
|
28371
28465
|
function accessTokenFromEnv() {
|
|
28372
|
-
return (process.env.CURSOR_ACCESS_TOKEN ||
|
|
28466
|
+
return (process.env.CURSOR_ACCESS_TOKEN || "").trim();
|
|
28373
28467
|
}
|
|
28374
28468
|
function gatewayTokenFromEnv() {
|
|
28375
|
-
return (process.env.GROK_BOT_GATEWAY_TOKEN ||
|
|
28469
|
+
return (process.env.GROK_BOT_GATEWAY_TOKEN || "").trim();
|
|
28376
28470
|
}
|
|
28377
28471
|
function gatewayOverride() {
|
|
28378
28472
|
const token = gatewayTokenFromEnv();
|
|
28379
|
-
const
|
|
28380
|
-
const localUrl = token ? "http://127.0.0.1:" + (process.env.SAND_HOST_PORT || "1340") : "";
|
|
28381
|
-
const url = explicitUrl || localUrl;
|
|
28473
|
+
const url = (process.env.GROK_BOT_GATEWAY_URL || "").trim();
|
|
28382
28474
|
if (url && token) {
|
|
28383
28475
|
return {
|
|
28384
28476
|
gatewayUrl: (0,_url_policy_js__rspack_import_3/* .assertAllowedCredentialUrl */.Re)(url.replace(/\/$/, ""), {
|
|
@@ -28698,14 +28790,13 @@ async function removeGroupMember(session, groupRef, memberRef) {
|
|
|
28698
28790
|
const next = group.memberIds.filter((id)=>id !== member.id);
|
|
28699
28791
|
return setGroupMembers(session, group.id, next);
|
|
28700
28792
|
}
|
|
28701
|
-
async function sendPrompt(session, ref, prompt
|
|
28793
|
+
async function sendPrompt(session, ref, prompt) {
|
|
28702
28794
|
const rec = await resolveRef(session, ref);
|
|
28703
28795
|
const body = {
|
|
28704
28796
|
agentId: rec.id,
|
|
28705
28797
|
prompt,
|
|
28706
|
-
clientNonce:
|
|
28798
|
+
clientNonce: (0,node_crypto__rspack_import_0.randomUUID)()
|
|
28707
28799
|
};
|
|
28708
|
-
if (extra.replyToId) body.replyToId = extra.replyToId;
|
|
28709
28800
|
let data;
|
|
28710
28801
|
try {
|
|
28711
28802
|
data = await gatewayCall(session, "sendPrompt", body);
|
|
@@ -28882,8 +28973,8 @@ function saveHistory(out, { dir, disabled, event, prompt, rootId } = {}) {
|
|
|
28882
28973
|
rootId
|
|
28883
28974
|
} : {},
|
|
28884
28975
|
role: String(entry.role || entry.kind || entry.sender || entry.type || "msg"),
|
|
28885
|
-
...entry.id
|
|
28886
|
-
messageId: String(entry.id
|
|
28976
|
+
...entry.id ? {
|
|
28977
|
+
messageId: String(entry.id)
|
|
28887
28978
|
} : {},
|
|
28888
28979
|
...entry.timestamp || entry.createdAt ? {
|
|
28889
28980
|
timestamp: String(entry.timestamp || entry.createdAt)
|
|
@@ -29027,10 +29118,7 @@ function isUuid(value) {
|
|
|
29027
29118
|
function defaultCandidateRoots() {
|
|
29028
29119
|
const home = (0,node_os__rspack_import_2.homedir)();
|
|
29029
29120
|
const env = [
|
|
29030
|
-
process.env.GROK_BOT_AGENTS_DIR
|
|
29031
|
-
process.env.SAND_AGENTS_DIR,
|
|
29032
|
-
process.env.SAND_DATA_ROOT && (0,node_path__rspack_import_3.join)(process.env.SAND_DATA_ROOT, "agents"),
|
|
29033
|
-
process.env.SAND_DATA_ROOT && (0,node_path__rspack_import_3.join)(process.env.SAND_DATA_ROOT, "agent-data", "agents")
|
|
29121
|
+
process.env.GROK_BOT_AGENTS_DIR
|
|
29034
29122
|
].filter(Boolean);
|
|
29035
29123
|
return [
|
|
29036
29124
|
...env,
|
|
@@ -29309,7 +29397,6 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
29309
29397
|
|
|
29310
29398
|
},
|
|
29311
29399
|
"./src/core/transcript.js"(__unused_rspack___webpack_module__, __webpack_exports__, __webpack_require__) {
|
|
29312
|
-
// Shared by `gbot thread` and the grok-bot plugin's gbot_thread tool.
|
|
29313
29400
|
/** Coerce anything to a string without throwing (numbers, BigInt, unserializable objects). */ function toSafeText(value) {
|
|
29314
29401
|
if (typeof value === "string") return value;
|
|
29315
29402
|
if (value == null) return "";
|
|
@@ -29347,15 +29434,12 @@ function entryTextRaw(e) {
|
|
|
29347
29434
|
return typeof e.preview === "string" ? e.preview : "";
|
|
29348
29435
|
}
|
|
29349
29436
|
function transcriptEntries(payload) {
|
|
29350
|
-
if (Array.isArray(payload)) return payload;
|
|
29351
29437
|
if (!payload || typeof payload !== "object") return [];
|
|
29352
|
-
|
|
29353
|
-
return Array.isArray(entries) ? entries : [];
|
|
29438
|
+
return Array.isArray(payload.entries) ? payload.entries : [];
|
|
29354
29439
|
}
|
|
29355
29440
|
function sourceEntryId(entry) {
|
|
29356
29441
|
if (!entry || typeof entry !== "object") return "";
|
|
29357
|
-
|
|
29358
|
-
return typeof entry.messageId === "string" ? entry.messageId : "";
|
|
29442
|
+
return typeof entry.id === "string" ? entry.id : "";
|
|
29359
29443
|
}
|
|
29360
29444
|
function lastSourceId(entries, fallback = "") {
|
|
29361
29445
|
for(let index = entries.length - 1; index >= 0; index -= 1){
|
|
@@ -29420,6 +29504,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
29420
29504
|
* never *.cursorvm.com, so a CURSOR_ACCESS_TOKEN cannot be pointed at a box host.
|
|
29421
29505
|
* Local/dev gateways: http(s)://127.0.0.1|localhost|::1 when GROK_BOT_ALLOW_LOCAL_GATEWAY=1.
|
|
29422
29506
|
* Escape hatch: GROK_BOT_ALLOW_ANY_GATEWAY=1 (unsafe; disables host checks; warns once).
|
|
29507
|
+
* Test mode (GROK_BOT_TEST=1 or NODE_ENV=test): loopback only, for gateway and backend
|
|
29508
|
+
* alike, and the escape hatches are ignored — a test can never reach a live thread.
|
|
29423
29509
|
*/ function truthyEnv(name) {
|
|
29424
29510
|
const v = (process.env[name] || "").trim().toLowerCase();
|
|
29425
29511
|
return v === "1" || v === "true" || v === "yes";
|
|
@@ -29430,6 +29516,9 @@ function allowAnyGateway() {
|
|
|
29430
29516
|
function allowLocalGateway() {
|
|
29431
29517
|
return truthyEnv("GROK_BOT_ALLOW_LOCAL_GATEWAY");
|
|
29432
29518
|
}
|
|
29519
|
+
function testMode() {
|
|
29520
|
+
return truthyEnv("GROK_BOT_TEST") || process.env.NODE_ENV === "test";
|
|
29521
|
+
}
|
|
29433
29522
|
const warned = new Set();
|
|
29434
29523
|
function warnOnce(key, message) {
|
|
29435
29524
|
if (warned.has(key)) return;
|
|
@@ -29469,6 +29558,12 @@ function isCursorGatewayHostname(hostname) {
|
|
|
29469
29558
|
if (parsed.username || parsed.password) {
|
|
29470
29559
|
throw new Error("Rejected " + label + ": userinfo is not allowed.");
|
|
29471
29560
|
}
|
|
29561
|
+
if (testMode()) {
|
|
29562
|
+
if (!isLocalHostname(parsed.hostname) || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
29563
|
+
throw new Error("Rejected " + label + " host \"" + parsed.hostname + "\": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.");
|
|
29564
|
+
}
|
|
29565
|
+
return String(rawUrl).replace(/\/$/, "");
|
|
29566
|
+
}
|
|
29472
29567
|
if (allowAnyGateway()) {
|
|
29473
29568
|
warnOnce("ALLOW_ANY", "warning: GROK_BOT_ALLOW_ANY_GATEWAY is set; credential host checks are disabled.");
|
|
29474
29569
|
return String(rawUrl).replace(/\/$/, "");
|