@bubblebrain-ai/bubble 0.0.35 → 0.0.36
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/README.md +27 -7
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -0
- package/dist/agent/profiles.js +25 -11
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/workflow/control.d.ts +14 -0
- package/dist/agent/workflow/control.js +30 -9
- package/dist/agent.d.ts +75 -5
- package/dist/agent.js +313 -61
- package/dist/approval/controller.js +28 -0
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +21 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/context/compact-llm.d.ts +2 -2
- package/dist/external-runtime/grok-acp.d.ts +56 -0
- package/dist/external-runtime/grok-acp.js +386 -0
- package/dist/external-runtime/grok-binary.d.ts +25 -0
- package/dist/external-runtime/grok-binary.js +108 -0
- package/dist/external-runtime/grok-errors.d.ts +7 -0
- package/dist/external-runtime/grok-errors.js +15 -0
- package/dist/external-runtime/grok-input-policy.d.ts +58 -0
- package/dist/external-runtime/grok-input-policy.js +52 -0
- package/dist/external-runtime/grok-ndjson.d.ts +14 -0
- package/dist/external-runtime/grok-ndjson.js +193 -0
- package/dist/external-runtime/grok-network.d.ts +15 -0
- package/dist/external-runtime/grok-network.js +81 -0
- package/dist/external-runtime/grok-preflight.d.ts +28 -0
- package/dist/external-runtime/grok-preflight.js +194 -0
- package/dist/external-runtime/grok-process.d.ts +26 -0
- package/dist/external-runtime/grok-process.js +184 -0
- package/dist/external-runtime/grok-profile.d.ts +32 -0
- package/dist/external-runtime/grok-profile.js +419 -0
- package/dist/external-runtime/grok-provider.d.ts +31 -0
- package/dist/external-runtime/grok-provider.js +41 -0
- package/dist/external-runtime/grok-runtime.d.ts +91 -0
- package/dist/external-runtime/grok-runtime.js +882 -0
- package/dist/external-runtime/index.d.ts +13 -0
- package/dist/external-runtime/index.js +13 -0
- package/dist/external-runtime/session-policy.d.ts +11 -0
- package/dist/external-runtime/session-policy.js +30 -0
- package/dist/external-runtime/types.d.ts +65 -0
- package/dist/external-runtime/types.js +1 -0
- package/dist/feishu/agent-host/approval-card.js +21 -0
- package/dist/feishu/agent-host/run-driver.js +19 -3
- package/dist/feishu/serve.js +1 -0
- package/dist/goal/tools.d.ts +6 -3
- package/dist/goal/tools.js +7 -19
- package/dist/main.js +105 -27
- package/dist/memory/db.d.ts +2 -0
- package/dist/memory/db.js +10 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/phase1.js +14 -2
- package/dist/memory/phase2.js +21 -2
- package/dist/memory/prompts.js +1 -1
- package/dist/memory/session-policy.d.ts +25 -0
- package/dist/memory/session-policy.js +55 -0
- package/dist/memory/storage.d.ts +1 -0
- package/dist/memory/storage.js +13 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +87 -41
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +11 -0
- package/dist/oauth/grok.d.ts +25 -0
- package/dist/oauth/grok.js +218 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/storage.d.ts +9 -0
- package/dist/oauth/storage.js +25 -0
- package/dist/orchestrator/default-hooks.js +17 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.js +22 -3
- package/dist/prompt/environment.js +0 -4
- package/dist/prompt/reminders.d.ts +0 -1
- package/dist/prompt/reminders.js +1 -11
- package/dist/prompt/routing.d.ts +14 -0
- package/dist/prompt/routing.js +71 -0
- package/dist/prompt/runtime.js +1 -1
- package/dist/prompt/task-reminders.d.ts +19 -0
- package/dist/prompt/task-reminders.js +49 -0
- package/dist/provider-ai-sdk.js +2 -2
- package/dist/provider-grok.d.ts +26 -0
- package/dist/provider-grok.js +59 -0
- package/dist/provider-openai-codex.d.ts +17 -1
- package/dist/provider-openai-codex.js +101 -29
- package/dist/provider-registry.d.ts +72 -0
- package/dist/provider-registry.js +336 -59
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +28 -5
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +14 -0
- package/dist/session-types.d.ts +19 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +4 -0
- package/dist/slash-commands/commands.js +224 -14
- package/dist/slash-commands/types.d.ts +14 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.js +9 -11
- package/dist/tools/bash.js +1 -1
- package/dist/tools/child-tools.js +2 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +7 -5
- package/dist/tools/lsp.js +1 -0
- package/dist/tools/memory.d.ts +7 -2
- package/dist/tools/memory.js +28 -43
- package/dist/tools/server.js +4 -0
- package/dist/tools/tool-search.d.ts +1 -1
- package/dist/tools/tool-search.js +23 -3
- package/dist/tui/model-switch.d.ts +7 -0
- package/dist/tui/model-switch.js +15 -5
- package/dist/tui-ink/app.d.ts +3 -1
- package/dist/tui-ink/app.js +648 -232
- package/dist/tui-ink/approval/approval-dialog.js +36 -0
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +7 -2
- package/dist/tui-ink/input-box.d.ts +35 -1
- package/dist/tui-ink/input-box.js +215 -122
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +453 -123
- package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
- package/dist/tui-ink/paste-operation-tracker.js +25 -0
- package/dist/tui-ink/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +31 -0
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/types.d.ts +5 -5
- package/dist/types.js +1 -1
- package/dist/variant/thinking-level.d.ts +2 -2
- package/dist/variant/thinking-level.js +2 -1
- package/dist/variant/variant-resolver.d.ts +9 -0
- package/dist/variant/variant-resolver.js +17 -0
- package/package.json +2 -1
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class GrokRuntimeError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
diagnostic;
|
|
4
|
+
constructor(code, message, diagnostic) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "GrokRuntimeError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.diagnostic = diagnostic;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function safeErrorMessage(error) {
|
|
12
|
+
if (error instanceof GrokRuntimeError)
|
|
13
|
+
return error.message;
|
|
14
|
+
return "Grok runtime is unavailable.";
|
|
15
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Commands that remain local while a Grok subscription session is bound. */
|
|
2
|
+
export declare const GROK_LOCAL_COMMAND_HELP: readonly [{
|
|
3
|
+
readonly name: "help";
|
|
4
|
+
readonly usage: "/help";
|
|
5
|
+
readonly description: "Show commands";
|
|
6
|
+
}, {
|
|
7
|
+
readonly name: "model";
|
|
8
|
+
readonly usage: "/model";
|
|
9
|
+
readonly description: "Choose the Grok subscription model and reasoning effort";
|
|
10
|
+
}, {
|
|
11
|
+
readonly name: "provider";
|
|
12
|
+
readonly usage: "/provider";
|
|
13
|
+
readonly description: "Start a fresh native session and manage providers";
|
|
14
|
+
}, {
|
|
15
|
+
readonly name: "login";
|
|
16
|
+
readonly usage: "/login [openai|grok]";
|
|
17
|
+
readonly description: "Start a fresh session with that login";
|
|
18
|
+
}, {
|
|
19
|
+
readonly name: "logout";
|
|
20
|
+
readonly usage: "/logout grok";
|
|
21
|
+
readonly description: "Remove this device's local Grok login";
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "session";
|
|
24
|
+
readonly usage: "/session";
|
|
25
|
+
readonly description: "Browse sessions";
|
|
26
|
+
}, {
|
|
27
|
+
readonly name: "theme";
|
|
28
|
+
readonly usage: "/theme";
|
|
29
|
+
readonly description: "Pick the color theme";
|
|
30
|
+
}, {
|
|
31
|
+
readonly name: "feedback";
|
|
32
|
+
readonly usage: "/feedback";
|
|
33
|
+
readonly description: "Send product feedback";
|
|
34
|
+
}, {
|
|
35
|
+
readonly name: "quit";
|
|
36
|
+
readonly usage: "/quit";
|
|
37
|
+
readonly description: "Exit Bubble";
|
|
38
|
+
}, {
|
|
39
|
+
readonly name: "exit";
|
|
40
|
+
readonly usage: "/exit";
|
|
41
|
+
readonly description: "Exit Bubble";
|
|
42
|
+
}];
|
|
43
|
+
export declare const GROK_LOCAL_SLASH_COMMANDS: ReadonlySet<string>;
|
|
44
|
+
export type GrokInputDecision = {
|
|
45
|
+
kind: "prompt";
|
|
46
|
+
} | {
|
|
47
|
+
kind: "local_command";
|
|
48
|
+
command: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "blocked";
|
|
51
|
+
message: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function grokSlashCommandName(input: string): string | undefined;
|
|
54
|
+
export declare function isGrokLocalSlashCommand(command: string): boolean;
|
|
55
|
+
export declare function classifyGrokInput(input: {
|
|
56
|
+
text: string;
|
|
57
|
+
imageCount: number;
|
|
58
|
+
}): GrokInputDecision;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Commands that remain local while a Grok subscription session is bound. */
|
|
2
|
+
export const GROK_LOCAL_COMMAND_HELP = [
|
|
3
|
+
{ name: "help", usage: "/help", description: "Show commands" },
|
|
4
|
+
{ name: "model", usage: "/model", description: "Choose the Grok subscription model and reasoning effort" },
|
|
5
|
+
{ name: "provider", usage: "/provider", description: "Start a fresh native session and manage providers" },
|
|
6
|
+
{ name: "login", usage: "/login [openai|grok]", description: "Start a fresh session with that login" },
|
|
7
|
+
{ name: "logout", usage: "/logout grok", description: "Remove this device's local Grok login" },
|
|
8
|
+
{ name: "session", usage: "/session", description: "Browse sessions" },
|
|
9
|
+
{ name: "theme", usage: "/theme", description: "Pick the color theme" },
|
|
10
|
+
{ name: "feedback", usage: "/feedback", description: "Send product feedback" },
|
|
11
|
+
{ name: "quit", usage: "/quit", description: "Exit Bubble" },
|
|
12
|
+
{ name: "exit", usage: "/exit", description: "Exit Bubble" },
|
|
13
|
+
];
|
|
14
|
+
export const GROK_LOCAL_SLASH_COMMANDS = new Set(GROK_LOCAL_COMMAND_HELP.map((command) => command.name));
|
|
15
|
+
export function grokSlashCommandName(input) {
|
|
16
|
+
const match = /^\s*\/([^\s/]+)/.exec(input);
|
|
17
|
+
return match?.[1]?.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
export function isGrokLocalSlashCommand(command) {
|
|
20
|
+
return GROK_LOCAL_SLASH_COMMANDS.has(command.toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
export function classifyGrokInput(input) {
|
|
23
|
+
if (input.imageCount > 0) {
|
|
24
|
+
return {
|
|
25
|
+
kind: "blocked",
|
|
26
|
+
message: "This Grok runtime does not advertise image or attachment input support.",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const command = grokSlashCommandName(input.text);
|
|
30
|
+
if (command) {
|
|
31
|
+
if (!isGrokLocalSlashCommand(command)) {
|
|
32
|
+
return {
|
|
33
|
+
kind: "blocked",
|
|
34
|
+
message: `/${command} is not available in this Grok workspace session.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (command === "logout") {
|
|
38
|
+
const argument = input.text.trim().split(/\s+/)[1]?.toLowerCase();
|
|
39
|
+
if (argument !== "grok") {
|
|
40
|
+
return {
|
|
41
|
+
kind: "blocked",
|
|
42
|
+
message: "Use /logout grok while Grok subscription chat is active.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { kind: "local_command", command };
|
|
47
|
+
}
|
|
48
|
+
if (input.text.trimStart().startsWith("/")) {
|
|
49
|
+
return { kind: "blocked", message: "Unknown slash command." };
|
|
50
|
+
}
|
|
51
|
+
return { kind: "prompt" };
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyMessage, Stream } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
3
|
+
export declare const GROK_ACP_MAX_LINE_BYTES: number;
|
|
4
|
+
export interface BoundedNdjsonOptions {
|
|
5
|
+
maxLineBytes?: number;
|
|
6
|
+
onProtocolError?: (error: GrokRuntimeError) => void;
|
|
7
|
+
/** Return true to consume a validated envelope before SDK async dispatch. */
|
|
8
|
+
interceptIncoming?: (message: AnyMessage) => boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A credential-safe NDJSON transport. Unlike the SDK convenience transport,
|
|
12
|
+
* this implementation bounds each line and never prints rejected input.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createBoundedGrokNdjsonStream(output: WritableStream<Uint8Array>, input: ReadableStream<Uint8Array>, options?: BoundedNdjsonOptions): Stream;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
2
|
+
export const GROK_ACP_MAX_LINE_BYTES = 1024 * 1024;
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function isJsonRpcId(value) {
|
|
7
|
+
return value === null || typeof value === "string" || (typeof value === "number" && Number.isFinite(value));
|
|
8
|
+
}
|
|
9
|
+
function isJsonRpcMessage(value) {
|
|
10
|
+
if (!isRecord(value) || value.jsonrpc !== "2.0")
|
|
11
|
+
return false;
|
|
12
|
+
if (typeof value.method === "string" && value.method.length > 0) {
|
|
13
|
+
return !("id" in value) || isJsonRpcId(value.id);
|
|
14
|
+
}
|
|
15
|
+
if (!("id" in value) || !isJsonRpcId(value.id))
|
|
16
|
+
return false;
|
|
17
|
+
const hasResult = Object.prototype.hasOwnProperty.call(value, "result");
|
|
18
|
+
const hasError = Object.prototype.hasOwnProperty.call(value, "error");
|
|
19
|
+
if (hasResult === hasError)
|
|
20
|
+
return false;
|
|
21
|
+
if (hasError) {
|
|
22
|
+
return isRecord(value.error)
|
|
23
|
+
&& typeof value.error.code === "number"
|
|
24
|
+
&& typeof value.error.message === "string";
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function malformedMessage() {
|
|
29
|
+
return new GrokRuntimeError("protocol_error", "Grok ACP sent a malformed JSON-RPC message.");
|
|
30
|
+
}
|
|
31
|
+
function oversizedMessage() {
|
|
32
|
+
return new GrokRuntimeError("protocol_error", "Grok ACP message exceeded the safe line limit.");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A credential-safe NDJSON transport. Unlike the SDK convenience transport,
|
|
36
|
+
* this implementation bounds each line and never prints rejected input.
|
|
37
|
+
*/
|
|
38
|
+
export function createBoundedGrokNdjsonStream(output, input, options = {}) {
|
|
39
|
+
const maxLineBytes = options.maxLineBytes ?? GROK_ACP_MAX_LINE_BYTES;
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
42
|
+
let inputReader;
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
let reported = false;
|
|
45
|
+
const pendingRequestIds = new Set();
|
|
46
|
+
const requestKey = (id) => `${typeof id}:${String(id)}`;
|
|
47
|
+
const report = (error) => {
|
|
48
|
+
if (reported)
|
|
49
|
+
return;
|
|
50
|
+
reported = true;
|
|
51
|
+
options.onProtocolError?.(error);
|
|
52
|
+
};
|
|
53
|
+
const readable = new ReadableStream({
|
|
54
|
+
async start(controller) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
let lineBytes = 0;
|
|
57
|
+
const append = (segment) => {
|
|
58
|
+
if (lineBytes + segment.byteLength > maxLineBytes)
|
|
59
|
+
throw oversizedMessage();
|
|
60
|
+
if (segment.byteLength > 0)
|
|
61
|
+
parts.push(new Uint8Array(segment));
|
|
62
|
+
lineBytes += segment.byteLength;
|
|
63
|
+
};
|
|
64
|
+
const consumeLine = () => {
|
|
65
|
+
if (lineBytes === 0)
|
|
66
|
+
return;
|
|
67
|
+
const bytes = new Uint8Array(lineBytes);
|
|
68
|
+
let offset = 0;
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
bytes.set(part, offset);
|
|
71
|
+
offset += part.byteLength;
|
|
72
|
+
}
|
|
73
|
+
parts.length = 0;
|
|
74
|
+
lineBytes = 0;
|
|
75
|
+
let text;
|
|
76
|
+
try {
|
|
77
|
+
text = decoder.decode(bytes).trim();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw malformedMessage();
|
|
81
|
+
}
|
|
82
|
+
if (!text)
|
|
83
|
+
return;
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw malformedMessage();
|
|
90
|
+
}
|
|
91
|
+
if (!isJsonRpcMessage(parsed))
|
|
92
|
+
throw malformedMessage();
|
|
93
|
+
if (!("method" in parsed)) {
|
|
94
|
+
// The pinned CLI emits a small number of internal responses (for
|
|
95
|
+
// example `skills-reload`) that were never requested by Bubble. The
|
|
96
|
+
// SDK logs unknown response IDs verbatim, so consume them here unless
|
|
97
|
+
// the ID belongs to an outgoing Bubble request.
|
|
98
|
+
if (!pendingRequestIds.delete(requestKey(parsed.id)))
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (options.interceptIncoming?.(parsed))
|
|
102
|
+
return;
|
|
103
|
+
controller.enqueue(parsed);
|
|
104
|
+
};
|
|
105
|
+
const reader = input.getReader();
|
|
106
|
+
inputReader = reader;
|
|
107
|
+
try {
|
|
108
|
+
while (!cancelled) {
|
|
109
|
+
const { value, done } = await reader.read();
|
|
110
|
+
if (done)
|
|
111
|
+
break;
|
|
112
|
+
if (!value)
|
|
113
|
+
continue;
|
|
114
|
+
let start = 0;
|
|
115
|
+
for (let index = 0; index < value.byteLength; index += 1) {
|
|
116
|
+
if (value[index] !== 0x0a)
|
|
117
|
+
continue;
|
|
118
|
+
append(value.subarray(start, index));
|
|
119
|
+
consumeLine();
|
|
120
|
+
start = index + 1;
|
|
121
|
+
}
|
|
122
|
+
if (start < value.byteLength)
|
|
123
|
+
append(value.subarray(start));
|
|
124
|
+
}
|
|
125
|
+
if (!cancelled && lineBytes > 0)
|
|
126
|
+
consumeLine();
|
|
127
|
+
if (!cancelled)
|
|
128
|
+
controller.close();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!cancelled) {
|
|
132
|
+
const safe = error instanceof GrokRuntimeError ? error : malformedMessage();
|
|
133
|
+
report(safe);
|
|
134
|
+
await reader.cancel(safe).catch(() => undefined);
|
|
135
|
+
// The typed failure is delivered through onProtocolError. Close the
|
|
136
|
+
// SDK-facing stream cleanly so its router never logs the rejected raw
|
|
137
|
+
// JSON-RPC line as part of an exception path.
|
|
138
|
+
try {
|
|
139
|
+
controller.close();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// The connection may have been concurrently closed by the
|
|
143
|
+
// protocol-error callback. Either state is safely terminal.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (inputReader === reader)
|
|
149
|
+
inputReader = undefined;
|
|
150
|
+
reader.releaseLock();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
async cancel(reason) {
|
|
154
|
+
cancelled = true;
|
|
155
|
+
await inputReader?.cancel(reason);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const writable = new WritableStream({
|
|
159
|
+
async write(message) {
|
|
160
|
+
let bytes;
|
|
161
|
+
try {
|
|
162
|
+
bytes = encoder.encode(`${JSON.stringify(message)}\n`);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
const error = malformedMessage();
|
|
166
|
+
report(error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
if (bytes.byteLength > maxLineBytes) {
|
|
170
|
+
const error = oversizedMessage();
|
|
171
|
+
report(error);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if ("method" in message && "id" in message) {
|
|
175
|
+
pendingRequestIds.add(requestKey(message.id));
|
|
176
|
+
}
|
|
177
|
+
const writer = output.getWriter();
|
|
178
|
+
try {
|
|
179
|
+
await writer.write(bytes);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
writer.releaseLock();
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
async abort(reason) {
|
|
186
|
+
await output.abort(reason);
|
|
187
|
+
},
|
|
188
|
+
async close() {
|
|
189
|
+
await output.close();
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
return { readable, writable };
|
|
193
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type GrokNetworkRouteSource = "direct" | "clash-verge" | "clashx";
|
|
2
|
+
export interface GrokNetworkRoute {
|
|
3
|
+
source: GrokNetworkRouteSource;
|
|
4
|
+
proxy?: string;
|
|
5
|
+
}
|
|
6
|
+
export type GrokNetworkProbe = (proxy?: string) => Promise<boolean>;
|
|
7
|
+
export interface GrokNetworkResolverDependencies {
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
userHome?: string;
|
|
10
|
+
uid?: number;
|
|
11
|
+
probe?: GrokNetworkProbe;
|
|
12
|
+
}
|
|
13
|
+
export declare function probeXaiOidc(proxy?: string, timeoutMs?: number): Promise<boolean>;
|
|
14
|
+
export declare function parseClashHttpProxyPort(contents: string): number | undefined;
|
|
15
|
+
export declare function resolveGrokNetworkRoute(deps?: GrokNetworkResolverDependencies): Promise<GrokNetworkRoute>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
6
|
+
const XAI_OIDC_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
|
|
7
|
+
const MAX_PROXY_CONFIG_BYTES = 64 * 1024;
|
|
8
|
+
export async function probeXaiOidc(proxy, timeoutMs = 5_000) {
|
|
9
|
+
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1_000)).toString();
|
|
10
|
+
const args = [
|
|
11
|
+
"--fail",
|
|
12
|
+
"--silent",
|
|
13
|
+
"--show-error",
|
|
14
|
+
"--output", "/dev/null",
|
|
15
|
+
"--connect-timeout", timeoutSeconds,
|
|
16
|
+
"--max-time", timeoutSeconds,
|
|
17
|
+
"--proto", "=https",
|
|
18
|
+
];
|
|
19
|
+
if (proxy)
|
|
20
|
+
args.push("--proxy", proxy);
|
|
21
|
+
else
|
|
22
|
+
args.push("--noproxy", "*");
|
|
23
|
+
args.push(XAI_OIDC_DISCOVERY_URL);
|
|
24
|
+
return await new Promise((resolve) => {
|
|
25
|
+
execFile("/usr/bin/curl", args, { timeout: timeoutMs + 1_000, maxBuffer: 4 * 1024 }, (error) => resolve(error === null));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function parseClashHttpProxyPort(contents) {
|
|
29
|
+
for (const key of ["mixed-port", "port"]) {
|
|
30
|
+
const match = new RegExp(`^${key}:\\s*([0-9]{1,5})\\s*$`, "m").exec(contents);
|
|
31
|
+
const port = match ? Number(match[1]) : 0;
|
|
32
|
+
if (port >= 1 && port <= 65_535)
|
|
33
|
+
return port;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
async function readOwnedProxyPort(path, uid) {
|
|
38
|
+
try {
|
|
39
|
+
const metadata = await lstat(path);
|
|
40
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid
|
|
41
|
+
|| (metadata.mode & 0o022) !== 0 || metadata.size > MAX_PROXY_CONFIG_BYTES)
|
|
42
|
+
return undefined;
|
|
43
|
+
return parseClashHttpProxyPort(await readFile(path, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function discoverConfiguredLoopbackProxies(deps) {
|
|
50
|
+
if ((deps.platform ?? process.platform) !== "darwin")
|
|
51
|
+
return [];
|
|
52
|
+
const uid = deps.uid ?? process.getuid?.();
|
|
53
|
+
if (uid === undefined)
|
|
54
|
+
return [];
|
|
55
|
+
const home = deps.userHome ?? homedir();
|
|
56
|
+
const configs = [
|
|
57
|
+
{ source: "clash-verge", path: join(home, "Library", "Application Support", "io.github.clash-verge-rev.clash-verge-rev", "config.yaml") },
|
|
58
|
+
{ source: "clashx", path: join(home, "Library", "Application Support", "com.west2online.ClashX", "config.yaml") },
|
|
59
|
+
];
|
|
60
|
+
const candidates = [];
|
|
61
|
+
for (const config of configs) {
|
|
62
|
+
const port = await readOwnedProxyPort(config.path, uid);
|
|
63
|
+
if (port)
|
|
64
|
+
candidates.push({ source: config.source, proxy: `http://127.0.0.1:${port}` });
|
|
65
|
+
}
|
|
66
|
+
return candidates;
|
|
67
|
+
}
|
|
68
|
+
export async function resolveGrokNetworkRoute(deps = {}) {
|
|
69
|
+
const probe = deps.probe ?? probeXaiOidc;
|
|
70
|
+
if (await probe())
|
|
71
|
+
return { source: "direct" };
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const candidate of await discoverConfiguredLoopbackProxies(deps)) {
|
|
74
|
+
if (seen.has(candidate.proxy))
|
|
75
|
+
continue;
|
|
76
|
+
seen.add(candidate.proxy);
|
|
77
|
+
if (await probe(candidate.proxy))
|
|
78
|
+
return candidate;
|
|
79
|
+
}
|
|
80
|
+
throw new GrokRuntimeError("not_authenticated", "Bubble could not reach xAI through this Mac's current network configuration.");
|
|
81
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GrokProfile } from "./grok-profile.js";
|
|
2
|
+
import type { GrokSpawn } from "./grok-process.js";
|
|
3
|
+
/**
|
|
4
|
+
* How a workspace-scoped preflight may relax the empty-surface rule. The
|
|
5
|
+
* pinned Grok CLI always *lists* project-level extensions in `inspect`, even
|
|
6
|
+
* when their loading is disabled, so a workspace containing its own
|
|
7
|
+
* `.grok/skills` or `.agents/skills` directories can never produce an empty
|
|
8
|
+
* report. Loading is prevented instead: every discovered project skill must be
|
|
9
|
+
* written into the profile's `[skills] disabled` list before the sidecar
|
|
10
|
+
* starts, and MCP entries are acceptable only when they come from the
|
|
11
|
+
* cursor/claude compatibility scanners that the profile verifiably disables
|
|
12
|
+
* (agent-stdio sessions additionally use only the MCP servers passed by
|
|
13
|
+
* Bubble, which passes none).
|
|
14
|
+
*/
|
|
15
|
+
export interface GrokWorkspaceExtensionPolicy {
|
|
16
|
+
allowedProjectSkills: ReadonlySet<string>;
|
|
17
|
+
allowCompatMcpServers: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Collect the names of every skill Grok discovered for this workspace so they
|
|
21
|
+
* can be written into the profile's `[skills] disabled` list. Fails closed on
|
|
22
|
+
* any skill that is not a plain project-level discovery inside the workspace
|
|
23
|
+
* scope, or whose name cannot be disabled safely.
|
|
24
|
+
*/
|
|
25
|
+
export declare function collectGrokProjectSkills(payload: unknown, workspace: string): Promise<string[]>;
|
|
26
|
+
export declare function validateGrokInspect(payload: unknown, profile: GrokProfile, workspace?: string, policy?: GrokWorkspaceExtensionPolicy): Promise<void>;
|
|
27
|
+
export declare function runGrokInspectJson(spawn: GrokSpawn, binary: string, profile: GrokProfile, env: NodeJS.ProcessEnv, workspace?: string, signal?: AbortSignal): Promise<unknown>;
|
|
28
|
+
export declare function runGrokPreflight(spawn: GrokSpawn, binary: string, profile: GrokProfile, env: NodeJS.ProcessEnv, workspace?: string, signal?: AbortSignal, policy?: GrokWorkspaceExtensionPolicy): Promise<void>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { resolve, sep } from "node:path";
|
|
3
|
+
import { isSafeGrokSkillName } from "./grok-profile.js";
|
|
4
|
+
import { runGrokCommand } from "./grok-process.js";
|
|
5
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function requireEmptyArray(root, key) {
|
|
10
|
+
const value = root[key];
|
|
11
|
+
if (!Array.isArray(value) || value.length !== 0) {
|
|
12
|
+
throw new GrokRuntimeError("preflight_failed", `Grok isolation check failed: ${key} must be empty.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function sourcePath(value) {
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
// Grok 0.2.93 renders permission origins as
|
|
18
|
+
// "/absolute/file.toml (config)" in inspect JSON.
|
|
19
|
+
return value.replace(/\s+\((?:config|requirements)\)$/, "");
|
|
20
|
+
}
|
|
21
|
+
if (!isRecord(value))
|
|
22
|
+
return undefined;
|
|
23
|
+
for (const key of ["path", "file", "sourcePath"]) {
|
|
24
|
+
if (typeof value[key] === "string")
|
|
25
|
+
return value[key];
|
|
26
|
+
}
|
|
27
|
+
const source = value.source;
|
|
28
|
+
if (typeof source === "string" && source.startsWith("/"))
|
|
29
|
+
return source;
|
|
30
|
+
if (isRecord(source))
|
|
31
|
+
return sourcePath(source);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
async function canonicalOrResolved(path) {
|
|
35
|
+
try {
|
|
36
|
+
return await realpath(path);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return resolve(path);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Render a workspace-controlled path safely inside an error message. */
|
|
43
|
+
function describeForeignPath(path) {
|
|
44
|
+
if (!path)
|
|
45
|
+
return "an unidentified configuration source";
|
|
46
|
+
const printable = path.replace(/[\p{Cc}\p{Cf}]/gu, "?");
|
|
47
|
+
return printable.length > 160 ? `${printable.slice(0, 157)}...` : printable;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The directory subtree Grok legitimately scans for project extensions: the
|
|
51
|
+
* validated project root when the workspace sits inside one, otherwise the
|
|
52
|
+
* workspace itself.
|
|
53
|
+
*/
|
|
54
|
+
async function workspaceExtensionScope(payload, workspace) {
|
|
55
|
+
const expectedCwd = await canonicalOrResolved(workspace);
|
|
56
|
+
if (typeof payload.projectRoot === "string") {
|
|
57
|
+
const projectRoot = await canonicalOrResolved(payload.projectRoot);
|
|
58
|
+
if (expectedCwd === projectRoot || expectedCwd.startsWith(`${projectRoot}${sep}`))
|
|
59
|
+
return projectRoot;
|
|
60
|
+
}
|
|
61
|
+
return expectedCwd;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Collect the names of every skill Grok discovered for this workspace so they
|
|
65
|
+
* can be written into the profile's `[skills] disabled` list. Fails closed on
|
|
66
|
+
* any skill that is not a plain project-level discovery inside the workspace
|
|
67
|
+
* scope, or whose name cannot be disabled safely.
|
|
68
|
+
*/
|
|
69
|
+
export async function collectGrokProjectSkills(payload, workspace) {
|
|
70
|
+
if (!isRecord(payload) || !Array.isArray(payload.skills)) {
|
|
71
|
+
throw new GrokRuntimeError("preflight_failed", "Grok isolation check returned invalid skill data.");
|
|
72
|
+
}
|
|
73
|
+
const scope = await workspaceExtensionScope(payload, workspace);
|
|
74
|
+
const names = new Set();
|
|
75
|
+
for (const entry of payload.skills) {
|
|
76
|
+
if (!isRecord(entry) || typeof entry.name !== "string" || !isSafeGrokSkillName(entry.name)) {
|
|
77
|
+
throw new GrokRuntimeError("preflight_failed", "Grok discovered a skill Bubble cannot disable safely.");
|
|
78
|
+
}
|
|
79
|
+
const source = entry.source;
|
|
80
|
+
if (!isRecord(source) || source.type !== "project" || typeof source.path !== "string") {
|
|
81
|
+
throw new GrokRuntimeError("preflight_failed", `Grok discovered the skill "${entry.name}" outside the workspace project scope.`);
|
|
82
|
+
}
|
|
83
|
+
const path = await canonicalOrResolved(source.path);
|
|
84
|
+
if (path !== scope && !path.startsWith(`${scope}${sep}`)) {
|
|
85
|
+
throw new GrokRuntimeError("preflight_failed", `Grok discovered the skill "${entry.name}" outside the workspace project scope.`);
|
|
86
|
+
}
|
|
87
|
+
names.add(entry.name);
|
|
88
|
+
}
|
|
89
|
+
return [...names].sort();
|
|
90
|
+
}
|
|
91
|
+
export async function validateGrokInspect(payload, profile, workspace = profile.workspace, policy) {
|
|
92
|
+
if (!isRecord(payload))
|
|
93
|
+
throw new GrokRuntimeError("preflight_failed", "Grok isolation check returned invalid data.");
|
|
94
|
+
const expectedCwd = await canonicalOrResolved(workspace);
|
|
95
|
+
if (typeof payload.cwd !== "string" || (await canonicalOrResolved(payload.cwd)) !== expectedCwd) {
|
|
96
|
+
throw new GrokRuntimeError("preflight_failed", "Grok isolation check used an unexpected workspace.");
|
|
97
|
+
}
|
|
98
|
+
if (payload.projectRoot !== null) {
|
|
99
|
+
if (typeof payload.projectRoot !== "string") {
|
|
100
|
+
throw new GrokRuntimeError("preflight_failed", "Grok isolation check returned an invalid project root.");
|
|
101
|
+
}
|
|
102
|
+
const projectRoot = await canonicalOrResolved(payload.projectRoot);
|
|
103
|
+
if (expectedCwd !== projectRoot && !expectedCwd.startsWith(`${projectRoot}${sep}`)) {
|
|
104
|
+
throw new GrokRuntimeError("preflight_failed", "Grok isolation check discovered an unrelated project root.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const key of ["projectInstructions", "hooks", "plugins", "marketplaces", "lspServers"]) {
|
|
108
|
+
requireEmptyArray(payload, key);
|
|
109
|
+
}
|
|
110
|
+
if (policy) {
|
|
111
|
+
for (const name of await collectGrokProjectSkills(payload, workspace)) {
|
|
112
|
+
if (!policy.allowedProjectSkills.has(name)) {
|
|
113
|
+
throw new GrokRuntimeError("preflight_failed", `Grok discovered the workspace skill "${name}" that Bubble has not disabled.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
requireEmptyArray(payload, "skills");
|
|
119
|
+
}
|
|
120
|
+
if (policy?.allowCompatMcpServers) {
|
|
121
|
+
const mcpServers = payload.mcpServers;
|
|
122
|
+
if (!Array.isArray(mcpServers)) {
|
|
123
|
+
throw new GrokRuntimeError("preflight_failed", "Grok isolation check returned invalid MCP data.");
|
|
124
|
+
}
|
|
125
|
+
for (const entry of mcpServers) {
|
|
126
|
+
// Compat-scanner discoveries stay listed even though the disabled
|
|
127
|
+
// compatibility cells (verified below) and Bubble's empty session MCP
|
|
128
|
+
// list keep them from loading. Anything else fails closed.
|
|
129
|
+
if (!isRecord(entry) || (entry.vendor !== "cursor" && entry.vendor !== "claude")) {
|
|
130
|
+
throw new GrokRuntimeError("preflight_failed", "Grok discovered an MCP server it could load.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
requireEmptyArray(payload, "mcpServers");
|
|
136
|
+
}
|
|
137
|
+
const externalCompat = payload.externalCompat;
|
|
138
|
+
if (!isRecord(externalCompat) || !Array.isArray(externalCompat.cells) || externalCompat.cells.length === 0) {
|
|
139
|
+
throw new GrokRuntimeError("preflight_failed", "Grok compatibility scanners could not be verified as disabled.");
|
|
140
|
+
}
|
|
141
|
+
for (const cell of externalCompat.cells) {
|
|
142
|
+
if (!isRecord(cell) || cell.enabled !== false) {
|
|
143
|
+
throw new GrokRuntimeError("preflight_failed", "Grok compatibility scanners are not fully disabled.");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const allowedSources = new Set([
|
|
147
|
+
await canonicalOrResolved(profile.configPath),
|
|
148
|
+
await canonicalOrResolved(profile.requirementsPath),
|
|
149
|
+
]);
|
|
150
|
+
const permissions = payload.permissions;
|
|
151
|
+
if (!isRecord(permissions) || !Array.isArray(permissions.sources)) {
|
|
152
|
+
throw new GrokRuntimeError("preflight_failed", "Grok permission sources could not be verified.");
|
|
153
|
+
}
|
|
154
|
+
for (const source of permissions.sources) {
|
|
155
|
+
const path = sourcePath(source);
|
|
156
|
+
if (!path || !allowedSources.has(await canonicalOrResolved(path))) {
|
|
157
|
+
throw new GrokRuntimeError("preflight_failed", "Grok discovered a permission source outside Bubble's isolated profile.");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(permissions.skipped) && permissions.skipped.length > 0) {
|
|
161
|
+
throw new GrokRuntimeError("preflight_failed", "Grok discovered an unverified permission source.");
|
|
162
|
+
}
|
|
163
|
+
if (permissions.managedSettingsActive === true) {
|
|
164
|
+
throw new GrokRuntimeError("preflight_failed", "Grok discovered active managed settings outside Bubble's isolated profile.");
|
|
165
|
+
}
|
|
166
|
+
const configSources = payload.configSources;
|
|
167
|
+
if (isRecord(configSources) && Array.isArray(configSources.layers)) {
|
|
168
|
+
for (const layer of configSources.layers) {
|
|
169
|
+
const path = sourcePath(layer);
|
|
170
|
+
if (!path || !allowedSources.has(await canonicalOrResolved(path))) {
|
|
171
|
+
// A project-scope config layer overrides the isolated profile's
|
|
172
|
+
// settings (verified on the pinned CLI: it can re-enable disabled
|
|
173
|
+
// skills past requirements.toml), so it can never be allowed.
|
|
174
|
+
throw new GrokRuntimeError("preflight_failed", `Grok would load ${describeForeignPath(path)} as project configuration. Start Bubble in a workspace without its own Grok config file.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export async function runGrokInspectJson(spawn, binary, profile, env, workspace = profile.workspace, signal) {
|
|
180
|
+
const result = await runGrokCommand(spawn, binary, ["--no-auto-update", "--cwd", workspace, "inspect", "--json"], { cwd: workspace, env, signal });
|
|
181
|
+
if (result.code !== 0) {
|
|
182
|
+
throw new GrokRuntimeError("preflight_failed", "Grok isolation check failed to run.", result.stderr);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(result.stdout);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new GrokRuntimeError("preflight_failed", "Grok isolation check returned invalid JSON.");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export async function runGrokPreflight(spawn, binary, profile, env, workspace = profile.workspace, signal, policy) {
|
|
192
|
+
const payload = await runGrokInspectJson(spawn, binary, profile, env, workspace, signal);
|
|
193
|
+
await validateGrokInspect(payload, profile, workspace, policy);
|
|
194
|
+
}
|