Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ is safe to re-run; if anything is missing, add it manually:
"opencode-supermemory",
// ... other OpenCode 2 plugins
],
"permissions": [
{ "action": "supermemory_recall", "resource": "*", "effect": "allow" },
],
// OpenCode V1 rejects V2 "permissions"; OpenCode 2 reads this as the same rule
"permission": {
"supermemory_recall": "allow",
},
}
```

Expand Down Expand Up @@ -220,9 +221,7 @@ OpenCode 2 side by side:
{
"plugin": ["opencode-supermemory@latest"],
"plugins": ["opencode-supermemory"],
"permissions": [
{ "action": "supermemory_recall", "resource": "*", "effect": "allow" },
],
"permission": { "supermemory_recall": "allow" },
}
```

Expand Down
27 changes: 24 additions & 3 deletions src/services/opencode-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ describe("OpenCode config editor", () => {
expect(result.changed).toBe(true);
expect(config.plugin).toEqual([V1_PLUGIN_ENTRY]);
expect(config.plugins).toEqual([V2_PLUGIN_ENTRY]);
expect(config.permissions).toEqual([RECALL_PERMISSION]);
expect(config.permission).toEqual({ supermemory_recall: "allow" });
expect(config.permissions).toBeUndefined();
expect(result.warnings).toEqual([]);
});

Expand All @@ -36,7 +37,7 @@ describe("OpenCode config editor", () => {
expect(first.content).not.toContain(V1_PLUGIN_ENTRY);
const config = JSON.parse(first.content.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1"));
expect(config.plugins).toEqual([V2_PLUGIN_ENTRY]);
expect(config.permissions).toEqual([RECALL_PERMISSION]);
expect(config.permission).toEqual({ supermemory_recall: "allow" });

const second = editOpenCodeConfig(first.content);
expect(second.changed).toBe(false);
Expand All @@ -55,19 +56,39 @@ describe("OpenCode config editor", () => {
const content = JSON.stringify({
plugin: ["opencode-supermemory@2.0.13"],
plugins: [{ package: "opencode-supermemory@latest", options: {} }],
permissions: [RECALL_PERMISSION],
permission: { supermemory_recall: "allow" },
});
expect(editOpenCodeConfig(content).changed).toBe(false);
});

test("moves the V2 recall rule that OpenCode V1 rejects into V1 permission rules", () => {
const other = { action: "bash", resource: "*", effect: "ask" };
const onlyRecall = JSON.parse(
editOpenCodeConfig(JSON.stringify({ permissions: [RECALL_PERMISSION] })).content,
);
expect(onlyRecall.permissions).toBeUndefined();
expect(onlyRecall.permission).toEqual({ supermemory_recall: "allow" });

const withOther = JSON.parse(
editOpenCodeConfig(JSON.stringify({ permissions: [RECALL_PERMISSION, other] })).content,
);
expect(withOther.permissions).toEqual([other]);
expect(withOther.permission).toEqual({ supermemory_recall: "allow" });
});

test("keeps an explicit recall deny and warns instead of overriding it", () => {
const deny = { ...RECALL_PERMISSION, effect: "deny" };
const result = editOpenCodeConfig(JSON.stringify({ permissions: [deny] }));
const config = JSON.parse(result.content);

expect(config.permissions).toEqual([deny]);
expect(config.permission).toBeUndefined();
expect(result.warnings).toHaveLength(1);
expect(readOpenCodeRegistration(result.content).recallDenied).toBe(true);

const v1Deny = editOpenCodeConfig(JSON.stringify({ permission: { supermemory_recall: "deny" } }));
expect(JSON.parse(v1Deny.content).permission).toEqual({ supermemory_recall: "deny" });
expect(v1Deny.warnings).toHaveLength(1);
});

test("rejects configs whose arrays are not arrays", () => {
Expand Down
43 changes: 28 additions & 15 deletions src/services/opencode-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,23 @@ function isRecallPermission(value: unknown, effect: "allow" | "deny"): boolean {
);
}

function getV1RecallRule(config: JsonObject): unknown {
return isObject(config.permission) ? config.permission[RECALL_PERMISSION.action] : undefined;
}

export function readOpenCodeRegistration(content: string): OpenCodeRegistration {
const config = parseOpenCodeConfig(content);
const plugin = Array.isArray(config.plugin) ? config.plugin : [];
const plugins = Array.isArray(config.plugins) ? config.plugins : [];
const permissions = Array.isArray(config.permissions) ? config.permissions : [];
const v1Rule = getV1RecallRule(config);
return {
v1: plugin.some(isSupermemoryPluginEntry),
v2: plugins.some(isSupermemoryPluginEntry),
recallAllowed: permissions.some((entry) => isRecallPermission(entry, "allow")),
recallDenied: permissions.some((entry) => isRecallPermission(entry, "deny")),
recallAllowed:
v1Rule === "allow" || permissions.some((entry) => isRecallPermission(entry, "allow")),
recallDenied:
v1Rule === "deny" || permissions.some((entry) => isRecallPermission(entry, "deny")),
};
}

Expand All @@ -178,26 +185,32 @@ export function editOpenCodeConfig(rawContent: string): OpenCodeConfigEditResult
content = addArrayEntry(content, "plugin", V1_PLUGIN_ENTRY, isSupermemoryPluginEntry);
content = addArrayEntry(content, "plugins", V2_PLUGIN_ENTRY, isSupermemoryPluginEntry);

const config = parseOpenCodeConfig(content);
let config = parseOpenCodeConfig(content);
const permissions = config.permissions;
if (permissions !== undefined && !Array.isArray(permissions)) {
throw new Error('OpenCode config property "permissions" must be an array');
}

const permissionEntries = permissions ?? [];
if (permissionEntries.some((entry) => isRecallPermission(entry, "deny"))) {
warnings.push(
'OpenCode 2 permission "supermemory_recall" is explicitly denied; preserving the deny instead of adding an allow.',
);
} else if (
!permissionEntries.some((entry) => isRecallPermission(entry, "allow"))
) {
content = addArrayEntry(
const legacyAllow = (permissions ?? []).findIndex((entry) => isRecallPermission(entry, "allow"));
if (legacyAllow !== -1) {
content = applyModification(
content,
"permissions",
RECALL_PERMISSION,
(entry) => isRecallPermission(entry, "allow"),
permissions!.length === 1 ? ["permissions"] : ["permissions", legacyAllow],
undefined,
);
config = parseOpenCodeConfig(content);
}

const v1Rule = getV1RecallRule(config);
const recallDenied =
v1Rule === "deny" ||
(permissions ?? []).some((entry) => isRecallPermission(entry, "deny"));
if (recallDenied) {
warnings.push(
'OpenCode permission "supermemory_recall" is explicitly denied; preserving the deny instead of adding an allow.',
);
} else if (v1Rule === undefined && (config.permission === undefined || isObject(config.permission))) {
content = applyModification(content, ["permission", RECALL_PERMISSION.action], "allow");
}

return {
Expand Down
Loading