@vellumai/assistant 0.3.6 → 0.3.7
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/package.json +1 -1
- package/src/config/defaults.ts +1 -1
- package/src/config/env-registry.ts +7 -0
- package/src/config/memory-schema.ts +3 -3
- package/src/config/schema.ts +1 -1
- package/src/daemon/daemon-control.ts +43 -6
- package/src/daemon/handlers/sessions.ts +20 -0
- package/src/daemon/ipc-contract/sessions.ts +6 -0
- package/src/daemon/ipc-contract-inventory.json +2 -0
- package/src/daemon/ipc-contract.ts +2 -1
- package/src/daemon/session-queue-manager.ts +13 -11
- package/src/daemon/session-surfaces.ts +8 -1
- package/src/memory/migrations/016-memory-segments-indexes.ts +4 -4
- package/src/memory/migrations/017-memory-items-indexes.ts +5 -3
- package/src/memory/retriever.ts +4 -1
- package/src/permissions/checker.ts +14 -7
- package/src/runtime/assistant-event-hub.ts +3 -1
- package/src/runtime/http-server.ts +17 -5
- package/src/runtime/routes/pairing-routes.ts +2 -1
- package/src/tools/browser/browser-execution.ts +4 -1
- package/src/tools/executor.ts +12 -9
- package/src/util/logger.ts +7 -2
package/package.json
CHANGED
package/src/config/defaults.ts
CHANGED
|
@@ -136,11 +136,18 @@ const KNOWN_VELLUM_VARS = new Set([
|
|
|
136
136
|
'VELLUM_DAEMON_TCP_ENABLED',
|
|
137
137
|
'VELLUM_DAEMON_TCP_HOST',
|
|
138
138
|
'VELLUM_DAEMON_IOS_PAIRING',
|
|
139
|
+
'VELLUM_DAEMON_NOAUTH',
|
|
140
|
+
'VELLUM_DAEMON_AUTOSTART',
|
|
139
141
|
'VELLUM_DEBUG',
|
|
140
142
|
'VELLUM_LOG_STDERR',
|
|
141
143
|
'VELLUM_ENABLE_MONITORING',
|
|
142
144
|
'VELLUM_HOOK_EVENT',
|
|
143
145
|
'VELLUM_HOOK_NAME',
|
|
146
|
+
'VELLUM_HOOK_SETTINGS',
|
|
147
|
+
'VELLUM_ROOT_DIR',
|
|
148
|
+
'VELLUM_WORKSPACE_DIR',
|
|
149
|
+
'VELLUM_CLAUDE_CODE_DEPTH',
|
|
150
|
+
'VELLUM_ASSISTANT_PLATFORM_URL',
|
|
144
151
|
]);
|
|
145
152
|
|
|
146
153
|
/**
|
|
@@ -60,7 +60,7 @@ export const QdrantConfigSchema = z.object({
|
|
|
60
60
|
export const MemoryRerankingConfigSchema = z.object({
|
|
61
61
|
enabled: z
|
|
62
62
|
.boolean({ error: 'memory.retrieval.reranking.enabled must be a boolean' })
|
|
63
|
-
.default(
|
|
63
|
+
.default(false),
|
|
64
64
|
model: z
|
|
65
65
|
.string({ error: 'memory.retrieval.reranking.model must be a string' })
|
|
66
66
|
.default('claude-haiku-4-5-20251001'),
|
|
@@ -186,7 +186,7 @@ export const MemoryRetrievalConfigSchema = z.object({
|
|
|
186
186
|
})
|
|
187
187
|
.default('prepend_user_block'),
|
|
188
188
|
reranking: MemoryRerankingConfigSchema.default({
|
|
189
|
-
enabled:
|
|
189
|
+
enabled: false,
|
|
190
190
|
model: 'claude-haiku-4-5-20251001',
|
|
191
191
|
topK: 20,
|
|
192
192
|
}),
|
|
@@ -430,7 +430,7 @@ export const MemoryConfigSchema = z.object({
|
|
|
430
430
|
injectionFormat: 'markdown',
|
|
431
431
|
injectionStrategy: 'prepend_user_block',
|
|
432
432
|
reranking: {
|
|
433
|
-
enabled:
|
|
433
|
+
enabled: false,
|
|
434
434
|
model: 'claude-haiku-4-5-20251001',
|
|
435
435
|
topK: 20,
|
|
436
436
|
},
|
package/src/config/schema.ts
CHANGED
|
@@ -5,14 +5,51 @@ import {
|
|
|
5
5
|
getSocketPath,
|
|
6
6
|
getPidPath,
|
|
7
7
|
getRootDir,
|
|
8
|
+
getWorkspaceConfigPath,
|
|
8
9
|
removeSocketFile,
|
|
9
10
|
} from '../util/platform.js';
|
|
10
11
|
import { getLogger } from '../util/logger.js';
|
|
11
12
|
import { DaemonError } from '../util/errors.js';
|
|
12
|
-
import { getConfig } from '../config/loader.js';
|
|
13
13
|
|
|
14
14
|
const log = getLogger('lifecycle');
|
|
15
15
|
|
|
16
|
+
const DAEMON_TIMEOUT_DEFAULTS = {
|
|
17
|
+
startupSocketWaitMs: 5000,
|
|
18
|
+
stopTimeoutMs: 5000,
|
|
19
|
+
sigkillGracePeriodMs: 2000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read daemon timeout values directly from the config JSON file, bypassing
|
|
24
|
+
* loadConfig() and its ensureMigratedDataDir()/ensureDataDir() side effects.
|
|
25
|
+
* Falls back to hardcoded defaults on any error (missing file, malformed JSON,
|
|
26
|
+
* unexpected shape) so daemon stop/start never fails due to config issues.
|
|
27
|
+
*/
|
|
28
|
+
function readDaemonTimeouts(): typeof DAEMON_TIMEOUT_DEFAULTS {
|
|
29
|
+
try {
|
|
30
|
+
const raw = JSON.parse(readFileSync(getWorkspaceConfigPath(), 'utf-8'));
|
|
31
|
+
if (raw.daemon && typeof raw.daemon === 'object') {
|
|
32
|
+
return {
|
|
33
|
+
startupSocketWaitMs:
|
|
34
|
+
typeof raw.daemon.startupSocketWaitMs === 'number'
|
|
35
|
+
? raw.daemon.startupSocketWaitMs
|
|
36
|
+
: DAEMON_TIMEOUT_DEFAULTS.startupSocketWaitMs,
|
|
37
|
+
stopTimeoutMs:
|
|
38
|
+
typeof raw.daemon.stopTimeoutMs === 'number'
|
|
39
|
+
? raw.daemon.stopTimeoutMs
|
|
40
|
+
: DAEMON_TIMEOUT_DEFAULTS.stopTimeoutMs,
|
|
41
|
+
sigkillGracePeriodMs:
|
|
42
|
+
typeof raw.daemon.sigkillGracePeriodMs === 'number'
|
|
43
|
+
? raw.daemon.sigkillGracePeriodMs
|
|
44
|
+
: DAEMON_TIMEOUT_DEFAULTS.sigkillGracePeriodMs,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Missing file, malformed JSON, etc. — use defaults.
|
|
49
|
+
}
|
|
50
|
+
return { ...DAEMON_TIMEOUT_DEFAULTS };
|
|
51
|
+
}
|
|
52
|
+
|
|
16
53
|
function isProcessRunning(pid: number): boolean {
|
|
17
54
|
try {
|
|
18
55
|
process.kill(pid, 0);
|
|
@@ -125,8 +162,8 @@ export async function startDaemon(): Promise<{
|
|
|
125
162
|
writePid(pid);
|
|
126
163
|
|
|
127
164
|
// Wait for socket to appear
|
|
128
|
-
const
|
|
129
|
-
const maxWait =
|
|
165
|
+
const timeouts = readDaemonTimeouts();
|
|
166
|
+
const maxWait = timeouts.startupSocketWaitMs;
|
|
130
167
|
const interval = 100;
|
|
131
168
|
let waited = 0;
|
|
132
169
|
while (waited < maxWait) {
|
|
@@ -165,10 +202,10 @@ export async function stopDaemon(): Promise<StopResult> {
|
|
|
165
202
|
|
|
166
203
|
process.kill(pid, 'SIGTERM');
|
|
167
204
|
|
|
168
|
-
const
|
|
205
|
+
const timeouts = readDaemonTimeouts();
|
|
169
206
|
|
|
170
207
|
// Wait for process to exit
|
|
171
|
-
const maxWait =
|
|
208
|
+
const maxWait = timeouts.stopTimeoutMs;
|
|
172
209
|
const interval = 100;
|
|
173
210
|
let waited = 0;
|
|
174
211
|
while (waited < maxWait) {
|
|
@@ -190,7 +227,7 @@ export async function stopDaemon(): Promise<StopResult> {
|
|
|
190
227
|
// Wait for the process to actually die after SIGKILL. Without this,
|
|
191
228
|
// startDaemon() can race with the dying process's shutdown handler,
|
|
192
229
|
// which removes the socket file and bricks the new daemon.
|
|
193
|
-
const killMaxWait =
|
|
230
|
+
const killMaxWait = timeouts.sigkillGracePeriodMs;
|
|
194
231
|
let killWaited = 0;
|
|
195
232
|
while (killWaited < killMaxWait && isProcessRunning(pid)) {
|
|
196
233
|
await new Promise((r) => setTimeout(r, 100));
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
SecretResponse,
|
|
17
17
|
SessionCreateRequest,
|
|
18
18
|
SessionSwitchRequest,
|
|
19
|
+
SessionRenameRequest,
|
|
19
20
|
CancelRequest,
|
|
20
21
|
DeleteQueuedMessage,
|
|
21
22
|
HistoryRequest,
|
|
@@ -352,6 +353,24 @@ export async function handleSessionSwitch(
|
|
|
352
353
|
});
|
|
353
354
|
}
|
|
354
355
|
|
|
356
|
+
export function handleSessionRename(
|
|
357
|
+
msg: SessionRenameRequest,
|
|
358
|
+
socket: net.Socket,
|
|
359
|
+
ctx: HandlerContext,
|
|
360
|
+
): void {
|
|
361
|
+
const conversation = conversationStore.getConversation(msg.sessionId);
|
|
362
|
+
if (!conversation) {
|
|
363
|
+
ctx.send(socket, { type: 'error', message: `Session ${msg.sessionId} not found` });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
conversationStore.updateConversationTitle(msg.sessionId, msg.title);
|
|
367
|
+
ctx.send(socket, {
|
|
368
|
+
type: 'session_title_updated',
|
|
369
|
+
sessionId: msg.sessionId,
|
|
370
|
+
title: msg.title,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
355
374
|
export function handleCancel(msg: CancelRequest, socket: net.Socket, ctx: HandlerContext): void {
|
|
356
375
|
const sessionId = msg.sessionId || ctx.socketToSession.get(socket);
|
|
357
376
|
if (sessionId) {
|
|
@@ -597,6 +616,7 @@ export const sessionHandlers = defineHandlers({
|
|
|
597
616
|
session_create: handleSessionCreate,
|
|
598
617
|
sessions_clear: (_msg, socket, ctx) => handleSessionsClear(socket, ctx),
|
|
599
618
|
session_switch: handleSessionSwitch,
|
|
619
|
+
session_rename: handleSessionRename,
|
|
600
620
|
cancel: handleCancel,
|
|
601
621
|
delete_queued_message: handleDeleteQueuedMessage,
|
|
602
622
|
history_request: handleHistoryRequest,
|
|
@@ -43,6 +43,12 @@ export interface SessionSwitchRequest {
|
|
|
43
43
|
sessionId: string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface SessionRenameRequest {
|
|
47
|
+
type: 'session_rename';
|
|
48
|
+
sessionId: string;
|
|
49
|
+
title: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
export interface AuthMessage {
|
|
47
53
|
type: 'auth';
|
|
48
54
|
token: string;
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"SecretResponse",
|
|
80
80
|
"SessionCreateRequest",
|
|
81
81
|
"SessionListRequest",
|
|
82
|
+
"SessionRenameRequest",
|
|
82
83
|
"SessionSwitchRequest",
|
|
83
84
|
"SessionsClearRequest",
|
|
84
85
|
"ShareAppCloudRequest",
|
|
@@ -363,6 +364,7 @@
|
|
|
363
364
|
"secret_response",
|
|
364
365
|
"session_create",
|
|
365
366
|
"session_list",
|
|
367
|
+
"session_rename",
|
|
366
368
|
"session_switch",
|
|
367
369
|
"sessions_clear",
|
|
368
370
|
"share_app_cloud",
|
|
@@ -29,7 +29,7 @@ export * from './ipc-contract/inbox.js';
|
|
|
29
29
|
export * from './ipc-contract/pairing.js';
|
|
30
30
|
|
|
31
31
|
// Import types needed for aggregate unions and SubagentEvent
|
|
32
|
-
import type { AuthMessage, PingMessage, CancelRequest, DeleteQueuedMessage, ModelGetRequest, ModelSetRequest, ImageGenModelSetRequest, HistoryRequest, UndoRequest, RegenerateRequest, UsageRequest, SandboxSetRequest, SessionListRequest, SessionCreateRequest, SessionSwitchRequest, SessionsClearRequest, ConversationSearchRequest } from './ipc-contract/sessions.js';
|
|
32
|
+
import type { AuthMessage, PingMessage, CancelRequest, DeleteQueuedMessage, ModelGetRequest, ModelSetRequest, ImageGenModelSetRequest, HistoryRequest, UndoRequest, RegenerateRequest, UsageRequest, SandboxSetRequest, SessionListRequest, SessionCreateRequest, SessionSwitchRequest, SessionRenameRequest, SessionsClearRequest, ConversationSearchRequest } from './ipc-contract/sessions.js';
|
|
33
33
|
import type { UserMessage, ConfirmationResponse, SecretResponse, SuggestionRequest } from './ipc-contract/messages.js';
|
|
34
34
|
import type { UiSurfaceAction, UiSurfaceUndoRequest } from './ipc-contract/surfaces.js';
|
|
35
35
|
import type { SkillsListRequest, SkillDetailRequest, SkillsEnableRequest, SkillsDisableRequest, SkillsConfigureRequest, SkillsInstallRequest, SkillsUninstallRequest, SkillsUpdateRequest, SkillsCheckUpdatesRequest, SkillsSearchRequest, SkillsInspectRequest } from './ipc-contract/skills.js';
|
|
@@ -84,6 +84,7 @@ export type ClientMessage =
|
|
|
84
84
|
| SessionListRequest
|
|
85
85
|
| SessionCreateRequest
|
|
86
86
|
| SessionSwitchRequest
|
|
87
|
+
| SessionRenameRequest
|
|
87
88
|
| PingMessage
|
|
88
89
|
| CancelRequest
|
|
89
90
|
| DeleteQueuedMessage
|
|
@@ -78,11 +78,6 @@ export class MessageQueue {
|
|
|
78
78
|
|
|
79
79
|
if (this.items.length >= MAX_QUEUE_DEPTH) {
|
|
80
80
|
this.droppedCount++;
|
|
81
|
-
item.onEvent({
|
|
82
|
-
type: 'error',
|
|
83
|
-
message: 'Message queue is full. Please wait for current messages to be processed.',
|
|
84
|
-
category: 'queue_full',
|
|
85
|
-
});
|
|
86
81
|
return false;
|
|
87
82
|
}
|
|
88
83
|
|
|
@@ -149,21 +144,28 @@ export class MessageQueue {
|
|
|
149
144
|
private expireStale(): void {
|
|
150
145
|
const now = Date.now();
|
|
151
146
|
const cutoff = now - this.maxWaitMs;
|
|
152
|
-
const
|
|
147
|
+
const expired: QueuedMessage[] = [];
|
|
153
148
|
this.items = this.items.filter((item) => {
|
|
154
149
|
if (item.queuedAt < cutoff) {
|
|
155
150
|
this.expiredCount++;
|
|
156
|
-
|
|
151
|
+
expired.push(item);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
for (const item of expired) {
|
|
157
|
+
log.warn({ requestId: item.requestId, waitMs: now - item.queuedAt }, 'Expiring stale queued message');
|
|
158
|
+
try {
|
|
157
159
|
item.onEvent({
|
|
158
160
|
type: 'error',
|
|
159
161
|
message: 'Your queued message was dropped because it waited too long in the queue.',
|
|
160
162
|
category: 'queue_expired',
|
|
161
163
|
});
|
|
162
|
-
|
|
164
|
+
} catch (e) {
|
|
165
|
+
log.debug({ err: e, requestId: item.requestId }, 'Failed to notify client of expired message');
|
|
163
166
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (this.items.length < before && this.items.length / MAX_QUEUE_DEPTH < CAPACITY_WARNING_THRESHOLD) {
|
|
167
|
+
}
|
|
168
|
+
if (expired.length > 0 && this.items.length / MAX_QUEUE_DEPTH < CAPACITY_WARNING_THRESHOLD) {
|
|
167
169
|
this.capacityWarned = false;
|
|
168
170
|
}
|
|
169
171
|
}
|
|
@@ -140,7 +140,14 @@ export function createSurfaceMutex(): <T>(surfaceId: string, fn: () => T | Promi
|
|
|
140
140
|
const prev = chains.get(surfaceId) ?? Promise.resolve();
|
|
141
141
|
const next = prev.then(fn, fn);
|
|
142
142
|
// Keep the chain alive but swallow errors so one failure doesn't block subsequent ops
|
|
143
|
-
|
|
143
|
+
const tail = next.then(() => {}, () => {});
|
|
144
|
+
chains.set(surfaceId, tail);
|
|
145
|
+
// Clean up the map entry once the queue settles to prevent unbounded growth
|
|
146
|
+
tail.then(() => {
|
|
147
|
+
if (chains.get(surfaceId) === tail) {
|
|
148
|
+
chains.delete(surfaceId);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
144
151
|
return next;
|
|
145
152
|
};
|
|
146
153
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { DrizzleDb } from '../db-connection.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Idempotent migration to
|
|
5
|
-
* conversation_id
|
|
6
|
-
*
|
|
4
|
+
* Idempotent migration to add a scope_id index on memory_segments.
|
|
5
|
+
* conversation_id is already covered by the composite index
|
|
6
|
+
* idx_memory_segments_conversation_created(conversation_id, created_at DESC)
|
|
7
|
+
* in db-init, so a standalone index is unnecessary.
|
|
7
8
|
*/
|
|
8
9
|
export function migrateMemorySegmentsIndexes(database: DrizzleDb): void {
|
|
9
10
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_segments_scope_id ON memory_segments(scope_id)`);
|
|
10
|
-
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_segments_conversation_id ON memory_segments(conversation_id)`);
|
|
11
11
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { DrizzleDb } from '../db-connection.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Idempotent migration to add
|
|
5
|
-
*
|
|
4
|
+
* Idempotent migration to add an index on memory_items.scope_id for
|
|
5
|
+
* scope-filtered queries. The standalone fingerprint index is intentionally
|
|
6
|
+
* omitted — it's superseded by the compound unique index
|
|
7
|
+
* idx_memory_items_fingerprint_scope(fingerprint, scope_id), which already
|
|
8
|
+
* covers single-column lookups on fingerprint as its leading column.
|
|
6
9
|
*/
|
|
7
10
|
export function migrateMemoryItemsIndexes(database: DrizzleDb): void {
|
|
8
11
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_scope_id ON memory_items(scope_id)`);
|
|
9
|
-
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_fingerprint ON memory_items(fingerprint)`);
|
|
10
12
|
}
|
package/src/memory/retriever.ts
CHANGED
|
@@ -779,7 +779,10 @@ function isAbortError(err: unknown): boolean {
|
|
|
779
779
|
* in the message. This avoids false positives from dimension numbers like 512.
|
|
780
780
|
*/
|
|
781
781
|
function getErrorStatusCode(err: Error): unknown {
|
|
782
|
-
if ('status' in err)
|
|
782
|
+
if ('status' in err) {
|
|
783
|
+
const status = (err as { status: unknown }).status;
|
|
784
|
+
if (status != null) return status;
|
|
785
|
+
}
|
|
783
786
|
if ('statusCode' in err) return (err as { statusCode: unknown }).statusCode;
|
|
784
787
|
return undefined;
|
|
785
788
|
}
|
|
@@ -16,16 +16,23 @@ import type { ManifestOverride } from '../tools/execution-target.js';
|
|
|
16
16
|
|
|
17
17
|
// ── Risk classification cache ────────────────────────────────────────────────
|
|
18
18
|
// classifyRisk() is called on every permission check and can invoke WASM
|
|
19
|
-
// parsing for shell commands. Cache results keyed on
|
|
19
|
+
// parsing for shell commands. Cache results keyed on
|
|
20
|
+
// (toolName, inputHash, workingDir, manifestOverride).
|
|
20
21
|
// Invalidated when trust rules change since risk classification for file tools
|
|
21
22
|
// depends on skill source path checks which reference config, but the core
|
|
22
23
|
// risk logic is input-deterministic.
|
|
23
24
|
const RISK_CACHE_MAX = 256;
|
|
24
25
|
const riskCache = new Map<string, RiskLevel>();
|
|
25
26
|
|
|
26
|
-
function riskCacheKey(toolName: string, input: Record<string, unknown
|
|
27
|
+
function riskCacheKey(toolName: string, input: Record<string, unknown>, workingDir?: string, manifestOverride?: ManifestOverride): string {
|
|
27
28
|
const inputJson = JSON.stringify(input);
|
|
28
|
-
const hash = createHash('sha256')
|
|
29
|
+
const hash = createHash('sha256')
|
|
30
|
+
.update(inputJson)
|
|
31
|
+
.update('\0')
|
|
32
|
+
.update(workingDir ?? '')
|
|
33
|
+
.update('\0')
|
|
34
|
+
.update(manifestOverride ? JSON.stringify(manifestOverride) : '')
|
|
35
|
+
.digest('hex');
|
|
29
36
|
return `${toolName}\0${hash}`;
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -305,11 +312,11 @@ async function buildCommandCandidates(toolName: string, input: Record<string, un
|
|
|
305
312
|
}
|
|
306
313
|
|
|
307
314
|
export async function classifyRisk(toolName: string, input: Record<string, unknown>, workingDir?: string, preParsed?: ParsedCommand, manifestOverride?: ManifestOverride, signal?: AbortSignal): Promise<RiskLevel> {
|
|
308
|
-
|
|
315
|
+
signal?.throwIfAborted();
|
|
309
316
|
|
|
310
317
|
// Check cache first (skip when preParsed is provided since caller already
|
|
311
318
|
// parsed and we'd just be duplicating the key computation cost).
|
|
312
|
-
const cacheKey = preParsed ? null : riskCacheKey(toolName, input);
|
|
319
|
+
const cacheKey = preParsed ? null : riskCacheKey(toolName, input, workingDir, manifestOverride);
|
|
313
320
|
if (cacheKey) {
|
|
314
321
|
const cached = riskCache.get(cacheKey);
|
|
315
322
|
if (cached !== undefined) {
|
|
@@ -466,7 +473,7 @@ export async function check(
|
|
|
466
473
|
manifestOverride?: ManifestOverride,
|
|
467
474
|
signal?: AbortSignal,
|
|
468
475
|
): Promise<PermissionCheckResult> {
|
|
469
|
-
|
|
476
|
+
signal?.throwIfAborted();
|
|
470
477
|
|
|
471
478
|
// For shell tools, parse once and share the result to avoid duplicate tree-sitter work.
|
|
472
479
|
let shellParsed: ParsedCommand | undefined;
|
|
@@ -608,7 +615,7 @@ function friendlyHostname(url: URL): string {
|
|
|
608
615
|
}
|
|
609
616
|
|
|
610
617
|
export async function generateAllowlistOptions(toolName: string, input: Record<string, unknown>, signal?: AbortSignal): Promise<AllowlistOption[]> {
|
|
611
|
-
|
|
618
|
+
signal?.throwIfAborted();
|
|
612
619
|
if (toolName === 'bash' || toolName === 'host_bash') {
|
|
613
620
|
const command = ((input.command as string) ?? '').trim();
|
|
614
621
|
return buildShellAllowlistOptions(command);
|
|
@@ -123,7 +123,9 @@ export class AssistantEventHub {
|
|
|
123
123
|
for (const entry of snapshot) {
|
|
124
124
|
if (!entry.active) continue;
|
|
125
125
|
if (entry.filter.assistantId !== event.assistantId) continue;
|
|
126
|
-
|
|
126
|
+
// System events (no sessionId) match all subscribers; scoped events
|
|
127
|
+
// must match the subscriber's sessionId filter when present.
|
|
128
|
+
if (event.sessionId != null && entry.filter.sessionId != null && entry.filter.sessionId !== event.sessionId) continue;
|
|
127
129
|
try {
|
|
128
130
|
await entry.callback(event);
|
|
129
131
|
} catch (err) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optional HTTP server that exposes the canonical runtime API.
|
|
3
3
|
*
|
|
4
|
-
* Runs in the same process as the daemon.
|
|
5
|
-
*
|
|
4
|
+
* Runs in the same process as the daemon. Always started on the
|
|
5
|
+
* configured port (default: 7821).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -77,6 +77,9 @@ import type { BrowserRelayWebSocketData } from '../browser-extension-relay/serve
|
|
|
77
77
|
import { handleSubscribeAssistantEvents } from './routes/events-routes.js';
|
|
78
78
|
import { consumeCallback, consumeCallbackError } from '../security/oauth-callback-registry.js';
|
|
79
79
|
import { PairingStore } from '../daemon/pairing-store.js';
|
|
80
|
+
import type { ServerMessage } from '../daemon/ipc-contract.js';
|
|
81
|
+
import { assistantEventHub } from './assistant-event-hub.js';
|
|
82
|
+
import { buildAssistantEvent } from './assistant-event.js';
|
|
80
83
|
|
|
81
84
|
// Middleware
|
|
82
85
|
import {
|
|
@@ -152,7 +155,7 @@ export class RuntimeHttpServer {
|
|
|
152
155
|
private retrySweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
153
156
|
private sweepInProgress = false;
|
|
154
157
|
private pairingStore = new PairingStore();
|
|
155
|
-
private pairingBroadcast?: (msg:
|
|
158
|
+
private pairingBroadcast?: (msg: ServerMessage) => void;
|
|
156
159
|
|
|
157
160
|
constructor(options: RuntimeHttpServerOptions = {}) {
|
|
158
161
|
this.port = options.port ?? DEFAULT_PORT;
|
|
@@ -177,15 +180,24 @@ export class RuntimeHttpServer {
|
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
/** Set a callback for broadcasting IPC messages (wired by daemon server). */
|
|
180
|
-
setPairingBroadcast(fn: (msg:
|
|
183
|
+
setPairingBroadcast(fn: (msg: ServerMessage) => void): void {
|
|
181
184
|
this.pairingBroadcast = fn;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
private get pairingContext(): PairingHandlerContext {
|
|
188
|
+
const ipcBroadcast = this.pairingBroadcast;
|
|
185
189
|
return {
|
|
186
190
|
pairingStore: this.pairingStore,
|
|
187
191
|
bearerToken: this.bearerToken,
|
|
188
|
-
pairingBroadcast:
|
|
192
|
+
pairingBroadcast: ipcBroadcast
|
|
193
|
+
? (msg) => {
|
|
194
|
+
// Broadcast to IPC socket clients (local Unix socket)
|
|
195
|
+
ipcBroadcast(msg);
|
|
196
|
+
// Also publish to the event hub so HTTP/SSE clients (e.g. macOS
|
|
197
|
+
// app with localHttpEnabled) receive pairing approval requests.
|
|
198
|
+
void assistantEventHub.publish(buildAssistantEvent('self', msg));
|
|
199
|
+
}
|
|
200
|
+
: undefined,
|
|
189
201
|
};
|
|
190
202
|
}
|
|
191
203
|
|
|
@@ -9,13 +9,14 @@ import {
|
|
|
9
9
|
refreshDevice,
|
|
10
10
|
hashDeviceId,
|
|
11
11
|
} from '../../daemon/approved-devices-store.js';
|
|
12
|
+
import type { ServerMessage } from '../../daemon/ipc-contract.js';
|
|
12
13
|
|
|
13
14
|
const log = getLogger('runtime-http');
|
|
14
15
|
|
|
15
16
|
export interface PairingHandlerContext {
|
|
16
17
|
pairingStore: PairingStore;
|
|
17
18
|
bearerToken: string | undefined;
|
|
18
|
-
pairingBroadcast?: (msg:
|
|
19
|
+
pairingBroadcast?: (msg: ServerMessage) => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -320,7 +320,10 @@ export async function executeBrowserNavigate(
|
|
|
320
320
|
if (challenge?.type === 'captcha') {
|
|
321
321
|
log.info('CAPTCHA detected, waiting up to 5s for auto-resolve');
|
|
322
322
|
for (let i = 0; i < 5; i++) {
|
|
323
|
-
if (context.signal?.aborted)
|
|
323
|
+
if (context.signal?.aborted) {
|
|
324
|
+
if (sender) updateBrowserStatus(context.sessionId, sender, 'idle');
|
|
325
|
+
return { content: 'Navigation cancelled.', isError: true };
|
|
326
|
+
}
|
|
324
327
|
await new Promise((r) => setTimeout(r, 1000));
|
|
325
328
|
const still = await detectCaptchaChallenge(page);
|
|
326
329
|
if (!still) {
|
package/src/tools/executor.ts
CHANGED
|
@@ -699,15 +699,18 @@ export class ToolExecutor {
|
|
|
699
699
|
} catch (err) {
|
|
700
700
|
const durationMs = Date.now() - startTime;
|
|
701
701
|
const msg = err instanceof Error ? err.message : String(err);
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
702
|
+
const isAbort = err instanceof Error && err.name === 'AbortError';
|
|
703
|
+
const isExpected = isAbort || err instanceof PermissionDeniedError || err instanceof ToolError || err instanceof TokenExpiredError;
|
|
704
|
+
|
|
705
|
+
const errorCategory = isAbort
|
|
706
|
+
? 'tool_failure' as const
|
|
707
|
+
: err instanceof PermissionDeniedError
|
|
708
|
+
? 'permission_denied' as const
|
|
709
|
+
: err instanceof TokenExpiredError
|
|
710
|
+
? 'auth' as const
|
|
711
|
+
: err instanceof ToolError
|
|
712
|
+
? 'tool_failure' as const
|
|
713
|
+
: 'unexpected' as const;
|
|
711
714
|
|
|
712
715
|
emitLifecycleEvent(context, {
|
|
713
716
|
type: 'error',
|
package/src/util/logger.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { Writable } from 'node:stream';
|
|
4
4
|
import pino from 'pino';
|
|
@@ -67,6 +67,8 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
|
67
67
|
const today = formatDate(new Date());
|
|
68
68
|
const filePath = logFilePathForDate(config.dir, new Date());
|
|
69
69
|
const fileStream = pino.destination({ dest: filePath, sync: false, mkdir: true, mode: 0o600 });
|
|
70
|
+
// Tighten permissions on pre-existing log files that may have been created with looser modes
|
|
71
|
+
try { chmodSync(filePath, 0o600); } catch { /* best-effort */ }
|
|
70
72
|
|
|
71
73
|
activeLogDate = today;
|
|
72
74
|
activeLogFileConfig = config;
|
|
@@ -130,7 +132,10 @@ function getRootLogger(): pino.Logger {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
try {
|
|
133
|
-
const
|
|
135
|
+
const logPath = getLogPath();
|
|
136
|
+
const fileStream = pino.destination({ dest: logPath, sync: false, mkdir: true, mode: 0o600 });
|
|
137
|
+
// Tighten permissions on pre-existing log files that may have been created with looser modes
|
|
138
|
+
try { chmodSync(logPath, 0o600); } catch { /* best-effort */ }
|
|
134
139
|
|
|
135
140
|
if (getDebugMode()) {
|
|
136
141
|
const prettyStream = pinoPretty({ destination: 2 });
|