vellum 0.2.7 → 0.2.9
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 +4 -4
- package/package.json +4 -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 +0 -6
- package/src/__tests__/forbidden-legacy-symbols.test.ts +69 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +538 -0
- package/src/__tests__/ingress-url-consistency.test.ts +214 -0
- package/src/__tests__/ipc-snapshot.test.ts +17 -5
- package/src/__tests__/oauth-callback-registry.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +304 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +51 -12
- package/src/__tests__/public-ingress-urls.test.ts +222 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +343 -0
- package/src/__tests__/runtime-events-sse.test.ts +162 -0
- package/src/__tests__/tool-executor.test.ts +88 -0
- package/src/__tests__/turn-commit.test.ts +64 -0
- package/src/__tests__/twilio-provider.test.ts +1 -1
- package/src/__tests__/twilio-routes.test.ts +4 -4
- package/src/__tests__/twitter-auth-handler.test.ts +87 -2
- package/src/calls/call-domain.ts +8 -6
- package/src/calls/twilio-config.ts +18 -3
- package/src/calls/twilio-routes.ts +10 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +25 -0
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +9 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +25 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +32 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +370 -0
- package/src/config/defaults.ts +4 -1
- package/src/config/schema.ts +30 -6
- package/src/config/system-prompt.ts +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +5 -4
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -2
- package/src/config/vellum-skills/telegram-setup/SKILL.md +3 -3
- package/src/daemon/computer-use-session.ts +2 -1
- package/src/daemon/handlers/config.ts +49 -17
- package/src/daemon/handlers/sessions.ts +2 -2
- package/src/daemon/handlers/shared.ts +1 -0
- package/src/daemon/handlers/subagents.ts +85 -2
- package/src/daemon/handlers/twitter-auth.ts +31 -2
- package/src/daemon/handlers/work-items.ts +1 -1
- package/src/daemon/ipc-contract-inventory.json +8 -4
- package/src/daemon/ipc-contract.ts +34 -15
- package/src/daemon/lifecycle.ts +9 -4
- package/src/daemon/server.ts +7 -0
- package/src/daemon/session-tool-setup.ts +8 -1
- package/src/inbound/public-ingress-urls.ts +112 -0
- package/src/memory/attachments-store.ts +0 -1
- package/src/memory/channel-delivery-store.ts +0 -1
- package/src/memory/conversation-key-store.ts +0 -1
- package/src/memory/db.ts +472 -148
- package/src/memory/llm-usage-store.ts +0 -1
- package/src/memory/runs-store.ts +51 -6
- package/src/memory/schema.ts +2 -6
- package/src/runtime/gateway-client.ts +7 -1
- package/src/runtime/http-server.ts +174 -7
- package/src/runtime/routes/channel-routes.ts +7 -2
- package/src/runtime/routes/events-routes.ts +79 -0
- package/src/runtime/routes/run-routes.ts +43 -0
- package/src/runtime/run-orchestrator.ts +64 -7
- package/src/security/oauth-callback-registry.ts +66 -0
- package/src/security/oauth2.ts +208 -58
- package/src/subagent/manager.ts +3 -1
- package/src/swarm/backend-claude-code.ts +1 -1
- package/src/tools/assets/search.ts +1 -36
- package/src/tools/claude-code/claude-code.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +16 -2
- package/src/tools/tasks/work-item-run.ts +78 -0
- package/src/util/platform.ts +1 -1
- package/src/work-items/work-item-runner.ts +171 -0
- package/src/workspace/provider-commit-message-generator.ts +39 -23
- package/src/workspace/turn-commit.ts +6 -2
- package/src/__tests__/handlers-twilio-config.test.ts +0 -221
- package/src/calls/__tests__/twilio-webhook-urls.test.ts +0 -162
- package/src/calls/twilio-webhook-urls.ts +0 -50
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
ReminderCancel,
|
|
20
20
|
ShareToSlackRequest,
|
|
21
21
|
SlackWebhookConfigRequest,
|
|
22
|
-
|
|
22
|
+
IngressConfigRequest,
|
|
23
23
|
VercelApiConfigRequest,
|
|
24
24
|
TwitterIntegrationConfigRequest,
|
|
25
25
|
} from '../ipc-protocol.js';
|
|
@@ -89,11 +89,12 @@ export function handleModelSet(
|
|
|
89
89
|
// Suppress the file watcher callback — handleModelSet already does
|
|
90
90
|
// the full reload sequence; a redundant watcher-triggered reload
|
|
91
91
|
// would incorrectly evict sessions created after this method returns.
|
|
92
|
+
const wasSuppressed = ctx.suppressConfigReload;
|
|
92
93
|
ctx.setSuppressConfigReload(true);
|
|
93
94
|
try {
|
|
94
95
|
saveRawConfig(raw);
|
|
95
96
|
} catch (err) {
|
|
96
|
-
ctx.setSuppressConfigReload(
|
|
97
|
+
ctx.setSuppressConfigReload(wasSuppressed);
|
|
97
98
|
throw err;
|
|
98
99
|
}
|
|
99
100
|
const existingSuppressTimer = ctx.debounceTimers.get('__suppress_reset__');
|
|
@@ -138,11 +139,12 @@ export function handleImageGenModelSet(
|
|
|
138
139
|
const raw = loadRawConfig();
|
|
139
140
|
raw.imageGenModel = msg.model;
|
|
140
141
|
|
|
142
|
+
const wasSuppressed = ctx.suppressConfigReload;
|
|
141
143
|
ctx.setSuppressConfigReload(true);
|
|
142
144
|
try {
|
|
143
145
|
saveRawConfig(raw);
|
|
144
146
|
} catch (err) {
|
|
145
|
-
ctx.setSuppressConfigReload(
|
|
147
|
+
ctx.setSuppressConfigReload(wasSuppressed);
|
|
146
148
|
throw err;
|
|
147
149
|
}
|
|
148
150
|
const existingSuppressTimer = ctx.debounceTimers.get('__suppress_reset__');
|
|
@@ -165,9 +167,9 @@ export function handleAddTrustRule(
|
|
|
165
167
|
): void {
|
|
166
168
|
try {
|
|
167
169
|
addRule(msg.toolName, msg.pattern, msg.scope, msg.decision);
|
|
168
|
-
log.info({
|
|
170
|
+
log.info({ toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope, decision: msg.decision }, 'Trust rule added via client');
|
|
169
171
|
} catch (err) {
|
|
170
|
-
log.error({ err }, 'Failed to add trust rule');
|
|
172
|
+
log.error({ err, toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope }, 'Failed to add trust rule via client');
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
|
|
@@ -397,25 +399,40 @@ export function handleSlackWebhookConfig(
|
|
|
397
399
|
}
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
+
function computeLocalGatewayTarget(): string {
|
|
403
|
+
const portRaw = process.env.GATEWAY_PORT || '7830';
|
|
404
|
+
const port = Number(portRaw) || 7830;
|
|
405
|
+
return `http://127.0.0.1:${port}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function handleIngressConfig(
|
|
409
|
+
msg: IngressConfigRequest,
|
|
402
410
|
socket: net.Socket,
|
|
403
411
|
ctx: HandlerContext,
|
|
404
412
|
): void {
|
|
413
|
+
const localGatewayTarget = computeLocalGatewayTarget();
|
|
405
414
|
try {
|
|
406
415
|
if (msg.action === 'get') {
|
|
407
416
|
const raw = loadRawConfig();
|
|
408
|
-
const
|
|
409
|
-
|
|
417
|
+
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
418
|
+
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? '';
|
|
419
|
+
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl, localGatewayTarget, success: true });
|
|
410
420
|
} else if (msg.action === 'set') {
|
|
411
|
-
const value = (msg.
|
|
421
|
+
const value = (msg.publicBaseUrl ?? '').trim().replace(/\/+$/, '');
|
|
412
422
|
const raw = loadRawConfig();
|
|
413
|
-
|
|
414
|
-
|
|
423
|
+
|
|
424
|
+
// Update ingress.publicBaseUrl — this is the single source of truth for
|
|
425
|
+
// the canonical public ingress URL. The gateway receives this value via
|
|
426
|
+
// the INGRESS_PUBLIC_BASE_URL env var at spawn time (see hatch.ts).
|
|
427
|
+
// A gateway restart is required for the new value to take effect in
|
|
428
|
+
// inbound Twilio signature validation.
|
|
429
|
+
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
430
|
+
ingress.publicBaseUrl = value || undefined;
|
|
431
|
+
|
|
415
432
|
const wasSuppressed = ctx.suppressConfigReload;
|
|
416
433
|
ctx.setSuppressConfigReload(true);
|
|
417
434
|
try {
|
|
418
|
-
saveRawConfig({ ...raw,
|
|
435
|
+
saveRawConfig({ ...raw, ingress });
|
|
419
436
|
} catch (err) {
|
|
420
437
|
ctx.setSuppressConfigReload(wasSuppressed);
|
|
421
438
|
throw err;
|
|
@@ -424,11 +441,26 @@ export function handleTwilioWebhookConfig(
|
|
|
424
441
|
if (existingSuppressTimer) clearTimeout(existingSuppressTimer);
|
|
425
442
|
const resetTimer = setTimeout(() => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
426
443
|
ctx.debounceTimers.set('__suppress_reset__', resetTimer);
|
|
427
|
-
|
|
444
|
+
|
|
445
|
+
// Propagate to the gateway's process environment so it picks up the
|
|
446
|
+
// new URL on its next config load. For the local-deployment path the
|
|
447
|
+
// gateway runs as a child process that inherited the assistant's env,
|
|
448
|
+
// so updating process.env here ensures the value is visible when the
|
|
449
|
+
// gateway is restarted (e.g. by the self-upgrade skill or a manual
|
|
450
|
+
// `pkill -f gateway`).
|
|
451
|
+
if (value) {
|
|
452
|
+
process.env.INGRESS_PUBLIC_BASE_URL = value;
|
|
453
|
+
}
|
|
454
|
+
// When cleared, do NOT delete the env var — the original env-provided
|
|
455
|
+
// value (if any) serves as a fallback per the documented precedence model.
|
|
456
|
+
|
|
457
|
+
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl: value, localGatewayTarget, success: true });
|
|
458
|
+
} else {
|
|
459
|
+
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
428
460
|
}
|
|
429
461
|
} catch (err) {
|
|
430
462
|
const message = err instanceof Error ? err.message : String(err);
|
|
431
|
-
ctx.send(socket, { type: '
|
|
463
|
+
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl: '', localGatewayTarget, success: false, error: message });
|
|
432
464
|
}
|
|
433
465
|
}
|
|
434
466
|
|
|
@@ -565,7 +597,7 @@ export function handleTwitterIntegrationConfig(
|
|
|
565
597
|
type: 'twitter_integration_config_response',
|
|
566
598
|
success: false,
|
|
567
599
|
managedAvailable: false,
|
|
568
|
-
localClientConfigured:
|
|
600
|
+
localClientConfigured: !!previousClientId,
|
|
569
601
|
connected: false,
|
|
570
602
|
error: 'Failed to store client secret in secure storage',
|
|
571
603
|
});
|
|
@@ -657,7 +689,7 @@ export const configHandlers = defineHandlers({
|
|
|
657
689
|
reminder_cancel: handleReminderCancel,
|
|
658
690
|
share_to_slack: handleShareToSlack,
|
|
659
691
|
slack_webhook_config: handleSlackWebhookConfig,
|
|
660
|
-
|
|
692
|
+
ingress_config: handleIngressConfig,
|
|
661
693
|
vercel_api_config: handleVercelApiConfig,
|
|
662
694
|
twitter_integration_config: handleTwitterIntegrationConfig,
|
|
663
695
|
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
|
);
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as net from 'node:net';
|
|
6
|
-
import type { SubagentAbortRequest, SubagentStatusRequest, SubagentMessageRequest } from '../ipc-protocol.js';
|
|
6
|
+
import type { SubagentAbortRequest, SubagentStatusRequest, SubagentMessageRequest, SubagentDetailRequest } from '../ipc-protocol.js';
|
|
7
|
+
import * as conversationStore from '../../memory/conversation-store.js';
|
|
7
8
|
import type { HandlerContext } from './shared.js';
|
|
8
9
|
import { getSubagentManager } from '../../subagent/index.js';
|
|
9
|
-
import { log, defineHandlers } from './shared.js';
|
|
10
|
+
import { log, defineHandlers, isRecord } from './shared.js';
|
|
10
11
|
|
|
11
12
|
export function handleSubagentAbort(
|
|
12
13
|
msg: SubagentAbortRequest,
|
|
@@ -120,8 +121,90 @@ export function handleSubagentMessage(
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
export function handleSubagentDetailRequest(
|
|
125
|
+
msg: SubagentDetailRequest,
|
|
126
|
+
socket: net.Socket,
|
|
127
|
+
ctx: HandlerContext,
|
|
128
|
+
): void {
|
|
129
|
+
// Ownership check: reject if the socket has no bound session.
|
|
130
|
+
const callerSessionId = ctx.socketToSession.get(socket);
|
|
131
|
+
if (!callerSessionId) {
|
|
132
|
+
log.warn({ subagentId: msg.subagentId }, 'Detail request rejected: socket has no bound session');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// If the subagent is still in memory, verify the caller owns it.
|
|
137
|
+
// After daemon restart getState() returns null — we allow the request
|
|
138
|
+
// since the conversationId itself acts as a capability token (the client
|
|
139
|
+
// only knows it because it was sent in a prior subagent_notification).
|
|
140
|
+
const manager = getSubagentManager();
|
|
141
|
+
const state = manager.getState(msg.subagentId);
|
|
142
|
+
if (state && state.config.parentSessionId !== callerSessionId) {
|
|
143
|
+
log.warn({ subagentId: msg.subagentId, callerSessionId }, 'Detail request rejected: subagent not owned by caller');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const subagentMsgs = conversationStore.getMessages(msg.conversationId);
|
|
148
|
+
|
|
149
|
+
// Extract objective from the first user message
|
|
150
|
+
let objective: string | undefined;
|
|
151
|
+
const firstUser = subagentMsgs.find(m => m.role === 'user');
|
|
152
|
+
if (firstUser) {
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(firstUser.content);
|
|
155
|
+
if (Array.isArray(parsed)) {
|
|
156
|
+
const textBlock = parsed.find((b: Record<string, unknown>) => isRecord(b) && b.type === 'text');
|
|
157
|
+
if (textBlock && typeof textBlock.text === 'string') {
|
|
158
|
+
objective = textBlock.text;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch { /* ignore */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Extract events from both assistant and user messages.
|
|
165
|
+
// Subagent conversations are not consolidated, so tool_result blocks
|
|
166
|
+
// live in separate user-role messages following the assistant's tool_use.
|
|
167
|
+
const events: Array<{ type: string; content: string; toolName?: string; isError?: boolean }> = [];
|
|
168
|
+
const pendingTools = new Map<string, string>();
|
|
169
|
+
for (const m of subagentMsgs) {
|
|
170
|
+
if (m.role !== 'assistant' && m.role !== 'user') continue;
|
|
171
|
+
let content: unknown[];
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(m.content);
|
|
174
|
+
content = Array.isArray(parsed) ? parsed : [];
|
|
175
|
+
} catch { continue; }
|
|
176
|
+
|
|
177
|
+
for (const block of content) {
|
|
178
|
+
if (!isRecord(block) || typeof block.type !== 'string') continue;
|
|
179
|
+
if (m.role === 'assistant' && block.type === 'text' && typeof block.text === 'string') {
|
|
180
|
+
events.push({ type: 'text', content: block.text });
|
|
181
|
+
} else if (block.type === 'tool_use') {
|
|
182
|
+
const name = typeof block.name === 'string' ? block.name : 'unknown';
|
|
183
|
+
const input = isRecord(block.input) ? block.input as Record<string, unknown> : {};
|
|
184
|
+
const id = typeof block.id === 'string' ? block.id : '';
|
|
185
|
+
events.push({ type: 'tool_use', content: JSON.stringify(input), toolName: name });
|
|
186
|
+
if (id) pendingTools.set(id, name);
|
|
187
|
+
} else if (block.type === 'tool_result') {
|
|
188
|
+
const toolUseId = typeof block.tool_use_id === 'string' ? block.tool_use_id : '';
|
|
189
|
+
const resultContent = typeof block.content === 'string' ? block.content : '';
|
|
190
|
+
const isError = block.is_error === true;
|
|
191
|
+
const toolName = toolUseId ? pendingTools.get(toolUseId) : undefined;
|
|
192
|
+
events.push({ type: 'tool_result', content: resultContent, toolName: toolName ?? 'unknown', isError });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
ctx.send(socket, {
|
|
198
|
+
type: 'subagent_detail_response',
|
|
199
|
+
subagentId: msg.subagentId,
|
|
200
|
+
objective,
|
|
201
|
+
events,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
123
205
|
export const subagentHandlers = defineHandlers({
|
|
124
206
|
subagent_abort: handleSubagentAbort,
|
|
125
207
|
subagent_status: handleSubagentStatus,
|
|
126
208
|
subagent_message: handleSubagentMessage,
|
|
209
|
+
subagent_detail_request: handleSubagentDetailRequest,
|
|
127
210
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as net from 'node:net';
|
|
2
|
-
import { loadRawConfig } from '../../config/loader.js';
|
|
2
|
+
import { loadRawConfig, loadConfig } from '../../config/loader.js';
|
|
3
3
|
import { getSecureKey, setSecureKey, deleteSecureKey } from '../../security/secure-keys.js';
|
|
4
4
|
import { startOAuth2Flow } from '../../security/oauth2.js';
|
|
5
|
+
import { getPublicBaseUrl } from '../../inbound/public-ingress-urls.js';
|
|
5
6
|
import { upsertCredentialMetadata, getCredentialMetadata } from '../../tools/credentials/metadata-store.js';
|
|
7
|
+
import { ConfigError } from '../../util/errors.js';
|
|
6
8
|
import type { TwitterAuthStartRequest, TwitterAuthStatusRequest } from '../ipc-protocol.js';
|
|
7
9
|
import { log, defineHandlers, type HandlerContext } from './shared.js';
|
|
8
10
|
import type { OAuth2Config } from '../../security/oauth2.js';
|
|
@@ -36,6 +38,33 @@ export async function handleTwitterAuthStart(
|
|
|
36
38
|
|
|
37
39
|
const clientSecret = getSecureKey('credential:integration:twitter:oauth_client_secret') || undefined;
|
|
38
40
|
|
|
41
|
+
// Fail fast if no public ingress URL is configured — Twitter OAuth
|
|
42
|
+
// callbacks must route through the gateway, never via loopback.
|
|
43
|
+
let config;
|
|
44
|
+
try {
|
|
45
|
+
config = loadConfig();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const detail = err instanceof ConfigError ? err.message : String(err);
|
|
48
|
+
ctx.send(socket, {
|
|
49
|
+
type: 'twitter_auth_result',
|
|
50
|
+
success: false,
|
|
51
|
+
error: `Unable to load config: ${detail}`,
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
getPublicBaseUrl(config);
|
|
58
|
+
} catch {
|
|
59
|
+
ctx.send(socket, {
|
|
60
|
+
type: 'twitter_auth_result',
|
|
61
|
+
success: false,
|
|
62
|
+
error:
|
|
63
|
+
'Set ingress.publicBaseUrl (or INGRESS_PUBLIC_BASE_URL) so OAuth callbacks can route through /webhooks/oauth/callback on the gateway.',
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
39
68
|
const oauthConfig: OAuth2Config = {
|
|
40
69
|
authUrl: 'https://twitter.com/i/oauth2/authorize',
|
|
41
70
|
tokenUrl: 'https://api.x.com/2/oauth2/token',
|
|
@@ -49,7 +78,7 @@ export async function handleTwitterAuthStart(
|
|
|
49
78
|
openUrl: (url: string) => {
|
|
50
79
|
ctx.send(socket, { type: 'open_url', url });
|
|
51
80
|
},
|
|
52
|
-
});
|
|
81
|
+
}, { callbackTransport: 'gateway' });
|
|
53
82
|
|
|
54
83
|
// Verify identity via Twitter API before persisting any tokens
|
|
55
84
|
let accountInfo: string;
|
|
@@ -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",
|
|
@@ -75,12 +76,12 @@
|
|
|
75
76
|
"SkillsUpdateRequest",
|
|
76
77
|
"SlackWebhookConfigRequest",
|
|
77
78
|
"SubagentAbortRequest",
|
|
79
|
+
"SubagentDetailRequest",
|
|
78
80
|
"SubagentMessageRequest",
|
|
79
81
|
"SubagentStatusRequest",
|
|
80
82
|
"SuggestionRequest",
|
|
81
83
|
"TaskSubmit",
|
|
82
84
|
"TrustRulesList",
|
|
83
|
-
"TwilioWebhookConfigRequest",
|
|
84
85
|
"TwitterAuthStartRequest",
|
|
85
86
|
"TwitterAuthStatusRequest",
|
|
86
87
|
"TwitterIntegrationConfigRequest",
|
|
@@ -142,6 +143,7 @@
|
|
|
142
143
|
"GetSigningIdentityRequest",
|
|
143
144
|
"HistoryResponse",
|
|
144
145
|
"HomeBaseGetResponse",
|
|
146
|
+
"IngressConfigResponse",
|
|
145
147
|
"IntegrationConnectResult",
|
|
146
148
|
"IntegrationListResponse",
|
|
147
149
|
"IpcBlobProbeResult",
|
|
@@ -179,6 +181,7 @@
|
|
|
179
181
|
"SkillsListResponse",
|
|
180
182
|
"SkillsOperationResponse",
|
|
181
183
|
"SlackWebhookConfigResponse",
|
|
184
|
+
"SubagentDetailResponse",
|
|
182
185
|
"SubagentEvent",
|
|
183
186
|
"SubagentSpawned",
|
|
184
187
|
"SubagentStatusChanged",
|
|
@@ -192,7 +195,6 @@
|
|
|
192
195
|
"ToolUseStart",
|
|
193
196
|
"TraceEvent",
|
|
194
197
|
"TrustRulesListResponse",
|
|
195
|
-
"TwilioWebhookConfigResponse",
|
|
196
198
|
"TwitterAuthResult",
|
|
197
199
|
"TwitterAuthStatusResponse",
|
|
198
200
|
"TwitterIntegrationConfigResponse",
|
|
@@ -255,6 +257,7 @@
|
|
|
255
257
|
"history_request",
|
|
256
258
|
"home_base_get",
|
|
257
259
|
"image_gen_model_set",
|
|
260
|
+
"ingress_config",
|
|
258
261
|
"integration_connect",
|
|
259
262
|
"integration_disconnect",
|
|
260
263
|
"integration_list",
|
|
@@ -298,12 +301,12 @@
|
|
|
298
301
|
"skills_update",
|
|
299
302
|
"slack_webhook_config",
|
|
300
303
|
"subagent_abort",
|
|
304
|
+
"subagent_detail_request",
|
|
301
305
|
"subagent_message",
|
|
302
306
|
"subagent_status",
|
|
303
307
|
"suggestion_request",
|
|
304
308
|
"task_submit",
|
|
305
309
|
"trust_rules_list",
|
|
306
|
-
"twilio_webhook_config",
|
|
307
310
|
"twitter_auth_start",
|
|
308
311
|
"twitter_auth_status",
|
|
309
312
|
"twitter_integration_config",
|
|
@@ -365,6 +368,7 @@
|
|
|
365
368
|
"get_signing_identity",
|
|
366
369
|
"history_response",
|
|
367
370
|
"home_base_get_response",
|
|
371
|
+
"ingress_config_response",
|
|
368
372
|
"integration_connect_result",
|
|
369
373
|
"integration_list_response",
|
|
370
374
|
"ipc_blob_probe_result",
|
|
@@ -402,6 +406,7 @@
|
|
|
402
406
|
"skills_operation_response",
|
|
403
407
|
"skills_state_changed",
|
|
404
408
|
"slack_webhook_config_response",
|
|
409
|
+
"subagent_detail_response",
|
|
405
410
|
"subagent_event",
|
|
406
411
|
"subagent_spawned",
|
|
407
412
|
"subagent_status_changed",
|
|
@@ -415,7 +420,6 @@
|
|
|
415
420
|
"tool_use_start",
|
|
416
421
|
"trace_event",
|
|
417
422
|
"trust_rules_list_response",
|
|
418
|
-
"twilio_webhook_config_response",
|
|
419
423
|
"twitter_auth_result",
|
|
420
424
|
"twitter_auth_status_response",
|
|
421
425
|
"twitter_integration_config_response",
|
|
@@ -472,10 +472,10 @@ export interface SlackWebhookConfigRequest {
|
|
|
472
472
|
webhookUrl?: string;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
export interface
|
|
476
|
-
type: '
|
|
475
|
+
export interface IngressConfigRequest {
|
|
476
|
+
type: 'ingress_config';
|
|
477
477
|
action: 'get' | 'set';
|
|
478
|
-
|
|
478
|
+
publicBaseUrl?: string;
|
|
479
479
|
}
|
|
480
480
|
|
|
481
481
|
export interface VercelApiConfigRequest {
|
|
@@ -941,7 +941,7 @@ export type ClientMessage =
|
|
|
941
941
|
| ShareAppCloudRequest
|
|
942
942
|
| ShareToSlackRequest
|
|
943
943
|
| SlackWebhookConfigRequest
|
|
944
|
-
|
|
|
944
|
+
| IngressConfigRequest
|
|
945
945
|
| VercelApiConfigRequest
|
|
946
946
|
| TwitterIntegrationConfigRequest
|
|
947
947
|
| TwitterAuthStartRequest
|
|
@@ -978,11 +978,8 @@ export type ClientMessage =
|
|
|
978
978
|
| WorkItemCancelRequest
|
|
979
979
|
| SubagentAbortRequest
|
|
980
980
|
| SubagentStatusRequest
|
|
981
|
-
| SubagentMessageRequest
|
|
982
|
-
|
|
983
|
-
// ── Legacy integration IPC stubs ────────────────────────────────────
|
|
984
|
-
// The macOS Settings panel still sends these messages. Stub types keep
|
|
985
|
-
// the dispatch map happy until the client-side migration lands.
|
|
981
|
+
| SubagentMessageRequest
|
|
982
|
+
| SubagentDetailRequest;
|
|
986
983
|
|
|
987
984
|
export interface IntegrationListRequest {
|
|
988
985
|
type: 'integration_list';
|
|
@@ -1227,6 +1224,7 @@ export interface HistoryResponse {
|
|
|
1227
1224
|
label: string;
|
|
1228
1225
|
status: 'completed' | 'failed' | 'aborted';
|
|
1229
1226
|
error?: string;
|
|
1227
|
+
conversationId?: string;
|
|
1230
1228
|
};
|
|
1231
1229
|
}>;
|
|
1232
1230
|
}
|
|
@@ -1690,9 +1688,11 @@ export interface SlackWebhookConfigResponse {
|
|
|
1690
1688
|
error?: string;
|
|
1691
1689
|
}
|
|
1692
1690
|
|
|
1693
|
-
export interface
|
|
1694
|
-
type: '
|
|
1695
|
-
|
|
1691
|
+
export interface IngressConfigResponse {
|
|
1692
|
+
type: 'ingress_config_response';
|
|
1693
|
+
publicBaseUrl: string;
|
|
1694
|
+
/** Read-only gateway target computed from GATEWAY_PORT env var (default 7830) + loopback host. */
|
|
1695
|
+
localGatewayTarget: string;
|
|
1696
1696
|
success: boolean;
|
|
1697
1697
|
error?: string;
|
|
1698
1698
|
}
|
|
@@ -2015,7 +2015,7 @@ export interface WorkItemDeleteResponse {
|
|
|
2015
2015
|
success: boolean;
|
|
2016
2016
|
}
|
|
2017
2017
|
|
|
2018
|
-
export type WorkItemRunTaskErrorCode = 'not_found' | 'already_running' | 'invalid_status' | 'no_task';
|
|
2018
|
+
export type WorkItemRunTaskErrorCode = 'not_found' | 'already_running' | 'invalid_status' | 'no_task' | 'permission_required';
|
|
2019
2019
|
|
|
2020
2020
|
export interface WorkItemRunTaskResponse {
|
|
2021
2021
|
type: 'work_item_run_task_response';
|
|
@@ -2180,7 +2180,7 @@ export type ServerMessage =
|
|
|
2180
2180
|
| GalleryInstallResponse
|
|
2181
2181
|
| ShareToSlackResponse
|
|
2182
2182
|
| SlackWebhookConfigResponse
|
|
2183
|
-
|
|
|
2183
|
+
| IngressConfigResponse
|
|
2184
2184
|
| VercelApiConfigResponse
|
|
2185
2185
|
| TwitterIntegrationConfigResponse
|
|
2186
2186
|
| TwitterAuthResult
|
|
@@ -2219,7 +2219,8 @@ export type ServerMessage =
|
|
|
2219
2219
|
| OpenTasksWindow
|
|
2220
2220
|
| SubagentSpawned
|
|
2221
2221
|
| SubagentStatusChanged
|
|
2222
|
-
| SubagentEvent
|
|
2222
|
+
| SubagentEvent
|
|
2223
|
+
| SubagentDetailResponse;
|
|
2223
2224
|
|
|
2224
2225
|
// === Subagent IPC ─────────────────────────────────────────────────────
|
|
2225
2226
|
|
|
@@ -2239,6 +2240,18 @@ export interface SubagentStatusChanged {
|
|
|
2239
2240
|
usage?: UsageStats;
|
|
2240
2241
|
}
|
|
2241
2242
|
|
|
2243
|
+
export interface SubagentDetailResponse {
|
|
2244
|
+
type: 'subagent_detail_response';
|
|
2245
|
+
subagentId: string;
|
|
2246
|
+
objective?: string;
|
|
2247
|
+
events: Array<{
|
|
2248
|
+
type: string;
|
|
2249
|
+
content: string;
|
|
2250
|
+
toolName?: string;
|
|
2251
|
+
isError?: boolean;
|
|
2252
|
+
}>;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2242
2255
|
/** Wraps any ServerMessage emitted by a subagent session for routing to the client. */
|
|
2243
2256
|
export interface SubagentEvent {
|
|
2244
2257
|
type: 'subagent_event';
|
|
@@ -2265,6 +2278,12 @@ export interface SubagentMessageRequest {
|
|
|
2265
2278
|
content: string;
|
|
2266
2279
|
}
|
|
2267
2280
|
|
|
2281
|
+
export interface SubagentDetailRequest {
|
|
2282
|
+
type: 'subagent_detail_request';
|
|
2283
|
+
subagentId: string;
|
|
2284
|
+
conversationId: string;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2268
2287
|
// === Contract schema ===
|
|
2269
2288
|
|
|
2270
2289
|
export interface IPCContractSchema {
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -316,6 +316,14 @@ export async function runDaemon(): Promise<void> {
|
|
|
316
316
|
await initializeTools();
|
|
317
317
|
log.info('Daemon startup: providers and tools initialized');
|
|
318
318
|
|
|
319
|
+
// Start the IPC socket BEFORE Qdrant so that clients can connect
|
|
320
|
+
// immediately. Qdrant startup can take 30+ seconds (binary download,
|
|
321
|
+
// /readyz polling) which previously blocked the socket from appearing.
|
|
322
|
+
log.info('Daemon startup: starting DaemonServer (IPC socket)');
|
|
323
|
+
const server = new DaemonServer();
|
|
324
|
+
await server.start();
|
|
325
|
+
log.info('Daemon startup: DaemonServer started');
|
|
326
|
+
|
|
319
327
|
// Initialize Qdrant vector store — non-fatal so the daemon stays up without it
|
|
320
328
|
const qdrantUrl = process.env.QDRANT_URL?.trim() || config.memory.qdrant.url;
|
|
321
329
|
log.info({ qdrantUrl }, 'Daemon startup: initializing Qdrant');
|
|
@@ -336,10 +344,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
336
344
|
log.warn({ err }, 'Qdrant failed to start — memory features will be unavailable');
|
|
337
345
|
}
|
|
338
346
|
|
|
339
|
-
log.info('Daemon startup: starting
|
|
340
|
-
const server = new DaemonServer();
|
|
341
|
-
await server.start();
|
|
342
|
-
log.info('Daemon startup: DaemonServer started, starting memory worker');
|
|
347
|
+
log.info('Daemon startup: starting memory worker');
|
|
343
348
|
const memoryWorker = startMemoryJobsWorker();
|
|
344
349
|
// Initialize watcher engine and register providers
|
|
345
350
|
registerWatcherProvider(gmailProvider);
|
package/src/daemon/server.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { getSubagentManager } from '../subagent/index.js';
|
|
|
39
39
|
import { tryHandlePendingCallAnswer } from '../calls/call-bridge.js';
|
|
40
40
|
import { resolveSlash } from './session-slash.js';
|
|
41
41
|
import { createUserMessage, createAssistantMessage } from '../agent/message-types.js';
|
|
42
|
+
import { registerDaemonCallbacks } from '../work-items/work-item-runner.js';
|
|
42
43
|
|
|
43
44
|
const log = getLogger('server');
|
|
44
45
|
|
|
@@ -184,6 +185,12 @@ export class DaemonServer {
|
|
|
184
185
|
|
|
185
186
|
this.evictor.start();
|
|
186
187
|
|
|
188
|
+
// Register daemon callbacks so tools can trigger work item execution
|
|
189
|
+
registerDaemonCallbacks({
|
|
190
|
+
getOrCreateSession: (conversationId) => this.getOrCreateSession(conversationId),
|
|
191
|
+
broadcast: (msg) => this.broadcast(msg),
|
|
192
|
+
});
|
|
193
|
+
|
|
187
194
|
ensureBlobDir();
|
|
188
195
|
this.blobSweepTimer = setInterval(() => {
|
|
189
196
|
sweepStaleBlobs(30 * 60 * 1000).catch((err) => {
|
|
@@ -14,6 +14,9 @@ import type { PermissionPrompter } from '../permissions/prompter.js';
|
|
|
14
14
|
import type { SecretPrompter } from '../permissions/secret-prompter.js';
|
|
15
15
|
import { addRule, findHighestPriorityRule } from '../permissions/trust-store.js';
|
|
16
16
|
import { generateAllowlistOptions, generateScopeOptions, normalizeWebFetchUrl } from '../permissions/checker.js';
|
|
17
|
+
import { getLogger } from '../util/logger.js';
|
|
18
|
+
|
|
19
|
+
const log = getLogger('session-tool-setup');
|
|
17
20
|
import { getAllToolDefinitions } from '../tools/registry.js';
|
|
18
21
|
import { allUiSurfaceTools } from '../tools/ui-surface/definitions.js';
|
|
19
22
|
import { coreAppProxyTools } from '../tools/apps/definitions.js';
|
|
@@ -248,10 +251,12 @@ export function createToolExecutor(
|
|
|
248
251
|
req.principal,
|
|
249
252
|
);
|
|
250
253
|
if ((response.decision === 'always_allow' || response.decision === 'always_allow_high_risk') && response.selectedPattern && response.selectedScope) {
|
|
254
|
+
log.info({ toolName: 'cc:' + req.toolName, pattern: response.selectedPattern, scope: response.selectedScope, highRisk: response.decision === 'always_allow_high_risk' }, 'Persisting always-allow trust rule');
|
|
251
255
|
addRule('cc:' + req.toolName, response.selectedPattern, response.selectedScope, 'allow', 100,
|
|
252
256
|
response.decision === 'always_allow_high_risk' ? { allowHighRisk: true } : undefined);
|
|
253
257
|
}
|
|
254
258
|
if (response.decision === 'always_deny' && response.selectedPattern && response.selectedScope) {
|
|
259
|
+
log.info({ toolName: 'cc:' + req.toolName, pattern: response.selectedPattern, scope: response.selectedScope }, 'Persisting always-deny trust rule');
|
|
255
260
|
addRule('cc:' + req.toolName, response.selectedPattern, response.selectedScope, 'deny');
|
|
256
261
|
}
|
|
257
262
|
return {
|
|
@@ -277,7 +282,7 @@ export function createToolExecutor(
|
|
|
277
282
|
|
|
278
283
|
// Broadcast tasks_changed so connected clients (e.g. macOS Tasks window)
|
|
279
284
|
// auto-refresh when the LLM mutates the task queue via tools
|
|
280
|
-
if ((name === 'task_list_add' || name === 'task_list_update' || name === 'task_list_remove') && !result.isError) {
|
|
285
|
+
if ((name === 'task_list_add' || name === 'task_list_update' || name === 'task_list_remove' || name === 'task_queue_run') && !result.isError) {
|
|
281
286
|
broadcastToAllClients?.({ type: 'tasks_changed' });
|
|
282
287
|
}
|
|
283
288
|
|
|
@@ -440,10 +445,12 @@ export function createProxyApprovalCallback(
|
|
|
440
445
|
|
|
441
446
|
// Persist trust rule if the user chose "always allow" or "always deny"
|
|
442
447
|
if ((response.decision === 'always_allow' || response.decision === 'always_allow_high_risk') && response.selectedPattern && response.selectedScope) {
|
|
448
|
+
log.info({ toolName, pattern: response.selectedPattern, scope: response.selectedScope, highRisk: response.decision === 'always_allow_high_risk' }, 'Persisting always-allow trust rule (proxy)');
|
|
443
449
|
addRule(toolName, response.selectedPattern, response.selectedScope, 'allow', 100,
|
|
444
450
|
response.decision === 'always_allow_high_risk' ? { allowHighRisk: true } : undefined);
|
|
445
451
|
}
|
|
446
452
|
if (response.decision === 'always_deny' && response.selectedPattern && response.selectedScope) {
|
|
453
|
+
log.info({ toolName, pattern: response.selectedPattern, scope: response.selectedScope }, 'Persisting always-deny trust rule (proxy)');
|
|
447
454
|
addRule(toolName, response.selectedPattern, response.selectedScope, 'deny');
|
|
448
455
|
}
|
|
449
456
|
|