From b12d6849404ce3fc473e637bbb8d0ff8ed6faf91 Mon Sep 17 00:00:00 2001 From: Nic Barrett Date: Wed, 1 Apr 2026 17:02:33 -0500 Subject: [PATCH 1/2] fix(opencode): guard string-valued permission config (#781) --- bin/install.js | 9 +++- tests/opencode-permissions.test.cjs | 72 +++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/opencode-permissions.test.cjs diff --git a/bin/install.js b/bin/install.js index 9d5a58c4e..55a79a64e 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3985,8 +3985,14 @@ function configureOpencodePermissions(isGlobal = true) { } } + // OpenCode also allows a top-level string permission like "allow". + // In that case, path-specific permission entries are unnecessary. + if (typeof config.permission === 'string') { + return; + } + // Ensure permission structure exists - if (!config.permission) { + if (!config.permission || typeof config.permission !== 'object') { config.permission = {}; } @@ -5207,6 +5213,7 @@ if (process.env.GSD_TEST_MODE) { validateHookFields, installSdk, promptSdk, + configureOpencodePermissions, }; } else { diff --git a/tests/opencode-permissions.test.cjs b/tests/opencode-permissions.test.cjs new file mode 100644 index 000000000..a50745b0e --- /dev/null +++ b/tests/opencode-permissions.test.cjs @@ -0,0 +1,72 @@ +/** + * Regression tests for OpenCode permission config handling. + * + * Ensures the installer does not crash when opencode.json uses the valid + * top-level string form: "permission": "allow". + */ + +const { test, describe, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +process.env.GSD_TEST_MODE = '1'; +const { configureOpencodePermissions } = require('../bin/install.js'); + +const envKeys = ['OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG', 'XDG_CONFIG_HOME']; +const originalEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]])); + +function restoreEnv(snapshot) { + for (const key of envKeys) { + if (snapshot[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = snapshot[key]; + } + } +} + +function createOpencodeConfigDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-opencode-')); +} + +afterEach(() => { + restoreEnv(originalEnv); +}); + +describe('configureOpencodePermissions', () => { + test('does not crash or rewrite top-level string permissions', () => { + const configDir = createOpencodeConfigDir(); + const configPath = path.join(configDir, 'opencode.json'); + const original = JSON.stringify({ + $schema: 'https://opencode.ai/config.json', + permission: 'allow', + skills: { paths: ['/tmp/skills'] }, + }, null, 2) + '\n'; + + fs.writeFileSync(configPath, original); + process.env.OPENCODE_CONFIG_DIR = configDir; + + assert.doesNotThrow(() => configureOpencodePermissions(true)); + assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original); + + fs.rmSync(configDir, { recursive: true, force: true }); + }); + + test('adds path-specific read and external_directory permissions for object configs', () => { + const configDir = createOpencodeConfigDir(); + const configPath = path.join(configDir, 'opencode.json'); + fs.writeFileSync(configPath, JSON.stringify({ permission: {} }, null, 2) + '\n'); + process.env.OPENCODE_CONFIG_DIR = configDir; + + configureOpencodePermissions(true); + + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + const gsdPath = `${configDir.replace(/\\/g, '/')}/get-shit-done/*`; + assert.strictEqual(config.permission.read[gsdPath], 'allow'); + assert.strictEqual(config.permission.external_directory[gsdPath], 'allow'); + + fs.rmSync(configDir, { recursive: true, force: true }); + }); +}); From 56ab54953811670157aaa01a53348c2648f42736 Mon Sep 17 00:00:00 2001 From: Nic Barrett Date: Thu, 2 Apr 2026 11:57:54 -0500 Subject: [PATCH 2/2] test(opencode): align regression test with contributing guide --- tests/opencode-permissions.test.cjs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/opencode-permissions.test.cjs b/tests/opencode-permissions.test.cjs index a50745b0e..5d9f3d202 100644 --- a/tests/opencode-permissions.test.cjs +++ b/tests/opencode-permissions.test.cjs @@ -5,11 +5,11 @@ * top-level string form: "permission": "allow". */ -const { test, describe, afterEach } = require('node:test'); -const assert = require('node:assert'); +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); const fs = require('fs'); -const os = require('os'); const path = require('path'); +const { createTempDir, cleanup } = require('./helpers.cjs'); process.env.GSD_TEST_MODE = '1'; const { configureOpencodePermissions } = require('../bin/install.js'); @@ -27,17 +27,19 @@ function restoreEnv(snapshot) { } } -function createOpencodeConfigDir() { - return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-opencode-')); -} +let configDir; + +beforeEach(() => { + configDir = createTempDir('gsd-opencode-'); +}); afterEach(() => { + cleanup(configDir); restoreEnv(originalEnv); }); describe('configureOpencodePermissions', () => { test('does not crash or rewrite top-level string permissions', () => { - const configDir = createOpencodeConfigDir(); const configPath = path.join(configDir, 'opencode.json'); const original = JSON.stringify({ $schema: 'https://opencode.ai/config.json', @@ -50,12 +52,9 @@ describe('configureOpencodePermissions', () => { assert.doesNotThrow(() => configureOpencodePermissions(true)); assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original); - - fs.rmSync(configDir, { recursive: true, force: true }); }); test('adds path-specific read and external_directory permissions for object configs', () => { - const configDir = createOpencodeConfigDir(); const configPath = path.join(configDir, 'opencode.json'); fs.writeFileSync(configPath, JSON.stringify({ permission: {} }, null, 2) + '\n'); process.env.OPENCODE_CONFIG_DIR = configDir; @@ -66,7 +65,5 @@ describe('configureOpencodePermissions', () => { const gsdPath = `${configDir.replace(/\\/g, '/')}/get-shit-done/*`; assert.strictEqual(config.permission.read[gsdPath], 'allow'); assert.strictEqual(config.permission.external_directory[gsdPath], 'allow'); - - fs.rmSync(configDir, { recursive: true, force: true }); }); });