@link-assistant/agent 0.22.10 → 0.24.0

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/src/tool/edit.ts CHANGED
@@ -13,6 +13,7 @@ import { Bus } from '../bus';
13
13
  import { FileTime } from '../file/time';
14
14
  import { Instance } from '../project/instance';
15
15
  import { Snapshot } from '../snapshot';
16
+ import { Permission } from '../permission';
16
17
 
17
18
  function normalizeLineEndings(text: string): string {
18
19
  return text.replaceAll('\r\n', '\n');
@@ -42,11 +43,21 @@ export const EditTool = Tool.define('edit', {
42
43
  throw new Error('oldString and newString must be different');
43
44
  }
44
45
 
45
- // No restrictions - unrestricted file editing
46
46
  const filePath = path.isAbsolute(params.filePath)
47
47
  ? params.filePath
48
48
  : path.join(Instance.directory, params.filePath);
49
49
 
50
+ // Permission enforcement (issue #271). `auto` mode allows immediately;
51
+ // plan/readonly modes deny; `ask` mode emits a JSON permission request.
52
+ await Permission.check({
53
+ type: 'edit',
54
+ title: filePath,
55
+ sessionID: ctx.sessionID,
56
+ messageID: ctx.messageID,
57
+ callID: ctx.callID,
58
+ metadata: { filePath },
59
+ });
60
+
50
61
  let diff = '';
51
62
  let contentOld = '';
52
63
  let contentNew = '';
package/src/tool/patch.ts CHANGED
@@ -8,6 +8,7 @@ import { FileWatcher } from '../file/watcher';
8
8
  import { Instance } from '../project/instance';
9
9
  import { Patch } from '../patch';
10
10
  import { Filesystem } from '../util/filesystem';
11
+ import { Permission } from '../permission';
11
12
  import { createTwoFilesPatch } from 'diff';
12
13
 
13
14
  const PatchParams = z.object({
@@ -141,7 +142,24 @@ export const PatchTool = Tool.define('patch', {
141
142
  }
142
143
  }
143
144
 
144
- // No restrictions - apply changes directly
145
+ // Permission enforcement (issue #271). Patching mutates the filesystem and
146
+ // is governed by the `edit` policy. `auto` allows; plan/readonly deny;
147
+ // `ask` emits a JSON permission request per affected file.
148
+ for (const change of fileChanges) {
149
+ await Permission.check({
150
+ type: 'edit',
151
+ title: change.movePath ?? change.filePath,
152
+ sessionID: ctx.sessionID,
153
+ messageID: ctx.messageID,
154
+ callID: ctx.callID,
155
+ metadata: {
156
+ filePath: change.filePath,
157
+ movePath: change.movePath,
158
+ changeType: change.type,
159
+ },
160
+ });
161
+ }
162
+
145
163
  // Apply the changes
146
164
  const changedFiles: string[] = [];
147
165
 
@@ -16,8 +16,44 @@ import { Instance } from '../project/instance';
16
16
  import { Config } from '../config/file-config';
17
17
  import { WebSearchTool } from './websearch';
18
18
  import { CodeSearchTool } from './codesearch';
19
+ import { config } from '../config/config';
19
20
 
20
21
  export namespace ToolRegistry {
22
+ /**
23
+ * Tools that can mutate the filesystem or execute shell commands.
24
+ * These are disabled when read-only / planning mode is active so that the
25
+ * agent can only read, search and plan. This is the native, enforceable
26
+ * counterpart of agent-commander's `--read-only` flag (issue #271).
27
+ */
28
+ export const READ_ONLY_DENIED_TOOLS = [
29
+ 'bash',
30
+ 'edit',
31
+ 'write',
32
+ 'multiedit',
33
+ 'patch',
34
+ ] as const;
35
+
36
+ /**
37
+ * Resolve the set of tool ids that must be denied for the current run.
38
+ *
39
+ * Combines the read-only/planning preset (`config.readOnly`) with any
40
+ * explicit `--disable-tools a,b,c` list. The result is enforced both when
41
+ * exposing tools to the model and when tools are invoked indirectly via the
42
+ * `batch` tool, so the restriction cannot be bypassed.
43
+ */
44
+ export function deniedTools(): Set<string> {
45
+ const denied = new Set<string>();
46
+ if (config.readOnly) {
47
+ for (const id of READ_ONLY_DENIED_TOOLS) denied.add(id);
48
+ }
49
+ const list = config.disableTools ?? '';
50
+ for (const raw of list.split(',')) {
51
+ const id = raw.trim().toLowerCase();
52
+ if (id) denied.add(id);
53
+ }
54
+ return denied;
55
+ }
56
+
21
57
  export const state = Instance.state(async () => {
22
58
  const custom = [] as Tool.Info[];
23
59
  return { custom };
@@ -3,6 +3,7 @@ import { Tool } from './tool';
3
3
  import TurndownService from 'turndown';
4
4
  import DESCRIPTION from './webfetch.txt';
5
5
  import { createVerboseFetch } from '../util/verbose-fetch';
6
+ import { Permission } from '../permission';
6
7
 
7
8
  const verboseFetch = createVerboseFetch(fetch, { caller: 'webfetch' });
8
9
 
@@ -33,7 +34,17 @@ export const WebFetchTool = Tool.define('webfetch', {
33
34
  throw new Error('URL must start with http:// or https://');
34
35
  }
35
36
 
36
- // No restrictions - unrestricted web fetch
37
+ // Permission enforcement (issue #271). `auto` allows; plan/readonly allow
38
+ // (webfetch is read-only); `ask` emits a JSON permission request.
39
+ await Permission.check({
40
+ type: 'webfetch',
41
+ title: params.url,
42
+ sessionID: ctx.sessionID,
43
+ messageID: ctx.messageID,
44
+ callID: ctx.callID,
45
+ metadata: { url: params.url },
46
+ });
47
+
37
48
  const timeout = Math.min(
38
49
  (params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
39
50
  MAX_TIMEOUT
package/src/tool/write.ts CHANGED
@@ -3,6 +3,7 @@ import * as path from 'path';
3
3
  import { Tool } from './tool';
4
4
  import DESCRIPTION from './write.txt';
5
5
  import { Instance } from '../project/instance';
6
+ import { Permission } from '../permission';
6
7
 
7
8
  export const WriteTool = Tool.define('write', {
8
9
  description: DESCRIPTION,
@@ -15,11 +16,22 @@ export const WriteTool = Tool.define('write', {
15
16
  ),
16
17
  }),
17
18
  async execute(params, ctx) {
18
- // No restrictions - unrestricted file system access
19
19
  const filepath = path.isAbsolute(params.filePath)
20
20
  ? params.filePath
21
21
  : path.join(Instance.directory, params.filePath);
22
22
 
23
+ // Permission enforcement (issue #271). Writing is governed by the same
24
+ // `edit` policy as the edit tool: `auto` allows, plan/readonly deny,
25
+ // `ask` emits a JSON permission request.
26
+ await Permission.check({
27
+ type: 'edit',
28
+ title: filepath,
29
+ sessionID: ctx.sessionID,
30
+ messageID: ctx.messageID,
31
+ callID: ctx.callID,
32
+ metadata: { filePath: filepath },
33
+ });
34
+
23
35
  const file = Bun.file(filepath);
24
36
  const exists = await file.exists();
25
37