opencode-workspace-scope 0.1.1 → 0.2.0-beta.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.
package/README.md CHANGED
@@ -1,32 +1,40 @@
1
1
  # opencode-workspace-scope
2
2
 
3
- 全局安装、**sidecar 自激活**的 [opencode](https://opencode.ai) 插件,为"一个项目涉及多个代码仓库"的并行开发场景提供工作区作用域隔离。
3
+ 全局安装、**sidecar 自激活**的 [opencode2](https://opencode.ai)(V2)插件,为"一个项目涉及多个代码仓库"的并行开发场景提供工作区作用域隔离。
4
4
 
5
5
  在 `.opencode/workspace-scope.jsonc` 里声明本项目涉及的各个仓库路径与职责(如前端仓库、后端仓库、编排根),插件据此:
6
6
 
7
- - 自动注入 `permission.external_directory` 白名单,**只放开配置中声明的仓库**,其余外部路径保持默认 ask
7
+ - 在权限评估阶段**动态放行**已声明仓库的 `external_directory` 请求,其余外部路径保持默认 ask,配置中的显式 deny 仍然最终生效;
8
8
  - 把"工作区根 + 各仓库路径及职责"注入 system prompt,让 AI 每轮都知道改哪里、哪些路径不能碰。
9
9
 
10
10
  sidecar 只需在编排根(或任一仓库)放一份,其他并行会话通过向上查找自动继承作用域,配合 init 工具自动探测兄弟仓库,开箱即用、零重复配置。
11
11
 
12
- 无 sidecar 的项目中插件完全 no-op,不影响任何正常使用。
12
+ 无 sidecar 的项目中插件不注册任何 hook,完全 no-op,不影响任何正常使用。
13
+
14
+ > 仅适配 opencode2(V2 插件 API)。V1 版本见 `main` 分支,二者不可混用:V2 下加载 V1 插件会以 WARN 失败跳过。
13
15
 
14
16
  ## 安装
15
17
 
16
- 全局安装(推荐,配合 sidecar 自激活):
18
+ 已发布为 npm 包,通过 V2 CLI 全局安装:
17
19
 
18
20
  ```bash
19
- mkdir -p ~/.config/opencode/plugins
20
- ln -s /path/to/opencode-workspace-scope/src/index.ts ~/.config/opencode/plugins/workspace-scope.ts
21
- # 或按项目使用 npm:
22
- # npm install opencode-workspace-scope
21
+ opencode2 plugin add opencode-workspace-scope@beta
22
+ ```
23
+
24
+ 或手动写入全局 `~/.config/opencode/opencode.json(c)` `plugins` 数组(注意是复数 `plugins`,不是 V1 的 `plugin`):
25
+
26
+ ```jsonc
27
+ {
28
+ "$schema": "https://opencode.ai/config.json",
29
+ "plugins": ["opencode-workspace-scope@beta"]
30
+ }
23
31
  ```
24
32
 
25
- 重启 opencode 后生效。
33
+ 重启 opencode2(`opencode2 service restart`)后生效。建议条目带版本/tag 固定,避免被 `plugin update` 升到不兼容版本。
26
34
 
27
35
  ## 快速开始
28
36
 
29
- 1. 在某个工作区根目录启动 opencode,对它说:
37
+ 1. 在某个工作区根目录启动 opencode2,对它说:
30
38
 
31
39
  > 初始化这个工作区的 scope 配置
32
40
 
@@ -53,19 +61,21 @@ ln -s /path/to/opencode-workspace-scope/src/index.ts ~/.config/opencode/plugins/
53
61
 
54
62
  > `worktrees` 泛指本工作区涉及的各代码仓库/目录,不要求是 git worktree;可以是兄弟仓库、monorepo 子目录,或任意绝对/相对路径。
55
63
 
56
- 3. **重启 opencode**,使 external_directory 权限生效(上下文注入无需重启)。
64
+ 3. **重启 opencode2**,使 external_directory 放行生效(上下文注入无需重启,sidecar 修改后每轮调用会重新加载)。
57
65
 
58
66
  ## 权限行为
59
67
 
60
- - 注入到 `permission.external_directory`:`"<worktree>/**: allow"`,采用"末尾追加、last-match-wins"合并,不覆盖你已有的其他规则;对每个 worktree 同时写入**词法路径与 realpath 两种 pattern**,避免符号链接祖先导致白名单失效;
61
- - 工作区根目录之外、且不在白名单内的路径:维持 opencode 默认 `ask`;
68
+ - V2 插件 API 无 config hook,插件**不再改写配置**,改为注册 `permission.evaluate` hook:当 `external_directory` 请求的资源边界全部落在 sidecar 声明的 worktree(词法路径或 realpath)内时,把 `ask` 升级为 `allow`;
69
+ - 只做 `ask allow` 的单向升级:配置显式 `deny` 不会进入 hook(最终生效),已是 `allow` 的请求保持不变;部分命中的多资源请求**整体不放行**,避免越权放大;
70
+ - 对每个 worktree 同时比对**词法路径与 realpath**,规避符号链接祖先导致匹配失效;
71
+ - 工作区根目录之外、且不在声明范围内的路径:维持默认 `ask`;
62
72
  - sidecar 中不存在的路径会被跳过,不会崩溃;
63
73
  - **sidecar 向上查找**:从启动目录逐级向上找 `.opencode/workspace-scope.jsonc`(最多 10 层),嵌套子项目会继承父工作区的作用域(对齐 opencode 自身配置发现行为);
64
- - 注意:`opencode --auto` 会把 `ask` 自动放行。若并行会话需要硬隔离,请在配置里为 `external_directory` 追加 `"*": "deny"` 兜底。
74
+ - 注意:`/tmp` 等托管临时目录被 opencode2 内置豁免,不经过 `external_directory` 决策;并行会话需要硬隔离时请在配置中追加显式 deny 规则。
65
75
 
66
76
  ## 注入的上下文
67
77
 
68
- 每轮请求会把以下块拼进 system prompt 主块(`output.system[0]`,无 `sessionID` 时不注入):
78
+ 每轮模型调用(`session.context` hook)会把以下块 push system parts(title 请求等无会话上下文的调用不注入):
69
79
 
70
80
  ```
71
81
  ## Workspace Scope(由 workspace-scope 插件自动注入)
@@ -77,11 +87,10 @@ ln -s /path/to/opencode-workspace-scope/src/index.ts ~/.config/opencode/plugins/
77
87
 
78
88
  ## 设计约束
79
89
 
80
- - **自激活**:无 sidecar 时插件必须完全 no-op;
81
- - **权限只押 `external_directory`**:不依赖 read/edit 的路径 pattern(存在绝对/相对不对称问题,见 opencode issue #26524);
82
- - **不改默认语义**:用户 `permission` 为字符串缩写(如 `"allow"`)时不改写;对象形式按"末尾追加 allow"合并;
83
- - **system.transform 原地变更**:只拼 `output.system[0]`,禁止整体赋值(静默 no-op,issue #25754);
84
- - **零新增运行时依赖**:仅依赖 `@opencode-ai/plugin`。
90
+ - **自激活**:无 sidecar setup 不注册任何 hook,完全 no-op;
91
+ - **权限只押 `external_directory`**:不依赖 read/edit 的路径 pattern
92
+ - **不改默认语义**:只升级 `ask allow`,deny/allow 原样保留,多资源部分命中不放行;
93
+ - **零新增运行时依赖**:仅依赖 `@opencode-ai/plugin`(beta)。
85
94
 
86
95
  ## 开发
87
96
 
@@ -89,10 +98,18 @@ ln -s /path/to/opencode-workspace-scope/src/index.ts ~/.config/opencode/plugins/
89
98
  npm install
90
99
  npm run typecheck # tsc --noEmit
91
100
  npm run build # 输出到 dist/
92
- bun /tmp/opencode/smoke.ts # 冒烟测试(临时脚本,不入库)
93
101
  ```
94
102
 
95
- 发布:`npm version patch && npm publish`(`prepublishOnly` 自动 typecheck + build)。
103
+ 本地联调:在测试工作区的 `.opencode/plugins/` 放一个转发文件(V2 自动发现该目录,直接 `.ts/.js` 文件形式最稳):
104
+
105
+ ```ts
106
+ // <测试工作区>/.opencode/plugins/ws-scope.ts
107
+ export { default } from "/absolute/path/to/opencode-workspace-scope/dist/index.js"
108
+ ```
109
+
110
+ 注意本地包目录形式的插件需要依赖可解析(`@opencode-ai/plugin`),建议用上面的转发文件 + 仓库内 `node_modules`。
111
+
112
+ 发布:`npm version prerelease && npm publish --tag beta`(`prepublishOnly` 自动 typecheck + build);V2 插件 API 仍在 beta,随 opencode2 版本升级需回归验证。
96
113
 
97
114
  ## License
98
115
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
2
- export declare const WorkspaceScopePlugin: Plugin;
1
+ import { Plugin } from "@opencode-ai/plugin";
2
+ export declare const WorkspaceScopePlugin: Plugin.Plugin;
3
3
  export default WorkspaceScopePlugin;
package/dist/index.js CHANGED
@@ -1,49 +1,54 @@
1
+ import { Plugin } from "@opencode-ai/plugin";
1
2
  import { loadScope } from "./scope.js";
2
- import { buildExternalDirectory, mergePermission } from "./permissions.js";
3
+ import { coveredByScope } from "./permissions.js";
3
4
  import { buildScopeBlock } from "./context.js";
4
- import { initTool } from "./init.js";
5
- export const WorkspaceScopePlugin = async ({ directory, worktree, client }) => {
6
- const anchor = worktree || directory;
7
- const logApplied = (sidecarPath) => {
8
- try {
9
- const result = client.app?.log?.({
10
- body: {
11
- service: "workspace-scope",
12
- level: "info",
13
- message: `已应用工作区作用域 ${sidecarPath}`,
14
- },
5
+ import { INIT_ARGS_SCHEMA, INIT_TOOL_DESCRIPTION, runInit } from "./init.js";
6
+ const log = (message) => {
7
+ console.log(`[workspace-scope] ${message}`);
8
+ };
9
+ export const WorkspaceScopePlugin = Plugin.define({
10
+ id: "opencode-workspace-scope",
11
+ async setup(ctx) {
12
+ // 对齐 V1 语义:优先项目规范根(原 worktree),回退会话目录
13
+ const anchor = ctx.location.project.canonical || ctx.location.directory;
14
+ const scope = loadScope(anchor);
15
+ if (scope)
16
+ log(`已应用工作区作用域 ${scope.sidecarPath}`);
17
+ if (scope) {
18
+ // 权限:不再改写配置,改为在评估阶段动态放行。
19
+ // 仅 escalate ask→allow;配置显式 deny 不会进入本 hook(最终生效)。
20
+ await ctx.permission.hook("evaluate", (event) => {
21
+ if (event.action !== "external_directory")
22
+ return;
23
+ if (event.effect !== "ask")
24
+ return;
25
+ const current = loadScope(anchor);
26
+ if (!current)
27
+ return;
28
+ if (coveredByScope(event.resources, current)) {
29
+ event.effect = "allow";
30
+ event.message = "workspace-scope:目标路径位于本工作区声明的范围内";
31
+ }
32
+ });
33
+ // 上下文:每轮模型调用注入工作区块;V2 为可 push 数组,无 sessionID 时不会触发 title 请求
34
+ await ctx.session.hook("context", (event) => {
35
+ const current = loadScope(anchor);
36
+ if (!current)
37
+ return;
38
+ event.system.push({ type: "text", text: buildScopeBlock(current) });
15
39
  });
16
- if (result && typeof result.catch === "function") {
17
- result.catch(() => { });
18
- }
19
- }
20
- catch {
21
- // ignore
22
40
  }
23
- };
24
- return {
25
- config: async (cfg) => {
26
- const scope = loadScope(anchor);
27
- if (!scope)
28
- return;
29
- logApplied(scope.sidecarPath);
30
- const external = buildExternalDirectory(scope);
31
- if (Object.keys(external).length === 0)
32
- return;
33
- const anyCfg = cfg;
34
- anyCfg.permission = mergePermission(anyCfg.permission, external);
35
- },
36
- "experimental.chat.system.transform": async (input, output) => {
37
- if (!input.sessionID)
38
- return;
39
- const scope = loadScope(anchor);
40
- if (!scope)
41
- return;
42
- output.system[0] = `${output.system[0] ?? ""}\n\n${buildScopeBlock(scope)}`;
43
- },
44
- tool: {
45
- workspace_scope_init: initTool,
46
- },
47
- };
48
- };
41
+ // init 工具始终注册:sidecar 尚不存在时也要能引导创建
42
+ await ctx.tool.transform((editor) => {
43
+ editor.add({
44
+ name: "workspace_scope_init",
45
+ description: INIT_TOOL_DESCRIPTION,
46
+ input: INIT_ARGS_SCHEMA,
47
+ async execute(input) {
48
+ return { content: runInit(anchor, input) };
49
+ },
50
+ });
51
+ });
52
+ },
53
+ });
49
54
  export default WorkspaceScopePlugin;
package/dist/init.d.ts CHANGED
@@ -1,21 +1,46 @@
1
- export declare const initTool: {
2
- description: string;
3
- args: {
4
- name: import("zod").ZodOptional<import("zod").ZodString>;
5
- workspaceDescription: import("zod").ZodOptional<import("zod").ZodString>;
6
- worktrees: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
7
- name: import("zod").ZodString;
8
- path: import("zod").ZodString;
9
- description: import("zod").ZodOptional<import("zod").ZodString>;
10
- }, import("zod/v4/core").$strip>>>;
1
+ export interface InitArgs {
2
+ name?: string;
3
+ workspaceDescription?: string;
4
+ worktrees?: Array<{
5
+ name: string;
6
+ path: string;
7
+ description?: string;
8
+ }>;
9
+ }
10
+ export declare const INIT_ARGS_SCHEMA: {
11
+ type: string;
12
+ properties: {
13
+ name: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ workspaceDescription: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ worktrees: {
22
+ type: string;
23
+ description: string;
24
+ items: {
25
+ type: string;
26
+ required: string[];
27
+ properties: {
28
+ name: {
29
+ type: string;
30
+ description: string;
31
+ };
32
+ path: {
33
+ type: string;
34
+ description: string;
35
+ };
36
+ description: {
37
+ type: string;
38
+ description: string;
39
+ };
40
+ };
41
+ };
42
+ };
11
43
  };
12
- execute(args: {
13
- name?: string | undefined;
14
- workspaceDescription?: string | undefined;
15
- worktrees?: {
16
- name: string;
17
- path: string;
18
- description?: string | undefined;
19
- }[] | undefined;
20
- }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
21
44
  };
45
+ export declare const INIT_TOOL_DESCRIPTION = "\u521D\u59CB\u5316\u6216\u91CD\u5EFA\u5F53\u524D\u5DE5\u4F5C\u533A\u7684 workspace-scope \u914D\u7F6E\uFF08.opencode/workspace-scope.jsonc\uFF09\u3002\u4E0D\u4F20 worktrees \u65F6\u81EA\u52A8\u63A2\u6D4B\u7236\u76EE\u5F55\u4E0B\u7684\u5144\u5F1F git \u4ED3\u5E93\u4F5C\u4E3A\u5019\u9009 worktree\u3002\u751F\u6210\u7684 sidecar \u4E0D\u7EB3\u5165\u7248\u672C\u7BA1\u7406\uFF08\u81EA\u52A8\u5199\u5165 .gitignore\uFF09\uFF0C\u5E76\u9644\u5E26 JSON Schema\u3002";
46
+ export declare function runInit(anchor: string, args: InitArgs): string;
package/dist/init.js CHANGED
@@ -1,8 +1,28 @@
1
- import { tool } from "@opencode-ai/plugin";
2
1
  import { basename, dirname, join } from "node:path";
3
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
4
3
  import { invalidateScope } from "./scope.js";
5
4
  import { WORKSPACE_SCOPE_SCHEMA } from "./schema.js";
5
+ export const INIT_ARGS_SCHEMA = {
6
+ type: "object",
7
+ properties: {
8
+ name: { type: "string", description: "工作区名称,缺省使用当前目录名" },
9
+ workspaceDescription: { type: "string", description: "工作区职责说明" },
10
+ worktrees: {
11
+ type: "array",
12
+ description: "可选:显式指定 worktree 列表;缺省自动探测父目录兄弟 git 仓库",
13
+ items: {
14
+ type: "object",
15
+ required: ["name", "path"],
16
+ properties: {
17
+ name: { type: "string", description: "仓库名,例如 frontend / backend" },
18
+ path: { type: "string", description: "绝对路径" },
19
+ description: { type: "string", description: "该路径职责说明,会注入 AI 上下文" },
20
+ },
21
+ },
22
+ },
23
+ },
24
+ };
25
+ export const INIT_TOOL_DESCRIPTION = "初始化或重建当前工作区的 workspace-scope 配置(.opencode/workspace-scope.jsonc)。不传 worktrees 时自动探测父目录下的兄弟 git 仓库作为候选 worktree。生成的 sidecar 不纳入版本管理(自动写入 .gitignore),并附带 JSON Schema。";
6
26
  function isGitRepo(p) {
7
27
  return existsSync(join(p, ".git"));
8
28
  }
@@ -51,60 +71,44 @@ function writeSchema(sidecarDir) {
51
71
  // ignore
52
72
  }
53
73
  }
54
- export const initTool = tool({
55
- description: "初始化或重建当前工作区的 workspace-scope 配置(.opencode/workspace-scope.jsonc)。不传 worktrees 时自动探测父目录下的兄弟 git 仓库作为候选 worktree。生成的 sidecar 不纳入版本管理(自动写入 .gitignore),并附带 JSON Schema。",
56
- args: {
57
- name: tool.schema.string().optional().describe("工作区名称,缺省使用当前目录名"),
58
- workspaceDescription: tool.schema.string().optional().describe("工作区职责说明"),
59
- worktrees: tool.schema
60
- .array(tool.schema.object({
61
- name: tool.schema.string().describe("仓库名,例如 frontend / backend"),
62
- path: tool.schema.string().describe("绝对路径"),
63
- description: tool.schema.string().optional().describe("该路径职责说明,会注入 AI 上下文"),
64
- }))
65
- .optional()
66
- .describe("可选:显式指定 worktree 列表;缺省自动探测父目录兄弟 git 仓库"),
67
- },
68
- async execute(args, context) {
69
- const anchor = context.worktree || context.directory;
70
- const sidecarDir = join(anchor, ".opencode");
71
- mkdirSync(sidecarDir, { recursive: true });
72
- const worktrees = args.worktrees && args.worktrees.length > 0 ? args.worktrees : detectSiblingRepos(anchor);
73
- if (worktrees.length === 0) {
74
- return [
75
- "未探测到兄弟 git 仓库,也未收到显式 worktrees 参数。",
76
- "请传入 worktrees(name+path+description),或确认已在 workspace 根目录启动 opencode。",
77
- ].join("\n");
78
- }
79
- const filePath = join(sidecarDir, "workspace-scope.jsonc");
80
- const content = {
81
- $schema: "./workspace-scope.schema.json",
82
- name: args.name || basename(anchor),
83
- workspace: {
84
- path: anchor,
85
- description: args.workspaceDescription,
86
- },
87
- worktrees: worktrees.map((w) => ({
88
- name: w.name,
89
- path: w.path,
90
- description: w.description,
91
- })),
92
- };
93
- writeFileSync(filePath, JSON.stringify(content, null, 2));
94
- writeSchema(sidecarDir);
95
- ensureGitignore(sidecarDir, ["workspace-scope.jsonc", "workspace-scope.schema.json"]);
96
- invalidateScope();
74
+ export function runInit(anchor, args) {
75
+ const sidecarDir = join(anchor, ".opencode");
76
+ mkdirSync(sidecarDir, { recursive: true });
77
+ const worktrees = args.worktrees && args.worktrees.length > 0 ? args.worktrees : detectSiblingRepos(anchor);
78
+ if (worktrees.length === 0) {
97
79
  return [
98
- `已生成 ${filePath}`,
99
- "",
100
- "内容:",
101
- "```jsonc",
102
- JSON.stringify(content, null, 2),
103
- "```",
104
- "",
105
- "下一步:",
106
- "1. 编辑该文件,为每个 worktree 补充 description(职责说明,会注入 AI 上下文);",
107
- "2. 重启 opencode 使 external_directory 权限生效(上下文注入无需重启)。",
80
+ "未探测到兄弟 git 仓库,也未收到显式 worktrees 参数。",
81
+ "请传入 worktrees(name+path+description),或确认已在 workspace 根目录启动 opencode。",
108
82
  ].join("\n");
109
- },
110
- });
83
+ }
84
+ const filePath = join(sidecarDir, "workspace-scope.jsonc");
85
+ const content = {
86
+ $schema: "./workspace-scope.schema.json",
87
+ name: args.name || basename(anchor),
88
+ workspace: {
89
+ path: anchor,
90
+ description: args.workspaceDescription,
91
+ },
92
+ worktrees: worktrees.map((w) => ({
93
+ name: w.name,
94
+ path: w.path,
95
+ description: w.description,
96
+ })),
97
+ };
98
+ writeFileSync(filePath, JSON.stringify(content, null, 2));
99
+ writeSchema(sidecarDir);
100
+ ensureGitignore(sidecarDir, ["workspace-scope.jsonc", "workspace-scope.schema.json"]);
101
+ invalidateScope();
102
+ return [
103
+ `已生成 ${filePath}`,
104
+ "",
105
+ "内容:",
106
+ "```jsonc",
107
+ JSON.stringify(content, null, 2),
108
+ "```",
109
+ "",
110
+ "下一步:",
111
+ "1. 编辑该文件,为每个 worktree 补充 description(职责说明,会注入 AI 上下文);",
112
+ "2. 重启 opencode 使 external_directory 权限生效(上下文注入无需重启)。",
113
+ ].join("\n");
114
+ }
@@ -1,4 +1,13 @@
1
1
  import type { ScopeResult } from "./scope.js";
2
- export declare function buildExternalDirectory(scope: ScopeResult): Record<string, string>;
3
- export declare function mergeExternalDirectory(current: unknown, allows: Record<string, string>): Record<string, string>;
4
- export declare function mergePermission(current: unknown, external: Record<string, string>): unknown;
2
+ /**
3
+ * 归一化 external_directory 资源:去掉结尾通配与斜杠、展开 ~,
4
+ * 得到目录边界路径(V2 中资源通常以 "dir/*" 形式出现且已规范化)。
5
+ */
6
+ export declare function normalizeBoundary(resource: string): string;
7
+ /** 单个 external_directory 资源边界是否落在作用域声明的 worktree(词法路径或 realpath)内 */
8
+ export declare function inScope(resource: string, scope: ScopeResult): boolean;
9
+ /**
10
+ * 全部资源都命中作用域才放行;部分命中时保持默认(ask),
11
+ * 避免把边界外资源一并放行造成越权放大。
12
+ */
13
+ export declare function coveredByScope(resources: ReadonlyArray<string>, scope: ScopeResult): boolean;
@@ -1,36 +1,32 @@
1
- import { existsSync } from "node:fs";
2
- function patternPath(p) {
3
- return p.replaceAll("\\", "/") + "/**";
1
+ import { isAbsolute, relative } from "node:path";
2
+ import { homedir } from "node:os";
3
+ /**
4
+ * 归一化 external_directory 资源:去掉结尾通配与斜杠、展开 ~,
5
+ * 得到目录边界路径(V2 中资源通常以 "dir/*" 形式出现且已规范化)。
6
+ */
7
+ export function normalizeBoundary(resource) {
8
+ let p = resource.replaceAll("\\", "/");
9
+ if (p === "~" || p.startsWith("~/"))
10
+ p = homedir() + p.slice(1);
11
+ if (p.endsWith("/*"))
12
+ p = p.slice(0, -2);
13
+ return p.replace(/\/+$/, "");
4
14
  }
5
- export function buildExternalDirectory(scope) {
6
- const rules = {};
7
- for (const wt of scope.config.worktrees) {
8
- if (!existsSync(wt.path))
9
- continue;
10
- rules[patternPath(wt.path)] = "allow";
11
- if (wt.realpath !== wt.path)
12
- rules[patternPath(wt.realpath)] = "allow";
13
- }
14
- return rules;
15
+ function isWithin(candidate, root) {
16
+ if (candidate === root)
17
+ return true;
18
+ const rel = relative(root, candidate);
19
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
15
20
  }
16
- export function mergeExternalDirectory(current, allows) {
17
- if (current && typeof current === "object" && !Array.isArray(current)) {
18
- return { ...current, ...allows };
19
- }
20
- if (typeof current === "string") {
21
- return { "*": current, ...allows };
22
- }
23
- return allows;
21
+ /** 单个 external_directory 资源边界是否落在作用域声明的 worktree(词法路径或 realpath)内 */
22
+ export function inScope(resource, scope) {
23
+ const boundary = normalizeBoundary(resource);
24
+ return scope.config.worktrees.some((wt) => isWithin(boundary, wt.path) || isWithin(boundary, wt.realpath));
24
25
  }
25
- export function mergePermission(current, external) {
26
- if (typeof current === "string")
27
- return current;
28
- if (!current || typeof current !== "object" || Array.isArray(current)) {
29
- return { external_directory: external };
30
- }
31
- const base = current;
32
- return {
33
- ...base,
34
- external_directory: mergeExternalDirectory(base.external_directory, external),
35
- };
26
+ /**
27
+ * 全部资源都命中作用域才放行;部分命中时保持默认(ask),
28
+ * 避免把边界外资源一并放行造成越权放大。
29
+ */
30
+ export function coveredByScope(resources, scope) {
31
+ return resources.length > 0 && resources.every((r) => inScope(r, scope));
36
32
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-workspace-scope",
3
- "version": "0.1.1",
4
- "description": "全局安装、sidecar 自激活的 opencode 插件:按工作区配置自动生成 external_directory 权限,并把工作区 worktree 路径与职责注入 AI 上下文,支撑多 worktree 并行开发互不干扰。",
3
+ "version": "0.2.0-beta.1",
4
+ "description": "全局安装、sidecar 自激活的 opencode2 插件:在工作区权限评估时动态放行已声明 worktree external_directory,并把工作区路径与职责注入 AI 上下文,支撑多 worktree 并行开发互不干扰。",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "keywords": [
23
23
  "opencode",
24
+ "opencode2",
24
25
  "plugin",
25
26
  "worktree",
26
27
  "workspace",
@@ -28,7 +29,7 @@
28
29
  ],
29
30
  "license": "MIT",
30
31
  "dependencies": {
31
- "@opencode-ai/plugin": "^1.18.3"
32
+ "@opencode-ai/plugin": "0.0.0-beta-19151"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "^22.0.0",