@vellumai/assistant 0.4.45 → 0.4.48
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 +6 -6
- package/docs/architecture/memory.md +1 -1
- package/docs/architecture/scheduling.md +2 -3
- package/docs/architecture/security.md +5 -5
- package/docs/trusted-contact-access.md +5 -6
- package/package.json +4 -1
- package/src/__tests__/avatar-e2e.test.ts +18 -219
- package/src/__tests__/avatar-generator.test.ts +5 -57
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-readiness-routes.test.ts +20 -19
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +240 -18
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +104 -7
- package/src/__tests__/credential-vault-unit.test.ts +22 -20
- package/src/__tests__/credential-vault.test.ts +284 -12
- package/src/__tests__/credentials-cli.test.ts +11 -6
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/integration-status.test.ts +53 -21
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/schedule-store.test.ts +1 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +0 -9
- package/src/__tests__/slack-channel-config.test.ts +9 -8
- package/src/__tests__/slack-share-routes.test.ts +11 -6
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
- package/src/approvals/AGENTS.md +1 -1
- package/src/calls/call-domain.ts +7 -4
- package/src/calls/twilio-config.ts +2 -1
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -2
- package/src/cli/commands/browser-relay.ts +40 -15
- package/src/cli/commands/credentials.ts +9 -8
- package/src/cli/commands/oauth.ts +1 -1
- package/src/cli.ts +3 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/loader.ts +6 -0
- package/src/daemon/computer-use-session.ts +7 -1
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-slack-channel.ts +37 -20
- package/src/daemon/handlers/config-telegram.ts +33 -20
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/ride-shotgun-handler.ts +3 -1
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-tool-setup.ts +18 -2
- package/src/daemon/session.ts +1 -1
- package/src/email/providers/index.ts +2 -1
- package/src/instrument.ts +15 -1
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +28 -62
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/schema/guardian.ts +1 -1
- package/src/messaging/provider.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +203 -122
- package/src/messaging/providers/gmail/people-client.ts +26 -18
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
- package/src/messaging/providers/whatsapp/adapter.ts +6 -3
- package/src/messaging/registry.ts +2 -1
- package/src/oauth/byo-connection.test.ts +436 -0
- package/src/oauth/byo-connection.ts +112 -0
- package/src/oauth/connect-orchestrator.ts +27 -0
- package/src/oauth/connection-resolver.ts +34 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/platform-connection.test.ts +163 -0
- package/src/oauth/platform-connection.ts +110 -0
- package/src/oauth/provider-base-urls.ts +21 -0
- package/src/oauth/provider-profiles.ts +1 -1
- package/src/oauth/token-persistence.ts +20 -20
- package/src/permissions/checker.ts +6 -1
- package/src/prompts/system-prompt.ts +52 -15
- package/src/prompts/templates/BOOTSTRAP.md +1 -1
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/runtime/AGENTS.md +18 -1
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -2
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +9 -4
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -3
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
- package/src/runtime/routes/integrations/slack/share.ts +3 -2
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +75 -17
- package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
- package/src/runtime/telegram-streaming-delivery.ts +11 -1
- package/src/schedule/integration-status.ts +5 -4
- package/src/security/credential-key.ts +170 -0
- package/src/security/token-manager.ts +36 -7
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/request-computer-control.ts +0 -5
- package/src/tools/credentials/broker.ts +6 -4
- package/src/tools/credentials/metadata-store.ts +72 -20
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +77 -16
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +0 -5
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +3 -44
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +93 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/avatar-router.test.ts +0 -149
- package/src/__tests__/managed-avatar-client.test.ts +0 -337
- package/src/config/bundled-skills/doordash/SKILL.md +0 -170
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
- package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
- package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
- package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
- package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
- package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
- package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
- package/src/media/avatar-types.ts +0 -53
- package/src/media/managed-avatar-client.ts +0 -225
|
@@ -532,11 +532,16 @@ export class RuntimeHttpServer {
|
|
|
532
532
|
if (!isHttpAuthDisabled()) {
|
|
533
533
|
const clientIp = extractClientIp(req, server);
|
|
534
534
|
const token = extractBearerToken(req);
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
535
|
+
const limiter = token ? apiRateLimiter : ipRateLimiter;
|
|
536
|
+
const limiterKind = token ? "authenticated" : "unauthenticated";
|
|
537
|
+
const result = limiter.check(clientIp, path);
|
|
538
538
|
if (!result.allowed) {
|
|
539
|
-
return rateLimitResponse(result
|
|
539
|
+
return rateLimitResponse(result, {
|
|
540
|
+
clientIp,
|
|
541
|
+
deniedPath: path,
|
|
542
|
+
limiterKind: limiterKind as "authenticated" | "unauthenticated",
|
|
543
|
+
pathCounts: limiter.getRecentPathCounts(clientIp),
|
|
544
|
+
});
|
|
540
545
|
}
|
|
541
546
|
const routerResponse = await this.router.dispatch(
|
|
542
547
|
endpoint,
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
// Tracks request counts per key and returns 429 when the limit is exceeded.
|
|
3
3
|
// Follows the same sliding-window pattern as gateway/src/auth-rate-limiter.ts.
|
|
4
4
|
|
|
5
|
+
import { getLogger } from "../../util/logger.js";
|
|
5
6
|
import type { HttpErrorResponse } from "../http-errors.js";
|
|
6
7
|
import { isPrivateAddress } from "./auth.js";
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
+
const log = getLogger("rate-limiter");
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_REQUESTS = 300;
|
|
9
12
|
const DEFAULT_WINDOW_MS = 60_000; // 60 seconds
|
|
10
13
|
const MAX_TRACKED_TOKENS = 10_000;
|
|
11
14
|
|
|
@@ -14,8 +17,13 @@ const DEFAULT_IP_MAX_REQUESTS = 20;
|
|
|
14
17
|
const DEFAULT_IP_WINDOW_MS = 60_000;
|
|
15
18
|
const MAX_TRACKED_IPS = 50_000;
|
|
16
19
|
|
|
20
|
+
interface RequestEntry {
|
|
21
|
+
timestamp: number;
|
|
22
|
+
path: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
export class TokenRateLimiter {
|
|
18
|
-
private requests = new Map<string,
|
|
26
|
+
private requests = new Map<string, RequestEntry[]>();
|
|
19
27
|
private readonly maxRequests: number;
|
|
20
28
|
private readonly windowMs: number;
|
|
21
29
|
private readonly maxTrackedKeys: number;
|
|
@@ -34,11 +42,11 @@ export class TokenRateLimiter {
|
|
|
34
42
|
* Check whether the request should be allowed and record it.
|
|
35
43
|
* Returns rate limit metadata for response headers.
|
|
36
44
|
*/
|
|
37
|
-
check(key: string): RateLimitResult {
|
|
45
|
+
check(key: string, path?: string): RateLimitResult {
|
|
38
46
|
const now = Date.now();
|
|
39
|
-
let
|
|
47
|
+
let entries = this.requests.get(key);
|
|
40
48
|
|
|
41
|
-
if (!
|
|
49
|
+
if (!entries) {
|
|
42
50
|
if (this.requests.size >= this.maxTrackedKeys) {
|
|
43
51
|
this.evictStale(now);
|
|
44
52
|
if (this.requests.size >= this.maxTrackedKeys) {
|
|
@@ -46,24 +54,24 @@ export class TokenRateLimiter {
|
|
|
46
54
|
if (oldest !== undefined) this.requests.delete(oldest);
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
|
-
|
|
50
|
-
this.requests.set(key,
|
|
57
|
+
entries = [];
|
|
58
|
+
this.requests.set(key, entries);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
const cutoff = now - this.windowMs;
|
|
54
62
|
|
|
55
|
-
// Remove expired
|
|
56
|
-
while (
|
|
57
|
-
|
|
63
|
+
// Remove expired entries from the front
|
|
64
|
+
while (entries.length > 0 && entries[0].timestamp <= cutoff) {
|
|
65
|
+
entries.shift();
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
const remaining = Math.max(0, this.maxRequests -
|
|
68
|
+
const remaining = Math.max(0, this.maxRequests - entries.length);
|
|
61
69
|
const resetAt =
|
|
62
|
-
|
|
63
|
-
? Math.ceil((
|
|
70
|
+
entries.length > 0
|
|
71
|
+
? Math.ceil((entries[0].timestamp + this.windowMs) / 1000)
|
|
64
72
|
: Math.ceil((now + this.windowMs) / 1000);
|
|
65
73
|
|
|
66
|
-
if (
|
|
74
|
+
if (entries.length >= this.maxRequests) {
|
|
67
75
|
return {
|
|
68
76
|
allowed: false,
|
|
69
77
|
limit: this.maxRequests,
|
|
@@ -72,7 +80,7 @@ export class TokenRateLimiter {
|
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
|
|
83
|
+
entries.push({ timestamp: now, path: path ?? "unknown" });
|
|
76
84
|
|
|
77
85
|
return {
|
|
78
86
|
allowed: true,
|
|
@@ -82,13 +90,36 @@ export class TokenRateLimiter {
|
|
|
82
90
|
};
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Return a count of recent requests grouped by path for the given key.
|
|
95
|
+
* Sorted descending by count. Useful for diagnosing which endpoints
|
|
96
|
+
* are consuming the rate limit budget.
|
|
97
|
+
*/
|
|
98
|
+
getRecentPathCounts(key: string): Array<{ path: string; count: number }> {
|
|
99
|
+
const entries = this.requests.get(key);
|
|
100
|
+
if (!entries || entries.length === 0) return [];
|
|
101
|
+
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const cutoff = now - this.windowMs;
|
|
104
|
+
const counts = new Map<string, number>();
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (entry.timestamp > cutoff) {
|
|
107
|
+
counts.set(entry.path, (counts.get(entry.path) ?? 0) + 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Array.from(counts.entries())
|
|
112
|
+
.map(([path, count]) => ({ path, count }))
|
|
113
|
+
.sort((a, b) => b.count - a.count);
|
|
114
|
+
}
|
|
115
|
+
|
|
85
116
|
private evictStale(now: number): void {
|
|
86
117
|
const cutoff = now - this.windowMs;
|
|
87
|
-
for (const [key,
|
|
88
|
-
while (
|
|
89
|
-
|
|
118
|
+
for (const [key, entries] of this.requests) {
|
|
119
|
+
while (entries.length > 0 && entries[0].timestamp <= cutoff) {
|
|
120
|
+
entries.shift();
|
|
90
121
|
}
|
|
91
|
-
if (
|
|
122
|
+
if (entries.length === 0) {
|
|
92
123
|
this.requests.delete(key);
|
|
93
124
|
}
|
|
94
125
|
}
|
|
@@ -115,8 +146,31 @@ export function rateLimitHeaders(
|
|
|
115
146
|
}
|
|
116
147
|
|
|
117
148
|
/** Return a 429 response with rate limit headers and a Retry-After hint. */
|
|
118
|
-
export function rateLimitResponse(
|
|
149
|
+
export function rateLimitResponse(
|
|
150
|
+
result: RateLimitResult,
|
|
151
|
+
diagnostics?: {
|
|
152
|
+
clientIp: string;
|
|
153
|
+
deniedPath: string;
|
|
154
|
+
limiterKind: "authenticated" | "unauthenticated";
|
|
155
|
+
pathCounts: Array<{ path: string; count: number }>;
|
|
156
|
+
},
|
|
157
|
+
): Response {
|
|
119
158
|
const retryAfter = Math.max(1, result.resetAt - Math.ceil(Date.now() / 1000));
|
|
159
|
+
|
|
160
|
+
if (diagnostics) {
|
|
161
|
+
log.warn(
|
|
162
|
+
{
|
|
163
|
+
clientIp: diagnostics.clientIp,
|
|
164
|
+
deniedPath: diagnostics.deniedPath,
|
|
165
|
+
limiterKind: diagnostics.limiterKind,
|
|
166
|
+
limit: result.limit,
|
|
167
|
+
retryAfterSec: retryAfter,
|
|
168
|
+
recentRequests: diagnostics.pathCounts,
|
|
169
|
+
},
|
|
170
|
+
`Rate limited ${diagnostics.limiterKind} request: ${diagnostics.deniedPath} (${result.limit} req/min exceeded)`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
120
174
|
const body: HttpErrorResponse = {
|
|
121
175
|
error: { code: "RATE_LIMITED", message: "Too Many Requests" },
|
|
122
176
|
};
|
|
@@ -29,12 +29,11 @@ export const GATEWAY_SUBPATH_MAP: Record<string, string> = {
|
|
|
29
29
|
voice: "voice-webhook",
|
|
30
30
|
status: "status",
|
|
31
31
|
"connect-action": "connect-action",
|
|
32
|
-
sms: "sms",
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
35
|
* Direct Twilio webhook subpaths that are blocked in gateway_only mode.
|
|
37
|
-
* Includes all public-facing webhook paths (voice, status, connect-action
|
|
36
|
+
* Includes all public-facing webhook paths (voice, status, connect-action)
|
|
38
37
|
* because the runtime must never serve as a direct ingress for external webhooks.
|
|
39
38
|
* Internal forwarding endpoints (gateway->runtime) are unaffected.
|
|
40
39
|
*/
|
|
@@ -42,7 +41,6 @@ export const GATEWAY_ONLY_BLOCKED_SUBPATHS = new Set([
|
|
|
42
41
|
"voice-webhook",
|
|
43
42
|
"status",
|
|
44
43
|
"connect-action",
|
|
45
|
-
"sms",
|
|
46
44
|
]);
|
|
47
45
|
|
|
48
46
|
/**
|
|
@@ -38,6 +38,7 @@ export async function handleGetChannelReadiness(url: URL): Promise<Response> {
|
|
|
38
38
|
return {
|
|
39
39
|
channel: s.channel,
|
|
40
40
|
ready: s.ready,
|
|
41
|
+
setupStatus: s.setupStatus,
|
|
41
42
|
checkedAt: s.checkedAt,
|
|
42
43
|
stale: s.stale,
|
|
43
44
|
reasons: s.reasons,
|
|
@@ -91,6 +92,7 @@ export async function handleRefreshChannelReadiness(
|
|
|
91
92
|
return {
|
|
92
93
|
channel: s.channel,
|
|
93
94
|
ready: s.ready,
|
|
95
|
+
setupStatus: s.setupStatus,
|
|
94
96
|
checkedAt: s.checkedAt,
|
|
95
97
|
stale: s.stale,
|
|
96
98
|
reasons: s.reasons,
|
|
@@ -186,6 +186,7 @@ async function handleDiagnosticsExport(body: {
|
|
|
186
186
|
// been persisted — the user message and in-flight tool/usage data are
|
|
187
187
|
// still captured.
|
|
188
188
|
let anchorMessage;
|
|
189
|
+
let anchorIsFallback = false;
|
|
189
190
|
if (anchorMessageId) {
|
|
190
191
|
anchorMessage = db
|
|
191
192
|
.select()
|
|
@@ -220,6 +221,7 @@ async function handleDiagnosticsExport(body: {
|
|
|
220
221
|
.orderBy(desc(messages.createdAt))
|
|
221
222
|
.limit(1)
|
|
222
223
|
.get();
|
|
224
|
+
anchorIsFallback = true;
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
// 2. Compute the export time range.
|
|
@@ -250,15 +252,15 @@ async function handleDiagnosticsExport(body: {
|
|
|
250
252
|
rangeStart =
|
|
251
253
|
precedingUserMessage?.createdAt ?? anchorMessage.createdAt - 2000;
|
|
252
254
|
|
|
253
|
-
// When the anchor
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
rangeEnd =
|
|
259
|
-
usageRangeEnd =
|
|
260
|
-
?
|
|
261
|
-
:
|
|
255
|
+
// When the anchor was selected via the fallback "any message" path
|
|
256
|
+
// (because the assistant reply hasn't been persisted yet), extend the
|
|
257
|
+
// range to the current time so in-flight tool invocations and usage
|
|
258
|
+
// recorded after the user message are captured. An explicit anchor to a
|
|
259
|
+
// non-assistant message uses the message's own timestamp.
|
|
260
|
+
rangeEnd = anchorIsFallback ? now : anchorMessage.createdAt;
|
|
261
|
+
usageRangeEnd = anchorIsFallback
|
|
262
|
+
? now + 5000
|
|
263
|
+
: anchorMessage.createdAt + 5000;
|
|
262
264
|
} else {
|
|
263
265
|
// No messages at all — use the current time so we capture any
|
|
264
266
|
// in-flight LLM usage or tool invocations.
|
|
@@ -10,6 +10,7 @@ import { applyGuardianDecision } from "../../approvals/guardian-decision-primiti
|
|
|
10
10
|
import type { ChannelId } from "../../channels/types.js";
|
|
11
11
|
import type { TrustContext } from "../../daemon/session-runtime-assembly.js";
|
|
12
12
|
import {
|
|
13
|
+
getAllPendingApprovalsByGuardianChat,
|
|
13
14
|
getPendingApprovalForRequest,
|
|
14
15
|
getUnresolvedApprovalForRequest,
|
|
15
16
|
updateApprovalDecision,
|
|
@@ -123,7 +124,7 @@ export async function handleApprovalInterception(
|
|
|
123
124
|
// Only guardians can approve via reaction — non-guardian reactions are
|
|
124
125
|
// silently ignored to prevent self-approval.
|
|
125
126
|
if (callbackData?.startsWith("reaction:")) {
|
|
126
|
-
if (trustCtx.trustClass !== "guardian") {
|
|
127
|
+
if (trustCtx.trustClass !== "guardian" || !actorExternalId) {
|
|
127
128
|
return { handled: true, type: "stale_ignored" };
|
|
128
129
|
}
|
|
129
130
|
const reactionDecision = parseReactionCallbackData(callbackData);
|
|
@@ -131,13 +132,27 @@ export async function handleApprovalInterception(
|
|
|
131
132
|
// Unknown emoji — ignore silently
|
|
132
133
|
return { handled: true, type: "stale_ignored" };
|
|
133
134
|
}
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
|
|
136
|
+
const allPending = getAllPendingApprovalsByGuardianChat(
|
|
137
|
+
sourceChannel,
|
|
138
|
+
conversationExternalId,
|
|
139
|
+
);
|
|
140
|
+
const guardianPending = allPending.filter(
|
|
141
|
+
(approval) => approval.guardianExternalUserId === actorExternalId,
|
|
142
|
+
);
|
|
143
|
+
if (guardianPending.length !== 1) {
|
|
136
144
|
return { handled: true, type: "stale_ignored" };
|
|
137
145
|
}
|
|
138
|
-
|
|
146
|
+
|
|
147
|
+
const result = applyGuardianDecision({
|
|
148
|
+
approval: guardianPending[0],
|
|
149
|
+
decision: reactionDecision,
|
|
150
|
+
actorPrincipalId: undefined,
|
|
151
|
+
actorExternalUserId: actorExternalId,
|
|
152
|
+
actorChannel: sourceChannel,
|
|
153
|
+
});
|
|
139
154
|
if (result.applied) {
|
|
140
|
-
return { handled: true, type: "
|
|
155
|
+
return { handled: true, type: "guardian_decision_applied" };
|
|
141
156
|
}
|
|
142
157
|
return { handled: true, type: "stale_ignored" };
|
|
143
158
|
}
|
|
@@ -314,21 +314,35 @@ export async function enforceIngressAcl(
|
|
|
314
314
|
);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
// DM the requester so they have a private channel to reply with
|
|
318
|
+
// the verification code. Sending to the Slack user ID (not
|
|
319
|
+
// conversationExternalId) auto-opens a DM conversation.
|
|
317
320
|
if (replyCallbackUrl) {
|
|
321
|
+
const senderUserId = (canonicalSenderId ?? rawSenderId)!;
|
|
322
|
+
// Strip threadTs from the callback URL — it belongs to the
|
|
323
|
+
// originating channel thread and would cause errors in the DM.
|
|
324
|
+
let dmCallbackUrl = replyCallbackUrl;
|
|
325
|
+
try {
|
|
326
|
+
const url = new URL(replyCallbackUrl);
|
|
327
|
+
url.searchParams.delete("threadTs");
|
|
328
|
+
dmCallbackUrl = url.toString();
|
|
329
|
+
} catch {
|
|
330
|
+
// Malformed URL — use as-is
|
|
331
|
+
}
|
|
318
332
|
try {
|
|
319
333
|
await deliverChannelReply(
|
|
320
|
-
|
|
334
|
+
dmCallbackUrl,
|
|
321
335
|
{
|
|
322
|
-
chatId:
|
|
323
|
-
text: "I've notified the owner. They'll share a verification code with you if they approve access.",
|
|
336
|
+
chatId: senderUserId,
|
|
337
|
+
text: "I've notified the owner. They'll share a verification code with you if they approve access. You can reply with the code here.",
|
|
324
338
|
assistantId,
|
|
325
339
|
},
|
|
326
340
|
mintBearerToken(),
|
|
327
341
|
);
|
|
328
342
|
} catch (err) {
|
|
329
343
|
log.error(
|
|
330
|
-
{ err,
|
|
331
|
-
"Failed to deliver Slack verification
|
|
344
|
+
{ err, senderUserId },
|
|
345
|
+
"Failed to deliver Slack verification DM to requester",
|
|
332
346
|
);
|
|
333
347
|
}
|
|
334
348
|
}
|
|
@@ -371,14 +385,20 @@ export async function enforceIngressAcl(
|
|
|
371
385
|
const replyText = guardianNotified
|
|
372
386
|
? "Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."
|
|
373
387
|
: "Sorry, you haven't been approved to message this assistant.";
|
|
388
|
+
const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
389
|
+
chatId: conversationExternalId,
|
|
390
|
+
text: replyText,
|
|
391
|
+
assistantId,
|
|
392
|
+
};
|
|
393
|
+
// On Slack, send as ephemeral so only the requester sees the rejection
|
|
394
|
+
if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
|
|
395
|
+
replyPayload.ephemeral = true;
|
|
396
|
+
replyPayload.user = (canonicalSenderId ?? rawSenderId)!;
|
|
397
|
+
}
|
|
374
398
|
try {
|
|
375
399
|
await deliverChannelReply(
|
|
376
400
|
replyCallbackUrl,
|
|
377
|
-
|
|
378
|
-
chatId: conversationExternalId,
|
|
379
|
-
text: replyText,
|
|
380
|
-
assistantId,
|
|
381
|
-
},
|
|
401
|
+
replyPayload,
|
|
382
402
|
mintBearerToken(),
|
|
383
403
|
);
|
|
384
404
|
} catch (err) {
|
|
@@ -543,21 +563,31 @@ export async function enforceIngressAcl(
|
|
|
543
563
|
);
|
|
544
564
|
}
|
|
545
565
|
|
|
566
|
+
// DM the requester (same as non-member path)
|
|
546
567
|
if (replyCallbackUrl) {
|
|
568
|
+
const senderUserId = (canonicalSenderId ?? rawSenderId)!;
|
|
569
|
+
let dmCallbackUrl = replyCallbackUrl;
|
|
570
|
+
try {
|
|
571
|
+
const url = new URL(replyCallbackUrl);
|
|
572
|
+
url.searchParams.delete("threadTs");
|
|
573
|
+
dmCallbackUrl = url.toString();
|
|
574
|
+
} catch {
|
|
575
|
+
// Malformed URL — use as-is
|
|
576
|
+
}
|
|
547
577
|
try {
|
|
548
578
|
await deliverChannelReply(
|
|
549
|
-
|
|
579
|
+
dmCallbackUrl,
|
|
550
580
|
{
|
|
551
|
-
chatId:
|
|
552
|
-
text: "I've notified the owner. They'll share a verification code with you if they approve access.",
|
|
581
|
+
chatId: senderUserId,
|
|
582
|
+
text: "I've notified the owner. They'll share a verification code with you if they approve access. You can reply with the code here.",
|
|
553
583
|
assistantId,
|
|
554
584
|
},
|
|
555
585
|
mintBearerToken(),
|
|
556
586
|
);
|
|
557
587
|
} catch (err) {
|
|
558
588
|
log.error(
|
|
559
|
-
{ err,
|
|
560
|
-
"Failed to deliver Slack verification
|
|
589
|
+
{ err, senderUserId },
|
|
590
|
+
"Failed to deliver Slack verification DM to requester (inactive member)",
|
|
561
591
|
);
|
|
562
592
|
}
|
|
563
593
|
}
|
|
@@ -605,14 +635,25 @@ export async function enforceIngressAcl(
|
|
|
605
635
|
const replyText = guardianNotified
|
|
606
636
|
? "Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."
|
|
607
637
|
: "Sorry, you haven't been approved to message this assistant.";
|
|
638
|
+
const inactiveReplyPayload: Parameters<
|
|
639
|
+
typeof deliverChannelReply
|
|
640
|
+
>[1] = {
|
|
641
|
+
chatId: conversationExternalId,
|
|
642
|
+
text: replyText,
|
|
643
|
+
assistantId,
|
|
644
|
+
};
|
|
645
|
+
// On Slack, send as ephemeral so only the requester sees the rejection
|
|
646
|
+
if (
|
|
647
|
+
sourceChannel === "slack" &&
|
|
648
|
+
(canonicalSenderId ?? rawSenderId)
|
|
649
|
+
) {
|
|
650
|
+
inactiveReplyPayload.ephemeral = true;
|
|
651
|
+
inactiveReplyPayload.user = (canonicalSenderId ?? rawSenderId)!;
|
|
652
|
+
}
|
|
608
653
|
try {
|
|
609
654
|
await deliverChannelReply(
|
|
610
655
|
replyCallbackUrl,
|
|
611
|
-
|
|
612
|
-
chatId: conversationExternalId,
|
|
613
|
-
text: replyText,
|
|
614
|
-
assistantId,
|
|
615
|
-
},
|
|
656
|
+
inactiveReplyPayload,
|
|
616
657
|
mintBearerToken(),
|
|
617
658
|
);
|
|
618
659
|
} catch (err) {
|
|
@@ -640,14 +681,19 @@ export async function enforceIngressAcl(
|
|
|
640
681
|
"Ingress ACL: member policy deny",
|
|
641
682
|
);
|
|
642
683
|
if (replyCallbackUrl) {
|
|
684
|
+
const denyPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
685
|
+
chatId: conversationExternalId,
|
|
686
|
+
text: "Sorry, you haven't been approved to message this assistant.",
|
|
687
|
+
assistantId,
|
|
688
|
+
};
|
|
689
|
+
if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
|
|
690
|
+
denyPayload.ephemeral = true;
|
|
691
|
+
denyPayload.user = (canonicalSenderId ?? rawSenderId)!;
|
|
692
|
+
}
|
|
643
693
|
try {
|
|
644
694
|
await deliverChannelReply(
|
|
645
695
|
replyCallbackUrl,
|
|
646
|
-
|
|
647
|
-
chatId: conversationExternalId,
|
|
648
|
-
text: "Sorry, you haven't been approved to message this assistant.",
|
|
649
|
-
assistantId,
|
|
650
|
-
},
|
|
696
|
+
denyPayload,
|
|
651
697
|
mintBearerToken(),
|
|
652
698
|
);
|
|
653
699
|
} catch (err) {
|
|
@@ -149,14 +149,21 @@ export async function handleGuardianReplyIntercept(
|
|
|
149
149
|
if (routerResult.consumed) {
|
|
150
150
|
// Deliver reply text if the router produced one
|
|
151
151
|
if (routerResult.replyText) {
|
|
152
|
+
const routerReplyPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
153
|
+
chatId: conversationExternalId,
|
|
154
|
+
text: routerResult.replyText,
|
|
155
|
+
assistantId: canonicalAssistantId,
|
|
156
|
+
};
|
|
157
|
+
// On Slack, send guardian management replies (disambiguation, pending
|
|
158
|
+
// request lists, etc.) as ephemeral so only the guardian sees them.
|
|
159
|
+
if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
|
|
160
|
+
routerReplyPayload.ephemeral = true;
|
|
161
|
+
routerReplyPayload.user = (canonicalSenderId ?? rawSenderId)!;
|
|
162
|
+
}
|
|
152
163
|
try {
|
|
153
164
|
await deliverChannelReply(
|
|
154
165
|
replyCallbackUrl,
|
|
155
|
-
|
|
156
|
-
chatId: conversationExternalId,
|
|
157
|
-
text: routerResult.replyText,
|
|
158
|
-
assistantId: canonicalAssistantId,
|
|
159
|
-
},
|
|
166
|
+
routerReplyPayload,
|
|
160
167
|
mintBearerToken(),
|
|
161
168
|
);
|
|
162
169
|
} catch (err) {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
userInfo,
|
|
13
13
|
} from "../../../../messaging/providers/slack/client.js";
|
|
14
14
|
import type { SlackConversation } from "../../../../messaging/providers/slack/types.js";
|
|
15
|
+
import { credentialKey } from "../../../../security/credential-key.js";
|
|
15
16
|
import { getSecureKey } from "../../../../security/secure-keys.js";
|
|
16
17
|
import { getLogger } from "../../../../util/logger.js";
|
|
17
18
|
import { httpError } from "../../../http-errors.js";
|
|
@@ -29,8 +30,8 @@ const log = getLogger("slack-share");
|
|
|
29
30
|
*/
|
|
30
31
|
function resolveSlackToken(): string | undefined {
|
|
31
32
|
return (
|
|
32
|
-
getSecureKey("
|
|
33
|
-
getSecureKey("
|
|
33
|
+
getSecureKey(credentialKey("integration:slack", "access_token")) ??
|
|
34
|
+
getSecureKey(credentialKey("slack_channel", "bot_token"))
|
|
34
35
|
);
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { loadRawConfig, saveRawConfig } from "../../../config/loader.js";
|
|
22
22
|
import { syncTwilioWebhooks } from "../../../daemon/handlers/config-ingress.js";
|
|
23
23
|
import type { IngressConfig } from "../../../inbound/public-ingress-urls.js";
|
|
24
|
+
import { credentialKey } from "../../../security/credential-key.js";
|
|
24
25
|
import {
|
|
25
26
|
deleteSecureKeyAsync,
|
|
26
27
|
setSecureKeyAsync,
|
|
@@ -136,7 +137,7 @@ export async function handleSetTwilioCredentials(
|
|
|
136
137
|
// validation (gateway/src/credential-reader.ts), while the assistant reads
|
|
137
138
|
// from config via resolveAccountSid(). Both stores must stay in sync.
|
|
138
139
|
const sidStored = await setSecureKeyAsync(
|
|
139
|
-
"
|
|
140
|
+
credentialKey("twilio", "account_sid"),
|
|
140
141
|
body.accountSid,
|
|
141
142
|
);
|
|
142
143
|
if (!sidStored) {
|
|
@@ -148,11 +149,11 @@ export async function handleSetTwilioCredentials(
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
const tokenStored = await setSecureKeyAsync(
|
|
151
|
-
"
|
|
152
|
+
credentialKey("twilio", "auth_token"),
|
|
152
153
|
body.authToken,
|
|
153
154
|
);
|
|
154
155
|
if (!tokenStored) {
|
|
155
|
-
await deleteSecureKeyAsync("
|
|
156
|
+
await deleteSecureKeyAsync(credentialKey("twilio", "account_sid"));
|
|
156
157
|
return Response.json({
|
|
157
158
|
success: false,
|
|
158
159
|
hasCredentials: false,
|
|
@@ -202,8 +203,8 @@ export async function handleSetTwilioCredentials(
|
|
|
202
203
|
* DELETE /v1/integrations/twilio/credentials
|
|
203
204
|
*/
|
|
204
205
|
export async function handleClearTwilioCredentials(): Promise<Response> {
|
|
205
|
-
const r1 = await deleteSecureKeyAsync("
|
|
206
|
-
const r2 = await deleteSecureKeyAsync("
|
|
206
|
+
const r1 = await deleteSecureKeyAsync(credentialKey("twilio", "account_sid"));
|
|
207
|
+
const r2 = await deleteSecureKeyAsync(credentialKey("twilio", "auth_token"));
|
|
207
208
|
|
|
208
209
|
if (r1 === "error" || r2 === "error") {
|
|
209
210
|
return Response.json(
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
invalidateConfigCache,
|
|
5
5
|
} from "../../config/loader.js";
|
|
6
6
|
import { initializeProviders } from "../../providers/registry.js";
|
|
7
|
+
import { credentialKey } from "../../security/credential-key.js";
|
|
7
8
|
import {
|
|
8
9
|
deleteSecureKeyAsync,
|
|
9
10
|
getSecureKeyAsync,
|
|
@@ -78,7 +79,7 @@ export async function handleAddSecret(req: Request): Promise<Response> {
|
|
|
78
79
|
assertMetadataWritable();
|
|
79
80
|
const service = name.slice(0, colonIdx);
|
|
80
81
|
const field = name.slice(colonIdx + 1);
|
|
81
|
-
const key =
|
|
82
|
+
const key = credentialKey(service, field);
|
|
82
83
|
const stored = await setSecureKeyAsync(key, value);
|
|
83
84
|
if (!stored) {
|
|
84
85
|
return httpError(
|
|
@@ -164,7 +165,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
164
165
|
const service = name.slice(0, colonIdx);
|
|
165
166
|
const field = name.slice(colonIdx + 1);
|
|
166
167
|
assertMetadataWritable();
|
|
167
|
-
const key =
|
|
168
|
+
const key = credentialKey(service, field);
|
|
168
169
|
// Check existence first — the broker always returns "deleted" even
|
|
169
170
|
// for keys that don't exist, so we need a pre-check for 404 semantics.
|
|
170
171
|
const existing = await getSecureKeyAsync(key);
|