@vellumai/assistant 0.4.37 → 0.4.41
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/ARCHITECTURE.md +3 -3
- package/README.md +13 -13
- package/bun.lock +80 -24
- package/docs/architecture/integrations.md +126 -128
- package/docs/runbook-trusted-contacts.md +1 -1
- package/docs/trusted-contact-access.md +12 -12
- package/package.json +3 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
- package/src/__tests__/app-bundler.test.ts +209 -0
- package/src/__tests__/app-compiler.test.ts +279 -0
- package/src/__tests__/app-executors.test.ts +293 -483
- package/src/__tests__/app-migration.test.ts +148 -0
- package/src/__tests__/app-routes-csp.test.ts +202 -0
- package/src/__tests__/avatar-e2e.test.ts +452 -0
- package/src/__tests__/avatar-generator.test.ts +193 -0
- package/src/__tests__/avatar-router.test.ts +186 -0
- package/src/__tests__/browser-download-timeout.test.ts +28 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
- package/src/__tests__/call-domain.test.ts +3 -7
- package/src/__tests__/credential-security-e2e.test.ts +19 -12
- package/src/__tests__/credentials-cli.test.ts +30 -4
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/handlers-slack-config.test.ts +0 -72
- package/src/__tests__/handlers-telegram-config.test.ts +19 -12
- package/src/__tests__/handlers-twitter-config.test.ts +105 -48
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/integration-status.test.ts +15 -5
- package/src/__tests__/integrations-cli.test.ts +1 -1
- package/src/__tests__/invite-redemption-service.test.ts +62 -7
- package/src/__tests__/ipc-snapshot.test.ts +0 -8
- package/src/__tests__/managed-avatar-client.test.ts +280 -0
- package/src/__tests__/mcp-cli.test.ts +3 -3
- package/src/__tests__/oauth-cli.test.ts +203 -0
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/secret-onetime-send.test.ts +19 -12
- package/src/__tests__/secure-keys.test.ts +78 -0
- package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
- package/src/__tests__/slack-channel-config.test.ts +23 -16
- package/src/__tests__/slack-share-routes.test.ts +263 -0
- package/src/__tests__/sms-messaging-provider.test.ts +3 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/__tests__/twilio-config.test.ts +15 -36
- package/src/__tests__/twilio-provider.test.ts +4 -0
- package/src/__tests__/twitter-auth-handler.test.ts +27 -14
- package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
- package/src/__tests__/twitter-cli-routing.test.ts +38 -53
- package/src/__tests__/twitter-oauth-client.test.ts +18 -47
- package/src/__tests__/voice-invite-redemption.test.ts +27 -3
- package/src/amazon/cart.ts +1 -1
- package/src/amazon/client.ts +89 -7
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/bundler/app-bundler.ts +77 -32
- package/src/bundler/app-compiler.ts +195 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/package-resolver.ts +185 -0
- package/src/calls/call-domain.ts +4 -14
- package/src/calls/relay-server.ts +2 -2
- package/src/calls/twilio-config.ts +5 -24
- package/src/calls/twilio-rest.ts +19 -5
- package/src/cli/amazon.ts +74 -249
- package/src/cli/audit.ts +2 -2
- package/src/cli/autonomy.ts +9 -9
- package/src/cli/channels.ts +5 -5
- package/src/cli/completions.ts +27 -27
- package/src/cli/config.ts +14 -14
- package/src/cli/contacts.ts +27 -27
- package/src/cli/credentials.ts +28 -28
- package/src/cli/dev.ts +2 -2
- package/src/cli/doctor.ts +2 -2
- package/src/cli/email.ts +82 -82
- package/src/cli/influencer.ts +13 -13
- package/src/cli/integrations.ts +19 -144
- package/src/cli/keys.ts +10 -10
- package/src/cli/map.ts +4 -4
- package/src/cli/mcp.ts +17 -17
- package/src/cli/memory.ts +18 -18
- package/src/cli/notifications.ts +13 -13
- package/src/cli/oauth.ts +77 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/sequence.ts +27 -27
- package/src/cli/sessions.ts +12 -12
- package/src/cli/trust.ts +8 -8
- package/src/cli/twitter.ts +124 -70
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
- package/src/config/bundled-skills/amazon/SKILL.md +54 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
- package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
- package/src/config/bundled-skills/contacts/SKILL.md +12 -12
- package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
- package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/influencer/SKILL.md +13 -13
- package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
- package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
- package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
- package/src/config/bundled-skills/twitter/SKILL.md +68 -44
- package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
- package/src/config/core-schema.ts +26 -0
- package/src/config/env.ts +4 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/schema.ts +8 -0
- package/src/config/system-prompt.ts +6 -3
- package/src/config/templates/BOOTSTRAP.md +7 -5
- package/src/contacts/contacts-write.ts +5 -1
- package/src/daemon/handlers/apps.ts +31 -4
- package/src/daemon/handlers/config-ingress.ts +3 -3
- package/src/daemon/handlers/config-integrations.ts +120 -49
- package/src/daemon/handlers/config-slack-channel.ts +26 -7
- package/src/daemon/handlers/config-slack.ts +1 -54
- package/src/daemon/handlers/config-telegram.ts +28 -10
- package/src/daemon/handlers/config.ts +1 -4
- package/src/daemon/handlers/twitter-auth.ts +11 -4
- package/src/daemon/ipc-contract/apps.ts +0 -13
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/session-messaging.ts +2 -2
- package/src/daemon/tool-side-effects.ts +30 -0
- package/src/email/providers/agentmail.ts +1 -1
- package/src/email/providers/index.ts +1 -1
- package/src/email/service.ts +1 -1
- package/src/gallery/default-gallery.ts +538 -0
- package/src/gallery/gallery-manifest.ts +5 -1
- package/src/influencer/client.ts +8 -6
- package/src/mcp/client.ts +1 -1
- package/src/media/avatar-router.ts +99 -0
- package/src/media/avatar-types.ts +60 -0
- package/src/media/managed-avatar-client.ts +189 -0
- package/src/memory/app-migration.ts +114 -0
- package/src/memory/app-store.ts +11 -0
- package/src/memory/qdrant-client.ts +1 -1
- package/src/messaging/providers/slack/client.ts +12 -2
- package/src/messaging/providers/sms/adapter.ts +6 -10
- package/src/migrations/data-layout.ts +8 -1
- package/src/oauth/token-persistence.ts +9 -6
- package/src/runtime/assistant-scope.ts +5 -0
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-readiness-service.ts +9 -4
- package/src/runtime/gateway-internal-client.ts +11 -3
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +23 -13
- package/src/runtime/middleware/twilio-validation.ts +2 -2
- package/src/runtime/routes/app-routes.ts +131 -3
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
- package/src/runtime/routes/integration-routes.ts +2 -2
- package/src/runtime/routes/slack-share-routes.ts +235 -0
- package/src/runtime/routes/twilio-routes.ts +47 -34
- package/src/schedule/integration-status.ts +2 -3
- package/src/security/token-manager.ts +11 -3
- package/src/tools/apps/executors.ts +116 -8
- package/src/tools/browser/browser-manager.ts +30 -2
- package/src/tools/browser/chrome-cdp.ts +31 -3
- package/src/tools/credentials/vault.ts +9 -7
- package/src/tools/executor.ts +4 -0
- package/src/tools/system/avatar-generator.ts +55 -34
- package/src/twitter/client.ts +1 -1
- package/src/twitter/oauth-client.ts +31 -43
- package/src/twitter/router.ts +25 -23
- package/src/util/platform.ts +5 -0
- package/src/slack/slack-webhook.ts +0 -66
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy router for avatar generation.
|
|
3
|
+
* Selects managed platform or local Gemini path based on config.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getConfig } from "../config/loader.js";
|
|
7
|
+
import { ConfigError, ProviderError } from "../util/errors.js";
|
|
8
|
+
import { getLogger } from "../util/logger.js";
|
|
9
|
+
import type {
|
|
10
|
+
AvatarGenerationResult,
|
|
11
|
+
AvatarGenerationStrategy,
|
|
12
|
+
} from "./avatar-types.js";
|
|
13
|
+
import { generateImage } from "./gemini-image-service.js";
|
|
14
|
+
import {
|
|
15
|
+
generateManagedAvatar,
|
|
16
|
+
isManagedAvailable,
|
|
17
|
+
} from "./managed-avatar-client.js";
|
|
18
|
+
|
|
19
|
+
const log = getLogger("avatar-router");
|
|
20
|
+
|
|
21
|
+
export function getAvatarStrategy(): AvatarGenerationStrategy {
|
|
22
|
+
return getConfig().avatar.generationStrategy;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function generateLocal(
|
|
26
|
+
prompt: string,
|
|
27
|
+
correlationId?: string,
|
|
28
|
+
): Promise<AvatarGenerationResult> {
|
|
29
|
+
const config = getConfig();
|
|
30
|
+
const geminiKey = config.apiKeys.gemini ?? process.env.GEMINI_API_KEY;
|
|
31
|
+
if (!geminiKey) {
|
|
32
|
+
throw new ConfigError(
|
|
33
|
+
"Gemini API key is not configured. Set it via `config set apiKeys.gemini <key>` or the GEMINI_API_KEY environment variable.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await generateImage(geminiKey, {
|
|
38
|
+
prompt,
|
|
39
|
+
mode: "generate",
|
|
40
|
+
model: config.imageGenModel,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const image = result.images[0];
|
|
44
|
+
if (!image) {
|
|
45
|
+
throw new ProviderError(
|
|
46
|
+
"Local Gemini image generation returned no images.",
|
|
47
|
+
"gemini",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
imageBase64: image.dataBase64,
|
|
53
|
+
mimeType: image.mimeType,
|
|
54
|
+
pathUsed: "local",
|
|
55
|
+
correlationId,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function routedGenerateAvatar(
|
|
60
|
+
prompt: string,
|
|
61
|
+
options?: { correlationId?: string },
|
|
62
|
+
): Promise<AvatarGenerationResult> {
|
|
63
|
+
const strategy = getAvatarStrategy();
|
|
64
|
+
const correlationId = options?.correlationId;
|
|
65
|
+
|
|
66
|
+
if (strategy === "managed_required") {
|
|
67
|
+
const managed = await generateManagedAvatar(prompt, { correlationId });
|
|
68
|
+
return {
|
|
69
|
+
imageBase64: managed.image.data_base64,
|
|
70
|
+
mimeType: managed.image.mime_type,
|
|
71
|
+
pathUsed: "managed",
|
|
72
|
+
correlationId: managed.correlation_id,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (strategy === "local_only") {
|
|
77
|
+
return generateLocal(prompt, correlationId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// managed_prefer: try managed first if available, fall back to local
|
|
81
|
+
if (isManagedAvailable()) {
|
|
82
|
+
try {
|
|
83
|
+
const managed = await generateManagedAvatar(prompt, { correlationId });
|
|
84
|
+
return {
|
|
85
|
+
imageBase64: managed.image.data_base64,
|
|
86
|
+
mimeType: managed.image.mime_type,
|
|
87
|
+
pathUsed: "managed",
|
|
88
|
+
correlationId: managed.correlation_id,
|
|
89
|
+
};
|
|
90
|
+
} catch (err) {
|
|
91
|
+
log.warn(
|
|
92
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
93
|
+
"Managed avatar generation failed, falling back to local Gemini",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return generateLocal(prompt, correlationId);
|
|
99
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type AvatarGenerationStrategy = "managed_required" | "managed_prefer" | "local_only";
|
|
2
|
+
|
|
3
|
+
export interface ManagedAvatarImagePayload {
|
|
4
|
+
mime_type: string;
|
|
5
|
+
data_base64: string;
|
|
6
|
+
bytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ManagedAvatarResponse {
|
|
11
|
+
image: ManagedAvatarImagePayload;
|
|
12
|
+
usage: { billable: boolean; class_name: string };
|
|
13
|
+
generation_source: string;
|
|
14
|
+
profile: string;
|
|
15
|
+
correlation_id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ManagedAvatarErrorResponse {
|
|
19
|
+
code: string;
|
|
20
|
+
subcode: string;
|
|
21
|
+
detail: string;
|
|
22
|
+
retryable: boolean;
|
|
23
|
+
correlation_id: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ManagedAvatarError extends Error {
|
|
27
|
+
readonly code: string;
|
|
28
|
+
readonly subcode: string;
|
|
29
|
+
readonly retryable: boolean;
|
|
30
|
+
readonly correlationId: string;
|
|
31
|
+
readonly statusCode: number;
|
|
32
|
+
|
|
33
|
+
constructor(opts: {
|
|
34
|
+
code: string;
|
|
35
|
+
subcode: string;
|
|
36
|
+
detail: string;
|
|
37
|
+
retryable: boolean;
|
|
38
|
+
correlationId: string;
|
|
39
|
+
statusCode: number;
|
|
40
|
+
}) {
|
|
41
|
+
super(opts.detail);
|
|
42
|
+
this.name = "ManagedAvatarError";
|
|
43
|
+
this.code = opts.code;
|
|
44
|
+
this.subcode = opts.subcode;
|
|
45
|
+
this.retryable = opts.retryable;
|
|
46
|
+
this.correlationId = opts.correlationId;
|
|
47
|
+
this.statusCode = opts.statusCode;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AvatarGenerationResult {
|
|
52
|
+
imageBase64: string;
|
|
53
|
+
mimeType: string;
|
|
54
|
+
pathUsed: "managed" | "local";
|
|
55
|
+
correlationId?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const AVATAR_MIME_ALLOWLIST = new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
59
|
+
export const AVATAR_MAX_DECODED_BYTES = 10 * 1024 * 1024;
|
|
60
|
+
export const AVATAR_PROMPT_MAX_LENGTH = 2000;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { getPlatformBaseUrl } from "../config/env.js";
|
|
2
|
+
import { getConfig } from "../config/loader.js";
|
|
3
|
+
import { getSecureKey } from "../security/secure-keys.js";
|
|
4
|
+
import { getLogger } from "../util/logger.js";
|
|
5
|
+
import {
|
|
6
|
+
AVATAR_MAX_DECODED_BYTES,
|
|
7
|
+
AVATAR_MIME_ALLOWLIST,
|
|
8
|
+
AVATAR_PROMPT_MAX_LENGTH,
|
|
9
|
+
ManagedAvatarError,
|
|
10
|
+
type ManagedAvatarErrorResponse,
|
|
11
|
+
type ManagedAvatarResponse,
|
|
12
|
+
} from "./avatar-types.js";
|
|
13
|
+
|
|
14
|
+
const log = getLogger("managed-avatar-client");
|
|
15
|
+
|
|
16
|
+
export function getAssistantApiKey(): string | undefined {
|
|
17
|
+
return getSecureKey("credential:vellum:assistant_api_key");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getManagedAvatarBaseUrl(): string {
|
|
21
|
+
const baseUrl = getConfig().platform.baseUrl || getPlatformBaseUrl();
|
|
22
|
+
return baseUrl.replace(/\/+$/, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isManagedAvailable(): boolean {
|
|
26
|
+
const apiKey = getAssistantApiKey();
|
|
27
|
+
const baseUrl = getManagedAvatarBaseUrl();
|
|
28
|
+
return !!apiKey && apiKey.length > 0 && !!baseUrl && baseUrl.length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function generateManagedAvatar(
|
|
32
|
+
prompt: string,
|
|
33
|
+
options?: { correlationId?: string; idempotencyKey?: string },
|
|
34
|
+
): Promise<ManagedAvatarResponse> {
|
|
35
|
+
if (prompt.length > AVATAR_PROMPT_MAX_LENGTH) {
|
|
36
|
+
throw new ManagedAvatarError({
|
|
37
|
+
code: "validation_error",
|
|
38
|
+
subcode: "prompt_too_long",
|
|
39
|
+
detail: `Prompt exceeds maximum length of ${AVATAR_PROMPT_MAX_LENGTH} characters`,
|
|
40
|
+
retryable: false,
|
|
41
|
+
correlationId: options?.correlationId ?? crypto.randomUUID(),
|
|
42
|
+
statusCode: 0,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const apiKey = getAssistantApiKey();
|
|
47
|
+
if (!apiKey) {
|
|
48
|
+
throw new ManagedAvatarError({
|
|
49
|
+
code: "configuration_error",
|
|
50
|
+
subcode: "missing_api_key",
|
|
51
|
+
detail: "Assistant API key is not configured",
|
|
52
|
+
retryable: false,
|
|
53
|
+
correlationId: options?.correlationId ?? crypto.randomUUID(),
|
|
54
|
+
statusCode: 0,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const baseUrl = getManagedAvatarBaseUrl();
|
|
59
|
+
if (!baseUrl) {
|
|
60
|
+
throw new ManagedAvatarError({
|
|
61
|
+
code: "configuration_error",
|
|
62
|
+
subcode: "missing_base_url",
|
|
63
|
+
detail:
|
|
64
|
+
"Platform base URL is not configured. Set platform.baseUrl in config or PLATFORM_BASE_URL environment variable.",
|
|
65
|
+
retryable: false,
|
|
66
|
+
correlationId: options?.correlationId ?? crypto.randomUUID(),
|
|
67
|
+
statusCode: 0,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const url = `${baseUrl}/v1/assistants/avatar/generate/`;
|
|
72
|
+
const idempotencyKey = options?.idempotencyKey ?? crypto.randomUUID();
|
|
73
|
+
const correlationId = options?.correlationId ?? crypto.randomUUID();
|
|
74
|
+
|
|
75
|
+
const headers: Record<string, string> = {
|
|
76
|
+
Authorization: `Api-Key ${apiKey}`,
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
"Idempotency-Key": idempotencyKey,
|
|
79
|
+
"X-Correlation-Id": correlationId,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
log.debug({ url, correlationId }, "Requesting managed avatar generation");
|
|
83
|
+
|
|
84
|
+
let response: Response;
|
|
85
|
+
try {
|
|
86
|
+
response = await fetch(url, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: JSON.stringify({ prompt }),
|
|
89
|
+
signal: AbortSignal.timeout(60_000),
|
|
90
|
+
headers,
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const isTimeout =
|
|
94
|
+
err instanceof DOMException && err.name === "TimeoutError";
|
|
95
|
+
throw new ManagedAvatarError({
|
|
96
|
+
code: "avatar_generation_failed",
|
|
97
|
+
subcode: isTimeout ? "upstream_timeout" : "network_error",
|
|
98
|
+
detail: isTimeout
|
|
99
|
+
? "Request to avatar generation service timed out"
|
|
100
|
+
: `Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
101
|
+
retryable: true,
|
|
102
|
+
correlationId,
|
|
103
|
+
statusCode: 0,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
let errorBody: ManagedAvatarErrorResponse;
|
|
109
|
+
try {
|
|
110
|
+
errorBody = (await response.json()) as ManagedAvatarErrorResponse;
|
|
111
|
+
} catch {
|
|
112
|
+
throw new ManagedAvatarError({
|
|
113
|
+
code: "upstream_error",
|
|
114
|
+
subcode: "unparseable_response",
|
|
115
|
+
detail: `HTTP ${response.status}: unable to parse error response`,
|
|
116
|
+
retryable: response.status >= 500 || response.status === 429,
|
|
117
|
+
correlationId,
|
|
118
|
+
statusCode: response.status,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new ManagedAvatarError({
|
|
123
|
+
code: errorBody.code ?? "upstream_error",
|
|
124
|
+
subcode: errorBody.subcode ?? "unknown",
|
|
125
|
+
detail: errorBody.detail ?? `HTTP ${response.status}`,
|
|
126
|
+
retryable:
|
|
127
|
+
errorBody.retryable ??
|
|
128
|
+
(response.status >= 500 || response.status === 429),
|
|
129
|
+
correlationId: errorBody.correlation_id ?? correlationId,
|
|
130
|
+
statusCode: response.status,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let body: ManagedAvatarResponse;
|
|
135
|
+
try {
|
|
136
|
+
body = (await response.json()) as ManagedAvatarResponse;
|
|
137
|
+
|
|
138
|
+
if (!AVATAR_MIME_ALLOWLIST.has(body.image.mime_type)) {
|
|
139
|
+
throw new ManagedAvatarError({
|
|
140
|
+
code: "validation_error",
|
|
141
|
+
subcode: "disallowed_mime_type",
|
|
142
|
+
detail: `Response MIME type "${body.image.mime_type}" is not in the allowlist`,
|
|
143
|
+
retryable: false,
|
|
144
|
+
correlationId,
|
|
145
|
+
statusCode: 0,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const b64 = body.image.data_base64;
|
|
150
|
+
const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
151
|
+
const estimatedDecodedBytes = Math.ceil((b64.length * 3) / 4) - padding;
|
|
152
|
+
if (
|
|
153
|
+
estimatedDecodedBytes > AVATAR_MAX_DECODED_BYTES ||
|
|
154
|
+
body.image.bytes > AVATAR_MAX_DECODED_BYTES
|
|
155
|
+
) {
|
|
156
|
+
throw new ManagedAvatarError({
|
|
157
|
+
code: "validation_error",
|
|
158
|
+
subcode: "oversized_image",
|
|
159
|
+
detail: `Response image size ${Math.max(estimatedDecodedBytes, body.image.bytes)} exceeds maximum of ${AVATAR_MAX_DECODED_BYTES} bytes`,
|
|
160
|
+
retryable: false,
|
|
161
|
+
correlationId,
|
|
162
|
+
statusCode: 0,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
log.debug(
|
|
167
|
+
{
|
|
168
|
+
correlationId,
|
|
169
|
+
mimeType: body.image.mime_type,
|
|
170
|
+
bytes: body.image.bytes,
|
|
171
|
+
},
|
|
172
|
+
"Managed avatar generation succeeded",
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return body;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof ManagedAvatarError) {
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
throw new ManagedAvatarError({
|
|
181
|
+
code: "upstream_error",
|
|
182
|
+
subcode: "unparseable_response",
|
|
183
|
+
detail: `Failed to parse avatar generation response: ${err instanceof Error ? err.message : String(err)}`,
|
|
184
|
+
retryable: false,
|
|
185
|
+
correlationId,
|
|
186
|
+
statusCode: 0,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-way migration helper: converts legacy single-HTML apps to the
|
|
3
|
+
* multi-file src/ layout (formatVersion 2).
|
|
4
|
+
*
|
|
5
|
+
* Non-destructive — the root index.html is preserved as a legacy fallback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { getApp, getAppsDir, isMultifileApp } from "./app-store.js";
|
|
12
|
+
|
|
13
|
+
export interface MigrationResult {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract inline `<style>` blocks from HTML, returning the extracted CSS
|
|
20
|
+
* and the HTML with those blocks replaced by a `<link>` tag.
|
|
21
|
+
*/
|
|
22
|
+
function extractInlineStyles(html: string): {
|
|
23
|
+
css: string;
|
|
24
|
+
html: string;
|
|
25
|
+
} {
|
|
26
|
+
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
27
|
+
const cssChunks: string[] = [];
|
|
28
|
+
|
|
29
|
+
for (const match of html.matchAll(styleRegex)) {
|
|
30
|
+
cssChunks.push(match[1].trim());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (cssChunks.length === 0) {
|
|
34
|
+
return { css: "", html };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const css = cssChunks.join("\n\n");
|
|
38
|
+
|
|
39
|
+
// Replace the first <style> block with a <link> tag, remove the rest
|
|
40
|
+
let replaced = false;
|
|
41
|
+
const updatedHtml = html.replace(styleRegex, () => {
|
|
42
|
+
if (!replaced) {
|
|
43
|
+
replaced = true;
|
|
44
|
+
return `<link rel="stylesheet" href="styles.css">`;
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return { css, html: updatedHtml };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Migrate a legacy single-HTML app to the multi-file src/ layout.
|
|
54
|
+
*
|
|
55
|
+
* Steps:
|
|
56
|
+
* 1. Read existing index.html
|
|
57
|
+
* 2. Create {appId}/src/ directory
|
|
58
|
+
* 3. Copy HTML content to src/index.html (with inline styles extracted)
|
|
59
|
+
* 4. Create src/main.tsx placeholder entry point
|
|
60
|
+
* 5. If inline styles found, write src/styles.css
|
|
61
|
+
* 6. Update app metadata to formatVersion: 2
|
|
62
|
+
* 7. Keep root index.html untouched (legacy fallback)
|
|
63
|
+
*/
|
|
64
|
+
export function migrateAppToMultifile(appId: string): MigrationResult {
|
|
65
|
+
const app = getApp(appId);
|
|
66
|
+
if (!app) {
|
|
67
|
+
return { ok: false, error: `App not found: ${appId}` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Already migrated — treat as no-op
|
|
71
|
+
if (isMultifileApp(app)) {
|
|
72
|
+
return { ok: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const appsDir = getAppsDir();
|
|
76
|
+
const appDir = join(appsDir, appId);
|
|
77
|
+
const rootIndex = join(appDir, "index.html");
|
|
78
|
+
|
|
79
|
+
if (!existsSync(rootIndex)) {
|
|
80
|
+
return { ok: false, error: `Root index.html not found for app ${appId}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const originalHtml = readFileSync(rootIndex, "utf-8");
|
|
84
|
+
|
|
85
|
+
// Create src/ directory
|
|
86
|
+
const srcDir = join(appDir, "src");
|
|
87
|
+
mkdirSync(srcDir, { recursive: true });
|
|
88
|
+
|
|
89
|
+
// Extract inline styles if present
|
|
90
|
+
const { css, html: processedHtml } = extractInlineStyles(originalHtml);
|
|
91
|
+
|
|
92
|
+
// Write src/index.html
|
|
93
|
+
writeFileSync(join(srcDir, "index.html"), processedHtml, "utf-8");
|
|
94
|
+
|
|
95
|
+
// Write src/main.tsx placeholder
|
|
96
|
+
const styleImport = css ? "import './styles.css';\n" : "";
|
|
97
|
+
const mainTsx = `// Entry point — migrated from legacy single-file app\n${styleImport}\nconsole.log('App loaded');\n`;
|
|
98
|
+
writeFileSync(join(srcDir, "main.tsx"), mainTsx, "utf-8");
|
|
99
|
+
|
|
100
|
+
// Write src/styles.css if styles were extracted
|
|
101
|
+
if (css) {
|
|
102
|
+
writeFileSync(join(srcDir, "styles.css"), css, "utf-8");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Update metadata to formatVersion 2
|
|
106
|
+
const metadataPath = join(appsDir, `${appId}.json`);
|
|
107
|
+
const rawMeta = readFileSync(metadataPath, "utf-8");
|
|
108
|
+
const metadata = JSON.parse(rawMeta);
|
|
109
|
+
metadata.formatVersion = 2;
|
|
110
|
+
metadata.updatedAt = Date.now();
|
|
111
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
112
|
+
|
|
113
|
+
return { ok: true };
|
|
114
|
+
}
|
package/src/memory/app-store.ts
CHANGED
|
@@ -51,6 +51,15 @@ export interface AppDefinition {
|
|
|
51
51
|
pages?: Record<string, string>;
|
|
52
52
|
createdAt: number;
|
|
53
53
|
updatedAt: number;
|
|
54
|
+
/** App format version. undefined or 1 = legacy single-HTML, 2 = multi-file TSX. */
|
|
55
|
+
formatVersion?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns true if the app uses the multi-file TSX format (formatVersion 2).
|
|
60
|
+
*/
|
|
61
|
+
export function isMultifileApp(app: AppDefinition): boolean {
|
|
62
|
+
return app.formatVersion === 2;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
export interface AppRecord {
|
|
@@ -195,6 +204,7 @@ export function createApp(params: {
|
|
|
195
204
|
htmlDefinition: string;
|
|
196
205
|
version?: string;
|
|
197
206
|
pages?: Record<string, string>;
|
|
207
|
+
formatVersion?: number;
|
|
198
208
|
}): AppDefinition {
|
|
199
209
|
const dir = getAppsDir();
|
|
200
210
|
const now = Date.now();
|
|
@@ -209,6 +219,7 @@ export function createApp(params: {
|
|
|
209
219
|
version: params.version,
|
|
210
220
|
createdAt: now,
|
|
211
221
|
updatedAt: now,
|
|
222
|
+
formatVersion: params.formatVersion,
|
|
212
223
|
};
|
|
213
224
|
|
|
214
225
|
// Write htmlDefinition to {appId}/index.html on disk
|
|
@@ -365,7 +365,7 @@ export class VellumQdrantClient {
|
|
|
365
365
|
/**
|
|
366
366
|
* Detect "collection not found" errors from Qdrant so callers can
|
|
367
367
|
* reset collectionReady and retry after an external deletion
|
|
368
|
-
* (e.g. `
|
|
368
|
+
* (e.g. `assistant sessions clear`).
|
|
369
369
|
*/
|
|
370
370
|
private isCollectionMissing(err: unknown): boolean {
|
|
371
371
|
if (
|
|
@@ -231,14 +231,24 @@ export async function userInfo(
|
|
|
231
231
|
return request<SlackUserInfoResponse>(token, "users.info", { user: userId });
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
export interface PostMessageOptions {
|
|
235
|
+
threadTs?: string;
|
|
236
|
+
blocks?: unknown[];
|
|
237
|
+
}
|
|
238
|
+
|
|
234
239
|
export async function postMessage(
|
|
235
240
|
token: string,
|
|
236
241
|
channel: string,
|
|
237
242
|
text: string,
|
|
238
|
-
|
|
243
|
+
optionsOrThreadTs?: PostMessageOptions | string,
|
|
239
244
|
): Promise<SlackPostMessageResponse> {
|
|
245
|
+
const opts: PostMessageOptions =
|
|
246
|
+
typeof optionsOrThreadTs === "string"
|
|
247
|
+
? { threadTs: optionsOrThreadTs }
|
|
248
|
+
: (optionsOrThreadTs ?? {});
|
|
240
249
|
const body: Record<string, unknown> = { channel, text };
|
|
241
|
-
if (threadTs) body.thread_ts = threadTs;
|
|
250
|
+
if (opts.threadTs) body.thread_ts = opts.threadTs;
|
|
251
|
+
if (opts.blocks) body.blocks = opts.blocks;
|
|
242
252
|
return request<SlackPostMessageResponse>(
|
|
243
253
|
token,
|
|
244
254
|
"chat.postMessage",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* a per-user OAuth token.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import {
|
|
18
|
+
getTwilioCredentials,
|
|
19
|
+
hasTwilioCredentials,
|
|
20
|
+
} from "../../../calls/twilio-rest.js";
|
|
17
21
|
import {
|
|
18
22
|
getGatewayInternalBaseUrl,
|
|
19
23
|
getTwilioPhoneNumberEnv,
|
|
@@ -48,14 +52,6 @@ function getBearerToken(): string {
|
|
|
48
52
|
return mintDaemonDeliveryToken();
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
/** Check whether Twilio credentials are stored. */
|
|
52
|
-
function hasTwilioCredentials(): boolean {
|
|
53
|
-
return (
|
|
54
|
-
!!getSecureKey("credential:twilio:account_sid") &&
|
|
55
|
-
!!getSecureKey("credential:twilio:auth_token")
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
55
|
/** Resolve the configured SMS phone number. */
|
|
60
56
|
function getPhoneNumber(): string | undefined {
|
|
61
57
|
const fromEnv = getTwilioPhoneNumberEnv();
|
|
@@ -118,7 +114,7 @@ export const smsMessagingProvider: MessagingProvider = {
|
|
|
118
114
|
| Record<string, string>
|
|
119
115
|
| undefined;
|
|
120
116
|
if (mappings && Object.keys(mappings).length > 0) {
|
|
121
|
-
const accountSid =
|
|
117
|
+
const accountSid = getTwilioCredentials().accountSid;
|
|
122
118
|
return {
|
|
123
119
|
connected: true,
|
|
124
120
|
user: "assistant-scoped",
|
|
@@ -143,7 +139,7 @@ export const smsMessagingProvider: MessagingProvider = {
|
|
|
143
139
|
};
|
|
144
140
|
}
|
|
145
141
|
|
|
146
|
-
const accountSid =
|
|
142
|
+
const accountSid = getTwilioCredentials().accountSid;
|
|
147
143
|
|
|
148
144
|
return {
|
|
149
145
|
connected: true,
|
|
@@ -51,7 +51,14 @@ export function migrateToDataLayout(): void {
|
|
|
51
51
|
migrateItem(join(root, "qdrant.pid"), join(data, "qdrant", "qdrant.pid"));
|
|
52
52
|
|
|
53
53
|
// Qdrant binary: ~/.vellum/bin/ → ~/.vellum/data/qdrant/bin/
|
|
54
|
-
|
|
54
|
+
// Only migrate if the directory actually contains a qdrant binary.
|
|
55
|
+
// After the CLI-launcher feature landed, ~/.vellum/bin/ is used for
|
|
56
|
+
// launcher scripts (doordash, map, etc.), not qdrant, so moving it
|
|
57
|
+
// blindly would break CLI launchers on every fresh hatch.
|
|
58
|
+
const legacyBinDir = join(root, "bin");
|
|
59
|
+
if (existsSync(join(legacyBinDir, "qdrant"))) {
|
|
60
|
+
migrateItem(legacyBinDir, join(data, "qdrant", "bin"));
|
|
61
|
+
}
|
|
55
62
|
|
|
56
63
|
// Logs: ~/.vellum/logs/ → ~/.vellum/data/logs/
|
|
57
64
|
migrateItem(join(root, "logs"), join(data, "logs"));
|
|
@@ -10,7 +10,10 @@ import type {
|
|
|
10
10
|
OAuth2FlowResult,
|
|
11
11
|
TokenEndpointAuthMethod,
|
|
12
12
|
} from "../security/oauth2.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
deleteSecureKeyAsync,
|
|
15
|
+
setSecureKeyAsync,
|
|
16
|
+
} from "../security/secure-keys.js";
|
|
14
17
|
import {
|
|
15
18
|
deleteCredentialMetadata,
|
|
16
19
|
upsertCredentialMetadata,
|
|
@@ -67,7 +70,7 @@ export async function storeOAuth2Tokens(
|
|
|
67
70
|
wellKnownInjectionTemplates,
|
|
68
71
|
} = params;
|
|
69
72
|
|
|
70
|
-
const tokenStored =
|
|
73
|
+
const tokenStored = await setSecureKeyAsync(
|
|
71
74
|
`credential:${service}:access_token`,
|
|
72
75
|
tokens.accessToken,
|
|
73
76
|
);
|
|
@@ -95,7 +98,7 @@ export async function storeOAuth2Tokens(
|
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
// Persist client credentials in keychain for defense in depth
|
|
98
|
-
const clientIdStored =
|
|
101
|
+
const clientIdStored = await setSecureKeyAsync(
|
|
99
102
|
`credential:${service}:client_id`,
|
|
100
103
|
clientId,
|
|
101
104
|
);
|
|
@@ -103,7 +106,7 @@ export async function storeOAuth2Tokens(
|
|
|
103
106
|
throw new Error("Failed to store client_id in secure storage");
|
|
104
107
|
}
|
|
105
108
|
if (clientSecret) {
|
|
106
|
-
const clientSecretStored =
|
|
109
|
+
const clientSecretStored = await setSecureKeyAsync(
|
|
107
110
|
`credential:${service}:client_secret`,
|
|
108
111
|
clientSecret,
|
|
109
112
|
);
|
|
@@ -129,7 +132,7 @@ export async function storeOAuth2Tokens(
|
|
|
129
132
|
});
|
|
130
133
|
|
|
131
134
|
if (tokens.refreshToken) {
|
|
132
|
-
const refreshStored =
|
|
135
|
+
const refreshStored = await setSecureKeyAsync(
|
|
133
136
|
`credential:${service}:refresh_token`,
|
|
134
137
|
tokens.refreshToken,
|
|
135
138
|
);
|
|
@@ -140,7 +143,7 @@ export async function storeOAuth2Tokens(
|
|
|
140
143
|
// Re-auth grants that omit refresh_token must clear any stale stored
|
|
141
144
|
// token — otherwise withValidToken() will attempt refresh with invalid
|
|
142
145
|
// credentials.
|
|
143
|
-
|
|
146
|
+
await deleteSecureKeyAsync(`credential:${service}:refresh_token`);
|
|
144
147
|
deleteCredentialMetadata(service, "refresh_token");
|
|
145
148
|
}
|
|
146
149
|
|
|
@@ -6,5 +6,10 @@
|
|
|
6
6
|
* gateway and platform layers (hatch, invite links, etc.). Daemon code
|
|
7
7
|
* should never derive scoping decisions from externally-provided assistant
|
|
8
8
|
* IDs — use this constant instead.
|
|
9
|
+
*
|
|
10
|
+
* Multi-instance invariant: each daemon process is single-tenant within
|
|
11
|
+
* its own BASE_DATA_DIR. The fixed "self" value works across multiple
|
|
12
|
+
* local instances because each instance has isolated storage — there is
|
|
13
|
+
* no cross-instance data sharing that would require disambiguating IDs.
|
|
9
14
|
*/
|
|
10
15
|
export const DAEMON_INTERNAL_ASSISTANT_ID = "self" as const;
|
|
@@ -244,6 +244,10 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
244
244
|
{ endpoint: "integrations/twilio/sms/test", scopes: ["settings.write"] },
|
|
245
245
|
{ endpoint: "integrations/twilio/sms/doctor", scopes: ["settings.write"] },
|
|
246
246
|
|
|
247
|
+
// Slack share
|
|
248
|
+
{ endpoint: "slack/channels", scopes: ["settings.read"] },
|
|
249
|
+
{ endpoint: "slack/share", scopes: ["settings.write"] },
|
|
250
|
+
|
|
247
251
|
// Channel readiness
|
|
248
252
|
{ endpoint: "channels/readiness", scopes: ["settings.read"] },
|
|
249
253
|
{ endpoint: "channels/readiness/refresh", scopes: ["settings.write"] },
|