@pellux/goodvibes-agent 0.1.15 → 0.1.17

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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.17 - 2026-05-31
6
+
7
+ - f148186 Restrict fetch side effects in agent runtime
8
+
9
+ ## 0.1.16 - 2026-05-31
10
+
11
+ - bea1197 Restrict MCP tool mutations in agent runtime
12
+
5
13
  ## 0.1.15 - 2026-05-31
6
14
 
7
15
  - 67de700 Restrict remote and channel tools in agent runtime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "private": false,
5
5
  "description": "Near-fork GoodVibes operator assistant with the GoodVibes TUI shell, renderer, input, fullscreen workspace, and daemon-connected Agent product brain.",
6
6
  "type": "module",
@@ -26,6 +26,14 @@ type ModeToolArgs = {
26
26
  readonly [key: string]: unknown;
27
27
  };
28
28
 
29
+ type FetchToolArgs = {
30
+ readonly urls?: unknown;
31
+ readonly parallel?: unknown;
32
+ readonly sanitize_mode?: unknown;
33
+ readonly trusted_hosts?: unknown;
34
+ readonly [key: string]: unknown;
35
+ };
36
+
29
37
  type AgentToolPolicyGuardOptions = {
30
38
  readonly getLastUserMessage?: () => string | null;
31
39
  };
@@ -50,8 +58,12 @@ const BLOCKED_MAIN_CONVERSATION_TOOL_NAME_SET = new Set<string>(BLOCKED_MAIN_CON
50
58
 
51
59
  const READ_ONLY_REMOTE_TOOL_MODES = ['pools', 'contracts', 'artifacts', 'review'] as const;
52
60
  const READ_ONLY_CHANNEL_TOOL_MODES = ['accounts', 'directory', 'resolve_target', 'capabilities', 'tools', 'agent_tools', 'actions'] as const;
61
+ const READ_ONLY_MCP_TOOL_MODES = ['servers', 'tools', 'schema', 'resources', 'security', 'auth'] as const;
62
+ const READ_ONLY_FETCH_METHODS = ['GET', 'HEAD', 'OPTIONS'] as const;
53
63
  const READ_ONLY_REMOTE_TOOL_MODE_SET = new Set<string>(READ_ONLY_REMOTE_TOOL_MODES);
54
64
  const READ_ONLY_CHANNEL_TOOL_MODE_SET = new Set<string>(READ_ONLY_CHANNEL_TOOL_MODES);
65
+ const READ_ONLY_MCP_TOOL_MODE_SET = new Set<string>(READ_ONLY_MCP_TOOL_MODES);
66
+ const READ_ONLY_FETCH_METHOD_SET = new Set<string>(READ_ONLY_FETCH_METHODS);
55
67
 
56
68
  const LOCAL_AGENT_DENIAL = [
57
69
  'GoodVibes Agent does not spawn local Engineer/Reviewer/Tester/Verifier roots or run local WRFC chains.',
@@ -83,6 +95,18 @@ const CHANNEL_ACTION_DENIAL = [
83
95
  'External channel side effects require an explicit Agent approval flow before they can run.',
84
96
  ].join(' ');
85
97
 
98
+ const MCP_SECURITY_MUTATION_DENIAL = [
99
+ 'GoodVibes Agent only inspects MCP servers, tools, schemas, resources, security, and auth state from the main conversation.',
100
+ 'MCP quarantine approval, trust changes, and role changes are disabled here.',
101
+ 'MCP security mutations require an explicit Agent approval flow before they can run.',
102
+ ].join(' ');
103
+
104
+ const FETCH_NETWORK_MUTATION_DENIAL = [
105
+ 'GoodVibes Agent only performs serial, unauthenticated, read-only HTTP fetches from the main conversation.',
106
+ 'Non-read methods, request bodies, custom auth/header/service credentials, trust overrides, raw unsanitized responses, and parallel fetch batches are disabled here.',
107
+ 'Network writes or credentialed external calls require an explicit Agent approval flow before they can run.',
108
+ ].join(' ');
109
+
86
110
  export function installAgentToolPolicyGuard(registry: ToolRegistry, options: AgentToolPolicyGuardOptions = {}): void {
87
111
  const agentTool = registry.list().find((tool) => tool.definition.name === 'agent');
88
112
  if (!agentTool) throw new Error('Agent tool policy guard could not find the agent tool.');
@@ -102,6 +126,18 @@ export function installAgentToolPolicyGuard(registry: ToolRegistry, options: Age
102
126
  });
103
127
  } else if (tool.definition.name === 'channel') {
104
128
  wrapChannelToolForAgentPolicy(tool);
129
+ } else if (tool.definition.name === 'mcp') {
130
+ wrapModeRestrictedToolForAgentPolicy(tool, {
131
+ allowedModes: READ_ONLY_MCP_TOOL_MODES,
132
+ modeSet: READ_ONLY_MCP_TOOL_MODE_SET,
133
+ description: [
134
+ 'Read-only MCP inspection for GoodVibes Agent.',
135
+ 'Quarantine approval, trust mutation, and role mutation are disabled in the main conversation.',
136
+ ].join(' '),
137
+ denial: MCP_SECURITY_MUTATION_DENIAL,
138
+ });
139
+ } else if (tool.definition.name === 'fetch') {
140
+ wrapFetchToolForAgentPolicy(tool);
105
141
  } else if (BLOCKED_MAIN_CONVERSATION_TOOL_NAME_SET.has(tool.definition.name)) {
106
142
  wrapBlockedMainConversationToolForAgentPolicy(tool);
107
143
  }
@@ -147,6 +183,16 @@ export function wrapExecToolForAgentPolicy(tool: Tool): void {
147
183
  };
148
184
  }
149
185
 
186
+ export function wrapFetchToolForAgentPolicy(tool: Tool): void {
187
+ narrowFetchToolDefinitionForAgentPolicy(tool);
188
+ const originalExecute = tool.execute.bind(tool);
189
+ tool.execute = async (args) => {
190
+ const denial = validateFetchToolInvocationForAgentPolicy(args as FetchToolArgs);
191
+ if (denial) return { success: false, error: denial };
192
+ return originalExecute(normalizeFetchToolInvocationForAgentPolicy(args as FetchToolArgs) as Parameters<Tool['execute']>[0]);
193
+ };
194
+ }
195
+
150
196
  export function validateExecToolInvocationForAgentPolicy(args: ExecToolArgs): string | null {
151
197
  if (args.parallel === true) return BACKGROUND_EXEC_DENIAL;
152
198
  if (Array.isArray(args.file_ops) && args.file_ops.length > 0) return BACKGROUND_EXEC_DENIAL;
@@ -169,6 +215,31 @@ export function validateExecToolInvocationForAgentPolicy(args: ExecToolArgs): st
169
215
  return null;
170
216
  }
171
217
 
218
+ export function validateFetchToolInvocationForAgentPolicy(args: FetchToolArgs): string | null {
219
+ if (args.parallel === true) return FETCH_NETWORK_MUTATION_DENIAL;
220
+ if (args.sanitize_mode === 'none') return FETCH_NETWORK_MUTATION_DENIAL;
221
+ if (isPresent(args.trusted_hosts)) return FETCH_NETWORK_MUTATION_DENIAL;
222
+ if (!Array.isArray(args.urls)) return null;
223
+
224
+ for (const urlArgs of args.urls) {
225
+ if (!isRecord(urlArgs)) continue;
226
+ const method = typeof urlArgs.method === 'string' ? urlArgs.method.toUpperCase() : 'GET';
227
+ if (!READ_ONLY_FETCH_METHOD_SET.has(method)) return FETCH_NETWORK_MUTATION_DENIAL;
228
+ if (isPresent(urlArgs.body) || isPresent(urlArgs.body_base64) || isPresent(urlArgs.body_type) || isPresent(urlArgs.body_data)) {
229
+ return FETCH_NETWORK_MUTATION_DENIAL;
230
+ }
231
+ if (isPresent(urlArgs.headers) || isPresent(urlArgs.auth) || isPresent(urlArgs.service) || isPresent(urlArgs.retry_on_auth)) {
232
+ return FETCH_NETWORK_MUTATION_DENIAL;
233
+ }
234
+ }
235
+
236
+ return null;
237
+ }
238
+
239
+ export function normalizeFetchToolInvocationForAgentPolicy(args: FetchToolArgs): FetchToolArgs {
240
+ return { ...args, parallel: false };
241
+ }
242
+
172
243
  type ModeRestrictedToolPolicy = {
173
244
  readonly allowedModes: readonly string[];
174
245
  readonly modeSet: ReadonlySet<string>;
@@ -221,13 +292,25 @@ export const AGENT_MAIN_CONVERSATION_TOOL_DENIAL_MESSAGE = LOCAL_CODING_TOOL_DEN
221
292
  export const AGENT_EXEC_BACKGROUND_DENIAL_MESSAGE = BACKGROUND_EXEC_DENIAL;
222
293
  export const AGENT_READ_ONLY_REMOTE_TOOL_MODES = READ_ONLY_REMOTE_TOOL_MODES;
223
294
  export const AGENT_READ_ONLY_CHANNEL_TOOL_MODES = READ_ONLY_CHANNEL_TOOL_MODES;
295
+ export const AGENT_READ_ONLY_MCP_TOOL_MODES = READ_ONLY_MCP_TOOL_MODES;
296
+ export const AGENT_READ_ONLY_FETCH_METHODS = READ_ONLY_FETCH_METHODS;
224
297
  export const AGENT_REMOTE_MUTATION_DENIAL_MESSAGE = REMOTE_MUTATION_DENIAL;
225
298
  export const AGENT_CHANNEL_ACTION_DENIAL_MESSAGE = CHANNEL_ACTION_DENIAL;
299
+ export const AGENT_MCP_SECURITY_MUTATION_DENIAL_MESSAGE = MCP_SECURITY_MUTATION_DENIAL;
300
+ export const AGENT_FETCH_NETWORK_MUTATION_DENIAL_MESSAGE = FETCH_NETWORK_MUTATION_DENIAL;
226
301
 
227
302
  function isRecord(value: unknown): value is Record<string, unknown> {
228
303
  return typeof value === 'object' && value !== null && !Array.isArray(value);
229
304
  }
230
305
 
306
+ function isPresent(value: unknown): boolean {
307
+ if (value === undefined || value === null) return false;
308
+ if (typeof value === 'string') return value.length > 0;
309
+ if (Array.isArray(value)) return value.length > 0;
310
+ if (isRecord(value)) return Object.keys(value).length > 0;
311
+ return true;
312
+ }
313
+
231
314
  function narrowAgentToolDefinitionForAgentPolicy(tool: Tool): void {
232
315
  tool.definition.description = [
233
316
  'Read-only local Agent inspection for GoodVibes Agent.',
@@ -273,6 +356,47 @@ function narrowExecToolDefinitionForAgentPolicy(tool: Tool): void {
273
356
  }
274
357
  }
275
358
 
359
+ function narrowFetchToolDefinitionForAgentPolicy(tool: Tool): void {
360
+ tool.definition.description = [
361
+ 'Fetch public URLs for GoodVibes Agent with serial, read-only HTTP requests.',
362
+ 'Only GET, HEAD, and OPTIONS are available in the main conversation.',
363
+ 'Credentialed requests, request bodies, trust overrides, raw unsanitized responses, and parallel batches are disabled by Agent policy.',
364
+ ].join(' ');
365
+
366
+ const properties = tool.definition.parameters.properties;
367
+ if (!isRecord(properties)) return;
368
+ delete properties.parallel;
369
+ delete properties.trusted_hosts;
370
+
371
+ const sanitizeModeProperty = properties.sanitize_mode;
372
+ if (isRecord(sanitizeModeProperty)) {
373
+ sanitizeModeProperty.enum = ['safe-text', 'strict'];
374
+ sanitizeModeProperty.description = 'Response sanitization mode. Raw unsanitized responses are disabled in GoodVibes Agent.';
375
+ }
376
+
377
+ const urlsProperty = properties.urls;
378
+ if (!isRecord(urlsProperty)) return;
379
+ const itemSchema = urlsProperty.items;
380
+ if (!isRecord(itemSchema)) return;
381
+ const urlProperties = itemSchema.properties;
382
+ if (!isRecord(urlProperties)) return;
383
+
384
+ const methodProperty = urlProperties.method;
385
+ if (isRecord(methodProperty)) {
386
+ methodProperty.enum = [...READ_ONLY_FETCH_METHODS];
387
+ methodProperty.description = 'Read-only HTTP method. GoodVibes Agent disables POST, PUT, PATCH, and DELETE in the main conversation.';
388
+ }
389
+
390
+ delete urlProperties.headers;
391
+ delete urlProperties.body;
392
+ delete urlProperties.body_base64;
393
+ delete urlProperties.body_type;
394
+ delete urlProperties.body_data;
395
+ delete urlProperties.retry_on_auth;
396
+ delete urlProperties.service;
397
+ delete urlProperties.auth;
398
+ }
399
+
276
400
  function narrowModeToolDefinitionForAgentPolicy(tool: Tool, allowedModes: readonly string[], description: string): void {
277
401
  tool.definition.description = description;
278
402
 
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.1.15';
9
+ let _version = '0.1.17';
10
10
  let _sdkVersion = '0.33.35';
11
11
  try {
12
12
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {