vellum 0.2.2 → 0.2.8
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/bun.lock +68 -100
- package/package.json +3 -3
- package/src/__tests__/asset-materialize-tool.test.ts +2 -2
- package/src/__tests__/checker.test.ts +104 -0
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +458 -0
- package/src/__tests__/handlers-twilio-config.test.ts +221 -0
- package/src/__tests__/ipc-snapshot.test.ts +20 -0
- package/src/__tests__/memory-regressions.test.ts +100 -2
- package/src/__tests__/oauth-callback-registry.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +298 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +342 -0
- package/src/__tests__/public-ingress-urls.test.ts +206 -0
- package/src/__tests__/session-conflict-gate.test.ts +28 -25
- package/src/__tests__/tool-executor.test.ts +88 -0
- package/src/__tests__/turn-commit.test.ts +64 -0
- package/src/calls/__tests__/twilio-webhook-urls.test.ts +162 -0
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/twilio-config.ts +25 -9
- package/src/calls/twilio-provider.ts +4 -4
- package/src/calls/twilio-routes.ts +10 -2
- package/src/calls/twilio-webhook-urls.ts +47 -0
- package/src/cli/map.ts +30 -6
- package/src/config/defaults.ts +5 -0
- package/src/config/schema.ts +34 -2
- package/src/config/system-prompt.ts +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -5
- package/src/daemon/computer-use-session.ts +2 -1
- package/src/daemon/handlers/config.ts +95 -4
- package/src/daemon/handlers/sessions.ts +2 -2
- package/src/daemon/handlers/work-items.ts +1 -1
- package/src/daemon/ipc-contract-inventory.json +8 -0
- package/src/daemon/ipc-contract.ts +39 -1
- package/src/daemon/ride-shotgun-handler.ts +2 -1
- package/src/daemon/session-agent-loop.ts +37 -2
- package/src/daemon/session-conflict-gate.ts +18 -109
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/inbound/public-ingress-urls.ts +106 -0
- package/src/memory/attachments-store.ts +0 -1
- package/src/memory/channel-delivery-store.ts +0 -1
- package/src/memory/conflict-intent.ts +114 -0
- package/src/memory/conversation-key-store.ts +0 -1
- package/src/memory/db.ts +346 -149
- package/src/memory/job-handlers/conflict.ts +23 -1
- package/src/memory/runs-store.ts +0 -3
- package/src/memory/schema.ts +0 -4
- package/src/runtime/gateway-client.ts +36 -0
- package/src/runtime/http-server.ts +140 -2
- package/src/runtime/routes/channel-routes.ts +121 -79
- package/src/security/oauth-callback-registry.ts +56 -0
- package/src/security/oauth2.ts +174 -58
- package/src/swarm/backend-claude-code.ts +1 -1
- package/src/tools/assets/search.ts +1 -36
- package/src/tools/browser/api-map.ts +123 -50
- package/src/tools/claude-code/claude-code.ts +131 -1
- package/src/tools/tasks/work-item-list.ts +16 -2
- package/src/workspace/commit-message-enrichment-service.ts +3 -3
- package/src/workspace/provider-commit-message-generator.ts +57 -14
- package/src/workspace/turn-commit.ts +6 -2
package/src/calls/call-domain.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { getCallOrchestrator, unregisterCallOrchestrator } from './call-state.js
|
|
|
20
20
|
import { activeRelayConnections } from './relay-server.js';
|
|
21
21
|
import { TwilioConversationRelayProvider } from './twilio-provider.js';
|
|
22
22
|
import { getTwilioConfig } from './twilio-config.js';
|
|
23
|
+
import { buildTwilioVoiceWebhookUrl, buildTwilioStatusCallbackUrl } from './twilio-webhook-urls.js';
|
|
23
24
|
import type { CallSession } from './types.js';
|
|
24
25
|
|
|
25
26
|
const log = getLogger('call-domain');
|
|
@@ -102,12 +103,11 @@ export async function startCall(input: StartCallInput): Promise<StartCallResult
|
|
|
102
103
|
|
|
103
104
|
log.info({ callSessionId: session.id, to: phoneNumber, task }, 'Initiating outbound call');
|
|
104
105
|
|
|
105
|
-
const baseUrl = config.webhookBaseUrl.replace(/\/$/, '');
|
|
106
106
|
const { callSid } = await provider.initiateCall({
|
|
107
107
|
from: config.phoneNumber,
|
|
108
108
|
to: phoneNumber,
|
|
109
|
-
webhookUrl:
|
|
110
|
-
statusCallbackUrl:
|
|
109
|
+
webhookUrl: buildTwilioVoiceWebhookUrl(config.webhookBaseUrl, session.id),
|
|
110
|
+
statusCallbackUrl: buildTwilioStatusCallbackUrl(config.webhookBaseUrl),
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
updateCallSession(session.id, { providerCallSid: callSid });
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getSecureKey } from '../security/secure-keys.js';
|
|
2
2
|
import { getLogger } from '../util/logger.js';
|
|
3
|
+
import { loadConfig } from '../config/loader.js';
|
|
4
|
+
import { getWebhookBaseUrl } from './twilio-webhook-urls.js';
|
|
5
|
+
import { getTwilioRelayUrl } from '../inbound/public-ingress-urls.js';
|
|
3
6
|
|
|
4
7
|
const log = getLogger('twilio-config');
|
|
5
8
|
|
|
@@ -12,21 +15,34 @@ export interface TwilioConfig {
|
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export function getTwilioConfig(): TwilioConfig {
|
|
15
|
-
const accountSid = getSecureKey('
|
|
16
|
-
const authToken = getSecureKey('
|
|
17
|
-
const phoneNumber = process.env.TWILIO_PHONE_NUMBER || getSecureKey('
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
const accountSid = getSecureKey('credential:twilio:account_sid');
|
|
19
|
+
const authToken = getSecureKey('credential:twilio:auth_token');
|
|
20
|
+
const phoneNumber = process.env.TWILIO_PHONE_NUMBER || getSecureKey('credential:twilio:phone_number') || '';
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
const webhookBaseUrl = getWebhookBaseUrl(config);
|
|
23
|
+
|
|
24
|
+
// In gateway_only mode, ignore TWILIO_WSS_BASE_URL and always use the
|
|
25
|
+
// centralized relay URL derived from the public ingress base URL.
|
|
26
|
+
let wssBaseUrl: string;
|
|
27
|
+
if (config.ingress.mode === 'gateway_only') {
|
|
28
|
+
if (process.env.TWILIO_WSS_BASE_URL) {
|
|
29
|
+
log.warn('TWILIO_WSS_BASE_URL env var is ignored in gateway-only mode. Relay URL is derived from ingress.publicBaseUrl.');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
wssBaseUrl = getTwilioRelayUrl(config);
|
|
33
|
+
} catch {
|
|
34
|
+
wssBaseUrl = '';
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
wssBaseUrl = process.env.TWILIO_WSS_BASE_URL || '';
|
|
38
|
+
}
|
|
20
39
|
|
|
21
40
|
if (!accountSid || !authToken) {
|
|
22
|
-
throw new Error('Twilio credentials not configured. Set
|
|
41
|
+
throw new Error('Twilio credentials not configured. Set credential:twilio:account_sid and credential:twilio:auth_token via the credential_store tool.');
|
|
23
42
|
}
|
|
24
43
|
if (!phoneNumber) {
|
|
25
44
|
throw new Error('TWILIO_PHONE_NUMBER not configured.');
|
|
26
45
|
}
|
|
27
|
-
if (!webhookBaseUrl) {
|
|
28
|
-
throw new Error('TWILIO_WEBHOOK_BASE_URL not configured.');
|
|
29
|
-
}
|
|
30
46
|
|
|
31
47
|
log.debug('Twilio config loaded successfully');
|
|
32
48
|
|
|
@@ -17,11 +17,11 @@ export class TwilioConversationRelayProvider implements VoiceProvider {
|
|
|
17
17
|
// ── Credential helpers ──────────────────────────────────────────────
|
|
18
18
|
|
|
19
19
|
private getCredentials(): { accountSid: string; authToken: string } {
|
|
20
|
-
const accountSid = getSecureKey('
|
|
21
|
-
const authToken = getSecureKey('
|
|
20
|
+
const accountSid = getSecureKey('credential:twilio:account_sid');
|
|
21
|
+
const authToken = getSecureKey('credential:twilio:auth_token');
|
|
22
22
|
if (!accountSid || !authToken) {
|
|
23
23
|
throw new Error(
|
|
24
|
-
'Twilio credentials not configured. Set
|
|
24
|
+
'Twilio credentials not configured. Set credential:twilio:account_sid and credential:twilio:auth_token via the credential_store tool.',
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
return { accountSid, authToken };
|
|
@@ -134,7 +134,7 @@ export class TwilioConversationRelayProvider implements VoiceProvider {
|
|
|
134
134
|
* HTTP server webhook middleware) can check availability independently.
|
|
135
135
|
*/
|
|
136
136
|
static getAuthToken(): string | null {
|
|
137
|
-
return getSecureKey('
|
|
137
|
+
return getSecureKey('credential:twilio:auth_token') ?? null;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
@@ -22,6 +22,8 @@ import type { CallStatus } from './types.js';
|
|
|
22
22
|
import { logDeadLetterEvent } from './call-recovery.js';
|
|
23
23
|
import { isTerminalState } from './call-state-machine.js';
|
|
24
24
|
import { getTwilioConfig } from './twilio-config.js';
|
|
25
|
+
import { loadConfig } from '../config/loader.js';
|
|
26
|
+
import { getTwilioRelayUrl } from '../inbound/public-ingress-urls.js';
|
|
25
27
|
|
|
26
28
|
const log = getLogger('twilio-routes');
|
|
27
29
|
|
|
@@ -125,8 +127,14 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
125
127
|
log.info({ callSessionId, callSid }, 'Stored CallSid from voice webhook');
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
+
const twilioConfig = getTwilioConfig();
|
|
131
|
+
let relayUrl: string;
|
|
132
|
+
try {
|
|
133
|
+
relayUrl = getTwilioRelayUrl(loadConfig());
|
|
134
|
+
} catch {
|
|
135
|
+
// Fallback to legacy resolution when ingress is not configured
|
|
136
|
+
relayUrl = resolveRelayUrl(twilioConfig.wssBaseUrl, twilioConfig.webhookBaseUrl);
|
|
137
|
+
}
|
|
130
138
|
const welcomeGreeting = process.env.CALL_WELCOME_GREETING ?? 'Hello, how can I help you today?';
|
|
131
139
|
|
|
132
140
|
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twilio webhook URL helpers.
|
|
3
|
+
*
|
|
4
|
+
* This module is a thin backward-compat wrapper that delegates to the
|
|
5
|
+
* centralized URL builders in inbound/public-ingress-urls.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getPublicBaseUrl,
|
|
10
|
+
type IngressConfig,
|
|
11
|
+
} from '../inbound/public-ingress-urls.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the webhook base URL from config, falling back to the
|
|
15
|
+
* TWILIO_WEBHOOK_BASE_URL environment variable with a deprecation warning.
|
|
16
|
+
* Throws if neither source provides a value.
|
|
17
|
+
*
|
|
18
|
+
* @deprecated Use `getPublicBaseUrl` from `inbound/public-ingress-urls.ts` instead.
|
|
19
|
+
*/
|
|
20
|
+
export function getWebhookBaseUrl(config: { calls: { webhookBaseUrl?: string }; ingress?: { publicBaseUrl?: string } }): string {
|
|
21
|
+
return getPublicBaseUrl(config as IngressConfig);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Trim whitespace and strip trailing slash from a URL string.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeBaseUrl(url: string): string {
|
|
28
|
+
return url.trim().replace(/\/+$/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the Twilio voice webhook URL for a given call session.
|
|
33
|
+
*
|
|
34
|
+
* @deprecated Use `getTwilioVoiceWebhookUrl` from `inbound/public-ingress-urls.ts` instead.
|
|
35
|
+
*/
|
|
36
|
+
export function buildTwilioVoiceWebhookUrl(baseUrl: string, callSessionId: string): string {
|
|
37
|
+
return `${normalizeBaseUrl(baseUrl)}/webhooks/twilio/voice?callSessionId=${callSessionId}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the Twilio status callback URL.
|
|
42
|
+
*
|
|
43
|
+
* @deprecated Use `getTwilioStatusCallbackUrl` from `inbound/public-ingress-urls.ts` instead.
|
|
44
|
+
*/
|
|
45
|
+
export function buildTwilioStatusCallbackUrl(baseUrl: string): string {
|
|
46
|
+
return `${normalizeBaseUrl(baseUrl)}/webhooks/twilio/status`;
|
|
47
|
+
}
|
package/src/cli/map.ts
CHANGED
|
@@ -15,9 +15,20 @@ import {
|
|
|
15
15
|
serialize,
|
|
16
16
|
createMessageParser,
|
|
17
17
|
} from '../daemon/ipc-protocol.js';
|
|
18
|
+
import { parse as parseTld } from 'tldts';
|
|
18
19
|
import { loadRecording } from '../tools/browser/recording-store.js';
|
|
19
20
|
import { analyzeApiMap, saveApiMap, printApiMapTable } from '../tools/browser/api-map.js';
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Extract the registrable base domain from a hostname.
|
|
24
|
+
* e.g. "open.spotify.com" → "spotify.com", "connect.garmin.com" → "garmin.com"
|
|
25
|
+
* Falls back to the input if tldts can't parse it.
|
|
26
|
+
*/
|
|
27
|
+
function getBaseDomain(domain: string): string {
|
|
28
|
+
const result = parseTld(domain);
|
|
29
|
+
return result.domain ?? domain;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
// ---------------------------------------------------------------------------
|
|
22
33
|
// Helpers
|
|
23
34
|
// ---------------------------------------------------------------------------
|
|
@@ -95,8 +106,12 @@ interface LearnResult {
|
|
|
95
106
|
recordingPath?: string;
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
async function startLearnSession(
|
|
99
|
-
|
|
109
|
+
async function startLearnSession(
|
|
110
|
+
navigateDomain: string,
|
|
111
|
+
recordDomain: string,
|
|
112
|
+
durationSeconds: number,
|
|
113
|
+
): Promise<LearnResult> {
|
|
114
|
+
await ensureChromeWithCDP(navigateDomain);
|
|
100
115
|
|
|
101
116
|
return new Promise((resolve, reject) => {
|
|
102
117
|
const socketPath = getSocketPath();
|
|
@@ -123,7 +138,8 @@ async function startLearnSession(domain: string, durationSeconds: number): Promi
|
|
|
123
138
|
durationSeconds,
|
|
124
139
|
intervalSeconds: 5,
|
|
125
140
|
mode: 'learn',
|
|
126
|
-
targetDomain:
|
|
141
|
+
targetDomain: recordDomain,
|
|
142
|
+
navigateDomain,
|
|
127
143
|
autoNavigate: true,
|
|
128
144
|
} as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
|
|
129
145
|
);
|
|
@@ -196,11 +212,19 @@ export function registerMapCommand(program: Command): void {
|
|
|
196
212
|
const duration = parseInt(opts.duration, 10);
|
|
197
213
|
|
|
198
214
|
try {
|
|
199
|
-
//
|
|
215
|
+
// Split into navigation domain (what Chrome browses) and recording domain (network filter).
|
|
216
|
+
// e.g. "open.spotify.com" → navigate open.spotify.com, record *.spotify.com
|
|
217
|
+
const navigateDomain = domain;
|
|
218
|
+
const recordDomain = getBaseDomain(domain);
|
|
219
|
+
|
|
200
220
|
if (!json) {
|
|
201
|
-
|
|
221
|
+
if (navigateDomain !== recordDomain) {
|
|
222
|
+
console.log(`Starting API map session: navigating ${navigateDomain}, recording *.${recordDomain} (${duration}s)...`);
|
|
223
|
+
} else {
|
|
224
|
+
console.log(`Starting API map session for ${domain} (${duration}s)...`);
|
|
225
|
+
}
|
|
202
226
|
}
|
|
203
|
-
const result = await startLearnSession(
|
|
227
|
+
const result = await startLearnSession(navigateDomain, recordDomain, duration);
|
|
204
228
|
|
|
205
229
|
if (!result.recordingId) {
|
|
206
230
|
outputError('Recording completed but no recording ID returned');
|
package/src/config/defaults.ts
CHANGED
|
@@ -217,6 +217,7 @@ export const DEFAULT_CONFIG: AssistantConfig = {
|
|
|
217
217
|
calls: {
|
|
218
218
|
enabled: true,
|
|
219
219
|
provider: 'twilio' as const,
|
|
220
|
+
webhookBaseUrl: '',
|
|
220
221
|
maxDurationSeconds: 3600,
|
|
221
222
|
userConsultTimeoutSeconds: 120,
|
|
222
223
|
disclosure: {
|
|
@@ -227,4 +228,8 @@ export const DEFAULT_CONFIG: AssistantConfig = {
|
|
|
227
228
|
denyCategories: [],
|
|
228
229
|
},
|
|
229
230
|
},
|
|
231
|
+
ingress: {
|
|
232
|
+
publicBaseUrl: '',
|
|
233
|
+
mode: 'compat' as const,
|
|
234
|
+
},
|
|
230
235
|
};
|
package/src/config/schema.ts
CHANGED
|
@@ -9,6 +9,7 @@ const VALID_SANDBOX_BACKENDS = ['native', 'docker'] as const;
|
|
|
9
9
|
const VALID_DOCKER_NETWORKS = ['none', 'bridge'] as const;
|
|
10
10
|
const VALID_PERMISSIONS_MODES = ['legacy', 'strict'] as const;
|
|
11
11
|
const VALID_CALL_PROVIDERS = ['twilio'] as const;
|
|
12
|
+
const VALID_INGRESS_MODES = ['gateway_only', 'compat'] as const;
|
|
12
13
|
|
|
13
14
|
export const TimeoutConfigSchema = z.object({
|
|
14
15
|
shellMaxTimeoutSec: z
|
|
@@ -780,8 +781,19 @@ export const WorkspaceGitConfigSchema = z.object({
|
|
|
780
781
|
.int().positive().default(2000),
|
|
781
782
|
backoffMaxMs: z.number({ error: 'workspaceGit.commitMessageLLM.breaker.backoffMaxMs must be a number' })
|
|
782
783
|
.int().positive().default(60000),
|
|
783
|
-
}).default({}),
|
|
784
|
-
}).default({
|
|
784
|
+
}).default({ openAfterFailures: 3, backoffBaseMs: 2000, backoffMaxMs: 60000 }),
|
|
785
|
+
}).default({
|
|
786
|
+
enabled: false,
|
|
787
|
+
useConfiguredProvider: true,
|
|
788
|
+
providerFastModelOverrides: {},
|
|
789
|
+
timeoutMs: 600,
|
|
790
|
+
maxTokens: 120,
|
|
791
|
+
temperature: 0.2,
|
|
792
|
+
maxFilesInPrompt: 30,
|
|
793
|
+
maxDiffBytes: 12000,
|
|
794
|
+
minRemainingTurnBudgetMs: 1000,
|
|
795
|
+
breaker: { openAfterFailures: 3, backoffBaseMs: 2000, backoffMaxMs: 60000 },
|
|
796
|
+
}),
|
|
785
797
|
});
|
|
786
798
|
|
|
787
799
|
export const AgentHeartbeatConfigSchema = z.object({
|
|
@@ -883,6 +895,9 @@ export const CallsConfigSchema = z.object({
|
|
|
883
895
|
error: `calls.provider must be one of: ${VALID_CALL_PROVIDERS.join(', ')}`,
|
|
884
896
|
})
|
|
885
897
|
.default('twilio'),
|
|
898
|
+
webhookBaseUrl: z
|
|
899
|
+
.string({ error: 'calls.webhookBaseUrl must be a string' })
|
|
900
|
+
.default(''),
|
|
886
901
|
maxDurationSeconds: z
|
|
887
902
|
.number({ error: 'calls.maxDurationSeconds must be a number' })
|
|
888
903
|
.int('calls.maxDurationSeconds must be an integer')
|
|
@@ -911,6 +926,17 @@ export const SkillsConfigSchema = z.object({
|
|
|
911
926
|
allowBundled: z.array(z.string()).nullable().default(null),
|
|
912
927
|
});
|
|
913
928
|
|
|
929
|
+
export const IngressConfigSchema = z.object({
|
|
930
|
+
publicBaseUrl: z
|
|
931
|
+
.string({ error: 'ingress.publicBaseUrl must be a string' })
|
|
932
|
+
.default(''),
|
|
933
|
+
mode: z
|
|
934
|
+
.enum(VALID_INGRESS_MODES, {
|
|
935
|
+
error: `ingress.mode must be one of: ${VALID_INGRESS_MODES.join(', ')}`,
|
|
936
|
+
})
|
|
937
|
+
.default('compat'),
|
|
938
|
+
});
|
|
939
|
+
|
|
914
940
|
export const AssistantConfigSchema = z.object({
|
|
915
941
|
provider: z
|
|
916
942
|
.enum(VALID_PROVIDERS, {
|
|
@@ -1149,6 +1175,7 @@ export const AssistantConfigSchema = z.object({
|
|
|
1149
1175
|
calls: CallsConfigSchema.default({
|
|
1150
1176
|
enabled: true,
|
|
1151
1177
|
provider: 'twilio',
|
|
1178
|
+
webhookBaseUrl: '',
|
|
1152
1179
|
maxDurationSeconds: 3600,
|
|
1153
1180
|
userConsultTimeoutSeconds: 120,
|
|
1154
1181
|
disclosure: {
|
|
@@ -1159,6 +1186,10 @@ export const AssistantConfigSchema = z.object({
|
|
|
1159
1186
|
denyCategories: [],
|
|
1160
1187
|
},
|
|
1161
1188
|
}),
|
|
1189
|
+
ingress: IngressConfigSchema.default({
|
|
1190
|
+
publicBaseUrl: '',
|
|
1191
|
+
mode: 'compat',
|
|
1192
|
+
}),
|
|
1162
1193
|
}).superRefine((config, ctx) => {
|
|
1163
1194
|
if (config.contextWindow.targetInputTokens >= config.contextWindow.maxInputTokens) {
|
|
1164
1195
|
ctx.addIssue({
|
|
@@ -1219,3 +1250,4 @@ export type WorkspaceGitConfig = z.infer<typeof WorkspaceGitConfigSchema>;
|
|
|
1219
1250
|
export type CallsConfig = z.infer<typeof CallsConfigSchema>;
|
|
1220
1251
|
export type CallsDisclosureConfig = z.infer<typeof CallsDisclosureConfigSchema>;
|
|
1221
1252
|
export type CallsSafetyConfig = z.infer<typeof CallsSafetyConfigSchema>;
|
|
1253
|
+
export type IngressConfig = z.infer<typeof IngressConfigSchema>;
|
|
@@ -218,7 +218,7 @@ function buildTaskScheduleReminderRoutingSection(): string {
|
|
|
218
218
|
'',
|
|
219
219
|
'You can create ad-hoc work items by providing just a `title` to `task_list_add` — no existing task template is needed. A lightweight template is auto-created behind the scenes. For reusable task definitions with templates and input schemas, use `task_save` first.',
|
|
220
220
|
'',
|
|
221
|
-
'**IMPORTANT:** When you call `task_list_show`, the Tasks window opens automatically on the client. Do NOT also create a separate surface/UI (via `ui_show` or `app_create`) to display the task queue
|
|
221
|
+
'**IMPORTANT:** When you call `task_list_show`, the Tasks window opens automatically on the client AND the tool returns the current task list. Present a brief summary of the tasks in your chat response so the user can see them inline. Do NOT also create a separate surface/UI (via `ui_show` or `app_create`) to display the task queue — that causes duplicate windows.',
|
|
222
222
|
'',
|
|
223
223
|
'### Schedules (schedule_create / schedule_list / schedule_update / schedule_delete)',
|
|
224
224
|
'For recurring automated jobs that run on a recurrence schedule (cron or RRULE). Use ONLY when the user explicitly wants:',
|
package/src/config/types.ts
CHANGED
|
@@ -98,8 +98,4 @@ Summarize what was done:
|
|
|
98
98
|
- Bot commands registered: /new
|
|
99
99
|
- Credentials stored securely in the vault
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
- `TELEGRAM_BOT_TOKEN` — the bot token
|
|
103
|
-
- `TELEGRAM_WEBHOOK_SECRET` — the generated secret
|
|
104
|
-
|
|
105
|
-
The values are stored in the credential vault and can be retrieved for gateway configuration.
|
|
101
|
+
The gateway automatically detects credentials from the vault and will begin accepting Telegram webhooks shortly. No manual environment variable configuration is needed.
|
|
@@ -15,6 +15,7 @@ import { AgentLoop } from '../agent/loop.js';
|
|
|
15
15
|
import { ToolExecutor } from '../tools/executor.js';
|
|
16
16
|
import { PermissionPrompter } from '../permissions/prompter.js';
|
|
17
17
|
import { SecretPrompter } from '../permissions/secret-prompter.js';
|
|
18
|
+
import type { UserDecision } from '../permissions/types.js';
|
|
18
19
|
import { allUiSurfaceTools } from '../tools/ui-surface/definitions.js';
|
|
19
20
|
import { allComputerUseTools } from '../tools/computer-use/definitions.js';
|
|
20
21
|
import { registerSkillTools } from '../tools/registry.js';
|
|
@@ -893,7 +894,7 @@ export class ComputerUseSession {
|
|
|
893
894
|
|
|
894
895
|
handleConfirmationResponse(
|
|
895
896
|
requestId: string,
|
|
896
|
-
decision:
|
|
897
|
+
decision: UserDecision,
|
|
897
898
|
selectedPattern?: string,
|
|
898
899
|
selectedScope?: string,
|
|
899
900
|
): void {
|
|
@@ -19,6 +19,8 @@ import type {
|
|
|
19
19
|
ReminderCancel,
|
|
20
20
|
ShareToSlackRequest,
|
|
21
21
|
SlackWebhookConfigRequest,
|
|
22
|
+
TwilioWebhookConfigRequest,
|
|
23
|
+
IngressConfigRequest,
|
|
22
24
|
VercelApiConfigRequest,
|
|
23
25
|
TwitterIntegrationConfigRequest,
|
|
24
26
|
} from '../ipc-protocol.js';
|
|
@@ -164,9 +166,9 @@ export function handleAddTrustRule(
|
|
|
164
166
|
): void {
|
|
165
167
|
try {
|
|
166
168
|
addRule(msg.toolName, msg.pattern, msg.scope, msg.decision);
|
|
167
|
-
log.info({
|
|
169
|
+
log.info({ toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope, decision: msg.decision }, 'Trust rule added via client');
|
|
168
170
|
} catch (err) {
|
|
169
|
-
log.error({ err }, 'Failed to add trust rule');
|
|
171
|
+
log.error({ err, toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope }, 'Failed to add trust rule via client');
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
174
|
|
|
@@ -396,6 +398,88 @@ export function handleSlackWebhookConfig(
|
|
|
396
398
|
}
|
|
397
399
|
}
|
|
398
400
|
|
|
401
|
+
export function handleTwilioWebhookConfig(
|
|
402
|
+
msg: TwilioWebhookConfigRequest,
|
|
403
|
+
socket: net.Socket,
|
|
404
|
+
ctx: HandlerContext,
|
|
405
|
+
): void {
|
|
406
|
+
try {
|
|
407
|
+
if (msg.action === 'get') {
|
|
408
|
+
const raw = loadRawConfig();
|
|
409
|
+
const webhookBaseUrl = (raw?.calls as Record<string, unknown>)?.webhookBaseUrl as string ?? '';
|
|
410
|
+
ctx.send(socket, { type: 'twilio_webhook_config_response', webhookBaseUrl, success: true });
|
|
411
|
+
} else if (msg.action === 'set') {
|
|
412
|
+
const value = (msg.webhookBaseUrl ?? '').trim().replace(/\/+$/, '');
|
|
413
|
+
const raw = loadRawConfig();
|
|
414
|
+
const calls = (raw?.calls ?? {}) as Record<string, unknown>;
|
|
415
|
+
calls.webhookBaseUrl = value || undefined;
|
|
416
|
+
const wasSuppressed = ctx.suppressConfigReload;
|
|
417
|
+
ctx.setSuppressConfigReload(true);
|
|
418
|
+
try {
|
|
419
|
+
saveRawConfig({ ...raw, calls });
|
|
420
|
+
} catch (err) {
|
|
421
|
+
ctx.setSuppressConfigReload(wasSuppressed);
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
424
|
+
const existingSuppressTimer = ctx.debounceTimers.get('__suppress_reset__');
|
|
425
|
+
if (existingSuppressTimer) clearTimeout(existingSuppressTimer);
|
|
426
|
+
const resetTimer = setTimeout(() => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
427
|
+
ctx.debounceTimers.set('__suppress_reset__', resetTimer);
|
|
428
|
+
ctx.send(socket, { type: 'twilio_webhook_config_response', webhookBaseUrl: value, success: true });
|
|
429
|
+
} else {
|
|
430
|
+
ctx.send(socket, { type: 'twilio_webhook_config_response', webhookBaseUrl: '', success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
434
|
+
ctx.send(socket, { type: 'twilio_webhook_config_response', webhookBaseUrl: '', success: false, error: message });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function handleIngressConfig(
|
|
439
|
+
msg: IngressConfigRequest,
|
|
440
|
+
socket: net.Socket,
|
|
441
|
+
ctx: HandlerContext,
|
|
442
|
+
): void {
|
|
443
|
+
try {
|
|
444
|
+
if (msg.action === 'get') {
|
|
445
|
+
const raw = loadRawConfig();
|
|
446
|
+
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
447
|
+
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? '';
|
|
448
|
+
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl, success: true });
|
|
449
|
+
} else if (msg.action === 'set') {
|
|
450
|
+
const value = (msg.publicBaseUrl ?? '').trim().replace(/\/+$/, '');
|
|
451
|
+
const raw = loadRawConfig();
|
|
452
|
+
|
|
453
|
+
// Update ingress.publicBaseUrl
|
|
454
|
+
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
455
|
+
ingress.publicBaseUrl = value || undefined;
|
|
456
|
+
|
|
457
|
+
// Also update calls.webhookBaseUrl for backward compat
|
|
458
|
+
const calls = (raw?.calls ?? {}) as Record<string, unknown>;
|
|
459
|
+
calls.webhookBaseUrl = value || undefined;
|
|
460
|
+
|
|
461
|
+
const wasSuppressed = ctx.suppressConfigReload;
|
|
462
|
+
ctx.setSuppressConfigReload(true);
|
|
463
|
+
try {
|
|
464
|
+
saveRawConfig({ ...raw, ingress, calls });
|
|
465
|
+
} catch (err) {
|
|
466
|
+
ctx.setSuppressConfigReload(wasSuppressed);
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
const existingSuppressTimer = ctx.debounceTimers.get('__suppress_reset__');
|
|
470
|
+
if (existingSuppressTimer) clearTimeout(existingSuppressTimer);
|
|
471
|
+
const resetTimer = setTimeout(() => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
472
|
+
ctx.debounceTimers.set('__suppress_reset__', resetTimer);
|
|
473
|
+
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl: value, success: true });
|
|
474
|
+
} else {
|
|
475
|
+
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl: '', success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
476
|
+
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
479
|
+
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl: '', success: false, error: message });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
399
483
|
export function handleVercelApiConfig(
|
|
400
484
|
msg: VercelApiConfigRequest,
|
|
401
485
|
socket: net.Socket,
|
|
@@ -503,6 +587,7 @@ export function handleTwitterIntegrationConfig(
|
|
|
503
587
|
});
|
|
504
588
|
return;
|
|
505
589
|
}
|
|
590
|
+
const previousClientId = getSecureKey('credential:integration:twitter:oauth_client_id');
|
|
506
591
|
const storedId = setSecureKey('credential:integration:twitter:oauth_client_id', msg.clientId);
|
|
507
592
|
if (!storedId) {
|
|
508
593
|
ctx.send(socket, {
|
|
@@ -518,8 +603,12 @@ export function handleTwitterIntegrationConfig(
|
|
|
518
603
|
if (msg.clientSecret) {
|
|
519
604
|
const storedSecret = setSecureKey('credential:integration:twitter:oauth_client_secret', msg.clientSecret);
|
|
520
605
|
if (!storedSecret) {
|
|
521
|
-
// Roll back the
|
|
522
|
-
|
|
606
|
+
// Roll back the client ID to its previous value to avoid inconsistent OAuth state
|
|
607
|
+
if (previousClientId) {
|
|
608
|
+
setSecureKey('credential:integration:twitter:oauth_client_id', previousClientId);
|
|
609
|
+
} else {
|
|
610
|
+
deleteSecureKey('credential:integration:twitter:oauth_client_id');
|
|
611
|
+
}
|
|
523
612
|
ctx.send(socket, {
|
|
524
613
|
type: 'twitter_integration_config_response',
|
|
525
614
|
success: false,
|
|
@@ -616,6 +705,8 @@ export const configHandlers = defineHandlers({
|
|
|
616
705
|
reminder_cancel: handleReminderCancel,
|
|
617
706
|
share_to_slack: handleShareToSlack,
|
|
618
707
|
slack_webhook_config: handleSlackWebhookConfig,
|
|
708
|
+
twilio_webhook_config: handleTwilioWebhookConfig,
|
|
709
|
+
ingress_config: handleIngressConfig,
|
|
619
710
|
vercel_api_config: handleVercelApiConfig,
|
|
620
711
|
twitter_integration_config: handleTwitterIntegrationConfig,
|
|
621
712
|
env_vars_request: (_msg, socket, ctx) => handleEnvVarsRequest(socket, ctx),
|
|
@@ -145,7 +145,7 @@ export function handleConfirmationResponse(
|
|
|
145
145
|
ctx.touchSession(sessionId);
|
|
146
146
|
session.handleConfirmationResponse(
|
|
147
147
|
msg.requestId,
|
|
148
|
-
msg.decision
|
|
148
|
+
msg.decision,
|
|
149
149
|
msg.selectedPattern,
|
|
150
150
|
msg.selectedScope,
|
|
151
151
|
);
|
|
@@ -158,7 +158,7 @@ export function handleConfirmationResponse(
|
|
|
158
158
|
if (cuSession.hasPendingConfirmation(msg.requestId)) {
|
|
159
159
|
cuSession.handleConfirmationResponse(
|
|
160
160
|
msg.requestId,
|
|
161
|
-
msg.decision
|
|
161
|
+
msg.decision,
|
|
162
162
|
msg.selectedPattern,
|
|
163
163
|
msg.selectedScope,
|
|
164
164
|
);
|
|
@@ -426,8 +426,8 @@ export async function handleWorkItemRunTask(
|
|
|
426
426
|
// Execute task asynchronously — lazily create a session inside the callback
|
|
427
427
|
// using the conversationId provided by runTask, so the session references
|
|
428
428
|
// the conversation that was actually inserted into the database.
|
|
429
|
+
let session: Awaited<ReturnType<typeof ctx.getOrCreateSession>> | null = null;
|
|
429
430
|
try {
|
|
430
|
-
let session: Awaited<ReturnType<typeof ctx.getOrCreateSession>> | null = null;
|
|
431
431
|
const result = await runTask(
|
|
432
432
|
{ taskId: workItem.taskId, workingDir: process.cwd(), approvedTools },
|
|
433
433
|
async (conversationId, message, taskRunId) => {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"HistoryRequest",
|
|
33
33
|
"HomeBaseGetRequest",
|
|
34
34
|
"ImageGenModelSetRequest",
|
|
35
|
+
"IngressConfigRequest",
|
|
35
36
|
"IntegrationConnectRequest",
|
|
36
37
|
"IntegrationDisconnectRequest",
|
|
37
38
|
"IntegrationListRequest",
|
|
@@ -80,6 +81,7 @@
|
|
|
80
81
|
"SuggestionRequest",
|
|
81
82
|
"TaskSubmit",
|
|
82
83
|
"TrustRulesList",
|
|
84
|
+
"TwilioWebhookConfigRequest",
|
|
83
85
|
"TwitterAuthStartRequest",
|
|
84
86
|
"TwitterAuthStatusRequest",
|
|
85
87
|
"TwitterIntegrationConfigRequest",
|
|
@@ -141,6 +143,7 @@
|
|
|
141
143
|
"GetSigningIdentityRequest",
|
|
142
144
|
"HistoryResponse",
|
|
143
145
|
"HomeBaseGetResponse",
|
|
146
|
+
"IngressConfigResponse",
|
|
144
147
|
"IntegrationConnectResult",
|
|
145
148
|
"IntegrationListResponse",
|
|
146
149
|
"IpcBlobProbeResult",
|
|
@@ -191,6 +194,7 @@
|
|
|
191
194
|
"ToolUseStart",
|
|
192
195
|
"TraceEvent",
|
|
193
196
|
"TrustRulesListResponse",
|
|
197
|
+
"TwilioWebhookConfigResponse",
|
|
194
198
|
"TwitterAuthResult",
|
|
195
199
|
"TwitterAuthStatusResponse",
|
|
196
200
|
"TwitterIntegrationConfigResponse",
|
|
@@ -253,6 +257,7 @@
|
|
|
253
257
|
"history_request",
|
|
254
258
|
"home_base_get",
|
|
255
259
|
"image_gen_model_set",
|
|
260
|
+
"ingress_config",
|
|
256
261
|
"integration_connect",
|
|
257
262
|
"integration_disconnect",
|
|
258
263
|
"integration_list",
|
|
@@ -301,6 +306,7 @@
|
|
|
301
306
|
"suggestion_request",
|
|
302
307
|
"task_submit",
|
|
303
308
|
"trust_rules_list",
|
|
309
|
+
"twilio_webhook_config",
|
|
304
310
|
"twitter_auth_start",
|
|
305
311
|
"twitter_auth_status",
|
|
306
312
|
"twitter_integration_config",
|
|
@@ -362,6 +368,7 @@
|
|
|
362
368
|
"get_signing_identity",
|
|
363
369
|
"history_response",
|
|
364
370
|
"home_base_get_response",
|
|
371
|
+
"ingress_config_response",
|
|
365
372
|
"integration_connect_result",
|
|
366
373
|
"integration_list_response",
|
|
367
374
|
"ipc_blob_probe_result",
|
|
@@ -412,6 +419,7 @@
|
|
|
412
419
|
"tool_use_start",
|
|
413
420
|
"trace_event",
|
|
414
421
|
"trust_rules_list_response",
|
|
422
|
+
"twilio_webhook_config_response",
|
|
415
423
|
"twitter_auth_result",
|
|
416
424
|
"twitter_auth_status_response",
|
|
417
425
|
"twitter_integration_config_response",
|