hoomanjs 1.11.0 → 1.11.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hoomanjs",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "Bun-powered local AI agent CLI with chat, exec, ACP, MCP, and skills support.",
5
5
  "author": {
6
6
  "name": "Vaibhav Pandey",
@@ -350,7 +350,6 @@ export class AcpAgent implements AgentContract {
350
350
  createAcpToolApprovalHook(
351
351
  this.#connection,
352
352
  sessionId,
353
- config,
354
353
  () =>
355
354
  this.#sessions.get(sessionId)?.streamedToolCallIds ??
356
355
  EMPTY_STREAMED_TOOL_CALL_IDS,
@@ -458,7 +457,6 @@ export class AcpAgent implements AgentContract {
458
457
  createAcpToolApprovalHook(
459
458
  this.#connection,
460
459
  params.sessionId,
461
- config,
462
460
  () =>
463
461
  this.#sessions.get(params.sessionId)?.streamedToolCallIds ??
464
462
  EMPTY_STREAMED_TOOL_CALL_IDS,
@@ -1,17 +1,16 @@
1
1
  import type { AgentSideConnection } from "@agentclientprotocol/sdk";
2
2
  import { BeforeToolCallEvent, type HookCallback } from "@strands-agents/sdk";
3
- import type { Config } from "../core/config.ts";
4
3
  import {
5
4
  INTERNAL_ALWAYS_ALLOWED,
6
- inferToolKind,
7
- toolDisplayTitle,
8
- } from "./utils/tool-kind.ts";
5
+ allowToolForSession,
6
+ isToolSessionAllowed,
7
+ } from "../core/approvals/allowed-tools.ts";
8
+ import { inferToolKind, toolDisplayTitle } from "./utils/tool-kind.ts";
9
9
  import { toolCallLocationsFromInput } from "./utils/tool-locations.ts";
10
10
 
11
11
  export function createAcpToolApprovalHook(
12
12
  connection: AgentSideConnection,
13
13
  sessionId: string,
14
- config: Config,
15
14
  /** Tool calls already announced via model stream (`tool_call` pending). */
16
15
  streamPrimedToolCallIds?: () => ReadonlySet<string>,
17
16
  ): HookCallback<BeforeToolCallEvent> {
@@ -53,7 +52,7 @@ export function createAcpToolApprovalHook(
53
52
 
54
53
  if (
55
54
  INTERNAL_ALWAYS_ALLOWED.has(name) ||
56
- config.tools.allowed.includes(name)
55
+ isToolSessionAllowed(event.agent, name)
57
56
  ) {
58
57
  await connection.sessionUpdate({
59
58
  sessionId,
@@ -122,7 +121,7 @@ export function createAcpToolApprovalHook(
122
121
  return;
123
122
  }
124
123
  if (optionId === "allow_always") {
125
- config.update({ tools: { allowed: [...config.tools.allowed, name] } });
124
+ allowToolForSession(event.agent, name);
126
125
  await connection.sessionUpdate({
127
126
  sessionId,
128
127
  update: {
@@ -1,7 +1,7 @@
1
1
  import type { ToolKind } from "@agentclientprotocol/sdk";
2
2
  import type { Tool } from "@strands-agents/sdk";
3
+ import { INTERNAL_ALWAYS_ALLOWED } from "../../core/approvals/allowed-tools.ts";
3
4
 
4
- const INTERNAL_ALWAYS_ALLOWED = new Set(["strands_structured_output"]);
5
5
  const KNOWN_TOOL_KINDS = new Map<string, ToolKind>([
6
6
  ["read_file", "read"],
7
7
  ["read_multiple_files", "read"],
package/src/chat/app.tsx CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  type Agent,
12
12
  type AgentStreamEvent,
13
13
  } from "@strands-agents/sdk";
14
- import type { Config } from "../core/config.ts";
15
14
  import type { Manager as McpManager } from "../core/mcp/index.ts";
16
15
  import type { Registry } from "../core/skills/index.ts";
17
16
  import {
@@ -26,7 +25,6 @@ import type { ApprovalRequest, ChatLine } from "./types.ts";
26
25
 
27
26
  type ChatAppProps = {
28
27
  agent: Agent;
29
- config: Config;
30
28
  sessionId: string;
31
29
  manager: McpManager;
32
30
  registry: Registry;
@@ -69,7 +67,6 @@ function toToolResultText(result: unknown): string {
69
67
 
70
68
  export function ChatApp({
71
69
  agent,
72
- config,
73
70
  sessionId,
74
71
  manager,
75
72
  registry,
@@ -143,13 +140,13 @@ export function ChatApp({
143
140
  });
144
141
  const cleanupHook = agent.addHook(
145
142
  BeforeToolCallEvent,
146
- createChatApprovalHandler(config, controller, { yolo }),
143
+ createChatApprovalHandler(controller, { yolo }),
147
144
  );
148
145
  return () => {
149
146
  cleanupListener();
150
147
  cleanupHook();
151
148
  };
152
- }, [agent, config, yolo]);
149
+ }, [agent, yolo]);
153
150
 
154
151
  const appendLine = useCallback((line: ChatLine) => {
155
152
  setLines((prev) => [...prev, line]);
@@ -1,8 +1,10 @@
1
1
  import { BeforeToolCallEvent } from "@strands-agents/sdk";
2
- import type { Config } from "../core/config.ts";
2
+ import {
3
+ INTERNAL_ALWAYS_ALLOWED,
4
+ allowToolForSession,
5
+ isToolSessionAllowed,
6
+ } from "../core/approvals/allowed-tools.ts";
3
7
  import type { ApprovalDecision, ApprovalRequest } from "./types.ts";
4
-
5
- const INTERNAL_ALWAYS_ALLOWED = new Set(["strands_structured_output"]);
6
8
  const INPUT_PREVIEW_LIMIT = 256;
7
9
 
8
10
  function previewInput(input: unknown): string {
@@ -67,7 +69,6 @@ export class ChatApprovalController {
67
69
  }
68
70
 
69
71
  export function createChatApprovalHandler(
70
- config: Config,
71
72
  controller: ChatApprovalController,
72
73
  options?: { yolo?: boolean },
73
74
  ): (event: BeforeToolCallEvent) => Promise<void> {
@@ -78,7 +79,7 @@ export function createChatApprovalHandler(
78
79
  }
79
80
  if (
80
81
  INTERNAL_ALWAYS_ALLOWED.has(toolName) ||
81
- config.tools.allowed.includes(toolName)
82
+ isToolSessionAllowed(event.agent, toolName)
82
83
  ) {
83
84
  return;
84
85
  }
@@ -88,11 +89,7 @@ export function createChatApprovalHandler(
88
89
  return;
89
90
  }
90
91
  if (decision === "always") {
91
- if (!config.tools.allowed.includes(toolName)) {
92
- config.update({
93
- tools: { allowed: [...config.tools.allowed, toolName] },
94
- });
95
- }
92
+ allowToolForSession(event.agent, toolName);
96
93
  return;
97
94
  }
98
95
  event.cancel = `Tool "${toolName}" was rejected by the user.`;
@@ -1,14 +1,12 @@
1
1
  import React from "react";
2
2
  import { render } from "ink";
3
3
  import type { Agent } from "@strands-agents/sdk";
4
- import type { Config } from "../core/config.ts";
5
4
  import type { Manager as McpManager } from "../core/mcp/index.ts";
6
5
  import type { Registry } from "../core/skills/index.ts";
7
6
  import { ChatApp } from "./app.tsx";
8
7
 
9
8
  type LaunchChatOptions = {
10
9
  agent: Agent;
11
- config: Config;
12
10
  manager: McpManager;
13
11
  registry: Registry;
14
12
  sessionId: string;
@@ -21,7 +19,6 @@ export async function chat(options: LaunchChatOptions): Promise<void> {
21
19
  const { waitUntilExit, unmount } = render(
22
20
  <ChatApp
23
21
  agent={options.agent}
24
- config={options.config}
25
22
  manager={options.manager}
26
23
  registry={options.registry}
27
24
  sessionId={options.sessionId}
package/src/cli.ts CHANGED
@@ -53,13 +53,12 @@ program
53
53
  async (prompt: string, options: { session?: string; yolo?: boolean }) => {
54
54
  const sessionId = options.session?.trim() || crypto.randomUUID();
55
55
  const {
56
- config,
57
56
  agent,
58
57
  mcp: { manager },
59
58
  } = await bootstrap({ sessionId }, true);
60
59
  agent.addHook(
61
60
  BeforeToolCallEvent,
62
- createToolApprovalHandler(config, { yolo: Boolean(options.yolo) }),
61
+ createToolApprovalHandler({ yolo: Boolean(options.yolo) }),
63
62
  );
64
63
  try {
65
64
  await agent.invoke(prompt);
@@ -84,7 +83,6 @@ program
84
83
  ) => {
85
84
  const sessionId = options.session?.trim() || crypto.randomUUID();
86
85
  const {
87
- config,
88
86
  agent,
89
87
  mcp: { manager },
90
88
  registry,
@@ -93,7 +91,6 @@ program
93
91
  try {
94
92
  await chat({
95
93
  agent,
96
- config,
97
94
  manager,
98
95
  registry,
99
96
  sessionId,
@@ -129,7 +126,6 @@ program
129
126
  }) => {
130
127
  const session = options.session?.trim();
131
128
  const {
132
- config,
133
129
  agent,
134
130
  mcp: { manager },
135
131
  } = await bootstrap(
@@ -141,7 +137,7 @@ program
141
137
  );
142
138
  agent.addHook(
143
139
  BeforeToolCallEvent,
144
- createDaemonApprovalHandler(config, manager, agent, {
140
+ createDaemonApprovalHandler(manager, agent, {
145
141
  yolo: Boolean(options.yolo),
146
142
  }),
147
143
  );
@@ -0,0 +1,61 @@
1
+ type AppStateLike = {
2
+ get<T = unknown>(key: string): T;
3
+ set(key: string, value: unknown): void;
4
+ };
5
+
6
+ type AgentLike = {
7
+ appState: AppStateLike;
8
+ };
9
+
10
+ const SESSION_ALLOWED_TOOLS_KEY = "allowedTools";
11
+
12
+ export const INTERNAL_ALWAYS_ALLOWED = new Set(["strands_structured_output"]);
13
+
14
+ function normalizeAllowedTools(value: unknown): string[] {
15
+ if (!Array.isArray(value)) {
16
+ return [];
17
+ }
18
+
19
+ const unique = new Set<string>();
20
+ for (const entry of value) {
21
+ if (typeof entry !== "string") {
22
+ continue;
23
+ }
24
+ const normalized = entry.trim();
25
+ if (!normalized) {
26
+ continue;
27
+ }
28
+ unique.add(normalized);
29
+ }
30
+ return [...unique];
31
+ }
32
+
33
+ export function getSessionAllowedTools(agent: AgentLike): string[] {
34
+ const current = normalizeAllowedTools(
35
+ agent.appState.get(SESSION_ALLOWED_TOOLS_KEY),
36
+ );
37
+ const raw = agent.appState.get(SESSION_ALLOWED_TOOLS_KEY);
38
+ if (!Array.isArray(raw) || current.length !== raw.length) {
39
+ agent.appState.set(SESSION_ALLOWED_TOOLS_KEY, current);
40
+ }
41
+ return current;
42
+ }
43
+
44
+ export function isToolSessionAllowed(
45
+ agent: AgentLike,
46
+ toolName: string,
47
+ ): boolean {
48
+ return getSessionAllowedTools(agent).includes(toolName);
49
+ }
50
+
51
+ export function allowToolForSession(agent: AgentLike, toolName: string): void {
52
+ const normalized = toolName.trim();
53
+ if (!normalized) {
54
+ return;
55
+ }
56
+ const allowed = getSessionAllowedTools(agent);
57
+ if (allowed.includes(normalized)) {
58
+ return;
59
+ }
60
+ agent.appState.set(SESSION_ALLOWED_TOOLS_KEY, [...allowed, normalized]);
61
+ }
@@ -2,21 +2,26 @@ import {
2
2
  AfterInvocationEvent,
3
3
  BeforeInvocationEvent,
4
4
  Message,
5
+ contentBlockFromData,
5
6
  type LocalAgent,
6
7
  type MessageData,
7
8
  type Plugin,
9
+ type SystemPrompt,
10
+ type SystemPromptData,
8
11
  type Snapshot,
9
12
  type SnapshotLocation,
10
13
  type SnapshotStorage,
11
14
  } from "@strands-agents/sdk";
15
+ import type { JSONValue } from "@strands-agents/sdk";
12
16
 
13
17
  const DEFAULT_SESSION_ID = "default-session";
14
18
  const DEFAULT_APP_STATE_KEY = "sessionId";
15
19
  const DEFAULT_SCOPE_ID = "agent";
16
- const SCHEMA_VERSION = "1.0";
20
+ const SNAPSHOT_SCHEMA_VERSION = "1.0";
17
21
  // `FileStorage` (and any backend that follows its convention) validates ids
18
22
  // against `[a-z0-9_-]+`, so coerce anything else (e.g. `919599960600@c.us`).
19
23
  const UNSAFE_CHARS = /[^a-z0-9_-]+/g;
24
+ const RUNTIME_OWNED_KEYS = ["userId", "origin"] as const;
20
25
 
21
26
  export type LazySessionManagerConfig = {
22
27
  /** Pluggable snapshot backend (e.g. `FileStorage`). */
@@ -86,27 +91,21 @@ export class LazySessionManager implements Plugin {
86
91
  }
87
92
 
88
93
  private async restore(agent: LocalAgent): Promise<void> {
94
+ const runtimeState = this.captureRuntimeState(agent);
89
95
  const snapshot = await this.storage.loadSnapshot({
90
96
  location: this.location(agent),
91
97
  });
92
98
  agent.messages.length = 0;
93
- if (!snapshot) return;
94
- const raw = snapshot.data.messages;
95
- if (!Array.isArray(raw)) return;
96
- for (const md of raw as unknown as MessageData[]) {
97
- agent.messages.push(Message.fromJSON(md));
99
+ if (!snapshot) {
100
+ this.restoreRuntimeState(agent, runtimeState);
101
+ return;
98
102
  }
103
+ loadSnapshot(agent, snapshot);
104
+ this.restoreRuntimeState(agent, runtimeState);
99
105
  }
100
106
 
101
107
  private async save(agent: LocalAgent): Promise<void> {
102
- const messages = agent.messages.map((m) => m.toJSON());
103
- const snapshot: Snapshot = {
104
- scope: "agent",
105
- schemaVersion: SCHEMA_VERSION,
106
- createdAt: new Date().toISOString(),
107
- data: { messages: messages as unknown as Snapshot["data"]["messages"] },
108
- appData: {},
109
- };
108
+ const snapshot = takeSnapshot(agent);
110
109
  await this.storage.saveSnapshot({
111
110
  location: this.location(agent),
112
111
  snapshotId: "latest",
@@ -114,9 +113,108 @@ export class LazySessionManager implements Plugin {
114
113
  snapshot,
115
114
  });
116
115
  }
116
+
117
+ private captureRuntimeState(agent: LocalAgent): Map<string, unknown> {
118
+ const state = new Map<string, unknown>();
119
+ const keys = [this.appStateKey, ...RUNTIME_OWNED_KEYS];
120
+ for (const key of keys) {
121
+ state.set(key, agent.appState.get(key));
122
+ }
123
+ return state;
124
+ }
125
+
126
+ private restoreRuntimeState(
127
+ agent: LocalAgent,
128
+ runtimeState: Map<string, unknown>,
129
+ ): void {
130
+ for (const [key, value] of runtimeState.entries()) {
131
+ if (value === undefined) {
132
+ agent.appState.delete(key);
133
+ continue;
134
+ }
135
+ agent.appState.set(key, value);
136
+ }
137
+ }
117
138
  }
118
139
 
119
140
  function sanitize(value: string): string {
120
141
  const trimmed = value.trim().toLowerCase().replace(UNSAFE_CHARS, "_");
121
142
  return trimmed.length > 0 ? trimmed : DEFAULT_SESSION_ID;
122
143
  }
144
+
145
+ function takeSnapshot(agent: LocalAgent): Snapshot {
146
+ const data: Record<string, JSONValue> = {
147
+ messages: agent.messages.map((message) =>
148
+ message.toJSON(),
149
+ ) as unknown as JSONValue,
150
+ state: agent.appState.getAll() as JSONValue,
151
+ systemPrompt:
152
+ agent.systemPrompt !== undefined
153
+ ? (systemPromptToData(agent.systemPrompt) as JSONValue)
154
+ : null,
155
+ };
156
+
157
+ return {
158
+ scope: "agent",
159
+ schemaVersion: SNAPSHOT_SCHEMA_VERSION,
160
+ createdAt: new Date().toISOString(),
161
+ data,
162
+ appData: {},
163
+ };
164
+ }
165
+
166
+ function loadSnapshot(agent: LocalAgent, snapshot: Snapshot): void {
167
+ if (snapshot.scope !== "agent") {
168
+ throw new Error(`Expected snapshot scope 'agent', got '${snapshot.scope}'`);
169
+ }
170
+ if (snapshot.schemaVersion !== SNAPSHOT_SCHEMA_VERSION) {
171
+ throw new Error(
172
+ `Unsupported snapshot schema version: ${snapshot.schemaVersion}. Current version: ${SNAPSHOT_SCHEMA_VERSION}`,
173
+ );
174
+ }
175
+
176
+ if ("messages" in snapshot.data) {
177
+ const messages = snapshot.data.messages;
178
+ agent.messages.length = 0;
179
+ for (const msgData of messages as unknown as MessageData[]) {
180
+ agent.messages.push(Message.fromJSON(msgData));
181
+ }
182
+ }
183
+
184
+ if ("state" in snapshot.data) {
185
+ const state = snapshot.data.state;
186
+ const nextState =
187
+ state && typeof state === "object" && !Array.isArray(state)
188
+ ? (state as Record<string, JSONValue>)
189
+ : {};
190
+ agent.appState.clear();
191
+ for (const [key, value] of Object.entries(nextState)) {
192
+ agent.appState.set(key, value);
193
+ }
194
+ }
195
+
196
+ if ("systemPrompt" in snapshot.data) {
197
+ const systemPrompt = snapshot.data.systemPrompt;
198
+ if (systemPrompt !== null) {
199
+ agent.systemPrompt = systemPromptFromData(
200
+ systemPrompt as SystemPromptData,
201
+ );
202
+ } else {
203
+ delete agent.systemPrompt;
204
+ }
205
+ }
206
+ }
207
+
208
+ function systemPromptToData(prompt: SystemPrompt): SystemPromptData {
209
+ if (typeof prompt === "string") {
210
+ return prompt;
211
+ }
212
+ return prompt.map((block) => block.toJSON()) as SystemPromptData;
213
+ }
214
+
215
+ function systemPromptFromData(data: SystemPromptData): SystemPrompt {
216
+ if (typeof data === "string") {
217
+ return data;
218
+ }
219
+ return data.map((block) => contentBlockFromData(block)) as SystemPrompt;
220
+ }
@@ -1,9 +1,13 @@
1
1
  import type { Agent, BeforeToolCallEvent } from "@strands-agents/sdk";
2
- import type { Config } from "../core/config.ts";
3
2
  import type { Manager as McpManager } from "../core/mcp/index.ts";
4
- import { INTERNAL_ALWAYS_ALLOWED } from "../acp/utils/tool-kind.ts";
3
+ import {
4
+ INTERNAL_ALWAYS_ALLOWED,
5
+ allowToolForSession,
6
+ isToolSessionAllowed,
7
+ } from "../core/approvals/allowed-tools.ts";
5
8
 
6
- const INPUT_PREVIEW_LIMIT = 1_024;
9
+ const TOOL_DESCRIPTION_PREVIEW_LIMIT = 50;
10
+ const TOOL_ARGS_PREVIEW_LIMIT = 50;
7
11
 
8
12
  type ChannelOrigin = {
9
13
  server?: string;
@@ -17,14 +21,24 @@ function randomRequestId(): string {
17
21
  return crypto.randomUUID();
18
22
  }
19
23
 
24
+ function truncateWithEllipsis(text: string, max: number): string {
25
+ return text.length > max ? `${text.slice(0, max)}…` : text;
26
+ }
27
+
28
+ function truncateWithHiddenCharCount(text: string, max: number): string {
29
+ if (text.length <= max) {
30
+ return text;
31
+ }
32
+ const hidden = text.length - max;
33
+ return `${text.slice(0, max)}…(${hidden} chars)`;
34
+ }
35
+
20
36
  function inputPreview(input: unknown): string {
21
37
  try {
22
- const text = JSON.stringify(input, null, 2) ?? "null";
23
- return text.length > INPUT_PREVIEW_LIMIT
24
- ? `${text.slice(0, INPUT_PREVIEW_LIMIT)}\n... (truncated)`
25
- : text;
38
+ const text = JSON.stringify(input) ?? "null";
39
+ return truncateWithHiddenCharCount(text, TOOL_ARGS_PREVIEW_LIMIT);
26
40
  } catch {
27
- return String(input);
41
+ return truncateWithHiddenCharCount(String(input), TOOL_ARGS_PREVIEW_LIMIT);
28
42
  }
29
43
  }
30
44
 
@@ -51,7 +65,6 @@ function readOrigin(agent: Agent): ChannelOrigin | null {
51
65
  }
52
66
 
53
67
  export function createDaemonApprovalHandler(
54
- config: Config,
55
68
  manager: McpManager,
56
69
  agent: Agent,
57
70
  options?: { yolo?: boolean },
@@ -63,7 +76,7 @@ export function createDaemonApprovalHandler(
63
76
  }
64
77
  if (
65
78
  INTERNAL_ALWAYS_ALLOWED.has(name) ||
66
- config.tools.allowed.includes(name)
79
+ isToolSessionAllowed(event.agent, name)
67
80
  ) {
68
81
  return;
69
82
  }
@@ -85,9 +98,11 @@ export function createDaemonApprovalHandler(
85
98
  behavior = await manager.requestChannelPermission(origin.server, {
86
99
  requestId: randomRequestId(),
87
100
  tool: name,
88
- description:
101
+ description: truncateWithEllipsis(
89
102
  event.tool?.description?.trim() ??
90
- `Run tool "${name}" in daemon mode.`,
103
+ `Run tool "${name}" in daemon mode.`,
104
+ TOOL_DESCRIPTION_PREVIEW_LIMIT,
105
+ ),
91
106
  preview: inputPreview(event.toolUse.input),
92
107
  source: origin.source,
93
108
  user: origin.user,
@@ -103,9 +118,7 @@ export function createDaemonApprovalHandler(
103
118
  return;
104
119
  }
105
120
  if (behavior === "allow_always") {
106
- if (!config.tools.allowed.includes(name)) {
107
- config.update({ tools: { allowed: [...config.tools.allowed, name] } });
108
- }
121
+ allowToolForSession(event.agent, name);
109
122
  return;
110
123
  }
111
124
 
@@ -1,9 +1,11 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { stdin, stdout } from "node:process";
3
3
  import { BeforeToolCallEvent } from "@strands-agents/sdk";
4
- import type { Config } from "../core/config.ts";
5
-
6
- const INTERNAL_ALWAYS_ALLOWED = new Set(["strands_structured_output"]);
4
+ import {
5
+ INTERNAL_ALWAYS_ALLOWED,
6
+ allowToolForSession,
7
+ isToolSessionAllowed,
8
+ } from "../core/approvals/allowed-tools.ts";
7
9
  const INPUT_PREVIEW_LIMIT = 1_024;
8
10
 
9
11
  type ApprovalDecision = "allow" | "reject" | "always";
@@ -57,10 +59,9 @@ async function promptForApproval(
57
59
 
58
60
  type BeforeToolCallEventHandler = (event: BeforeToolCallEvent) => Promise<void>;
59
61
 
60
- export function createToolApprovalHandler(
61
- config: Config,
62
- options?: { yolo?: boolean },
63
- ): BeforeToolCallEventHandler {
62
+ export function createToolApprovalHandler(options?: {
63
+ yolo?: boolean;
64
+ }): BeforeToolCallEventHandler {
64
65
  return async function onBeforeToolCallEvent(event: BeforeToolCallEvent) {
65
66
  const name = event.toolUse.name;
66
67
  if (options?.yolo) {
@@ -68,12 +69,12 @@ export function createToolApprovalHandler(
68
69
  }
69
70
  if (
70
71
  INTERNAL_ALWAYS_ALLOWED.has(name) ||
71
- config.tools.allowed.includes(name)
72
+ isToolSessionAllowed(event.agent, name)
72
73
  ) {
73
74
  return;
74
75
  }
75
76
  if (!canPromptForApproval()) {
76
- event.cancel = `Tool "${name}" requires approval, but no interactive terminal is available. Add it to config.tools.allowed to always allow it.`;
77
+ event.cancel = `Tool "${name}" requires approval, but no interactive terminal is available.`;
77
78
  return;
78
79
  }
79
80
  const decision = await promptForApproval(event);
@@ -81,7 +82,7 @@ export function createToolApprovalHandler(
81
82
  return;
82
83
  }
83
84
  if (decision === "always") {
84
- config.update({ tools: { allowed: [...config.tools.allowed, name] } });
85
+ allowToolForSession(event.agent, name);
85
86
  return;
86
87
  }
87
88
  event.cancel = `Tool "${name}" was rejected by the user.`;