@pellux/goodvibes-agent 0.1.23 → 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,10 @@
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
+
5
9
  ## 0.1.23 - 2026-05-31
6
10
 
7
11
  - 18d7381 Harden analyze and registry tool policy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.23",
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,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
+ }
@@ -4,6 +4,7 @@ import {
4
4
  wrapAnalyzeToolForAgentPolicy,
5
5
  wrapRegistryToolForAgentPolicy,
6
6
  } from './agent-analysis-registry-policy.ts';
7
+ import { wrapWebSearchToolForAgentPolicy } from './agent-web-search-policy.ts';
7
8
 
8
9
  type AgentToolArgs = {
9
10
  readonly mode?: unknown;
@@ -230,6 +231,8 @@ export function installAgentToolPolicyGuard(registry: ToolRegistry, options: Age
230
231
  wrapAnalyzeToolForAgentPolicy(tool);
231
232
  } else if (tool.definition.name === 'registry') {
232
233
  wrapRegistryToolForAgentPolicy(tool);
234
+ } else if (tool.definition.name === 'web_search') {
235
+ wrapWebSearchToolForAgentPolicy(tool);
233
236
  } else if (tool.definition.name === 'control') {
234
237
  wrapModeRestrictedToolForAgentPolicy(tool, {
235
238
  allowedModes: READ_ONLY_CONTROL_TOOL_MODES,
@@ -553,6 +556,17 @@ export {
553
556
  wrapRegistryToolForAgentPolicy,
554
557
  } from './agent-analysis-registry-policy.ts';
555
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
+
556
570
  function isRecord(value: unknown): value is Record<string, unknown> {
557
571
  return typeof value === 'object' && value !== null && !Array.isArray(value);
558
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.23';
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 {