@commonlyai/cli 0.1.224 → 0.1.226
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/package.json +1 -1
- package/src/commands/dev.js +3 -0
- package/src/lib/adapters/goose.js +150 -25
- package/src/lib/default-environment.js +6 -4
package/package.json
CHANGED
package/src/commands/dev.js
CHANGED
|
@@ -440,12 +440,15 @@ export const bootstrapClawdbotRuntime = async ({
|
|
|
440
440
|
throw new Error('Runtime token was not returned by the runtime-tokens route');
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// Rotated like the runtime token above: an existing user token is stored
|
|
444
|
+
// only as a hash, so asking without force returns none to write here.
|
|
443
445
|
const userIssued = await client.post(
|
|
444
446
|
`/api/registry/pods/${pod.podId}/agents/${CLAWDBOT_AGENT_NAME}/user-token`,
|
|
445
447
|
{
|
|
446
448
|
instanceId,
|
|
447
449
|
displayName,
|
|
448
450
|
scopes: CLAWDBOT_USER_TOKEN_SCOPES,
|
|
451
|
+
force: true,
|
|
449
452
|
},
|
|
450
453
|
);
|
|
451
454
|
const userToken = userIssued.token;
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
/** Goose 1.54.0 wrapper. Public seats
|
|
1
|
+
/** Goose 1.54.0 wrapper. Public seats expose only the Commonly MCP extension. */
|
|
2
2
|
import { requireSeatModel } from './seat-model.js';
|
|
3
3
|
import { spawn as childSpawn, spawnSync } from 'node:child_process';
|
|
4
|
-
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
6
|
+
import { isAbsolute, join } from 'node:path';
|
|
7
7
|
import { StringDecoder } from 'node:string_decoder';
|
|
8
8
|
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
9
9
|
import { effectiveSandboxTrust, refuseWorkspaceTrust, validateEnvironmentSpec } from '../environment.js';
|
|
10
10
|
import { writeCredentialFile } from '../credential-file.js';
|
|
11
11
|
import { deliverSeatCredential, withholdRuntimeCredential } from '../mcp-credential-delivery.js';
|
|
12
|
-
import { prepareMcpSpawn } from '../mcp-home.js';
|
|
13
|
-
import {
|
|
12
|
+
import { prepareMcpSpawn, selectedMcpVersionDir } from '../mcp-home.js';
|
|
13
|
+
import { describeMcpCommand } from '../mcp-server-version.js';
|
|
14
|
+
import { resolveProviderKeyFile, startProviderProxy } from '../provider-proxy.js';
|
|
15
|
+
import { resolvePublicSandboxMode } from '../sandbox/mode.js';
|
|
16
|
+
import { detectBwrap, wrapArgvWithBwrap } from '../sandbox/bwrap.js';
|
|
17
|
+
import { wrapArgvWithSeatbelt } from '../sandbox/seatbelt.js';
|
|
14
18
|
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
15
19
|
|
|
16
20
|
// A version enters this list only after a real headless turn with that binary.
|
|
@@ -21,14 +25,51 @@ const DEFAULT_PROVIDER_HOST = 'https://litellm.commonly.me';
|
|
|
21
25
|
const DEFAULT_PROVIDER_KEY = 'COMMONLY_LITELLM_KEY';
|
|
22
26
|
const PLACEHOLDER_RE = /\$\{(COMMONLY_[A-Z_]+)\}/g;
|
|
23
27
|
const SAFE_ENV = /^(?:PATH|LANG|LC_[A-Z_]+|TERM|USER|LOGNAME|NO_COLOR|CI|SSL_CERT_FILE|SSL_CERT_DIR)$/;
|
|
28
|
+
const safeEnv = (env) => Object.fromEntries(Object.entries(env).filter(([name]) => SAFE_ENV.test(name)));
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
// Goose renders a recipe file as a template before it reads it, so a stranger's
|
|
31
|
+
// `{{ … }}` or `{% include %}` in the prompt would run. Every `{` is written as
|
|
32
|
+
// an expression that prints one, which renders back to exactly the given text.
|
|
33
|
+
const templateLiteral = (value) => {
|
|
34
|
+
if (typeof value === 'string') return value.replace(/\{/g, "{{ '{' }}");
|
|
35
|
+
if (Array.isArray(value)) return value.map(templateLiteral);
|
|
36
|
+
if (value && typeof value === 'object') {
|
|
37
|
+
return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, templateLiteral(item)]));
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
};
|
|
41
|
+
// Explicit API-only list: new MCP tools do not silently gain public authority.
|
|
42
|
+
// attach_file reads this process's filesystem, including its credential file.
|
|
43
|
+
export const PUBLIC_GOOSE_TOOLS = Object.freeze([
|
|
44
|
+
'commonly_get_started', 'commonly_post_message', 'commonly_claim_message',
|
|
45
|
+
'commonly_release_claim', 'commonly_get_messages', 'commonly_get_context',
|
|
46
|
+
'commonly_list_files', 'commonly_list_artifacts', 'commonly_read_file',
|
|
47
|
+
'commonly_get_posts', 'commonly_post_thread_comment', 'commonly_get_tasks',
|
|
48
|
+
'commonly_create_task', 'commonly_claim_task', 'commonly_watch_task',
|
|
49
|
+
'commonly_complete_task', 'commonly_update_task', 'commonly_create_pod',
|
|
50
|
+
'commonly_list_pods', 'commonly_self_install_into_pod', 'commonly_read_agent_memory',
|
|
51
|
+
'commonly_write_agent_memory', 'commonly_save_my_memory', 'commonly_log_cycle',
|
|
52
|
+
'commonly_dm_agent', 'commonly_ask_agent', 'commonly_request_decision',
|
|
53
|
+
'commonly_respond_to_ask', 'commonly_react_to_message',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export const assertSandboxSupported = (environment = {}) => {
|
|
26
57
|
const sandbox = environment?.sandbox || {};
|
|
27
58
|
refuseWorkspaceTrust(sandbox, 'goose');
|
|
28
59
|
if (effectiveSandboxTrust(sandbox.trust) === 'public') {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
if (sandbox.filesystem?.['read-outside']?.length) {
|
|
61
|
+
throw new Error('goose adapter: public seats do not allow sandbox.filesystem.read-outside');
|
|
62
|
+
}
|
|
63
|
+
if (!['workspace', 'read-only', 'bwrap'].includes(resolvePublicSandboxMode(sandbox))) {
|
|
64
|
+
throw new Error('goose adapter: public seats require an enforced sandbox');
|
|
65
|
+
}
|
|
66
|
+
if (!environment.provider || !environment.model) {
|
|
67
|
+
throw new Error('goose adapter: public seats require an explicit provider and model');
|
|
68
|
+
}
|
|
69
|
+
if (sandbox.network?.policy === 'restricted') {
|
|
70
|
+
throw new Error('goose adapter: restricted networking is not supported');
|
|
71
|
+
}
|
|
72
|
+
} else if (sandbox.mode && sandbox.mode !== 'none') {
|
|
32
73
|
throw new Error(`goose adapter: sandbox.mode=${sandbox.mode} cannot be enforced by Goose`);
|
|
33
74
|
}
|
|
34
75
|
};
|
|
@@ -51,7 +92,14 @@ const substitute = (value, ctx) => String(value).replace(PLACEHOLDER_RE, (whole,
|
|
|
51
92
|
/** The flag contains paths and URLs only. Literal MCP secrets cannot ride argv. */
|
|
52
93
|
export const extensionArgs = (servers, ctx) => {
|
|
53
94
|
const args = [];
|
|
95
|
+
if (ctx.publicSeat && (!Array.isArray(servers) || servers.length !== 1)) {
|
|
96
|
+
throw new Error('goose adapter: public seats require only the Commonly MCP extension');
|
|
97
|
+
}
|
|
54
98
|
for (const server of servers || []) {
|
|
99
|
+
if (ctx.publicSeat && (server.name !== 'commonly' || (server.transport || 'stdio') !== 'stdio'
|
|
100
|
+
|| !describeMcpCommand(server.command)?.isCommonly)) {
|
|
101
|
+
throw new Error('goose adapter: public seats require only the Commonly stdio MCP extension');
|
|
102
|
+
}
|
|
55
103
|
if (!/^[A-Za-z0-9_-]{2,}$/.test(server?.name || '')) {
|
|
56
104
|
throw new Error('goose adapter: MCP server needs a safe name');
|
|
57
105
|
}
|
|
@@ -75,7 +123,13 @@ export const extensionArgs = (servers, ctx) => {
|
|
|
75
123
|
if (transport !== 'stdio' || !Array.isArray(server.command) || !server.command.length) {
|
|
76
124
|
throw new Error(`goose adapter: MCP server ${server.name} has an unsupported transport or command`);
|
|
77
125
|
}
|
|
78
|
-
const command = prepareMcpSpawn(server.command, { apiUrl: ctx.instanceUrl });
|
|
126
|
+
const command = prepareMcpSpawn(server.command, { apiUrl: ctx.instanceUrl, publicSeat: ctx.publicSeat });
|
|
127
|
+
if (!command) throw new Error('goose adapter: Commonly MCP is not prepared for this public turn');
|
|
128
|
+
if (ctx.publicSeat) {
|
|
129
|
+
const tree = selectedMcpVersionDir(command);
|
|
130
|
+
if (!tree || !isAbsolute(tree)) throw new Error('goose adapter: public MCP requires an absolute prepared entry');
|
|
131
|
+
ctx.mcpReadDirectories?.push(tree);
|
|
132
|
+
}
|
|
79
133
|
const delivery = deliverSeatCredential({ ...server, command }, {
|
|
80
134
|
credentialFile: ctx.credentialFile, label: 'goose',
|
|
81
135
|
});
|
|
@@ -94,7 +148,15 @@ export const extensionArgs = (servers, ctx) => {
|
|
|
94
148
|
return gooseWord(`${name}=${resolved}`);
|
|
95
149
|
});
|
|
96
150
|
const commandWords = command.map((part) => gooseWord(substitute(part, ctx)));
|
|
97
|
-
|
|
151
|
+
if (ctx.publicSeat) {
|
|
152
|
+
ctx.publicExtensions.push({ type: 'stdio', name: server.name, cmd: command[0],
|
|
153
|
+
args: command.slice(1).map((part) => substitute(part, ctx)),
|
|
154
|
+
envs: Object.fromEntries(Object.entries(delivery.env).map(([name, value]) => [name, substitute(value, ctx)])),
|
|
155
|
+
available_tools: PUBLIC_GOOSE_TOOLS,
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
args.push('--with-extension', `${server.name}:${[...env, ...commandWords].join(' ')}`);
|
|
159
|
+
}
|
|
98
160
|
}
|
|
99
161
|
return args;
|
|
100
162
|
};
|
|
@@ -181,7 +243,7 @@ export default {
|
|
|
181
243
|
supportsProvider: true,
|
|
182
244
|
|
|
183
245
|
async validateEnvironment(environment, workspacePath) {
|
|
184
|
-
|
|
246
|
+
assertSandboxSupported(environment);
|
|
185
247
|
const validation = validateEnvironmentSpec(environment || {});
|
|
186
248
|
if (!validation.ok) throw new Error(validation.errors.join('; '));
|
|
187
249
|
return resolveProviderKeyFile(environment?.provider, workspacePath, 'Goose');
|
|
@@ -197,61 +259,124 @@ export default {
|
|
|
197
259
|
async spawn(prompt, ctx = {}) {
|
|
198
260
|
const seatModel = requireSeatModel(ctx.environment, 'goose', true);
|
|
199
261
|
const keyFile = await this.validateEnvironment(ctx.environment, ctx.cwd);
|
|
262
|
+
const isPublic = effectiveSandboxTrust(ctx.environment?.sandbox?.trust) === 'public';
|
|
200
263
|
const sourceEnv = ctx.env || process.env;
|
|
201
264
|
const configured = ctx.environment?.provider;
|
|
202
|
-
const key = configured
|
|
265
|
+
const key = isPublic ? '' : configured
|
|
203
266
|
? (await readFile(keyFile, 'utf8')).trim() : sourceEnv[DEFAULT_PROVIDER_KEY];
|
|
204
|
-
if (!key) throw new Error(`goose adapter: ${configured ? 'provider.keyFile' : DEFAULT_PROVIDER_KEY} is missing`);
|
|
267
|
+
if (!isPublic && !key) throw new Error(`goose adapter: ${configured ? 'provider.keyFile' : DEFAULT_PROVIDER_KEY} is missing`);
|
|
205
268
|
const baseUrl = new URL(configured?.baseURL || `${DEFAULT_PROVIDER_HOST}/v1`);
|
|
206
269
|
if (!/^https?:$/.test(baseUrl.protocol) || baseUrl.username || baseUrl.password || baseUrl.search || baseUrl.hash) {
|
|
207
270
|
throw new Error('goose adapter: provider.baseURL must be a plain HTTP(S) URL');
|
|
208
271
|
}
|
|
209
272
|
const model = seatModel;
|
|
210
|
-
const binary = ctx._binaryPath ||
|
|
211
|
-
|
|
273
|
+
const binary = ctx._binaryPath || (isPublic
|
|
274
|
+
? String(spawnSync('which', ['goose'], { encoding: 'utf8', env: sourceEnv }).stdout || '').trim()
|
|
275
|
+
: 'goose');
|
|
276
|
+
if (isPublic && !isAbsolute(binary)) throw new Error('goose adapter: public seats require an absolute Goose executable');
|
|
277
|
+
// A cold first launch can take several seconds while macOS scans the binary.
|
|
278
|
+
const version = ctx._version || versionOf(spawnSync(binary, ['--version'], {
|
|
279
|
+
encoding: 'utf8', timeout: 30000, env: safeEnv(sourceEnv),
|
|
280
|
+
}).stdout);
|
|
212
281
|
if (!TESTED_GOOSE_VERSIONS.includes(version)) {
|
|
282
|
+
if (isPublic) throw new Error(`goose adapter: public seats refuse untested version ${version || 'unknown'}`);
|
|
213
283
|
// eslint-disable-next-line no-console
|
|
214
284
|
console.warn(`[goose] installed ${version || 'unknown'} differs from smoke-tested ${TESTED_GOOSE_VERSIONS.join(', ')}; private seat continues`);
|
|
215
285
|
}
|
|
216
286
|
const home = await mkdtemp(join(tmpdir(), 'commonly-goose-'));
|
|
287
|
+
let proxy;
|
|
217
288
|
try {
|
|
218
289
|
const credential = writeCredentialFile(ctx.runtimeToken, {
|
|
219
290
|
root: home, agentName: ctx.agentName || 'agent',
|
|
220
291
|
});
|
|
292
|
+
if (isPublic) proxy = await startProviderProxy({
|
|
293
|
+
provider: configured, keyFile, tokenFile: join(home, 'provider-proxy-token'), adapterLabel: 'Goose',
|
|
294
|
+
pinnedModel: model,
|
|
295
|
+
});
|
|
296
|
+
const mcpReadDirectories = [];
|
|
297
|
+
const publicExtensions = [];
|
|
298
|
+
// Node's compiled-in OpenSSL config can sit outside the permitted tree
|
|
299
|
+
// (including hostedtoolcache on CI). Use our empty config, retaining
|
|
300
|
+
// Node's bundled TLS roots, instead of granting host configuration reads.
|
|
301
|
+
const opensslConfig = join(home, 'openssl.cnf');
|
|
302
|
+
if (isPublic) await writeFile(opensslConfig, '', { mode: 0o600 });
|
|
303
|
+
// The recipe's own directory is the template include root, so it holds
|
|
304
|
+
// the recipe alone, never the proxy token or the credential file.
|
|
305
|
+
const recipeDir = join(home, 'recipe');
|
|
306
|
+
const recipePath = join(recipeDir, 'public-recipe.json');
|
|
221
307
|
const args = [
|
|
222
|
-
'run', '--no-session', '--no-profile', '--with-builtin', 'developer',
|
|
308
|
+
'run', '--no-session', ...(!isPublic ? ['--no-profile', '--with-builtin', 'developer'] : []),
|
|
309
|
+
...(isPublic ? ['--recipe', recipePath] : []),
|
|
223
310
|
'--output-format', 'stream-json', '--provider', 'openai', '--model', model,
|
|
224
311
|
...extensionArgs(ctx.environment?.mcp, {
|
|
225
312
|
credentialFile: credential?.path || '', instanceUrl: ctx.instanceUrl || '',
|
|
226
313
|
runtimeToken: ctx.runtimeToken || '',
|
|
314
|
+
publicSeat: isPublic, mcpReadDirectories, publicExtensions,
|
|
227
315
|
}),
|
|
228
|
-
'-t', buildMemoryPreamble(prompt, ctx.memoryLongTerm, { freshSession: true }),
|
|
316
|
+
...(!isPublic ? ['-t', buildMemoryPreamble(prompt, ctx.memoryLongTerm, { freshSession: true })] : []),
|
|
229
317
|
];
|
|
318
|
+
if (isPublic) await mkdir(recipeDir, { mode: 0o700 });
|
|
319
|
+
if (isPublic) await writeFile(recipePath, JSON.stringify(templateLiteral({ version: '1.0.0',
|
|
320
|
+
title: 'Commonly public seat', description: 'Only audited Commonly API tools are available.',
|
|
321
|
+
instructions: 'Respond to the user using the available Commonly tools.',
|
|
322
|
+
extensions: publicExtensions.map((extension) => ({ ...extension,
|
|
323
|
+
envs: { ...extension.envs, OPENSSL_CONF: opensslConfig },
|
|
324
|
+
})),
|
|
325
|
+
prompt: buildMemoryPreamble(prompt, ctx.memoryLongTerm, { freshSession: true }),
|
|
326
|
+
})), { mode: 0o600 });
|
|
230
327
|
const env = {
|
|
231
|
-
...
|
|
328
|
+
...safeEnv(sourceEnv),
|
|
232
329
|
HOME: home,
|
|
330
|
+
TMPDIR: home,
|
|
233
331
|
XDG_CONFIG_HOME: join(home, 'config'),
|
|
234
332
|
XDG_DATA_HOME: join(home, 'data'),
|
|
333
|
+
GOOSE_PATH_ROOT: home,
|
|
235
334
|
GOOSE_PROVIDER: 'openai',
|
|
236
335
|
GOOSE_MODEL: model,
|
|
237
336
|
GOOSE_MODE: 'auto',
|
|
238
337
|
GOOSE_TELEMETRY_ENABLED: 'false',
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
338
|
+
...(isPublic ? { OPENSSL_CONF: opensslConfig } : {}),
|
|
339
|
+
OPENAI_HOST: proxy ? new URL(proxy.baseURL).origin : baseUrl.origin,
|
|
340
|
+
OPENAI_BASE_PATH: `${new URL(proxy?.baseURL || baseUrl).pathname.replace(/\/$/, '')}/chat/completions`.replace(/^\//, ''),
|
|
341
|
+
OPENAI_API_KEY: proxy ? (await readFile(proxy.tokenFile, 'utf8')).trim() : key,
|
|
242
342
|
};
|
|
243
343
|
withholdRuntimeCredential(env, { credentialFile: credential?.path || null });
|
|
244
344
|
const requestedTimeout = Number(ctx.timeoutMs);
|
|
245
345
|
const timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout > 0
|
|
246
346
|
? Math.min(requestedTimeout, DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
347
|
+
let executable = binary;
|
|
348
|
+
let spawnArgs = args;
|
|
349
|
+
if (isPublic) {
|
|
350
|
+
const sandbox = { ...ctx.environment.sandbox, trust: 'public' };
|
|
351
|
+
const mode = resolvePublicSandboxMode(sandbox, ctx._platform || process.platform);
|
|
352
|
+
if (mode === 'bwrap') {
|
|
353
|
+
const available = (ctx._detectBwrap || detectBwrap)();
|
|
354
|
+
if (!available.available) throw new Error(`goose adapter: bwrap unavailable: ${available.error}`);
|
|
355
|
+
[executable, ...spawnArgs] = (ctx._wrapArgvWithBwrap || wrapArgvWithBwrap)([binary, ...args], {
|
|
356
|
+
...ctx.environment, sandbox: { ...sandbox, filesystem: {
|
|
357
|
+
...(sandbox.filesystem || {}), 'write-outside': [home],
|
|
358
|
+
} },
|
|
359
|
+
}, { workspacePath: ctx.cwd, homePath: home,
|
|
360
|
+
readOnlyPaths: [binary, process.execPath, ...mcpReadDirectories] });
|
|
361
|
+
} else {
|
|
362
|
+
if ((ctx._platform || process.platform) !== 'darwin') throw new Error('goose adapter: Seatbelt requires macOS');
|
|
363
|
+
[executable, ...spawnArgs] = (ctx._wrapArgvWithSeatbelt || wrapArgvWithSeatbelt)([binary, ...args], {
|
|
364
|
+
workspacePath: ctx.cwd, workspaceAccess: mode === 'read-only' ? 'read' : 'write',
|
|
365
|
+
executablePath: binary, statePath: home, mcpConfigDir: home,
|
|
366
|
+
loopbackNetworkPorts: [proxy.port], readOnlyDirectories: mcpReadDirectories,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
247
370
|
const text = await runGoose({
|
|
248
|
-
binary, args, cwd: ctx.cwd, env, timeoutMs,
|
|
249
|
-
credentials: [...spawnCredentials(ctx, [DEFAULT_PROVIDER_KEY]), key],
|
|
371
|
+
binary: executable, args: spawnArgs, cwd: ctx.cwd, env, timeoutMs,
|
|
372
|
+
credentials: [...spawnCredentials(ctx, [DEFAULT_PROVIDER_KEY]), key, env.OPENAI_API_KEY],
|
|
250
373
|
spawnImpl: ctx._spawnImpl,
|
|
251
374
|
});
|
|
252
375
|
return { text, newSessionId: null };
|
|
253
376
|
} finally {
|
|
254
|
-
|
|
377
|
+
try { await proxy?.close(); } finally {
|
|
378
|
+
await rm(home, { recursive: true, force: true });
|
|
379
|
+
}
|
|
255
380
|
}
|
|
256
381
|
},
|
|
257
382
|
};
|
|
@@ -34,8 +34,8 @@ export const ADAPTERS_WITH_DEFAULT_MCP = new Set(['claude', 'codex', 'pi', 'open
|
|
|
34
34
|
* The adapters that can ENFORCE the default sandbox — a strict subset of the
|
|
35
35
|
* ones that consume `mcp[]`.
|
|
36
36
|
*
|
|
37
|
-
* `pi`
|
|
38
|
-
*
|
|
37
|
+
* `pi` is absent deliberately: it cannot enforce the declared sandbox.
|
|
38
|
+
* Goose uses Seatbelt/bwrap and exposes only its prepared Commonly MCP extension.
|
|
39
39
|
* `pi` has no sandbox path until #1740's transport
|
|
40
40
|
* work gives it one, and since #1727 it REFUSES TO START on a spec that
|
|
41
41
|
* declares one: `assertNoSandboxDeclared` in adapters/pi.js throws on an
|
|
@@ -75,8 +75,10 @@ export const ADAPTERS_WITH_GRANT_BROKER = new Set(['claude', 'codex']);
|
|
|
75
75
|
* moves a host fact into the database and breaks the day the seat is re-homed.
|
|
76
76
|
* An explicit mode in a record still wins over the derived one.
|
|
77
77
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
78
|
+
* Claude, codex, opencode and qwen receive this default block
|
|
79
|
+
* (ADAPTERS_WITH_DEFAULT_SANDBOX). Goose supports explicitly configured public
|
|
80
|
+
* confinement, but defaults cannot supply its required provider and model.
|
|
81
|
+
* Grant-broker eligibility is
|
|
80
82
|
* narrower until the server admits OpenCode after its confinement proof.
|
|
81
83
|
* A pi seat must never be handed this block: it fails closed on a declared
|
|
82
84
|
* sandbox rather than run under a spec it cannot honour.
|