@vellumai/assistant 0.5.3 → 0.5.5
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/Dockerfile +18 -27
- package/docs/architecture/memory.md +105 -0
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/archive-recall.test.ts +560 -0
- package/src/__tests__/conversation-clear-safety.test.ts +259 -0
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
- package/src/__tests__/memory-reducer-job.test.ts +538 -0
- package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
- package/src/__tests__/memory-reducer-types.test.ts +12 -4
- package/src/__tests__/memory-reducer.test.ts +7 -1
- package/src/__tests__/memory-regressions.test.ts +24 -4
- package/src/__tests__/memory-simplified-config.test.ts +4 -4
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
- package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +18 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +0 -1
- package/src/config/schemas/memory-simplified.ts +1 -1
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/config-watcher.ts +4 -1
- package/src/daemon/conversation-memory.ts +117 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +11 -0
- package/src/daemon/lifecycle.ts +51 -2
- package/src/daemon/providers-setup.ts +2 -1
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/archive-recall.ts +516 -0
- package/src/memory/brief-time.ts +5 -4
- package/src/memory/conversation-crud.ts +210 -0
- package/src/memory/conversation-key-store.ts +33 -4
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
- package/src/memory/job-handlers/conversation-starters.ts +24 -30
- package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
- package/src/memory/jobs-store.ts +2 -0
- package/src/memory/jobs-worker.ts +8 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
- package/src/memory/migrations/141-rename-verification-table.ts +8 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
- package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
- package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/reducer-scheduler.ts +242 -0
- package/src/memory/reducer-types.ts +9 -2
- package/src/memory/reducer.ts +25 -11
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/trust-client.ts +343 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +523 -36
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +10 -1
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +88 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/secret-routes.ts +5 -1
- package/src/schedule/schedule-store.ts +7 -0
- package/src/schedule/scheduler.ts +6 -2
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +22 -20
- package/src/tools/filesystem/edit.ts +6 -1
- package/src/tools/filesystem/read.ts +6 -1
- package/src/tools/filesystem/write.ts +6 -1
- package/src/tools/memory/handlers.ts +129 -1
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +5 -1
- package/src/tools/schedule/update.ts +6 -0
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for CES credential CRUD endpoints.
|
|
3
|
+
*
|
|
4
|
+
* In containerized mode the assistant cannot access `keys.enc` directly.
|
|
5
|
+
* Instead, the CES sidecar exposes credential management over HTTP and the
|
|
6
|
+
* assistant talks to it via this client.
|
|
7
|
+
*
|
|
8
|
+
* Endpoints (served by `credential-executor/src/http/credential-routes.ts`):
|
|
9
|
+
* - GET /v1/credentials → { accounts: string[] }
|
|
10
|
+
* - GET /v1/credentials/:account → { account, value } | 404
|
|
11
|
+
* - POST /v1/credentials/:account → { ok: true, account }
|
|
12
|
+
* - DELETE /v1/credentials/:account → { ok: true, account } | 404 | 500
|
|
13
|
+
*
|
|
14
|
+
* Auth: Bearer token from `CES_SERVICE_TOKEN` env var.
|
|
15
|
+
* Base URL: `CES_CREDENTIAL_URL` env var (e.g. `http://ces-container:8090`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getLogger } from "../util/logger.js";
|
|
19
|
+
import type { CredentialBackend, DeleteResult } from "./credential-backend.js";
|
|
20
|
+
|
|
21
|
+
const log = getLogger("ces-credential-client");
|
|
22
|
+
|
|
23
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Env helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function getBaseUrl(): string | undefined {
|
|
30
|
+
return process.env.CES_CREDENTIAL_URL;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getServiceToken(): string | undefined {
|
|
34
|
+
return process.env.CES_SERVICE_TOKEN;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Internal fetch wrapper
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
async function cesRequest(
|
|
42
|
+
method: string,
|
|
43
|
+
path: string,
|
|
44
|
+
body?: unknown,
|
|
45
|
+
): Promise<Response | null> {
|
|
46
|
+
const baseUrl = getBaseUrl();
|
|
47
|
+
const token = getServiceToken();
|
|
48
|
+
if (!baseUrl || !token) return null;
|
|
49
|
+
|
|
50
|
+
const url = `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
51
|
+
const headers: Record<string, string> = {
|
|
52
|
+
Authorization: `Bearer ${token}`,
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return await fetch(url, {
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
61
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
log.warn({ err, method, path }, "CES credential request failed");
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// CesCredentialBackend
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export class CesCredentialBackend implements CredentialBackend {
|
|
74
|
+
readonly name = "ces-http";
|
|
75
|
+
|
|
76
|
+
isAvailable(): boolean {
|
|
77
|
+
return !!getBaseUrl() && !!getServiceToken();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async get(account: string): Promise<string | undefined> {
|
|
81
|
+
try {
|
|
82
|
+
const res = await cesRequest(
|
|
83
|
+
"GET",
|
|
84
|
+
`/v1/credentials/${encodeURIComponent(account)}`,
|
|
85
|
+
);
|
|
86
|
+
if (!res) return undefined;
|
|
87
|
+
if (res.status === 404) return undefined;
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
log.warn(
|
|
90
|
+
{ account, status: res.status },
|
|
91
|
+
"CES credential get returned non-OK status",
|
|
92
|
+
);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const data = (await res.json()) as { value?: string };
|
|
96
|
+
return data.value;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
log.warn({ err, account }, "CES credential get threw unexpectedly");
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async set(account: string, value: string): Promise<boolean> {
|
|
104
|
+
try {
|
|
105
|
+
const res = await cesRequest(
|
|
106
|
+
"POST",
|
|
107
|
+
`/v1/credentials/${encodeURIComponent(account)}`,
|
|
108
|
+
{ value },
|
|
109
|
+
);
|
|
110
|
+
if (!res) return false;
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
log.warn(
|
|
113
|
+
{ account, status: res.status },
|
|
114
|
+
"CES credential set returned non-OK status",
|
|
115
|
+
);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.warn({ err, account }, "CES credential set threw unexpectedly");
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async delete(account: string): Promise<DeleteResult> {
|
|
126
|
+
try {
|
|
127
|
+
const res = await cesRequest(
|
|
128
|
+
"DELETE",
|
|
129
|
+
`/v1/credentials/${encodeURIComponent(account)}`,
|
|
130
|
+
);
|
|
131
|
+
if (!res) return "error";
|
|
132
|
+
if (res.status === 404) return "not-found";
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
log.warn(
|
|
135
|
+
{ account, status: res.status },
|
|
136
|
+
"CES credential delete returned non-OK status",
|
|
137
|
+
);
|
|
138
|
+
return "error";
|
|
139
|
+
}
|
|
140
|
+
return "deleted";
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log.warn({ err, account }, "CES credential delete threw unexpectedly");
|
|
143
|
+
return "error";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async list(): Promise<string[]> {
|
|
148
|
+
try {
|
|
149
|
+
const res = await cesRequest("GET", "/v1/credentials");
|
|
150
|
+
if (!res) return [];
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
log.warn(
|
|
153
|
+
{ status: res.status },
|
|
154
|
+
"CES credential list returned non-OK status",
|
|
155
|
+
);
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const data = (await res.json()) as { accounts?: string[] };
|
|
159
|
+
return data.accounts ?? [];
|
|
160
|
+
} catch (err) {
|
|
161
|
+
log.warn({ err }, "CES credential list threw unexpectedly");
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Factory
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
export function createCesCredentialBackend(): CesCredentialBackend {
|
|
172
|
+
return new CesCredentialBackend();
|
|
173
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* adapters.
|
|
4
4
|
*
|
|
5
5
|
* Backend selection (`resolveBackend`) is the single decision point:
|
|
6
|
+
* - Containerized (IS_CONTAINERIZED + CES_CREDENTIAL_URL set): CES HTTP client.
|
|
6
7
|
* - Production (VELLUM_DEV unset or "0"): keychain backend when available.
|
|
7
8
|
* - Dev mode (VELLUM_DEV=1): encrypted file store always.
|
|
8
9
|
*
|
|
@@ -16,7 +17,10 @@ import type {
|
|
|
16
17
|
SecureKeyDeleteResult,
|
|
17
18
|
} from "@vellumai/credential-storage";
|
|
18
19
|
|
|
20
|
+
import providerEnvVarsRegistry from "../../../meta/provider-env-vars.json" with { type: "json" };
|
|
21
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
19
22
|
import { getLogger } from "../util/logger.js";
|
|
23
|
+
import { createCesCredentialBackend } from "./ces-credential-client.js";
|
|
20
24
|
import type { CredentialBackend, DeleteResult } from "./credential-backend.js";
|
|
21
25
|
import {
|
|
22
26
|
createEncryptedStoreBackend,
|
|
@@ -50,18 +54,39 @@ function getEncryptedStoreBackend(): CredentialBackend {
|
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
56
|
* Resolve the primary credential backend for this process.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
57
|
+
*
|
|
58
|
+
* Priority:
|
|
59
|
+
* 1. Containerized + CES_CREDENTIAL_URL → CES HTTP client (skip keychain
|
|
60
|
+
* and encrypted store entirely — the sidecar owns credential storage).
|
|
61
|
+
* 2. Production (VELLUM_DEV unset or "0") → keychain when available.
|
|
62
|
+
* 3. Dev mode (VELLUM_DEV=1) → encrypted file store always.
|
|
55
63
|
*
|
|
56
64
|
* Once resolved, the backend does not change during the process lifetime.
|
|
57
65
|
* Call `_resetBackend()` in tests to clear the cached resolution.
|
|
58
66
|
*/
|
|
59
67
|
function resolveBackend(): CredentialBackend {
|
|
60
68
|
if (!_resolvedBackend) {
|
|
61
|
-
if (process.env.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
if (getIsContainerized() && process.env.CES_CREDENTIAL_URL) {
|
|
70
|
+
const ces = createCesCredentialBackend();
|
|
71
|
+
if (ces.isAvailable()) {
|
|
72
|
+
_resolvedBackend = ces;
|
|
73
|
+
} else {
|
|
74
|
+
log.warn(
|
|
75
|
+
"CES_CREDENTIAL_URL is set but CES backend is not available — " +
|
|
76
|
+
"falling back to local credential store",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!_resolvedBackend) {
|
|
82
|
+
if (
|
|
83
|
+
process.env.VELLUM_DEV !== "1" &&
|
|
84
|
+
getKeychainBackend().isAvailable()
|
|
85
|
+
) {
|
|
86
|
+
_resolvedBackend = getKeychainBackend();
|
|
87
|
+
} else {
|
|
88
|
+
_resolvedBackend = getEncryptedStoreBackend();
|
|
89
|
+
}
|
|
65
90
|
}
|
|
66
91
|
}
|
|
67
92
|
return _resolvedBackend;
|
|
@@ -70,6 +95,8 @@ function resolveBackend(): CredentialBackend {
|
|
|
70
95
|
/**
|
|
71
96
|
* List all account names across both backends (async).
|
|
72
97
|
*
|
|
98
|
+
* In CES mode, only the CES backend is queried — there are no local stores.
|
|
99
|
+
*
|
|
73
100
|
* When the primary backend is the keychain, this merges keys from the keychain
|
|
74
101
|
* and the encrypted store (for legacy keys that haven't been migrated). The
|
|
75
102
|
* result is deduplicated. When the primary backend is already the encrypted
|
|
@@ -79,6 +106,9 @@ export async function listSecureKeysAsync(): Promise<string[]> {
|
|
|
79
106
|
const backend = resolveBackend();
|
|
80
107
|
const primaryKeys = await backend.list();
|
|
81
108
|
|
|
109
|
+
// CES mode — the sidecar is the single source of truth, no local merge.
|
|
110
|
+
if (backend.name === "ces-http") return primaryKeys;
|
|
111
|
+
|
|
82
112
|
// If primary backend is NOT the encrypted store, also check
|
|
83
113
|
// the encrypted store for legacy keys that haven't been migrated.
|
|
84
114
|
if (backend !== getEncryptedStoreBackend()) {
|
|
@@ -96,8 +126,10 @@ export async function listSecureKeysAsync(): Promise<string[]> {
|
|
|
96
126
|
|
|
97
127
|
/**
|
|
98
128
|
* Retrieve a secret from secure storage. Reads from the primary backend
|
|
99
|
-
* first.
|
|
100
|
-
*
|
|
129
|
+
* first. In CES mode, the sidecar is the single source of truth — no
|
|
130
|
+
* local fallback. In local mode, if the primary backend is the keychain,
|
|
131
|
+
* falls back to the encrypted store for legacy keys that haven't been
|
|
132
|
+
* migrated.
|
|
101
133
|
*/
|
|
102
134
|
export async function getSecureKeyAsync(
|
|
103
135
|
account: string,
|
|
@@ -106,6 +138,9 @@ export async function getSecureKeyAsync(
|
|
|
106
138
|
const result = await backend.get(account);
|
|
107
139
|
if (result != null) return result;
|
|
108
140
|
|
|
141
|
+
// CES mode — no local fallback.
|
|
142
|
+
if (backend.name === "ces-http") return undefined;
|
|
143
|
+
|
|
109
144
|
// Legacy fallback: if primary backend is NOT the encrypted store,
|
|
110
145
|
// check the encrypted store for keys that haven't been migrated.
|
|
111
146
|
if (backend !== getEncryptedStoreBackend()) {
|
|
@@ -135,13 +170,25 @@ export async function setSecureKeyAsync(
|
|
|
135
170
|
}
|
|
136
171
|
|
|
137
172
|
/**
|
|
138
|
-
* Delete a secret from secure storage.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
173
|
+
* Delete a secret from secure storage.
|
|
174
|
+
*
|
|
175
|
+
* In containerized mode with CES, deletion is routed exclusively through the
|
|
176
|
+
* CES backend — there are no local stores to clean up.
|
|
177
|
+
*
|
|
178
|
+
* In local mode, always attempts deletion on both the keychain backend (if
|
|
179
|
+
* available) and the encrypted store backend, regardless of routing mode.
|
|
180
|
+
* This cleans up legacy data from both stores.
|
|
141
181
|
*/
|
|
142
182
|
export async function deleteSecureKeyAsync(
|
|
143
183
|
account: string,
|
|
144
184
|
): Promise<DeleteResult> {
|
|
185
|
+
const backend = resolveBackend();
|
|
186
|
+
|
|
187
|
+
// In CES mode, the sidecar is the only store — no local cleanup needed.
|
|
188
|
+
if (backend.name === "ces-http") {
|
|
189
|
+
return backend.delete(account);
|
|
190
|
+
}
|
|
191
|
+
|
|
145
192
|
const keychain = getKeychainBackend();
|
|
146
193
|
const enc = getEncryptedStoreBackend();
|
|
147
194
|
|
|
@@ -164,18 +211,14 @@ export async function deleteSecureKeyAsync(
|
|
|
164
211
|
// ---------------------------------------------------------------------------
|
|
165
212
|
|
|
166
213
|
/**
|
|
167
|
-
* Env var names keyed by provider.
|
|
168
|
-
*
|
|
214
|
+
* Env var names keyed by provider. Loaded from the shared registry at
|
|
215
|
+
* `meta/provider-env-vars.json` — the single source of truth also consumed
|
|
216
|
+
* by the CLI and the macOS client.
|
|
217
|
+
* Ollama is intentionally omitted from the registry — it doesn't require
|
|
218
|
+
* an API key.
|
|
169
219
|
*/
|
|
170
|
-
const PROVIDER_ENV_VARS: Record<string, string> =
|
|
171
|
-
|
|
172
|
-
openai: "OPENAI_API_KEY",
|
|
173
|
-
gemini: "GEMINI_API_KEY",
|
|
174
|
-
fireworks: "FIREWORKS_API_KEY",
|
|
175
|
-
openrouter: "OPENROUTER_API_KEY",
|
|
176
|
-
brave: "BRAVE_API_KEY",
|
|
177
|
-
perplexity: "PERPLEXITY_API_KEY",
|
|
178
|
-
};
|
|
220
|
+
const PROVIDER_ENV_VARS: Record<string, string> =
|
|
221
|
+
providerEnvVarsRegistry.providers;
|
|
179
222
|
|
|
180
223
|
/**
|
|
181
224
|
* Retrieve a provider API key, checking secure storage first and falling
|
package/src/signals/bash.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { spawn } from "node:child_process";
|
|
|
20
20
|
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
|
|
23
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
23
24
|
import { getLogger } from "../util/logger.js";
|
|
24
25
|
import { getSignalsDir, getWorkspaceDir } from "../util/platform.js";
|
|
25
26
|
|
|
@@ -65,6 +66,8 @@ function writeResult(requestId: string, result: BashSignalResult): void {
|
|
|
65
66
|
* when a matching signal file is created or modified.
|
|
66
67
|
*/
|
|
67
68
|
export function handleBashSignal(filename: string): void {
|
|
69
|
+
if (getIsContainerized()) return;
|
|
70
|
+
|
|
68
71
|
if (!isDebugMode()) {
|
|
69
72
|
log.warn(
|
|
70
73
|
{ filename },
|
package/src/signals/cancel.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
|
|
16
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
16
17
|
import { getLogger } from "../util/logger.js";
|
|
17
18
|
import { getSignalsDir } from "../util/platform.js";
|
|
18
19
|
|
|
@@ -39,6 +40,8 @@ export function registerCancelCallback(cb: CancelCallback): void {
|
|
|
39
40
|
* Called by ConfigWatcher when the signal file is written or modified.
|
|
40
41
|
*/
|
|
41
42
|
export function handleCancelSignal(): void {
|
|
43
|
+
if (getIsContainerized()) return;
|
|
44
|
+
|
|
42
45
|
try {
|
|
43
46
|
const content = readFileSync(join(getSignalsDir(), "cancel"), "utf-8");
|
|
44
47
|
const parsed = JSON.parse(content) as { conversationId?: string };
|
package/src/signals/confirm.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
13
14
|
import type { UserDecision } from "../permissions/types.js";
|
|
14
15
|
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
15
16
|
import { getLogger } from "../util/logger.js";
|
|
@@ -37,6 +38,8 @@ function isUserDecision(value: string): value is UserDecision {
|
|
|
37
38
|
* Called by ConfigWatcher when the signal file is written or modified.
|
|
38
39
|
*/
|
|
39
40
|
export function handleConfirmationSignal(): void {
|
|
41
|
+
if (getIsContainerized()) return;
|
|
42
|
+
|
|
40
43
|
try {
|
|
41
44
|
const content = readFileSync(join(getSignalsDir(), "confirm"), "utf-8");
|
|
42
45
|
const parsed = JSON.parse(content) as {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
19
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
19
20
|
import { getLogger } from "../util/logger.js";
|
|
20
21
|
import { getSignalsDir } from "../util/platform.js";
|
|
21
22
|
|
|
@@ -46,6 +47,8 @@ export function registerConversationUndoCallback(cb: UndoCallback): void {
|
|
|
46
47
|
* file is written.
|
|
47
48
|
*/
|
|
48
49
|
export async function handleConversationUndoSignal(): Promise<void> {
|
|
50
|
+
if (getIsContainerized()) return;
|
|
51
|
+
|
|
49
52
|
const resultPath = join(getSignalsDir(), "conversation-undo.result");
|
|
50
53
|
|
|
51
54
|
const writeResult = (
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "node:fs";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
|
|
27
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
27
28
|
import type { AssistantEvent } from "../runtime/assistant-event.js";
|
|
28
29
|
import { getSignalsDir } from "../util/platform.js";
|
|
29
30
|
|
|
@@ -86,6 +87,8 @@ export function appendEventToStream(
|
|
|
86
87
|
conversationId: string,
|
|
87
88
|
event: AssistantEvent,
|
|
88
89
|
): void {
|
|
90
|
+
if (getIsContainerized()) return;
|
|
91
|
+
|
|
89
92
|
const dirs = getSubscriberDirs(conversationId);
|
|
90
93
|
if (dirs.length === 0) return;
|
|
91
94
|
|
|
@@ -130,6 +133,10 @@ export function watchEventStream(
|
|
|
130
133
|
conversationId: string,
|
|
131
134
|
callback: (event: AssistantEvent) => void,
|
|
132
135
|
): EventStreamWatcher {
|
|
136
|
+
if (getIsContainerized()) {
|
|
137
|
+
return { dispose() {} };
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
const parentDir = eventsDir();
|
|
134
141
|
mkdirSync(parentDir, { recursive: true });
|
|
135
142
|
const subDir = join(parentDir, `${conversationId}.${process.pid}`);
|
package/src/signals/shotgun.ts
CHANGED
|
@@ -15,6 +15,7 @@ import crypto from "node:crypto";
|
|
|
15
15
|
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
|
|
18
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
18
19
|
import {
|
|
19
20
|
fireWatchCompletionNotifier,
|
|
20
21
|
fireWatchStartNotifier,
|
|
@@ -54,6 +55,8 @@ function writeResult(requestId: string, result: ShotgunResult): void {
|
|
|
54
55
|
* when a matching signal file is created or modified.
|
|
55
56
|
*/
|
|
56
57
|
export function handleShotgunSignal(filename: string): void {
|
|
58
|
+
if (getIsContainerized()) return;
|
|
59
|
+
|
|
57
60
|
const signalPath = join(getSignalsDir(), filename);
|
|
58
61
|
let raw: string;
|
|
59
62
|
try {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
|
|
15
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
15
16
|
import { addRule } from "../permissions/trust-store.js";
|
|
16
17
|
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
17
18
|
import { getTool } from "../tools/registry.js";
|
|
@@ -27,6 +28,8 @@ const VALID_TRUST_DECISIONS: ReadonlySet<string> = new Set(["allow", "deny"]);
|
|
|
27
28
|
* Called by ConfigWatcher when the signal file is written or modified.
|
|
28
29
|
*/
|
|
29
30
|
export function handleTrustRuleSignal(): void {
|
|
31
|
+
if (getIsContainerized()) return;
|
|
32
|
+
|
|
30
33
|
const resultPath = join(getSignalsDir(), "trust-rule.result");
|
|
31
34
|
|
|
32
35
|
const writeError = (requestId: string | undefined, error: string): void => {
|
|
@@ -41,14 +41,12 @@ mock.module("../memory/turn-events-store.js", () => ({
|
|
|
41
41
|
queryUnreportedTurnEvents: mockQueryUnreportedTurnEvents,
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
enabled: false,
|
|
46
|
-
platformBaseUrl: "",
|
|
47
|
-
assistantApiKey: "",
|
|
48
|
-
}));
|
|
44
|
+
let mockPlatformClient: Record<string, unknown> | null = null;
|
|
49
45
|
|
|
50
|
-
mock.module("../
|
|
51
|
-
|
|
46
|
+
mock.module("../platform/client.js", () => ({
|
|
47
|
+
VellumPlatformClient: {
|
|
48
|
+
create: async () => mockPlatformClient,
|
|
49
|
+
},
|
|
52
50
|
}));
|
|
53
51
|
|
|
54
52
|
const mockGetTelemetryPlatformUrl = mock(() => "https://platform.vellum.ai");
|
|
@@ -142,7 +140,7 @@ beforeEach(() => {
|
|
|
142
140
|
mockQueryUnreportedUsageEvents.mockReset();
|
|
143
141
|
mockQueryUnreportedTurnEvents.mockReset();
|
|
144
142
|
mockQueryUnreportedTurnEvents.mockReturnValue([]);
|
|
145
|
-
|
|
143
|
+
mockPlatformClient = null;
|
|
146
144
|
mockGetTelemetryPlatformUrl.mockReset();
|
|
147
145
|
mockGetTelemetryAppToken.mockReset();
|
|
148
146
|
mockGetDeviceId.mockReset();
|
|
@@ -156,11 +154,6 @@ beforeEach(() => {
|
|
|
156
154
|
|
|
157
155
|
// Defaults
|
|
158
156
|
mockGetMemoryCheckpoint.mockReturnValue(null);
|
|
159
|
-
mockResolveManagedProxyContext.mockResolvedValue({
|
|
160
|
-
enabled: false,
|
|
161
|
-
platformBaseUrl: "",
|
|
162
|
-
assistantApiKey: "",
|
|
163
|
-
});
|
|
164
157
|
mockGetTelemetryPlatformUrl.mockReturnValue("https://platform.vellum.ai");
|
|
165
158
|
mockGetTelemetryAppToken.mockReturnValue("default-test-token");
|
|
166
159
|
|
|
@@ -179,37 +172,31 @@ afterEach(() => {
|
|
|
179
172
|
// ---------------------------------------------------------------------------
|
|
180
173
|
|
|
181
174
|
describe("UsageTelemetryReporter", () => {
|
|
182
|
-
test("authenticated flush uses
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
platformBaseUrl: "https://test.vellum.ai",
|
|
186
|
-
assistantApiKey: "test-key",
|
|
175
|
+
test("authenticated flush uses client.fetch with platform path", async () => {
|
|
176
|
+
const clientFetchMock = mock(async (_path: string, _init?: RequestInit) => {
|
|
177
|
+
return new Response('{"accepted":2}', { status: 200 });
|
|
187
178
|
});
|
|
179
|
+
mockPlatformClient = {
|
|
180
|
+
baseUrl: "https://test.vellum.ai",
|
|
181
|
+
assistantApiKey: "test-key",
|
|
182
|
+
platformAssistantId: "asst-123",
|
|
183
|
+
fetch: clientFetchMock,
|
|
184
|
+
};
|
|
188
185
|
const events = [makeUsageEvent(), makeUsageEvent()];
|
|
189
186
|
mockQueryUnreportedUsageEvents.mockReturnValue(events);
|
|
190
|
-
mockFetch.mockImplementation(() =>
|
|
191
|
-
Promise.resolve(
|
|
192
|
-
new Response(`{"accepted":${events.length}}`, { status: 200 }),
|
|
193
|
-
),
|
|
194
|
-
);
|
|
195
187
|
|
|
196
188
|
const reporter = new UsageTelemetryReporter();
|
|
197
189
|
await reporter.flush();
|
|
198
190
|
|
|
199
|
-
expect(
|
|
200
|
-
const [
|
|
201
|
-
expect(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
);
|
|
191
|
+
expect(clientFetchMock).toHaveBeenCalledTimes(1);
|
|
192
|
+
const [path] = clientFetchMock.mock.calls[0] as [string, RequestInit];
|
|
193
|
+
expect(path).toBe("/v1/telemetry/ingest/");
|
|
194
|
+
// globalThis.fetch should NOT have been called directly
|
|
195
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
205
196
|
});
|
|
206
197
|
|
|
207
198
|
test("anonymous flush uses X-Telemetry-Token and default URL", async () => {
|
|
208
|
-
|
|
209
|
-
enabled: false,
|
|
210
|
-
platformBaseUrl: "",
|
|
211
|
-
assistantApiKey: "",
|
|
212
|
-
});
|
|
199
|
+
mockPlatformClient = null;
|
|
213
200
|
mockGetTelemetryPlatformUrl.mockReturnValue("https://platform.test.ai");
|
|
214
201
|
mockGetTelemetryAppToken.mockReturnValue("anon-token");
|
|
215
202
|
|
|
@@ -375,9 +362,9 @@ describe("UsageTelemetryReporter", () => {
|
|
|
375
362
|
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
376
363
|
);
|
|
377
364
|
|
|
378
|
-
// Top-level: installation_id,
|
|
365
|
+
// Top-level: installation_id, assistant_version, and events array (no turn_events key)
|
|
379
366
|
expect(body.installation_id).toBe("test-device-id");
|
|
380
|
-
expect(body.
|
|
367
|
+
expect(body.assistant_version).toBe("1.2.3-test");
|
|
381
368
|
expect(Array.isArray(body.events)).toBe(true);
|
|
382
369
|
expect(body.events.length).toBe(1);
|
|
383
370
|
expect(body.turn_events).toBeUndefined();
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { queryUnreportedLifecycleEvents } from "../memory/lifecycle-events-store.js";
|
|
23
23
|
import { queryUnreportedUsageEvents } from "../memory/llm-usage-store.js";
|
|
24
24
|
import { queryUnreportedTurnEvents } from "../memory/turn-events-store.js";
|
|
25
|
-
import {
|
|
25
|
+
import { VellumPlatformClient } from "../platform/client.js";
|
|
26
26
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
27
27
|
import { getExternalAssistantId } from "../runtime/auth/external-assistant-id.js";
|
|
28
28
|
import { getDeviceId } from "../util/device-id.js";
|
|
@@ -139,22 +139,11 @@ export class UsageTelemetryReporter {
|
|
|
139
139
|
return;
|
|
140
140
|
|
|
141
141
|
// Resolve auth context — skip flush when neither auth mode is viable
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
142
|
+
const client = await VellumPlatformClient.create();
|
|
143
|
+
if (!client && !getTelemetryAppToken()) {
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
let url: string;
|
|
148
|
-
let authHeaders: Record<string, string>;
|
|
149
|
-
|
|
150
|
-
if (proxyCtx.enabled) {
|
|
151
|
-
url = `${proxyCtx.platformBaseUrl}${TELEMETRY_PATH}`;
|
|
152
|
-
authHeaders = { Authorization: `Api-Key ${proxyCtx.assistantApiKey}` };
|
|
153
|
-
} else {
|
|
154
|
-
url = `${getTelemetryPlatformUrl()}${TELEMETRY_PATH}`;
|
|
155
|
-
authHeaders = { "X-Telemetry-Token": getTelemetryAppToken() };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
147
|
// Build payload
|
|
159
148
|
const typedEvents: TelemetryEvent[] = [
|
|
160
149
|
...events.map(
|
|
@@ -193,28 +182,41 @@ export class UsageTelemetryReporter {
|
|
|
193
182
|
const organizationId = getPlatformOrganizationId() || undefined;
|
|
194
183
|
const userId = getPlatformUserId() || undefined;
|
|
195
184
|
const payload = {
|
|
196
|
-
|
|
185
|
+
device_id: getDeviceId(),
|
|
197
186
|
assistant_id: assistantId,
|
|
198
|
-
|
|
187
|
+
assistant_version: APP_VERSION,
|
|
199
188
|
...(organizationId ? { organization_id: organizationId } : {}),
|
|
200
189
|
...(userId ? { user_id: userId } : {}),
|
|
201
190
|
events: typedEvents,
|
|
202
191
|
};
|
|
203
192
|
|
|
204
193
|
// Send
|
|
205
|
-
const
|
|
194
|
+
const fetchInit: RequestInit = {
|
|
206
195
|
method: "POST",
|
|
207
196
|
headers: {
|
|
208
197
|
"Content-Type": "application/json",
|
|
209
|
-
...authHeaders,
|
|
210
198
|
},
|
|
211
199
|
body: JSON.stringify(payload),
|
|
212
|
-
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
let resp: Response;
|
|
203
|
+
if (client) {
|
|
204
|
+
resp = await client.fetch(TELEMETRY_PATH, fetchInit);
|
|
205
|
+
} else {
|
|
206
|
+
const url = `${getTelemetryPlatformUrl()}${TELEMETRY_PATH}`;
|
|
207
|
+
resp = await fetch(url, {
|
|
208
|
+
...fetchInit,
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
"X-Telemetry-Token": getTelemetryAppToken(),
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
213
215
|
|
|
214
216
|
if (!resp.ok) {
|
|
215
217
|
await resp.text(); // consume body to release connection
|
|
216
218
|
log.warn(
|
|
217
|
-
{ status: resp.status
|
|
219
|
+
{ status: resp.status },
|
|
218
220
|
"Usage telemetry POST failed — will retry next cycle",
|
|
219
221
|
);
|
|
220
222
|
return;
|
|
@@ -38,8 +38,13 @@ class FileEditTool implements Tool {
|
|
|
38
38
|
description:
|
|
39
39
|
"Replace all occurrences of old_string instead of requiring a unique match (default: false)",
|
|
40
40
|
},
|
|
41
|
+
activity: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description:
|
|
44
|
+
"Brief non-technical explanation of what you are doing and why, shown to the user as a status update.",
|
|
45
|
+
},
|
|
41
46
|
},
|
|
42
|
-
required: ["path", "old_string", "new_string"],
|
|
47
|
+
required: ["path", "old_string", "new_string", "activity"],
|
|
43
48
|
},
|
|
44
49
|
};
|
|
45
50
|
}
|