@pellux/goodvibes-agent 1.0.30 → 1.0.33

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +7 -5
  3. package/dist/package/main.js +7323 -2268
  4. package/docs/README.md +2 -2
  5. package/docs/channels-remote-and-api.md +6 -2
  6. package/docs/connected-host.md +26 -5
  7. package/docs/getting-started.md +30 -10
  8. package/docs/knowledge-artifacts-and-multimodal.md +16 -4
  9. package/docs/project-planning.md +2 -2
  10. package/docs/providers-and-routing.md +3 -2
  11. package/docs/release-and-publishing.md +15 -11
  12. package/docs/tools-and-commands.md +20 -8
  13. package/package.json +8 -3
  14. package/release/live-verification/live-verification.json +148 -0
  15. package/release/live-verification/live-verification.md +187 -0
  16. package/release/performance-snapshot.json +57 -0
  17. package/release/release-notes.md +28 -0
  18. package/release/release-readiness.json +581 -0
  19. package/src/cli/agent-knowledge-command.ts +5 -5
  20. package/src/cli/agent-knowledge-format.ts +11 -0
  21. package/src/cli/agent-knowledge-runtime.ts +92 -13
  22. package/src/cli/bundle-command.ts +5 -4
  23. package/src/cli/entrypoint.ts +5 -2
  24. package/src/cli/external-runtime.ts +2 -15
  25. package/src/cli/management.ts +4 -3
  26. package/src/input/commands/guidance-runtime.ts +1 -1
  27. package/src/input/commands/knowledge.ts +2 -2
  28. package/src/runtime/bootstrap.ts +2 -2
  29. package/src/runtime/connected-host-auth.ts +16 -0
  30. package/src/tools/agent-channel-send-tool.ts +5 -7
  31. package/src/tools/agent-harness-channel-metadata.ts +177 -0
  32. package/src/tools/agent-harness-connected-host-status.ts +1 -1
  33. package/src/tools/agent-harness-delegation-posture.ts +216 -0
  34. package/src/tools/agent-harness-keybinding-metadata.ts +57 -22
  35. package/src/tools/agent-harness-mcp-metadata.ts +246 -0
  36. package/src/tools/agent-harness-media-posture.ts +282 -0
  37. package/src/tools/agent-harness-metadata.ts +21 -4
  38. package/src/tools/agent-harness-model-routing.ts +501 -0
  39. package/src/tools/agent-harness-notification-metadata.ts +217 -0
  40. package/src/tools/agent-harness-operator-methods.ts +285 -0
  41. package/src/tools/agent-harness-pairing-posture.ts +265 -0
  42. package/src/tools/agent-harness-provider-account-metadata.ts +203 -0
  43. package/src/tools/agent-harness-release-evidence.ts +364 -0
  44. package/src/tools/agent-harness-release-readiness.ts +298 -0
  45. package/src/tools/agent-harness-security-posture.ts +646 -0
  46. package/src/tools/agent-harness-service-posture.ts +201 -0
  47. package/src/tools/agent-harness-session-metadata.ts +282 -0
  48. package/src/tools/agent-harness-setup-posture.ts +295 -0
  49. package/src/tools/agent-harness-tool-schema.ts +103 -27
  50. package/src/tools/agent-harness-tool.ts +209 -236
  51. package/src/tools/agent-harness-ui-surface-metadata.ts +1 -1
  52. package/src/tools/agent-harness-workspace-actions.ts +232 -0
  53. package/src/tools/agent-knowledge-ingest-tool.ts +6 -8
  54. package/src/tools/agent-knowledge-tool.ts +122 -23
  55. package/src/tools/agent-local-registry-tool.ts +4 -5
  56. package/src/tools/agent-media-generate-tool.ts +4 -6
  57. package/src/tools/agent-notify-tool.ts +5 -6
  58. package/src/tools/agent-operator-action-tool.ts +6 -8
  59. package/src/tools/agent-operator-briefing-tool.ts +3 -4
  60. package/src/tools/agent-reminder-schedule-tool.ts +6 -7
  61. package/src/tools/agent-tool-policy-guard.ts +8 -17
  62. package/src/tools/agent-work-plan-tool.ts +3 -4
  63. package/src/version.ts +2 -2
@@ -160,7 +160,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
160
160
  const [id] = commandValues(rest);
161
161
  if (!id) return { output: 'Usage: goodvibes-agent knowledge get <source|node|issue id>', exitCode: 2 };
162
162
  const route = `/api/goodvibes-agent/knowledge/items/${encodeURIComponent(id)}`;
163
- const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.itemGet, async (connection) => (
163
+ const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.itemGet, route }, async (connection) => (
164
164
  await getAgentKnowledgeJson(connection, route)
165
165
  ));
166
166
  if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
@@ -189,7 +189,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
189
189
  const id = values[1];
190
190
  if (!id) return { output: 'Usage: goodvibes-agent knowledge connectors doctor <connectorId>', exitCode: 2 };
191
191
  const route = `/api/goodvibes-agent/knowledge/connectors/${encodeURIComponent(id)}/doctor`;
192
- const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorDoctor, async (connection) => (
192
+ const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.connectorDoctor, route }, async (connection) => (
193
193
  await getAgentKnowledgeJson(connection, route)
194
194
  ));
195
195
  if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
@@ -201,7 +201,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
201
201
  const id = values[0];
202
202
  if (id) {
203
203
  const route = `/api/goodvibes-agent/knowledge/connectors/${encodeURIComponent(id)}`;
204
- const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorGet, async (connection) => (
204
+ const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.connectorGet, route }, async (connection) => (
205
205
  await getAgentKnowledgeJson(connection, route)
206
206
  ));
207
207
  if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
@@ -224,7 +224,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
224
224
  const [id] = commandValues(rest);
225
225
  if (!id) return { output: 'Usage: goodvibes-agent knowledge connector <connectorId>', exitCode: 2 };
226
226
  const route = `/api/goodvibes-agent/knowledge/connectors/${encodeURIComponent(id)}`;
227
- const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorGet, async (connection) => (
227
+ const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.connectorGet, route }, async (connection) => (
228
228
  await getAgentKnowledgeJson(connection, route)
229
229
  ));
230
230
  if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
@@ -238,7 +238,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
238
238
  const [id] = commandValues(rest);
239
239
  if (!id) return { output: 'Usage: goodvibes-agent knowledge connector-doctor <connectorId>', exitCode: 2 };
240
240
  const route = `/api/goodvibes-agent/knowledge/connectors/${encodeURIComponent(id)}/doctor`;
241
- const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorDoctor, async (connection) => (
241
+ const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.connectorDoctor, route }, async (connection) => (
242
242
  await getAgentKnowledgeJson(connection, route)
243
243
  ));
244
244
  if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
@@ -47,6 +47,7 @@ export function formatAgentKnowledgeFailureKind(kind: string): string {
47
47
  if (kind === 'connected_host_unavailable') return 'connected host unavailable';
48
48
  if (kind === 'connected_host_route_unavailable') return 'connected host route unavailable';
49
49
  if (kind === 'connected_host_error') return 'connected host error';
50
+ if (kind === 'scope_contamination') return 'scope contamination';
50
51
  if (kind === 'version_mismatch') return 'version mismatch';
51
52
  return kind.replace(/[_-]+/g, ' ');
52
53
  }
@@ -163,6 +164,7 @@ export function formatEntityList(data: unknown, kind: 'sources' | 'nodes' | 'iss
163
164
  ...values.slice(0, limit).map((value, index) => (
164
165
  kind === 'issues' ? format(value) : ` ${index + 1}. ${format(value)}`
165
166
  )),
167
+ ` route /api/goodvibes-agent/knowledge/${kind}`,
166
168
  ].join('\n');
167
169
  }
168
170
 
@@ -179,6 +181,7 @@ export function formatItem(data: unknown, id: string): string {
179
181
  `Agent Knowledge item ${id}`,
180
182
  ` ${sourceLine(source)}`,
181
183
  relatedKnowledgeCountsLine(relatedEdges, linkedSources, linkedNodes),
184
+ ' route /api/goodvibes-agent/knowledge/items/{id}',
182
185
  ].join('\n');
183
186
  }
184
187
  if (node) {
@@ -186,6 +189,7 @@ export function formatItem(data: unknown, id: string): string {
186
189
  `Agent Knowledge item ${id}`,
187
190
  ` ${nodeLine(node)}`,
188
191
  relatedKnowledgeCountsLine(relatedEdges, linkedSources, linkedNodes),
192
+ ' route /api/goodvibes-agent/knowledge/items/{id}',
189
193
  ].join('\n');
190
194
  }
191
195
  if (issue) {
@@ -193,6 +197,7 @@ export function formatItem(data: unknown, id: string): string {
193
197
  `Agent Knowledge item ${id}`,
194
198
  issueLine(issue),
195
199
  relatedKnowledgeCountsLine(relatedEdges, linkedSources, linkedNodes),
200
+ ' route /api/goodvibes-agent/knowledge/items/{id}',
196
201
  ].join('\n');
197
202
  }
198
203
  return [
@@ -231,6 +236,7 @@ export function formatConnectors(data: unknown): string {
231
236
  return [
232
237
  `Agent Knowledge connectors (${connectors.length})`,
233
238
  ...connectors.map(connectorLine),
239
+ ' route /api/goodvibes-agent/knowledge/connectors',
234
240
  ].join('\n');
235
241
  }
236
242
 
@@ -299,6 +305,7 @@ export function formatAsk(data: unknown, query: string): string {
299
305
  }
300
306
  if (facts.length > 0) lines.push('', `Facts ${facts.length}`);
301
307
  if (gaps.length > 0) lines.push('', `Gaps ${gaps.length}`);
308
+ lines.push('', 'route /api/goodvibes-agent/knowledge/ask');
302
309
  return lines.join('\n');
303
310
  }
304
311
 
@@ -316,6 +323,7 @@ export function formatSearch(data: unknown, query: string): string {
316
323
  return [
317
324
  `Agent Knowledge search ${query}`,
318
325
  ...results.slice(0, 10).map((result, index) => ` ${index + 1}. ${resultLine(result)}`),
326
+ ' route /api/goodvibes-agent/knowledge/search',
319
327
  ].join('\n');
320
328
  }
321
329
 
@@ -385,5 +393,8 @@ export function formatFailure(failure: AgentKnowledgeFailureLike, json: boolean)
385
393
  failure.kind === 'connected_host_route_unavailable'
386
394
  ? ' next update the connected GoodVibes host to the SDK version required by this Agent package.'
387
395
  : null,
396
+ failure.kind === 'scope_contamination'
397
+ ? ' next update the connected GoodVibes host so Agent Knowledge routes return only Agent-owned scope data.'
398
+ : null,
388
399
  ].filter((line): line is string => Boolean(line)).join('\n');
389
400
  }
@@ -1,8 +1,7 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
1
  import { createBrowserAgentSdk } from '@pellux/goodvibes-sdk/browser/agent';
4
2
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
3
  import { SDK_VERSION, VERSION } from '../version.ts';
4
+ import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
6
5
  import type { ConnectedHostCallMethod } from './agent-knowledge-methods.ts';
7
6
 
8
7
  export type JsonRecord = Record<string, unknown>;
@@ -22,7 +21,13 @@ export interface AgentKnowledgeConnectionRuntime {
22
21
 
23
22
  export interface AgentKnowledgeFailure {
24
23
  readonly ok: false;
25
- readonly kind: 'connected_host_unavailable' | 'auth_required' | 'version_mismatch' | 'connected_host_route_unavailable' | 'connected_host_error';
24
+ readonly kind:
25
+ | 'connected_host_unavailable'
26
+ | 'auth_required'
27
+ | 'version_mismatch'
28
+ | 'connected_host_route_unavailable'
29
+ | 'connected_host_error'
30
+ | 'scope_contamination';
26
31
  readonly error: string;
27
32
  readonly baseUrl: string;
28
33
  readonly route: string;
@@ -56,15 +61,8 @@ export function resolveConnectedHostConnection(runtime: AgentKnowledgeConnection
56
61
  const host = String(runtime.configManager.get('controlPlane.host') ?? '127.0.0.1');
57
62
  const port = Number(runtime.configManager.get('controlPlane.port') ?? 3421);
58
63
  const baseUrl = `http://${host}:${Number.isFinite(port) ? port : 3421}`;
59
- const tokenPath = join(runtime.homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
60
- if (!existsSync(tokenPath)) return { baseUrl, token: null, tokenPath };
61
- try {
62
- const parsed = JSON.parse(readFileSync(tokenPath, 'utf-8')) as unknown;
63
- const token = isRecord(parsed) && typeof parsed.token === 'string' ? parsed.token : null;
64
- return { baseUrl, token, tokenPath };
65
- } catch {
66
- return { baseUrl, token: null, tokenPath };
67
- }
64
+ const token = readConnectedHostOperatorToken(runtime.homeDirectory);
65
+ return { baseUrl, token: token.token, tokenPath: token.path };
68
66
  }
69
67
 
70
68
  export async function fetchConnectedHostStatus(connection: AgentKnowledgeConnection): Promise<{ readonly ok: boolean; readonly status: number; readonly body: unknown }> {
@@ -122,6 +120,87 @@ export function createAgentSdk(connection: AgentKnowledgeConnection) {
122
120
  });
123
121
  }
124
122
 
123
+ const AGENT_KNOWLEDGE_SCOPE_KEYS = new Set(['spaceid', 'knowledgespaceid']);
124
+ const AGENT_KNOWLEDGE_SCOPE_TEXT_PATTERNS: readonly { readonly label: string; readonly pattern: RegExp }[] = [
125
+ {
126
+ label: 'default knowledge scope id',
127
+ pattern: /["']?(?:knowledge[-_\s]*space[-_\s]*id|knowledgespaceid|space[-_\s]*id|spaceid)["']?\s*[:=]\s*["']?default["']?/i,
128
+ },
129
+ { label: 'host assistant payload marker', pattern: /home\s*assistant/i },
130
+ { label: 'host graph payload marker', pattern: /home\s*graph|homegraph/i },
131
+ ];
132
+
133
+ function normalizedJsonKey(key: string): string {
134
+ return key.replace(/[-_\s]/g, '').toLowerCase();
135
+ }
136
+
137
+ function findAgentKnowledgeTextContamination(value: string): string | null {
138
+ for (const { label, pattern } of AGENT_KNOWLEDGE_SCOPE_TEXT_PATTERNS) {
139
+ if (pattern.test(value)) return label;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ export function findAgentKnowledgeScopeContamination(value: unknown): string | null {
145
+ const seen = new WeakSet<object>();
146
+ const visit = (node: unknown): string | null => {
147
+ if (typeof node === 'string') return findAgentKnowledgeTextContamination(node);
148
+ if (!node || typeof node !== 'object') return null;
149
+ if (seen.has(node)) return null;
150
+ seen.add(node);
151
+ if (Array.isArray(node)) {
152
+ for (const item of node) {
153
+ const nested = visit(item);
154
+ if (nested) return nested;
155
+ }
156
+ return null;
157
+ }
158
+ for (const [key, nestedValue] of Object.entries(node as JsonRecord)) {
159
+ const keyFinding = findAgentKnowledgeTextContamination(key);
160
+ if (keyFinding && keyFinding !== 'default knowledge scope id') return keyFinding;
161
+ const normalizedKey = normalizedJsonKey(key);
162
+ if (
163
+ AGENT_KNOWLEDGE_SCOPE_KEYS.has(normalizedKey)
164
+ && typeof nestedValue === 'string'
165
+ && nestedValue.trim().toLowerCase() === 'default'
166
+ ) {
167
+ return `${key}=default`;
168
+ }
169
+ const nested = visit(nestedValue);
170
+ if (nested) return nested;
171
+ }
172
+ return null;
173
+ };
174
+ return visit(value);
175
+ }
176
+
177
+ export function agentKnowledgeScopeContaminationFailure(
178
+ connection: AgentKnowledgeConnection,
179
+ route: string,
180
+ finding: string,
181
+ ): AgentKnowledgeFailure {
182
+ return {
183
+ ok: false,
184
+ kind: 'scope_contamination',
185
+ error: [
186
+ `Agent Knowledge route returned non-Agent knowledge contamination (${finding}).`,
187
+ 'GoodVibes Agent must use only isolated /api/goodvibes-agent/knowledge/* scope data.',
188
+ ].join(' '),
189
+ baseUrl: connection.baseUrl,
190
+ route,
191
+ };
192
+ }
193
+
194
+ export function validateAgentKnowledgeData<TData>(
195
+ data: TData,
196
+ connection: AgentKnowledgeConnection,
197
+ method: ConnectedHostCallMethod,
198
+ ): AgentKnowledgeResult<TData> {
199
+ const contamination = findAgentKnowledgeScopeContamination(data);
200
+ if (contamination) return agentKnowledgeScopeContaminationFailure(connection, method.route, contamination);
201
+ return { ok: true, kind: method.kind, route: method.route, data };
202
+ }
203
+
125
204
  export async function postAgentKnowledgeJson<TData>(
126
205
  connection: AgentKnowledgeConnection,
127
206
  route: string,
@@ -239,7 +318,7 @@ export async function runKnowledgeCall<TData>(
239
318
  }
240
319
  try {
241
320
  const data = await call(connection);
242
- return { ok: true, kind: method.kind, route: method.route, data };
321
+ return validateAgentKnowledgeData(data, connection, method);
243
322
  } catch (error) {
244
323
  return classifyKnowledgeError(error, connection, method.route);
245
324
  }
@@ -1,10 +1,11 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
2
+ import { dirname } from 'node:path';
3
3
  import { RuntimeEventBus } from '@/runtime/index.ts';
4
4
  import { createShellPathService } from '@/runtime/index.ts';
5
5
  import { listProviderRuntimeSnapshots } from '@pellux/goodvibes-sdk/platform/providers';
6
6
  import { createRuntimeServices } from '../runtime/services.ts';
7
7
  import { createRuntimeStore } from '../runtime/store/index.ts';
8
+ import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
8
9
  import { getOnboardingCheckMarkerPath } from '../runtime/onboarding/index.ts';
9
10
  import { CONFIG_SCHEMA } from '../config/index.ts';
10
11
  import { SecretsManager } from '../config/secrets.ts';
@@ -76,14 +77,14 @@ function readAuthPosture(runtime: CliCommandRuntime) {
76
77
  });
77
78
  const userStorePath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-users.json');
78
79
  const bootstrapCredentialPath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-bootstrap.txt');
79
- const operatorTokenPath = join(runtime.homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
80
+ const operatorToken = readConnectedHostOperatorToken(runtime.homeDirectory);
80
81
  return {
81
82
  userStorePath,
82
83
  userStorePresent: existsSync(userStorePath),
83
84
  bootstrapCredentialPath,
84
85
  bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
85
- operatorTokenPath,
86
- operatorTokenPresent: existsSync(operatorTokenPath),
86
+ operatorTokenPath: operatorToken.path,
87
+ operatorTokenPresent: operatorToken.present && Boolean(operatorToken.token),
87
88
  };
88
89
  }
89
90
 
@@ -186,6 +186,9 @@ export async function prepareShellCliRuntime(
186
186
  configManager,
187
187
  homeDirectory: bootstrapHomeDirectory,
188
188
  });
189
+ const effectiveOperatorTokenPath = externalRuntime.operatorToken.present
190
+ ? externalRuntime.operatorToken.path
191
+ : operatorTokenPath;
189
192
  const statusOptions = {
190
193
  configManager,
191
194
  workingDirectory: bootstrapWorkingDir,
@@ -196,8 +199,8 @@ export async function prepareShellCliRuntime(
196
199
  userStorePresent: existsSync(userStorePath),
197
200
  bootstrapCredentialPath,
198
201
  bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
199
- operatorTokenPath,
200
- operatorTokenPresent: existsSync(operatorTokenPath),
202
+ operatorTokenPath: effectiveOperatorTokenPath,
203
+ operatorTokenPresent: externalRuntime.operatorToken.present,
201
204
  },
202
205
  service,
203
206
  externalRuntime,
@@ -1,8 +1,7 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
1
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
4
2
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
3
  import { SDK_VERSION } from '../version.ts';
4
+ import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
6
5
 
7
6
  type JsonRecord = Record<string, unknown>;
8
7
 
@@ -47,18 +46,6 @@ function resolveBaseUrl(configManager: Pick<ConfigManager, 'get'>): string {
47
46
  return `http://${host}:${Number.isFinite(port) ? port : 3421}`;
48
47
  }
49
48
 
50
- function readOperatorToken(homeDirectory: string): { readonly token: string | null; readonly path: string } {
51
- const path = join(homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
52
- if (!existsSync(path)) return { token: null, path };
53
- try {
54
- const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
55
- const token = isRecord(parsed) && typeof parsed.token === 'string' ? parsed.token : null;
56
- return { token, path };
57
- } catch {
58
- return { token: null, path };
59
- }
60
- }
61
-
62
49
  async function fetchJson(
63
50
  url: string,
64
51
  token: string | null,
@@ -90,7 +77,7 @@ export async function inspectCliExternalRuntime(
90
77
  options: CliExternalRuntimeInspectionOptions,
91
78
  ): Promise<CliExternalRuntimeSnapshot> {
92
79
  const baseUrl = resolveBaseUrl(options.configManager);
93
- const token = readOperatorToken(options.homeDirectory);
80
+ const token = readConnectedHostOperatorToken(options.homeDirectory);
94
81
  const timeoutMs = options.timeoutMs ?? 1500;
95
82
  const route = '/api/goodvibes-agent/knowledge/status' as const;
96
83
 
@@ -9,6 +9,7 @@ import { formatProviderModel, getModelIdFromProviderModel } from '../config/prov
9
9
  import { bootstrapRuntime } from '../runtime/bootstrap.ts';
10
10
  import { createRuntimeServices } from '../runtime/services.ts';
11
11
  import { createRuntimeStore } from '../runtime/store/index.ts';
12
+ import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
12
13
  import type { RuntimeServices } from '../runtime/services.ts';
13
14
  import { SecretsManager } from '../config/secrets.ts';
14
15
  import { RuntimeEventBus, type TurnEvent } from '@/runtime/index.ts';
@@ -190,14 +191,14 @@ export function readAuthPaths(runtime: CliCommandRuntime) {
190
191
  });
191
192
  const userStorePath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-users.json');
192
193
  const bootstrapCredentialPath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-bootstrap.txt');
193
- const operatorTokenPath = join(runtime.homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
194
+ const operatorToken = readConnectedHostOperatorToken(runtime.homeDirectory);
194
195
  return {
195
196
  userStorePath,
196
197
  userStorePresent: existsSync(userStorePath),
197
198
  bootstrapCredentialPath,
198
199
  bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
199
- operatorTokenPath,
200
- operatorTokenPresent: existsSync(operatorTokenPath),
200
+ operatorTokenPath: operatorToken.path,
201
+ operatorTokenPresent: operatorToken.present && Boolean(operatorToken.token),
201
202
  };
202
203
  }
203
204
 
@@ -21,7 +21,7 @@ export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void
21
21
  'Welcome To GoodVibes Agent',
22
22
  ' /setup - open Agent setup with current settings preloaded',
23
23
  ' /agent - open the Agent operator workspace',
24
- ' /knowledge - inspect isolated Agent Knowledge status, ask, and search',
24
+ ' /knowledge - inspect isolated Agent Knowledge status, ask/search, libraries, map, connectors, and ingest paths',
25
25
  ' /memory - manage Agent-local memory',
26
26
  ' /personas - manage Agent-local personas',
27
27
  ' /skills - manage reusable Agent-local skills',
@@ -209,9 +209,9 @@ function renderKnowledgeAskResult(result: KnowledgeAskResult): string {
209
209
  export const knowledgeCommand: SlashCommand = {
210
210
  name: 'knowledge',
211
211
  aliases: ['know', 'kb'],
212
- description: 'Agent Knowledge: isolated Agent-owned sources, graph, review queue, and compact prompt packets.',
212
+ description: 'Agent Knowledge: isolated Agent-owned status, ask/search, source/node/issue lists, item lookup, map, connectors, ingest, and review queue.',
213
213
  usage: '<subcommand> [args]',
214
- argsHint: 'status|ask|ingest-url --yes|ingest-file --yes|import-bookmarks --yes|list|search|get|queue|review-issue --yes',
214
+ argsHint: 'status|ask|search|list|get|map|connectors|connector|connector-doctor|ingest-url --yes|ingest-file --yes|import-urls --yes|import-bookmarks --yes|import-browser-history --yes|ingest-connector --yes|review-issue --yes|reindex --yes',
215
215
  handler: async (args: string[], context: CommandContext): Promise<void> => {
216
216
  if (args.length === 0 && context.openAgentWorkspace) {
217
217
  context.openAgentWorkspace('knowledge');
@@ -51,8 +51,8 @@ const GOODVIBES_AGENT_OPERATOR_POLICY = [
51
51
  '- Use the `agent_operator_briefing` tool for read-only main-conversation summaries of connected work plan, approvals, automation, schedules, and scheduler capacity. This tool must not call mutation routes, connected-host lifecycle routes, default knowledge, non-Agent knowledge spaces, separate Agent job creation, or GoodVibes TUI delegation.',
52
52
  '- When the user explicitly asks Agent to approve, deny, or cancel a specific approval, run/pause/resume a specific automation job, cancel/retry a specific automation run, or run a specific schedule, use the `agent_operator_action` tool with confirm:true and the original user request. It can call only its allowlisted public operator routes and must not create, edit, delete, or discover automation definitions.',
53
53
  '- Use the `agent_work_plan` tool to keep the visible Agent-local work plan current while working in the main conversation. Create, inspect, and update local work items proactively when useful. Removing items or clearing completed items requires an explicit user request and confirm:true.',
54
- '- Use the `agent_harness` tool to inspect and operate the harness surface itself: Agent workspace action catalog, slash command catalog, model tool catalog, connected-host posture, and Agent settings. Use it to change Agent settings, invoke workspace action mirrors, or invoke slash-command mirrors only when the user explicitly asks; preserve confirm:true and explicitUserRequest for mutations, keep secret-backed settings in the secret manager, and keep connected-host lifecycle/posture externally owned.',
55
- '- Use the `agent_knowledge` tool for Agent Knowledge status, ask, and search from the main conversation. It must use only /api/goodvibes-agent/knowledge/* and must fail closed instead of falling back to default knowledge or non-Agent knowledge spaces.',
54
+ '- Use the `agent_harness` tool to inspect and operate the harness surface itself: Agent workspace action catalog, slash command catalog, model tool catalog, release evidence bundle, release readiness inventory, connected-host posture, and Agent settings. Use it to change Agent settings, invoke workspace action mirrors, or invoke slash-command mirrors only when the user explicitly asks; preserve confirm:true and explicitUserRequest for mutations, keep secret-backed settings in the secret manager, and keep connected-host lifecycle/posture externally owned.',
55
+ '- Use the `agent_knowledge` tool for Agent Knowledge status, ask, search, source/node/issue lists, item lookup, map summary, connector list/detail, and connector doctor from the main conversation. It must use only /api/goodvibes-agent/knowledge/* and must fail closed instead of falling back to default knowledge or non-Agent knowledge spaces.',
56
56
  '- When the user explicitly asks Agent to add, import, remember, or ingest a URL, URL-list file, local file, bookmarks file, browser history, or connector input into Agent Knowledge, use the `agent_knowledge_ingest` tool with confirm:true and the original user request. It must write only to /api/goodvibes-agent/knowledge/* ingest routes and never to default knowledge or non-Agent knowledge spaces.',
57
57
  '- Use the `agent_local_registry` tool when a scratchpad note, durable memory, reusable persona, skill, skill bundle, or routine would improve later work. Keep those records Agent-local, non-secret, source/provenance tagged, and reviewable. Notes are for temporary/source-triage context; promote them explicitly into memory, skills, personas, routines, or Agent Knowledge only when that is the correct durable home. Review memory with a confidence score when it should shape later turns. Starting a routine means applying its steps in this same serial conversation, not creating a background job.',
58
58
  '- When the user explicitly asks Agent to generate an image, video, or other media artifact, use the `agent_media_generate` tool with confirm:true and the original user request. Generated media is stored as artifacts; do not print base64, call default knowledge, use non-Agent knowledge spaces, send channels, create separate Agent jobs, or request GoodVibes TUI delegation for media generation.',
@@ -20,6 +20,22 @@ export function connectedHostOperatorTokenPath(homeDirectory: string): string {
20
20
  }
21
21
 
22
22
  export function readConnectedHostOperatorToken(homeDirectory: string): ConnectedHostOperatorToken {
23
+ const connectedHostEnvToken = process.env.GOODVIBES_CONNECTED_HOST_TOKEN?.trim();
24
+ if (connectedHostEnvToken) {
25
+ return {
26
+ path: 'env:GOODVIBES_CONNECTED_HOST_TOKEN',
27
+ present: true,
28
+ token: connectedHostEnvToken,
29
+ };
30
+ }
31
+ const legacyEnvToken = process.env.GOODVIBES_DAEMON_TOKEN?.trim();
32
+ if (legacyEnvToken) {
33
+ return {
34
+ path: 'env:GOODVIBES_DAEMON_TOKEN',
35
+ present: true,
36
+ token: legacyEnvToken,
37
+ };
38
+ }
23
39
  const path = connectedHostOperatorTokenPath(homeDirectory);
24
40
  if (!existsSync(path)) return { path, present: false, token: null };
25
41
  try {
@@ -42,11 +42,9 @@ export function createAgentChannelSendTool(
42
42
  definition: {
43
43
  name: 'agent_channel_send',
44
44
  description: [
45
- 'Send one explicitly confirmed message through a configured GoodVibes Agent delivery target from the main conversation.',
46
- 'Use only when the user explicitly asks Agent to send, message, alert, or notify a channel target.',
47
- 'Exactly one target must be supplied: channel, route, webhook, or link.',
48
- 'This tool does not create channel routes, authorize accounts, manage connected-host hosting, use default knowledge, use non-Agent knowledge segments, create separate Agent jobs, run delegated review, or invoke arbitrary routes.',
49
- 'Set confirm:true only for an explicit user request. Otherwise return the preview/confirmation error.',
45
+ 'Send one confirmed message through a configured Agent target.',
46
+ 'Use only for explicit send/message/alert requests.',
47
+ 'Provide exactly one target.',
50
48
  ].join(' '),
51
49
  parameters: {
52
50
  type: 'object',
@@ -61,7 +59,7 @@ export function createAgentChannelSendTool(
61
59
  },
62
60
  channel: {
63
61
  type: 'string',
64
- description: 'Optional channel target surface[:route[:label]], such as slack:ops:Ops or telephony:+15551234567. Use exactly one target field.',
62
+ description: 'Optional channel target. Use exactly one target field.',
65
63
  },
66
64
  route: {
67
65
  type: 'string',
@@ -77,7 +75,7 @@ export function createAgentChannelSendTool(
77
75
  },
78
76
  confirm: {
79
77
  type: 'boolean',
80
- description: 'Required true only when the user explicitly asked for this exact channel delivery.',
78
+ description: 'Required true for confirmed delivery.',
81
79
  },
82
80
  explicitUserRequest: {
83
81
  type: 'string',
@@ -0,0 +1,177 @@
1
+ import type { CommandContext } from '../input/command-registry.ts';
2
+ import type { AgentWorkspaceChannelStatus } from '../input/agent-workspace-channels.ts';
3
+ import { buildAgentWorkspaceChannels } from '../input/agent-workspace-channels.ts';
4
+
5
+ export interface AgentHarnessChannelArgs {
6
+ readonly channelId?: unknown;
7
+ readonly target?: unknown;
8
+ readonly query?: unknown;
9
+ readonly includeParameters?: unknown;
10
+ readonly limit?: unknown;
11
+ }
12
+
13
+ type ChannelLookupSource = 'channelId' | 'target' | 'query';
14
+
15
+ type ChannelResolution =
16
+ | { readonly status: 'found'; readonly channel: Record<string, unknown> }
17
+ | { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
18
+ | { readonly status: 'missing_lookup'; readonly usage: string };
19
+
20
+ function readString(value: unknown): string {
21
+ return typeof value === 'string' ? value.trim() : '';
22
+ }
23
+
24
+ function readLimit(value: unknown, fallback: number): number {
25
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
26
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
27
+ return Math.max(1, Math.min(500, Math.trunc(parsed)));
28
+ }
29
+
30
+ function channelLookupFromArgs(args: AgentHarnessChannelArgs): { readonly source: ChannelLookupSource; readonly input: string } | null {
31
+ const channelId = readString(args.channelId);
32
+ if (channelId) return { source: 'channelId', input: channelId };
33
+ const target = readString(args.target);
34
+ if (target) return { source: 'target', input: target };
35
+ const query = readString(args.query);
36
+ return query ? { source: 'query', input: query } : null;
37
+ }
38
+
39
+ function channelSearchText(channel: AgentWorkspaceChannelStatus): string {
40
+ return [
41
+ channel.id,
42
+ channel.label,
43
+ channel.delivery,
44
+ channel.risk,
45
+ channel.riskLabel,
46
+ channel.setupState,
47
+ channel.nextStep,
48
+ ...channel.requiredKeys,
49
+ ...channel.missingRequiredKeys,
50
+ ...channel.defaultTargetKeys,
51
+ ...channel.configuredDefaultTargetKeys,
52
+ ].join('\n').toLowerCase();
53
+ }
54
+
55
+ function describeChannelCandidate(channel: AgentWorkspaceChannelStatus): Record<string, unknown> {
56
+ return {
57
+ channelId: channel.id,
58
+ label: channel.label,
59
+ setupState: channel.setupState,
60
+ ready: channel.ready,
61
+ delivery: channel.delivery,
62
+ };
63
+ }
64
+
65
+ function describeChannel(
66
+ channel: AgentWorkspaceChannelStatus,
67
+ options: { readonly includeParameters?: boolean; readonly lookup?: Record<string, unknown> } = {},
68
+ ): Record<string, unknown> {
69
+ return {
70
+ id: channel.id,
71
+ label: channel.label,
72
+ enabled: channel.enabled,
73
+ ready: channel.ready,
74
+ setupState: channel.setupState,
75
+ delivery: channel.delivery,
76
+ risk: channel.risk,
77
+ riskLabel: channel.riskLabel,
78
+ missingConfigCount: channel.missingConfigCount,
79
+ defaultTarget: channel.defaultTarget,
80
+ nextStep: channel.nextStep,
81
+ requiredConfigKeys: channel.requiredKeys,
82
+ missingConfigKeys: channel.missingRequiredKeys,
83
+ defaultTargetKeys: channel.defaultTargetKeys,
84
+ configuredDefaultTargetKeys: channel.configuredDefaultTargetKeys,
85
+ ...(options.lookup ? { lookup: options.lookup } : {}),
86
+ policy: {
87
+ effect: 'read-only',
88
+ values: 'Config key names and target key names are shown; secret values and stored target values are never returned.',
89
+ delivery: 'Use agent_channel_send only for one explicit, confirmed delivery target requested by the user.',
90
+ setup: 'Use connected-host setup/account/policy routes only as read-only diagnostics unless another confirmed first-class tool owns the mutation.',
91
+ },
92
+ modelAccess: {
93
+ sendTool: 'agent_channel_send',
94
+ notificationTool: 'agent_notify',
95
+ reminderTool: 'agent_reminder_schedule',
96
+ slashCommandDetail: `/channels show ${channel.id}`,
97
+ readOnlyConnectedRoutes: [
98
+ '/channels accounts',
99
+ '/channels policies',
100
+ '/channels status',
101
+ `/channels doctor ${channel.id}`,
102
+ `/channels setup ${channel.id}`,
103
+ ],
104
+ settingsFilter: `agent_harness mode:"settings" prefix:"surfaces.${channel.id}" includeHidden:true`,
105
+ connectedHostBoundary: 'agent_harness mode:"connected_host_capability" query:"delivery"',
106
+ ...(options.includeParameters ? {
107
+ deliveryTargetShape: 'surface[:route[:label]]',
108
+ exampleTarget: `${channel.id}:route:Label`,
109
+ } : {}),
110
+ },
111
+ };
112
+ }
113
+
114
+ export function channelReadinessCatalogStatus(context: CommandContext): Record<string, unknown> {
115
+ const channels = buildAgentWorkspaceChannels(context);
116
+ return {
117
+ modes: ['channels', 'channel'],
118
+ channels: channels.length,
119
+ enabled: channels.filter((channel) => channel.enabled).length,
120
+ ready: channels.filter((channel) => channel.ready).length,
121
+ attention: channels.filter((channel) => channel.enabled && channel.setupState !== 'ready').length,
122
+ readOnly: true,
123
+ deliveryTool: 'agent_channel_send',
124
+ };
125
+ }
126
+
127
+ export function listHarnessChannels(context: CommandContext, args: AgentHarnessChannelArgs): Record<string, unknown> {
128
+ const query = readString(args.query).toLowerCase();
129
+ const includeParameters = args.includeParameters === true;
130
+ const limit = readLimit(args.limit, 200);
131
+ const channels = buildAgentWorkspaceChannels(context);
132
+ const filtered = channels
133
+ .filter((channel) => !query || channelSearchText(channel).includes(query))
134
+ .slice(0, limit);
135
+ return {
136
+ channels: filtered.map((channel) => describeChannel(channel, { includeParameters })),
137
+ returned: filtered.length,
138
+ total: channels.length,
139
+ enabled: channels.filter((channel) => channel.enabled).length,
140
+ ready: channels.filter((channel) => channel.ready).length,
141
+ attention: channels.filter((channel) => channel.enabled && channel.setupState !== 'ready').length,
142
+ policy: 'Read-only channel readiness catalog. It returns key names, setup state, delivery posture, and model routes without printing secrets or sending messages.',
143
+ };
144
+ }
145
+
146
+ export function describeHarnessChannel(context: CommandContext, args: AgentHarnessChannelArgs): ChannelResolution {
147
+ const lookup = channelLookupFromArgs(args);
148
+ if (!lookup) {
149
+ return {
150
+ status: 'missing_lookup',
151
+ usage: 'channel requires channelId, target, or query. Use mode:"channels" to inspect available channel ids.',
152
+ };
153
+ }
154
+ const channels = buildAgentWorkspaceChannels(context);
155
+ const normalized = lookup.input.toLowerCase();
156
+ const exact = channels.find((channel) => channel.id === lookup.input);
157
+ if (exact) return { status: 'found', channel: describeChannel(exact, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'id' } }) };
158
+ const insensitiveId = channels.find((channel) => channel.id.toLowerCase() === normalized);
159
+ if (insensitiveId) return { status: 'found', channel: describeChannel(insensitiveId, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } }) };
160
+ const insensitiveLabel = channels.find((channel) => channel.label.toLowerCase() === normalized);
161
+ if (insensitiveLabel) return { status: 'found', channel: describeChannel(insensitiveLabel, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'case-insensitive-label' } }) };
162
+ const searched = channels.filter((channel) => channelSearchText(channel).includes(normalized));
163
+ if (searched.length === 1) {
164
+ return { status: 'found', channel: describeChannel(searched[0]!, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'search' } }) };
165
+ }
166
+ if (searched.length > 1) {
167
+ return {
168
+ status: 'ambiguous',
169
+ input: lookup.input,
170
+ candidates: searched.slice(0, 8).map(describeChannelCandidate),
171
+ };
172
+ }
173
+ return {
174
+ status: 'missing_lookup',
175
+ usage: `Unknown channel ${lookup.input}. Use mode:"channels" to inspect available channel ids.`,
176
+ };
177
+ }