@pellux/goodvibes-agent 1.9.1 → 1.10.0
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 +10 -0
- package/README.md +12 -1
- package/dist/package/main.js +36712 -28803
- package/dist/package/{web-tree-sitter-jbz042ba.wasm → web-tree-sitter-e011xaqr.wasm} +0 -0
- package/docs/README.md +1 -1
- package/docs/connected-host.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/tools-and-commands.md +1 -0
- package/package.json +3 -3
- package/release/live-verification/live-verification.json +11 -11
- package/release/live-verification/live-verification.md +12 -12
- package/src/agent/email/email-service.ts +1 -1
- package/src/agent/email/imap-client.ts +4 -4
- package/src/agent/email/smtp-client.ts +5 -5
- package/src/cli/config-overrides.ts +29 -14
- package/src/cli/entrypoint.ts +12 -4
- package/src/cli/launch-auto-update.ts +218 -0
- package/src/cli/relay-command.ts +4 -4
- package/src/cli/service-posture.ts +2 -2
- package/src/cli/tui-startup.ts +31 -0
- package/src/cli/workspaces-command.ts +5 -1
- package/src/cli-flags.ts +1 -1
- package/src/config/index.ts +1 -1
- package/src/config/update-settings.ts +45 -0
- package/src/config/workspace-registration.ts +214 -15
- package/src/input/commands/runtime-services.ts +0 -5
- package/src/input/commands/update-runtime.ts +313 -0
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-feed.ts +8 -8
- package/src/input/handler.ts +1 -1
- package/src/input/mcp-workspace.ts +5 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/settings-modal-types.ts +11 -5
- package/src/input/settings-modal.ts +56 -61
- package/src/main.ts +19 -20
- package/src/renderer/activity-sidebar.ts +53 -4
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +37 -25
- package/src/runtime/bootstrap-core.ts +16 -5
- package/src/runtime/bootstrap-external-services.ts +107 -11
- package/src/runtime/bootstrap-hook-bridge.ts +2 -2
- package/src/runtime/bootstrap-shell.ts +4 -4
- package/src/runtime/bootstrap.ts +34 -12
- package/src/runtime/connected-host-autostart.ts +269 -0
- package/src/runtime/daemon-receipts.ts +66 -0
- package/src/runtime/diagnostics/panels/index.ts +0 -2
- package/src/runtime/feature-enablement.ts +176 -0
- package/src/runtime/index.ts +12 -29
- package/src/runtime/memory-spine-adoption.ts +18 -0
- package/src/runtime/onboarding/apply.ts +50 -37
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/onboarding/verify.ts +22 -4
- package/src/runtime/release-artifacts.ts +113 -0
- package/src/runtime/services.ts +228 -19
- package/src/runtime/session-spine-rest-transport.ts +84 -4
- package/src/runtime/ui-services.ts +1 -1
- package/src/runtime/update-check.ts +64 -0
- package/src/shell/ui-openers.ts +8 -0
- package/src/tools/agent-harness-metadata.ts +8 -0
- package/src/tools/agent-harness-setup-connected-host.ts +1 -1
- package/src/tools/agent-harness-setup-posture.ts +1 -1
- package/src/version.ts +1 -1
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/surface-feature-flags.ts +0 -100
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connected-host receipt delivery.
|
|
3
|
+
*
|
|
4
|
+
* The daemon keeps honesty receipts ("updated from X to Y", "restarted
|
|
5
|
+
* after a crash at HH:MM", settings-migration notes) and delivers the
|
|
6
|
+
* undelivered ones ONLY to a `/status` read that opts in with
|
|
7
|
+
* `?receipts=consume` — a plain `/status` read is receipt-neutral. Delivery is
|
|
8
|
+
* destructive at the daemon (served exactly once to the consuming reader), so
|
|
9
|
+
* whichever read consumes them must render them or they are lost. The agent's
|
|
10
|
+
* consuming reader is a single `?receipts=consume` read issued once per attach
|
|
11
|
+
* (createSpineReceiptConsumer in session-spine-rest-transport.ts, wired through
|
|
12
|
+
* services.consumeDaemonReceipts); the frequent liveness keepalive probe stays
|
|
13
|
+
* plain and never touches receipts. Every consumed payload is pushed here.
|
|
14
|
+
*
|
|
15
|
+
* This feed buffers receipts captured before the renderer exists (the first
|
|
16
|
+
* consuming read can fire during boot, before the render sink attaches) and
|
|
17
|
+
* delivers each receipt exactly once (dedupe by id) as soon as — and whenever —
|
|
18
|
+
* a delivery sink is attached.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** One daemon honesty receipt, as served on the /status body. */
|
|
22
|
+
export interface DaemonReceipt {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly text: string;
|
|
25
|
+
readonly at: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Parse the receipts array off a /status response body; [] when absent/malformed. */
|
|
29
|
+
export function extractDaemonReceipts(body: unknown): DaemonReceipt[] {
|
|
30
|
+
if (typeof body !== 'object' || body === null) return [];
|
|
31
|
+
const receipts = (body as { receipts?: unknown }).receipts;
|
|
32
|
+
if (!Array.isArray(receipts)) return [];
|
|
33
|
+
const parsed: DaemonReceipt[] = [];
|
|
34
|
+
for (const entry of receipts) {
|
|
35
|
+
if (typeof entry !== 'object' || entry === null) continue;
|
|
36
|
+
const { id, text, at } = entry as { id?: unknown; text?: unknown; at?: unknown };
|
|
37
|
+
if (typeof id !== 'string' || id.length === 0 || typeof text !== 'string' || text.length === 0) continue;
|
|
38
|
+
parsed.push({ id, text, at: typeof at === 'number' ? at : Date.now() });
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class AgentDaemonReceiptFeed {
|
|
44
|
+
private readonly buffered: DaemonReceipt[] = [];
|
|
45
|
+
private readonly seen = new Set<string>();
|
|
46
|
+
private deliver: ((receipt: DaemonReceipt) => void) | null = null;
|
|
47
|
+
|
|
48
|
+
/** Ingest receipts from a /status read; buffers until a sink attaches. */
|
|
49
|
+
push(receipts: readonly DaemonReceipt[]): void {
|
|
50
|
+
for (const receipt of receipts) {
|
|
51
|
+
if (this.seen.has(receipt.id)) continue;
|
|
52
|
+
this.seen.add(receipt.id);
|
|
53
|
+
if (this.deliver) {
|
|
54
|
+
this.deliver(receipt);
|
|
55
|
+
} else {
|
|
56
|
+
this.buffered.push(receipt);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Attach the rendering sink; anything captured earlier flushes immediately. */
|
|
62
|
+
attach(deliver: (receipt: DaemonReceipt) => void): void {
|
|
63
|
+
this.deliver = deliver;
|
|
64
|
+
for (const receipt of this.buffered.splice(0)) deliver(receipt);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -17,7 +17,5 @@ export type { PolicyDiagnosticsSnapshot } from './policy.ts';
|
|
|
17
17
|
export { ToolContractsPanel } from '@/runtime/index.ts';
|
|
18
18
|
export { TransportPanel } from '@/runtime/index.ts';
|
|
19
19
|
export type { TransportPanelSnapshot } from '@/runtime/index.ts';
|
|
20
|
-
export { OpsPanel } from './ops.ts';
|
|
21
|
-
export type { OpsAuditEntry } from './ops.ts';
|
|
22
20
|
export { SecurityPanel } from '@/runtime/index.ts';
|
|
23
21
|
export type { SecurityPanelSnapshot } from '@/runtime/index.ts';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feature-enablement.ts — explicit feature on/off requests over the dissolved
|
|
3
|
+
* feature model.
|
|
4
|
+
*
|
|
5
|
+
* The SDK dissolved the feature-flag category: every capability is enabled
|
|
6
|
+
* through a first-class settings key in its natural domain (FEATURE_SETTINGS
|
|
7
|
+
* describes each feature's binding). This module turns an EXPLICIT
|
|
8
|
+
* present-tense request — "enable feature X" from the CLI or an onboarding
|
|
9
|
+
* plan — into the domain-key write that honestly fulfills it:
|
|
10
|
+
* - boolean bindings write the key true/false;
|
|
11
|
+
* - enum bindings write the canonical enabled value (first enabledValues
|
|
12
|
+
* entry) or a schema-honest disabled value;
|
|
13
|
+
* - constant bindings have no separate off switch, so the request fails with
|
|
14
|
+
* the real settings keys to configure instead.
|
|
15
|
+
*
|
|
16
|
+
* This intentionally differs from the SDK's load-time MIGRATION of persisted
|
|
17
|
+
* legacy featureFlags entries (migrateLegacyFeatureToggles preserves
|
|
18
|
+
* historical AND-of-both-switches ambiguity); an explicit request has no such
|
|
19
|
+
* ambiguity — the operator wants the feature on or off now.
|
|
20
|
+
*
|
|
21
|
+
* Legacy `featureFlags` / `featureFlags.<id>` set-config keys from stored
|
|
22
|
+
* onboarding plans are accepted and expanded here so old plans keep working
|
|
23
|
+
* against the domain keys.
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
FEATURE_SETTINGS,
|
|
27
|
+
deriveFeatureState,
|
|
28
|
+
getFeatureSettingsBinding,
|
|
29
|
+
} from '@/runtime/index.ts';
|
|
30
|
+
import type { FeatureSetting } from '@/runtime/index.ts';
|
|
31
|
+
import { CONFIG_SCHEMA } from '@pellux/goodvibes-sdk/platform/config';
|
|
32
|
+
import type { ConfigKey, ConfigManager, ConfigSetting } from '../config/index.ts';
|
|
33
|
+
|
|
34
|
+
export type LegacyFeatureConfigKey = 'featureFlags' | `featureFlags.${string}`;
|
|
35
|
+
|
|
36
|
+
export type FeatureEnablementRequest = 'enabled' | 'disabled';
|
|
37
|
+
|
|
38
|
+
/** One domain-key write that fulfills an explicit feature request. */
|
|
39
|
+
export interface FeatureEnablementWrite {
|
|
40
|
+
readonly key: ConfigKey;
|
|
41
|
+
readonly value: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FEATURE_SETTINGS_BY_ID: ReadonlyMap<string, FeatureSetting> = new Map(
|
|
45
|
+
FEATURE_SETTINGS.map((feature) => [feature.id, feature]),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const CONFIG_SCHEMA_BY_KEY: ReadonlyMap<string, ConfigSetting> = new Map(
|
|
49
|
+
CONFIG_SCHEMA.map((setting) => [setting.key, setting]),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
53
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isLegacyFeatureConfigKey(key: string): key is LegacyFeatureConfigKey {
|
|
57
|
+
return key === 'featureFlags' || key.startsWith('featureFlags.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isFeatureEnablementRequest(value: unknown): value is FeatureEnablementRequest {
|
|
61
|
+
return value === 'enabled' || value === 'disabled';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getFeatureSetting(featureId: string): FeatureSetting | null {
|
|
65
|
+
return FEATURE_SETTINGS_BY_ID.get(featureId) ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function knownFeatureIdsHint(): string {
|
|
69
|
+
return FEATURE_SETTINGS.map((feature) => feature.id).join(', ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** An enum key's honest "disabled" value: the schema default when it is not an enabled value, else the first non-enabled option. */
|
|
73
|
+
function resolveEnumDisabledValue(feature: FeatureSetting): string {
|
|
74
|
+
const enabledValues = feature.enablement.enabledValues ?? [];
|
|
75
|
+
const schema = CONFIG_SCHEMA_BY_KEY.get(feature.enablement.key);
|
|
76
|
+
const schemaDefault = typeof schema?.default === 'string' ? schema.default : undefined;
|
|
77
|
+
if (schemaDefault !== undefined && !enabledValues.includes(schemaDefault)) return schemaDefault;
|
|
78
|
+
const candidate = (schema?.enumValues ?? []).find((value) => !enabledValues.includes(value));
|
|
79
|
+
if (candidate !== undefined) return candidate;
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Feature "${feature.id}" cannot be disabled through ${feature.enablement.key}: `
|
|
82
|
+
+ 'every allowed value keeps it enabled.',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The domain-key write that fulfills an explicit enable/disable request for
|
|
88
|
+
* one feature. Throws with real guidance for unknown ids and for constant
|
|
89
|
+
* bindings (whose own domain keys govern activation directly).
|
|
90
|
+
*/
|
|
91
|
+
export function resolveFeatureEnablementWrite(
|
|
92
|
+
featureId: string,
|
|
93
|
+
desired: FeatureEnablementRequest,
|
|
94
|
+
): FeatureEnablementWrite {
|
|
95
|
+
const feature = FEATURE_SETTINGS_BY_ID.get(featureId);
|
|
96
|
+
if (!feature) {
|
|
97
|
+
throw new Error(`Unknown feature "${featureId}". Known features: ${knownFeatureIdsHint()}.`);
|
|
98
|
+
}
|
|
99
|
+
const { key, kind, enabledValues } = feature.enablement;
|
|
100
|
+
if (kind === 'boolean') {
|
|
101
|
+
return { key, value: desired === 'enabled' };
|
|
102
|
+
}
|
|
103
|
+
if (kind === 'enum') {
|
|
104
|
+
if (desired === 'enabled') {
|
|
105
|
+
const canonical = enabledValues?.[0];
|
|
106
|
+
if (canonical === undefined) {
|
|
107
|
+
throw new Error(`Feature "${featureId}" has no enabled value declared for ${key}.`);
|
|
108
|
+
}
|
|
109
|
+
return { key, value: canonical };
|
|
110
|
+
}
|
|
111
|
+
return { key, value: resolveEnumDisabledValue(feature) };
|
|
112
|
+
}
|
|
113
|
+
// constant: always available; its associated domain keys govern activation.
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Feature "${featureId}" is always available and has no separate on/off switch; `
|
|
116
|
+
+ `configure its settings directly: ${feature.settings.join(', ')}.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Whether a feature is currently enabled, derived from its bound domain settings key. */
|
|
121
|
+
export function isFeatureEnabledInConfig(
|
|
122
|
+
config: Pick<ConfigManager, 'get'>,
|
|
123
|
+
featureId: string,
|
|
124
|
+
): boolean {
|
|
125
|
+
const binding = getFeatureSettingsBinding(featureId);
|
|
126
|
+
if (!binding) return false;
|
|
127
|
+
return deriveFeatureState(binding, config.get(binding.key)) === 'enabled';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Expand a legacy `featureFlags` / `featureFlags.<id>` set-config value into
|
|
132
|
+
* the domain-key writes that fulfill it. Validates shape with the historical
|
|
133
|
+
* error wording; unknown ids and constant bindings throw with real guidance.
|
|
134
|
+
*/
|
|
135
|
+
export function expandLegacyFeatureConfigValue(
|
|
136
|
+
key: LegacyFeatureConfigKey,
|
|
137
|
+
value: unknown,
|
|
138
|
+
): readonly FeatureEnablementWrite[] {
|
|
139
|
+
if (key === 'featureFlags') {
|
|
140
|
+
if (!isRecord(value)) throw new Error('featureFlags expects an object value.');
|
|
141
|
+
const writes: FeatureEnablementWrite[] = [];
|
|
142
|
+
for (const [featureId, state] of Object.entries(value)) {
|
|
143
|
+
if (featureId.trim().length === 0) throw new Error('featureFlags cannot contain an empty feature id.');
|
|
144
|
+
if (!isFeatureEnablementRequest(state)) {
|
|
145
|
+
throw new Error(`featureFlags.${featureId} expects enabled or disabled.`);
|
|
146
|
+
}
|
|
147
|
+
writes.push(resolveFeatureEnablementWrite(featureId, state));
|
|
148
|
+
}
|
|
149
|
+
return writes;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const featureId = key.slice('featureFlags.'.length);
|
|
153
|
+
if (featureId.trim().length === 0) throw new Error('featureFlags requires a feature id.');
|
|
154
|
+
if (!isFeatureEnablementRequest(value)) {
|
|
155
|
+
throw new Error(`Config key ${key} expects enabled or disabled.`);
|
|
156
|
+
}
|
|
157
|
+
return [resolveFeatureEnablementWrite(featureId, value)];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The feature ids a legacy set-config operation addresses (for verification:
|
|
162
|
+
* each id's DERIVED state must match the requested one after apply).
|
|
163
|
+
*/
|
|
164
|
+
export function legacyFeatureConfigTargets(
|
|
165
|
+
key: LegacyFeatureConfigKey,
|
|
166
|
+
value: unknown,
|
|
167
|
+
): ReadonlyArray<{ readonly featureId: string; readonly desired: FeatureEnablementRequest }> {
|
|
168
|
+
if (key === 'featureFlags') {
|
|
169
|
+
if (!isRecord(value)) return [];
|
|
170
|
+
return Object.entries(value)
|
|
171
|
+
.filter((entry): entry is [string, FeatureEnablementRequest] => isFeatureEnablementRequest(entry[1]))
|
|
172
|
+
.map(([featureId, desired]) => ({ featureId, desired }));
|
|
173
|
+
}
|
|
174
|
+
const featureId = key.slice('featureFlags.'.length);
|
|
175
|
+
return isFeatureEnablementRequest(value) ? [{ featureId, desired: value }] : [];
|
|
176
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -13,10 +13,6 @@ import {
|
|
|
13
13
|
shell,
|
|
14
14
|
transport,
|
|
15
15
|
} from '@pellux/goodvibes-sdk/platform/runtime';
|
|
16
|
-
import {
|
|
17
|
-
createFeatureFlagManager as createSdkFeatureFlagManager,
|
|
18
|
-
FEATURE_FLAG_MAP,
|
|
19
|
-
} from '@pellux/goodvibes-sdk/platform/runtime/state';
|
|
20
16
|
import { existsSync, statSync } from 'node:fs';
|
|
21
17
|
import { join, resolve } from 'node:path';
|
|
22
18
|
import type {
|
|
@@ -26,7 +22,6 @@ import type {
|
|
|
26
22
|
shell as Shell,
|
|
27
23
|
transport as Transport,
|
|
28
24
|
} from '@pellux/goodvibes-sdk/platform/runtime';
|
|
29
|
-
import type { FeatureFlagManager, FlagConfig } from '@pellux/goodvibes-sdk/platform/runtime/state';
|
|
30
25
|
|
|
31
26
|
// Local runtime entry points.
|
|
32
27
|
export { bootstrapRuntime } from './bootstrap.ts';
|
|
@@ -35,22 +30,10 @@ export type { BootstrapContext } from './bootstrap.ts';
|
|
|
35
30
|
export { createUiRuntimeServices } from './ui-services.ts';
|
|
36
31
|
export type { UiRuntimeServices } from './ui-services.ts';
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
manager.loadFromConfig = (config: FlagConfig): void => {
|
|
43
|
-
originalLoadFromConfig({
|
|
44
|
-
flags: Object.fromEntries(
|
|
45
|
-
Object.entries(config.flags).filter(([id]) => FEATURE_FLAG_MAP.has(id)),
|
|
46
|
-
) as FlagConfig['flags'],
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return manager;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Public runtime exports.
|
|
33
|
+
// Public runtime exports. createFeatureFlagManager comes straight from the
|
|
34
|
+
// SDK state surface below: gate states are seeded from domain settings keys
|
|
35
|
+
// via deriveFeatureStates (exported there too), so the old local wrapper that
|
|
36
|
+
// filtered unknown persisted featureFlags-category ids has nothing to filter.
|
|
54
37
|
export * from '@pellux/goodvibes-sdk/platform/runtime/state';
|
|
55
38
|
export * from '@pellux/goodvibes-sdk/platform/runtime/store';
|
|
56
39
|
export * from '@pellux/goodvibes-sdk/platform/runtime/ui';
|
|
@@ -101,9 +84,10 @@ export const registerHostRuntimeEvents = bootstrap.registerHostRuntimeEvents;
|
|
|
101
84
|
// Shared adopt-or-spawn policy (daemon-adoption-policy.ts): probes the
|
|
102
85
|
// configured host/port, band-checks any GoodVibes daemon found there, and
|
|
103
86
|
// only ever ADOPTS a compatible one — Agent passes `adoptOnly: true` at the
|
|
104
|
-
// call site (bootstrap.ts) so it never spawns or embeds a
|
|
105
|
-
//
|
|
106
|
-
//
|
|
87
|
+
// call site (bootstrap-external-services.ts) so it never spawns or embeds a
|
|
88
|
+
// daemon itself. A daemon that is INSTALLED on this machine but stopped is
|
|
89
|
+
// handled separately at boot: one start through the platform service manager,
|
|
90
|
+
// then a fresh adopt-only probe (see runtime/connected-host-autostart.ts).
|
|
107
91
|
export const startHostServices = bootstrap.startHostServices;
|
|
108
92
|
export const startExternalServices = bootstrap.startHostServices;
|
|
109
93
|
export const registerBootstrapHookBridge = bootstrap.registerBootstrapHookBridge;
|
|
@@ -223,10 +207,10 @@ export type RemoteRuntimeEventsOptions = Transport.RemoteRuntimeEventsOptions;
|
|
|
223
207
|
export type RemoteRuntimeEvents = Transport.RemoteRuntimeEvents;
|
|
224
208
|
export type SerializedRuntimeEnvelope = Transport.SerializedRuntimeEnvelope;
|
|
225
209
|
|
|
226
|
-
// Operations compatibility aliases.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
210
|
+
// Operations compatibility aliases. (OpsControlPlane and its error classes
|
|
211
|
+
// are deliberately NOT re-exported: the Agent never constructs the ops
|
|
212
|
+
// intervention plane — connected-host tasks are read-only by product policy,
|
|
213
|
+
// with mutations routed to /workplan and /delegate.)
|
|
230
214
|
export const ToolContractVerifier = operations.ToolContractVerifier;
|
|
231
215
|
export const McpLifecycleManager = operations.McpLifecycleManager;
|
|
232
216
|
export const McpPermissionManager = operations.McpPermissionManager;
|
|
@@ -354,7 +338,6 @@ export type DistributedRuntimeSnapshotStore = Operations.DistributedRuntimeSnaps
|
|
|
354
338
|
export type RemoteRunnerRegistry = Operations.RemoteRunnerRegistry;
|
|
355
339
|
export type RemoteSupervisor = Operations.RemoteSupervisor;
|
|
356
340
|
export type DistributedRuntimeManager = Operations.DistributedRuntimeManager;
|
|
357
|
-
export type OpsControlPlane = Operations.OpsControlPlane;
|
|
358
341
|
export type RuntimeTransitionResult = Operations.RuntimeTransitionResult;
|
|
359
342
|
export type RetentionClass = Operations.RetentionClass;
|
|
360
343
|
export type RetentionClassConfig = Operations.RetentionClassConfig;
|
|
@@ -22,6 +22,18 @@ export interface MemorySpineAdoptionOptions {
|
|
|
22
22
|
/** The existing daemon-adoption signal — reuse services.sessionSpineClient.probeReachability() in production. */
|
|
23
23
|
readonly probeReachability: () => Promise<'unknown' | 'online' | 'offline'>;
|
|
24
24
|
readonly deactivateReason?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Called exactly on the transition INTO adoption (reachable AND not yet
|
|
27
|
+
* active) — i.e. once per (re)attach to a daemon, at boot and again whenever a
|
|
28
|
+
* daemon reappears after a loss. The agent wires this to the single
|
|
29
|
+
* `?receipts=consume` /status read the daemon delivers its one-shot honesty
|
|
30
|
+
* receipts to (see services.consumeDaemonReceipts / daemon-receipts.ts):
|
|
31
|
+
* reusing the existing adoption edge instead of inventing a second signal, per
|
|
32
|
+
* the same reuse this reconciler already documents for reachability. Best
|
|
33
|
+
* effort — its rejection is swallowed here so a failed receipt read can never
|
|
34
|
+
* undo the adoption that just happened.
|
|
35
|
+
*/
|
|
36
|
+
readonly onAttach?: () => void | Promise<void>;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
/**
|
|
@@ -42,6 +54,12 @@ export async function reconcileMemorySpineAdoption(options: MemorySpineAdoptionO
|
|
|
42
54
|
const reachable = (await options.probeReachability()) === 'online';
|
|
43
55
|
if (reachable && !options.memorySpineClient.active) {
|
|
44
56
|
options.memorySpineClient.activate(options.transport);
|
|
57
|
+
if (options.onAttach) {
|
|
58
|
+
// Adoption already happened synchronously above; a receipt-read failure
|
|
59
|
+
// must not propagate and make the caller log a false "reachability
|
|
60
|
+
// recheck failed", nor undo the adoption.
|
|
61
|
+
await Promise.resolve().then(() => options.onAttach!()).catch(() => {});
|
|
62
|
+
}
|
|
45
63
|
return;
|
|
46
64
|
}
|
|
47
65
|
if (!reachable && options.memorySpineClient.active) {
|
|
@@ -14,11 +14,9 @@ import { AgentPersonaRegistry, assertNoSecretLikeText } from '../../agent/person
|
|
|
14
14
|
import { AgentRoutineRegistry } from '../../agent/routine-registry.ts';
|
|
15
15
|
import { AgentSkillRegistry } from '../../agent/skill-registry.ts';
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type FeatureFlagConfigKey,
|
|
21
|
-
} from '../surface-feature-flags.ts';
|
|
17
|
+
expandLegacyFeatureConfigValue,
|
|
18
|
+
isLegacyFeatureConfigKey,
|
|
19
|
+
} from '../feature-enablement.ts';
|
|
22
20
|
import {
|
|
23
21
|
getOnboardingRuntimeStatePath,
|
|
24
22
|
readOnboardingRuntimeState,
|
|
@@ -50,25 +48,13 @@ function isMalformedGoodVibesSecretReferenceValue(value: string): boolean {
|
|
|
50
48
|
return normalized.startsWith('goodvibes://') && !isGoodVibesSecretReferenceValue(normalized);
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
function
|
|
54
|
-
if (!
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (state !== 'enabled' && state !== 'disabled') {
|
|
61
|
-
throw new Error(`featureFlags.${flagId} expects enabled or disabled.`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const flagId = operation.key.slice('featureFlags.'.length);
|
|
68
|
-
if (flagId.trim().length === 0) throw new Error('featureFlags requires a feature id.');
|
|
69
|
-
if (operation.value !== 'enabled' && operation.value !== 'disabled') {
|
|
70
|
-
throw new Error(`Config key ${operation.key} expects enabled or disabled.`);
|
|
71
|
-
}
|
|
51
|
+
function validateLegacyFeatureConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): boolean {
|
|
52
|
+
if (!isLegacyFeatureConfigKey(operation.key)) return false;
|
|
53
|
+
// Throws with real guidance on malformed shapes, unknown feature ids, and
|
|
54
|
+
// always-available (constant-binding) features. The legacy featureFlags
|
|
55
|
+
// namespace dissolved onto domain settings keys; the expansion below is
|
|
56
|
+
// exactly what apply will write.
|
|
57
|
+
expandLegacyFeatureConfigValue(operation.key, operation.value);
|
|
72
58
|
return true;
|
|
73
59
|
}
|
|
74
60
|
|
|
@@ -77,7 +63,7 @@ function validateConfigValue(operation: Extract<OnboardingApplyOperation, { kind
|
|
|
77
63
|
throw new Error(`Config key ${operation.key} only accepts goodvibes://secrets/... secret references.`);
|
|
78
64
|
}
|
|
79
65
|
|
|
80
|
-
if (
|
|
66
|
+
if (validateLegacyFeatureConfigValue(operation)) return;
|
|
81
67
|
|
|
82
68
|
const schema = CONFIG_SCHEMA.find((entry) => entry.key === operation.key);
|
|
83
69
|
if (!schema) {
|
|
@@ -252,6 +238,21 @@ function applyConfigOperation(
|
|
|
252
238
|
if ((operation.scope ?? 'global') === 'project') {
|
|
253
239
|
const path = deps.shellPaths.resolveProjectPath(GOODVIBES_AGENT_SURFACE_ROOT, 'settings.json');
|
|
254
240
|
const existing = readJsonObject(path);
|
|
241
|
+
if (isLegacyFeatureConfigKey(operation.key)) {
|
|
242
|
+
// Legacy featureFlags keys dissolved onto domain settings keys: persist
|
|
243
|
+
// the translated domain-key writes, never a featureFlags record.
|
|
244
|
+
const writes = expandLegacyFeatureConfigValue(operation.key, operation.value);
|
|
245
|
+
let updated = existing;
|
|
246
|
+
for (const write of writes) {
|
|
247
|
+
updated = setNestedValue(updated, write.key, write.value);
|
|
248
|
+
}
|
|
249
|
+
writeJsonObject(path, updated);
|
|
250
|
+
deps.config.load();
|
|
251
|
+
return {
|
|
252
|
+
kind: operation.kind,
|
|
253
|
+
summary: `Persisted ${writes.map((write) => write.key).join(', ')} (legacy ${operation.key}) in project onboarding settings.`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
255
256
|
const updated = setNestedValue(existing, operation.key, operation.value);
|
|
256
257
|
writeJsonObject(path, updated);
|
|
257
258
|
deps.config.load();
|
|
@@ -262,11 +263,17 @@ function applyConfigOperation(
|
|
|
262
263
|
};
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
if (isLegacyFeatureConfigKey(operation.key)) {
|
|
267
|
+
const writes = expandLegacyFeatureConfigValue(operation.key, operation.value);
|
|
268
|
+
for (const write of writes) {
|
|
269
|
+
deps.config.setDynamic(write.key, write.value);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
kind: operation.kind,
|
|
273
|
+
summary: `Updated ${writes.map((write) => write.key).join(', ')} (legacy ${operation.key}) in global onboarding settings.`,
|
|
274
|
+
};
|
|
269
275
|
}
|
|
276
|
+
deps.config.setDynamic(operation.key, operation.value);
|
|
270
277
|
return {
|
|
271
278
|
kind: operation.kind,
|
|
272
279
|
summary: `Updated ${operation.key} in global onboarding settings.`,
|
|
@@ -319,15 +326,21 @@ async function buildRollbackAction(
|
|
|
319
326
|
);
|
|
320
327
|
}
|
|
321
328
|
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
329
|
+
const operationKey = operation.key;
|
|
330
|
+
if (isLegacyFeatureConfigKey(operationKey)) {
|
|
331
|
+
// Snapshot the domain settings keys the translated writes will touch and
|
|
332
|
+
// restore those exact values — no featureFlags category exists to merge.
|
|
333
|
+
const writes = expandLegacyFeatureConfigValue(operationKey, operation.value);
|
|
334
|
+
const previousValues = writes.map((write) => ({ key: write.key, previous: deps.config.get(write.key) }));
|
|
335
|
+
return () => {
|
|
336
|
+
for (const entry of previousValues) {
|
|
337
|
+
deps.config.setDynamic(entry.key, entry.previous);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const previous = deps.config.get(operationKey);
|
|
325
342
|
return () => {
|
|
326
|
-
|
|
327
|
-
mergeFeatureFlagConfigValue(deps.config, operation.key, previous ?? 'disabled');
|
|
328
|
-
} else {
|
|
329
|
-
deps.config.setDynamic(operation.key, previous);
|
|
330
|
-
}
|
|
343
|
+
deps.config.setDynamic(operationKey, previous);
|
|
331
344
|
};
|
|
332
345
|
}
|
|
333
346
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConfigManager, ConfigKey, GoodVibesConfig } from '../../config/index.ts';
|
|
2
2
|
import type { SecretsManager, SecretRecord, SecretStorageReview } from '../../config/secrets.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { LegacyFeatureConfigKey } from '../feature-enablement.ts';
|
|
4
4
|
import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
5
5
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
6
6
|
import type {
|
|
@@ -226,7 +226,7 @@ export interface OnboardingStepDerivationState {
|
|
|
226
226
|
export type OnboardingApplyOperation =
|
|
227
227
|
| {
|
|
228
228
|
readonly kind: 'set-config';
|
|
229
|
-
readonly key: ConfigKey |
|
|
229
|
+
readonly key: ConfigKey | LegacyFeatureConfigKey;
|
|
230
230
|
readonly value: unknown;
|
|
231
231
|
readonly scope?: 'global' | 'project';
|
|
232
232
|
}
|
|
@@ -6,7 +6,7 @@ import { AgentPersonaRegistry } from '../../agent/persona-registry.ts';
|
|
|
6
6
|
import { AgentRoutineRegistry } from '../../agent/routine-registry.ts';
|
|
7
7
|
import { readAgentRuntimeProfileSelection, resolveAgentRuntimeProfileHome } from '../../agent/runtime-profile.ts';
|
|
8
8
|
import { AgentSkillRegistry } from '../../agent/skill-registry.ts';
|
|
9
|
-
import {
|
|
9
|
+
import { isFeatureEnabledInConfig, isLegacyFeatureConfigKey, legacyFeatureConfigTargets } from '../feature-enablement.ts';
|
|
10
10
|
import { readOnboardingRuntimeState } from './state.ts';
|
|
11
11
|
import type {
|
|
12
12
|
OnboardingApplyOperation,
|
|
@@ -51,9 +51,27 @@ function verifyConfigOperation(
|
|
|
51
51
|
deps: OnboardingVerificationDependencies,
|
|
52
52
|
operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>,
|
|
53
53
|
): OnboardingVerificationItem {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
if (isLegacyFeatureConfigKey(operation.key)) {
|
|
55
|
+
// Legacy featureFlags keys dissolved onto domain settings keys: verify the
|
|
56
|
+
// DERIVED enablement of every addressed feature, not a category readback.
|
|
57
|
+
const targets = legacyFeatureConfigTargets(operation.key, operation.value);
|
|
58
|
+
const mismatched = targets.filter((target) => (
|
|
59
|
+
(isFeatureEnabledInConfig(deps.config, target.featureId) ? 'enabled' : 'disabled') !== target.desired
|
|
60
|
+
));
|
|
61
|
+
const ok = targets.length > 0 && mismatched.length === 0;
|
|
62
|
+
return {
|
|
63
|
+
id: `config:${operation.key}`,
|
|
64
|
+
status: ok ? 'pass' : 'fail',
|
|
65
|
+
message: ok
|
|
66
|
+
? `${operation.key} matches the requested onboarding value (derived from the domain settings keys).`
|
|
67
|
+
: `${operation.key} does not match the requested onboarding value${
|
|
68
|
+
mismatched.length > 0 ? ` (mismatched: ${mismatched.map((target) => target.featureId).join(', ')})` : ''
|
|
69
|
+
}.`,
|
|
70
|
+
target: operation.key,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const actual = deps.config.get(operation.key);
|
|
57
75
|
const ok = isDeepEqual(actual, operation.value);
|
|
58
76
|
|
|
59
77
|
return {
|