@vellumai/assistant 0.4.29 → 0.4.30

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 (174) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/README.md +5 -6
  3. package/docs/runbook-trusted-contacts.md +79 -43
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  6. package/scripts/test.sh +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  8. package/src/__tests__/actor-token-service.test.ts +4 -3
  9. package/src/__tests__/app-executors.test.ts +7 -17
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  11. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  12. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  13. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  14. package/src/__tests__/channel-approval.test.ts +8 -0
  15. package/src/__tests__/channel-approvals.test.ts +39 -1
  16. package/src/__tests__/channel-guardian.test.ts +15 -5
  17. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  18. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  20. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  21. package/src/__tests__/gemini-image-service.test.ts +2 -2
  22. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  23. package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
  24. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  25. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  26. package/src/__tests__/integrations-cli.test.ts +3 -27
  27. package/src/__tests__/intent-routing.test.ts +3 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  29. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  30. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  31. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  32. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  33. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  34. package/src/__tests__/relay-server.test.ts +1 -1
  35. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  36. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  37. package/src/__tests__/session-media-retry.test.ts +147 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  39. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  40. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  41. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  42. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  43. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  44. package/src/__tests__/slack-skill.test.ts +3 -2
  45. package/src/__tests__/starter-task-flow.test.ts +0 -1
  46. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  47. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  48. package/src/amazon/client.ts +7 -24
  49. package/src/calls/relay-server.ts +39 -11
  50. package/src/channels/config.ts +1 -1
  51. package/src/cli/integrations.ts +10 -66
  52. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  54. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  55. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  56. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  57. package/src/config/bundled-skills/contacts/SKILL.md +42 -35
  58. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  59. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
  60. package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
  61. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
  62. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  63. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  64. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  65. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  66. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  67. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  68. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  69. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  70. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  71. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  72. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  73. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  74. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  75. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  76. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  77. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  78. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  79. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  80. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  81. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  82. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  83. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  84. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  85. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  86. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  87. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  88. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  89. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  90. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  91. package/src/config/bundled-tool-registry.ts +2 -0
  92. package/src/config/channel-permission-profiles.ts +155 -0
  93. package/src/config/env.ts +4 -1
  94. package/src/contacts/contact-store.ts +195 -4
  95. package/src/contacts/types.ts +26 -0
  96. package/src/daemon/assistant-attachments.ts +23 -3
  97. package/src/daemon/guardian-verification-intent.ts +7 -4
  98. package/src/daemon/handlers/apps.ts +1 -2
  99. package/src/daemon/handlers/config-inbox.ts +16 -134
  100. package/src/daemon/handlers/guardian-actions.ts +20 -87
  101. package/src/daemon/handlers/sessions.ts +0 -1
  102. package/src/daemon/ipc-contract/apps.ts +0 -1
  103. package/src/daemon/ipc-contract/inbox.ts +7 -66
  104. package/src/daemon/ipc-contract/sessions.ts +1 -0
  105. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  106. package/src/daemon/ipc-contract-inventory.json +2 -4
  107. package/src/daemon/lifecycle.ts +14 -2
  108. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  109. package/src/daemon/session-agent-loop.ts +1 -0
  110. package/src/daemon/session-attachments.ts +5 -1
  111. package/src/daemon/session-error.ts +18 -0
  112. package/src/daemon/session-lifecycle.ts +4 -5
  113. package/src/daemon/session-media-retry.ts +15 -1
  114. package/src/daemon/session-surfaces.ts +0 -1
  115. package/src/daemon/session-tool-setup.ts +7 -4
  116. package/src/events/domain-events.ts +2 -1
  117. package/src/home-base/prebuilt/seed.ts +0 -1
  118. package/src/influencer/client.ts +7 -24
  119. package/src/media/gemini-image-service.ts +48 -3
  120. package/src/memory/app-store.ts +0 -4
  121. package/src/memory/conversation-attention-store.ts +3 -1
  122. package/src/memory/db-init.ts +4 -0
  123. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  124. package/src/memory/migrations/index.ts +1 -0
  125. package/src/memory/schema.ts +12 -0
  126. package/src/memory/slack-thread-store.ts +187 -0
  127. package/src/messaging/providers/slack/client.ts +84 -26
  128. package/src/messaging/providers/slack/types.ts +4 -0
  129. package/src/notifications/adapters/slack.ts +90 -0
  130. package/src/notifications/destination-resolver.ts +42 -1
  131. package/src/notifications/emit-signal.ts +17 -1
  132. package/src/oauth/provider-profiles.ts +22 -0
  133. package/src/providers/anthropic/client.ts +3 -0
  134. package/src/providers/openai/client.ts +3 -0
  135. package/src/providers/retry.ts +9 -1
  136. package/src/runtime/actor-trust-resolver.ts +8 -0
  137. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  138. package/src/runtime/auth/route-policy.ts +4 -8
  139. package/src/runtime/channel-approval-types.ts +18 -0
  140. package/src/runtime/channel-approvals.ts +8 -0
  141. package/src/runtime/channel-invite-transport.ts +1 -1
  142. package/src/runtime/channel-reply-delivery.ts +62 -3
  143. package/src/runtime/gateway-client.ts +36 -2
  144. package/src/runtime/gateway-internal-client.ts +86 -0
  145. package/src/runtime/guardian-action-service.ts +127 -0
  146. package/src/runtime/guardian-verification-templates.ts +16 -1
  147. package/src/runtime/http-server.ts +20 -49
  148. package/src/runtime/invite-redemption-service.ts +1 -1
  149. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  150. package/src/runtime/nl-approval-parser.ts +138 -0
  151. package/src/runtime/routes/approval-routes.ts +1 -40
  152. package/src/runtime/routes/channel-route-shared.ts +35 -1
  153. package/src/runtime/routes/contact-routes.ts +196 -28
  154. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  155. package/src/runtime/routes/guardian-approval-interception.ts +76 -0
  156. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
  158. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  159. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  160. package/src/runtime/slack-block-formatting.ts +176 -0
  161. package/src/schedule/scheduler.ts +11 -2
  162. package/src/tools/apps/executors.ts +16 -15
  163. package/src/tools/calls/call-end.ts +1 -1
  164. package/src/tools/computer-use/definitions.ts +16 -0
  165. package/src/tools/credentials/vault.ts +86 -2
  166. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  167. package/src/tools/permission-checker.ts +18 -0
  168. package/src/tools/terminal/shell.ts +15 -5
  169. package/src/tools/tool-approval-handler.ts +48 -4
  170. package/src/tools/types.ts +38 -1
  171. package/src/util/errors.ts +5 -1
  172. package/src/util/retry.ts +21 -0
  173. package/src/watcher/providers/slack.ts +33 -3
  174. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Lightweight Block Kit block generation for Slack channel replies.
3
+ *
4
+ * The gateway's text-to-blocks utility handles the full conversion, but
5
+ * the assistant pre-generates blocks so the gateway can pass them through
6
+ * without re-parsing. This keeps the conversion logic self-contained and
7
+ * avoids the gateway needing to distinguish pre-formatted from raw text.
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Block types (mirrors gateway/src/slack/block-kit-builder.ts)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ interface TextObject {
15
+ type: "mrkdwn" | "plain_text";
16
+ text: string;
17
+ }
18
+
19
+ interface SectionBlock {
20
+ type: "section";
21
+ text: TextObject;
22
+ }
23
+
24
+ interface DividerBlock {
25
+ type: "divider";
26
+ }
27
+
28
+ interface HeaderBlock {
29
+ type: "header";
30
+ text: TextObject;
31
+ }
32
+
33
+ type Block = SectionBlock | DividerBlock | HeaderBlock;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Public API
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Convert markdown/plain text into Slack Block Kit blocks.
41
+ *
42
+ * Returns undefined when the input is empty so callers can
43
+ * skip sending the `blocks` field entirely.
44
+ */
45
+ export function textToSlackBlocks(text: string): Block[] | undefined {
46
+ if (!text || text.trim().length === 0) return undefined;
47
+
48
+ const segments = splitIntoSegments(text);
49
+ const blocks: Block[] = [];
50
+
51
+ for (let i = 0; i < segments.length; i++) {
52
+ if (i > 0) {
53
+ blocks.push({ type: "divider" });
54
+ }
55
+
56
+ const segment = segments[i];
57
+
58
+ if (segment.type === "code") {
59
+ const lang = segment.lang ?? "";
60
+ const codeText = "```" + lang + "\n" + segment.content + "\n```";
61
+ blocks.push({
62
+ type: "section",
63
+ text: { type: "mrkdwn", text: codeText },
64
+ });
65
+ } else if (segment.type === "header") {
66
+ blocks.push({
67
+ type: "header",
68
+ text: { type: "plain_text", text: segment.content },
69
+ });
70
+ } else {
71
+ blocks.push({
72
+ type: "section",
73
+ text: { type: "mrkdwn", text: markdownToMrkdwn(segment.content) },
74
+ });
75
+ }
76
+ }
77
+
78
+ return blocks.length > 0 ? blocks : undefined;
79
+ }
80
+
81
+ /**
82
+ * Detect whether a callback URL points to the gateway's Slack delivery endpoint.
83
+ */
84
+ export function isSlackCallbackUrl(callbackUrl: string): boolean {
85
+ try {
86
+ const url = new URL(callbackUrl);
87
+ return (
88
+ url.pathname === "/deliver/slack" ||
89
+ url.pathname.startsWith("/deliver/slack?")
90
+ );
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Internals
98
+ // ---------------------------------------------------------------------------
99
+
100
+ interface TextSegment {
101
+ type: "text";
102
+ content: string;
103
+ }
104
+
105
+ interface CodeSegment {
106
+ type: "code";
107
+ content: string;
108
+ lang?: string;
109
+ }
110
+
111
+ interface HeaderSegment {
112
+ type: "header";
113
+ content: string;
114
+ }
115
+
116
+ type Segment = TextSegment | CodeSegment | HeaderSegment;
117
+
118
+ function splitIntoSegments(text: string): Segment[] {
119
+ const lines = text.split("\n");
120
+ const segments: Segment[] = [];
121
+ let currentTextLines: string[] = [];
122
+
123
+ function flushText(): void {
124
+ const joined = currentTextLines.join("\n").trim();
125
+ if (joined.length > 0) {
126
+ segments.push({ type: "text", content: joined });
127
+ }
128
+ currentTextLines = [];
129
+ }
130
+
131
+ let i = 0;
132
+ while (i < lines.length) {
133
+ const line = lines[i];
134
+ const fenceMatch = line.match(/^```(\w*)\s*$/);
135
+
136
+ if (fenceMatch) {
137
+ flushText();
138
+ const lang = fenceMatch[1] || undefined;
139
+ const codeLines: string[] = [];
140
+ i++;
141
+ while (i < lines.length && !lines[i].match(/^```\s*$/)) {
142
+ codeLines.push(lines[i]);
143
+ i++;
144
+ }
145
+ segments.push({ type: "code", content: codeLines.join("\n"), lang });
146
+ i++; // skip closing fence
147
+ continue;
148
+ }
149
+
150
+ const headingMatch = line.match(/^#{1,3}\s+(.+)$/);
151
+ if (headingMatch) {
152
+ flushText();
153
+ segments.push({ type: "header", content: headingMatch[1].trim() });
154
+ i++;
155
+ continue;
156
+ }
157
+
158
+ currentTextLines.push(line);
159
+ i++;
160
+ }
161
+
162
+ flushText();
163
+ return segments;
164
+ }
165
+
166
+ function markdownToMrkdwn(text: string): string {
167
+ let result = text;
168
+ // [text](url) → <url|text>
169
+ result = result.replace(
170
+ /\[([^\]]+)\]\(([^)]+)\)/g,
171
+ (_match, linkText, url) => `<${url}|${linkText}>`,
172
+ );
173
+ // **bold** → *bold*
174
+ result = result.replace(/\*\*(.+?)\*\*/g, "*$1*");
175
+ return result;
176
+ }
@@ -23,9 +23,14 @@ import {
23
23
 
24
24
  const log = getLogger("scheduler");
25
25
 
26
+ export interface ScheduleMessageOptions {
27
+ trustClass?: "guardian" | "trusted_contact" | "unknown";
28
+ }
29
+
26
30
  export type ScheduleMessageProcessor = (
27
31
  conversationId: string,
28
32
  message: string,
33
+ options?: ScheduleMessageOptions,
29
34
  ) => Promise<unknown>;
30
35
 
31
36
  export type ReminderNotifier = (reminder: {
@@ -218,7 +223,9 @@ async function runScheduleOnce(
218
223
  },
219
224
  "Executing schedule",
220
225
  );
221
- await processMessage(conversation.id, job.message);
226
+ await processMessage(conversation.id, job.message, {
227
+ trustClass: "guardian",
228
+ });
222
229
  completeScheduleRun(runId, { status: "ok" });
223
230
  notifySchedule({ id: job.id, name: job.name });
224
231
  processed += 1;
@@ -267,7 +274,9 @@ async function runScheduleOnce(
267
274
  },
268
275
  "Executing reminder",
269
276
  );
270
- await processMessage(conversation.id, reminder.message);
277
+ await processMessage(conversation.id, reminder.message, {
278
+ trustClass: "guardian",
279
+ });
271
280
  completeReminder(reminder.id);
272
281
  } catch (err) {
273
282
  log.warn(
@@ -43,7 +43,6 @@ export interface AppStoreWriter {
43
43
  schemaJson: string;
44
44
  htmlDefinition: string;
45
45
  pages?: Record<string, string>;
46
- appType?: "app" | "site";
47
46
  }): AppDefinition;
48
47
  updateApp(
49
48
  id: string,
@@ -84,9 +83,8 @@ export interface AppCreateInput {
84
83
  name: string;
85
84
  description?: string;
86
85
  schema_json?: string;
87
- html: string;
86
+ html?: string;
88
87
  pages?: Record<string, string>;
89
- type?: "app" | "site";
90
88
  auto_open?: boolean;
91
89
  set_as_home_base?: boolean;
92
90
  preview?: Record<string, unknown>;
@@ -100,11 +98,24 @@ export async function executeAppCreate(
100
98
  const name = input.name;
101
99
  const description = input.description;
102
100
  const schemaJson = input.schema_json ?? "{}";
103
- const htmlDefinition = input.html;
101
+ // Default to a minimal scaffold only when html is truly omitted; reject
102
+ // invalid types (e.g. object/number) so malformed tool calls surface errors.
103
+ let htmlDefinition: string;
104
+ if (typeof input.html === "string") {
105
+ htmlDefinition = input.html;
106
+ } else if (input.html == null) {
107
+ htmlDefinition = "<!DOCTYPE html><html><head></head><body></body></html>";
108
+ } else {
109
+ return {
110
+ content: JSON.stringify({
111
+ error: `html must be a string, got ${typeof input.html}`,
112
+ }),
113
+ isError: true,
114
+ };
115
+ }
104
116
  const pages = input.pages;
105
117
  const autoOpen = input.auto_open !== false; // default true
106
118
  const preview = input.preview;
107
- const appType = input.type === "site" ? ("site" as const) : ("app" as const);
108
119
 
109
120
  // Validate required fields — LLM input is not type-checked at runtime
110
121
  if (typeof name !== "string" || name.trim() === "") {
@@ -115,15 +126,6 @@ export async function executeAppCreate(
115
126
  isError: true,
116
127
  };
117
128
  }
118
- if (typeof htmlDefinition !== "string") {
119
- return {
120
- content: JSON.stringify({
121
- error:
122
- "html is required and must be a string containing the HTML definition",
123
- }),
124
- isError: true,
125
- };
126
- }
127
129
  if (pages) {
128
130
  for (const [filename, content] of Object.entries(pages)) {
129
131
  if (typeof content !== "string") {
@@ -143,7 +145,6 @@ export async function executeAppCreate(
143
145
  schemaJson,
144
146
  htmlDefinition,
145
147
  pages,
146
- appType,
147
148
  });
148
149
 
149
150
  if (input.set_as_home_base) {
@@ -13,7 +13,7 @@ export async function executeCallEnd(
13
13
  };
14
14
  }
15
15
 
16
- const reason = input.reason as string | undefined;
16
+ const reason = input.end_reason as string | undefined;
17
17
 
18
18
  const result = await cancelCall({ callSessionId, reason });
19
19
 
@@ -21,6 +21,12 @@ function proxyExecute(): Promise<ToolExecutionResult> {
21
21
  );
22
22
  }
23
23
 
24
+ const reasonProperty = {
25
+ type: "string" as const,
26
+ description:
27
+ "Brief non-technical explanation of why this tool is being called",
28
+ };
29
+
24
30
  function makeClickTool(name: string, verb: string): Tool {
25
31
  return {
26
32
  name,
@@ -55,6 +61,7 @@ function makeClickTool(name: string, verb: string): Tool {
55
61
  type: "string",
56
62
  description: `Explanation of what you see and why you are ${verb.toLowerCase()}ing here`,
57
63
  },
64
+ reason: reasonProperty,
58
65
  },
59
66
  required: ["reasoning"],
60
67
  },
@@ -109,6 +116,7 @@ export const computerUseTypeTextTool: Tool = {
109
116
  type: "string",
110
117
  description: "Explanation of what you are typing and why",
111
118
  },
119
+ reason: reasonProperty,
112
120
  },
113
121
  required: ["text", "reasoning"],
114
122
  },
@@ -146,6 +154,7 @@ export const computerUseKeyTool: Tool = {
146
154
  type: "string",
147
155
  description: "Explanation of why you are pressing this key",
148
156
  },
157
+ reason: reasonProperty,
149
158
  },
150
159
  required: ["key", "reasoning"],
151
160
  },
@@ -200,6 +209,7 @@ export const computerUseScrollTool: Tool = {
200
209
  type: "string",
201
210
  description: "Explanation of why you are scrolling",
202
211
  },
212
+ reason: reasonProperty,
203
213
  },
204
214
  required: ["direction", "amount", "reasoning"],
205
215
  },
@@ -260,6 +270,7 @@ export const computerUseDragTool: Tool = {
260
270
  type: "string",
261
271
  description: "Explanation of what you are dragging and why",
262
272
  },
273
+ reason: reasonProperty,
263
274
  },
264
275
  required: ["reasoning"],
265
276
  },
@@ -295,6 +306,7 @@ export const computerUseWaitTool: Tool = {
295
306
  type: "string",
296
307
  description: "Explanation of what you are waiting for",
297
308
  },
309
+ reason: reasonProperty,
298
310
  },
299
311
  required: ["duration_ms", "reasoning"],
300
312
  },
@@ -333,6 +345,7 @@ export const computerUseOpenAppTool: Tool = {
333
345
  description:
334
346
  "Explanation of why you need to open or switch to this app",
335
347
  },
348
+ reason: reasonProperty,
336
349
  },
337
350
  required: ["app_name", "reasoning"],
338
351
  },
@@ -376,6 +389,7 @@ export const computerUseRunAppleScriptTool: Tool = {
376
389
  description:
377
390
  "Explanation of what this script does and why AppleScript is better than UI interaction for this step",
378
391
  },
392
+ reason: reasonProperty,
379
393
  },
380
394
  required: ["script", "reasoning"],
381
395
  },
@@ -407,6 +421,7 @@ export const computerUseDoneTool: Tool = {
407
421
  type: "string",
408
422
  description: "Human-readable summary of what was accomplished",
409
423
  },
424
+ reason: reasonProperty,
410
425
  },
411
426
  required: ["summary"],
412
427
  },
@@ -443,6 +458,7 @@ export const computerUseRespondTool: Tool = {
443
458
  type: "string",
444
459
  description: "Explanation of how you determined the answer",
445
460
  },
461
+ reason: reasonProperty,
446
462
  },
447
463
  required: ["answer", "reasoning"],
448
464
  },
@@ -227,7 +227,7 @@ class CredentialStoreTool implements Tool {
227
227
  required: ["hostPattern", "injectionType"],
228
228
  },
229
229
  description:
230
- "Templates describing how to inject this credential into proxied requests (only for store action)",
230
+ "Templates describing how to inject this credential into proxied requests (for store and prompt actions)",
231
231
  },
232
232
  reason: {
233
233
  type: "string",
@@ -568,6 +568,88 @@ class CredentialStoreTool implements Tool {
568
568
  }
569
569
  const promptPolicy = toPolicyFromInput(promptPolicyInput);
570
570
 
571
+ // Parse and validate injection templates (same logic as store action)
572
+ const promptRawTemplates = input.injection_templates as unknown[] | undefined;
573
+ let promptInjectionTemplates: CredentialInjectionTemplate[] | undefined;
574
+ if (promptRawTemplates !== undefined) {
575
+ if (!Array.isArray(promptRawTemplates)) {
576
+ return {
577
+ content: "Error: injection_templates must be an array",
578
+ isError: true,
579
+ };
580
+ }
581
+ const promptTemplateErrors: string[] = [];
582
+ promptInjectionTemplates = [];
583
+ for (let i = 0; i < promptRawTemplates.length; i++) {
584
+ const t = promptRawTemplates[i] as Record<string, unknown>;
585
+ if (typeof t !== "object" || t == null) {
586
+ promptTemplateErrors.push(
587
+ `injection_templates[${i}] must be an object`,
588
+ );
589
+ continue;
590
+ }
591
+ if (
592
+ typeof t.hostPattern !== "string" ||
593
+ t.hostPattern.trim().length === 0
594
+ ) {
595
+ promptTemplateErrors.push(
596
+ `injection_templates[${i}].hostPattern must be a non-empty string`,
597
+ );
598
+ }
599
+ if (t.injectionType !== "header" && t.injectionType !== "query") {
600
+ promptTemplateErrors.push(
601
+ `injection_templates[${i}].injectionType must be 'header' or 'query'`,
602
+ );
603
+ } else if (t.injectionType === "header") {
604
+ if (
605
+ typeof t.headerName !== "string" ||
606
+ t.headerName.trim().length === 0
607
+ ) {
608
+ promptTemplateErrors.push(
609
+ `injection_templates[${i}].headerName is required when injectionType is 'header'`,
610
+ );
611
+ }
612
+ } else if (t.injectionType === "query") {
613
+ if (
614
+ typeof t.queryParamName !== "string" ||
615
+ t.queryParamName.trim().length === 0
616
+ ) {
617
+ promptTemplateErrors.push(
618
+ `injection_templates[${i}].queryParamName is required when injectionType is 'query'`,
619
+ );
620
+ }
621
+ }
622
+ if (
623
+ t.valuePrefix !== undefined &&
624
+ typeof t.valuePrefix !== "string"
625
+ ) {
626
+ promptTemplateErrors.push(
627
+ `injection_templates[${i}].valuePrefix must be a string`,
628
+ );
629
+ }
630
+ if (promptTemplateErrors.length === 0) {
631
+ promptInjectionTemplates.push({
632
+ hostPattern: t.hostPattern as string,
633
+ injectionType: t.injectionType as "header" | "query",
634
+ headerName:
635
+ typeof t.headerName === "string" ? t.headerName : undefined,
636
+ valuePrefix:
637
+ typeof t.valuePrefix === "string" ? t.valuePrefix : undefined,
638
+ queryParamName:
639
+ typeof t.queryParamName === "string"
640
+ ? t.queryParamName
641
+ : undefined,
642
+ });
643
+ }
644
+ }
645
+ if (promptTemplateErrors.length > 0) {
646
+ return {
647
+ content: `Error: ${promptTemplateErrors.join("; ")}`,
648
+ isError: true,
649
+ };
650
+ }
651
+ }
652
+
571
653
  try {
572
654
  assertMetadataWritable();
573
655
  } catch {
@@ -626,6 +708,7 @@ class CredentialStoreTool implements Tool {
626
708
  allowedTools: promptPolicy.allowedTools,
627
709
  allowedDomains: promptPolicy.allowedDomains,
628
710
  usageDescription: promptPolicy.usageDescription,
711
+ injectionTemplates: promptInjectionTemplates,
629
712
  });
630
713
  } catch (err) {
631
714
  // Without metadata the broker's policy checks will reject usage,
@@ -666,6 +749,7 @@ class CredentialStoreTool implements Tool {
666
749
  allowedTools: promptPolicy.allowedTools,
667
750
  allowedDomains: promptPolicy.allowedDomains,
668
751
  usageDescription: promptPolicy.usageDescription,
752
+ injectionTemplates: promptInjectionTemplates,
669
753
  });
670
754
  } catch (err) {
671
755
  log.warn(
@@ -673,7 +757,7 @@ class CredentialStoreTool implements Tool {
673
757
  "metadata write failed after storing credential",
674
758
  );
675
759
  }
676
- const promptMeta = getCredentialMetadata(service, field);
760
+ const promptMeta = getCredentialMetadata(service, field);
677
761
  const promptCredIdSuffix = promptMeta
678
762
  ? ` (credential_id: ${promptMeta.credentialId})`
679
763
  : "";
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import type { Server } from "node:http";
3
3
  import { join } from "node:path";
4
4
 
5
+ import { getProviderProfile } from "../../../oauth/provider-profiles.js";
5
6
  import {
6
7
  buildDecisionTrace,
7
8
  createProxyServer,
@@ -29,7 +30,10 @@ import {
29
30
  } from "../../credentials/host-pattern-match.js";
30
31
  import { listCredentialMetadata } from "../../credentials/metadata-store.js";
31
32
  import type { CredentialInjectionTemplate } from "../../credentials/policy-types.js";
32
- import { resolveById } from "../../credentials/resolve.js";
33
+ import {
34
+ resolveById,
35
+ type ResolvedCredential,
36
+ } from "../../credentials/resolve.js";
33
37
 
34
38
  const log = getLogger("proxy-session");
35
39
 
@@ -62,6 +66,26 @@ const sessions = new Map<ProxySessionId, ManagedSession>();
62
66
  */
63
67
  const acquireLocks = new Map<string, Promise<ProxySession>>();
64
68
 
69
+ /**
70
+ * Resolve injection templates for a credential.
71
+ *
72
+ * Preferred source is credential metadata. For legacy OAuth credentials that
73
+ * predate provider-level template registration, fall back to well-known
74
+ * profile templates when this is an access_token credential.
75
+ */
76
+ function resolveInjectionTemplates(
77
+ resolved: ResolvedCredential | undefined,
78
+ ): CredentialInjectionTemplate[] {
79
+ if (!resolved) return [];
80
+ if (resolved.injectionTemplates.length > 0)
81
+ return resolved.injectionTemplates;
82
+ if (resolved.field !== "access_token") return [];
83
+
84
+ const profile = getProviderProfile(resolved.service);
85
+ if (!profile?.injectionTemplates?.length) return [];
86
+ return profile.injectionTemplates;
87
+ }
88
+
65
89
  /** Return a defensive copy so callers cannot mutate internal state. */
66
90
  function cloneSession(s: ProxySession): ProxySession {
67
91
  return {
@@ -153,8 +177,9 @@ export async function startSession(
153
177
  const templates = new Map<string, CredentialInjectionTemplate[]>();
154
178
  for (const credId of managed.session.credentialIds) {
155
179
  const resolved = resolveById(credId);
156
- if (resolved?.injectionTemplates?.length) {
157
- templates.set(credId, resolved.injectionTemplates);
180
+ const injectionTemplates = resolveInjectionTemplates(resolved);
181
+ if (injectionTemplates.length > 0) {
182
+ templates.set(credId, injectionTemplates);
158
183
  }
159
184
  }
160
185
 
@@ -127,6 +127,24 @@ export class PermissionChecker {
127
127
  }
128
128
 
129
129
  if (result.decision === "prompt") {
130
+ // Guardian-trust sessions (e.g. scheduled jobs, reminders) should be
131
+ // able to use bundled tools without interactive approval. The guardian
132
+ // is the owner — prompting makes no sense when there is no client.
133
+ if (
134
+ context.isInteractive === false &&
135
+ context.trustClass === "guardian"
136
+ ) {
137
+ log.info(
138
+ { toolName: name, riskLevel },
139
+ "Auto-approving for non-interactive guardian session",
140
+ );
141
+ return {
142
+ allowed: true,
143
+ decision: "guardian_auto_approve",
144
+ riskLevel,
145
+ };
146
+ }
147
+
130
148
  // Non-interactive sessions have no client to respond to prompts —
131
149
  // deny immediately instead of blocking for the full permission timeout.
132
150
  if (context.isInteractive === false) {
@@ -1,10 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
 
3
3
  import { getConfig } from "../../config/loader.js";
4
- import {
5
- buildCredentialRefTrace,
6
- type ProxyEnvVars,
7
- } from "../../outbound-proxy/index.js";
8
4
  import { RiskLevel } from "../../permissions/types.js";
9
5
  import type { ToolDefinition } from "../../providers/types.js";
10
6
  import { redactSecrets } from "../../security/secret-scanner.js";
@@ -17,10 +13,24 @@ import {
17
13
  } from "../network/script-proxy/index.js";
18
14
  import { registerTool } from "../registry.js";
19
15
  import { formatShellOutput } from "../shared/shell-output.js";
20
- import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
16
+ import type {
17
+ ProxyEnvVars,
18
+ Tool,
19
+ ToolContext,
20
+ ToolExecutionResult,
21
+ } from "../types.js";
21
22
  import { buildSanitizedEnv } from "./safe-env.js";
22
23
  import { wrapCommand } from "./sandbox.js";
23
24
 
25
+ /** Build a credential ref resolution trace for diagnostic logging. */
26
+ function buildCredentialRefTrace(
27
+ rawRefs: string[],
28
+ resolvedIds: string[],
29
+ unresolvedRefs: string[],
30
+ ) {
31
+ return { rawRefs, resolvedIds, unresolvedRefs };
32
+ }
33
+
24
34
  const log = getLogger("shell-tool");
25
35
 
26
36
  class ShellTool implements Tool {