@bubblebrain-ai/bubble 0.0.34 → 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/abort-errors.d.ts +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -2
- package/dist/agent/profiles.js +25 -14
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +21 -0
- package/dist/agent/workflow/control.js +50 -8
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +82 -48
- package/dist/agent.js +360 -232
- 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 +5 -3
- package/dist/config.js +10 -5
- 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 +114 -28
- 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 +92 -52
- 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 +23 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +41 -8
- 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 +20 -0
- package/dist/prompt/task-reminders.js +73 -1
- 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 +35 -17
- 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.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +42 -272
- 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 +672 -243
- package/dist/tui-ink/approval/approval-dialog.js +37 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- 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 +36 -7
- package/dist/tui-ink/input-box.js +229 -157
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +464 -128
- 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/plan-confirm.js +1 -1
- 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 +34 -1
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +8 -31
- 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 +22 -0
- package/dist/variant/variant-resolver.js +43 -0
- package/package.json +6 -2
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
|
+
export type GrokSpawn = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
|
|
3
|
+
export interface GrokCommandResult {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
code: number;
|
|
7
|
+
}
|
|
8
|
+
export interface RunGrokCommandOptions {
|
|
9
|
+
cwd: string;
|
|
10
|
+
env: NodeJS.ProcessEnv;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
maxStdoutBytes?: number;
|
|
13
|
+
maxStderrBytes?: number;
|
|
14
|
+
/** Login-only hook. Called once, and only with Bubble's strictly allowlisted xAI OAuth URL. */
|
|
15
|
+
onOAuthAuthorizeUrl?: (url: string) => void;
|
|
16
|
+
/** Keep login stdout inside the process boundary; callers only receive the trusted URL hook above. */
|
|
17
|
+
discardStdout?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare const defaultGrokSpawn: GrokSpawn;
|
|
20
|
+
export declare function sanitizeGrokDiagnostic(input: string, maxLength?: number): string;
|
|
21
|
+
/**
|
|
22
|
+
* Extract only the pinned xAI authorization endpoint. OAuth query values are
|
|
23
|
+
* intentionally treated as opaque and are never copied into diagnostics.
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractGrokOAuthAuthorizeUrl(input: string): string | undefined;
|
|
26
|
+
export declare function runGrokCommand(spawn: GrokSpawn, binary: string, args: readonly string[], options: RunGrokCommandOptions): Promise<GrokCommandResult>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { GrokRuntimeError } from "./grok-errors.js";
|
|
3
|
+
export const defaultGrokSpawn = (command, args, options) => nodeSpawn(command, [...args], { ...options, shell: false });
|
|
4
|
+
function appendBounded(chunks, chunk, used, limit) {
|
|
5
|
+
if (used >= limit)
|
|
6
|
+
return used;
|
|
7
|
+
const remaining = limit - used;
|
|
8
|
+
chunks.push(chunk.subarray(0, remaining));
|
|
9
|
+
return used + Math.min(chunk.byteLength, remaining);
|
|
10
|
+
}
|
|
11
|
+
export function sanitizeGrokDiagnostic(input, maxLength = 2048) {
|
|
12
|
+
let value = input
|
|
13
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
|
|
14
|
+
.replace(/https:\/\/auth\.x\.ai\/oauth2\/authorize[^\s"'<>`]*/gi, "https://auth.x.ai/oauth2/authorize?[redacted]")
|
|
15
|
+
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [redacted]")
|
|
16
|
+
.replace(/\bxai-[A-Za-z0-9_-]+/gi, "xai-[redacted]")
|
|
17
|
+
.replace(/([?&](?:access_token|refresh_token|token|api_key|key|secret|password)=)[^\s&#]+/gi, "$1[redacted]")
|
|
18
|
+
.replace(/(["']?(?:access_token|refresh_token|id_token|token|api_key|secret|password)["']?\s*[:=]\s*["']?)[^"',\s}]+/gi, "$1[redacted]")
|
|
19
|
+
.replace(/\b([A-Z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD|AUTH)[A-Z0-9_]*)\s*=\s*[^\s]+/gi, "$1=[redacted]")
|
|
20
|
+
.replace(/\beyJ[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]{8,})?/g, "[redacted-jwt]")
|
|
21
|
+
.replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/gi, "$1[redacted]@");
|
|
22
|
+
if (value.length > maxLength)
|
|
23
|
+
value = `${value.slice(0, maxLength)}…`;
|
|
24
|
+
return value.trim();
|
|
25
|
+
}
|
|
26
|
+
const GROK_OAUTH_AUTHORIZE_PREFIX = "https://auth.x.ai/oauth2/authorize";
|
|
27
|
+
/**
|
|
28
|
+
* Extract only the pinned xAI authorization endpoint. OAuth query values are
|
|
29
|
+
* intentionally treated as opaque and are never copied into diagnostics.
|
|
30
|
+
*/
|
|
31
|
+
export function extractGrokOAuthAuthorizeUrl(input) {
|
|
32
|
+
let offset = 0;
|
|
33
|
+
while (offset < input.length) {
|
|
34
|
+
const start = input.indexOf(GROK_OAUTH_AUTHORIZE_PREFIX, offset);
|
|
35
|
+
if (start < 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
let end = start + GROK_OAUTH_AUTHORIZE_PREFIX.length;
|
|
38
|
+
while (end < input.length) {
|
|
39
|
+
const char = input[end];
|
|
40
|
+
const code = char.charCodeAt(0);
|
|
41
|
+
if (code <= 0x20 || code === 0x7f || char === '"' || char === "'" || char === "<" || char === ">" || char === "`") {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
end += 1;
|
|
45
|
+
}
|
|
46
|
+
let candidate = input.slice(start, end);
|
|
47
|
+
while (/[),.;\]}]$/.test(candidate))
|
|
48
|
+
candidate = candidate.slice(0, -1);
|
|
49
|
+
try {
|
|
50
|
+
const parsed = new URL(candidate);
|
|
51
|
+
if (parsed.protocol === "https:"
|
|
52
|
+
&& parsed.hostname === "auth.x.ai"
|
|
53
|
+
&& parsed.port === ""
|
|
54
|
+
&& parsed.username === ""
|
|
55
|
+
&& parsed.password === ""
|
|
56
|
+
&& parsed.pathname === "/oauth2/authorize"
|
|
57
|
+
&& parsed.hash === "") {
|
|
58
|
+
return parsed.toString();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Keep scanning: a malformed lookalike must not suppress a later valid URL.
|
|
63
|
+
}
|
|
64
|
+
offset = start + GROK_OAUTH_AUTHORIZE_PREFIX.length;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
export async function runGrokCommand(spawn, binary, args, options) {
|
|
69
|
+
return await new Promise((resolve, reject) => {
|
|
70
|
+
if (options.signal?.aborted) {
|
|
71
|
+
reject(new GrokRuntimeError("cancelled", "Grok command cancelled."));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const child = spawn(binary, args, {
|
|
75
|
+
cwd: options.cwd,
|
|
76
|
+
env: options.env,
|
|
77
|
+
shell: false,
|
|
78
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
79
|
+
});
|
|
80
|
+
const stdout = [];
|
|
81
|
+
const stderr = [];
|
|
82
|
+
const maxStdout = options.maxStdoutBytes ?? 512 * 1024;
|
|
83
|
+
const maxStderr = options.maxStderrBytes ?? 8 * 1024;
|
|
84
|
+
let stdoutBytes = 0;
|
|
85
|
+
let stdoutStoredBytes = 0;
|
|
86
|
+
let stderrBytes = 0;
|
|
87
|
+
let stdoutOverflow = false;
|
|
88
|
+
let oauthStdoutScanBuffer = "";
|
|
89
|
+
let oauthStderrScanBuffer = "";
|
|
90
|
+
let oauthUrlSeen = false;
|
|
91
|
+
let oauthHandlerFailed = false;
|
|
92
|
+
let childError;
|
|
93
|
+
let aborted = false;
|
|
94
|
+
let killTimer;
|
|
95
|
+
const terminate = () => {
|
|
96
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
97
|
+
return;
|
|
98
|
+
child.kill("SIGTERM");
|
|
99
|
+
if (killTimer)
|
|
100
|
+
return;
|
|
101
|
+
killTimer = setTimeout(() => {
|
|
102
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
103
|
+
child.kill("SIGKILL");
|
|
104
|
+
}, 1_000);
|
|
105
|
+
killTimer.unref?.();
|
|
106
|
+
};
|
|
107
|
+
const onAbort = () => {
|
|
108
|
+
aborted = true;
|
|
109
|
+
terminate();
|
|
110
|
+
};
|
|
111
|
+
const scanOAuthChunk = (source, text) => {
|
|
112
|
+
if (oauthUrlSeen || !options.onOAuthAuthorizeUrl)
|
|
113
|
+
return;
|
|
114
|
+
if (source === "stdout") {
|
|
115
|
+
oauthStdoutScanBuffer = `${oauthStdoutScanBuffer}${text}`.slice(-64 * 1024);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
oauthStderrScanBuffer = `${oauthStderrScanBuffer}${text}`.slice(-64 * 1024);
|
|
119
|
+
}
|
|
120
|
+
const url = extractGrokOAuthAuthorizeUrl(source === "stdout" ? oauthStdoutScanBuffer : oauthStderrScanBuffer);
|
|
121
|
+
if (!url)
|
|
122
|
+
return;
|
|
123
|
+
oauthUrlSeen = true;
|
|
124
|
+
try {
|
|
125
|
+
options.onOAuthAuthorizeUrl(url);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
oauthHandlerFailed = true;
|
|
129
|
+
terminate();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
133
|
+
child.stdout?.on("data", (chunk) => {
|
|
134
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
135
|
+
if (!stdoutOverflow && stdoutBytes + bytes.byteLength > maxStdout) {
|
|
136
|
+
stdoutOverflow = true;
|
|
137
|
+
terminate();
|
|
138
|
+
}
|
|
139
|
+
stdoutBytes += bytes.byteLength;
|
|
140
|
+
if (!options.discardStdout) {
|
|
141
|
+
stdoutStoredBytes = appendBounded(stdout, bytes, stdoutStoredBytes, maxStdout);
|
|
142
|
+
}
|
|
143
|
+
scanOAuthChunk("stdout", bytes.toString("utf8"));
|
|
144
|
+
});
|
|
145
|
+
child.stderr?.on("data", (chunk) => {
|
|
146
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
147
|
+
stderrBytes = appendBounded(stderr, bytes, stderrBytes, maxStderr);
|
|
148
|
+
scanOAuthChunk("stderr", bytes.toString("utf8"));
|
|
149
|
+
});
|
|
150
|
+
child.once("error", (error) => {
|
|
151
|
+
childError = error;
|
|
152
|
+
});
|
|
153
|
+
child.once("close", (code, signal) => {
|
|
154
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
155
|
+
if (killTimer)
|
|
156
|
+
clearTimeout(killTimer);
|
|
157
|
+
if (aborted || options.signal?.aborted) {
|
|
158
|
+
reject(new GrokRuntimeError("cancelled", "Grok command cancelled."));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (stdoutOverflow) {
|
|
162
|
+
reject(new GrokRuntimeError("protocol_error", "Grok command output exceeded the safe capture limit."));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (oauthHandlerFailed) {
|
|
166
|
+
reject(new GrokRuntimeError("protocol_error", "Grok OAuth URL handling failed."));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (childError) {
|
|
170
|
+
reject(childError);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (code === null) {
|
|
174
|
+
reject(new GrokRuntimeError("process_crashed", `Grok runtime terminated by ${signal ?? "an unknown signal"}.`));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
resolve({
|
|
178
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
179
|
+
stderr: sanitizeGrokDiagnostic(Buffer.concat(stderr).toString("utf8")),
|
|
180
|
+
code,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface GrokProfile {
|
|
2
|
+
root: string;
|
|
3
|
+
home: string;
|
|
4
|
+
grokHome: string;
|
|
5
|
+
tmp: string;
|
|
6
|
+
workspace: string;
|
|
7
|
+
configPath: string;
|
|
8
|
+
requirementsPath: string;
|
|
9
|
+
lockPath: string;
|
|
10
|
+
}
|
|
11
|
+
export interface GrokProfileLock {
|
|
12
|
+
release(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function isSafeGrokSkillName(name: string): boolean;
|
|
15
|
+
export declare function buildGrokConfigToml(extraDisabledSkills?: readonly string[]): string;
|
|
16
|
+
export declare function buildGrokRequirementsToml(extraDisabledSkills?: readonly string[]): string;
|
|
17
|
+
export declare const GROK_CONFIG_TOML: string;
|
|
18
|
+
export declare const GROK_REQUIREMENTS_TOML: string;
|
|
19
|
+
export declare function getGrokProfile(bubbleHome?: string): GrokProfile;
|
|
20
|
+
/**
|
|
21
|
+
* Establish only the private directory needed to create runtime.lock. Existing
|
|
22
|
+
* profile content is validated but never chmodded or rewritten here, because
|
|
23
|
+
* another Bubble process may already own the lock and be using that profile.
|
|
24
|
+
*/
|
|
25
|
+
export declare function prepareGrokProfileLockRoot(profile?: GrokProfile, uid?: number | undefined): Promise<GrokProfile>;
|
|
26
|
+
export declare function prepareGrokProfile(profile?: GrokProfile, uid?: number | undefined, extraDisabledSkills?: readonly string[]): Promise<GrokProfile>;
|
|
27
|
+
export declare function buildGrokChildEnv(profile: GrokProfile, parentEnv?: NodeJS.ProcessEnv, proxy?: string): NodeJS.ProcessEnv;
|
|
28
|
+
export declare function acquireGrokProfileLock(profile: GrokProfile): Promise<GrokProfileLock>;
|
|
29
|
+
export declare function grokProfileHasAuth(profile: GrokProfile): Promise<boolean>;
|
|
30
|
+
export declare function clearGrokRuntimeData(profile: GrokProfile, includeAuth: boolean): Promise<void>;
|
|
31
|
+
/** Remove extension surfaces that the pinned CLI may materialize at startup. */
|
|
32
|
+
export declare function clearGrokGeneratedExtensions(profile: GrokProfile): Promise<void>;
|