@pellux/goodvibes-agent 0.1.25 → 0.1.26
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/agent-read-policy.ts +179 -0
- package/src/tools/wrfc-agent-guard.ts +13 -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.26",
|
|
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,179 @@
|
|
|
1
|
+
import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
+
|
|
3
|
+
type ReadFileArgs = {
|
|
4
|
+
readonly path?: unknown;
|
|
5
|
+
readonly image_mode?: unknown;
|
|
6
|
+
readonly [key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ReadToolArgs = {
|
|
10
|
+
readonly files?: unknown;
|
|
11
|
+
readonly image_mode?: unknown;
|
|
12
|
+
readonly max_image_size?: unknown;
|
|
13
|
+
readonly [key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const READ_IMAGE_MODES = ['default', 'metadata-only', 'thumbnail-only'] as const;
|
|
17
|
+
const READ_IMAGE_MODE_SET = new Set<string>(READ_IMAGE_MODES);
|
|
18
|
+
const MAX_READ_FILES = 10;
|
|
19
|
+
const MAX_READ_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
const SECRET_FILE_EXTENSIONS = new Set([
|
|
22
|
+
'',
|
|
23
|
+
'.cfg',
|
|
24
|
+
'.conf',
|
|
25
|
+
'.env',
|
|
26
|
+
'.ini',
|
|
27
|
+
'.json',
|
|
28
|
+
'.properties',
|
|
29
|
+
'.toml',
|
|
30
|
+
'.txt',
|
|
31
|
+
'.yaml',
|
|
32
|
+
'.yml',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const SECRET_EXACT_SEGMENTS = new Set([
|
|
36
|
+
'.netrc',
|
|
37
|
+
'.npmrc',
|
|
38
|
+
'.pypirc',
|
|
39
|
+
'apikey',
|
|
40
|
+
'api_key',
|
|
41
|
+
'credentials',
|
|
42
|
+
'id_dsa',
|
|
43
|
+
'id_ecdsa',
|
|
44
|
+
'id_ed25519',
|
|
45
|
+
'id_rsa',
|
|
46
|
+
'password',
|
|
47
|
+
'passwords',
|
|
48
|
+
'passwd',
|
|
49
|
+
'secret',
|
|
50
|
+
'secrets',
|
|
51
|
+
'token',
|
|
52
|
+
'tokens',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const SECRET_EXACT_FILE_NAMES = new Set([
|
|
56
|
+
'known_hosts',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const PRIVATE_KEY_EXTENSIONS = new Set([
|
|
60
|
+
'.key',
|
|
61
|
+
'.p12',
|
|
62
|
+
'.pem',
|
|
63
|
+
'.pfx',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const READ_POLICY_DENIAL = [
|
|
67
|
+
'GoodVibes Agent only exposes bounded, non-secret project reads from the main conversation.',
|
|
68
|
+
'Hidden paths, secret-looking files, broad batches, unoptimized image extraction, and oversized image reads are disabled here.',
|
|
69
|
+
'Use explicit Agent CLI/slash commands or GoodVibes TUI delegation when the user intentionally asks for sensitive or deeper local inspection.',
|
|
70
|
+
].join(' ');
|
|
71
|
+
|
|
72
|
+
export const AGENT_READ_IMAGE_MODES = READ_IMAGE_MODES;
|
|
73
|
+
export const AGENT_MAX_READ_FILES = MAX_READ_FILES;
|
|
74
|
+
export const AGENT_MAX_READ_IMAGE_SIZE_BYTES = MAX_READ_IMAGE_SIZE_BYTES;
|
|
75
|
+
export const AGENT_READ_POLICY_DENIAL_MESSAGE = READ_POLICY_DENIAL;
|
|
76
|
+
|
|
77
|
+
export function wrapReadToolForAgentPolicy(tool: Tool): void {
|
|
78
|
+
narrowReadToolDefinitionForAgentPolicy(tool);
|
|
79
|
+
const originalExecute = tool.execute.bind(tool);
|
|
80
|
+
tool.execute = async (args) => {
|
|
81
|
+
const readArgs = args as ReadToolArgs;
|
|
82
|
+
const denial = validateReadToolInvocationForAgentPolicy(readArgs);
|
|
83
|
+
if (denial) return { success: false, error: denial };
|
|
84
|
+
return originalExecute(args);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function validateReadToolInvocationForAgentPolicy(args: ReadToolArgs): string | null {
|
|
89
|
+
if (Array.isArray(args.files) && args.files.length > MAX_READ_FILES) return READ_POLICY_DENIAL;
|
|
90
|
+
if (args.image_mode === 'unoptimized') return READ_POLICY_DENIAL;
|
|
91
|
+
if (typeof args.image_mode === 'string' && !READ_IMAGE_MODE_SET.has(args.image_mode)) return READ_POLICY_DENIAL;
|
|
92
|
+
if (typeof args.max_image_size === 'number' && args.max_image_size > MAX_READ_IMAGE_SIZE_BYTES) {
|
|
93
|
+
return READ_POLICY_DENIAL;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Array.isArray(args.files)) return null;
|
|
97
|
+
for (const file of args.files) {
|
|
98
|
+
if (!isRecord(file)) continue;
|
|
99
|
+
const fileArgs = file as ReadFileArgs;
|
|
100
|
+
if (fileArgs.image_mode === 'unoptimized') return READ_POLICY_DENIAL;
|
|
101
|
+
if (typeof fileArgs.image_mode === 'string' && !READ_IMAGE_MODE_SET.has(fileArgs.image_mode)) {
|
|
102
|
+
return READ_POLICY_DENIAL;
|
|
103
|
+
}
|
|
104
|
+
if (typeof fileArgs.path === 'string' && isBlockedReadPath(fileArgs.path)) return READ_POLICY_DENIAL;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isBlockedReadPath(path: string): boolean {
|
|
111
|
+
const segments = path.replaceAll('\\', '/').split('/').filter((segment) => segment.length > 0);
|
|
112
|
+
for (const segment of segments) {
|
|
113
|
+
if (segment === '.' || segment === '..') continue;
|
|
114
|
+
const lower = segment.toLowerCase();
|
|
115
|
+
if (lower.startsWith('.')) return true;
|
|
116
|
+
if (SECRET_EXACT_SEGMENTS.has(lower)) return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const fileName = segments.at(-1)?.toLowerCase() ?? '';
|
|
120
|
+
if (fileName.length === 0) return false;
|
|
121
|
+
if (SECRET_EXACT_FILE_NAMES.has(fileName)) return true;
|
|
122
|
+
const extension = getExtension(fileName);
|
|
123
|
+
if (PRIVATE_KEY_EXTENSIONS.has(extension)) return true;
|
|
124
|
+
const stem = extension.length > 0 ? fileName.slice(0, -extension.length) : fileName;
|
|
125
|
+
return SECRET_EXACT_SEGMENTS.has(stem) && SECRET_FILE_EXTENSIONS.has(extension);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function narrowReadToolDefinitionForAgentPolicy(tool: Tool): void {
|
|
129
|
+
tool.definition.description = [
|
|
130
|
+
'Read ordinary project files for GoodVibes Agent with bounded, non-secret, main-conversation policy.',
|
|
131
|
+
'Hidden paths, secret-looking files, broad batches, unoptimized image extraction, and oversized image reads are disabled.',
|
|
132
|
+
].join(' ');
|
|
133
|
+
tool.definition.sideEffects = ['read_fs'];
|
|
134
|
+
tool.definition.concurrency = 'serial';
|
|
135
|
+
|
|
136
|
+
const properties = tool.definition.parameters.properties;
|
|
137
|
+
if (!isRecord(properties)) return;
|
|
138
|
+
|
|
139
|
+
const files = properties.files;
|
|
140
|
+
if (isRecord(files)) {
|
|
141
|
+
files.maxItems = MAX_READ_FILES;
|
|
142
|
+
files.description = 'Ordinary non-hidden, non-secret-looking project files to read. Sensitive paths require explicit user-directed workflows.';
|
|
143
|
+
const itemSchema = files.items;
|
|
144
|
+
if (isRecord(itemSchema)) {
|
|
145
|
+
const fileProperties = itemSchema.properties;
|
|
146
|
+
if (isRecord(fileProperties)) {
|
|
147
|
+
const pathProperty = fileProperties.path;
|
|
148
|
+
if (isRecord(pathProperty)) {
|
|
149
|
+
pathProperty.description = 'Relative or absolute path to a non-hidden, non-secret-looking project file.';
|
|
150
|
+
}
|
|
151
|
+
narrowImageModeProperty(fileProperties, 'image_mode');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
narrowImageModeProperty(properties, 'image_mode');
|
|
157
|
+
const maxImageSize = properties.max_image_size;
|
|
158
|
+
if (isRecord(maxImageSize)) {
|
|
159
|
+
maxImageSize.maximum = MAX_READ_IMAGE_SIZE_BYTES;
|
|
160
|
+
maxImageSize.description = 'Maximum image file size in bytes allowed by GoodVibes Agent read policy.';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function narrowImageModeProperty(properties: Record<string, unknown>, key: string): void {
|
|
165
|
+
const property = properties[key];
|
|
166
|
+
if (!isRecord(property)) return;
|
|
167
|
+
property.enum = [...READ_IMAGE_MODES];
|
|
168
|
+
property.description = 'Image handling mode allowed by GoodVibes Agent read policy. Full-resolution unoptimized extraction is disabled.';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getExtension(fileName: string): string {
|
|
172
|
+
const dotIndex = fileName.lastIndexOf('.');
|
|
173
|
+
if (dotIndex <= 0) return '';
|
|
174
|
+
return fileName.slice(dotIndex);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
178
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
179
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
wrapRegistryToolForAgentPolicy,
|
|
6
6
|
} from './agent-analysis-registry-policy.ts';
|
|
7
7
|
import { wrapFindToolForAgentPolicy } from './agent-find-policy.ts';
|
|
8
|
+
import { wrapReadToolForAgentPolicy } from './agent-read-policy.ts';
|
|
8
9
|
import { wrapWebSearchToolForAgentPolicy } from './agent-web-search-policy.ts';
|
|
9
10
|
|
|
10
11
|
type AgentToolArgs = {
|
|
@@ -198,6 +199,8 @@ export function installAgentToolPolicyGuard(registry: ToolRegistry, options: Age
|
|
|
198
199
|
for (const tool of registry.list()) {
|
|
199
200
|
if (tool.definition.name === 'exec') {
|
|
200
201
|
wrapExecToolForAgentPolicy(tool);
|
|
202
|
+
} else if (tool.definition.name === 'read') {
|
|
203
|
+
wrapReadToolForAgentPolicy(tool);
|
|
201
204
|
} else if (tool.definition.name === 'remote') {
|
|
202
205
|
wrapModeRestrictedToolForAgentPolicy(tool, {
|
|
203
206
|
allowedModes: READ_ONLY_REMOTE_TOOL_MODES,
|
|
@@ -559,6 +562,16 @@ export {
|
|
|
559
562
|
wrapRegistryToolForAgentPolicy,
|
|
560
563
|
} from './agent-analysis-registry-policy.ts';
|
|
561
564
|
|
|
565
|
+
export {
|
|
566
|
+
AGENT_MAX_READ_FILES,
|
|
567
|
+
AGENT_MAX_READ_IMAGE_SIZE_BYTES,
|
|
568
|
+
AGENT_READ_IMAGE_MODES,
|
|
569
|
+
AGENT_READ_POLICY_DENIAL_MESSAGE,
|
|
570
|
+
isBlockedReadPath,
|
|
571
|
+
validateReadToolInvocationForAgentPolicy,
|
|
572
|
+
wrapReadToolForAgentPolicy,
|
|
573
|
+
} from './agent-read-policy.ts';
|
|
574
|
+
|
|
562
575
|
export {
|
|
563
576
|
AGENT_FIND_POLICY_DENIAL_MESSAGE,
|
|
564
577
|
AGENT_READ_ONLY_FIND_OUTPUT_FORMATS,
|
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.26';
|
|
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 {
|