@vellumai/assistant 0.12.2-staging.1 → 0.12.2-staging.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/docs/desktop-browser-cli.md +33 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +50 -4
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
- 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 +33 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/credential-routes.test.ts +38 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +6 -2
- 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 +185 -30
- 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__/oauth-commands-routes.test.ts +55 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- 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 +69 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +81 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +132 -0
- package/src/calls/voice-session-bridge.ts +56 -26
- package/src/calls/voice-triage-escalate.ts +3 -3
- 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/oauth/status.ts +5 -0
- 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/computer-use/SKILL.md +43 -4
- package/src/config/bundled-skills/computer-use/TOOLS.json +3 -3
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +11 -3
- package/src/config/loader.ts +1 -0
- 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/services.ts +10 -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 +19 -7
- package/src/daemon/conversation-surfaces.ts +6 -1
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/host-cu-proxy.ts +94 -31
- 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 +143 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +9 -7
- package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +47 -0
- package/src/live-voice/__tests__/session-controls.test.ts +113 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +88 -7
- package/src/live-voice/protocol.ts +69 -0
- package/src/live-voice/session-controls.ts +139 -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/oauth/__tests__/seed-providers-managed.test.ts +95 -0
- package/src/oauth/connection-resolver.test.ts +27 -0
- package/src/oauth/connection-resolver.ts +25 -1
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/seed-providers.ts +121 -2
- 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-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/app-management-routes.ts +17 -2
- 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/credential-routes.ts +1 -4
- 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/oauth-commands-routes.ts +14 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- 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 +3 -4
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/watch/watch-retro.ts +6 -8
- 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();
|
|
@@ -60,6 +60,26 @@ describe("PROVIDER_SEED_DATA managed mode wiring", () => {
|
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// `folders:read` is documented too, but the app's scope picker only
|
|
64
|
+
// offers `folder_metadata:read` under Folders, so Figma answers
|
|
65
|
+
// "Invalid scopes for app" whenever it is requested.
|
|
66
|
+
expect(figma.defaultScopes).not.toContain("folders:read");
|
|
67
|
+
if (Array.isArray(availableScopes)) {
|
|
68
|
+
expect(availableScopes.map(({ scope }) => scope)).toContain(
|
|
69
|
+
"folders:read",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// `file_code_connect:write` is only offered to apps owned by an
|
|
74
|
+
// Organization-plan team. Managed apps in other environments are not,
|
|
75
|
+
// so requesting it fails the whole authorization there.
|
|
76
|
+
expect(figma.defaultScopes).not.toContain("file_code_connect:write");
|
|
77
|
+
if (Array.isArray(availableScopes)) {
|
|
78
|
+
expect(availableScopes.map(({ scope }) => scope)).toContain(
|
|
79
|
+
"file_code_connect:write",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
// GET /v1/me backs both the ping and the identity label.
|
|
64
84
|
expect(figma.defaultScopes).toContain("current_user:read");
|
|
65
85
|
});
|
|
@@ -181,6 +201,81 @@ describe("PROVIDER_SEED_DATA managed mode wiring", () => {
|
|
|
181
201
|
expect(template?.hostPattern).toBe("*.myshopify.com");
|
|
182
202
|
});
|
|
183
203
|
|
|
204
|
+
test("quickbooks is wired up for managed mode behind its flag", () => {
|
|
205
|
+
const quickbooks = PROVIDER_SEED_DATA.quickbooks;
|
|
206
|
+
expect(quickbooks).toBeDefined();
|
|
207
|
+
expect(quickbooks.managedServiceConfigKey).toBe("quickbooks-oauth");
|
|
208
|
+
expect("quickbooks-oauth" in ServicesSchema.shape).toBe(true);
|
|
209
|
+
// Hidden until the platform side and client ids are live, like Figma
|
|
210
|
+
// and Shopify.
|
|
211
|
+
expect(quickbooks.featureFlag).toBe("quickbooks-oauth");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("quickbooks uses Intuit's OAuth endpoints with HTTP Basic", () => {
|
|
215
|
+
const quickbooks = PROVIDER_SEED_DATA.quickbooks;
|
|
216
|
+
expect(quickbooks.authorizeUrl).toBe(
|
|
217
|
+
"https://appcenter.intuit.com/connect/oauth2",
|
|
218
|
+
);
|
|
219
|
+
expect(quickbooks.tokenExchangeUrl).toBe(
|
|
220
|
+
"https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
|
|
221
|
+
);
|
|
222
|
+
expect(quickbooks.refreshUrl).toBe(quickbooks.tokenExchangeUrl);
|
|
223
|
+
expect(quickbooks.tokenEndpointAuthMethod).toBe("client_secret_basic");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("quickbooks keeps its per-company URL templates", () => {
|
|
227
|
+
// Every Accounting API path is scoped to the realm the user picked on
|
|
228
|
+
// Intuit's consent screen. The platform fills {realm_id} from the
|
|
229
|
+
// connection; a hardcoded realm here would send every company's calls
|
|
230
|
+
// to the wrong books.
|
|
231
|
+
const quickbooks = PROVIDER_SEED_DATA.quickbooks;
|
|
232
|
+
for (const url of [quickbooks.baseUrl, quickbooks.identityUrl]) {
|
|
233
|
+
expect(url).toContain("/v3/company/{realm_id}");
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("quickbooks seeds no ping or revoke URL", () => {
|
|
238
|
+
// The ping route sends the URL's origin as a base override and cannot
|
|
239
|
+
// fill {realm_id}; the daemon's revoke helper posts an unauthenticated
|
|
240
|
+
// form body that Intuit's JSON + Basic-auth endpoint rejects. Either
|
|
241
|
+
// would fail silently on every your-own connection.
|
|
242
|
+
const quickbooks = PROVIDER_SEED_DATA.quickbooks;
|
|
243
|
+
expect(quickbooks.pingUrl).toBeUndefined();
|
|
244
|
+
expect(quickbooks.revokeUrl).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("quickbooks requests only the accounting scope by default", () => {
|
|
248
|
+
// Payments needs a Payments-enabled app and the OpenID scopes only add
|
|
249
|
+
// the user's profile; requesting either widens the consent screen for
|
|
250
|
+
// nothing the Accounting API needs.
|
|
251
|
+
const quickbooks = PROVIDER_SEED_DATA.quickbooks;
|
|
252
|
+
expect(quickbooks.defaultScopes).toEqual([
|
|
253
|
+
"com.intuit.quickbooks.accounting",
|
|
254
|
+
]);
|
|
255
|
+
const available = quickbooks.availableScopes;
|
|
256
|
+
expect(Array.isArray(available)).toBe(true);
|
|
257
|
+
if (Array.isArray(available)) {
|
|
258
|
+
expect(available.map(({ scope }) => scope)).toContain(
|
|
259
|
+
"com.intuit.quickbooks.payment",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("quickbooks allows the credential on both Intuit API hosts", () => {
|
|
265
|
+
// Development keys only work against sandbox companies, which live on
|
|
266
|
+
// the sandbox host. Both hosts belong to Intuit and take the same
|
|
267
|
+
// bearer token.
|
|
268
|
+
const templates = PROVIDER_SEED_DATA.quickbooks.injectionTemplates ?? [];
|
|
269
|
+
expect(templates.map((t) => t.hostPattern).sort()).toEqual([
|
|
270
|
+
"quickbooks.api.intuit.com",
|
|
271
|
+
"sandbox-quickbooks.api.intuit.com",
|
|
272
|
+
]);
|
|
273
|
+
for (const template of templates) {
|
|
274
|
+
expect(template.headerName).toBe("Authorization");
|
|
275
|
+
expect(template.valuePrefix).toBe("Bearer ");
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
184
279
|
test("every managedServiceConfigKey resolves to a ServicesSchema key", () => {
|
|
185
280
|
// Cross-repo invariant: a provider with managedServiceConfigKey but no
|
|
186
281
|
// matching ServicesSchema entry silently falls back to BYO mode in
|
|
@@ -72,6 +72,7 @@ import { setConfig } from "../__tests__/helpers/set-config.js";
|
|
|
72
72
|
import { BYOOAuthConnection } from "./byo-connection.js";
|
|
73
73
|
import {
|
|
74
74
|
formatNoConnectionError,
|
|
75
|
+
platformProxyBaseUrl,
|
|
75
76
|
resolveEffectiveBaseUrl,
|
|
76
77
|
resolveOAuthConnection,
|
|
77
78
|
resolveOAuthConnectionWithMeta,
|
|
@@ -755,6 +756,32 @@ describe("resolveEffectiveBaseUrl", () => {
|
|
|
755
756
|
});
|
|
756
757
|
});
|
|
757
758
|
|
|
759
|
+
describe("platformProxyBaseUrl", () => {
|
|
760
|
+
test("forwards a concrete seed base URL", () => {
|
|
761
|
+
expect(platformProxyBaseUrl("https://api.figma.com")).toBe(
|
|
762
|
+
"https://api.figma.com",
|
|
763
|
+
);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test("withholds a templated base URL so the platform fills it", () => {
|
|
767
|
+
// Shopify's host and QuickBooks' realm are pinned to the connection on
|
|
768
|
+
// the platform. Forwarding the unfilled template would fail the proxy's
|
|
769
|
+
// allowlist check and shadow the platform's own default.
|
|
770
|
+
expect(platformProxyBaseUrl("https://{tenant_host}")).toBeUndefined();
|
|
771
|
+
expect(
|
|
772
|
+
platformProxyBaseUrl(
|
|
773
|
+
"https://quickbooks.api.intuit.com/v3/company/{realm_id}",
|
|
774
|
+
),
|
|
775
|
+
).toBeUndefined();
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test("treats a missing base URL as no override", () => {
|
|
779
|
+
expect(platformProxyBaseUrl(undefined)).toBeUndefined();
|
|
780
|
+
expect(platformProxyBaseUrl(null)).toBeUndefined();
|
|
781
|
+
expect(platformProxyBaseUrl("")).toBeUndefined();
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
758
785
|
describe("resolveTokenHeader", () => {
|
|
759
786
|
const shopify = JSON.stringify([
|
|
760
787
|
{
|
|
@@ -129,7 +129,10 @@ export async function resolveOAuthConnectionWithMeta(
|
|
|
129
129
|
accountInfo: resolution.accountLabel ?? account ?? null,
|
|
130
130
|
client,
|
|
131
131
|
connectionId: resolution.id,
|
|
132
|
-
|
|
132
|
+
// A templated base URL (`https://{tenant_host}`, `.../{realm_id}`) is
|
|
133
|
+
// filled in by the platform from what it pinned to the connection;
|
|
134
|
+
// sent as-is it would fail the proxy's allowlist and shadow that.
|
|
135
|
+
baseUrl: platformProxyBaseUrl(providerRow?.baseUrl),
|
|
133
136
|
});
|
|
134
137
|
return {
|
|
135
138
|
connection,
|
|
@@ -317,6 +320,24 @@ function hostMatchesPattern(host: string, pattern: string): boolean {
|
|
|
317
320
|
return host === p;
|
|
318
321
|
}
|
|
319
322
|
|
|
323
|
+
/**
|
|
324
|
+
* The base URL a managed connection sends to the platform proxy, or
|
|
325
|
+
* `undefined` to let the proxy use the provider's configured default.
|
|
326
|
+
*
|
|
327
|
+
* Per-tenant and per-realm providers seed a template the platform fills from
|
|
328
|
+
* the connection (`{tenant_host}`, `{realm_id}`). Forwarding the unfilled
|
|
329
|
+
* template would fail the proxy's allowlist check, so those fall through to
|
|
330
|
+
* the platform's own default.
|
|
331
|
+
*/
|
|
332
|
+
export function platformProxyBaseUrl(
|
|
333
|
+
seedBaseUrl: string | null | undefined,
|
|
334
|
+
): string | undefined {
|
|
335
|
+
if (!seedBaseUrl || /\{[a-z_]+\}/.test(seedBaseUrl)) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
return seedBaseUrl;
|
|
339
|
+
}
|
|
340
|
+
|
|
320
341
|
/**
|
|
321
342
|
* Resolve the effective API base URL for a connection, preferring per-tenant
|
|
322
343
|
* values stored on the connection's `metadata` over the provider's static
|
|
@@ -394,6 +415,9 @@ interface PlatformConnectionEntry {
|
|
|
394
415
|
/** Scopes the platform actually granted this connection. May be absent for
|
|
395
416
|
* older connections or providers that don't report scopes. */
|
|
396
417
|
scopes_granted?: string[] | null;
|
|
418
|
+
/** Provider-supplied values the connection is scoped by (QuickBooks'
|
|
419
|
+
* `realm_id`). Absent from older platforms; empty for most providers. */
|
|
420
|
+
provider_params?: Record<string, string> | null;
|
|
397
421
|
}
|
|
398
422
|
|
|
399
423
|
interface PlatformConnectionResolution {
|
|
@@ -20,6 +20,8 @@ export type SerializedProvider = ReturnType<typeof serializeProvider> &
|
|
|
20
20
|
|
|
21
21
|
import { isChannelBotProvider } from "@vellumai/service-contracts/channels";
|
|
22
22
|
|
|
23
|
+
import { PROVIDER_SEED_DATA } from "./seed-providers.js";
|
|
24
|
+
|
|
23
25
|
/**
|
|
24
26
|
* Lightweight summary projection of an OAuth provider, suitable for API
|
|
25
27
|
* list responses where full detail is not needed. All keys are snake_case
|
|
@@ -36,6 +38,12 @@ export interface SerializedProviderSummary {
|
|
|
36
38
|
supports_managed_mode: boolean;
|
|
37
39
|
managed_service_is_paid: boolean;
|
|
38
40
|
feature_flag: string | null;
|
|
41
|
+
/**
|
|
42
|
+
* Per-tenant providers only (Shopify): what a client must collect before
|
|
43
|
+
* starting a managed connect, since the provider's OAuth endpoints live on
|
|
44
|
+
* the customer's own host. `null` for providers with one global host.
|
|
45
|
+
*/
|
|
46
|
+
tenant_host: { pattern: string; label: string; placeholder: string } | null;
|
|
39
47
|
/**
|
|
40
48
|
* Which sense of "connected" this provider represents: `assistant` for a bot
|
|
41
49
|
* credential people reach the assistant through, `user` for a grant letting
|
|
@@ -164,6 +172,7 @@ export function serializeProviderSummary(
|
|
|
164
172
|
supports_managed_mode: !!row.managedServiceConfigKey,
|
|
165
173
|
managed_service_is_paid: !!row.managedServiceIsPaid,
|
|
166
174
|
feature_flag: row.featureFlag ?? null,
|
|
175
|
+
tenant_host: PROVIDER_SEED_DATA[row.provider]?.tenantHost ?? null,
|
|
167
176
|
acts_as: isChannelBotProvider(row.provider) ? "assistant" : "user",
|
|
168
177
|
};
|
|
169
178
|
}
|