@bakers-build/addon-grok 0.5.1-next.557 → 0.5.1-next.746

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.
@@ -1,7 +1,7 @@
1
- import { type AgentFootprint, type AgentPlugin, type AgentRunIdentity, type BbAgent } from '@bakers-build/conductor/contract';
1
+ import { type AgentFootprint, type AgentPlugin, type AgentRunIdentity, type BbAgent, type RunEnvPolicy, type SandboxHost, type SandboxPreflight, type SecurityOptions } from '@bakers-build/conductor/contract';
2
2
  export { parseGrokTranscript } from './footprint.js';
3
3
  export type GrokReasoningEffort = 'high' | 'medium' | 'low';
4
- export interface GrokAgentOptions {
4
+ export interface GrokAgentOptions extends SecurityOptions {
5
5
  model?: string;
6
6
  reasoningEffort?: GrokReasoningEffort | string;
7
7
  effort?: string;
@@ -15,8 +15,14 @@ export declare class GrokAgent implements BbAgent {
15
15
  private readonly model;
16
16
  private readonly effort;
17
17
  private readonly overrides;
18
+ private readonly security;
19
+ readonly runEnvPolicy: RunEnvPolicy;
18
20
  constructor(options?: GrokAgentOptions, home?: string);
19
21
  launchCommand(prompt: string, run?: AgentRunIdentity): string;
22
+ /** The launch flags a security mode owns (`docs/addons/grok.md`). */
23
+ private securityFlags;
24
+ /** Grok's sandbox is Landlock on Linux and Seatbelt on macOS. */
25
+ sandboxPreflight(host?: SandboxHost): Promise<SandboxPreflight>;
20
26
  /**
21
27
  * `grok --session-id <uuid>` documents "new conversation… must not already
22
28
  * exist" (BB-473 review round 2), so the conductor checks this before
package/dist/src/index.js CHANGED
@@ -1,11 +1,112 @@
1
1
  import { readdir, readFile, stat } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
- import { resolveAgentCliOptions, validateOptionalString, } from '@bakers-build/conductor/contract';
4
+ import { resolveAgentCliOptions, resolveSecurityOptions, systemSandboxHost, validateOptionalString, } from '@bakers-build/conductor/contract';
5
5
  import { shellQuote } from '@bakers-build/core';
6
6
  import { parseGrokTranscript } from './footprint.js';
7
7
  export { parseGrokTranscript } from './footprint.js';
8
- const GROK_RESERVED = ['--permission-mode', '--session-id'];
8
+ const GROK_RESERVED = ['--permission-mode', '--session-id', '-s'];
9
+ /** Flags sandbox mode owns on top of `GROK_RESERVED` (`docs/addons/grok.md`). */
10
+ const GROK_SANDBOX_RESERVED = [
11
+ ...GROK_RESERVED,
12
+ '--sandbox',
13
+ '--always-approve',
14
+ '--allow',
15
+ '--allowedTools',
16
+ '--allowed-tools',
17
+ '--add-dir',
18
+ // the workspace profile makes the cwd writable; agent definitions may carry their own permissions
19
+ '--cwd',
20
+ '-w',
21
+ '--worktree',
22
+ '--agents',
23
+ '--agent',
24
+ ];
25
+ /**
26
+ * Grok splits short-flag clusters (`-cw` is `-c -w`), which the shared matcher
27
+ * only checks at the token's start. Only `-m`/`-p` end the scan: their value
28
+ * follows. Fail-closed: an optional-value flag such as `-r` does not end it.
29
+ */
30
+ function rejectClusteredReserved(args, reserved) {
31
+ const letters = reserved.filter((f) => /^-[^-]$/.test(f)).map((f) => f[1]);
32
+ for (const token of args) {
33
+ if (!/^-[^-]/.test(token))
34
+ continue;
35
+ for (const letter of token.slice(1)) {
36
+ if (letters.includes(letter)) {
37
+ throw new Error(`args contains reserved flag -${letter}`);
38
+ }
39
+ if (letter === 'm' || letter === 'p')
40
+ break;
41
+ }
42
+ }
43
+ }
44
+ /** Grok flags whose value is the next token (`grok --help`, grok 1.0.70). */
45
+ const GROK_VALUE_FLAGS = new Set([
46
+ '--model',
47
+ '--single',
48
+ '--resume',
49
+ '--debug-file',
50
+ '--deny',
51
+ '--disallowedTools',
52
+ '--disallowed-tools',
53
+ '--json-schema',
54
+ '--leader-socket',
55
+ '--max-turns',
56
+ '--output-format',
57
+ '--prompt-file',
58
+ '--prompt-json',
59
+ '--reasoning-effort',
60
+ '--effort',
61
+ '--rules',
62
+ '--system-prompt-override',
63
+ '--system-prompt',
64
+ '--tools',
65
+ '--worktree-ref',
66
+ '--ref',
67
+ ]);
68
+ /**
69
+ * After the prompt, Grok parses a bare token as a subcommand (`wrap`, `agent`),
70
+ * which replaces the sandboxed session. Allowlist: only a value of a known
71
+ * value flag may be bare. A short cluster qualifies only when its last letter
72
+ * is the first that takes a value (`-cm`): in `-pm` the `m` is `-p`'s value.
73
+ * `-c`, `-h`, `-v` are Grok's only short flags without a value.
74
+ */
75
+ function rejectBareArgs(args) {
76
+ args.forEach((token, i) => {
77
+ if (token.startsWith('-'))
78
+ return;
79
+ const prev = args[i - 1] ?? '';
80
+ if (GROK_VALUE_FLAGS.has(prev) || /^-[chv]*[mpr]$/.test(prev))
81
+ return;
82
+ throw new Error(`args contains bare token ${token}, which Grok would run as a subcommand outside the sandboxed session`);
83
+ });
84
+ }
85
+ /** Only what Grok's built-in `workspace` profile enforces. */
86
+ const SANDBOX_DEFAULTS = {
87
+ trusted_commands: [],
88
+ deny_read: [],
89
+ allow_write: [],
90
+ network: ['*'],
91
+ mcp_servers: {},
92
+ };
93
+ /** Throws on a sandbox key Grok cannot enforce from the command line. */
94
+ function rejectUnsupportedSandbox(sandbox) {
95
+ if (sandbox.network.length !== 1 || sandbox.network[0] !== '*') {
96
+ throw new Error("Grok cannot restrict sandbox.network: its workspace profile allows all hosts; leave network at ['*']");
97
+ }
98
+ if (Object.keys(sandbox.mcp_servers).length > 0) {
99
+ throw new Error('Grok cannot allow only named sandbox.mcp_servers: sandbox mode passes a deny rule for every MCP tool; leave mcp_servers empty');
100
+ }
101
+ if (sandbox.trusted_commands.length > 0) {
102
+ throw new Error("Grok has no command exception narrowed to executable, arguments and directory for sandbox.trusted_commands; use security: 'system' with your own sandbox.toml profile");
103
+ }
104
+ for (const key of ['deny_read', 'allow_write']) {
105
+ if (sandbox[key].length > 0) {
106
+ throw new Error(`Grok needs a sandbox.toml profile for sandbox.${key}, which bakers.build does not write; use security: 'system' with args: ['--sandbox', '<profile>']`);
107
+ }
108
+ }
109
+ }
9
110
  const GROK_KNOWN = [
10
111
  { kind: 'model', flags: ['--model', '-m'] },
11
112
  { kind: 'effort', flags: ['--reasoning-effort', '--effort'] },
@@ -39,9 +140,21 @@ export class GrokAgent {
39
140
  model;
40
141
  effort;
41
142
  overrides;
143
+ security;
144
+ runEnvPolicy;
42
145
  constructor(options = {}, home = homedir()) {
43
146
  this.home = home;
44
- const cli = resolveAgentCliOptions(options, GROK_RESERVED, GROK_KNOWN);
147
+ this.security = resolveSecurityOptions(options, SANDBOX_DEFAULTS);
148
+ this.runEnvPolicy = this.security.runEnvPolicy;
149
+ if (this.security.mode === 'custom') {
150
+ throw new Error("Grok has no config flags for security: 'custom'; use security: 'system' with args: ['--sandbox', '<profile>'] for your own sandbox.toml profile");
151
+ }
152
+ rejectUnsupportedSandbox(this.security.sandbox);
153
+ const reserved = this.security.mode === 'sandbox' ? GROK_SANDBOX_RESERVED : GROK_RESERVED;
154
+ const cli = resolveAgentCliOptions(options, reserved, GROK_KNOWN);
155
+ rejectClusteredReserved(cli.args, reserved);
156
+ if (this.security.mode === 'sandbox')
157
+ rejectBareArgs(cli.args);
45
158
  this.args = cli.args;
46
159
  this.model = cli.model;
47
160
  this.effort =
@@ -58,7 +171,7 @@ export class GrokAgent {
58
171
  // dispatched into instead of holding a session there.
59
172
  parts.push(shellQuote(prompt));
60
173
  }
61
- parts.push('--permission-mode', 'bypassPermissions');
174
+ parts.push(...this.securityFlags());
62
175
  if (this.model !== undefined && !this.overrides.model) {
63
176
  parts.push('-m', shellQuote(this.model));
64
177
  }
@@ -77,6 +190,50 @@ export class GrokAgent {
77
190
  }
78
191
  return parts.join(' ');
79
192
  }
193
+ /** The launch flags a security mode owns (`docs/addons/grok.md`). */
194
+ securityFlags() {
195
+ if (this.security.mode === 'system')
196
+ return [];
197
+ if (this.security.mode === 'yolo') {
198
+ return ['--permission-mode', 'bypassPermissions'];
199
+ }
200
+ return [
201
+ '--sandbox',
202
+ 'workspace',
203
+ '--permission-mode',
204
+ 'acceptEdits',
205
+ '--allow',
206
+ shellQuote('Bash(*)'),
207
+ '--deny',
208
+ shellQuote('MCPTool(*)'),
209
+ ];
210
+ }
211
+ /** Grok's sandbox is Landlock on Linux and Seatbelt on macOS. */
212
+ async sandboxPreflight(host = systemSandboxHost()) {
213
+ if (this.security.mode !== 'sandbox' || host.platform === 'darwin') {
214
+ return { ok: true };
215
+ }
216
+ const fallback = "set security: 'system' or 'yolo' for Grok";
217
+ if (host.platform !== 'linux') {
218
+ return {
219
+ ok: false,
220
+ cause: `Grok's sandbox does not support ${host.platform}`,
221
+ fix: fallback,
222
+ };
223
+ }
224
+ const probe = await host.run('grep', [
225
+ '-qw',
226
+ 'landlock',
227
+ '/sys/kernel/security/lsm',
228
+ ]);
229
+ return probe.ok
230
+ ? { ok: true }
231
+ : {
232
+ ok: false,
233
+ cause: 'Landlock is not active in this kernel',
234
+ fix: `boot a kernel with landlock in its lsm= list, or ${fallback}`,
235
+ };
236
+ }
80
237
  /**
81
238
  * `grok --session-id <uuid>` documents "new conversation… must not already
82
239
  * exist" (BB-473 review round 2), so the conductor checks this before
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakers-build/addon-grok",
3
- "version": "0.5.1-next.557",
3
+ "version": "0.5.1-next.746",
4
4
  "description": "bakers.build agent plugin for xAI Grok Build, with a real session footprint.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "directory": "bakers-cli/addons/grok"
21
21
  },
22
22
  "peerDependencies": {
23
- "@bakers-build/conductor": "^0.5.1-next.557",
24
- "@bakers-build/core": "^0.5.1-next.557"
23
+ "@bakers-build/conductor": "^0.5.1-next.746",
24
+ "@bakers-build/core": "^0.5.1-next.746"
25
25
  }
26
26
  }