vellum 0.2.12 → 0.2.14
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/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +171 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +402 -5
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +271 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +28 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +96 -8
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +97 -0
- package/src/calls/elevenlabs-config.ts +31 -0
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +50 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +114 -0
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +26 -2
- package/src/config/schema.ts +178 -9
- package/src/config/types.ts +3 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/defaults.ts +11 -0
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/conversation-routes.ts +12 -5
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -2,12 +2,17 @@ import * as net from 'node:net';
|
|
|
2
2
|
import { getConfig, loadRawConfig, saveRawConfig } from '../../config/loader.js';
|
|
3
3
|
import { initializeProviders } from '../../providers/registry.js';
|
|
4
4
|
import { addRule, removeRule, updateRule, getAllRules, acceptStarterBundle } from '../../permissions/trust-store.js';
|
|
5
|
+
import { classifyRisk, check, generateAllowlistOptions, generateScopeOptions } from '../../permissions/checker.js';
|
|
6
|
+
import { isSideEffectTool } from '../../tools/executor.js';
|
|
7
|
+
import { resolveExecutionTarget } from '../../tools/execution-target.js';
|
|
8
|
+
import { getAllTools, getTool } from '../../tools/registry.js';
|
|
5
9
|
import { listSchedules, updateSchedule, deleteSchedule, describeCronExpression } from '../../schedule/schedule-store.js';
|
|
6
10
|
import { listReminders, cancelReminder } from '../../tools/reminder/reminder-store.js';
|
|
7
11
|
import { getSecureKey, setSecureKey, deleteSecureKey } from '../../security/secure-keys.js';
|
|
8
12
|
import { upsertCredentialMetadata, deleteCredentialMetadata, getCredentialMetadata } from '../../tools/credentials/metadata-store.js';
|
|
9
13
|
import { postToSlackWebhook } from '../../slack/slack-webhook.js';
|
|
10
14
|
import { getApp } from '../../memory/app-store.js';
|
|
15
|
+
import { readHttpToken } from '../../util/platform.js';
|
|
11
16
|
import type {
|
|
12
17
|
ModelSetRequest,
|
|
13
18
|
ImageGenModelSetRequest,
|
|
@@ -22,13 +27,52 @@ import type {
|
|
|
22
27
|
IngressConfigRequest,
|
|
23
28
|
VercelApiConfigRequest,
|
|
24
29
|
TwitterIntegrationConfigRequest,
|
|
30
|
+
TelegramConfigRequest,
|
|
31
|
+
ToolPermissionSimulateRequest,
|
|
25
32
|
} from '../ipc-protocol.js';
|
|
26
33
|
import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
|
|
27
34
|
import { MODEL_TO_PROVIDER } from '../session-slash.js';
|
|
28
35
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
36
|
+
// Lazily capture the env-provided INGRESS_PUBLIC_BASE_URL on first access
|
|
37
|
+
// rather than at module load time. The daemon loads ~/.vellum/.env inside
|
|
38
|
+
// runDaemon() (see lifecycle.ts), which runs AFTER static ES module imports
|
|
39
|
+
// resolve. A module-level snapshot would miss dotenv-provided values.
|
|
40
|
+
let _originalIngressEnvCaptured = false;
|
|
41
|
+
let _originalIngressEnv: string | undefined;
|
|
42
|
+
function getOriginalIngressEnv(): string | undefined {
|
|
43
|
+
if (!_originalIngressEnvCaptured) {
|
|
44
|
+
_originalIngressEnv = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
45
|
+
_originalIngressEnvCaptured = true;
|
|
46
|
+
}
|
|
47
|
+
return _originalIngressEnv;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const TELEGRAM_BOT_TOKEN_IN_URL_PATTERN = /\/bot\d{8,10}:[A-Za-z0-9_-]{30,120}\//g;
|
|
51
|
+
const TELEGRAM_BOT_TOKEN_PATTERN = /(?<![A-Za-z0-9_-])\d{8,10}:[A-Za-z0-9_-]{30,120}(?![A-Za-z0-9_-])/g;
|
|
52
|
+
|
|
53
|
+
function redactTelegramBotTokens(value: string): string {
|
|
54
|
+
return value
|
|
55
|
+
.replace(TELEGRAM_BOT_TOKEN_IN_URL_PATTERN, '/bot[REDACTED]/')
|
|
56
|
+
.replace(TELEGRAM_BOT_TOKEN_PATTERN, '[REDACTED]');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function summarizeTelegramError(err: unknown): string {
|
|
60
|
+
const parts: string[] = [];
|
|
61
|
+
if (err instanceof Error) {
|
|
62
|
+
parts.push(err.message);
|
|
63
|
+
} else {
|
|
64
|
+
parts.push(String(err));
|
|
65
|
+
}
|
|
66
|
+
const path = (err as { path?: unknown })?.path;
|
|
67
|
+
if (typeof path === 'string' && path.length > 0) {
|
|
68
|
+
parts.push(`path=${path}`);
|
|
69
|
+
}
|
|
70
|
+
const code = (err as { code?: unknown })?.code;
|
|
71
|
+
if (typeof code === 'string' && code.length > 0) {
|
|
72
|
+
parts.push(`code=${code}`);
|
|
73
|
+
}
|
|
74
|
+
return redactTelegramBotTokens(parts.join(' '));
|
|
75
|
+
}
|
|
32
76
|
|
|
33
77
|
export function handleModelGet(socket: net.Socket, ctx: HandlerContext): void {
|
|
34
78
|
const config = getConfig();
|
|
@@ -101,10 +145,7 @@ export function handleModelSet(
|
|
|
101
145
|
ctx.setSuppressConfigReload(wasSuppressed);
|
|
102
146
|
throw err;
|
|
103
147
|
}
|
|
104
|
-
|
|
105
|
-
if (existingSuppressTimer) clearTimeout(existingSuppressTimer);
|
|
106
|
-
const resetTimer = setTimeout(() => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
107
|
-
ctx.debounceTimers.set('__suppress_reset__', resetTimer);
|
|
148
|
+
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
108
149
|
|
|
109
150
|
// Re-initialize provider with the new model so LLM calls use it
|
|
110
151
|
const config = getConfig();
|
|
@@ -151,10 +192,7 @@ export function handleImageGenModelSet(
|
|
|
151
192
|
ctx.setSuppressConfigReload(wasSuppressed);
|
|
152
193
|
throw err;
|
|
153
194
|
}
|
|
154
|
-
|
|
155
|
-
if (existingSuppressTimer) clearTimeout(existingSuppressTimer);
|
|
156
|
-
const resetTimer = setTimeout(() => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
157
|
-
ctx.debounceTimers.set('__suppress_reset__', resetTimer);
|
|
195
|
+
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
158
196
|
|
|
159
197
|
ctx.updateConfigFingerprint();
|
|
160
198
|
log.info({ model: msg.model }, 'Image generation model updated');
|
|
@@ -170,7 +208,22 @@ export function handleAddTrustRule(
|
|
|
170
208
|
_ctx: HandlerContext,
|
|
171
209
|
): void {
|
|
172
210
|
try {
|
|
173
|
-
|
|
211
|
+
const hasMetadata = msg.allowHighRisk != null
|
|
212
|
+
|| msg.executionTarget != null;
|
|
213
|
+
|
|
214
|
+
addRule(
|
|
215
|
+
msg.toolName,
|
|
216
|
+
msg.pattern,
|
|
217
|
+
msg.scope,
|
|
218
|
+
msg.decision,
|
|
219
|
+
undefined, // priority — use default
|
|
220
|
+
hasMetadata
|
|
221
|
+
? {
|
|
222
|
+
allowHighRisk: msg.allowHighRisk,
|
|
223
|
+
executionTarget: msg.executionTarget,
|
|
224
|
+
}
|
|
225
|
+
: undefined,
|
|
226
|
+
);
|
|
174
227
|
log.info({ toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope, decision: msg.decision }, 'Trust rule added via client');
|
|
175
228
|
} catch (err) {
|
|
176
229
|
log.error({ err, toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope }, 'Failed to add trust rule via client');
|
|
@@ -403,27 +456,70 @@ export function handleSlackWebhookConfig(
|
|
|
403
456
|
}
|
|
404
457
|
}
|
|
405
458
|
|
|
406
|
-
function
|
|
459
|
+
function computeGatewayTarget(): string {
|
|
460
|
+
if (process.env.GATEWAY_INTERNAL_BASE_URL) {
|
|
461
|
+
return process.env.GATEWAY_INTERNAL_BASE_URL.replace(/\/+$/, '');
|
|
462
|
+
}
|
|
407
463
|
const portRaw = process.env.GATEWAY_PORT || '7830';
|
|
408
464
|
const port = Number(portRaw) || 7830;
|
|
409
465
|
return `http://127.0.0.1:${port}`;
|
|
410
466
|
}
|
|
411
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Best-effort call to the gateway's internal reconcile endpoint so that
|
|
470
|
+
* Telegram webhook registration is updated immediately when the ingress
|
|
471
|
+
* URL changes, without requiring a gateway restart.
|
|
472
|
+
*/
|
|
473
|
+
function triggerGatewayReconcile(ingressPublicBaseUrl: string | undefined): void {
|
|
474
|
+
const gatewayBase = computeGatewayTarget();
|
|
475
|
+
const token = readHttpToken();
|
|
476
|
+
if (!token) {
|
|
477
|
+
log.debug('Skipping gateway reconcile trigger: no HTTP bearer token available');
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const url = `${gatewayBase}/internal/telegram/reconcile`;
|
|
482
|
+
const body = JSON.stringify({ ingressPublicBaseUrl: ingressPublicBaseUrl ?? '' });
|
|
483
|
+
|
|
484
|
+
fetch(url, {
|
|
485
|
+
method: 'POST',
|
|
486
|
+
headers: {
|
|
487
|
+
'Content-Type': 'application/json',
|
|
488
|
+
'Authorization': `Bearer ${token}`,
|
|
489
|
+
},
|
|
490
|
+
body,
|
|
491
|
+
signal: AbortSignal.timeout(5_000),
|
|
492
|
+
}).then((res) => {
|
|
493
|
+
if (res.ok) {
|
|
494
|
+
log.info('Gateway Telegram webhook reconcile triggered successfully');
|
|
495
|
+
} else {
|
|
496
|
+
log.warn({ status: res.status }, 'Gateway Telegram webhook reconcile returned non-OK status');
|
|
497
|
+
}
|
|
498
|
+
}).catch((err) => {
|
|
499
|
+
log.debug({ err }, 'Gateway Telegram webhook reconcile failed (gateway may not be running)');
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
412
503
|
export function handleIngressConfig(
|
|
413
504
|
msg: IngressConfigRequest,
|
|
414
505
|
socket: net.Socket,
|
|
415
506
|
ctx: HandlerContext,
|
|
416
507
|
): void {
|
|
417
|
-
const localGatewayTarget =
|
|
508
|
+
const localGatewayTarget = computeGatewayTarget();
|
|
418
509
|
try {
|
|
419
510
|
if (msg.action === 'get') {
|
|
420
511
|
const raw = loadRawConfig();
|
|
421
512
|
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
422
513
|
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? '';
|
|
423
|
-
|
|
514
|
+
// Backward compatibility: if `enabled` was never explicitly set,
|
|
515
|
+
// infer from whether a publicBaseUrl is configured so existing users
|
|
516
|
+
// who predate the toggle aren't silently disabled.
|
|
517
|
+
const enabled = (ingress.enabled as boolean | undefined) ?? (publicBaseUrl ? true : false);
|
|
424
518
|
ctx.send(socket, { type: 'ingress_config_response', enabled, publicBaseUrl, localGatewayTarget, success: true });
|
|
425
519
|
} else if (msg.action === 'set') {
|
|
426
520
|
const value = (msg.publicBaseUrl ?? '').trim().replace(/\/+$/, '');
|
|
521
|
+
// Ensure we capture the original env value before any mutation below
|
|
522
|
+
getOriginalIngressEnv();
|
|
427
523
|
const raw = loadRawConfig();
|
|
428
524
|
|
|
429
525
|
// Update ingress.publicBaseUrl — this is the single source of truth for
|
|
@@ -445,10 +541,7 @@ export function handleIngressConfig(
|
|
|
445
541
|
ctx.setSuppressConfigReload(wasSuppressed);
|
|
446
542
|
throw err;
|
|
447
543
|
}
|
|
448
|
-
|
|
449
|
-
if (existingSuppressTimer) clearTimeout(existingSuppressTimer);
|
|
450
|
-
const resetTimer = setTimeout(() => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
451
|
-
ctx.debounceTimers.set('__suppress_reset__', resetTimer);
|
|
544
|
+
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
452
545
|
|
|
453
546
|
// Propagate to the gateway's process environment so it picks up the
|
|
454
547
|
// new URL when it is restarted. For the local-deployment path the
|
|
@@ -456,16 +549,32 @@ export function handleIngressConfig(
|
|
|
456
549
|
// so updating process.env here ensures the value is visible when the
|
|
457
550
|
// gateway is restarted (e.g. by the self-upgrade skill or a manual
|
|
458
551
|
// `pkill -f gateway`).
|
|
459
|
-
|
|
552
|
+
// Only export the URL when ingress is enabled; clearing it when
|
|
553
|
+
// disabled ensures the gateway stops accepting inbound webhooks.
|
|
554
|
+
const isEnabled = (ingress.enabled as boolean | undefined) ?? (value ? true : false);
|
|
555
|
+
if (value && isEnabled) {
|
|
460
556
|
process.env.INGRESS_PUBLIC_BASE_URL = value;
|
|
461
|
-
} else if (
|
|
462
|
-
|
|
557
|
+
} else if (isEnabled && getOriginalIngressEnv() !== undefined) {
|
|
558
|
+
// Ingress is enabled but the user cleared the URL — fall back to the
|
|
559
|
+
// env var that was present when the process started.
|
|
560
|
+
process.env.INGRESS_PUBLIC_BASE_URL = getOriginalIngressEnv()!;
|
|
463
561
|
} else {
|
|
562
|
+
// Ingress is disabled or no URL is configured and no startup env var
|
|
563
|
+
// exists — remove the env var so the gateway stops accepting webhooks.
|
|
464
564
|
delete process.env.INGRESS_PUBLIC_BASE_URL;
|
|
465
565
|
}
|
|
466
566
|
|
|
467
|
-
|
|
468
|
-
|
|
567
|
+
ctx.send(socket, { type: 'ingress_config_response', enabled: isEnabled, publicBaseUrl: value, localGatewayTarget, success: true });
|
|
568
|
+
|
|
569
|
+
// Trigger immediate Telegram webhook reconcile on the gateway so
|
|
570
|
+
// that changing the ingress URL takes effect without a restart.
|
|
571
|
+
// Called unconditionally so the gateway clears its in-memory URL
|
|
572
|
+
// when ingress is disabled, preventing stale re-registration on
|
|
573
|
+
// credential rotation.
|
|
574
|
+
// Use the effective URL from process.env (which accounts for the
|
|
575
|
+
// fallback branch above) rather than the raw `value` from the UI.
|
|
576
|
+
const effectiveUrl = isEnabled ? process.env.INGRESS_PUBLIC_BASE_URL : undefined;
|
|
577
|
+
triggerGatewayReconcile(effectiveUrl);
|
|
469
578
|
} else {
|
|
470
579
|
ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
471
580
|
}
|
|
@@ -546,6 +655,8 @@ export function handleTwitterIntegrationConfig(
|
|
|
546
655
|
if (msg.action === 'get') {
|
|
547
656
|
const raw = loadRawConfig();
|
|
548
657
|
const mode = (raw.twitterIntegrationMode as 'local_byo' | 'managed' | undefined) ?? 'local_byo';
|
|
658
|
+
const strategy = (raw.twitterOperationStrategy as 'oauth' | 'browser' | 'auto' | undefined) ?? 'auto';
|
|
659
|
+
const strategyConfigured = Object.prototype.hasOwnProperty.call(raw, 'twitterOperationStrategy');
|
|
549
660
|
const localClientConfigured = !!getSecureKey('credential:integration:twitter:oauth_client_id');
|
|
550
661
|
const connected = !!getSecureKey('credential:integration:twitter:access_token');
|
|
551
662
|
const meta = getCredentialMetadata('integration:twitter', 'access_token');
|
|
@@ -557,6 +668,47 @@ export function handleTwitterIntegrationConfig(
|
|
|
557
668
|
localClientConfigured,
|
|
558
669
|
connected,
|
|
559
670
|
accountInfo: meta?.accountInfo ?? undefined,
|
|
671
|
+
strategy,
|
|
672
|
+
strategyConfigured,
|
|
673
|
+
});
|
|
674
|
+
} else if (msg.action === 'get_strategy') {
|
|
675
|
+
const raw = loadRawConfig();
|
|
676
|
+
const strategy = (raw.twitterOperationStrategy as 'oauth' | 'browser' | 'auto' | undefined) ?? 'auto';
|
|
677
|
+
const strategyConfigured = Object.prototype.hasOwnProperty.call(raw, 'twitterOperationStrategy');
|
|
678
|
+
ctx.send(socket, {
|
|
679
|
+
type: 'twitter_integration_config_response',
|
|
680
|
+
success: true,
|
|
681
|
+
managedAvailable: false,
|
|
682
|
+
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
683
|
+
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
684
|
+
strategy,
|
|
685
|
+
strategyConfigured,
|
|
686
|
+
});
|
|
687
|
+
} else if (msg.action === 'set_strategy') {
|
|
688
|
+
const valid = ['oauth', 'browser', 'auto'];
|
|
689
|
+
const value = msg.strategy;
|
|
690
|
+
if (!value || !valid.includes(value)) {
|
|
691
|
+
ctx.send(socket, {
|
|
692
|
+
type: 'twitter_integration_config_response',
|
|
693
|
+
success: false,
|
|
694
|
+
managedAvailable: false,
|
|
695
|
+
localClientConfigured: false,
|
|
696
|
+
connected: false,
|
|
697
|
+
error: `Invalid strategy value: ${String(value)}. Must be one of: ${valid.join(', ')}`,
|
|
698
|
+
});
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const raw = loadRawConfig();
|
|
702
|
+
raw.twitterOperationStrategy = value;
|
|
703
|
+
saveRawConfig(raw);
|
|
704
|
+
ctx.send(socket, {
|
|
705
|
+
type: 'twitter_integration_config_response',
|
|
706
|
+
success: true,
|
|
707
|
+
managedAvailable: false,
|
|
708
|
+
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
709
|
+
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
710
|
+
strategy: value as 'oauth' | 'browser' | 'auto',
|
|
711
|
+
strategyConfigured: true,
|
|
560
712
|
});
|
|
561
713
|
} else if (msg.action === 'set_mode') {
|
|
562
714
|
const raw = loadRawConfig();
|
|
@@ -676,6 +828,267 @@ export function handleTwitterIntegrationConfig(
|
|
|
676
828
|
}
|
|
677
829
|
}
|
|
678
830
|
|
|
831
|
+
export async function handleTelegramConfig(
|
|
832
|
+
msg: TelegramConfigRequest,
|
|
833
|
+
socket: net.Socket,
|
|
834
|
+
ctx: HandlerContext,
|
|
835
|
+
): Promise<void> {
|
|
836
|
+
try {
|
|
837
|
+
if (msg.action === 'get') {
|
|
838
|
+
const hasBotToken = !!getSecureKey('credential:telegram:bot_token');
|
|
839
|
+
const hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
840
|
+
const meta = getCredentialMetadata('telegram', 'bot_token');
|
|
841
|
+
const botUsername = meta?.accountInfo ?? undefined;
|
|
842
|
+
ctx.send(socket, {
|
|
843
|
+
type: 'telegram_config_response',
|
|
844
|
+
success: true,
|
|
845
|
+
hasBotToken,
|
|
846
|
+
botUsername,
|
|
847
|
+
connected: hasBotToken && hasWebhookSecret,
|
|
848
|
+
hasWebhookSecret,
|
|
849
|
+
});
|
|
850
|
+
} else if (msg.action === 'set') {
|
|
851
|
+
// Resolve token: prefer explicit msg.botToken, fall back to secure storage.
|
|
852
|
+
// Track provenance so we only rollback tokens that were freshly provided.
|
|
853
|
+
const isNewToken = !!msg.botToken;
|
|
854
|
+
const botToken = msg.botToken || getSecureKey('credential:telegram:bot_token');
|
|
855
|
+
if (!botToken) {
|
|
856
|
+
ctx.send(socket, {
|
|
857
|
+
type: 'telegram_config_response',
|
|
858
|
+
success: false,
|
|
859
|
+
hasBotToken: false,
|
|
860
|
+
connected: false,
|
|
861
|
+
hasWebhookSecret: false,
|
|
862
|
+
error: 'botToken is required for set action',
|
|
863
|
+
});
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Validate token via Telegram getMe API
|
|
868
|
+
let botUsername: string;
|
|
869
|
+
try {
|
|
870
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
871
|
+
if (!res.ok) {
|
|
872
|
+
const body = await res.text();
|
|
873
|
+
ctx.send(socket, {
|
|
874
|
+
type: 'telegram_config_response',
|
|
875
|
+
success: false,
|
|
876
|
+
hasBotToken: false,
|
|
877
|
+
connected: false,
|
|
878
|
+
hasWebhookSecret: false,
|
|
879
|
+
error: `Telegram API validation failed: ${body}`,
|
|
880
|
+
});
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const data = await res.json() as { ok: boolean; result?: { username?: string } };
|
|
884
|
+
if (!data.ok || !data.result?.username) {
|
|
885
|
+
ctx.send(socket, {
|
|
886
|
+
type: 'telegram_config_response',
|
|
887
|
+
success: false,
|
|
888
|
+
hasBotToken: false,
|
|
889
|
+
connected: false,
|
|
890
|
+
hasWebhookSecret: false,
|
|
891
|
+
error: 'Telegram API returned unexpected response',
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
botUsername = data.result.username;
|
|
896
|
+
} catch (err) {
|
|
897
|
+
const message = summarizeTelegramError(err);
|
|
898
|
+
ctx.send(socket, {
|
|
899
|
+
type: 'telegram_config_response',
|
|
900
|
+
success: false,
|
|
901
|
+
hasBotToken: false,
|
|
902
|
+
connected: false,
|
|
903
|
+
hasWebhookSecret: false,
|
|
904
|
+
error: `Failed to validate bot token: ${message}`,
|
|
905
|
+
});
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Store bot token securely
|
|
910
|
+
const stored = setSecureKey('credential:telegram:bot_token', botToken);
|
|
911
|
+
if (!stored) {
|
|
912
|
+
ctx.send(socket, {
|
|
913
|
+
type: 'telegram_config_response',
|
|
914
|
+
success: false,
|
|
915
|
+
hasBotToken: false,
|
|
916
|
+
connected: false,
|
|
917
|
+
hasWebhookSecret: false,
|
|
918
|
+
error: 'Failed to store bot token in secure storage',
|
|
919
|
+
});
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Store metadata with bot username
|
|
924
|
+
upsertCredentialMetadata('telegram', 'bot_token', {
|
|
925
|
+
accountInfo: botUsername,
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// Ensure webhook secret exists (generate if missing)
|
|
929
|
+
let hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
930
|
+
if (!hasWebhookSecret) {
|
|
931
|
+
const { randomUUID } = await import('node:crypto');
|
|
932
|
+
const webhookSecret = randomUUID();
|
|
933
|
+
const secretStored = setSecureKey('credential:telegram:webhook_secret', webhookSecret);
|
|
934
|
+
if (secretStored) {
|
|
935
|
+
upsertCredentialMetadata('telegram', 'webhook_secret', {});
|
|
936
|
+
hasWebhookSecret = true;
|
|
937
|
+
} else {
|
|
938
|
+
// Only roll back the bot token if it was freshly provided.
|
|
939
|
+
// When the token came from secure storage it was already valid
|
|
940
|
+
// configuration; deleting it would destroy working state.
|
|
941
|
+
if (isNewToken) {
|
|
942
|
+
deleteSecureKey('credential:telegram:bot_token');
|
|
943
|
+
deleteCredentialMetadata('telegram', 'bot_token');
|
|
944
|
+
}
|
|
945
|
+
ctx.send(socket, {
|
|
946
|
+
type: 'telegram_config_response',
|
|
947
|
+
success: false,
|
|
948
|
+
hasBotToken: !isNewToken,
|
|
949
|
+
connected: false,
|
|
950
|
+
hasWebhookSecret: false,
|
|
951
|
+
error: 'Failed to store webhook secret',
|
|
952
|
+
});
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
// Self-heal: ensure metadata exists even when the secret was
|
|
957
|
+
// already present (covers previously lost/corrupted metadata).
|
|
958
|
+
upsertCredentialMetadata('telegram', 'webhook_secret', {});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
ctx.send(socket, {
|
|
962
|
+
type: 'telegram_config_response',
|
|
963
|
+
success: true,
|
|
964
|
+
hasBotToken: true,
|
|
965
|
+
botUsername,
|
|
966
|
+
connected: true,
|
|
967
|
+
hasWebhookSecret,
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// Trigger gateway reconcile so the webhook registration updates immediately
|
|
971
|
+
const effectiveUrl = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
972
|
+
if (effectiveUrl) {
|
|
973
|
+
triggerGatewayReconcile(effectiveUrl);
|
|
974
|
+
}
|
|
975
|
+
} else if (msg.action === 'clear') {
|
|
976
|
+
// Deregister the Telegram webhook before deleting credentials.
|
|
977
|
+
// The gateway reconcile short-circuits when credentials are absent,
|
|
978
|
+
// so we must call the Telegram API directly while the token is still
|
|
979
|
+
// available.
|
|
980
|
+
const botToken = getSecureKey('credential:telegram:bot_token');
|
|
981
|
+
if (botToken) {
|
|
982
|
+
try {
|
|
983
|
+
await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`);
|
|
984
|
+
} catch (err) {
|
|
985
|
+
log.warn(
|
|
986
|
+
{ error: summarizeTelegramError(err) },
|
|
987
|
+
'Failed to deregister Telegram webhook (proceeding with credential cleanup)',
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
deleteSecureKey('credential:telegram:bot_token');
|
|
993
|
+
deleteCredentialMetadata('telegram', 'bot_token');
|
|
994
|
+
deleteSecureKey('credential:telegram:webhook_secret');
|
|
995
|
+
deleteCredentialMetadata('telegram', 'webhook_secret');
|
|
996
|
+
|
|
997
|
+
ctx.send(socket, {
|
|
998
|
+
type: 'telegram_config_response',
|
|
999
|
+
success: true,
|
|
1000
|
+
hasBotToken: false,
|
|
1001
|
+
connected: false,
|
|
1002
|
+
hasWebhookSecret: false,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Trigger reconcile to deregister webhook
|
|
1006
|
+
const effectiveUrl = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
1007
|
+
if (effectiveUrl) {
|
|
1008
|
+
triggerGatewayReconcile(effectiveUrl);
|
|
1009
|
+
}
|
|
1010
|
+
} else if (msg.action === 'set_commands') {
|
|
1011
|
+
const storedToken = getSecureKey('credential:telegram:bot_token');
|
|
1012
|
+
if (!storedToken) {
|
|
1013
|
+
ctx.send(socket, {
|
|
1014
|
+
type: 'telegram_config_response',
|
|
1015
|
+
success: false,
|
|
1016
|
+
hasBotToken: false,
|
|
1017
|
+
connected: false,
|
|
1018
|
+
hasWebhookSecret: false,
|
|
1019
|
+
error: 'Bot token not configured. Run set action first.',
|
|
1020
|
+
});
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const commands = msg.commands ?? [
|
|
1025
|
+
{ command: 'new', description: 'Start a new conversation' },
|
|
1026
|
+
];
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
const res = await fetch(`https://api.telegram.org/bot${storedToken}/setMyCommands`, {
|
|
1030
|
+
method: 'POST',
|
|
1031
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1032
|
+
body: JSON.stringify({ commands }),
|
|
1033
|
+
});
|
|
1034
|
+
if (!res.ok) {
|
|
1035
|
+
const body = await res.text();
|
|
1036
|
+
ctx.send(socket, {
|
|
1037
|
+
type: 'telegram_config_response',
|
|
1038
|
+
success: false,
|
|
1039
|
+
hasBotToken: true,
|
|
1040
|
+
connected: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1041
|
+
hasWebhookSecret: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1042
|
+
error: `Failed to set bot commands: ${body}`,
|
|
1043
|
+
});
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
const message = summarizeTelegramError(err);
|
|
1048
|
+
ctx.send(socket, {
|
|
1049
|
+
type: 'telegram_config_response',
|
|
1050
|
+
success: false,
|
|
1051
|
+
hasBotToken: true,
|
|
1052
|
+
connected: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1053
|
+
hasWebhookSecret: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1054
|
+
error: `Failed to set bot commands: ${message}`,
|
|
1055
|
+
});
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const hasBotToken = !!getSecureKey('credential:telegram:bot_token');
|
|
1060
|
+
const hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
1061
|
+
ctx.send(socket, {
|
|
1062
|
+
type: 'telegram_config_response',
|
|
1063
|
+
success: true,
|
|
1064
|
+
hasBotToken,
|
|
1065
|
+
connected: hasBotToken && hasWebhookSecret,
|
|
1066
|
+
hasWebhookSecret,
|
|
1067
|
+
});
|
|
1068
|
+
} else {
|
|
1069
|
+
ctx.send(socket, {
|
|
1070
|
+
type: 'telegram_config_response',
|
|
1071
|
+
success: false,
|
|
1072
|
+
hasBotToken: false,
|
|
1073
|
+
connected: false,
|
|
1074
|
+
hasWebhookSecret: false,
|
|
1075
|
+
error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1080
|
+
log.error({ err }, 'Failed to handle Telegram config');
|
|
1081
|
+
ctx.send(socket, {
|
|
1082
|
+
type: 'telegram_config_response',
|
|
1083
|
+
success: false,
|
|
1084
|
+
hasBotToken: false,
|
|
1085
|
+
connected: false,
|
|
1086
|
+
hasWebhookSecret: false,
|
|
1087
|
+
error: message,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
679
1092
|
export function handleEnvVarsRequest(socket: net.Socket, ctx: HandlerContext): void {
|
|
680
1093
|
const vars: Record<string, string> = {};
|
|
681
1094
|
for (const [key, value] of Object.entries(process.env)) {
|
|
@@ -684,6 +1097,109 @@ export function handleEnvVarsRequest(socket: net.Socket, ctx: HandlerContext): v
|
|
|
684
1097
|
ctx.send(socket, { type: 'env_vars_response', vars });
|
|
685
1098
|
}
|
|
686
1099
|
|
|
1100
|
+
export async function handleToolPermissionSimulate(
|
|
1101
|
+
msg: ToolPermissionSimulateRequest,
|
|
1102
|
+
socket: net.Socket,
|
|
1103
|
+
ctx: HandlerContext,
|
|
1104
|
+
): Promise<void> {
|
|
1105
|
+
try {
|
|
1106
|
+
if (!msg.toolName || typeof msg.toolName !== 'string') {
|
|
1107
|
+
ctx.send(socket, {
|
|
1108
|
+
type: 'tool_permission_simulate_response',
|
|
1109
|
+
success: false,
|
|
1110
|
+
error: 'toolName is required',
|
|
1111
|
+
});
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (!msg.input || typeof msg.input !== 'object') {
|
|
1115
|
+
ctx.send(socket, {
|
|
1116
|
+
type: 'tool_permission_simulate_response',
|
|
1117
|
+
success: false,
|
|
1118
|
+
error: 'input is required and must be an object',
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const workingDir = msg.workingDir ?? process.cwd();
|
|
1124
|
+
|
|
1125
|
+
// Only infer execution target when the tool is actually registered;
|
|
1126
|
+
// for unresolved tools, leave it undefined so trust rules are unscoped.
|
|
1127
|
+
const isRegistered = getTool(msg.toolName) !== undefined;
|
|
1128
|
+
const executionTarget = isRegistered ? resolveExecutionTarget(msg.toolName) : undefined;
|
|
1129
|
+
const policyContext = executionTarget ? { executionTarget } : undefined;
|
|
1130
|
+
|
|
1131
|
+
const riskLevel = await classifyRisk(msg.toolName, msg.input, workingDir);
|
|
1132
|
+
const result = await check(msg.toolName, msg.input, workingDir, policyContext);
|
|
1133
|
+
|
|
1134
|
+
// Private-thread override: promote allow → prompt for side-effect tools
|
|
1135
|
+
if (
|
|
1136
|
+
msg.forcePromptSideEffects
|
|
1137
|
+
&& result.decision === 'allow'
|
|
1138
|
+
&& isSideEffectTool(msg.toolName, msg.input)
|
|
1139
|
+
) {
|
|
1140
|
+
result.decision = 'prompt';
|
|
1141
|
+
result.reason = 'Private thread: side-effect tools require explicit approval';
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Non-interactive override: convert prompt → deny
|
|
1145
|
+
if (msg.isInteractive === false && result.decision === 'prompt') {
|
|
1146
|
+
result.decision = 'deny';
|
|
1147
|
+
result.reason = 'Non-interactive session: no client to approve prompt';
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// When decision is prompt, generate the full payload the UI needs
|
|
1151
|
+
let promptPayload: {
|
|
1152
|
+
allowlistOptions: Array<{ label: string; description: string; pattern: string }>;
|
|
1153
|
+
scopeOptions: Array<{ label: string; scope: string }>;
|
|
1154
|
+
persistentDecisionsAllowed: boolean;
|
|
1155
|
+
} | undefined;
|
|
1156
|
+
|
|
1157
|
+
if (result.decision === 'prompt') {
|
|
1158
|
+
const allowlistOptions = await generateAllowlistOptions(msg.toolName, msg.input);
|
|
1159
|
+
const scopeOptions = generateScopeOptions(workingDir, msg.toolName);
|
|
1160
|
+
const persistentDecisionsAllowed = !(
|
|
1161
|
+
msg.toolName === 'bash'
|
|
1162
|
+
&& msg.input.network_mode === 'proxied'
|
|
1163
|
+
);
|
|
1164
|
+
promptPayload = { allowlistOptions, scopeOptions, persistentDecisionsAllowed };
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
ctx.send(socket, {
|
|
1168
|
+
type: 'tool_permission_simulate_response',
|
|
1169
|
+
success: true,
|
|
1170
|
+
decision: result.decision,
|
|
1171
|
+
riskLevel,
|
|
1172
|
+
reason: result.reason,
|
|
1173
|
+
executionTarget,
|
|
1174
|
+
matchedRuleId: result.matchedRule?.id,
|
|
1175
|
+
promptPayload,
|
|
1176
|
+
});
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1179
|
+
log.error({ err }, 'Failed to simulate tool permission');
|
|
1180
|
+
ctx.send(socket, {
|
|
1181
|
+
type: 'tool_permission_simulate_response',
|
|
1182
|
+
success: false,
|
|
1183
|
+
error: message,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
export function handleToolNamesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
1189
|
+
const tools = getAllTools();
|
|
1190
|
+
const names = tools.map((t) => t.name).sort((a, b) => a.localeCompare(b));
|
|
1191
|
+
const schemas: Record<string, import('../ipc-contract.js').ToolInputSchema> = {};
|
|
1192
|
+
for (const tool of tools) {
|
|
1193
|
+
try {
|
|
1194
|
+
const def = tool.getDefinition();
|
|
1195
|
+
schemas[tool.name] = def.input_schema as import('../ipc-contract.js').ToolInputSchema;
|
|
1196
|
+
} catch {
|
|
1197
|
+
// Skip tools whose definitions can't be resolved
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
ctx.send(socket, { type: 'tool_names_list_response', names, schemas });
|
|
1201
|
+
}
|
|
1202
|
+
|
|
687
1203
|
export const configHandlers = defineHandlers({
|
|
688
1204
|
model_get: (_msg, socket, ctx) => handleModelGet(socket, ctx),
|
|
689
1205
|
model_set: handleModelSet,
|
|
@@ -703,5 +1219,8 @@ export const configHandlers = defineHandlers({
|
|
|
703
1219
|
ingress_config: handleIngressConfig,
|
|
704
1220
|
vercel_api_config: handleVercelApiConfig,
|
|
705
1221
|
twitter_integration_config: handleTwitterIntegrationConfig,
|
|
1222
|
+
telegram_config: handleTelegramConfig,
|
|
706
1223
|
env_vars_request: (_msg, socket, ctx) => handleEnvVarsRequest(socket, ctx),
|
|
1224
|
+
tool_permission_simulate: handleToolPermissionSimulate,
|
|
1225
|
+
tool_names_list: (_msg, socket, ctx) => handleToolNamesList(socket, ctx),
|
|
707
1226
|
});
|