@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.4
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/docs/desktop-browser-cli.md +33 -0
- package/openapi.yaml +17 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +6 -4
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +116 -18
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/session-snapshot.ts +260 -0
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +7 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +62 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +34 -4
- package/src/calls/voice-session-bridge.ts +23 -0
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -1
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/bundled-skills/computer-use/SKILL.md +30 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +2 -2
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -1
- package/src/daemon/conversation-tool-setup.ts +31 -29
- package/src/daemon/host-cu-proxy.ts +51 -26
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +22 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +3 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +11 -3
- package/src/live-voice/__tests__/session-controls.test.ts +34 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +3 -0
- package/src/live-voice/protocol.ts +10 -1
- package/src/live-voice/session-controls.ts +28 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/notifications/__tests__/decision-engine.test.ts +175 -0
- package/src/notifications/decision-engine.ts +43 -8
- package/src/oauth/seed-providers.ts +11 -0
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/acp-routes.test.ts +19 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-routes.ts +17 -161
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/acp/status.test.ts +276 -21
- package/src/tools/acp/status.ts +98 -32
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +2 -3
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount-table watcher for the assistant container, polled by the resource
|
|
3
|
+
* monitor.
|
|
4
|
+
*
|
|
5
|
+
* The monitor process shares the assistant container's mount namespace, so
|
|
6
|
+
* `/proc/self/mountinfo` is exactly that container's mounts (workspace and
|
|
7
|
+
* data PVC binds, plus any virtiofs handles). Gateway and CES sidecar mounts
|
|
8
|
+
* are out of scope. The watcher keeps a baseline and logs only when the
|
|
9
|
+
* watched set actually changes: a virtiofs bind that disappears, a fstype
|
|
10
|
+
* swap, or a statfs size collapse on `/workspace` or `/data`.
|
|
11
|
+
*
|
|
12
|
+
* Windows and macOS have no `/proc/self/mountinfo`; those platforms no-op.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, statfsSync } from "node:fs";
|
|
16
|
+
|
|
17
|
+
import type { MonitoringConfig } from "../config/schemas/monitoring.js";
|
|
18
|
+
import { getLogger } from "../util/logger.js";
|
|
19
|
+
|
|
20
|
+
const log = getLogger("mount-watch");
|
|
21
|
+
|
|
22
|
+
/** Assistant-container PVC bind targets in the managed pod template. */
|
|
23
|
+
export const ASSISTANT_VOLUME_MOUNT_POINTS = ["/workspace", "/data"] as const;
|
|
24
|
+
|
|
25
|
+
const VIRTIOFS_TYPES = new Set(["virtiofs", "fuse.virtiofs"]);
|
|
26
|
+
|
|
27
|
+
export interface MountInfoEntry {
|
|
28
|
+
mountId: string;
|
|
29
|
+
parentId: string;
|
|
30
|
+
majorMinor: string;
|
|
31
|
+
root: string;
|
|
32
|
+
mountPoint: string;
|
|
33
|
+
mountOptions: string;
|
|
34
|
+
fsType: string;
|
|
35
|
+
source: string;
|
|
36
|
+
superOptions: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PathFsSnapshot {
|
|
40
|
+
path: string;
|
|
41
|
+
/** `statfs.f_type`; null when the path could not be measured. */
|
|
42
|
+
type: number | null;
|
|
43
|
+
blockSize: number | null;
|
|
44
|
+
blocks: number | null;
|
|
45
|
+
files: number | null;
|
|
46
|
+
/** Present when the path exists but `statfs` failed. */
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MountSnapshot {
|
|
51
|
+
mounts: MountInfoEntry[];
|
|
52
|
+
paths: PathFsSnapshot[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MountDiff {
|
|
56
|
+
added: MountInfoEntry[];
|
|
57
|
+
removed: MountInfoEntry[];
|
|
58
|
+
changed: Array<{ previous: MountInfoEntry; current: MountInfoEntry }>;
|
|
59
|
+
pathChanges: Array<{ previous: PathFsSnapshot; current: PathFsSnapshot }>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CollectMountSnapshotOptions {
|
|
63
|
+
platform?: NodeJS.Platform;
|
|
64
|
+
readMountinfo?: () => string;
|
|
65
|
+
statPath?: (path: string) => PathFsSnapshot;
|
|
66
|
+
watchedMountPoints?: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface MountWatchHandle {
|
|
70
|
+
stop: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Kernel mountinfo encodes space/tab/newline in paths as octal escapes
|
|
75
|
+
* (`\040`, `\011`, `\012`) and a backslash as `\\`.
|
|
76
|
+
*/
|
|
77
|
+
export function unescapeMountField(value: string): string {
|
|
78
|
+
return value
|
|
79
|
+
.replace(/\\([0-7]{3})/g, (_match, oct: string) =>
|
|
80
|
+
String.fromCharCode(parseInt(oct, 8)),
|
|
81
|
+
)
|
|
82
|
+
.replace(/\\\\/g, "\\");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse `/proc/self/mountinfo` into structured entries. Malformed lines are
|
|
87
|
+
* skipped rather than failing the pass.
|
|
88
|
+
*/
|
|
89
|
+
export function parseMountinfo(raw: string): MountInfoEntry[] {
|
|
90
|
+
const entries: MountInfoEntry[] = [];
|
|
91
|
+
for (const line of raw.split("\n")) {
|
|
92
|
+
if (line.length === 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const separator = line.indexOf(" - ");
|
|
96
|
+
if (separator < 0) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const left = line.slice(0, separator).split(" ");
|
|
100
|
+
const right = line.slice(separator + 3).split(" ");
|
|
101
|
+
if (left.length < 6 || right.length < 3) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
entries.push({
|
|
105
|
+
mountId: left[0],
|
|
106
|
+
parentId: left[1],
|
|
107
|
+
majorMinor: left[2],
|
|
108
|
+
root: unescapeMountField(left[3]),
|
|
109
|
+
mountPoint: unescapeMountField(left[4]),
|
|
110
|
+
mountOptions: left[5],
|
|
111
|
+
fsType: unescapeMountField(right[0]),
|
|
112
|
+
source: unescapeMountField(right[1]),
|
|
113
|
+
superOptions: right.slice(2).join(" "),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return entries;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isWatchedMount(
|
|
120
|
+
entry: MountInfoEntry,
|
|
121
|
+
watchedMountPoints: readonly string[] = ASSISTANT_VOLUME_MOUNT_POINTS,
|
|
122
|
+
): boolean {
|
|
123
|
+
if (VIRTIOFS_TYPES.has(entry.fsType)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return watchedMountPoints.some(
|
|
127
|
+
(point) => entry.mountPoint === point || entry.mountPoint.startsWith(`${point}/`),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function mountIdentity(entry: MountInfoEntry): string {
|
|
132
|
+
return entry.mountPoint;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function mountSignature(entry: MountInfoEntry): string {
|
|
136
|
+
return [
|
|
137
|
+
entry.fsType,
|
|
138
|
+
entry.source,
|
|
139
|
+
entry.majorMinor,
|
|
140
|
+
entry.root,
|
|
141
|
+
entry.mountOptions,
|
|
142
|
+
entry.superOptions,
|
|
143
|
+
].join("\0");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function pathSignature(snapshot: PathFsSnapshot): string {
|
|
147
|
+
return [
|
|
148
|
+
String(snapshot.type),
|
|
149
|
+
String(snapshot.blockSize),
|
|
150
|
+
String(snapshot.blocks),
|
|
151
|
+
String(snapshot.files),
|
|
152
|
+
snapshot.error ?? "",
|
|
153
|
+
].join("\0");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function diffMountSnapshots(
|
|
157
|
+
previous: MountSnapshot,
|
|
158
|
+
current: MountSnapshot,
|
|
159
|
+
): MountDiff {
|
|
160
|
+
const prevByPoint = new Map(
|
|
161
|
+
previous.mounts.map((entry) => [mountIdentity(entry), entry]),
|
|
162
|
+
);
|
|
163
|
+
const currentByPoint = new Map(
|
|
164
|
+
current.mounts.map((entry) => [mountIdentity(entry), entry]),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const added: MountInfoEntry[] = [];
|
|
168
|
+
const removed: MountInfoEntry[] = [];
|
|
169
|
+
const changed: Array<{ previous: MountInfoEntry; current: MountInfoEntry }> =
|
|
170
|
+
[];
|
|
171
|
+
|
|
172
|
+
for (const [point, entry] of currentByPoint) {
|
|
173
|
+
const prior = prevByPoint.get(point);
|
|
174
|
+
if (prior == null) {
|
|
175
|
+
added.push(entry);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (mountSignature(prior) !== mountSignature(entry)) {
|
|
179
|
+
changed.push({ previous: prior, current: entry });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const [point, entry] of prevByPoint) {
|
|
183
|
+
if (!currentByPoint.has(point)) {
|
|
184
|
+
removed.push(entry);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const prevPaths = new Map(previous.paths.map((row) => [row.path, row]));
|
|
189
|
+
const pathChanges: Array<{
|
|
190
|
+
previous: PathFsSnapshot;
|
|
191
|
+
current: PathFsSnapshot;
|
|
192
|
+
}> = [];
|
|
193
|
+
for (const row of current.paths) {
|
|
194
|
+
const prior = prevPaths.get(row.path);
|
|
195
|
+
if (prior != null && pathSignature(prior) !== pathSignature(row)) {
|
|
196
|
+
pathChanges.push({ previous: prior, current: row });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { added, removed, changed, pathChanges };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function mountDiffIsEmpty(diff: MountDiff): boolean {
|
|
204
|
+
return (
|
|
205
|
+
diff.added.length === 0 &&
|
|
206
|
+
diff.removed.length === 0 &&
|
|
207
|
+
diff.changed.length === 0 &&
|
|
208
|
+
diff.pathChanges.length === 0
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function defaultReadMountinfo(): string {
|
|
213
|
+
return readFileSync("/proc/self/mountinfo", "utf8");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function defaultStatPath(path: string): PathFsSnapshot {
|
|
217
|
+
try {
|
|
218
|
+
const stats = statfsSync(path);
|
|
219
|
+
return {
|
|
220
|
+
path,
|
|
221
|
+
type: Number(stats.type),
|
|
222
|
+
blockSize: Number(stats.bsize),
|
|
223
|
+
blocks: Number(stats.blocks),
|
|
224
|
+
files: Number(stats.files),
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return {
|
|
228
|
+
path,
|
|
229
|
+
type: null,
|
|
230
|
+
blockSize: null,
|
|
231
|
+
blocks: null,
|
|
232
|
+
files: null,
|
|
233
|
+
error: "statfs-failed",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Snapshot the assistant container's watched mounts. Returns null on Windows
|
|
240
|
+
* and macOS, or when `/proc/self/mountinfo` is unreadable.
|
|
241
|
+
*/
|
|
242
|
+
export function collectMountSnapshot(
|
|
243
|
+
options: CollectMountSnapshotOptions = {},
|
|
244
|
+
): MountSnapshot | null {
|
|
245
|
+
const platform = options.platform ?? process.platform;
|
|
246
|
+
if (platform === "win32" || platform === "darwin") {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const watchedMountPoints =
|
|
251
|
+
options.watchedMountPoints ?? ASSISTANT_VOLUME_MOUNT_POINTS;
|
|
252
|
+
const readMountinfo = options.readMountinfo ?? defaultReadMountinfo;
|
|
253
|
+
const statPath = options.statPath ?? defaultStatPath;
|
|
254
|
+
|
|
255
|
+
let raw: string;
|
|
256
|
+
try {
|
|
257
|
+
raw = readMountinfo();
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const mounts = parseMountinfo(raw)
|
|
263
|
+
.filter((entry) => isWatchedMount(entry, watchedMountPoints))
|
|
264
|
+
.sort((a, b) => a.mountPoint.localeCompare(b.mountPoint));
|
|
265
|
+
|
|
266
|
+
const paths = watchedMountPoints.map((path) => statPath(path));
|
|
267
|
+
|
|
268
|
+
return { mounts, paths };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function summarizeEntry(entry: MountInfoEntry): Record<string, string> {
|
|
272
|
+
return {
|
|
273
|
+
mountPoint: entry.mountPoint,
|
|
274
|
+
fsType: entry.fsType,
|
|
275
|
+
source: entry.source,
|
|
276
|
+
majorMinor: entry.majorMinor,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function summarizePath(snapshot: PathFsSnapshot): Record<string, unknown> {
|
|
281
|
+
return {
|
|
282
|
+
path: snapshot.path,
|
|
283
|
+
type: snapshot.type,
|
|
284
|
+
blockSize: snapshot.blockSize,
|
|
285
|
+
blocks: snapshot.blocks,
|
|
286
|
+
files: snapshot.files,
|
|
287
|
+
...(snapshot.error != null ? { error: snapshot.error } : {}),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function logMountDiff(diff: MountDiff): void {
|
|
292
|
+
log.warn(
|
|
293
|
+
{
|
|
294
|
+
added: diff.added.map(summarizeEntry),
|
|
295
|
+
removed: diff.removed.map(summarizeEntry),
|
|
296
|
+
changed: diff.changed.map((row) => ({
|
|
297
|
+
previous: summarizeEntry(row.previous),
|
|
298
|
+
current: summarizeEntry(row.current),
|
|
299
|
+
})),
|
|
300
|
+
pathChanges: diff.pathChanges.map((row) => ({
|
|
301
|
+
previous: summarizePath(row.previous),
|
|
302
|
+
current: summarizePath(row.current),
|
|
303
|
+
})),
|
|
304
|
+
},
|
|
305
|
+
"Assistant container mount table changed",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Start the mount poller. The first pass establishes a silent baseline so a
|
|
311
|
+
* healthy boot does not log; later passes warn only when the watched mounts
|
|
312
|
+
* or `/workspace`/`/data` statfs values change.
|
|
313
|
+
*
|
|
314
|
+
* The timer is unref'd: the resource sampler is the monitor process's
|
|
315
|
+
* keep-alive, and this loop must not extend its life on its own.
|
|
316
|
+
*/
|
|
317
|
+
export function startMountWatch(
|
|
318
|
+
config: MonitoringConfig,
|
|
319
|
+
options: CollectMountSnapshotOptions = {},
|
|
320
|
+
): MountWatchHandle {
|
|
321
|
+
let previous: MountSnapshot | null = null;
|
|
322
|
+
|
|
323
|
+
const poll = () => {
|
|
324
|
+
try {
|
|
325
|
+
const snapshot = collectMountSnapshot(options);
|
|
326
|
+
if (snapshot == null) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (previous != null) {
|
|
330
|
+
const diff = diffMountSnapshots(previous, snapshot);
|
|
331
|
+
if (!mountDiffIsEmpty(diff)) {
|
|
332
|
+
logMountDiff(diff);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
previous = snapshot;
|
|
336
|
+
} catch (err) {
|
|
337
|
+
log.warn({ err }, "Mount-watch poll failed");
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
poll();
|
|
342
|
+
const timer = setInterval(poll, config.mountWatchIntervalMs);
|
|
343
|
+
timer.unref?.();
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
stop: () => clearInterval(timer),
|
|
347
|
+
};
|
|
348
|
+
}
|
package/src/monitoring/worker.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
type FileDescriptorMonitorHandle,
|
|
50
50
|
startFileDescriptorMonitor,
|
|
51
51
|
} from "./file-descriptors.js";
|
|
52
|
+
import { type MountWatchHandle, startMountWatch } from "./mount-watch.js";
|
|
52
53
|
import {
|
|
53
54
|
type PluginAutoUpdateHandle,
|
|
54
55
|
startPluginAutoUpdate,
|
|
@@ -99,6 +100,7 @@ async function main(): Promise<void> {
|
|
|
99
100
|
|
|
100
101
|
let sampler: ResourceSamplerHandle | null = null;
|
|
101
102
|
let fdMonitor: FileDescriptorMonitorHandle | null = null;
|
|
103
|
+
let mountWatch: MountWatchHandle | null = null;
|
|
102
104
|
let sourceWatch: PluginSourceWatchHandle | null = null;
|
|
103
105
|
let autoUpdate: PluginAutoUpdateHandle | null = null;
|
|
104
106
|
let recovery: RecoveryHandle | null = null;
|
|
@@ -125,6 +127,7 @@ async function main(): Promise<void> {
|
|
|
125
127
|
stopDbIntegritySampler();
|
|
126
128
|
autoUpdate?.stop();
|
|
127
129
|
sourceWatch?.stop();
|
|
130
|
+
mountWatch?.stop();
|
|
128
131
|
fdMonitor?.stop();
|
|
129
132
|
sampler?.stop();
|
|
130
133
|
// Bounded final telemetry flush, mirroring the daemon's shutdown. This
|
|
@@ -170,6 +173,9 @@ async function main(): Promise<void> {
|
|
|
170
173
|
// Descriptor exhaustion is per-process and slow-moving, so it polls on its
|
|
171
174
|
// own timer rather than riding the memory sampler's 250ms tick.
|
|
172
175
|
fdMonitor = startFileDescriptorMonitor(config.monitoring);
|
|
176
|
+
// Assistant-container mount table only (this process's mount namespace).
|
|
177
|
+
// Warns only when /workspace, /data, or a virtiofs bind actually changes.
|
|
178
|
+
mountWatch = startMountWatch(config.monitoring);
|
|
173
179
|
sourceWatch = startPluginSourceWatch(
|
|
174
180
|
config.monitoring.pluginSourceScanIntervalMs,
|
|
175
181
|
);
|
|
@@ -211,6 +217,7 @@ async function main(): Promise<void> {
|
|
|
211
217
|
recovery?.stop();
|
|
212
218
|
autoUpdate?.stop();
|
|
213
219
|
sourceWatch?.stop();
|
|
220
|
+
mountWatch?.stop();
|
|
214
221
|
fdMonitor?.stop();
|
|
215
222
|
sampler?.stop();
|
|
216
223
|
stopDbIntegritySampler();
|
|
@@ -223,6 +230,7 @@ async function main(): Promise<void> {
|
|
|
223
230
|
recovery?.stop();
|
|
224
231
|
autoUpdate?.stop();
|
|
225
232
|
sourceWatch?.stop();
|
|
233
|
+
mountWatch?.stop();
|
|
226
234
|
fdMonitor?.stop();
|
|
227
235
|
sampler?.stop();
|
|
228
236
|
stopDbIntegritySampler();
|
|
@@ -234,6 +242,7 @@ async function main(): Promise<void> {
|
|
|
234
242
|
recovery?.stop();
|
|
235
243
|
autoUpdate?.stop();
|
|
236
244
|
sourceWatch?.stop();
|
|
245
|
+
mountWatch?.stop();
|
|
237
246
|
fdMonitor?.stop();
|
|
238
247
|
sampler?.stop();
|
|
239
248
|
stopDbIntegritySampler();
|
|
@@ -736,3 +736,178 @@ describe("schedule.result pass-through in notification decision engine", () => {
|
|
|
736
736
|
expect(decision.shouldNotify).toBe(true);
|
|
737
737
|
});
|
|
738
738
|
});
|
|
739
|
+
|
|
740
|
+
const SCHEDULER_OWNED_REPORT = [
|
|
741
|
+
"# Daily briefing",
|
|
742
|
+
"",
|
|
743
|
+
"## Overnight",
|
|
744
|
+
"- Calendar is clear until 10:00.",
|
|
745
|
+
"- Two pull requests are waiting on review.",
|
|
746
|
+
"",
|
|
747
|
+
"## Account security",
|
|
748
|
+
"- Urgent: a sign-in from a new device needs confirmation before the weekly sync.",
|
|
749
|
+
"",
|
|
750
|
+
"## This week",
|
|
751
|
+
"- Project kickoff on Wednesday.",
|
|
752
|
+
"- Weekly planning on Friday.",
|
|
753
|
+
"- Follow up on the draft status update.",
|
|
754
|
+
].join("\n");
|
|
755
|
+
|
|
756
|
+
function makeSchedulerShareSignal(
|
|
757
|
+
overrides?: Partial<NotificationSignal>,
|
|
758
|
+
): NotificationSignal {
|
|
759
|
+
return {
|
|
760
|
+
signalId: "sig-scheduler-share-test-1",
|
|
761
|
+
createdAt: Date.now(),
|
|
762
|
+
sourceChannel: "scheduler",
|
|
763
|
+
sourceContextId: "conv-xyz",
|
|
764
|
+
sourceEventName: "assistant.share",
|
|
765
|
+
contextPayload: {
|
|
766
|
+
requestedMessage: SCHEDULER_OWNED_REPORT,
|
|
767
|
+
requestedBySource: "scheduler",
|
|
768
|
+
requestedTitle: "Your day",
|
|
769
|
+
},
|
|
770
|
+
attentionHints: {
|
|
771
|
+
requiresAction: true,
|
|
772
|
+
urgency: "high",
|
|
773
|
+
isAsyncBackground: true,
|
|
774
|
+
visibleInSourceNow: false,
|
|
775
|
+
},
|
|
776
|
+
...overrides,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
describe("scheduler requested-message pass-through in notification decision engine", () => {
|
|
781
|
+
beforeEach(() => {
|
|
782
|
+
persistedDecisions = [];
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("keeps a scheduler-owned report verbatim on every urgency-selected channel", async () => {
|
|
786
|
+
const available = [
|
|
787
|
+
"vellum",
|
|
788
|
+
"telegram",
|
|
789
|
+
"platform",
|
|
790
|
+
] as NotificationChannel[];
|
|
791
|
+
const decision = await evaluateSignal(
|
|
792
|
+
makeSchedulerShareSignal(),
|
|
793
|
+
available,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
expect(decision.shouldNotify).toBe(true);
|
|
797
|
+
expect(decision.selectedChannels).toEqual(available);
|
|
798
|
+
expect(decision.reasoningSummary).toBe(
|
|
799
|
+
"scheduler requested-message pass-through",
|
|
800
|
+
);
|
|
801
|
+
expect(decision.verbatimCopy).toBe(true);
|
|
802
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
803
|
+
for (const ch of available) {
|
|
804
|
+
expect(decision.renderedCopy[ch]?.title).toBe("Your day");
|
|
805
|
+
expect(decision.renderedCopy[ch]?.body).toBe(SCHEDULER_OWNED_REPORT);
|
|
806
|
+
expect(decision.renderedCopy[ch]?.conversationSeedMessage).toBe(
|
|
807
|
+
SCHEDULER_OWNED_REPORT,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("copies the complete report onto the urgency-narrowed channel set", async () => {
|
|
813
|
+
const available = [
|
|
814
|
+
"vellum",
|
|
815
|
+
"telegram",
|
|
816
|
+
"platform",
|
|
817
|
+
] as NotificationChannel[];
|
|
818
|
+
const decision = await evaluateSignal(
|
|
819
|
+
makeSchedulerShareSignal({
|
|
820
|
+
attentionHints: {
|
|
821
|
+
requiresAction: false,
|
|
822
|
+
urgency: "medium",
|
|
823
|
+
isAsyncBackground: true,
|
|
824
|
+
visibleInSourceNow: false,
|
|
825
|
+
},
|
|
826
|
+
}),
|
|
827
|
+
available,
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
expect(decision.shouldNotify).toBe(true);
|
|
831
|
+
expect(decision.selectedChannels).toEqual(["vellum"]);
|
|
832
|
+
expect(decision.reasoningSummary).toBe(
|
|
833
|
+
"scheduler requested-message pass-through",
|
|
834
|
+
);
|
|
835
|
+
expect(decision.renderedCopy.vellum?.body).toBe(SCHEDULER_OWNED_REPORT);
|
|
836
|
+
expect(decision.renderedCopy.vellum?.conversationSeedMessage).toBe(
|
|
837
|
+
SCHEDULER_OWNED_REPORT,
|
|
838
|
+
);
|
|
839
|
+
expect(decision.renderedCopy.telegram?.body).toBe(SCHEDULER_OWNED_REPORT);
|
|
840
|
+
expect(decision.renderedCopy.platform?.body).toBe(SCHEDULER_OWNED_REPORT);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test("leaves an unowned scheduler requestedMessage on the model path", async () => {
|
|
844
|
+
const previousSendMessage = providerSendMessage;
|
|
845
|
+
let providerCalled = false;
|
|
846
|
+
providerSendMessage = async () => {
|
|
847
|
+
providerCalled = true;
|
|
848
|
+
return {};
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
const decision = await evaluateSignal(
|
|
853
|
+
makeSchedulerShareSignal({
|
|
854
|
+
contextPayload: {
|
|
855
|
+
requestedMessage: SCHEDULER_OWNED_REPORT,
|
|
856
|
+
requestedTitle: "Your day",
|
|
857
|
+
},
|
|
858
|
+
}),
|
|
859
|
+
["vellum", "telegram", "platform"] as NotificationChannel[],
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
expect(providerCalled).toBe(true);
|
|
863
|
+
expect(decision.verbatimCopy).toBeUndefined();
|
|
864
|
+
expect(decision.reasoningSummary).not.toBe(
|
|
865
|
+
"scheduler requested-message pass-through",
|
|
866
|
+
);
|
|
867
|
+
} finally {
|
|
868
|
+
providerSendMessage = previousSendMessage;
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test("leaves scheduler notify-mode without the ownership marker on the model path", async () => {
|
|
873
|
+
const previousSendMessage = providerSendMessage;
|
|
874
|
+
let providerCalled = false;
|
|
875
|
+
providerSendMessage = async () => {
|
|
876
|
+
providerCalled = true;
|
|
877
|
+
return {};
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
const decision = await evaluateSignal(
|
|
882
|
+
{
|
|
883
|
+
signalId: "sig-schedule-notify-test-1",
|
|
884
|
+
createdAt: Date.now(),
|
|
885
|
+
sourceChannel: "scheduler",
|
|
886
|
+
sourceContextId: "sched-notify-1",
|
|
887
|
+
sourceEventName: "schedule.notify",
|
|
888
|
+
contextPayload: {
|
|
889
|
+
scheduleId: "sched-notify-1",
|
|
890
|
+
label: "Take out the trash",
|
|
891
|
+
message: "Take out the trash",
|
|
892
|
+
},
|
|
893
|
+
attentionHints: {
|
|
894
|
+
requiresAction: true,
|
|
895
|
+
urgency: "high",
|
|
896
|
+
isAsyncBackground: false,
|
|
897
|
+
visibleInSourceNow: false,
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
["vellum", "telegram", "platform"] as NotificationChannel[],
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
expect(providerCalled).toBe(true);
|
|
904
|
+
expect(decision.verbatimCopy).toBeUndefined();
|
|
905
|
+
expect(decision.reasoningSummary).not.toBe(
|
|
906
|
+
"scheduler requested-message pass-through",
|
|
907
|
+
);
|
|
908
|
+
expect(decision.reasoningSummary).not.toBe("schedule_result pass-through");
|
|
909
|
+
} finally {
|
|
910
|
+
providerSendMessage = previousSendMessage;
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
});
|
|
@@ -833,6 +833,19 @@ function buildPassThroughDecision(params: {
|
|
|
833
833
|
return decision;
|
|
834
834
|
}
|
|
835
835
|
|
|
836
|
+
function selectDefaultChannelsByUrgency(
|
|
837
|
+
urgency: NotificationSignal["attentionHints"]["urgency"],
|
|
838
|
+
availableChannels: NotificationChannel[],
|
|
839
|
+
): NotificationChannel[] {
|
|
840
|
+
const isUrgent = urgency === "critical" || urgency === "high";
|
|
841
|
+
if (isUrgent) {
|
|
842
|
+
return [...availableChannels];
|
|
843
|
+
}
|
|
844
|
+
return availableChannels.includes("vellum")
|
|
845
|
+
? ["vellum" as NotificationChannel]
|
|
846
|
+
: [];
|
|
847
|
+
}
|
|
848
|
+
|
|
836
849
|
/**
|
|
837
850
|
* The deterministic guards every decision passes through once the model,
|
|
838
851
|
* the assistant-tool pass-through, or the fallback has rendered copy.
|
|
@@ -870,14 +883,10 @@ export async function evaluateSignal(
|
|
|
870
883
|
);
|
|
871
884
|
if (signal.sourceChannel === "assistant_tool" && requestedBody) {
|
|
872
885
|
const payload = signal.contextPayload as Record<string, unknown>;
|
|
873
|
-
const
|
|
874
|
-
signal.attentionHints.urgency
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
? [...availableChannels]
|
|
878
|
-
: availableChannels.includes("vellum")
|
|
879
|
-
? ["vellum" as NotificationChannel]
|
|
880
|
-
: [];
|
|
886
|
+
const defaultChannels = selectDefaultChannelsByUrgency(
|
|
887
|
+
signal.attentionHints.urgency,
|
|
888
|
+
availableChannels,
|
|
889
|
+
);
|
|
881
890
|
// Honor `--preferred-channels` as ADDITIVE push targets on top of
|
|
882
891
|
// the default channel set. The notification center (vellum) is the
|
|
883
892
|
// always-on canonical inbox; preferred channels add push surfaces
|
|
@@ -926,6 +935,32 @@ export async function evaluateSignal(
|
|
|
926
935
|
});
|
|
927
936
|
}
|
|
928
937
|
|
|
938
|
+
// Scheduler-owned requested copy: the scheduler already authored the
|
|
939
|
+
// complete message. Ownership requires both the signal source and the
|
|
940
|
+
// payload marker so schedule.result (requestedMessage, no
|
|
941
|
+
// requestedBySource) and notify-mode (message, no requestedBySource)
|
|
942
|
+
// stay on their existing paths. Urgency still chooses channels; every
|
|
943
|
+
// selected channel keeps the producer body.
|
|
944
|
+
const requestedBySource = nonEmpty(
|
|
945
|
+
readPayloadString(signal.contextPayload, "requestedBySource"),
|
|
946
|
+
);
|
|
947
|
+
if (
|
|
948
|
+
signal.sourceChannel === "scheduler" &&
|
|
949
|
+
requestedBySource === "scheduler" &&
|
|
950
|
+
requestedBody
|
|
951
|
+
) {
|
|
952
|
+
return buildPassThroughDecision({
|
|
953
|
+
signal,
|
|
954
|
+
availableChannels,
|
|
955
|
+
selectedChannels: selectDefaultChannelsByUrgency(
|
|
956
|
+
signal.attentionHints.urgency,
|
|
957
|
+
availableChannels,
|
|
958
|
+
),
|
|
959
|
+
body: requestedBody,
|
|
960
|
+
reasoningSummary: "scheduler requested-message pass-through",
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
929
964
|
// Schedule-result pass-through: the body is the run's own reply, which is
|
|
930
965
|
// the whole point of the notification — a briefing, a digest, a report. The
|
|
931
966
|
// classifier rewrites bodies into short alerts, which would throw away the
|
|
@@ -1403,6 +1403,12 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1403
1403
|
"read_discounts",
|
|
1404
1404
|
"write_discounts",
|
|
1405
1405
|
"read_price_rules",
|
|
1406
|
+
// write_themes covers listing, duplicating, and publishing themes.
|
|
1407
|
+
// Writing theme files (settings, JSON templates, Liquid) also requires
|
|
1408
|
+
// Shopify's theme-code exemption on the app itself, which is granted
|
|
1409
|
+
// per app in the Shopify dashboard rather than through a scope.
|
|
1410
|
+
"read_themes",
|
|
1411
|
+
"write_themes",
|
|
1406
1412
|
],
|
|
1407
1413
|
availableScopes: [
|
|
1408
1414
|
{
|
|
@@ -1455,6 +1461,11 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1455
1461
|
scope: "write_price_rules",
|
|
1456
1462
|
description: "Create and update price rules",
|
|
1457
1463
|
},
|
|
1464
|
+
{ scope: "read_themes", description: "Read themes and theme files" },
|
|
1465
|
+
{
|
|
1466
|
+
scope: "write_themes",
|
|
1467
|
+
description: "Create, publish, and edit themes and theme files",
|
|
1468
|
+
},
|
|
1458
1469
|
{ scope: "read_files", description: "Read files uploaded to the store" },
|
|
1459
1470
|
{ scope: "write_files", description: "Upload and update files" },
|
|
1460
1471
|
{ scope: "read_gift_cards", description: "Read gift cards" },
|
|
@@ -69,7 +69,8 @@ export function buildCheckinTitle({
|
|
|
69
69
|
* Google Calendar strips nearly all styling, so the body relies only on the
|
|
70
70
|
* tags that survive (`<p>`, `<strong>`, `<a>`, emoji) and makes the CTA a bold
|
|
71
71
|
* link rather than a styled button. The deep link opens a fresh conversation
|
|
72
|
-
* (`uuid`)
|
|
72
|
+
* (`uuid`) with the first-week prompt staged in the composer; the user sends
|
|
73
|
+
* it. A clicked link never auto-sends (`clients/web/src/utils/auto-send-prompt.ts`).
|
|
73
74
|
*/
|
|
74
75
|
export function buildCheckinDescription(uuid: string): string {
|
|
75
76
|
// `vref` is an app-owned attribution param, NOT a marketing `utm_*`: the
|