diff --git a/.changeset/1151-sandbox-tier-load-bearing.md b/.changeset/1151-sandbox-tier-load-bearing.md new file mode 100644 index 000000000..4a3bae65b --- /dev/null +++ b/.changeset/1151-sandbox-tier-load-bearing.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1152 +--- +**`sandbox_mode` emission in Codex TOML is now gated on the runtime descriptor's `sandboxTier` axis** — previously `installCodexConfig` emitted `sandbox_mode` unconditionally from a hardcoded policy map regardless of whether the runtime descriptor declared a sandbox tier, making the descriptor field cosmetic. `resolveInstallPlan` now projects `sandboxTier` from the capability registry, and `generateCodexAgentToml` / `installCodexConfig` gate emission on `sandboxTier !== 'none'`. The per-agent mode table `CODEX_AGENT_SANDBOX` remains GSD agent policy (not a runtime-descriptor property). For codex (`sandboxTier === 'codex-agent-sandbox'`) output is byte-identical to before; for all other runtimes (`sandboxTier === 'none'`) `sandbox_mode` is correctly omitted. `resolveInstallPlan` now fails loud (throws `TypeError`) on a missing or invalid `sandboxTier` descriptor axis rather than silently coercing garbage to `'none'`, preventing a corrupt/stale registry from silently dropping sandbox enforcement. Full removal of the per-agent registration-tax map remains tracked under #1138. (#1151) diff --git a/CONTEXT.md b/CONTEXT.md index 2d90bf216..a7496af9e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -179,7 +179,7 @@ A Capability whose integration shape brings its own external process, service, o Standalone hook-surface writer module extracted from `bin/install.js` as ADR-857 phase 5f-1 (behavior-preserving relocation, no logic change). Owns: Cline rules-body/agents-md/pre-tool-use hook generation (`buildClineRulesBody`, `buildClineAgentsMdBody`, `buildClinePreToolUseHook`, `mergeGsdAgentsMd`, `writeClineArtifacts`); Cursor `hooks.json` lifecycle (`buildCursorHookEntry`, `isManagedCursorHookEntry`, `reconcileCursorHooksJson`, `writeCursorHooksJson`, `removeCursorHooksJson`); Copilot session-hook config (`buildCopilotHookConfig`, `writeCopilotHookConfig`); Codex hook-block and event management (`buildCodexHookBlock`, `rewriteLegacyCodexHookBlock`, `reconcileCodexHooksJsonEvent`, `reconcileCodexHooksJsonSessionStart`, `ensureCodexHooksJsonSessionStart`, `ensureCodexHooksJsonEvent`, `removeCodexHooksJsonEvent`, `removeCodexHooksJsonSessionStart`, `buildCodexHookWindowsShimIR`); and shared hook command helpers (`buildHookCommand`, `rewriteLegacyManagedNodeHookCommands`, `normalizeNodePath`, `resolveNodeRunner`). `bin/install.js` delegates to this module via thin wrappers and re-exports its functions unchanged so existing tests require no modification. Source: `src/runtime-hooks-surface.cts`. Built output: `gsd-core/bin/lib/runtime-hooks-surface.cjs`. ### Runtime Config Adapter Registry -Module owning the explicit per-runtime config-mutation dispatch table for the installer. `resolveRuntimeConfigIntent(runtime)` projects a typed config intent — `installSurface` (`settings-json` | `codex-toml` | `copilot-instructions` | `cline-rules` | `cursor-hooks-json` | `profile-marker-only`), `writesSharedSettings` (the `finishInstall` shared-settings write gate), and `finishPermissionWriter` (`opencode` | `kilo` | none) — that `bin/install.js` dispatches on instead of inline `runtime === '...'` branching. Owns adapter selection only: it performs no filesystem IO and does not execute config mutations (the install/finishInstall handlers and the per-runtime writers do that). Unknown runtimes fail loudly with a `TypeError`, guarded by an `Object.hasOwn` own-property check so prototype-chain keys (`__proto__`, `constructor`) also throw. Also exports `resolveInstallPlan(runtime)` — the ADR-58 `InstallPlan` capstone — which collects the install-level descriptor axes (`installSurface`, `writesSharedSettings`, `finishPermissionWriter`, `hookEvents`, `extendedHookEvents`, `hooksSurface`) into one typed `InstallPlan` value consumed by `install()` and `finishInstall()` in `bin/install.js`. The spatial axes (`configHome`, `artifactLayout`, `commandStyle`) remain behind their self-resolving adapter modules and are not part of the plan; they are the execution adapters. Realizes both the adapter-selection and plan-collection halves of the Runtime Install Policy Module boundary. Source: `gsd-core/bin/lib/runtime-config-adapter-registry.cjs`. See ADR-58, #60. +Module owning the explicit per-runtime config-mutation dispatch table for the installer. `resolveRuntimeConfigIntent(runtime)` projects a typed config intent — `installSurface` (`settings-json` | `codex-toml` | `copilot-instructions` | `cline-rules` | `cursor-hooks-json` | `profile-marker-only`), `writesSharedSettings` (the `finishInstall` shared-settings write gate), and `finishPermissionWriter` (`opencode` | `kilo` | none) — that `bin/install.js` dispatches on instead of inline `runtime === '...'` branching. Owns adapter selection only: it performs no filesystem IO and does not execute config mutations (the install/finishInstall handlers and the per-runtime writers do that). Unknown runtimes fail loudly with a `TypeError`, guarded by an `Object.hasOwn` own-property check so prototype-chain keys (`__proto__`, `constructor`) also throw. Also exports `resolveInstallPlan(runtime)` — the ADR-58 `InstallPlan` capstone — which collects the install-level descriptor axes (`installSurface`, `writesSharedSettings`, `finishPermissionWriter`, `hookEvents`, `extendedHookEvents`, `hooksSurface`, `sandboxTier`) into one typed `InstallPlan` value consumed by `install()` and `finishInstall()` in `bin/install.js`. `sandboxTier` (`none` | `codex-agent-sandbox`) gates per-agent `sandbox_mode` emission in the codex TOML path and fails loud on a missing/invalid value (#1151). The spatial axes (`configHome`, `artifactLayout`, `commandStyle`) remain behind their self-resolving adapter modules and are not part of the plan; they are the execution adapters. Realizes both the adapter-selection and plan-collection halves of the Runtime Install Policy Module boundary. Source: `gsd-core/bin/lib/runtime-config-adapter-registry.cjs`. See ADR-58, #60. ### Claude Code Plugin Manifest Module Module owning the projection of gsd-core's artifact surfaces (`commands`, `agents`, hooks) onto the Claude Code plugin contract (`.claude-plugin/plugin.json` + `hooks/hooks.json`) — the plugin-contract sibling of the Runtime Artifact Layout Module (which projects the same surfaces onto filesystem placements). Defined mapping: `name`=`binName` (drives the `/gsd-core:` command namespace), `repository`/`homepage`=`repoUrl` (Package Identity Module), `version`/`description`/`license` from `package.json` (`version` is required for `claude plugin validate --strict`), `commands`=`./commands/gsd/`, agents via Claude Code's default `agents/` discovery (the explicit string form is schema-rejected), `hooks`=`./hooks/hooks.json`. The hook projection carries ONLY the always-on subset of the Installer Module's Claude `settings.json` wiring (check-update, context-monitor, prompt-guard, read-guard, worktree-path-guard, read-injection-scanner) via `${CLAUDE_PLUGIN_ROOT}`; config-gated opt-in hooks are excluded because a static manifest cannot honor per-project config gates, and plugin-shipped agents cannot carry hook frontmatter (so all plugin-path hook wiring lives in hooks.json). `hooks.json` covers all seven Claude Code lifecycle events: SessionStart, PreToolUse, PostToolUse, SubagentStop, Stop, PreCompact (all wired to context-monitor for context-headroom awareness), and FileChanged (matcher: `config.json` → config-reload, injects `additionalContext` when `.planning/config.json` changes mid-session). Additive — the file-copy path (Runtime Artifact Layout / Install Policy / Installer Modules) is unchanged. Conformance is validated by `claude plugin validate --strict` plus the in-repo drift-guard `tests/issue-766-plugin-manifest.test.cjs`. _Avoid_: "the plugin API", "the plugin file" (when you mean the seam). See ADR-766 and Runtime Artifact Layout Module. diff --git a/bin/install.js b/bin/install.js index 3cd70ac60..463c47f34 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3166,7 +3166,7 @@ purpose: ${toSingleLine(description)} * @param {object|null} runtimeResolver — runtime-aware tier resolver from readGsdRuntimeProfileResolver * @param {object|null} effortCfg — #443: merged effort config from readGsdEffectiveEffortConfig */ -function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null, effortCfg = null) { +function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null, effortCfg = null, sandboxTier = 'codex-agent-sandbox') { const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only'; const { frontmatter, body } = extractFrontmatterAndBody(agentContent); const frontmatterText = frontmatter || ''; @@ -3179,8 +3179,10 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, const lines = [ `name = ${JSON.stringify(resolvedName)}`, `description = ${JSON.stringify(resolvedDescription)}`, - `sandbox_mode = "${sandboxMode}"`, ]; + if (sandboxTier != null && sandboxTier !== 'none') { + lines.push(`sandbox_mode = "${sandboxMode}"`); + } // Embed model override when configured in ~/.gsd/defaults.json so that // model_overrides is respected on Codex (which uses static TOML, not inline @@ -5745,7 +5747,7 @@ function writeCopilotHookConfig(targetDir) { * Generate config.toml and per-agent .toml files for Codex. * Reads agent .md files from source, extracts metadata, writes .toml configs. */ -function installCodexConfig(targetDir, agentsSrc) { +function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-sandbox') { const configPath = path.join(targetDir, 'config.toml'); const agentsTomlDir = path.join(targetDir, 'agents'); fs.mkdirSync(agentsTomlDir, { recursive: true }); @@ -5791,7 +5793,7 @@ function installCodexConfig(targetDir, agentsSrc) { // #443 — pass unified effort config so model_reasoning_effort in the .toml // follows the same config-driven precedence as the Claude .md effort key. const effortCfg = readGsdEffectiveEffortConfig(targetDir); - const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg); + const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier); fs.writeFileSync(path.join(agentsTomlDir, `${name}.toml`), tomlContent); } @@ -10717,7 +10719,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { if (!isMinimalMode(_effectiveInstallMode)) { try { // Generate Codex config.toml and per-agent .toml files. - agentCount = installCodexConfig(targetDir, agentsSrc); + agentCount = installCodexConfig(targetDir, agentsSrc, plan.sandboxTier); } catch (e) { restoreCodexSnapshot(); throw e; diff --git a/docs/adr/1016-runtime-capability-descriptor.md b/docs/adr/1016-runtime-capability-descriptor.md index a0f889c86..1b882fac9 100644 --- a/docs/adr/1016-runtime-capability-descriptor.md +++ b/docs/adr/1016-runtime-capability-descriptor.md @@ -150,7 +150,7 @@ The migration is staged the way phases 3–4 were (registry-only → consume inc 6. **5f — extract `hooksSurface` into its own module, then drive it** ← descriptor. `hooks-surface` is the one axis still scattered across `install.js`; its module extraction is a prerequisite, exactly as ADR-3660 was for `artifactLayout`. **5f-completion (done):** `installSurface` drives the config-writing adapter in `runtime-config-adapter-registry` (#1055, retiring the hand-kept `REGISTRY` table); `extendedHookEvents` drives per-event hook registration in `applySettingsJsonHooks` (#1076, retiring the three hardcoded per-event guards); `hooksSurface === 'none'` is now load-bearing for the hook-skip path (replacing `isOpencode/isKilo` flags). `writesSharedSettings` and `permissionWriter` complete the adapter-selection and permissions-writer parameterisation (all four axes added to `validateRuntimeBody`). 7. **5g — materialize the `InstallPlan`** (ADR-58) — **DONE**: the install-level descriptor axes (`installSurface`, `writesSharedSettings`, `finishPermissionWriter`, `hookEvents`, `extendedHookEvents`, `hooksSurface`) are collected into one typed `InstallPlan` value by the exported `resolveInstallPlan(runtime)` in `runtime-config-adapter-registry`. `install()` and `finishInstall()` in `bin/install.js` now route through it. The spatial axes (`configHome`, `artifactLayout`, `commandStyle`) remain behind their self-resolving adapter modules (`runtime-homes` / `runtime-artifact-layout` / `runtime-slash`) as the execution adapters — consistent with the "adapters execute" pattern. Phase 5 is fully materialized. -Each rung is its own approved-enhancement + PR + equivalence proof (`sandbox-tier`, codex-only, is tiny and rides along in 5e/5g). **Irreducible first-party code, named not hidden:** the artifact converter *functions* remain first-party code, selected by the descriptor's closed `ConverterName` — the descriptor never embeds them. +Each rung is its own approved-enhancement + PR + equivalence proof. (`sandbox-tier`, codex-only, was scoped to ride along in 5e/5g, but neither rung delivered the drive — the InstallPlan capstone shipped with six axes and `sandboxTier` was not among them. It is now wired directly: `resolveInstallPlan` projects `sandboxTier`, and `installCodexConfig` gates per-agent `sandbox_mode` emission on `sandboxTier !== 'none'`. The per-agent mode table `CODEX_AGENT_SANDBOX` remains GSD agent policy — not a runtime-descriptor property — so it is intentionally NOT retired here; full removal of that registration-tax map is tracked under #1138 (phase-6 descriptor-residue removal). The gate is behaviourally a no-op while codex is the only runtime with a non-`none` tier.) **Irreducible first-party code, named not hidden:** the artifact converter *functions* remain first-party code, selected by the descriptor's closed `ConverterName` — the descriptor never embeds them. ### 9. Two axes: the descriptor dogfoods the *runtime* interface only diff --git a/src/runtime-config-adapter-registry.cts b/src/runtime-config-adapter-registry.cts index 036234d13..d19715921 100644 --- a/src/runtime-config-adapter-registry.cts +++ b/src/runtime-config-adapter-registry.cts @@ -30,6 +30,9 @@ // eslint-disable-next-line @typescript-eslint/no-require-imports const { runtimes } = require('./capability-registry.cjs') as { runtimes: Record | undefined }> }; +/** Valid sandboxTier enum values — mirrors the gen-capability-registry validator vocabulary. */ +const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']); + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -71,6 +74,8 @@ interface InstallPlan extends RuntimeConfigIntent { extendedHookEvents: string[]; /** Which surface owns the hook registration for this runtime. */ hooksSurface: HooksSurface; + /** Runtime sandbox tier ('none' | 'codex-agent-sandbox'); gates per-agent sandbox_mode emission. */ + sandboxTier: string; } // --------------------------------------------------------------------------- @@ -130,6 +135,10 @@ function resolveInstallPlan(runtime: string): InstallPlan { const desc = runtimes[runtime]?.runtime; if (!desc) throw new TypeError(`Unknown runtime for install plan: ${runtime}`); const configIntent = resolveRuntimeConfigIntent(runtime); + const sandboxTier = desc['sandboxTier']; + if (typeof sandboxTier !== 'string' || !VALID_SANDBOX_TIERS.has(sandboxTier)) { + throw new TypeError(`Runtime '${runtime}' has a missing or invalid sandboxTier descriptor axis: ${JSON.stringify(sandboxTier)}`); + } return { runtime, installSurface: configIntent.installSurface, @@ -140,6 +149,7 @@ function resolveInstallPlan(runtime: string): InstallPlan { hooksSurface: desc['hooksSurface'] != null ? desc['hooksSurface'] as HooksSurface : ((runtime === 'opencode' || runtime === 'kilo') ? 'none' : 'settings-json'), + sandboxTier, }; } diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index 813d8f299..d0ceeffe4 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -58,6 +58,8 @@ const { resolveNodeRunner, } = require('../bin/install.js'); +const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs'); + function runCodexInstall(codexHome, cwd = path.join(__dirname, '..')) { const previousCodeHome = process.env.CODEX_HOME; const previousCwd = process.cwd(); @@ -579,6 +581,109 @@ description: Maps the codebase }); }); +// ─── sandboxTier gate on generateCodexAgentToml ──────────────────────────────── + +describe('generateCodexAgentToml sandboxTier gate', () => { + const sampleAgent = `--- +name: gsd-executor +description: Executes plans +tools: Read, Write, Edit +color: yellow +--- + +You are an executor.`; + + test('sandboxTier=none: does NOT emit sandbox_mode', () => { + const result = generateCodexAgentToml('gsd-executor', sampleAgent, null, null, null, 'none'); + assert.ok(!result.includes('sandbox_mode'), 'sandbox_mode must be absent when sandboxTier is none'); + }); + + test('sandboxTier=codex-agent-sandbox: emits sandbox_mode = "workspace-write"', () => { + const result = generateCodexAgentToml('gsd-executor', sampleAgent, null, null, null, 'codex-agent-sandbox'); + assert.ok(result.includes('sandbox_mode = "workspace-write"'), 'must emit workspace-write for codex-agent-sandbox tier'); + }); + + test('default (no sandboxTier arg): still emits sandbox_mode = "workspace-write" (no-op for codex)', () => { + const result = generateCodexAgentToml('gsd-executor', sampleAgent); + assert.ok(result.includes('sandbox_mode = "workspace-write"'), 'default preserves codex behavior'); + }); + + test('resolveInstallPlan projection: codex.sandboxTier === "codex-agent-sandbox"', () => { + const plan = resolveInstallPlan('codex'); + assert.strictEqual(plan.sandboxTier, 'codex-agent-sandbox', 'codex must project sandboxTier=codex-agent-sandbox'); + }); + + test('resolveInstallPlan projection: claude.sandboxTier === "none"', () => { + const plan = resolveInstallPlan('claude'); + assert.strictEqual(plan.sandboxTier, 'none', 'claude must project sandboxTier=none'); + }); +}); + +// ─── installCodexConfig threading-seam: sandboxTier → per-agent TOML ───────── + +describe('installCodexConfig sandboxTier threading seam', () => { + const { installCodexConfig } = require('../bin/install.js'); + + let tmpDir; + let agentsSrc; + let targetDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-sandboxtier-seam-')); + agentsSrc = path.join(tmpDir, 'agents'); + targetDir = path.join(tmpDir, 'codex'); + fs.mkdirSync(agentsSrc, { recursive: true }); + fs.mkdirSync(targetDir, { recursive: true }); + // Write a minimal gsd-executor agent fixture + fs.writeFileSync(path.join(agentsSrc, 'gsd-executor.md'), [ + '---', + 'name: gsd-executor', + 'description: Executes plans', + 'tools: Read, Write, Edit', + '---', + '', + 'You are an executor.', + ].join('\n')); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('sandboxTier=none: written per-agent .toml does NOT contain sandbox_mode', () => { + installCodexConfig(targetDir, agentsSrc, 'none'); + const tomlPath = path.join(targetDir, 'agents', 'gsd-executor.toml'); + assert.ok(fs.existsSync(tomlPath), 'per-agent TOML must be written'); + const toml = fs.readFileSync(tomlPath, 'utf8'); + assert.ok(!toml.includes('sandbox_mode'), 'sandbox_mode must be absent when sandboxTier=none'); + }); + + test('sandboxTier=codex-agent-sandbox: written per-agent .toml contains sandbox_mode', () => { + installCodexConfig(targetDir, agentsSrc, 'codex-agent-sandbox'); + const tomlPath = path.join(targetDir, 'agents', 'gsd-executor.toml'); + assert.ok(fs.existsSync(tomlPath), 'per-agent TOML must be written'); + const toml = fs.readFileSync(tomlPath, 'utf8'); + assert.ok(toml.includes('sandbox_mode'), 'sandbox_mode must be present when sandboxTier=codex-agent-sandbox'); + }); + + test('default 2-arg form (no sandboxTier): written per-agent .toml contains sandbox_mode (codex default)', () => { + installCodexConfig(targetDir, agentsSrc); + const tomlPath = path.join(targetDir, 'agents', 'gsd-executor.toml'); + assert.ok(fs.existsSync(tomlPath), 'per-agent TOML must be written'); + const toml = fs.readFileSync(tomlPath, 'utf8'); + assert.ok(toml.includes('sandbox_mode'), 'sandbox_mode must be present in default 2-arg form (codex-agent-sandbox default)'); + }); +}); + +// NOTE: A test for the new fail-loud throw on missing/invalid sandboxTier in +// resolveInstallPlan is omitted here. Constructing a descriptor without the +// field would require mocking the capability-registry module which is a +// singleton require(); patching it invasively would corrupt other tests in the +// same process. The throw path is verified at the type level (tsc) and by the +// build passing, and the happy-path coverage (claude.sandboxTier === 'none' and +// codex.sandboxTier === 'codex-agent-sandbox') confirms the real registry has +// valid values for all 16 runtimes. + // ─── CODEX_AGENT_SANDBOX mapping ──────────────────────────────────────────────── describe('CODEX_AGENT_SANDBOX', () => { diff --git a/tests/enh-1082-install-plan-capstone.test.cjs b/tests/enh-1082-install-plan-capstone.test.cjs index a8862a8da..457b27673 100644 --- a/tests/enh-1082-install-plan-capstone.test.cjs +++ b/tests/enh-1082-install-plan-capstone.test.cjs @@ -26,6 +26,7 @@ const EXPECTED = { hookEvents: 'claude', extendedHookEvents: ['SubagentStop', 'Stop', 'PreCompact', 'FileChanged'], hooksSurface: 'settings-json', + sandboxTier: 'none', }, codex: { runtime: 'codex', @@ -35,6 +36,7 @@ const EXPECTED = { hookEvents: 'claude', extendedHookEvents: [], hooksSurface: 'codex-hooks-json', + sandboxTier: 'codex-agent-sandbox', }, antigravity: { runtime: 'antigravity', @@ -44,6 +46,7 @@ const EXPECTED = { hookEvents: 'gemini', extendedHookEvents: [], hooksSurface: 'settings-json', + sandboxTier: 'none', }, gemini: { runtime: 'gemini', @@ -53,6 +56,7 @@ const EXPECTED = { hookEvents: 'gemini', extendedHookEvents: ['BeforeAgent', 'AfterAgent', 'BeforeModel'], hooksSurface: 'settings-json', + sandboxTier: 'none', }, cursor: { runtime: 'cursor', @@ -62,6 +66,7 @@ const EXPECTED = { hookEvents: 'claude', extendedHookEvents: [], hooksSurface: 'cursor-hooks-json', + sandboxTier: 'none', }, opencode: { runtime: 'opencode', @@ -71,6 +76,7 @@ const EXPECTED = { hookEvents: undefined, extendedHookEvents: [], hooksSurface: 'none', + sandboxTier: 'none', }, kilo: { runtime: 'kilo', @@ -80,6 +86,7 @@ const EXPECTED = { hookEvents: undefined, extendedHookEvents: [], hooksSurface: 'none', + sandboxTier: 'none', }, copilot: { runtime: 'copilot', @@ -89,6 +96,7 @@ const EXPECTED = { hookEvents: undefined, extendedHookEvents: [], hooksSurface: 'copilot-inline', + sandboxTier: 'none', }, augment: { runtime: 'augment', @@ -98,6 +106,7 @@ const EXPECTED = { hookEvents: 'claude', extendedHookEvents: [], hooksSurface: 'settings-json', + sandboxTier: 'none', }, trae: { runtime: 'trae', @@ -107,6 +116,7 @@ const EXPECTED = { hookEvents: undefined, extendedHookEvents: [], hooksSurface: 'none', + sandboxTier: 'none', }, qwen: { runtime: 'qwen', @@ -116,6 +126,7 @@ const EXPECTED = { hookEvents: 'claude', extendedHookEvents: ['SubagentStop', 'Stop', 'PreCompact'], hooksSurface: 'settings-json', + sandboxTier: 'none', }, hermes: { runtime: 'hermes', @@ -125,6 +136,7 @@ const EXPECTED = { hookEvents: 'claude', extendedHookEvents: [], hooksSurface: 'settings-json', + sandboxTier: 'none', }, codebuddy: { runtime: 'codebuddy', @@ -134,6 +146,7 @@ const EXPECTED = { hookEvents: 'claude', extendedHookEvents: [], hooksSurface: 'settings-json', + sandboxTier: 'none', }, cline: { runtime: 'cline', @@ -143,6 +156,7 @@ const EXPECTED = { hookEvents: undefined, extendedHookEvents: [], hooksSurface: 'cline-rules', + sandboxTier: 'none', }, kimi: { runtime: 'kimi', @@ -152,6 +166,7 @@ const EXPECTED = { hookEvents: undefined, extendedHookEvents: [], hooksSurface: 'none', + sandboxTier: 'none', }, windsurf: { runtime: 'windsurf', @@ -161,6 +176,7 @@ const EXPECTED = { hookEvents: undefined, extendedHookEvents: [], hooksSurface: 'none', + sandboxTier: 'none', }, };