@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 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.11",
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
- checks.push(await fetchJsonCheck(
442
- 'agent-knowledge-status',
443
- 'Agent Knowledge isolated /status',
444
- `${daemonBaseUrl}/api/goodvibes-agent/knowledge/status`,
445
- token,
446
- {
447
- validate: (status, body) => {
448
- if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/status returned ${status}.` };
449
- try {
450
- JSON.parse(body);
451
- return { status: 'pass', summary: 'Agent Knowledge status route returned parseable JSON.' };
452
- } catch {
453
- return { status: 'fail', summary: 'Agent Knowledge status was not parseable JSON.' };
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
- checks.push(await fetchJsonCheck(
460
- 'agent-knowledge-ask-isolated',
461
- 'Agent Knowledge isolated ask',
462
- `${daemonBaseUrl}/api/goodvibes-agent/knowledge/ask`,
463
- token,
464
- {
465
- method: 'POST',
466
- body: {
467
- query: 'What is GoodVibes Agent?',
468
- limit: 5,
469
- mode: 'concise',
470
- includeSources: true,
471
- includeConfidence: true,
472
- includeLinkedObjects: true,
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
- validate: (status, body) => {
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
- checks.push(await fetchJsonCheck(
491
- 'agent-knowledge-search-isolated',
492
- 'Agent Knowledge isolated search',
493
- `${daemonBaseUrl}/api/goodvibes-agent/knowledge/search`,
494
- token,
495
- {
496
- method: 'POST',
497
- body: { query: 'What is GoodVibes Agent?', limit: 5 },
498
- validate: (status, body) => {
499
- if (status !== 200) return { status: 'fail', summary: `/api/goodvibes-agent/knowledge/search returned ${status}.` };
500
- try {
501
- JSON.parse(body);
502
- } catch {
503
- return { status: 'fail', summary: 'Agent Knowledge search was not parseable JSON.' };
504
- }
505
- const lower = body.toLowerCase();
506
- if (lower.includes('home assistant') || lower.includes('homegraph') || lower.includes('home graph')) {
507
- return { status: 'fail', summary: 'Agent Knowledge search returned HomeGraph/Home Assistant contamination.' };
508
- }
509
- return { status: 'pass', summary: 'Agent Knowledge search stayed on the isolated Agent route.' };
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.11';
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 {