@pellux/goodvibes-agent 1.0.1 → 1.0.3

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.
@@ -0,0 +1,266 @@
1
+ import type { ConfigKey, ConfigManager, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
2
+ import { isValidConfigKey } from '@pellux/goodvibes-sdk/platform/config';
3
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
4
+ import type { SecretsManager } from '../config/secrets.ts';
5
+ import {
6
+ buildGoodVibesSecretKey,
7
+ isSecretConfigKey,
8
+ isSecretReferenceValue,
9
+ persistSecretBackedConfigValue,
10
+ } from '../config/secret-config.ts';
11
+ import {
12
+ AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON,
13
+ isAgentHiddenSettingKey,
14
+ isExternalHostOwnedSettingKey,
15
+ } from '../config/agent-settings-policy.ts';
16
+
17
+ export interface HarnessSettingFilters {
18
+ readonly key?: string;
19
+ readonly category?: string;
20
+ readonly prefix?: string;
21
+ readonly query?: string;
22
+ readonly includeHidden?: boolean;
23
+ readonly limit?: number;
24
+ }
25
+
26
+ export interface HarnessSettingDescriptor {
27
+ readonly key: string;
28
+ readonly category: string;
29
+ readonly type: ConfigSetting['type'];
30
+ readonly value: unknown;
31
+ readonly default: unknown;
32
+ readonly configured: boolean;
33
+ readonly writable: boolean;
34
+ readonly visibleInWorkspace: boolean;
35
+ readonly lockReason?: string;
36
+ readonly description: string;
37
+ readonly enumValues?: readonly string[];
38
+ }
39
+
40
+ export interface HarnessSettingMutationResult {
41
+ readonly key: string;
42
+ readonly action: 'set' | 'reset';
43
+ readonly previous: unknown;
44
+ readonly current: unknown;
45
+ }
46
+
47
+ const DEFAULT_SETTING_LIMIT = 100;
48
+ const SENSITIVE_KEY_PATTERN = /(?:secret|token|password|api[-_.]?key|signing)/i;
49
+
50
+ function valuesEqual(left: unknown, right: unknown): boolean {
51
+ return JSON.stringify(left) === JSON.stringify(right);
52
+ }
53
+
54
+ function clampLimit(value: unknown, fallback = DEFAULT_SETTING_LIMIT): number {
55
+ if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
56
+ return Math.max(1, Math.min(500, Math.trunc(value)));
57
+ }
58
+
59
+ function findSetting(configManager: Pick<ConfigManager, 'getSchema'>, rawKey: string): ConfigSetting | null {
60
+ if (!rawKey || !isValidConfigKey(rawKey)) return null;
61
+ return configManager.getSchema().find((setting) => setting.key === rawKey) ?? null;
62
+ }
63
+
64
+ export function redactHarnessSettingValue(key: string, value: unknown): unknown {
65
+ if (typeof value !== 'string') return value;
66
+ if (!value) return value;
67
+ if (isSecretConfigKey(key) || SENSITIVE_KEY_PATTERN.test(key)) {
68
+ if (isSecretReferenceValue(value)) return '<secret-ref>';
69
+ return '<redacted>';
70
+ }
71
+ return value;
72
+ }
73
+
74
+ export function describeHarnessSetting(
75
+ configManager: Pick<ConfigManager, 'get'>,
76
+ setting: ConfigSetting,
77
+ ): HarnessSettingDescriptor {
78
+ const value = configManager.get(setting.key as ConfigKey);
79
+ const hostOwned = isExternalHostOwnedSettingKey(setting.key);
80
+ return {
81
+ key: setting.key,
82
+ category: setting.key.split('.')[0] ?? '',
83
+ type: setting.type,
84
+ value: redactHarnessSettingValue(setting.key, value),
85
+ default: redactHarnessSettingValue(setting.key, setting.default),
86
+ configured: !valuesEqual(value, setting.default),
87
+ writable: !hostOwned,
88
+ visibleInWorkspace: !isAgentHiddenSettingKey(setting.key),
89
+ ...(hostOwned ? { lockReason: AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON } : {}),
90
+ description: setting.description,
91
+ ...(setting.enumValues ? { enumValues: setting.enumValues } : {}),
92
+ };
93
+ }
94
+
95
+ export function listHarnessSettings(
96
+ configManager: Pick<ConfigManager, 'get' | 'getSchema'>,
97
+ filters: HarnessSettingFilters = {},
98
+ ): readonly HarnessSettingDescriptor[] {
99
+ const key = filters.key?.trim();
100
+ const category = filters.category?.trim();
101
+ const prefix = filters.prefix?.trim();
102
+ const query = filters.query?.trim().toLowerCase();
103
+ const limit = clampLimit(filters.limit);
104
+
105
+ return configManager.getSchema()
106
+ .filter((setting) => {
107
+ if (key && setting.key !== key) return false;
108
+ if (category && setting.key.split('.')[0] !== category) return false;
109
+ if (prefix && !setting.key.startsWith(prefix)) return false;
110
+ if (!filters.includeHidden && isAgentHiddenSettingKey(setting.key)) return false;
111
+ if (query) {
112
+ const text = [setting.key, setting.description, setting.type, ...(setting.enumValues ?? [])].join('\n').toLowerCase();
113
+ if (!text.includes(query)) return false;
114
+ }
115
+ return true;
116
+ })
117
+ .map((setting) => describeHarnessSetting(configManager, setting))
118
+ .slice(0, limit);
119
+ }
120
+
121
+ export function getHarnessSetting(
122
+ configManager: Pick<ConfigManager, 'get' | 'getSchema'>,
123
+ key: string,
124
+ ): HarnessSettingDescriptor | null {
125
+ const setting = findSetting(configManager, key);
126
+ return setting ? describeHarnessSetting(configManager, setting) : null;
127
+ }
128
+
129
+ function coerceBoolean(value: unknown): boolean {
130
+ if (typeof value === 'boolean') return value;
131
+ if (typeof value === 'number') {
132
+ if (value === 1) return true;
133
+ if (value === 0) return false;
134
+ }
135
+ if (typeof value === 'string') {
136
+ const normalized = value.trim().toLowerCase();
137
+ if (['true', '1', 'yes', 'on', 'enabled'].includes(normalized)) return true;
138
+ if (['false', '0', 'no', 'off', 'disabled'].includes(normalized)) return false;
139
+ }
140
+ throw new Error(`Expected boolean value, got ${String(value)}.`);
141
+ }
142
+
143
+ export function coerceHarnessSettingValue(setting: ConfigSetting, value: unknown): unknown {
144
+ if (setting.type === 'boolean') return coerceBoolean(value);
145
+ if (setting.type === 'number') {
146
+ const parsed = typeof value === 'number' ? value : Number(String(value).trim());
147
+ if (!Number.isFinite(parsed)) throw new Error(`Expected numeric value for ${setting.key}.`);
148
+ return parsed;
149
+ }
150
+ if (setting.type === 'enum') {
151
+ const parsed = String(value).trim();
152
+ if (!setting.enumValues?.includes(parsed)) {
153
+ throw new Error(`Invalid value for ${setting.key}. Allowed: ${(setting.enumValues ?? []).join(', ')}.`);
154
+ }
155
+ return parsed;
156
+ }
157
+ return typeof value === 'string' ? value : String(value);
158
+ }
159
+
160
+ export async function setHarnessSetting(
161
+ configManager: ConfigManager,
162
+ secretsManager: Pick<SecretsManager, 'set' | 'delete'> | null | undefined,
163
+ key: string,
164
+ value: unknown,
165
+ ): Promise<HarnessSettingMutationResult> {
166
+ const setting = findSetting(configManager, key);
167
+ if (!setting) throw new Error(`Unknown setting ${key || '<missing>'}.`);
168
+ if (isExternalHostOwnedSettingKey(setting.key)) throw new Error(AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON);
169
+
170
+ const previous = configManager.get(setting.key as ConfigKey);
171
+ const coerced = coerceHarnessSettingValue(setting, value);
172
+ if (setting.type === 'string' && isSecretConfigKey(setting.key)) {
173
+ const secretValue = String(coerced);
174
+ if (secretValue.trim() && !isSecretReferenceValue(secretValue) && !secretsManager?.set) {
175
+ throw new Error(`Cannot store raw secret value for ${setting.key}: secrets manager is unavailable.`);
176
+ }
177
+ const current = await persistSecretBackedConfigValue(
178
+ configManager,
179
+ secretsManager,
180
+ setting.key as ConfigKey,
181
+ secretValue,
182
+ { scope: 'user' },
183
+ );
184
+ return {
185
+ key: setting.key,
186
+ action: 'set',
187
+ previous: redactHarnessSettingValue(setting.key, previous),
188
+ current: redactHarnessSettingValue(setting.key, current),
189
+ };
190
+ }
191
+
192
+ configManager.setDynamic(setting.key as ConfigKey, coerced);
193
+ return {
194
+ key: setting.key,
195
+ action: 'set',
196
+ previous: redactHarnessSettingValue(setting.key, previous),
197
+ current: redactHarnessSettingValue(setting.key, configManager.get(setting.key as ConfigKey)),
198
+ };
199
+ }
200
+
201
+ export async function resetHarnessSetting(
202
+ configManager: ConfigManager,
203
+ secretsManager: Pick<SecretsManager, 'delete'> | null | undefined,
204
+ key: string,
205
+ ): Promise<HarnessSettingMutationResult> {
206
+ const setting = findSetting(configManager, key);
207
+ if (!setting) throw new Error(`Unknown setting ${key || '<missing>'}.`);
208
+ if (isExternalHostOwnedSettingKey(setting.key)) throw new Error(AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON);
209
+
210
+ const previous = configManager.get(setting.key as ConfigKey);
211
+ if (isSecretConfigKey(setting.key)) {
212
+ if (typeof previous === 'string' && isSecretReferenceValue(previous) && !secretsManager?.delete) {
213
+ throw new Error(`Cannot reset ${setting.key}: secrets manager is unavailable to delete the stored secret.`);
214
+ }
215
+ await secretsManager?.delete?.(buildGoodVibesSecretKey(setting.key), { scope: 'user' });
216
+ }
217
+ configManager.reset(setting.key as ConfigKey);
218
+ return {
219
+ key: setting.key,
220
+ action: 'reset',
221
+ previous: redactHarnessSettingValue(setting.key, previous),
222
+ current: redactHarnessSettingValue(setting.key, configManager.get(setting.key as ConfigKey)),
223
+ };
224
+ }
225
+
226
+ export function formatHarnessSettingList(settings: readonly HarnessSettingDescriptor[]): string {
227
+ if (settings.length === 0) return 'No settings matched.';
228
+ return [
229
+ `Settings (${settings.length})`,
230
+ ...settings.map((setting) => {
231
+ const status = setting.writable ? 'writable' : 'read-only';
232
+ const visible = setting.visibleInWorkspace ? 'workspace' : 'scriptable';
233
+ return ` ${setting.key} ${setting.type} ${status}/${visible} current=${String(setting.value)}`;
234
+ }),
235
+ ].join('\n');
236
+ }
237
+
238
+ export function formatHarnessSetting(setting: HarnessSettingDescriptor | null): string {
239
+ if (!setting) return 'Unknown setting.';
240
+ return [
241
+ `Setting ${setting.key}`,
242
+ ` category ${setting.category}`,
243
+ ` type ${setting.type}`,
244
+ ` current ${String(setting.value)}`,
245
+ ` default ${String(setting.default)}`,
246
+ ` configured ${setting.configured ? 'yes' : 'no'}`,
247
+ ` writable ${setting.writable ? 'yes' : 'no'}`,
248
+ ` workspace visible ${setting.visibleInWorkspace ? 'yes' : 'no'}`,
249
+ ...(setting.enumValues ? [` values ${setting.enumValues.join(', ')}`] : []),
250
+ ...(setting.lockReason ? [` lock ${setting.lockReason}`] : []),
251
+ ` ${setting.description}`,
252
+ ].join('\n');
253
+ }
254
+
255
+ export function formatHarnessMutation(result: HarnessSettingMutationResult): string {
256
+ return [
257
+ `Setting ${result.action}`,
258
+ ` key ${result.key}`,
259
+ ` previous ${String(result.previous)}`,
260
+ ` current ${String(result.current)}`,
261
+ ].join('\n');
262
+ }
263
+
264
+ export function formatHarnessError(error: unknown): string {
265
+ return summarizeError(error);
266
+ }
package/src/cli/help.ts CHANGED
@@ -131,6 +131,15 @@ type CommandHelp = {
131
131
  readonly examples?: readonly string[];
132
132
  };
133
133
 
134
+ export interface GoodVibesCommandHelpDescriptor {
135
+ readonly command: string;
136
+ readonly aliases: readonly string[];
137
+ readonly summary: string;
138
+ readonly usage: readonly string[];
139
+ readonly subcommands: readonly string[];
140
+ readonly examples: readonly string[];
141
+ }
142
+
134
143
  const COMMAND_HELP: Record<string, CommandHelp> = {
135
144
  run: {
136
145
  usage: ['run [prompt] [--output text|json|stream-json]', 'exec [prompt]'],
@@ -416,6 +425,23 @@ export function hasGoodVibesCommandHelp(topic: string): boolean {
416
425
  return COMMAND_HELP[normalizeHelpTopic(topic)] !== undefined;
417
426
  }
418
427
 
428
+ export function describeGoodVibesCommandHelp(topic: string): GoodVibesCommandHelpDescriptor | null {
429
+ const normalized = normalizeHelpTopic(topic);
430
+ const help = COMMAND_HELP[normalized];
431
+ if (!help) return null;
432
+ return {
433
+ command: normalized,
434
+ aliases: Object.entries(HELP_ALIASES)
435
+ .filter(([, target]) => target === normalized)
436
+ .map(([alias]) => alias)
437
+ .sort(),
438
+ summary: help.summary,
439
+ usage: help.usage,
440
+ subcommands: help.subcommands ?? [],
441
+ examples: help.examples ?? [],
442
+ };
443
+ }
444
+
419
445
  export function renderGoodVibesCommandHelp(topic: string, binary = 'goodvibes-agent'): string {
420
446
  const normalized = normalizeHelpTopic(topic);
421
447
  const help = COMMAND_HELP[normalized];
@@ -0,0 +1,44 @@
1
+ export const AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON = 'GoodVibes Agent uses a connected GoodVibes host. Change host lifecycle and bind posture from the owning host; Agent settings are read-only for those controls.';
2
+
3
+ const AGENT_HIDDEN_SETTING_PREFIXES = [
4
+ ['cloud', 'flare.'].join(''),
5
+ ['surfaces.', 'home', 'assistant.'].join(''),
6
+ 'batch.',
7
+ 'controlPlane.',
8
+ 'danger.',
9
+ 'httpListener.',
10
+ 'network.',
11
+ 'orchestration.',
12
+ 'runtime.',
13
+ 'service.',
14
+ 'sandbox.',
15
+ 'web.',
16
+ 'watchers.',
17
+ 'wrfc.',
18
+ ] as const;
19
+
20
+ const AGENT_HIDDEN_SETTING_KEYS = new Set<string>([
21
+ 'ui.wrfcMessages',
22
+ ]);
23
+
24
+ const EXTERNAL_HOST_SETTING_PREFIXES = [
25
+ 'service.',
26
+ 'controlPlane.',
27
+ 'httpListener.',
28
+ 'web.',
29
+ ] as const;
30
+
31
+ const EXTERNAL_HOST_SETTING_KEYS = new Set<string>([
32
+ 'danger.daemon',
33
+ 'danger.httpListener',
34
+ ]);
35
+
36
+ export function isExternalHostOwnedSettingKey(key: string): boolean {
37
+ return EXTERNAL_HOST_SETTING_KEYS.has(key)
38
+ || EXTERNAL_HOST_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
39
+ }
40
+
41
+ export function isAgentHiddenSettingKey(key: string): boolean {
42
+ return AGENT_HIDDEN_SETTING_KEYS.has(key)
43
+ || AGENT_HIDDEN_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
44
+ }
@@ -9,12 +9,14 @@ import type {
9
9
  AgentWorkspaceActionResult,
10
10
  AgentWorkspaceCategory,
11
11
  AgentWorkspaceCommandDispatcher,
12
+ AgentWorkspaceEditorKind,
12
13
  AgentWorkspaceFocusPane,
13
14
  AgentWorkspaceLocalEditor,
14
15
  AgentWorkspaceLocalEditorKind,
15
16
  AgentWorkspaceLocalLibraryItem,
16
17
  AgentWorkspaceLocalOperation,
17
18
  AgentWorkspaceRuntimeSnapshot,
19
+ AgentWorkspaceRuntimeStarterTemplateItem,
18
20
  } from './agent-workspace-types.ts';
19
21
 
20
22
  interface AgentWorkspaceActivationHost {
@@ -55,7 +57,10 @@ export function activateAgentWorkspaceSelection(
55
57
  const action = workspace.selectedAction;
56
58
  if (!action) return;
57
59
  if (action.kind === 'editor' && action.editorKind) {
58
- const editor = createWorkspaceEditor(workspace, action.editorKind);
60
+ const editor = createAgentWorkspaceEditor(action.editorKind, {
61
+ runtimeStarterTemplates: workspace.runtimeSnapshot?.runtimeStarterTemplates ?? [],
62
+ selectedRoutine: workspace.selectedLocalLibraryItem('routine'),
63
+ });
59
64
  if (!editor) {
60
65
  workspace.status = `Editor unavailable: ${action.editorKind}.`;
61
66
  workspace.lastActionResult = {
@@ -143,11 +148,14 @@ export function activateAgentWorkspaceSelection(
143
148
  workspace.dispatchWorkspaceCommand(action.command);
144
149
  }
145
150
 
146
- function createWorkspaceEditor(
147
- workspace: AgentWorkspaceActivationHost,
148
- editorKind: NonNullable<AgentWorkspaceCategory['actions'][number]['editorKind']>,
151
+ export function createAgentWorkspaceEditor(
152
+ editorKind: AgentWorkspaceEditorKind,
153
+ options: {
154
+ readonly runtimeStarterTemplates?: readonly AgentWorkspaceRuntimeStarterTemplateItem[];
155
+ readonly selectedRoutine?: AgentWorkspaceLocalLibraryItem | null;
156
+ } = {},
149
157
  ): AgentWorkspaceLocalEditor | null {
150
- if (editorKind === 'profile') return createProfileEditor(workspace.runtimeSnapshot?.runtimeStarterTemplates ?? []);
158
+ if (editorKind === 'profile') return createProfileEditor(options.runtimeStarterTemplates ?? []);
151
159
  if (editorKind === 'learned-behavior') return createLearnedBehaviorEditor();
152
160
  if (editorKind === 'web-research') return createAgentWorkspaceWebResearchEditor('research');
153
161
  if (editorKind === 'web-fetch') return createAgentWorkspaceWebResearchEditor('fetch');
@@ -155,7 +163,7 @@ function createWorkspaceEditor(
155
163
  if (editorKind === 'knowledge-ask') return createAgentKnowledgeQueryEditor('ask');
156
164
  if (editorKind === 'knowledge-search') return createAgentKnowledgeQueryEditor('search');
157
165
  if (editorKind === 'reminder-schedule') return createReminderScheduleEditor();
158
- if (editorKind === 'routine-schedule') return createRoutineScheduleEditor(workspace.selectedLocalLibraryItem('routine'));
166
+ if (editorKind === 'routine-schedule') return createRoutineScheduleEditor(options.selectedRoutine ?? null);
159
167
  if (
160
168
  editorKind === 'memory'
161
169
  || editorKind === 'note'
@@ -2,14 +2,150 @@ import type { CommandRegistry } from '../command-registry.ts';
2
2
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
3
3
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
4
4
  import { requireYesFlag, stripYesFlag } from './confirmation.ts';
5
+ import {
6
+ formatHarnessError,
7
+ formatHarnessMutation,
8
+ formatHarnessSetting,
9
+ formatHarnessSettingList,
10
+ getHarnessSetting,
11
+ listHarnessSettings,
12
+ resetHarnessSetting,
13
+ setHarnessSetting,
14
+ } from '../../agent/harness-control.ts';
15
+
16
+ function readValuedFlag(args: readonly string[], flag: string): string | undefined {
17
+ const assignmentPrefix = `${flag}=`;
18
+ for (let index = 0; index < args.length; index += 1) {
19
+ const arg = args[index];
20
+ if (arg === flag) {
21
+ const value = args[index + 1];
22
+ return value && !value.startsWith('--') ? value : undefined;
23
+ }
24
+ if (arg?.startsWith(assignmentPrefix)) return arg.slice(assignmentPrefix.length);
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ function stripValuedFlag(args: readonly string[], flag: string): readonly string[] {
30
+ const next: string[] = [];
31
+ const assignmentPrefix = `${flag}=`;
32
+ for (let index = 0; index < args.length; index += 1) {
33
+ const arg = args[index];
34
+ if (arg === flag) {
35
+ const value = args[index + 1];
36
+ if (value && !value.startsWith('--')) index += 1;
37
+ continue;
38
+ }
39
+ if (arg?.startsWith(assignmentPrefix)) {
40
+ continue;
41
+ }
42
+ next.push(arg!);
43
+ }
44
+ return next;
45
+ }
46
+
47
+ function parseSettingListArgs(args: readonly string[]): {
48
+ readonly category?: string;
49
+ readonly prefix?: string;
50
+ readonly query?: string;
51
+ readonly includeHidden: boolean;
52
+ readonly limit?: number;
53
+ } {
54
+ const category = readValuedFlag(args, '--category');
55
+ const prefix = readValuedFlag(args, '--prefix');
56
+ const limitText = readValuedFlag(args, '--limit');
57
+ const remaining = stripValuedFlag(stripValuedFlag(stripValuedFlag(args, '--category'), '--prefix'), '--limit')
58
+ .filter((arg) => arg !== '--include-hidden');
59
+ const query = remaining.join(' ').trim();
60
+ return {
61
+ ...(category ? { category } : {}),
62
+ ...(prefix ? { prefix } : {}),
63
+ ...(query ? { query } : {}),
64
+ includeHidden: args.includes('--include-hidden'),
65
+ ...(limitText ? { limit: Number(limitText) } : {}),
66
+ };
67
+ }
5
68
 
6
69
  export function registerOperatorRuntimeCommands(registry: CommandRegistry): void {
7
70
  registry.register({
8
71
  name: 'settings',
9
72
  aliases: ['cfg-ui'],
10
- description: 'Open the fullscreen configuration workspace',
11
- handler(_args, ctx) {
12
- if (ctx.openSettingsModal) ctx.openSettingsModal();
73
+ description: 'Open, inspect, or update Agent settings',
74
+ usage: '[category|key|list|get <key>|set <key> <value> --yes|reset <key> --yes]',
75
+ argsHint: '[list|get|set|reset]',
76
+ async handler(args, ctx) {
77
+ const parsed = stripYesFlag(args);
78
+ const commandArgs = [...parsed.rest];
79
+ const sub = commandArgs[0];
80
+
81
+ if (!sub) {
82
+ if (ctx.openSettingsModal) ctx.openSettingsModal();
83
+ else ctx.print('Configuration workspace is not available in this runtime.');
84
+ return;
85
+ }
86
+
87
+ if (sub === 'list' || sub === 'schema') {
88
+ ctx.print(formatHarnessSettingList(listHarnessSettings(ctx.platform.configManager, parseSettingListArgs(commandArgs.slice(1)))));
89
+ return;
90
+ }
91
+
92
+ if (sub === 'get' || sub === 'show') {
93
+ const key = commandArgs[1];
94
+ if (!key) {
95
+ ctx.print('Usage: /settings get <key>');
96
+ return;
97
+ }
98
+ ctx.print(formatHarnessSetting(getHarnessSetting(ctx.platform.configManager, key)));
99
+ return;
100
+ }
101
+
102
+ if (sub === 'set') {
103
+ const key = commandArgs[1];
104
+ const rawValue = commandArgs.slice(2).join(' ');
105
+ if (!key || rawValue.length === 0) {
106
+ ctx.print('Usage: /settings set <key> <value> --yes');
107
+ return;
108
+ }
109
+ if (!parsed.yes) {
110
+ requireYesFlag(ctx, `set setting ${key}`, '/settings set <key> <value> --yes');
111
+ return;
112
+ }
113
+ try {
114
+ const result = await setHarnessSetting(ctx.platform.configManager, ctx.platform.secretsManager, key, rawValue);
115
+ ctx.print(formatHarnessMutation(result));
116
+ ctx.renderRequest();
117
+ } catch (error) {
118
+ ctx.print(formatHarnessError(error));
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (sub === 'reset') {
124
+ const key = commandArgs[1];
125
+ if (!key) {
126
+ ctx.print('Usage: /settings reset <key> --yes');
127
+ return;
128
+ }
129
+ if (!parsed.yes) {
130
+ requireYesFlag(ctx, `reset setting ${key}`, '/settings reset <key> --yes');
131
+ return;
132
+ }
133
+ try {
134
+ const result = await resetHarnessSetting(ctx.platform.configManager, ctx.platform.secretsManager, key);
135
+ ctx.print(formatHarnessMutation(result));
136
+ ctx.renderRequest();
137
+ } catch (error) {
138
+ ctx.print(formatHarnessError(error));
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (sub.includes('.')) {
144
+ ctx.print(formatHarnessSetting(getHarnessSetting(ctx.platform.configManager, sub)));
145
+ return;
146
+ }
147
+
148
+ if (ctx.openSettingsModal) ctx.openSettingsModal(sub);
13
149
  else ctx.print('Configuration workspace is not available in this runtime.');
14
150
  },
15
151
  });
@@ -1,44 +1,5 @@
1
- export const AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON = 'GoodVibes Agent uses a connected GoodVibes host. Change host lifecycle and bind posture from the owning host; Agent settings are read-only for those controls.';
2
-
3
- const AGENT_HIDDEN_SETTING_PREFIXES = [
4
- ['cloud', 'flare.'].join(''),
5
- ['surfaces.', 'home', 'assistant.'].join(''),
6
- 'batch.',
7
- 'controlPlane.',
8
- 'danger.',
9
- 'httpListener.',
10
- 'network.',
11
- 'orchestration.',
12
- 'runtime.',
13
- 'service.',
14
- 'sandbox.',
15
- 'web.',
16
- 'watchers.',
17
- 'wrfc.',
18
- ] as const;
19
-
20
- const AGENT_HIDDEN_SETTING_KEYS = new Set<string>([
21
- 'ui.wrfcMessages',
22
- ]);
23
-
24
- const EXTERNAL_HOST_SETTING_PREFIXES = [
25
- 'service.',
26
- 'controlPlane.',
27
- 'httpListener.',
28
- 'web.',
29
- ] as const;
30
-
31
- const EXTERNAL_HOST_SETTING_KEYS = new Set<string>([
32
- 'danger.daemon',
33
- 'danger.httpListener',
34
- ]);
35
-
36
- export function isExternalHostOwnedSettingKey(key: string): boolean {
37
- return EXTERNAL_HOST_SETTING_KEYS.has(key)
38
- || EXTERNAL_HOST_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
39
- }
40
-
41
- export function isAgentHiddenSettingKey(key: string): boolean {
42
- return AGENT_HIDDEN_SETTING_KEYS.has(key)
43
- || AGENT_HIDDEN_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
44
- }
1
+ export {
2
+ AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON,
3
+ isAgentHiddenSettingKey,
4
+ isExternalHostOwnedSettingKey,
5
+ } from '../config/agent-settings-policy.ts';
@@ -42,6 +42,7 @@ import { buildActivePersonaPrompt } from '../agent/persona-registry.ts';
42
42
  import { buildEnabledSkillsPrompt } from '../agent/skill-registry.ts';
43
43
  import { buildEnabledRoutinesPrompt } from '../agent/routine-registry.ts';
44
44
  import { buildReviewedMemoryPrompt } from '../agent/memory-prompt.ts';
45
+ import { registerAgentHarnessTool } from '../tools/agent-harness-tool.ts';
45
46
 
46
47
  const GOODVIBES_AGENT_OPERATOR_POLICY = [
47
48
  '## GoodVibes Agent Operator Policy',
@@ -50,6 +51,7 @@ const GOODVIBES_AGENT_OPERATOR_POLICY = [
50
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.',
51
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.',
52
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.',
53
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
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.',
55
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.',
@@ -285,6 +287,7 @@ export async function bootstrapRuntime(
285
287
  const commandRegistry = shell.commandRegistry;
286
288
  const commandContext = shell.commandContext;
287
289
  const inputHistory = shell.inputHistory;
290
+ registerAgentHarnessTool(toolRegistry, commandRegistry, commandContext);
288
291
  const pluginCommandRegistry = {
289
292
  register(command: {
290
293
  readonly name: string;