@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.
- package/CHANGELOG.md +14 -0
- package/README.md +8 -4
- package/dist/package/main.js +14321 -12304
- package/docs/README.md +4 -2
- package/docs/channels-remote-and-api.md +4 -0
- package/docs/connected-host.md +2 -0
- package/docs/getting-started.md +6 -2
- package/docs/knowledge-artifacts-and-multimodal.md +5 -3
- package/docs/project-planning.md +3 -1
- package/docs/providers-and-routing.md +3 -0
- package/docs/release-and-publishing.md +7 -6
- package/docs/tools-and-commands.md +101 -2
- package/docs/voice-and-live-tts.md +2 -0
- package/package.json +1 -1
- package/src/agent/harness-control.ts +266 -0
- package/src/cli/help.ts +26 -0
- package/src/config/agent-settings-policy.ts +44 -0
- package/src/input/agent-workspace-activation.ts +14 -6
- package/src/input/commands/operator-runtime.ts +139 -3
- package/src/input/settings-modal-agent-policy.ts +5 -44
- package/src/runtime/bootstrap.ts +3 -0
- package/src/tools/agent-harness-cli-metadata.ts +152 -0
- package/src/tools/agent-harness-local-operations.ts +233 -0
- package/src/tools/agent-harness-metadata.ts +408 -0
- package/src/tools/agent-harness-model-tool-catalog.ts +36 -0
- package/src/tools/agent-harness-panel-metadata.ts +121 -0
- package/src/tools/agent-harness-tool.ts +798 -0
- package/src/tools/agent-local-registry-requirements.ts +18 -0
- package/src/tools/agent-local-registry-tool.ts +88 -34
- package/src/version.ts +1 -1
|
@@ -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 =
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
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(
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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';
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -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;
|