@pellux/goodvibes-agent 0.1.17 → 0.1.18

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,10 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.18 - 2026-05-31
6
+
7
+ - 1fd7729 Restrict state tool mutations in agent runtime
8
+
5
9
  ## 0.1.17 - 2026-05-31
6
10
 
7
11
  - f148186 Restrict fetch side effects in agent runtime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
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",
@@ -34,6 +34,26 @@ type FetchToolArgs = {
34
34
  readonly [key: string]: unknown;
35
35
  };
36
36
 
37
+ type StateToolArgs = {
38
+ readonly mode?: unknown;
39
+ readonly memoryAction?: unknown;
40
+ readonly hookAction?: unknown;
41
+ readonly modeAction?: unknown;
42
+ readonly analyticsAction?: unknown;
43
+ readonly values?: unknown;
44
+ readonly clearKeys?: unknown;
45
+ readonly memoryValue?: unknown;
46
+ readonly hookDefinition?: unknown;
47
+ readonly modeName?: unknown;
48
+ readonly analyticsTool?: unknown;
49
+ readonly analyticsArgs?: unknown;
50
+ readonly analyticsResult?: unknown;
51
+ readonly analyticsDuration?: unknown;
52
+ readonly analyticsTokens?: unknown;
53
+ readonly analyticsFormat?: unknown;
54
+ readonly [key: string]: unknown;
55
+ };
56
+
37
57
  type AgentToolPolicyGuardOptions = {
38
58
  readonly getLastUserMessage?: () => string | null;
39
59
  };
@@ -60,10 +80,20 @@ const READ_ONLY_REMOTE_TOOL_MODES = ['pools', 'contracts', 'artifacts', 'review'
60
80
  const READ_ONLY_CHANNEL_TOOL_MODES = ['accounts', 'directory', 'resolve_target', 'capabilities', 'tools', 'agent_tools', 'actions'] as const;
61
81
  const READ_ONLY_MCP_TOOL_MODES = ['servers', 'tools', 'schema', 'resources', 'security', 'auth'] as const;
62
82
  const READ_ONLY_FETCH_METHODS = ['GET', 'HEAD', 'OPTIONS'] as const;
83
+ const READ_ONLY_STATE_TOOL_MODES = ['get', 'list', 'budget', 'context', 'memory', 'telemetry', 'hooks', 'mode', 'analytics'] as const;
84
+ const READ_ONLY_STATE_MEMORY_ACTIONS = ['list', 'get'] as const;
85
+ const READ_ONLY_STATE_HOOK_ACTIONS = ['list'] as const;
86
+ const READ_ONLY_STATE_MODE_ACTIONS = ['get', 'list'] as const;
87
+ const READ_ONLY_STATE_ANALYTICS_ACTIONS = ['summary', 'query', 'dashboard'] as const;
63
88
  const READ_ONLY_REMOTE_TOOL_MODE_SET = new Set<string>(READ_ONLY_REMOTE_TOOL_MODES);
64
89
  const READ_ONLY_CHANNEL_TOOL_MODE_SET = new Set<string>(READ_ONLY_CHANNEL_TOOL_MODES);
65
90
  const READ_ONLY_MCP_TOOL_MODE_SET = new Set<string>(READ_ONLY_MCP_TOOL_MODES);
66
91
  const READ_ONLY_FETCH_METHOD_SET = new Set<string>(READ_ONLY_FETCH_METHODS);
92
+ const READ_ONLY_STATE_TOOL_MODE_SET = new Set<string>(READ_ONLY_STATE_TOOL_MODES);
93
+ const READ_ONLY_STATE_MEMORY_ACTION_SET = new Set<string>(READ_ONLY_STATE_MEMORY_ACTIONS);
94
+ const READ_ONLY_STATE_HOOK_ACTION_SET = new Set<string>(READ_ONLY_STATE_HOOK_ACTIONS);
95
+ const READ_ONLY_STATE_MODE_ACTION_SET = new Set<string>(READ_ONLY_STATE_MODE_ACTIONS);
96
+ const READ_ONLY_STATE_ANALYTICS_ACTION_SET = new Set<string>(READ_ONLY_STATE_ANALYTICS_ACTIONS);
67
97
 
68
98
  const LOCAL_AGENT_DENIAL = [
69
99
  'GoodVibes Agent does not spawn local Engineer/Reviewer/Tester/Verifier roots or run local WRFC chains.',
@@ -107,6 +137,12 @@ const FETCH_NETWORK_MUTATION_DENIAL = [
107
137
  'Network writes or credentialed external calls require an explicit Agent approval flow before they can run.',
108
138
  ].join(' ');
109
139
 
140
+ const STATE_MUTATION_DENIAL = [
141
+ 'GoodVibes Agent only inspects copied runtime state from the main conversation.',
142
+ 'Arbitrary state set/clear, copied memory writes, hook mutation, output-mode mutation, and analytics writes are disabled here.',
143
+ 'Use Agent-owned memory, skills, personas, routines, and explicit CLI/slash commands for intentional local state changes.',
144
+ ].join(' ');
145
+
110
146
  export function installAgentToolPolicyGuard(registry: ToolRegistry, options: AgentToolPolicyGuardOptions = {}): void {
111
147
  const agentTool = registry.list().find((tool) => tool.definition.name === 'agent');
112
148
  if (!agentTool) throw new Error('Agent tool policy guard could not find the agent tool.');
@@ -138,6 +174,8 @@ export function installAgentToolPolicyGuard(registry: ToolRegistry, options: Age
138
174
  });
139
175
  } else if (tool.definition.name === 'fetch') {
140
176
  wrapFetchToolForAgentPolicy(tool);
177
+ } else if (tool.definition.name === 'state') {
178
+ wrapStateToolForAgentPolicy(tool);
141
179
  } else if (BLOCKED_MAIN_CONVERSATION_TOOL_NAME_SET.has(tool.definition.name)) {
142
180
  wrapBlockedMainConversationToolForAgentPolicy(tool);
143
181
  }
@@ -193,6 +231,16 @@ export function wrapFetchToolForAgentPolicy(tool: Tool): void {
193
231
  };
194
232
  }
195
233
 
234
+ export function wrapStateToolForAgentPolicy(tool: Tool): void {
235
+ narrowStateToolDefinitionForAgentPolicy(tool);
236
+ const originalExecute = tool.execute.bind(tool);
237
+ tool.execute = async (args) => {
238
+ const denial = validateStateToolInvocationForAgentPolicy(args as StateToolArgs);
239
+ if (denial) return { success: false, error: denial };
240
+ return originalExecute(args);
241
+ };
242
+ }
243
+
196
244
  export function validateExecToolInvocationForAgentPolicy(args: ExecToolArgs): string | null {
197
245
  if (args.parallel === true) return BACKGROUND_EXEC_DENIAL;
198
246
  if (Array.isArray(args.file_ops) && args.file_ops.length > 0) return BACKGROUND_EXEC_DENIAL;
@@ -240,6 +288,43 @@ export function normalizeFetchToolInvocationForAgentPolicy(args: FetchToolArgs):
240
288
  return { ...args, parallel: false };
241
289
  }
242
290
 
291
+ export function validateStateToolInvocationForAgentPolicy(args: StateToolArgs): string | null {
292
+ if (isPresent(args.values) || isPresent(args.clearKeys)) return STATE_MUTATION_DENIAL;
293
+ if (typeof args.mode === 'string' && !READ_ONLY_STATE_TOOL_MODE_SET.has(args.mode)) return STATE_MUTATION_DENIAL;
294
+
295
+ if (args.mode === 'memory') {
296
+ const action = typeof args.memoryAction === 'string' ? args.memoryAction : 'list';
297
+ if (!READ_ONLY_STATE_MEMORY_ACTION_SET.has(action) || isPresent(args.memoryValue)) return STATE_MUTATION_DENIAL;
298
+ }
299
+
300
+ if (args.mode === 'hooks') {
301
+ const action = typeof args.hookAction === 'string' ? args.hookAction : 'list';
302
+ if (!READ_ONLY_STATE_HOOK_ACTION_SET.has(action) || isPresent(args.hookDefinition)) return STATE_MUTATION_DENIAL;
303
+ }
304
+
305
+ if (args.mode === 'mode') {
306
+ const action = typeof args.modeAction === 'string' ? args.modeAction : 'get';
307
+ if (!READ_ONLY_STATE_MODE_ACTION_SET.has(action) || isPresent(args.modeName)) return STATE_MUTATION_DENIAL;
308
+ }
309
+
310
+ if (args.mode === 'analytics') {
311
+ const action = typeof args.analyticsAction === 'string' ? args.analyticsAction : 'summary';
312
+ if (!READ_ONLY_STATE_ANALYTICS_ACTION_SET.has(action)) return STATE_MUTATION_DENIAL;
313
+ if (
314
+ isPresent(args.analyticsTool)
315
+ || isPresent(args.analyticsArgs)
316
+ || isPresent(args.analyticsResult)
317
+ || isPresent(args.analyticsDuration)
318
+ || isPresent(args.analyticsTokens)
319
+ || isPresent(args.analyticsFormat)
320
+ ) {
321
+ return STATE_MUTATION_DENIAL;
322
+ }
323
+ }
324
+
325
+ return null;
326
+ }
327
+
243
328
  type ModeRestrictedToolPolicy = {
244
329
  readonly allowedModes: readonly string[];
245
330
  readonly modeSet: ReadonlySet<string>;
@@ -294,10 +379,16 @@ export const AGENT_READ_ONLY_REMOTE_TOOL_MODES = READ_ONLY_REMOTE_TOOL_MODES;
294
379
  export const AGENT_READ_ONLY_CHANNEL_TOOL_MODES = READ_ONLY_CHANNEL_TOOL_MODES;
295
380
  export const AGENT_READ_ONLY_MCP_TOOL_MODES = READ_ONLY_MCP_TOOL_MODES;
296
381
  export const AGENT_READ_ONLY_FETCH_METHODS = READ_ONLY_FETCH_METHODS;
382
+ export const AGENT_READ_ONLY_STATE_TOOL_MODES = READ_ONLY_STATE_TOOL_MODES;
383
+ export const AGENT_READ_ONLY_STATE_MEMORY_ACTIONS = READ_ONLY_STATE_MEMORY_ACTIONS;
384
+ export const AGENT_READ_ONLY_STATE_HOOK_ACTIONS = READ_ONLY_STATE_HOOK_ACTIONS;
385
+ export const AGENT_READ_ONLY_STATE_MODE_ACTIONS = READ_ONLY_STATE_MODE_ACTIONS;
386
+ export const AGENT_READ_ONLY_STATE_ANALYTICS_ACTIONS = READ_ONLY_STATE_ANALYTICS_ACTIONS;
297
387
  export const AGENT_REMOTE_MUTATION_DENIAL_MESSAGE = REMOTE_MUTATION_DENIAL;
298
388
  export const AGENT_CHANNEL_ACTION_DENIAL_MESSAGE = CHANNEL_ACTION_DENIAL;
299
389
  export const AGENT_MCP_SECURITY_MUTATION_DENIAL_MESSAGE = MCP_SECURITY_MUTATION_DENIAL;
300
390
  export const AGENT_FETCH_NETWORK_MUTATION_DENIAL_MESSAGE = FETCH_NETWORK_MUTATION_DENIAL;
391
+ export const AGENT_STATE_MUTATION_DENIAL_MESSAGE = STATE_MUTATION_DENIAL;
301
392
 
302
393
  function isRecord(value: unknown): value is Record<string, unknown> {
303
394
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -397,6 +488,40 @@ function narrowFetchToolDefinitionForAgentPolicy(tool: Tool): void {
397
488
  delete urlProperties.auth;
398
489
  }
399
490
 
491
+ function narrowStateToolDefinitionForAgentPolicy(tool: Tool): void {
492
+ tool.definition.description = [
493
+ 'Inspect copied runtime state for GoodVibes Agent.',
494
+ 'State mutation, copied memory writes, hook changes, output-mode changes, and analytics writes are disabled in the main conversation.',
495
+ 'Use Agent-owned commands for intentional memory, skill, persona, and routine changes.',
496
+ ].join(' ');
497
+
498
+ const properties = tool.definition.parameters.properties;
499
+ if (!isRecord(properties)) return;
500
+ const modeProperty = properties.mode;
501
+ if (isRecord(modeProperty)) {
502
+ modeProperty.enum = [...READ_ONLY_STATE_TOOL_MODES];
503
+ modeProperty.description = 'Read-only copied runtime state mode. set and clear are disabled in GoodVibes Agent.';
504
+ }
505
+
506
+ delete properties.values;
507
+ delete properties.clearKeys;
508
+ delete properties.memoryValue;
509
+ delete properties.hookDefinition;
510
+ delete properties.hookName;
511
+ delete properties.modeName;
512
+ delete properties.analyticsTool;
513
+ delete properties.analyticsArgs;
514
+ delete properties.analyticsResult;
515
+ delete properties.analyticsDuration;
516
+ delete properties.analyticsTokens;
517
+ delete properties.analyticsFormat;
518
+
519
+ narrowStringEnumProperty(properties, 'memoryAction', READ_ONLY_STATE_MEMORY_ACTIONS, 'Read-only copied memory actions allowed by GoodVibes Agent.');
520
+ narrowStringEnumProperty(properties, 'hookAction', READ_ONLY_STATE_HOOK_ACTIONS, 'Read-only hook action allowed by GoodVibes Agent.');
521
+ narrowStringEnumProperty(properties, 'modeAction', READ_ONLY_STATE_MODE_ACTIONS, 'Read-only mode actions allowed by GoodVibes Agent.');
522
+ narrowStringEnumProperty(properties, 'analyticsAction', READ_ONLY_STATE_ANALYTICS_ACTIONS, 'Read-only analytics actions allowed by GoodVibes Agent.');
523
+ }
524
+
400
525
  function narrowModeToolDefinitionForAgentPolicy(tool: Tool, allowedModes: readonly string[], description: string): void {
401
526
  tool.definition.description = description;
402
527
 
@@ -417,6 +542,18 @@ function narrowModeToolDefinitionForAgentPolicy(tool: Tool, allowedModes: readon
417
542
  }
418
543
  }
419
544
 
545
+ function narrowStringEnumProperty(
546
+ properties: Record<string, unknown>,
547
+ key: string,
548
+ values: readonly string[],
549
+ description: string,
550
+ ): void {
551
+ const property = properties[key];
552
+ if (!isRecord(property)) return;
553
+ property.enum = [...values];
554
+ property.description = description;
555
+ }
556
+
420
557
  // Compatibility exports for copied TUI tests/imports during the near-fork phase.
421
558
  export const installWrfcAgentToolGuard = installAgentToolPolicyGuard;
422
559
  export const wrapWrfcAgentTool = wrapAgentToolForAgentPolicy;
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.17';
9
+ let _version = '0.1.18';
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 {