opencode-claude-memory 2.1.0 → 2.1.1

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.
@@ -3,6 +3,13 @@ import type { MemoryAgents } from "../../config.js";
3
3
  import type { PermissionRule } from "./fork.js";
4
4
  export type { PermissionRule };
5
5
  export declare const BASELINE_PROBE_AGENT = "opencode-memory-baseline-probe";
6
+ export declare const PERMISSIONS_PROBE_AGENT = "opencode-memory-permissions-probe";
6
7
  export declare function sandboxRules(allowedTools: readonly string[]): PermissionRule[];
8
+ export declare const PROBE_RULES: readonly PermissionRule[];
7
9
  export declare function userRules(rules: readonly PermissionRule[], baseline: readonly PermissionRule[]): PermissionRule[];
8
10
  export declare function applyMemoryAgents(editor: AgentEditor, agents: MemoryAgents): void;
11
+ export declare function ownAgentRules(resolved: readonly PermissionRule[], sandbox: readonly PermissionRule[], probe: readonly PermissionRule[]): PermissionRule[] | undefined;
12
+ export declare function forkSessionRules(sandbox: readonly PermissionRule[], resolved: readonly PermissionRule[] | undefined, probe: readonly PermissionRule[] | undefined): {
13
+ rules: PermissionRule[];
14
+ own: boolean;
15
+ };
@@ -1,19 +1,26 @@
1
1
  import { memoryAgentDefaults } from "../../agents.js";
2
2
  // A throwaway id used to read the host's default ruleset inside the same draft.
3
3
  export const BASELINE_PROBE_AGENT = "opencode-memory-baseline-probe";
4
+ // A hidden, deny-all agent that is kept: every rule after its own PROBE_RULES was appended to all
5
+ // agents by a later transform (the global `permissions`, the browser plugin's `browser` deny). It
6
+ // tells the fork which part of a memory agent's resolved ruleset is global and which is its own.
7
+ export const PERMISSIONS_PROBE_AGENT = "opencode-memory-permissions-probe";
4
8
  export function sandboxRules(allowedTools) {
5
9
  return [
6
10
  { action: "*", resource: "*", effect: "deny" },
7
11
  ...allowedTools.map((tool) => ({ action: tool, resource: "*", effect: "allow" })),
8
12
  ];
9
13
  }
14
+ export const PROBE_RULES = sandboxRules([]);
10
15
  function ruleKey(rule) {
11
16
  return `${rule.action}\u0000${rule.resource}\u0000${rule.effect}`;
12
17
  }
18
+ function startsWith(rules, prefix) {
19
+ return prefix.every((rule, index) => rules[index] !== undefined && ruleKey(rules[index]) === ruleKey(rule));
20
+ }
13
21
  // The user's own rules: the agent's ruleset minus the host defaults every agent starts with.
14
22
  export function userRules(rules, baseline) {
15
- const prefix = baseline.every((rule, index) => rules[index] && ruleKey(rules[index]) === ruleKey(rule));
16
- if (prefix)
23
+ if (startsWith(rules, baseline))
17
24
  return rules.slice(baseline.length).map((rule) => ({ ...rule }));
18
25
  const base = new Set(baseline.map(ruleKey));
19
26
  return rules.filter((rule) => !base.has(ruleKey(rule))).map((rule) => ({ ...rule }));
@@ -22,6 +29,12 @@ export function applyMemoryAgents(editor, agents) {
22
29
  editor.update(BASELINE_PROBE_AGENT, () => { });
23
30
  const baseline = (editor.get(BASELINE_PROBE_AGENT)?.permissions ?? []).map((rule) => ({ ...rule }));
24
31
  editor.remove(BASELINE_PROBE_AGENT);
32
+ editor.update(PERMISSIONS_PROBE_AGENT, (agent) => {
33
+ agent.mode = "subagent";
34
+ agent.hidden = true;
35
+ agent.description = "opencode-claude-memory internal: permission probe, never runs";
36
+ agent.permissions = PROBE_RULES.map((rule) => ({ ...rule }));
37
+ });
25
38
  for (const [name, defaults] of Object.entries(memoryAgentDefaults(agents))) {
26
39
  applyAgent(editor, name, defaults, baseline);
27
40
  }
@@ -39,3 +52,32 @@ function applyAgent(editor, name, defaults, baseline) {
39
52
  agent.permissions = [...sandboxRules(defaults.allowedTools), ...configured];
40
53
  });
41
54
  }
55
+ // The memory agent's own rules: what follows the sandbox in its resolved ruleset, minus the rules
56
+ // appended to every agent (the probe's tail, removed as an ordered subsequence). These are the
57
+ // user's `agents.<name>.permissions` (and rules of a plugin that configured the agent first).
58
+ // undefined when either ruleset does not have the layout this plugin wrote.
59
+ export function ownAgentRules(resolved, sandbox, probe) {
60
+ if (!startsWith(resolved, sandbox) || !startsWith(probe, PROBE_RULES))
61
+ return undefined;
62
+ const shared = probe.slice(PROBE_RULES.length);
63
+ const own = [];
64
+ let next = 0;
65
+ for (const rule of resolved.slice(sandbox.length)) {
66
+ const expected = shared[next];
67
+ if (expected && ruleKey(expected) === ruleKey(rule))
68
+ next += 1;
69
+ else
70
+ own.push({ ...rule });
71
+ }
72
+ return next === shared.length ? own : undefined;
73
+ }
74
+ // The fork's session ruleset. The host evaluates `[...agent rules, ...session rules]` last match
75
+ // wins, and checks deny before any saved "always" approval, so a session ruleset that starts with the
76
+ // sandbox overrides every global rule (allow, ask, or a rule for `shell`, `read`,
77
+ // `external_directory`, ...). The agent's own rules follow it, so a user override in
78
+ // `agents.<name>.permissions` still widens or narrows the sandbox. Global rules no longer reach the
79
+ // fork; without a recoverable layout the fork runs under the bare sandbox.
80
+ export function forkSessionRules(sandbox, resolved, probe) {
81
+ const own = resolved && probe ? ownAgentRules(resolved, sandbox, probe) : undefined;
82
+ return { rules: [...sandbox.map((rule) => ({ ...rule })), ...(own ?? [])], own: own !== undefined };
83
+ }
@@ -1,3 +1,4 @@
1
+ import type { Logger } from "../../util/log.js";
1
2
  import { type GenerateInput, type MemoryHost } from "../types.js";
2
3
  import { type ModelRef, type PermissionRule, type V2SessionApi } from "./fork.js";
3
4
  export type V2AgentInfo = {
@@ -14,6 +15,7 @@ export type V2HostOptions = {
14
15
  }>;
15
16
  getAgent: (name: string) => Promise<V2AgentInfo | undefined>;
16
17
  sandboxFor: (name: string) => PermissionRule[];
18
+ log?: Logger;
17
19
  };
18
20
  export declare function buildGeneratePrompt(input: Pick<GenerateInput, "system" | "text" | "schema">): string;
19
21
  export declare class V2Host implements MemoryHost {
@@ -1,13 +1,6 @@
1
- // MemoryHost over the V2 plugin context (`setup` entry).
2
- //
3
- // Differences from V1 that the coordinators see through the interface:
4
- // - readTranscript only has the messages after the last compaction (`session.context`), so a
5
- // watermark message that was compacted away yields `truncated: true`;
6
- // - listSessions is unavailable (the plugin API cannot list sessions);
7
- // - generate is a one-shot `generate.text` call: no system prompt, no structured output, so both
8
- // are folded into the prompt and the caller parses the text.
9
1
  import { withDeadline } from "../../util/timeout.js";
10
2
  import { SDK_READ_TIMEOUT_MS } from "../types.js";
3
+ import { forkSessionRules, PERMISSIONS_PROBE_AGENT } from "./agents.js";
11
4
  import { runV2Fork } from "./fork.js";
12
5
  import { toTranscript } from "./transcript.js";
13
6
  // The V2 plugin calls take no AbortSignal; withDeadline only bounds the wait.
@@ -63,12 +56,18 @@ export class V2Host {
63
56
  }
64
57
  }
65
58
  async fork(input) {
66
- // The resolved agent's ruleset (sandbox + the user's overrides) is also passed as the session
67
- // ruleset; without one the child would inherit its parent's session permissions.
68
- const agent = await this.agent(input.agent);
69
- const permissions = agent?.permissions?.length
70
- ? agent.permissions.map((rule) => ({ ...rule }))
71
- : this.options.sandboxFor(input.agent);
59
+ // The resolved agent ruleset carries the global `permissions` after the sandbox (see agents.ts),
60
+ // so it is never passed through: the session ruleset is the sandbox followed by the agent's own
61
+ // rules only. Without an explicit one the child would also inherit its parent's session rules.
62
+ const sandbox = this.options.sandboxFor(input.agent);
63
+ const [agent, probe] = await Promise.all([this.agent(input.agent), this.agent(PERMISSIONS_PROBE_AGENT)]);
64
+ const { rules: permissions, own } = forkSessionRules(sandbox, agent?.permissions, probe?.permissions);
65
+ if (!own && agent?.permissions) {
66
+ this.options.log?.("warn", "Memory agent overrides could not be told apart from global rules; forking with the bare sandbox", {
67
+ agent: input.agent,
68
+ });
69
+ }
70
+ this.options.log?.("debug", "Memory fork session permissions", { agent: input.agent, permissions });
72
71
  return runV2Fork(this.options.session, {
73
72
  ...input,
74
73
  permissions,
@@ -52,6 +52,7 @@ export const createV2Setup = (env, homeDir) => async (ctx) => {
52
52
  return agent?.data;
53
53
  },
54
54
  sandboxFor: (name) => sandboxRules(defaults[name]?.allowedTools ?? []),
55
+ log,
55
56
  });
56
57
  const deps = { store, config, host, owned, log };
57
58
  const recall = new RecallCoordinator(deps);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claude-memory",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Claude Code memory: persistent, local-first shared memory with Claude Code-compatible Markdown files, LLM recall, in-process auto extraction, and auto-dream consolidation",
6
6
  "main": "dist/index.js",