@vellumai/assistant 0.12.2-staging.1 → 0.12.2-staging.2

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.
Files changed (64) hide show
  1. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  2. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  3. package/openapi.yaml +35 -1
  4. package/package.json +1 -1
  5. package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
  6. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  7. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  8. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  9. package/src/__tests__/credential-routes.test.ts +38 -0
  10. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  11. package/src/__tests__/cu-unified-flow.test.ts +6 -2
  12. package/src/__tests__/host-cu-proxy.test.ts +69 -12
  13. package/src/__tests__/oauth-commands-routes.test.ts +55 -0
  14. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  15. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  16. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  17. package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
  18. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  19. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  20. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  21. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  22. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  23. package/src/acp/acp-claude-oauth.ts +328 -14
  24. package/src/acp/acp-credentials.ts +19 -0
  25. package/src/acp/claude-token-refresh.ts +150 -0
  26. package/src/acp/prepare-agent-env.ts +21 -6
  27. package/src/calls/__tests__/voice-control-protocol.test.ts +62 -0
  28. package/src/calls/__tests__/voice-session-bridge.test.ts +19 -0
  29. package/src/calls/voice-control-protocol.ts +102 -0
  30. package/src/calls/voice-session-bridge.ts +33 -26
  31. package/src/calls/voice-triage-escalate.ts +3 -3
  32. package/src/cli/commands/oauth/status.ts +5 -0
  33. package/src/config/bundled-skills/computer-use/SKILL.md +13 -4
  34. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  35. package/src/config/feature-flag-registry.json +9 -1
  36. package/src/config/loader.ts +1 -0
  37. package/src/config/schemas/services.ts +10 -0
  38. package/src/daemon/conversation-runtime-assembly.ts +18 -6
  39. package/src/daemon/conversation-surfaces.ts +6 -1
  40. package/src/daemon/conversation-tool-setup.ts +8 -16
  41. package/src/daemon/host-cu-proxy.ts +43 -5
  42. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  43. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  44. package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
  45. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  46. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  47. package/src/live-voice/live-voice-session.ts +85 -7
  48. package/src/live-voice/protocol.ts +60 -0
  49. package/src/live-voice/session-controls.ts +111 -0
  50. package/src/oauth/__tests__/seed-providers-managed.test.ts +95 -0
  51. package/src/oauth/connection-resolver.test.ts +27 -0
  52. package/src/oauth/connection-resolver.ts +25 -1
  53. package/src/oauth/provider-serializer.ts +9 -0
  54. package/src/oauth/seed-providers.ts +110 -2
  55. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  56. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
  57. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  58. package/src/runtime/routes/app-management-routes.ts +17 -2
  59. package/src/runtime/routes/credential-routes.ts +1 -4
  60. package/src/runtime/routes/oauth-commands-routes.ts +14 -0
  61. package/src/runtime/routes/oauth-providers.ts +7 -0
  62. package/src/tools/computer-use/definitions.ts +1 -1
  63. package/src/tools/ui-surface/channel-variants.ts +10 -59
  64. package/src/watch/watch-retro.ts +6 -8
@@ -18,7 +18,7 @@
18
18
  * has landed.
19
19
  * POST /v1/acp/claude/auth/exchange — complete a manual/cloud flow: accept the
20
20
  * pasted `code#state` (or a raw code + state), exchange it, and store the
21
- * Claude OAuth token.
21
+ * Claude OAuth token set (access token plus any refresh token and expiry).
22
22
  *
23
23
  * `handleStartAuth` picks loopback vs. manual: a containerized host, or a client
24
24
  * that sets `preferManual` because its browser can't reach the daemon's
@@ -126,7 +126,11 @@ async function handleStartLocalAuth(): Promise<StartResponse> {
126
126
  // can react. Runs in the background — the web client opens `authorize_url`.
127
127
  void flow.completion
128
128
  .then(async (result) => {
129
- await storeAcpClaudeToken(result.tokens.accessToken);
129
+ await storeAcpClaudeToken({
130
+ accessToken: result.tokens.accessToken,
131
+ refreshToken: result.tokens.refreshToken,
132
+ expiresIn: result.tokens.expiresIn,
133
+ });
130
134
  pendingFlows.mark(flow.state, "connected");
131
135
  log.info("ACP Claude local OAuth flow connected");
132
136
  })
@@ -216,7 +220,11 @@ async function handleExchange(args: RouteHandlerArgs): Promise<{ ok: true }> {
216
220
  pending.codeVerifier,
217
221
  state,
218
222
  );
219
- await storeAcpClaudeToken(result.tokens.accessToken);
223
+ await storeAcpClaudeToken({
224
+ accessToken: result.tokens.accessToken,
225
+ refreshToken: result.tokens.refreshToken,
226
+ expiresIn: result.tokens.expiresIn,
227
+ });
220
228
  } catch (err) {
221
229
  const message = err instanceof Error ? err.message : String(err);
222
230
  pendingFlows.mark(state, "error", { error: message });
@@ -181,7 +181,7 @@ function workspaceAppItem(a: AppDefinition): AppListItem {
181
181
  description: a.description,
182
182
  icon: a.icon,
183
183
  createdAt: a.createdAt,
184
- updatedAt: a.updatedAt,
184
+ updatedAt: appContentUpdatedAt(getAppDirPath(a.id), a.updatedAt),
185
185
  version: a.version ?? "1.0.0",
186
186
  contentId: computeContentId(a.name),
187
187
  origin: "workspace",
@@ -203,13 +203,28 @@ function pluginAppItem(app: EnumeratedApp): AppListItem {
203
203
  id: app.id,
204
204
  name: app.name,
205
205
  createdAt: mtime,
206
- updatedAt: mtime,
206
+ updatedAt: appContentUpdatedAt(app.sourcePath, mtime),
207
207
  version: "1.0.0",
208
208
  contentId: computeContentId(app.name),
209
209
  origin: `plugin:${pluginName}`,
210
210
  };
211
211
  }
212
212
 
213
+ /** The newest persisted metadata or compiled-output revision for an app. */
214
+ function appContentUpdatedAt(
215
+ sourcePath: string,
216
+ metadataUpdatedAt: number,
217
+ ): number {
218
+ try {
219
+ return Math.max(
220
+ metadataUpdatedAt,
221
+ statSync(join(sourcePath, "dist")).mtimeMs,
222
+ );
223
+ } catch {
224
+ return metadataUpdatedAt;
225
+ }
226
+ }
227
+
213
228
  function getAppDataResult(
214
229
  method: string,
215
230
  appId: string,
@@ -34,7 +34,6 @@ import { credentialKey } from "../../security/credential-key.js";
34
34
  import {
35
35
  deleteSecureKeyAsync,
36
36
  getActiveBackendInfoAsync,
37
- getSecureKeyAsync,
38
37
  getSecureKeyResultAsync,
39
38
  } from "../../security/secure-keys.js";
40
39
  import {
@@ -561,9 +560,7 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
561
560
 
562
561
  const key = credentialKey(service, field);
563
562
  const affectedConnections = assertCredentialNotInUse(key, force === true);
564
- const existing = await getSecureKeyAsync(key);
565
- const deleteResult =
566
- existing != null ? await deleteSecureKeyAsync(key) : "not-found";
563
+ const deleteResult = await deleteSecureKeyAsync(key);
567
564
 
568
565
  if (deleteResult === "error") {
569
566
  throw new InternalError(
@@ -66,6 +66,7 @@ interface PlatformConnectionEntry {
66
66
  id: string;
67
67
  account_label?: string;
68
68
  scopes_granted?: string[];
69
+ provider_params?: Record<string, string> | null;
69
70
  status?: string;
70
71
  }
71
72
 
@@ -574,6 +575,9 @@ async function handleStatus({ queryParams = {} }: RouteHandlerArgs) {
574
575
  account: c.account_label ?? null,
575
576
  grantedScopes: c.scopes_granted ?? [],
576
577
  status: c.status ?? "ACTIVE",
578
+ // Values the provider scopes the connection by (QuickBooks' realm id),
579
+ // so a caller can address resources the proxy's base URL does not.
580
+ providerParams: c.provider_params ?? {},
577
581
  }));
578
582
 
579
583
  return {
@@ -1060,6 +1064,8 @@ async function handleManagedConnect({ body = {} }: RouteHandlerArgs) {
1060
1064
  provider: string;
1061
1065
  scopes?: string[];
1062
1066
  redirect_after_connect?: string;
1067
+ /** Per-tenant providers (Shopify): the customer's own host. */
1068
+ tenant_host?: string;
1063
1069
  };
1064
1070
 
1065
1071
  if (!b.provider) {
@@ -1076,6 +1082,13 @@ async function handleManagedConnect({ body = {} }: RouteHandlerArgs) {
1076
1082
  }
1077
1083
  reqBody.redirect_after_connect =
1078
1084
  b.redirect_after_connect ?? "/account/oauth/complete";
1085
+ // Only forwarded when present: the platform validates it against the
1086
+ // provider's pattern and rejects per-tenant providers that omit it.
1087
+ const tenantHost =
1088
+ typeof b.tenant_host === "string" ? b.tenant_host.trim() : "";
1089
+ if (tenantHost) {
1090
+ reqBody.tenant_host = tenantHost;
1091
+ }
1079
1092
 
1080
1093
  const response = await client.fetch(startPath, {
1081
1094
  method: "POST",
@@ -1122,6 +1135,7 @@ async function handleManagedConnectPoll({
1122
1135
  id: e.id,
1123
1136
  account_label: e.account_label ?? null,
1124
1137
  scopes_granted: e.scopes_granted ?? [],
1138
+ provider_params: e.provider_params ?? {},
1125
1139
  })),
1126
1140
  };
1127
1141
  }
@@ -397,6 +397,13 @@ export const oauthProviderSummarySchema = z.object({
397
397
  supports_managed_mode: z.boolean(),
398
398
  managed_service_is_paid: z.boolean(),
399
399
  feature_flag: z.string().nullable(),
400
+ tenant_host: z
401
+ .object({
402
+ pattern: z.string(),
403
+ label: z.string(),
404
+ placeholder: z.string(),
405
+ })
406
+ .nullable(),
400
407
  acts_as: z.enum(["user", "assistant"]),
401
408
  });
402
409
 
@@ -569,7 +569,7 @@ export const computerUseObserveTool = {
569
569
  include_screenshot: {
570
570
  type: "boolean",
571
571
  description:
572
- "Force a screenshot with this observation. The accessibility tree is returned every step; ask for pixels whenever the tree is not enough to act on, such as a canvas, a game, a custom-drawn view, a window with few or unlabeled controls, or a question about layout.",
572
+ "Force a screenshot with this observation. The accessibility tree is returned every step and actions always return a screenshot of their result; ask for pixels on an observation whenever the tree is not enough to act on, such as a canvas, a game, a custom-drawn view, a window with few or unlabeled controls, or a question about layout.",
573
573
  },
574
574
  full_tree: {
575
575
  type: "boolean",
@@ -1,68 +1,19 @@
1
1
  /**
2
- * Per-conversation wire projections of the UI surface tool definitions.
2
+ * Conversation-stable wire projections of the UI surface tool definitions.
3
3
  *
4
- * Slack renders exactly one surface — the task_progress card (everything else
5
- * is rejected at execution; see `isSlackTaskProgressUiException` in
6
- * `daemon/conversation-surfaces.ts`) — so Slack conversations get a `ui_show`
7
- * variant documenting only that shape instead of the full multi-surface docs.
8
- * Activation-rail conversations additionally get the `activation_moment`
9
- * telemetry param, which no other conversation pays schema tokens for (the
10
- * emit path is gated on `isActivationSession` regardless).
4
+ * `ui_show`, `ui_update`, and `ui_dismiss` keep one definition across every
5
+ * channel and across live vs clientless turns. Channel renderers enforce the
6
+ * surfaces they can display at execution. Clientless calls persist surface
7
+ * content for the next capable client that opens the conversation.
11
8
  *
12
- * Projection contract: shared definition objects are never mutated (variants
13
- * are module-level constants swapped in via spread-copy), and the projected
14
- * set is a pure function of per-conversation-stable state, so a
15
- * conversation's tools+system prompt-cache prefix stays stable across turns.
9
+ * `activation_moment` is the remaining schema projection. It applies only to
10
+ * activation-rail conversations, whose marker is written before the first
11
+ * tool resolution, so the extra property is stable for that conversation's
12
+ * lifetime. It must not vary by channel or turn presence.
16
13
  */
17
14
 
18
15
  import type { ToolDefinition } from "../../providers/types.js";
19
16
  import { ACTIVATION_MOMENT_PARAMS } from "../../telemetry/activation-funnel.js";
20
- import { TASK_PROGRESS_TEMPLATE_SHAPE } from "./surface-shape-docs.js";
21
-
22
- const SLACK_UI_SHOW_DESCRIPTION = `Show a live task-progress card for the current work — the only surface this channel renders. data: { template: "task_progress", templateData: ${TASK_PROGRESS_TEMPLATE_SHAPE} }. Show it early on multi-step turns and advance step statuses via ui_update as work progresses.`;
23
-
24
- const SLACK_UI_SHOW_INPUT_SCHEMA = {
25
- type: "object",
26
- properties: {
27
- surface_type: {
28
- type: "string",
29
- enum: ["card"],
30
- description: "Always card on this channel",
31
- },
32
- title: {
33
- type: "string",
34
- description: "Optional surface title",
35
- },
36
- data: {
37
- type: "object",
38
- description:
39
- 'Card data: { template: "task_progress", templateData: { title, status, steps } }',
40
- },
41
- },
42
- required: ["surface_type", "data"],
43
- };
44
-
45
- /**
46
- * Swap UI surface tool definitions for channel-appropriate variants. Returns
47
- * the input array unchanged (same identity) for channels without a variant.
48
- */
49
- export function projectUiToolsForChannel(
50
- definitions: ToolDefinition[],
51
- channel: string | undefined,
52
- ): ToolDefinition[] {
53
- if (channel !== "slack") {
54
- return definitions;
55
- }
56
- return definitions.map((def) =>
57
- def.name === "ui_show"
58
- ? {
59
- ...def,
60
- description: SLACK_UI_SHOW_DESCRIPTION,
61
- input_schema: SLACK_UI_SHOW_INPUT_SCHEMA,
62
- }
63
- : def,
64
- );
65
- }
66
17
 
67
18
  const ACTIVATION_MOMENT_PROPERTY = {
68
19
  type: "string",
@@ -73,7 +24,7 @@ const ACTIVATION_MOMENT_PROPERTY = {
73
24
 
74
25
  /**
75
26
  * Add the optional `activation_moment` param to ui_show's schema. Applied
76
- * only for activation-rail conversations (`isActivationSession`) — the rail
27
+ * only for activation-rail conversations (`isActivationSession`). The rail
77
28
  * bootstrap prompt instructs the tagging, and the daemon's emit path reads
78
29
  * the tag from tool input independently of this schema.
79
30
  */
@@ -61,14 +61,12 @@ const WATCH_RETRO_WAKE_SOURCE = "watch-retro";
61
61
  * under the `watch_retro` template, which the client draws as a paged card: the
62
62
  * record on the first page, one question per page after it.
63
63
  *
64
- * **The daemon appends the card; the model never reaches `ui_show`.** This wake
65
- * is `clientless`, and `conversation-tool-setup` gates the whole `ui_surface`
66
- * family on a client being present, so `ui_show` is absent from this turn's
67
- * tool set rather than merely denied. A retrospective told to call it can only
68
- * report that it cannot. `watch_retro_report` is an ordinary tool and passes
69
- * that gate, and the surface is appended here, the way the memory
70
- * retrospective's `skill_card` is appended by the daemon rather than requested
71
- * by the model.
64
+ * **The daemon appends the card; the model calls `watch_retro_report`.** This
65
+ * wake is `clientless`. `ui_show` stays on the wire so background surfaces can
66
+ * persist, but this flow still reports through `watch_retro_report` so the
67
+ * daemon can append the card after the turn ends. The memory retrospective's
68
+ * `skill_card` is appended the same way: the daemon writes the surface, the
69
+ * model does not request it.
72
70
  *
73
71
  * **The append waits for the turn to end.** A `ui_surface` row written mid-turn
74
72
  * can land between a persisted `tool_use` and its `tool_result`, an ordering