@pellux/goodvibes-agent 0.1.22 → 0.1.24

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.24 - 2026-05-31
6
+
7
+ - 2375df3 Bound web search tool policy
8
+
9
+ ## 0.1.23 - 2026-05-31
10
+
11
+ - 18d7381 Harden analyze and registry tool policy
12
+
5
13
  ## 0.1.22 - 2026-05-31
6
14
 
7
15
  - 07e4445 Mark control tool read-only in agent runtime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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",
@@ -0,0 +1,127 @@
1
+ import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
2
+
3
+ type AnalyzeToolArgs = {
4
+ readonly mode?: unknown;
5
+ readonly [key: string]: unknown;
6
+ };
7
+
8
+ type RegistryToolArgs = {
9
+ readonly mode?: unknown;
10
+ readonly path?: unknown;
11
+ readonly [key: string]: unknown;
12
+ };
13
+
14
+ const READ_ONLY_ANALYZE_TOOL_MODES = [
15
+ 'impact',
16
+ 'dependencies',
17
+ 'dead_code',
18
+ 'security',
19
+ 'coverage',
20
+ 'bundle',
21
+ 'preview',
22
+ 'diff',
23
+ 'surface',
24
+ 'breaking',
25
+ 'permissions',
26
+ 'env_audit',
27
+ 'test_find',
28
+ ] as const;
29
+
30
+ const READ_ONLY_REGISTRY_TOOL_MODES = ['search', 'recommend', 'dependencies', 'preview'] as const;
31
+
32
+ const READ_ONLY_ANALYZE_TOOL_MODE_SET = new Set<string>(READ_ONLY_ANALYZE_TOOL_MODES);
33
+ const READ_ONLY_REGISTRY_TOOL_MODE_SET = new Set<string>(READ_ONLY_REGISTRY_TOOL_MODES);
34
+
35
+ const ANALYZE_NETWORK_DENIAL = [
36
+ 'GoodVibes Agent only exposes local, static analyze modes from the main conversation.',
37
+ 'npm registry upgrade checks and hidden secondary LLM diff analysis are disabled here.',
38
+ 'Use explicit Agent CLI/slash commands or GoodVibes TUI delegation for package-upgrade and code-review workflows.',
39
+ ].join(' ');
40
+
41
+ const REGISTRY_CONTENT_DENIAL = [
42
+ 'GoodVibes Agent only exposes registry discovery, recommendation, dependency, and bounded preview from the main conversation.',
43
+ 'Full registry content materialization and arbitrary .goodvibes file reads are disabled here.',
44
+ 'Use explicit Agent CLI/slash commands for intentional local registry changes or deeper inspection.',
45
+ ].join(' ');
46
+
47
+ export const AGENT_READ_ONLY_ANALYZE_TOOL_MODES = READ_ONLY_ANALYZE_TOOL_MODES;
48
+ export const AGENT_READ_ONLY_REGISTRY_TOOL_MODES = READ_ONLY_REGISTRY_TOOL_MODES;
49
+ export const AGENT_ANALYZE_NETWORK_DENIAL_MESSAGE = ANALYZE_NETWORK_DENIAL;
50
+ export const AGENT_REGISTRY_CONTENT_DENIAL_MESSAGE = REGISTRY_CONTENT_DENIAL;
51
+
52
+ export function wrapAnalyzeToolForAgentPolicy(tool: Tool): void {
53
+ narrowAnalyzeToolDefinitionForAgentPolicy(tool);
54
+ const originalExecute = tool.execute.bind(tool);
55
+ tool.execute = async (args) => {
56
+ const denial = validateAnalyzeToolInvocationForAgentPolicy(args as AnalyzeToolArgs);
57
+ if (denial) return { success: false, error: denial };
58
+ return originalExecute(args);
59
+ };
60
+ }
61
+
62
+ export function wrapRegistryToolForAgentPolicy(tool: Tool): void {
63
+ narrowRegistryToolDefinitionForAgentPolicy(tool);
64
+ const originalExecute = tool.execute.bind(tool);
65
+ tool.execute = async (args) => {
66
+ const denial = validateRegistryToolInvocationForAgentPolicy(args as RegistryToolArgs);
67
+ if (denial) return { success: false, error: denial };
68
+ return originalExecute(args);
69
+ };
70
+ }
71
+
72
+ export function validateAnalyzeToolInvocationForAgentPolicy(args: AnalyzeToolArgs): string | null {
73
+ if (typeof args.mode === 'string' && !READ_ONLY_ANALYZE_TOOL_MODE_SET.has(args.mode)) return ANALYZE_NETWORK_DENIAL;
74
+ return null;
75
+ }
76
+
77
+ export function validateRegistryToolInvocationForAgentPolicy(args: RegistryToolArgs): string | null {
78
+ if (typeof args.mode === 'string' && !READ_ONLY_REGISTRY_TOOL_MODE_SET.has(args.mode)) return REGISTRY_CONTENT_DENIAL;
79
+ if (args.mode === 'preview' && typeof args.path === 'string' && !isAllowedAgentRegistryPreviewPath(args.path)) {
80
+ return REGISTRY_CONTENT_DENIAL;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function narrowAnalyzeToolDefinitionForAgentPolicy(tool: Tool): void {
86
+ tool.definition.description = [
87
+ 'Run local, static project analysis for GoodVibes Agent.',
88
+ 'npm registry upgrade checks and hidden secondary LLM diff analysis are disabled in the main conversation.',
89
+ 'Delegate package-upgrade and review workflows to GoodVibes TUI when they become build/fix/review work.',
90
+ ].join(' ');
91
+ tool.definition.sideEffects = ['read_fs'];
92
+
93
+ const properties = tool.definition.parameters.properties;
94
+ if (!isRecord(properties)) return;
95
+ const modeProperty = properties.mode;
96
+ if (isRecord(modeProperty)) {
97
+ modeProperty.enum = [...READ_ONLY_ANALYZE_TOOL_MODES];
98
+ modeProperty.description = 'Local static analysis mode allowed by GoodVibes Agent main-conversation policy.';
99
+ }
100
+
101
+ delete properties.packages;
102
+ }
103
+
104
+ function narrowRegistryToolDefinitionForAgentPolicy(tool: Tool): void {
105
+ tool.definition.description = [
106
+ 'Discover and preview GoodVibes Agent skills, agents, and tools.',
107
+ 'Full content materialization and arbitrary .goodvibes file reads are disabled in the main conversation.',
108
+ ].join(' ');
109
+ tool.definition.sideEffects = ['read_fs'];
110
+
111
+ const properties = tool.definition.parameters.properties;
112
+ if (!isRecord(properties)) return;
113
+ const modeProperty = properties.mode;
114
+ if (isRecord(modeProperty)) {
115
+ modeProperty.enum = [...READ_ONLY_REGISTRY_TOOL_MODES];
116
+ modeProperty.description = 'Bounded registry discovery modes allowed by GoodVibes Agent main-conversation policy.';
117
+ }
118
+ }
119
+
120
+ function isAllowedAgentRegistryPreviewPath(path: string): boolean {
121
+ const normalized = path.replace(/\\/g, '/');
122
+ return /(?:^|\/)\.goodvibes\/(?:skills|agents)\/.+(?:\.md|\/(?:SKILL|AGENT)\.md)$/i.test(normalized);
123
+ }
124
+
125
+ function isRecord(value: unknown): value is Record<string, unknown> {
126
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
127
+ }
@@ -0,0 +1,112 @@
1
+ import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
2
+
3
+ type WebSearchToolArgs = {
4
+ readonly maxResults?: unknown;
5
+ readonly verbosity?: unknown;
6
+ readonly safeSearch?: unknown;
7
+ readonly includeEvidence?: unknown;
8
+ readonly evidenceTopN?: unknown;
9
+ readonly evidenceExtract?: unknown;
10
+ readonly [key: string]: unknown;
11
+ };
12
+
13
+ const READ_ONLY_WEB_SEARCH_VERBOSITIES = ['urls_only', 'titles', 'snippets', 'evidence'] as const;
14
+ const READ_ONLY_WEB_SEARCH_EVIDENCE_EXTRACTS = [
15
+ 'text',
16
+ 'markdown',
17
+ 'readable',
18
+ 'code_blocks',
19
+ 'links',
20
+ 'metadata',
21
+ 'tables',
22
+ ] as const;
23
+
24
+ const READ_ONLY_WEB_SEARCH_VERBOSITY_SET = new Set<string>(READ_ONLY_WEB_SEARCH_VERBOSITIES);
25
+ const READ_ONLY_WEB_SEARCH_EVIDENCE_EXTRACT_SET = new Set<string>(READ_ONLY_WEB_SEARCH_EVIDENCE_EXTRACTS);
26
+
27
+ const MAX_WEB_SEARCH_RESULTS = 10;
28
+ const MAX_WEB_SEARCH_EVIDENCE_TOP_N = 3;
29
+
30
+ const WEB_SEARCH_POLICY_DENIAL = [
31
+ 'GoodVibes Agent only exposes bounded, read-only web research from the main conversation.',
32
+ 'Full-page/raw/summary evidence extraction, safe-search off, and high-fanout searches are disabled here.',
33
+ 'Use explicit Agent CLI/slash commands or an approval-backed workflow for broader external research.',
34
+ ].join(' ');
35
+
36
+ export const AGENT_READ_ONLY_WEB_SEARCH_VERBOSITIES = READ_ONLY_WEB_SEARCH_VERBOSITIES;
37
+ export const AGENT_READ_ONLY_WEB_SEARCH_EVIDENCE_EXTRACTS = READ_ONLY_WEB_SEARCH_EVIDENCE_EXTRACTS;
38
+ export const AGENT_MAX_WEB_SEARCH_RESULTS = MAX_WEB_SEARCH_RESULTS;
39
+ export const AGENT_MAX_WEB_SEARCH_EVIDENCE_TOP_N = MAX_WEB_SEARCH_EVIDENCE_TOP_N;
40
+ export const AGENT_WEB_SEARCH_POLICY_DENIAL_MESSAGE = WEB_SEARCH_POLICY_DENIAL;
41
+
42
+ export function wrapWebSearchToolForAgentPolicy(tool: Tool): void {
43
+ narrowWebSearchToolDefinitionForAgentPolicy(tool);
44
+ const originalExecute = tool.execute.bind(tool);
45
+ tool.execute = async (args) => {
46
+ const denial = validateWebSearchToolInvocationForAgentPolicy(args as WebSearchToolArgs);
47
+ if (denial) return { success: false, error: denial };
48
+ return originalExecute(normalizeWebSearchToolInvocationForAgentPolicy(args as WebSearchToolArgs) as Parameters<Tool['execute']>[0]);
49
+ };
50
+ }
51
+
52
+ export function validateWebSearchToolInvocationForAgentPolicy(args: WebSearchToolArgs): string | null {
53
+ if (typeof args.maxResults === 'number' && args.maxResults > MAX_WEB_SEARCH_RESULTS) return WEB_SEARCH_POLICY_DENIAL;
54
+ if (typeof args.evidenceTopN === 'number' && args.evidenceTopN > MAX_WEB_SEARCH_EVIDENCE_TOP_N) return WEB_SEARCH_POLICY_DENIAL;
55
+ if (args.safeSearch === 'off') return WEB_SEARCH_POLICY_DENIAL;
56
+ if (typeof args.verbosity === 'string' && !READ_ONLY_WEB_SEARCH_VERBOSITY_SET.has(args.verbosity)) {
57
+ return WEB_SEARCH_POLICY_DENIAL;
58
+ }
59
+ if (
60
+ typeof args.evidenceExtract === 'string'
61
+ && !READ_ONLY_WEB_SEARCH_EVIDENCE_EXTRACT_SET.has(args.evidenceExtract)
62
+ ) {
63
+ return WEB_SEARCH_POLICY_DENIAL;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ export function normalizeWebSearchToolInvocationForAgentPolicy(args: WebSearchToolArgs): WebSearchToolArgs {
69
+ return {
70
+ ...args,
71
+ safeSearch: typeof args.safeSearch === 'string' ? args.safeSearch : 'moderate',
72
+ };
73
+ }
74
+
75
+ function narrowWebSearchToolDefinitionForAgentPolicy(tool: Tool): void {
76
+ tool.definition.description = [
77
+ 'Run bounded, read-only web research for GoodVibes Agent.',
78
+ 'Full-page/raw/summary extraction, safe-search off, and high-fanout searches are disabled in the main conversation.',
79
+ ].join(' ');
80
+ tool.definition.sideEffects = ['network'];
81
+
82
+ const properties = tool.definition.parameters.properties;
83
+ if (!isRecord(properties)) return;
84
+
85
+ const verbosity = properties.verbosity;
86
+ if (isRecord(verbosity)) {
87
+ verbosity.enum = [...READ_ONLY_WEB_SEARCH_VERBOSITIES];
88
+ verbosity.description = 'Bounded result verbosity allowed by GoodVibes Agent main-conversation policy.';
89
+ }
90
+
91
+ const evidenceExtract = properties.evidenceExtract;
92
+ if (isRecord(evidenceExtract)) {
93
+ evidenceExtract.enum = [...READ_ONLY_WEB_SEARCH_EVIDENCE_EXTRACTS];
94
+ evidenceExtract.description = 'Bounded evidence extraction modes allowed by GoodVibes Agent main-conversation policy.';
95
+ }
96
+
97
+ const maxResults = properties.maxResults;
98
+ if (isRecord(maxResults)) {
99
+ maxResults.maximum = MAX_WEB_SEARCH_RESULTS;
100
+ maxResults.description = 'Maximum ranked results returned by GoodVibes Agent main-conversation web research.';
101
+ }
102
+
103
+ const evidenceTopN = properties.evidenceTopN;
104
+ if (isRecord(evidenceTopN)) {
105
+ evidenceTopN.maximum = MAX_WEB_SEARCH_EVIDENCE_TOP_N;
106
+ evidenceTopN.description = 'Maximum top results fetched for evidence by GoodVibes Agent main-conversation web research.';
107
+ }
108
+ }
109
+
110
+ function isRecord(value: unknown): value is Record<string, unknown> {
111
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
112
+ }
@@ -1,5 +1,10 @@
1
1
  import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
2
2
  import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
3
+ import {
4
+ wrapAnalyzeToolForAgentPolicy,
5
+ wrapRegistryToolForAgentPolicy,
6
+ } from './agent-analysis-registry-policy.ts';
7
+ import { wrapWebSearchToolForAgentPolicy } from './agent-web-search-policy.ts';
3
8
 
4
9
  type AgentToolArgs = {
5
10
  readonly mode?: unknown;
@@ -222,6 +227,12 @@ export function installAgentToolPolicyGuard(registry: ToolRegistry, options: Age
222
227
  wrapBlockedSettingsToolForAgentPolicy(tool);
223
228
  } else if (tool.definition.name === 'inspect') {
224
229
  wrapInspectToolForAgentPolicy(tool);
230
+ } else if (tool.definition.name === 'analyze') {
231
+ wrapAnalyzeToolForAgentPolicy(tool);
232
+ } else if (tool.definition.name === 'registry') {
233
+ wrapRegistryToolForAgentPolicy(tool);
234
+ } else if (tool.definition.name === 'web_search') {
235
+ wrapWebSearchToolForAgentPolicy(tool);
225
236
  } else if (tool.definition.name === 'control') {
226
237
  wrapModeRestrictedToolForAgentPolicy(tool, {
227
238
  allowedModes: READ_ONLY_CONTROL_TOOL_MODES,
@@ -534,6 +545,28 @@ export const AGENT_INSPECT_WRITE_DENIAL_MESSAGE = INSPECT_WRITE_DENIAL;
534
545
  export const AGENT_DURABLE_WORKFLOW_MUTATION_DENIAL_MESSAGE = DURABLE_WORKFLOW_MUTATION_DENIAL;
535
546
  export const AGENT_CONTROL_MUTATION_DENIAL_MESSAGE = CONTROL_MUTATION_DENIAL;
536
547
 
548
+ export {
549
+ AGENT_ANALYZE_NETWORK_DENIAL_MESSAGE,
550
+ AGENT_READ_ONLY_ANALYZE_TOOL_MODES,
551
+ AGENT_READ_ONLY_REGISTRY_TOOL_MODES,
552
+ AGENT_REGISTRY_CONTENT_DENIAL_MESSAGE,
553
+ validateAnalyzeToolInvocationForAgentPolicy,
554
+ validateRegistryToolInvocationForAgentPolicy,
555
+ wrapAnalyzeToolForAgentPolicy,
556
+ wrapRegistryToolForAgentPolicy,
557
+ } from './agent-analysis-registry-policy.ts';
558
+
559
+ export {
560
+ AGENT_MAX_WEB_SEARCH_EVIDENCE_TOP_N,
561
+ AGENT_MAX_WEB_SEARCH_RESULTS,
562
+ AGENT_READ_ONLY_WEB_SEARCH_EVIDENCE_EXTRACTS,
563
+ AGENT_READ_ONLY_WEB_SEARCH_VERBOSITIES,
564
+ AGENT_WEB_SEARCH_POLICY_DENIAL_MESSAGE,
565
+ normalizeWebSearchToolInvocationForAgentPolicy,
566
+ validateWebSearchToolInvocationForAgentPolicy,
567
+ wrapWebSearchToolForAgentPolicy,
568
+ } from './agent-web-search-policy.ts';
569
+
537
570
  function isRecord(value: unknown): value is Record<string, unknown> {
538
571
  return typeof value === 'object' && value !== null && !Array.isArray(value);
539
572
  }
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.22';
9
+ let _version = '0.1.24';
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 {