@pellux/goodvibes-agent 0.1.11 → 0.1.13
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 +9 -0
- package/package.json +1 -1
- package/src/tools/wrfc-agent-guard.ts +26 -0
- package/src/verification/live-verifier.ts +100 -68
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GoodVibes Agent will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.13 - 2026-05-31
|
|
6
|
+
|
|
7
|
+
- 989b048 Block local coding tools in agent runtime
|
|
8
|
+
|
|
9
|
+
## 0.1.12 - 2026-05-31
|
|
10
|
+
|
|
11
|
+
- 1843a77 Handle external daemon SDK mismatch in live verification
|
|
12
|
+
- 2b1a3f4 Align agent-owned test paths
|
|
13
|
+
|
|
5
14
|
## 0.1.11 - 2026-05-31
|
|
6
15
|
|
|
7
16
|
- d20a93e Allow explicit recall review without yes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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",
|
|
@@ -10,6 +10,8 @@ type AgentToolPolicyGuardOptions = {
|
|
|
10
10
|
readonly getLastUserMessage?: () => string | null;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
const BLOCKED_MAIN_CONVERSATION_TOOL_NAMES = ['write', 'edit', 'workflow', 'repl'] as const;
|
|
14
|
+
|
|
13
15
|
const READ_ONLY_AGENT_TOOL_MODES = [
|
|
14
16
|
'status',
|
|
15
17
|
'list',
|
|
@@ -23,6 +25,7 @@ const READ_ONLY_AGENT_TOOL_MODES = [
|
|
|
23
25
|
] as const;
|
|
24
26
|
|
|
25
27
|
const READ_ONLY_AGENT_TOOL_MODE_SET = new Set<string>(READ_ONLY_AGENT_TOOL_MODES);
|
|
28
|
+
const BLOCKED_MAIN_CONVERSATION_TOOL_NAME_SET = new Set<string>(BLOCKED_MAIN_CONVERSATION_TOOL_NAMES);
|
|
26
29
|
|
|
27
30
|
const LOCAL_AGENT_DENIAL = [
|
|
28
31
|
'GoodVibes Agent does not spawn local Engineer/Reviewer/Tester/Verifier roots or run local WRFC chains.',
|
|
@@ -30,10 +33,21 @@ const LOCAL_AGENT_DENIAL = [
|
|
|
30
33
|
'For explicit build/fix/review work, delegate one request to GoodVibes TUI through the public shared-session/build-delegation contract with the full original user ask.',
|
|
31
34
|
].join(' ');
|
|
32
35
|
|
|
36
|
+
const LOCAL_CODING_TOOL_DENIAL = [
|
|
37
|
+
'GoodVibes Agent does not perform direct local file mutation, local WRFC workflow execution, or local sandbox/REPL execution from the main conversation.',
|
|
38
|
+
'For explicit build/fix/review/code execution work, delegate one request to GoodVibes TUI through the public shared-session/build-delegation contract with the full original user ask.',
|
|
39
|
+
'For durable Agent memory, skills, personas, routines, and knowledge, use the Agent-owned commands and isolated Agent Knowledge routes.',
|
|
40
|
+
].join(' ');
|
|
41
|
+
|
|
33
42
|
export function installAgentToolPolicyGuard(registry: ToolRegistry, options: AgentToolPolicyGuardOptions = {}): void {
|
|
34
43
|
const agentTool = registry.list().find((tool) => tool.definition.name === 'agent');
|
|
35
44
|
if (!agentTool) throw new Error('Agent tool policy guard could not find the agent tool.');
|
|
36
45
|
wrapAgentToolForAgentPolicy(agentTool, options);
|
|
46
|
+
for (const tool of registry.list()) {
|
|
47
|
+
if (BLOCKED_MAIN_CONVERSATION_TOOL_NAME_SET.has(tool.definition.name)) {
|
|
48
|
+
wrapBlockedMainConversationToolForAgentPolicy(tool);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
export function wrapAgentToolForAgentPolicy(tool: Tool, _options: AgentToolPolicyGuardOptions = {}): void {
|
|
@@ -55,8 +69,20 @@ export function normalizeAgentToolInvocationForAgentPolicy(args: AgentToolArgs):
|
|
|
55
69
|
return args;
|
|
56
70
|
}
|
|
57
71
|
|
|
72
|
+
export function wrapBlockedMainConversationToolForAgentPolicy(tool: Tool): void {
|
|
73
|
+
tool.definition.description = [
|
|
74
|
+
`Blocked in GoodVibes Agent main conversation: ${tool.definition.name}.`,
|
|
75
|
+
'Use explicit GoodVibes TUI build delegation for build/fix/review/code execution work.',
|
|
76
|
+
'Use Agent-owned local registries and isolated Agent Knowledge routes for Agent memory and knowledge work.',
|
|
77
|
+
].join(' ');
|
|
78
|
+
tool.definition.sideEffects = [];
|
|
79
|
+
tool.execute = async () => ({ success: false, error: LOCAL_CODING_TOOL_DENIAL });
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
export const AGENT_LOCAL_SPAWN_DENIAL_MESSAGE = LOCAL_AGENT_DENIAL;
|
|
59
83
|
export const AGENT_READ_ONLY_TOOL_MODES = READ_ONLY_AGENT_TOOL_MODES;
|
|
84
|
+
export const AGENT_BLOCKED_MAIN_CONVERSATION_TOOL_NAMES = BLOCKED_MAIN_CONVERSATION_TOOL_NAMES;
|
|
85
|
+
export const AGENT_MAIN_CONVERSATION_TOOL_DENIAL_MESSAGE = LOCAL_CODING_TOOL_DENIAL;
|
|
60
86
|
|
|
61
87
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
88
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { auditGoodVibesHome } from '../config/goodvibes-home-audit.ts';
|
|
5
5
|
import { buildVerificationLedger } from './verification-ledger.ts';
|
|
6
|
+
import { SDK_VERSION } from '../version.ts';
|
|
6
7
|
|
|
7
8
|
export type LiveVerificationStatus = 'pass' | 'warn' | 'fail' | 'skip';
|
|
8
9
|
|
|
@@ -271,6 +272,25 @@ function countStatuses(checks: readonly LiveVerificationCheck[]): Record<LiveVer
|
|
|
271
272
|
);
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
export function buildAgentKnowledgeLiveSkipCheck(
|
|
276
|
+
id: string,
|
|
277
|
+
title: string,
|
|
278
|
+
daemonVersion: string,
|
|
279
|
+
expectedSdkVersion = SDK_VERSION,
|
|
280
|
+
): LiveVerificationCheck {
|
|
281
|
+
return {
|
|
282
|
+
id,
|
|
283
|
+
title,
|
|
284
|
+
status: 'skip',
|
|
285
|
+
summary: `Skipped because external daemon SDK ${daemonVersion} does not match Agent SDK pin ${expectedSdkVersion}.`,
|
|
286
|
+
detail: [
|
|
287
|
+
'Agent Knowledge is intentionally isolated under /api/goodvibes-agent/knowledge/*.',
|
|
288
|
+
'An older daemon cannot validate those routes, and Agent must not fall back to default Knowledge/Wiki or HomeGraph.',
|
|
289
|
+
'Update/restart the external daemon, then rerun live verification.',
|
|
290
|
+
].join('\n'),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
274
294
|
export async function buildLiveVerificationReport(options: LiveVerificationOptions): Promise<LiveVerificationReport> {
|
|
275
295
|
const homeDir = resolve(options.homeDir);
|
|
276
296
|
const projectRoot = resolve(options.projectRoot);
|
|
@@ -278,6 +298,7 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
|
|
|
278
298
|
const daemonBaseUrl = resolveDaemonBaseUrl(homeDir, options.daemonBaseUrl);
|
|
279
299
|
const token = options.token ?? readDaemonToken(homeDir);
|
|
280
300
|
const checks: LiveVerificationCheck[] = [];
|
|
301
|
+
let daemonSdkVersion: string | null = null;
|
|
281
302
|
|
|
282
303
|
const ledger = buildVerificationLedger(projectRoot);
|
|
283
304
|
checks.push({
|
|
@@ -328,7 +349,7 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
|
|
|
328
349
|
'Agent CLI compatibility JSON command',
|
|
329
350
|
await runCommand(binaryPath, ['compat', '--json'], projectRoot),
|
|
330
351
|
'Agent CLI compatibility returned parseable JSON.',
|
|
331
|
-
{ parseJson: true },
|
|
352
|
+
{ parseJson: true, warnOnNonZero: true },
|
|
332
353
|
));
|
|
333
354
|
checks.push(commandCheck(
|
|
334
355
|
'cli-agent-knowledge-status',
|
|
@@ -392,6 +413,7 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
|
|
|
392
413
|
const version = typeof parsed.sdkVersion === 'string'
|
|
393
414
|
? parsed.sdkVersion
|
|
394
415
|
: typeof parsed.version === 'string' ? parsed.version : 'unknown';
|
|
416
|
+
daemonSdkVersion = version;
|
|
395
417
|
return { status: 'pass', summary: `/status returned 200, version ${version}.` };
|
|
396
418
|
} catch {
|
|
397
419
|
return { status: 'warn', summary: '/status returned 200 but was not parseable JSON.' };
|
|
@@ -438,78 +460,88 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
|
|
|
438
460
|
},
|
|
439
461
|
));
|
|
440
462
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
463
|
+
const daemonVersionMismatch = daemonSdkVersion !== null && daemonSdkVersion !== 'unknown' && daemonSdkVersion !== SDK_VERSION;
|
|
464
|
+
if (daemonVersionMismatch) {
|
|
465
|
+
const mismatchedDaemonVersion = daemonSdkVersion ?? 'unknown';
|
|
466
|
+
checks.push(
|
|
467
|
+
buildAgentKnowledgeLiveSkipCheck('agent-knowledge-status', 'Agent Knowledge isolated /status', mismatchedDaemonVersion),
|
|
468
|
+
buildAgentKnowledgeLiveSkipCheck('agent-knowledge-ask-isolated', 'Agent Knowledge isolated ask', mismatchedDaemonVersion),
|
|
469
|
+
buildAgentKnowledgeLiveSkipCheck('agent-knowledge-search-isolated', 'Agent Knowledge isolated search', mismatchedDaemonVersion),
|
|
470
|
+
);
|
|
471
|
+
} else {
|
|
472
|
+
checks.push(await fetchJsonCheck(
|
|
473
|
+
'agent-knowledge-status',
|
|
474
|
+
'Agent Knowledge isolated /status',
|
|
475
|
+
`${daemonBaseUrl}/api/goodvibes-agent/knowledge/status`,
|
|
476
|
+
token,
|
|
477
|
+
{
|
|
478
|
+
validate: (status, body) => {
|
|
479
|
+
if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/status returned ${status}.` };
|
|
480
|
+
try {
|
|
481
|
+
JSON.parse(body);
|
|
482
|
+
return { status: 'pass', summary: 'Agent Knowledge status route returned parseable JSON.' };
|
|
483
|
+
} catch {
|
|
484
|
+
return { status: 'fail', summary: 'Agent Knowledge status was not parseable JSON.' };
|
|
485
|
+
}
|
|
486
|
+
},
|
|
455
487
|
},
|
|
456
|
-
|
|
457
|
-
));
|
|
488
|
+
));
|
|
458
489
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
490
|
+
checks.push(await fetchJsonCheck(
|
|
491
|
+
'agent-knowledge-ask-isolated',
|
|
492
|
+
'Agent Knowledge isolated ask',
|
|
493
|
+
`${daemonBaseUrl}/api/goodvibes-agent/knowledge/ask`,
|
|
494
|
+
token,
|
|
495
|
+
{
|
|
496
|
+
method: 'POST',
|
|
497
|
+
body: {
|
|
498
|
+
query: 'What is GoodVibes Agent?',
|
|
499
|
+
limit: 5,
|
|
500
|
+
mode: 'concise',
|
|
501
|
+
includeSources: true,
|
|
502
|
+
includeConfidence: true,
|
|
503
|
+
includeLinkedObjects: true,
|
|
504
|
+
},
|
|
505
|
+
validate: (status, body) => {
|
|
506
|
+
if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/ask returned ${status}.` };
|
|
507
|
+
try {
|
|
508
|
+
JSON.parse(body);
|
|
509
|
+
} catch {
|
|
510
|
+
return { status: 'fail', summary: 'Agent Knowledge ask was not parseable JSON.' };
|
|
511
|
+
}
|
|
512
|
+
const lower = body.toLowerCase();
|
|
513
|
+
if (lower.includes('home assistant') || lower.includes('homegraph') || lower.includes('home graph')) {
|
|
514
|
+
return { status: 'fail', summary: 'Agent Knowledge ask returned HomeGraph/Home Assistant contamination.' };
|
|
515
|
+
}
|
|
516
|
+
return { status: 'pass', summary: 'Agent Knowledge ask stayed on the isolated Agent route.' };
|
|
517
|
+
},
|
|
473
518
|
},
|
|
474
|
-
|
|
475
|
-
if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/ask returned ${status}.` };
|
|
476
|
-
try {
|
|
477
|
-
JSON.parse(body);
|
|
478
|
-
} catch {
|
|
479
|
-
return { status: 'fail', summary: 'Agent Knowledge ask was not parseable JSON.' };
|
|
480
|
-
}
|
|
481
|
-
const lower = body.toLowerCase();
|
|
482
|
-
if (lower.includes('home assistant') || lower.includes('homegraph') || lower.includes('home graph')) {
|
|
483
|
-
return { status: 'fail', summary: 'Agent Knowledge ask returned HomeGraph/Home Assistant contamination.' };
|
|
484
|
-
}
|
|
485
|
-
return { status: 'pass', summary: 'Agent Knowledge ask stayed on the isolated Agent route.' };
|
|
486
|
-
},
|
|
487
|
-
},
|
|
488
|
-
));
|
|
519
|
+
));
|
|
489
520
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
521
|
+
checks.push(await fetchJsonCheck(
|
|
522
|
+
'agent-knowledge-search-isolated',
|
|
523
|
+
'Agent Knowledge isolated search',
|
|
524
|
+
`${daemonBaseUrl}/api/goodvibes-agent/knowledge/search`,
|
|
525
|
+
token,
|
|
526
|
+
{
|
|
527
|
+
method: 'POST',
|
|
528
|
+
body: { query: 'What is GoodVibes Agent?', limit: 5 },
|
|
529
|
+
validate: (status, body) => {
|
|
530
|
+
if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/search returned ${status}.` };
|
|
531
|
+
try {
|
|
532
|
+
JSON.parse(body);
|
|
533
|
+
} catch {
|
|
534
|
+
return { status: 'fail', summary: 'Agent Knowledge search was not parseable JSON.' };
|
|
535
|
+
}
|
|
536
|
+
const lower = body.toLowerCase();
|
|
537
|
+
if (lower.includes('home assistant') || lower.includes('homegraph') || lower.includes('home graph')) {
|
|
538
|
+
return { status: 'fail', summary: 'Agent Knowledge search returned HomeGraph/Home Assistant contamination.' };
|
|
539
|
+
}
|
|
540
|
+
return { status: 'pass', summary: 'Agent Knowledge search stayed on the isolated Agent route.' };
|
|
541
|
+
},
|
|
510
542
|
},
|
|
511
|
-
|
|
512
|
-
|
|
543
|
+
));
|
|
544
|
+
}
|
|
513
545
|
|
|
514
546
|
const counts = countStatuses(checks);
|
|
515
547
|
const ok = counts.fail === 0 && (!options.strict || counts.warn === 0);
|
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.13';
|
|
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 {
|