@pellux/goodvibes-agent 0.1.16 → 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 +4 -0
- package/package.json +1 -1
- package/src/tools/wrfc-agent-guard.ts +104 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
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
|
};
|
|
@@ -51,9 +59,11 @@ const BLOCKED_MAIN_CONVERSATION_TOOL_NAME_SET = new Set<string>(BLOCKED_MAIN_CON
|
|
|
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;
|
|
53
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;
|
|
54
63
|
const READ_ONLY_REMOTE_TOOL_MODE_SET = new Set<string>(READ_ONLY_REMOTE_TOOL_MODES);
|
|
55
64
|
const READ_ONLY_CHANNEL_TOOL_MODE_SET = new Set<string>(READ_ONLY_CHANNEL_TOOL_MODES);
|
|
56
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);
|
|
57
67
|
|
|
58
68
|
const LOCAL_AGENT_DENIAL = [
|
|
59
69
|
'GoodVibes Agent does not spawn local Engineer/Reviewer/Tester/Verifier roots or run local WRFC chains.',
|
|
@@ -91,6 +101,12 @@ const MCP_SECURITY_MUTATION_DENIAL = [
|
|
|
91
101
|
'MCP security mutations require an explicit Agent approval flow before they can run.',
|
|
92
102
|
].join(' ');
|
|
93
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
|
+
|
|
94
110
|
export function installAgentToolPolicyGuard(registry: ToolRegistry, options: AgentToolPolicyGuardOptions = {}): void {
|
|
95
111
|
const agentTool = registry.list().find((tool) => tool.definition.name === 'agent');
|
|
96
112
|
if (!agentTool) throw new Error('Agent tool policy guard could not find the agent tool.');
|
|
@@ -120,6 +136,8 @@ export function installAgentToolPolicyGuard(registry: ToolRegistry, options: Age
|
|
|
120
136
|
].join(' '),
|
|
121
137
|
denial: MCP_SECURITY_MUTATION_DENIAL,
|
|
122
138
|
});
|
|
139
|
+
} else if (tool.definition.name === 'fetch') {
|
|
140
|
+
wrapFetchToolForAgentPolicy(tool);
|
|
123
141
|
} else if (BLOCKED_MAIN_CONVERSATION_TOOL_NAME_SET.has(tool.definition.name)) {
|
|
124
142
|
wrapBlockedMainConversationToolForAgentPolicy(tool);
|
|
125
143
|
}
|
|
@@ -165,6 +183,16 @@ export function wrapExecToolForAgentPolicy(tool: Tool): void {
|
|
|
165
183
|
};
|
|
166
184
|
}
|
|
167
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
|
+
|
|
168
196
|
export function validateExecToolInvocationForAgentPolicy(args: ExecToolArgs): string | null {
|
|
169
197
|
if (args.parallel === true) return BACKGROUND_EXEC_DENIAL;
|
|
170
198
|
if (Array.isArray(args.file_ops) && args.file_ops.length > 0) return BACKGROUND_EXEC_DENIAL;
|
|
@@ -187,6 +215,31 @@ export function validateExecToolInvocationForAgentPolicy(args: ExecToolArgs): st
|
|
|
187
215
|
return null;
|
|
188
216
|
}
|
|
189
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
|
+
|
|
190
243
|
type ModeRestrictedToolPolicy = {
|
|
191
244
|
readonly allowedModes: readonly string[];
|
|
192
245
|
readonly modeSet: ReadonlySet<string>;
|
|
@@ -240,14 +293,24 @@ export const AGENT_EXEC_BACKGROUND_DENIAL_MESSAGE = BACKGROUND_EXEC_DENIAL;
|
|
|
240
293
|
export const AGENT_READ_ONLY_REMOTE_TOOL_MODES = READ_ONLY_REMOTE_TOOL_MODES;
|
|
241
294
|
export const AGENT_READ_ONLY_CHANNEL_TOOL_MODES = READ_ONLY_CHANNEL_TOOL_MODES;
|
|
242
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;
|
|
243
297
|
export const AGENT_REMOTE_MUTATION_DENIAL_MESSAGE = REMOTE_MUTATION_DENIAL;
|
|
244
298
|
export const AGENT_CHANNEL_ACTION_DENIAL_MESSAGE = CHANNEL_ACTION_DENIAL;
|
|
245
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;
|
|
246
301
|
|
|
247
302
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
248
303
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
249
304
|
}
|
|
250
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
|
+
|
|
251
314
|
function narrowAgentToolDefinitionForAgentPolicy(tool: Tool): void {
|
|
252
315
|
tool.definition.description = [
|
|
253
316
|
'Read-only local Agent inspection for GoodVibes Agent.',
|
|
@@ -293,6 +356,47 @@ function narrowExecToolDefinitionForAgentPolicy(tool: Tool): void {
|
|
|
293
356
|
}
|
|
294
357
|
}
|
|
295
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
|
+
|
|
296
400
|
function narrowModeToolDefinitionForAgentPolicy(tool: Tool, allowedModes: readonly string[], description: string): void {
|
|
297
401
|
tool.definition.description = description;
|
|
298
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.
|
|
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 {
|