@vellumai/assistant 0.3.16 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +74 -13
- package/README.md +6 -0
- package/docs/architecture/http-token-refresh.md +23 -1
- package/docs/architecture/security.md +80 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -0
- package/src/__tests__/access-request-decision.test.ts +4 -7
- package/src/__tests__/call-controller.test.ts +170 -0
- package/src/__tests__/channel-guardian.test.ts +3 -1
- package/src/__tests__/checker.test.ts +139 -48
- package/src/__tests__/config-watcher.test.ts +11 -13
- package/src/__tests__/conversation-pairing.test.ts +103 -3
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -1
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +511 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +131 -0
- package/src/__tests__/guardian-action-store.test.ts +182 -0
- package/src/__tests__/guardian-dispatch.test.ts +180 -0
- package/src/__tests__/guardian-grant-minting.test.ts +543 -0
- package/src/__tests__/ipc-snapshot.test.ts +22 -0
- package/src/__tests__/non-member-access-request.test.ts +1 -2
- package/src/__tests__/notification-broadcaster.test.ts +115 -4
- package/src/__tests__/notification-decision-strategy.test.ts +2 -1
- package/src/__tests__/notification-deep-link.test.ts +44 -1
- package/src/__tests__/notification-guardian-path.test.ts +157 -0
- package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
- package/src/__tests__/remote-skill-policy.test.ts +215 -0
- package/src/__tests__/scoped-approval-grants.test.ts +521 -0
- package/src/__tests__/scoped-grant-security-matrix.test.ts +443 -0
- package/src/__tests__/slack-channel-config.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +23 -21
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +5 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
- package/src/__tests__/trusted-contact-verification.test.ts +9 -9
- package/src/__tests__/update-bulletin-state.test.ts +1 -1
- package/src/__tests__/update-bulletin.test.ts +66 -3
- package/src/__tests__/update-template-contract.test.ts +6 -11
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +571 -0
- package/src/__tests__/voice-session-bridge.test.ts +109 -9
- package/src/calls/call-controller.ts +150 -8
- package/src/calls/call-domain.ts +12 -0
- package/src/calls/guardian-action-sweep.ts +1 -1
- package/src/calls/guardian-dispatch.ts +16 -0
- package/src/calls/relay-server.ts +13 -0
- package/src/calls/voice-session-bridge.ts +46 -5
- package/src/cli/core-commands.ts +41 -1
- package/src/config/bundled-skills/notifications/SKILL.md +18 -0
- package/src/config/schema.ts +6 -0
- package/src/config/skills-schema.ts +27 -0
- package/src/config/templates/UPDATES.md +5 -6
- package/src/config/update-bulletin-format.ts +2 -0
- package/src/config/update-bulletin-state.ts +1 -1
- package/src/config/update-bulletin-template-path.ts +6 -0
- package/src/config/update-bulletin.ts +21 -6
- package/src/daemon/config-watcher.ts +3 -2
- package/src/daemon/daemon-control.ts +64 -10
- package/src/daemon/handlers/config-channels.ts +18 -0
- package/src/daemon/handlers/config-slack-channel.ts +1 -1
- package/src/daemon/handlers/identity.ts +45 -25
- package/src/daemon/handlers/sessions.ts +1 -1
- package/src/daemon/handlers/skills.ts +45 -2
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/skills.ts +1 -0
- package/src/daemon/ipc-contract/workspace.ts +12 -1
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +8 -0
- package/src/daemon/server.ts +25 -3
- package/src/daemon/session-process.ts +450 -184
- package/src/daemon/tls-certs.ts +17 -12
- package/src/daemon/tool-side-effects.ts +1 -1
- package/src/memory/channel-delivery-store.ts +18 -20
- package/src/memory/channel-guardian-store.ts +39 -42
- package/src/memory/conversation-crud.ts +2 -2
- package/src/memory/conversation-queries.ts +2 -2
- package/src/memory/conversation-store.ts +24 -25
- package/src/memory/db-init.ts +17 -1
- package/src/memory/embedding-local.ts +16 -7
- package/src/memory/fts-reconciler.ts +41 -26
- package/src/memory/guardian-action-store.ts +65 -7
- package/src/memory/guardian-verification.ts +1 -0
- package/src/memory/jobs-worker.ts +2 -2
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
- package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
- package/src/memory/migrations/index.ts +6 -2
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +36 -1
- package/src/memory/scoped-approval-grants.ts +509 -0
- package/src/memory/search/semantic.ts +3 -3
- package/src/notifications/README.md +158 -17
- package/src/notifications/broadcaster.ts +68 -50
- package/src/notifications/conversation-pairing.ts +96 -18
- package/src/notifications/decision-engine.ts +6 -3
- package/src/notifications/deliveries-store.ts +12 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/thread-candidates.ts +60 -25
- package/src/notifications/types.ts +2 -1
- package/src/permissions/checker.ts +28 -16
- package/src/permissions/defaults.ts +14 -4
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-action-grant-minter.ts +97 -0
- package/src/runtime/http-server.ts +11 -11
- package/src/runtime/routes/access-request-decision.ts +1 -1
- package/src/runtime/routes/debug-routes.ts +4 -4
- package/src/runtime/routes/guardian-approval-interception.ts +120 -4
- package/src/runtime/routes/inbound-message-handler.ts +100 -33
- package/src/runtime/routes/integration-routes.ts +2 -2
- package/src/security/tool-approval-digest.ts +67 -0
- package/src/skills/remote-skill-policy.ts +131 -0
- package/src/tools/permission-checker.ts +1 -2
- package/src/tools/secret-detection-handler.ts +1 -1
- package/src/tools/system/voice-config.ts +1 -1
- package/src/version.ts +29 -2
|
@@ -28,6 +28,9 @@ export interface NotificationDeliveryRow {
|
|
|
28
28
|
conversationId: string | null;
|
|
29
29
|
messageId: string | null;
|
|
30
30
|
conversationStrategy: string | null;
|
|
31
|
+
threadAction: string | null;
|
|
32
|
+
threadTargetConversationId: string | null;
|
|
33
|
+
threadDecisionFallbackUsed: number | null;
|
|
31
34
|
clientDeliveryStatus: string | null;
|
|
32
35
|
clientDeliveryError: string | null;
|
|
33
36
|
clientDeliveryAt: number | null;
|
|
@@ -52,6 +55,9 @@ function rowToDelivery(row: typeof notificationDeliveries.$inferSelect): Notific
|
|
|
52
55
|
conversationId: row.conversationId,
|
|
53
56
|
messageId: row.messageId,
|
|
54
57
|
conversationStrategy: row.conversationStrategy,
|
|
58
|
+
threadAction: row.threadAction,
|
|
59
|
+
threadTargetConversationId: row.threadTargetConversationId,
|
|
60
|
+
threadDecisionFallbackUsed: row.threadDecisionFallbackUsed,
|
|
55
61
|
clientDeliveryStatus: row.clientDeliveryStatus,
|
|
56
62
|
clientDeliveryError: row.clientDeliveryError,
|
|
57
63
|
clientDeliveryAt: row.clientDeliveryAt,
|
|
@@ -76,6 +82,9 @@ export interface CreateDeliveryParams {
|
|
|
76
82
|
conversationId?: string;
|
|
77
83
|
messageId?: string;
|
|
78
84
|
conversationStrategy?: string;
|
|
85
|
+
threadAction?: string;
|
|
86
|
+
threadTargetConversationId?: string;
|
|
87
|
+
threadDecisionFallbackUsed?: boolean;
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
/** Create a new delivery audit record. */
|
|
@@ -99,6 +108,9 @@ export function createDelivery(params: CreateDeliveryParams): NotificationDelive
|
|
|
99
108
|
conversationId: params.conversationId ?? null,
|
|
100
109
|
messageId: params.messageId ?? null,
|
|
101
110
|
conversationStrategy: params.conversationStrategy ?? null,
|
|
111
|
+
threadAction: params.threadAction ?? null,
|
|
112
|
+
threadTargetConversationId: params.threadTargetConversationId ?? null,
|
|
113
|
+
threadDecisionFallbackUsed: params.threadDecisionFallbackUsed != null ? (params.threadDecisionFallbackUsed ? 1 : 0) : null,
|
|
102
114
|
clientDeliveryStatus: null,
|
|
103
115
|
clientDeliveryError: null,
|
|
104
116
|
clientDeliveryAt: null,
|
|
@@ -205,6 +205,7 @@ export async function emitNotificationSignal(params: EmitSignalParams): Promise<
|
|
|
205
205
|
|
|
206
206
|
// Step 2: Evaluate the signal through the decision engine
|
|
207
207
|
const connectedChannels = getConnectedChannels(assistantId);
|
|
208
|
+
|
|
208
209
|
let decision = await evaluateSignal(signal, connectedChannels);
|
|
209
210
|
|
|
210
211
|
// Step 2.5: Enforce routing intent policy (fire-time guard)
|
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
* needs for a routing decision, not full conversation contents.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { and, desc, eq, isNotNull } from 'drizzle-orm';
|
|
13
|
+
import { and, count, desc, eq, inArray, isNotNull } from 'drizzle-orm';
|
|
14
14
|
|
|
15
15
|
import { getDb } from '../memory/db.js';
|
|
16
|
-
import {
|
|
17
|
-
import { conversations, notificationDeliveries, notificationDecisions, notificationEvents } from '../memory/schema.js';
|
|
16
|
+
import { channelGuardianApprovalRequests, conversations, notificationDecisions, notificationDeliveries, notificationEvents } from '../memory/schema.js';
|
|
18
17
|
import { getLogger } from '../util/logger.js';
|
|
19
18
|
import type { NotificationChannel } from './types.js';
|
|
20
19
|
|
|
@@ -148,49 +147,78 @@ function buildCandidatesForChannel(
|
|
|
148
147
|
|
|
149
148
|
seen.add(row.conversationId);
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
candidates.push({
|
|
152
151
|
conversationId: row.conversationId,
|
|
153
152
|
title: row.convTitle,
|
|
154
153
|
updatedAt: row.convUpdatedAt,
|
|
155
154
|
latestSourceEventName: row.sourceEventName ?? null,
|
|
156
155
|
channel: channel,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Enrich with guardian context
|
|
160
|
-
const guardianContext = buildGuardianContext(row.conversationId, assistantId);
|
|
161
|
-
if (guardianContext) {
|
|
162
|
-
candidate.guardianContext = guardianContext;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
candidates.push(candidate);
|
|
156
|
+
});
|
|
166
157
|
|
|
167
158
|
if (candidates.length >= MAX_CANDIDATES_PER_CHANNEL) break;
|
|
168
159
|
}
|
|
169
160
|
|
|
161
|
+
// Batch-enrich all candidates with guardian context in a single query
|
|
162
|
+
if (candidates.length > 0) {
|
|
163
|
+
const pendingCounts = batchCountPendingByConversation(
|
|
164
|
+
candidates.map((c) => c.conversationId),
|
|
165
|
+
assistantId,
|
|
166
|
+
);
|
|
167
|
+
for (const candidate of candidates) {
|
|
168
|
+
const pendingCount = pendingCounts.get(candidate.conversationId) ?? 0;
|
|
169
|
+
if (pendingCount > 0) {
|
|
170
|
+
candidate.guardianContext = { pendingUnresolvedRequestCount: pendingCount };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
170
175
|
return candidates;
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
// -- Guardian context enrichment ----------------------------------------------
|
|
174
179
|
|
|
175
180
|
/**
|
|
176
|
-
*
|
|
177
|
-
* Returns
|
|
181
|
+
* Batch-count pending guardian approval requests for multiple conversations
|
|
182
|
+
* in a single query. Returns a map from conversationId to pending count
|
|
183
|
+
* (only entries with count > 0 are included).
|
|
178
184
|
*/
|
|
179
|
-
function
|
|
180
|
-
|
|
185
|
+
function batchCountPendingByConversation(
|
|
186
|
+
conversationIds: string[],
|
|
181
187
|
assistantId: string,
|
|
182
|
-
):
|
|
188
|
+
): Map<string, number> {
|
|
189
|
+
const result = new Map<string, number>();
|
|
190
|
+
if (conversationIds.length === 0) return result;
|
|
191
|
+
|
|
183
192
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
const db = getDb();
|
|
194
|
+
|
|
195
|
+
const rows = db
|
|
196
|
+
.select({
|
|
197
|
+
conversationId: channelGuardianApprovalRequests.conversationId,
|
|
198
|
+
count: count(),
|
|
199
|
+
})
|
|
200
|
+
.from(channelGuardianApprovalRequests)
|
|
201
|
+
.where(
|
|
202
|
+
and(
|
|
203
|
+
inArray(channelGuardianApprovalRequests.conversationId, conversationIds),
|
|
204
|
+
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
205
|
+
eq(channelGuardianApprovalRequests.assistantId, assistantId),
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
.groupBy(channelGuardianApprovalRequests.conversationId)
|
|
209
|
+
.all();
|
|
210
|
+
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
if (row.count > 0) {
|
|
213
|
+
result.set(row.conversationId, row.count);
|
|
214
|
+
}
|
|
187
215
|
}
|
|
188
216
|
} catch (err) {
|
|
189
217
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
190
|
-
log.warn({ err: errMsg
|
|
218
|
+
log.warn({ err: errMsg }, 'Failed to batch-query guardian context for candidates');
|
|
191
219
|
}
|
|
192
220
|
|
|
193
|
-
return
|
|
221
|
+
return result;
|
|
194
222
|
}
|
|
195
223
|
|
|
196
224
|
// -- Prompt serialization -----------------------------------------------------
|
|
@@ -213,13 +241,20 @@ export function serializeCandidatesForPrompt(candidateSet: ThreadCandidateSet):
|
|
|
213
241
|
|
|
214
242
|
const lines: string[] = [`Channel: ${channel}`];
|
|
215
243
|
for (const c of candidates) {
|
|
244
|
+
// Escape title to prevent format corruption from quotes or newlines in
|
|
245
|
+
// user/model-provided text. JSON.stringify produces a safe single-line
|
|
246
|
+
// quoted string; we strip the outer quotes since we wrap in our own.
|
|
247
|
+
const safeTitle = c.title
|
|
248
|
+
? JSON.stringify(c.title).slice(1, -1)
|
|
249
|
+
: '(untitled)';
|
|
216
250
|
const parts: string[] = [
|
|
217
251
|
` - id=${c.conversationId}`,
|
|
218
|
-
`title="${
|
|
252
|
+
`title="${safeTitle}"`,
|
|
219
253
|
`updated=${new Date(c.updatedAt).toISOString()}`,
|
|
220
254
|
];
|
|
221
255
|
if (c.latestSourceEventName) {
|
|
222
|
-
|
|
256
|
+
const safeEventName = JSON.stringify(c.latestSourceEventName).slice(1, -1);
|
|
257
|
+
parts.push(`lastEvent="${safeEventName}"`);
|
|
223
258
|
}
|
|
224
259
|
if (c.guardianContext) {
|
|
225
260
|
parts.push(`pendingRequests=${c.guardianContext.pendingUnresolvedRequestCount}`);
|
|
@@ -95,13 +95,14 @@ export interface ThreadActionReuseExisting {
|
|
|
95
95
|
/** Per-channel thread action — either start a new thread or reuse an existing one. */
|
|
96
96
|
export type ThreadAction = ThreadActionStartNew | ThreadActionReuseExisting;
|
|
97
97
|
|
|
98
|
+
|
|
98
99
|
/** Output produced by the notification decision engine for a given signal. */
|
|
99
100
|
export interface NotificationDecision {
|
|
100
101
|
shouldNotify: boolean;
|
|
101
102
|
selectedChannels: NotificationChannel[];
|
|
102
103
|
reasoningSummary: string;
|
|
103
104
|
renderedCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>>;
|
|
104
|
-
/** Per-channel thread
|
|
105
|
+
/** Per-channel thread actions decided by the model. Absent channels default to start_new. */
|
|
105
106
|
threadActions?: Partial<Record<NotificationChannel, ThreadAction>>;
|
|
106
107
|
deepLinkTarget?: Record<string, unknown>;
|
|
107
108
|
dedupeKey: string;
|
|
@@ -101,6 +101,19 @@ const WRAPPER_PROGRAMS = new Set([
|
|
|
101
101
|
// value of -u) as the wrapped program instead of `echo`.
|
|
102
102
|
const ENV_VALUE_FLAGS = new Set(['-u', '--unset', '-C', '--chdir']);
|
|
103
103
|
|
|
104
|
+
// Bare filenames that `rm` is allowed to delete at Medium risk (instead of
|
|
105
|
+
// High) so workspace-scoped allow rules can approve them without the
|
|
106
|
+
// dangerous `allowHighRisk` flag. Only matches when the args contain no
|
|
107
|
+
// flags and exactly one of these filenames.
|
|
108
|
+
const RM_SAFE_BARE_FILES = new Set(['BOOTSTRAP.md', 'UPDATES.md']);
|
|
109
|
+
|
|
110
|
+
function isRmOfKnownSafeFile(args: string[]): boolean {
|
|
111
|
+
if (args.length !== 1) return false;
|
|
112
|
+
const target = args[0];
|
|
113
|
+
if (target.startsWith('-') || target.includes('/')) return false;
|
|
114
|
+
return RM_SAFE_BARE_FILES.has(target);
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
/**
|
|
105
118
|
* Given a segment whose program is a known wrapper, return the first
|
|
106
119
|
* non-flag argument (i.e. the wrapped program name). Returns `undefined`
|
|
@@ -123,19 +136,6 @@ function getWrappedProgram(seg: { program: string; args: string[] }): string | u
|
|
|
123
136
|
return undefined;
|
|
124
137
|
}
|
|
125
138
|
|
|
126
|
-
function isHighRiskRm(args: string[]): boolean {
|
|
127
|
-
// rm with -r, -rf, -fr, or targeting root/home
|
|
128
|
-
for (const arg of args) {
|
|
129
|
-
if (arg.startsWith('-') && (arg.includes('r') || arg.includes('f'))) {
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
if (arg === '/' || arg === '~' || arg === '$HOME') {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
139
|
function getStringField(input: Record<string, unknown>, ...keys: string[]): string {
|
|
140
140
|
for (const key of keys) {
|
|
141
141
|
const value = input[key];
|
|
@@ -398,9 +398,14 @@ async function classifyRiskUncached(toolName: string, input: Record<string, unkn
|
|
|
398
398
|
if (HIGH_RISK_PROGRAMS.has(prog)) return RiskLevel.High;
|
|
399
399
|
|
|
400
400
|
if (prog === 'rm') {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
// `rm` of known safe workspace files (no flags, bare filename) is
|
|
402
|
+
// Medium rather than High so scope-limited allow rules can approve
|
|
403
|
+
// it without needing allowHighRisk, which would bypass path checks.
|
|
404
|
+
if (isRmOfKnownSafeFile(seg.args)) {
|
|
405
|
+
maxRisk = RiskLevel.Medium;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
return RiskLevel.High;
|
|
404
409
|
}
|
|
405
410
|
|
|
406
411
|
if (prog === 'chmod' || prog === 'chown' || prog === 'chgrp'
|
|
@@ -417,7 +422,14 @@ async function classifyRiskUncached(toolName: string, input: Record<string, unkn
|
|
|
417
422
|
}
|
|
418
423
|
|
|
419
424
|
if (WRAPPER_PROGRAMS.has(prog)) {
|
|
425
|
+
// `command -v` and `command -V` are read-only lookups (print where
|
|
426
|
+
// a command lives) — don't escalate to high risk for those.
|
|
427
|
+
if (prog === 'command' && seg.args.length > 0 && (seg.args[0] === '-v' || seg.args[0] === '-V')) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
420
430
|
const wrapped = getWrappedProgram(seg);
|
|
431
|
+
if (wrapped === 'rm') return RiskLevel.High;
|
|
432
|
+
if (wrapped && HIGH_RISK_PROGRAMS.has(wrapped)) return RiskLevel.High;
|
|
421
433
|
if (wrapped === 'curl' || wrapped === 'wget') {
|
|
422
434
|
maxRisk = RiskLevel.Medium;
|
|
423
435
|
continue;
|
|
@@ -37,6 +37,13 @@ const COMPUTER_USE_TOOLS = [
|
|
|
37
37
|
* Computed at runtime so paths reflect the configured root directory.
|
|
38
38
|
*/
|
|
39
39
|
export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
40
|
+
// Some test suites mock getConfig() with partial objects; treat missing
|
|
41
|
+
// branches as defaults so rule generation remains deterministic.
|
|
42
|
+
const config = getConfig() as {
|
|
43
|
+
sandbox?: { enabled?: boolean };
|
|
44
|
+
skills?: { load?: { extraDirs?: unknown } };
|
|
45
|
+
};
|
|
46
|
+
|
|
40
47
|
const hostFileRules = HOST_FILE_TOOLS.map((tool) => ({
|
|
41
48
|
id: `default:ask-${tool}-global`,
|
|
42
49
|
tool,
|
|
@@ -50,11 +57,11 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
50
57
|
// global default ask rule uses "**" (globstar) instead of a "tool:*" prefix
|
|
51
58
|
// because commands often contain "/" (e.g. "cat /etc/hosts").
|
|
52
59
|
const hostShellRule: DefaultRuleTemplate = {
|
|
53
|
-
id: 'default:
|
|
60
|
+
id: 'default:allow-host_bash-global',
|
|
54
61
|
tool: 'host_bash',
|
|
55
62
|
pattern: '**',
|
|
56
63
|
scope: 'everywhere',
|
|
57
|
-
decision: '
|
|
64
|
+
decision: 'allow',
|
|
58
65
|
priority: 50,
|
|
59
66
|
};
|
|
60
67
|
|
|
@@ -62,7 +69,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
62
69
|
// them (including high-risk) so the user is never prompted for sandbox work.
|
|
63
70
|
// Only emit this rule when the sandbox is actually enabled; otherwise bash
|
|
64
71
|
// commands execute on the host and must go through normal permission checks.
|
|
65
|
-
const sandboxEnabled =
|
|
72
|
+
const sandboxEnabled = config.sandbox?.enabled !== false;
|
|
66
73
|
const sandboxShellRule: DefaultRuleTemplate | null = sandboxEnabled
|
|
67
74
|
? {
|
|
68
75
|
id: 'default:allow-bash-global',
|
|
@@ -149,7 +156,10 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
149
156
|
|
|
150
157
|
// Append any user-configured extra skill directories so they get the
|
|
151
158
|
// same default ask rules as managed and bundled dirs.
|
|
152
|
-
const
|
|
159
|
+
const rawExtraDirs = config.skills?.load?.extraDirs;
|
|
160
|
+
const extraDirs = Array.isArray(rawExtraDirs)
|
|
161
|
+
? rawExtraDirs.filter((dir): dir is string => typeof dir === 'string')
|
|
162
|
+
: [];
|
|
153
163
|
for (let i = 0; i < extraDirs.length; i++) {
|
|
154
164
|
skillDirs.push({ dir: extraDirs[i].replaceAll('\\', '/'), label: `extra-${i}` });
|
|
155
165
|
}
|
|
@@ -24,8 +24,8 @@ import { getGatewayInternalBaseUrl } from '../config/env.js';
|
|
|
24
24
|
import { getOrCreateConversation } from '../memory/conversation-key-store.js';
|
|
25
25
|
import {
|
|
26
26
|
finalizeFollowup,
|
|
27
|
-
getGuardianActionRequest,
|
|
28
27
|
type FollowupAction,
|
|
28
|
+
getGuardianActionRequest,
|
|
29
29
|
type GuardianActionRequest,
|
|
30
30
|
} from '../memory/guardian-action-store.js';
|
|
31
31
|
import { getLogger } from '../util/logger.js';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for minting scoped approval grants when a guardian-action
|
|
3
|
+
* request is resolved with tool metadata.
|
|
4
|
+
*
|
|
5
|
+
* Used by both the channel inbound path (inbound-message-handler.ts) and
|
|
6
|
+
* the desktop/IPC path (session-process.ts) to ensure grants are minted
|
|
7
|
+
* consistently regardless of which channel the guardian answers on.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { GuardianActionRequest } from '../memory/guardian-action-store.js';
|
|
11
|
+
import { createScopedApprovalGrant } from '../memory/scoped-approval-grants.js';
|
|
12
|
+
import { getLogger } from '../util/logger.js';
|
|
13
|
+
import { parseApprovalDecision } from './channel-approval-parser.js';
|
|
14
|
+
|
|
15
|
+
const log = getLogger('guardian-action-grant-minter');
|
|
16
|
+
|
|
17
|
+
/** TTL for scoped approval grants minted on guardian-action answer resolution. */
|
|
18
|
+
export const GUARDIAN_ACTION_GRANT_TTL_MS = 5 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mint a `tool_signature` scoped grant when a guardian-action request is
|
|
22
|
+
* resolved and the request carries tool metadata (toolName + inputDigest).
|
|
23
|
+
*
|
|
24
|
+
* Skips silently when:
|
|
25
|
+
* - The resolved request has no toolName/inputDigest (informational consult).
|
|
26
|
+
* - The guardian's answer is not an explicit approval (fail-closed).
|
|
27
|
+
*
|
|
28
|
+
* Fails silently on error -- grant minting is best-effort and must never
|
|
29
|
+
* block the guardian-action answer flow.
|
|
30
|
+
*/
|
|
31
|
+
export function tryMintGuardianActionGrant(params: {
|
|
32
|
+
resolvedRequest: GuardianActionRequest;
|
|
33
|
+
answerText: string;
|
|
34
|
+
decisionChannel: string;
|
|
35
|
+
guardianExternalUserId?: string;
|
|
36
|
+
}): void {
|
|
37
|
+
const { resolvedRequest, answerText, decisionChannel, guardianExternalUserId } = params;
|
|
38
|
+
|
|
39
|
+
// Only mint for requests that carry tool metadata -- informational
|
|
40
|
+
// ASK_GUARDIAN consults without tool context do not produce grants.
|
|
41
|
+
if (!resolvedRequest.toolName || !resolvedRequest.inputDigest) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Gate on explicit affirmative guardian decisions (fail-closed).
|
|
46
|
+
// Only mint when the deterministic parser recognises an approval keyword
|
|
47
|
+
// ("yes", "approve", "allow", "go ahead", etc.). Unrecognised text
|
|
48
|
+
// (e.g. "nope", "don't do that") is treated as non-approval and skipped,
|
|
49
|
+
// preventing ambiguous answers from producing grants.
|
|
50
|
+
const decision = parseApprovalDecision(answerText);
|
|
51
|
+
if (decision?.action !== 'approve_once' && decision?.action !== 'approve_always') {
|
|
52
|
+
log.info(
|
|
53
|
+
{
|
|
54
|
+
event: 'guardian_action_grant_skipped_no_approval',
|
|
55
|
+
toolName: resolvedRequest.toolName,
|
|
56
|
+
requestId: resolvedRequest.id,
|
|
57
|
+
answerText,
|
|
58
|
+
parsedAction: decision?.action ?? null,
|
|
59
|
+
decisionChannel,
|
|
60
|
+
},
|
|
61
|
+
'Skipped grant minting: guardian answer not classified as explicit approval',
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
createScopedApprovalGrant({
|
|
68
|
+
assistantId: resolvedRequest.assistantId,
|
|
69
|
+
scopeMode: 'tool_signature',
|
|
70
|
+
toolName: resolvedRequest.toolName,
|
|
71
|
+
inputDigest: resolvedRequest.inputDigest,
|
|
72
|
+
requestChannel: resolvedRequest.sourceChannel,
|
|
73
|
+
decisionChannel,
|
|
74
|
+
executionChannel: null,
|
|
75
|
+
conversationId: resolvedRequest.sourceConversationId,
|
|
76
|
+
callSessionId: resolvedRequest.callSessionId,
|
|
77
|
+
guardianExternalUserId: guardianExternalUserId ?? null,
|
|
78
|
+
expiresAt: new Date(Date.now() + GUARDIAN_ACTION_GRANT_TTL_MS).toISOString(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
log.info(
|
|
82
|
+
{
|
|
83
|
+
event: 'guardian_action_grant_minted',
|
|
84
|
+
toolName: resolvedRequest.toolName,
|
|
85
|
+
requestId: resolvedRequest.id,
|
|
86
|
+
callSessionId: resolvedRequest.callSessionId,
|
|
87
|
+
decisionChannel,
|
|
88
|
+
},
|
|
89
|
+
'Minted scoped approval grant for guardian-action answer resolution',
|
|
90
|
+
);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log.error(
|
|
93
|
+
{ err, toolName: resolvedRequest.toolName, requestId: resolvedRequest.id },
|
|
94
|
+
'Failed to mint scoped approval grant for guardian-action (non-fatal)',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -85,7 +85,6 @@ import {
|
|
|
85
85
|
handleGetAttachmentContent,
|
|
86
86
|
handleUploadAttachment,
|
|
87
87
|
} from './routes/attachment-routes.js';
|
|
88
|
-
import { handleDebug } from './routes/debug-routes.js';
|
|
89
88
|
import {
|
|
90
89
|
handleAnswerCall,
|
|
91
90
|
handleCancelCall,
|
|
@@ -116,8 +115,19 @@ import {
|
|
|
116
115
|
handleSearchConversations,
|
|
117
116
|
handleSendMessage,
|
|
118
117
|
} from './routes/conversation-routes.js';
|
|
118
|
+
import { handleDebug } from './routes/debug-routes.js';
|
|
119
119
|
import { handleSubscribeAssistantEvents } from './routes/events-routes.js';
|
|
120
120
|
import { handleGetIdentity,handleHealth } from './routes/identity-routes.js';
|
|
121
|
+
import {
|
|
122
|
+
handleBlockMember,
|
|
123
|
+
handleCreateInvite,
|
|
124
|
+
handleListInvites,
|
|
125
|
+
handleListMembers,
|
|
126
|
+
handleRedeemInvite,
|
|
127
|
+
handleRevokeInvite,
|
|
128
|
+
handleRevokeMember,
|
|
129
|
+
handleUpsertMember,
|
|
130
|
+
} from './routes/ingress-routes.js';
|
|
121
131
|
import {
|
|
122
132
|
handleCancelOutbound,
|
|
123
133
|
handleClearSlackChannelConfig,
|
|
@@ -140,16 +150,6 @@ import {
|
|
|
140
150
|
handlePairingRequest,
|
|
141
151
|
handlePairingStatus,
|
|
142
152
|
} from './routes/pairing-routes.js';
|
|
143
|
-
import {
|
|
144
|
-
handleBlockMember,
|
|
145
|
-
handleCreateInvite,
|
|
146
|
-
handleListInvites,
|
|
147
|
-
handleListMembers,
|
|
148
|
-
handleRedeemInvite,
|
|
149
|
-
handleRevokeInvite,
|
|
150
|
-
handleRevokeMember,
|
|
151
|
-
handleUpsertMember,
|
|
152
|
-
} from './routes/ingress-routes.js';
|
|
153
153
|
import { handleAddSecret } from './routes/secret-routes.js';
|
|
154
154
|
|
|
155
155
|
// Re-export for consumers
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* instead of resuming an agent loop.
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
|
-
resolveApprovalRequest,
|
|
10
9
|
type GuardianApprovalRequest,
|
|
10
|
+
resolveApprovalRequest,
|
|
11
11
|
} from '../../memory/channel-guardian-store.js';
|
|
12
12
|
import { getLogger } from '../../util/logger.js';
|
|
13
13
|
import { createOutboundSession } from '../channel-guardian-service.js';
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { statSync } from 'node:fs';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { getConfig } from '../../config/loader.js';
|
|
8
8
|
import { countConversations } from '../../memory/conversation-store.js';
|
|
9
|
-
import { getMemoryJobCounts } from '../../memory/jobs-store.js';
|
|
10
|
-
import { countSchedules } from '../../schedule/schedule-store.js';
|
|
11
9
|
import { rawAll } from '../../memory/db.js';
|
|
12
|
-
import {
|
|
10
|
+
import { getMemoryJobCounts } from '../../memory/jobs-store.js';
|
|
13
11
|
import { getProviderDebugStatus } from '../../providers/registry.js';
|
|
12
|
+
import { countSchedules } from '../../schedule/schedule-store.js';
|
|
13
|
+
import { getDbPath } from '../../util/platform.js';
|
|
14
14
|
|
|
15
15
|
/** Process start time — used to calculate uptime. */
|
|
16
16
|
const startedAt = Date.now();
|