@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.
- package/ARCHITECTURE.md +39 -37
- package/README.md +5 -6
- package/docs/runbook-trusted-contacts.md +79 -43
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
- package/scripts/test.sh +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
- package/src/__tests__/actor-token-service.test.ts +4 -3
- package/src/__tests__/app-executors.test.ts +7 -17
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
- package/src/__tests__/browser-skill-endstate.test.ts +10 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +44 -44
- package/src/__tests__/channel-approval.test.ts +8 -0
- package/src/__tests__/channel-approvals.test.ts +39 -1
- package/src/__tests__/channel-guardian.test.ts +15 -5
- package/src/__tests__/channel-reply-delivery.test.ts +31 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/gemini-image-service.test.ts +2 -2
- package/src/__tests__/guardian-grant-minting.test.ts +6 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
- package/src/__tests__/integrations-cli.test.ts +3 -27
- package/src/__tests__/intent-routing.test.ts +3 -0
- package/src/__tests__/invite-redemption-service.test.ts +1 -1
- package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
- package/src/__tests__/ipc-snapshot.test.ts +4 -31
- package/src/__tests__/nl-approval-parser.test.ts +305 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
- package/src/__tests__/provider-error-scenarios.test.ts +68 -0
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/retry-after-extraction.test.ts +111 -0
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
- package/src/__tests__/session-media-retry.test.ts +147 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
- package/src/__tests__/skill-feature-flags.test.ts +18 -12
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
- package/src/__tests__/slack-block-formatting.test.ts +100 -0
- package/src/__tests__/slack-inbound-verification.test.ts +346 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/starter-task-flow.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +1 -1
- package/src/amazon/client.ts +7 -24
- package/src/calls/relay-server.ts +39 -11
- package/src/channels/config.ts +1 -1
- package/src/cli/integrations.ts +10 -66
- package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
- package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
- package/src/config/bundled-skills/browser/TOOLS.json +59 -2
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
- package/src/config/bundled-skills/contacts/SKILL.md +42 -35
- package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
- package/src/config/bundled-skills/document/TOOLS.json +8 -0
- package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
- package/src/config/bundled-skills/followups/TOOLS.json +12 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
- package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
- package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
- package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
- package/src/config/bundled-skills/notifications/SKILL.md +3 -2
- package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
- package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
- package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
- package/src/config/bundled-skills/schedule/SKILL.md +33 -15
- package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
- package/src/config/bundled-skills/slack/SKILL.md +30 -1
- package/src/config/bundled-skills/slack/TOOLS.json +89 -2
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
- package/src/config/bundled-skills/weather/TOOLS.json +4 -0
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/channel-permission-profiles.ts +155 -0
- package/src/config/env.ts +4 -1
- package/src/contacts/contact-store.ts +195 -4
- package/src/contacts/types.ts +26 -0
- package/src/daemon/assistant-attachments.ts +23 -3
- package/src/daemon/guardian-verification-intent.ts +7 -4
- package/src/daemon/handlers/apps.ts +1 -2
- package/src/daemon/handlers/config-inbox.ts +16 -134
- package/src/daemon/handlers/guardian-actions.ts +20 -87
- package/src/daemon/handlers/sessions.ts +0 -1
- package/src/daemon/ipc-contract/apps.ts +0 -1
- package/src/daemon/ipc-contract/inbox.ts +7 -66
- package/src/daemon/ipc-contract/sessions.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/session-agent-loop-handlers.ts +9 -0
- package/src/daemon/session-agent-loop.ts +1 -0
- package/src/daemon/session-attachments.ts +5 -1
- package/src/daemon/session-error.ts +18 -0
- package/src/daemon/session-lifecycle.ts +4 -5
- package/src/daemon/session-media-retry.ts +15 -1
- package/src/daemon/session-surfaces.ts +0 -1
- package/src/daemon/session-tool-setup.ts +7 -4
- package/src/events/domain-events.ts +2 -1
- package/src/home-base/prebuilt/seed.ts +0 -1
- package/src/influencer/client.ts +7 -24
- package/src/media/gemini-image-service.ts +48 -3
- package/src/memory/app-store.ts +0 -4
- package/src/memory/conversation-attention-store.ts +3 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +12 -0
- package/src/memory/slack-thread-store.ts +187 -0
- package/src/messaging/providers/slack/client.ts +84 -26
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/adapters/slack.ts +90 -0
- package/src/notifications/destination-resolver.ts +42 -1
- package/src/notifications/emit-signal.ts +17 -1
- package/src/oauth/provider-profiles.ts +22 -0
- package/src/providers/anthropic/client.ts +3 -0
- package/src/providers/openai/client.ts +3 -0
- package/src/providers/retry.ts +9 -1
- package/src/runtime/actor-trust-resolver.ts +8 -0
- package/src/runtime/auth/require-bound-guardian.ts +44 -0
- package/src/runtime/auth/route-policy.ts +4 -8
- package/src/runtime/channel-approval-types.ts +18 -0
- package/src/runtime/channel-approvals.ts +8 -0
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +62 -3
- package/src/runtime/gateway-client.ts +36 -2
- package/src/runtime/gateway-internal-client.ts +86 -0
- package/src/runtime/guardian-action-service.ts +127 -0
- package/src/runtime/guardian-verification-templates.ts +16 -1
- package/src/runtime/http-server.ts +20 -49
- package/src/runtime/invite-redemption-service.ts +1 -1
- package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
- package/src/runtime/nl-approval-parser.ts +138 -0
- package/src/runtime/routes/approval-routes.ts +1 -40
- package/src/runtime/routes/channel-route-shared.ts +35 -1
- package/src/runtime/routes/contact-routes.ts +196 -28
- package/src/runtime/routes/guardian-action-routes.ts +19 -111
- package/src/runtime/routes/guardian-approval-interception.ts +76 -0
- package/src/runtime/routes/inbound-message-handler.ts +40 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
- package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
- package/src/runtime/slack-block-formatting.ts +176 -0
- package/src/schedule/scheduler.ts +11 -2
- package/src/tools/apps/executors.ts +16 -15
- package/src/tools/calls/call-end.ts +1 -1
- package/src/tools/computer-use/definitions.ts +16 -0
- package/src/tools/credentials/vault.ts +86 -2
- package/src/tools/network/script-proxy/session-manager.ts +28 -3
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/terminal/shell.ts +15 -5
- package/src/tools/tool-approval-handler.ts +48 -4
- package/src/tools/types.ts +38 -1
- package/src/util/errors.ts +5 -1
- package/src/util/retry.ts +21 -0
- package/src/watcher/providers/slack.ts +33 -3
- /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
|
|
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
|
-
|
|
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) {
|
|
@@ -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 (
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
157
|
-
|
|
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 {
|
|
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 {
|