@vellumai/assistant 0.3.3 → 0.3.5
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/Dockerfile +2 -0
- package/README.md +45 -18
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +391 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +397 -135
- package/src/__tests__/channel-approvals.test.ts +99 -3
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +261 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +636 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +480 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +85 -22
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +24 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +40 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +58 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +819 -22
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +114 -4
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +111 -504
- package/src/daemon/session-agent-loop.ts +10 -15
- package/src/daemon/session-runtime-assembly.ts +115 -44
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +19 -2
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1163 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +11 -1
- package/src/memory/media-store.ts +759 -0
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +36 -2
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +99 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +26 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +29 -7
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +65 -28
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +237 -103
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +43 -3
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +10 -2
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +43 -1
- package/src/util/retry.ts +4 -4
|
@@ -5,6 +5,7 @@ import { getOrCreateConversation } from '../../memory/conversation-key-store.js'
|
|
|
5
5
|
import * as attachmentsStore from '../../memory/attachments-store.js';
|
|
6
6
|
import * as runsStore from '../../memory/runs-store.js';
|
|
7
7
|
import { addRule } from '../../permissions/trust-store.js';
|
|
8
|
+
import { getTool } from '../../tools/registry.js';
|
|
8
9
|
import { getLogger } from '../../util/logger.js';
|
|
9
10
|
import type { RunOrchestrator } from '../run-orchestrator.js';
|
|
10
11
|
|
|
@@ -200,8 +201,13 @@ export async function handleAddTrustRule(
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
try {
|
|
204
|
+
// Only persist executionTarget for skill-origin tools — core tools don't
|
|
205
|
+
// set it in their PolicyContext, so a persisted value would prevent the
|
|
206
|
+
// rule from ever matching on subsequent permission checks.
|
|
207
|
+
const tool = getTool(confirmation.toolName);
|
|
208
|
+
const executionTarget = tool?.origin === 'skill' ? confirmation.executionTarget : undefined;
|
|
203
209
|
addRule(confirmation.toolName, pattern, scope, decision, undefined, {
|
|
204
|
-
executionTarget
|
|
210
|
+
executionTarget,
|
|
205
211
|
});
|
|
206
212
|
log.info(
|
|
207
213
|
{ tool: confirmation.toolName, pattern, scope, decision, runId },
|
|
@@ -18,6 +18,7 @@ import type { Run } from '../memory/runs-store.js';
|
|
|
18
18
|
import type { Session } from '../daemon/session.js';
|
|
19
19
|
import type { ServerMessage } from '../daemon/ipc-protocol.js';
|
|
20
20
|
import { resolveChannelCapabilities } from '../daemon/session-runtime-assembly.js';
|
|
21
|
+
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
21
22
|
import type { UserDecision } from '../permissions/types.js';
|
|
22
23
|
import { checkIngressForSecrets } from '../security/secret-ingress.js';
|
|
23
24
|
import { IngressBlockedError } from '../util/errors.js';
|
|
@@ -37,7 +38,11 @@ interface PendingRunState {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export interface RunOrchestratorDeps {
|
|
40
|
-
getOrCreateSession: (conversationId: string
|
|
41
|
+
getOrCreateSession: (conversationId: string, transport?: {
|
|
42
|
+
channelId: string;
|
|
43
|
+
hints?: string[];
|
|
44
|
+
uxBrief?: string;
|
|
45
|
+
}) => Promise<Session>;
|
|
41
46
|
resolveAttachments: (attachmentIds: string[]) => Array<{
|
|
42
47
|
id: string;
|
|
43
48
|
filename: string;
|
|
@@ -67,6 +72,20 @@ export interface RunStartOptions {
|
|
|
67
72
|
* default 'http-api'.
|
|
68
73
|
*/
|
|
69
74
|
sourceChannel?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Transport hints from sourceMetadata (e.g. reply-context cues).
|
|
77
|
+
* Forwarded to the session so the agent loop can incorporate them.
|
|
78
|
+
*/
|
|
79
|
+
hints?: string[];
|
|
80
|
+
/**
|
|
81
|
+
* Brief UX context from sourceMetadata (e.g. UI surface description).
|
|
82
|
+
* Forwarded to the session so the agent loop can tailor its response.
|
|
83
|
+
*/
|
|
84
|
+
uxBrief?: string;
|
|
85
|
+
/** Assistant scope for multi-assistant channels. */
|
|
86
|
+
assistantId?: string;
|
|
87
|
+
/** Guardian trust/identity context for the inbound actor. */
|
|
88
|
+
guardianContext?: GuardianRuntimeContext;
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
// ---------------------------------------------------------------------------
|
|
@@ -104,7 +123,17 @@ export class RunOrchestrator {
|
|
|
104
123
|
throw new IngressBlockedError(ingressCheck.userNotice!, ingressCheck.detectedTypes);
|
|
105
124
|
}
|
|
106
125
|
|
|
107
|
-
|
|
126
|
+
// Build transport metadata when channel context is available so the
|
|
127
|
+
// session receives the same hints/uxBrief as the non-orchestrator path.
|
|
128
|
+
const transport = options?.sourceChannel
|
|
129
|
+
? {
|
|
130
|
+
channelId: options.sourceChannel,
|
|
131
|
+
hints: options.hints,
|
|
132
|
+
uxBrief: options.uxBrief,
|
|
133
|
+
}
|
|
134
|
+
: undefined;
|
|
135
|
+
|
|
136
|
+
const session = await this.deps.getOrCreateSession(conversationId, transport);
|
|
108
137
|
|
|
109
138
|
if (session.isProcessing()) {
|
|
110
139
|
throw new Error('Session is already processing a message');
|
|
@@ -121,6 +150,8 @@ export class RunOrchestrator {
|
|
|
121
150
|
...session.memoryPolicy,
|
|
122
151
|
strictSideEffects,
|
|
123
152
|
};
|
|
153
|
+
session.setAssistantId(options?.assistantId ?? 'self');
|
|
154
|
+
session.setGuardianContext(options?.guardianContext ?? null);
|
|
124
155
|
|
|
125
156
|
const attachments = attachmentIds
|
|
126
157
|
? this.deps.resolveAttachments(attachmentIds)
|
|
@@ -201,6 +232,8 @@ export class RunOrchestrator {
|
|
|
201
232
|
// Reset channel capabilities so a subsequent IPC/desktop session on the
|
|
202
233
|
// same conversation is not incorrectly treated as an HTTP-API client.
|
|
203
234
|
session.setChannelCapabilities(null);
|
|
235
|
+
session.setGuardianContext(null);
|
|
236
|
+
session.setAssistantId('self');
|
|
204
237
|
// Reset the session's client callback to a no-op so the stale
|
|
205
238
|
// closure doesn't intercept events from future runs on the same session.
|
|
206
239
|
// Set hasNoClient=true here since the run is done and no HTTP caller
|
|
@@ -251,13 +284,20 @@ export class RunOrchestrator {
|
|
|
251
284
|
* - `'run_not_found'` – no run exists with the given ID
|
|
252
285
|
* - `'no_pending_decision'` – run exists but is not awaiting a confirmation
|
|
253
286
|
*/
|
|
254
|
-
submitDecision(
|
|
287
|
+
submitDecision(
|
|
288
|
+
runId: string,
|
|
289
|
+
decision: UserDecision,
|
|
290
|
+
decisionContext?: string,
|
|
291
|
+
): 'applied' | 'run_not_found' | 'no_pending_decision' {
|
|
255
292
|
const pendingState = this.pending.get(runId);
|
|
256
293
|
if (pendingState) {
|
|
257
294
|
runsStore.clearRunConfirmation(runId);
|
|
258
295
|
pendingState.session.handleConfirmationResponse(
|
|
259
296
|
pendingState.prompterRequestId,
|
|
260
297
|
decision,
|
|
298
|
+
undefined,
|
|
299
|
+
undefined,
|
|
300
|
+
decisionContext,
|
|
261
301
|
);
|
|
262
302
|
this.pending.delete(runId);
|
|
263
303
|
return 'applied';
|
|
@@ -457,6 +457,216 @@ function scanEntropy(
|
|
|
457
457
|
return matches;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Encoded secret detection — decode + re-scan pass
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Find percent-encoded segments containing 3+ encoded bytes, using a linear
|
|
466
|
+
* scan instead of a regex with nested quantifiers (which caused catastrophic
|
|
467
|
+
* backtracking on long near-miss inputs).
|
|
468
|
+
*/
|
|
469
|
+
function findPercentEncodedSegments(text: string): Array<{ start: number; end: number; match: string }> {
|
|
470
|
+
const results: Array<{ start: number; end: number; match: string }> = [];
|
|
471
|
+
const len = text.length;
|
|
472
|
+
const isUrlChar = (ch: string) => /[A-Za-z0-9_.~+/\-]/.test(ch);
|
|
473
|
+
const isHexDigit = (ch: string) => /[0-9A-Fa-f]/.test(ch);
|
|
474
|
+
|
|
475
|
+
let i = 0;
|
|
476
|
+
while (i < len) {
|
|
477
|
+
// Look for the start of a percent-encoded segment
|
|
478
|
+
if (text[i] !== '%' && !isUrlChar(text[i])) { i++; continue; }
|
|
479
|
+
|
|
480
|
+
// Walk a candidate segment of URL-safe chars and %XX sequences
|
|
481
|
+
const start = i;
|
|
482
|
+
let pctCount = 0;
|
|
483
|
+
while (i < len) {
|
|
484
|
+
if (text[i] === '%' && i + 2 < len && isHexDigit(text[i + 1]) && isHexDigit(text[i + 2])) {
|
|
485
|
+
pctCount++;
|
|
486
|
+
i += 3;
|
|
487
|
+
} else if (isUrlChar(text[i])) {
|
|
488
|
+
i++;
|
|
489
|
+
} else {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (pctCount >= 3) {
|
|
495
|
+
results.push({ start, end: i, match: text.slice(start, i) });
|
|
496
|
+
}
|
|
497
|
+
// Avoid re-scanning the same position if we didn't advance
|
|
498
|
+
if (i === start) i++;
|
|
499
|
+
}
|
|
500
|
+
return results;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Hex-escape sequences: \xHH patterns (3+ consecutive) */
|
|
504
|
+
const HEX_ESCAPE_RE = /(?:\\x[0-9A-Fa-f]{2}){3,}/g;
|
|
505
|
+
|
|
506
|
+
/** Candidate base64 segments — 24+ chars that could encode a secret (≥18 decoded bytes) */
|
|
507
|
+
const ENCODED_BASE64_RE = /\b([A-Za-z0-9+/\-_]{24,}={0,3})(?=\W|$)/g;
|
|
508
|
+
|
|
509
|
+
/** Continuous hex-encoded bytes — 32+ hex chars (16+ bytes decoded) */
|
|
510
|
+
const CONTINUOUS_HEX_RE = /\b([0-9a-fA-F]{32,})\b/g;
|
|
511
|
+
|
|
512
|
+
/** Check if decoded content is printable ASCII text */
|
|
513
|
+
function isPrintableText(s: string): boolean {
|
|
514
|
+
return s.length > 0 && /^[\x20-\x7E\t\n\r]+$/.test(s);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function tryDecodeBase64(encoded: string): string | null {
|
|
518
|
+
try {
|
|
519
|
+
// Handle both standard and URL-safe base64
|
|
520
|
+
const standardized = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
|
521
|
+
const decoded = Buffer.from(standardized, 'base64').toString('utf-8');
|
|
522
|
+
if (!isPrintableText(decoded)) return null;
|
|
523
|
+
// Verify round-trip to reject garbage decodes
|
|
524
|
+
const reEncoded = Buffer.from(decoded, 'utf-8').toString('base64').replace(/=+$/, '');
|
|
525
|
+
if (standardized.replace(/=+$/, '') !== reEncoded) return null;
|
|
526
|
+
return decoded;
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function tryDecodePercentEncoded(encoded: string): string | null {
|
|
533
|
+
try {
|
|
534
|
+
const decoded = decodeURIComponent(encoded);
|
|
535
|
+
if (decoded === encoded) return null;
|
|
536
|
+
if (!isPrintableText(decoded)) return null;
|
|
537
|
+
return decoded;
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function tryDecodeHexEscapes(encoded: string): string | null {
|
|
544
|
+
try {
|
|
545
|
+
const decoded = encoded.replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) =>
|
|
546
|
+
String.fromCharCode(parseInt(hex, 16)),
|
|
547
|
+
);
|
|
548
|
+
if (decoded === encoded) return null;
|
|
549
|
+
if (!isPrintableText(decoded)) return null;
|
|
550
|
+
return decoded;
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function tryDecodeContinuousHex(encoded: string): string | null {
|
|
557
|
+
try {
|
|
558
|
+
// Odd-length strings can't be decoded as pairs of hex digits
|
|
559
|
+
if (encoded.length % 2 !== 0) return null;
|
|
560
|
+
// Decode pairs of hex digits to bytes
|
|
561
|
+
const bytes: number[] = [];
|
|
562
|
+
for (let i = 0; i < encoded.length; i += 2) {
|
|
563
|
+
bytes.push(parseInt(encoded.slice(i, i + 2), 16));
|
|
564
|
+
}
|
|
565
|
+
const decoded = String.fromCharCode(...bytes);
|
|
566
|
+
if (!isPrintableText(decoded)) return null;
|
|
567
|
+
return decoded;
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Check if an encoded segment overlaps with any existing match range */
|
|
574
|
+
function overlapsExisting(start: number, end: number, ranges: Set<string>): boolean {
|
|
575
|
+
for (const rangeKey of ranges) {
|
|
576
|
+
const sep = rangeKey.indexOf(':');
|
|
577
|
+
const rStart = Number(rangeKey.slice(0, sep));
|
|
578
|
+
const rEnd = Number(rangeKey.slice(sep + 1));
|
|
579
|
+
if (start < rEnd && end > rStart) return true;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Scan for encoded secrets by decoding candidate segments and running
|
|
586
|
+
* pattern matching on the decoded content. Catches base64-encoded,
|
|
587
|
+
* hex-encoded, and percent-encoded secrets that raw regex would miss.
|
|
588
|
+
*/
|
|
589
|
+
function scanEncoded(
|
|
590
|
+
text: string,
|
|
591
|
+
existingRanges: Set<string>,
|
|
592
|
+
): SecretMatch[] {
|
|
593
|
+
const matches: SecretMatch[] = [];
|
|
594
|
+
|
|
595
|
+
// Helper: try to match decoded content against known secret patterns
|
|
596
|
+
const tryMatchDecoded = (
|
|
597
|
+
encoded: string,
|
|
598
|
+
decoded: string,
|
|
599
|
+
startIndex: number,
|
|
600
|
+
endIndex: number,
|
|
601
|
+
encoding: string,
|
|
602
|
+
) => {
|
|
603
|
+
for (const pattern of PATTERNS) {
|
|
604
|
+
pattern.regex.lastIndex = 0;
|
|
605
|
+
let pm: RegExpExecArray | null;
|
|
606
|
+
while ((pm = pattern.regex.exec(decoded)) !== null) {
|
|
607
|
+
const value = pm[1] ?? pm[0];
|
|
608
|
+
if (isPlaceholder(value)) continue;
|
|
609
|
+
if (isAllowlisted(value)) continue;
|
|
610
|
+
if (pattern.type === 'AWS Secret Key' && !isLikelyAwsSecret(value)) continue;
|
|
611
|
+
|
|
612
|
+
const key = `${startIndex}:${endIndex}`;
|
|
613
|
+
existingRanges.add(key);
|
|
614
|
+
matches.push({
|
|
615
|
+
type: `${pattern.type} (${encoding})`,
|
|
616
|
+
startIndex,
|
|
617
|
+
endIndex,
|
|
618
|
+
redactedValue: redact(encoded),
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// Percent-encoded segments: use linear-time scanner instead of regex
|
|
626
|
+
if (text.includes('%')) {
|
|
627
|
+
for (const seg of findPercentEncodedSegments(text)) {
|
|
628
|
+
if (seg.match.length > 1000) continue;
|
|
629
|
+
if (overlapsExisting(seg.start, seg.end, existingRanges)) continue;
|
|
630
|
+
const decoded = tryDecodePercentEncoded(seg.match);
|
|
631
|
+
if (!decoded) continue;
|
|
632
|
+
tryMatchDecoded(seg.match, decoded, seg.start, seg.end, 'percent-encoded');
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Regex-based decoders for the remaining encodings
|
|
637
|
+
const decoders: Array<{
|
|
638
|
+
regex: RegExp;
|
|
639
|
+
decode: (s: string) => string | null;
|
|
640
|
+
encoding: string;
|
|
641
|
+
quickCheck?: (t: string) => boolean;
|
|
642
|
+
}> = [
|
|
643
|
+
{ regex: HEX_ESCAPE_RE, decode: tryDecodeHexEscapes, encoding: 'hex-escaped', quickCheck: (t) => t.includes('\\x') },
|
|
644
|
+
{ regex: ENCODED_BASE64_RE, decode: tryDecodeBase64, encoding: 'base64-encoded' },
|
|
645
|
+
{ regex: CONTINUOUS_HEX_RE, decode: tryDecodeContinuousHex, encoding: 'hex-encoded' },
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
for (const { regex, decode, encoding, quickCheck } of decoders) {
|
|
649
|
+
if (quickCheck && !quickCheck(text)) continue;
|
|
650
|
+
regex.lastIndex = 0;
|
|
651
|
+
let m: RegExpExecArray | null;
|
|
652
|
+
while ((m = regex.exec(text)) !== null) {
|
|
653
|
+
const encoded = m[1] ?? m[0];
|
|
654
|
+
if (encoded.length > 1000) continue;
|
|
655
|
+
const startIndex = m.index + (m[0].indexOf(encoded));
|
|
656
|
+
const endIndex = startIndex + encoded.length;
|
|
657
|
+
|
|
658
|
+
if (overlapsExisting(startIndex, endIndex, existingRanges)) continue;
|
|
659
|
+
|
|
660
|
+
const decoded = decode(encoded);
|
|
661
|
+
if (!decoded) continue;
|
|
662
|
+
|
|
663
|
+
tryMatchDecoded(encoded, decoded, startIndex, endIndex, encoding);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return matches;
|
|
668
|
+
}
|
|
669
|
+
|
|
460
670
|
// ---------------------------------------------------------------------------
|
|
461
671
|
// Scan function
|
|
462
672
|
// ---------------------------------------------------------------------------
|
|
@@ -508,6 +718,10 @@ export function scanText(text: string, entropyConfig?: Partial<EntropyConfig>):
|
|
|
508
718
|
const entropyMatches = scanEntropy(text, eConfig, seen);
|
|
509
719
|
matches.push(...entropyMatches);
|
|
510
720
|
|
|
721
|
+
// Encoded secret detection — decode candidate segments and re-scan
|
|
722
|
+
const encodedMatches = scanEncoded(text, seen);
|
|
723
|
+
matches.push(...encodedMatches);
|
|
724
|
+
|
|
511
725
|
// Sort by position; at same start, wider match first so redaction covers the full span
|
|
512
726
|
matches.sort((a, b) => a.startIndex - b.startIndex || b.endIndex - a.endIndex);
|
|
513
727
|
return matches;
|
|
@@ -547,4 +761,8 @@ export {
|
|
|
547
761
|
redact as _redact,
|
|
548
762
|
PATTERNS as _PATTERNS,
|
|
549
763
|
hasSecretContext as _hasSecretContext,
|
|
764
|
+
tryDecodeBase64 as _tryDecodeBase64,
|
|
765
|
+
tryDecodePercentEncoded as _tryDecodePercentEncoded,
|
|
766
|
+
tryDecodeHexEscapes as _tryDecodeHexEscapes,
|
|
767
|
+
tryDecodeContinuousHex as _tryDecodeContinuousHex,
|
|
550
768
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared frontmatter parsing for SKILL.md files.
|
|
3
|
+
*
|
|
4
|
+
* Frontmatter is a YAML-like block delimited by `---` at the top of a file.
|
|
5
|
+
* This module provides a single implementation used by the skill catalog loader,
|
|
6
|
+
* the Vellum catalog installer, and the CC command registry.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Matches a `---` delimited frontmatter block at the start of a file. */
|
|
10
|
+
export const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
11
|
+
|
|
12
|
+
export interface FrontmatterParseResult {
|
|
13
|
+
/** Key-value pairs extracted from the frontmatter block. */
|
|
14
|
+
fields: Record<string, string>;
|
|
15
|
+
/** The remaining file content after the frontmatter block. */
|
|
16
|
+
body: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse frontmatter fields from file content.
|
|
21
|
+
*
|
|
22
|
+
* Extracts key-value pairs from the `---` delimited block at the top of the
|
|
23
|
+
* file. Handles single- and double-quoted values, and unescapes common escape
|
|
24
|
+
* sequences (`\n`, `\r`, `\\`, `\"`) in double-quoted values.
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` if no frontmatter block is found.
|
|
27
|
+
*/
|
|
28
|
+
export function parseFrontmatterFields(content: string): FrontmatterParseResult | null {
|
|
29
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
|
|
32
|
+
const frontmatter = match[1];
|
|
33
|
+
const fields: Record<string, string> = {};
|
|
34
|
+
|
|
35
|
+
for (const line of frontmatter.split(/\r?\n/)) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
38
|
+
const separatorIndex = trimmed.indexOf(':');
|
|
39
|
+
if (separatorIndex === -1) continue;
|
|
40
|
+
|
|
41
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
42
|
+
let value = trimmed.slice(separatorIndex + 1).trim();
|
|
43
|
+
|
|
44
|
+
const isDoubleQuoted = value.startsWith('"') && value.endsWith('"');
|
|
45
|
+
const isSingleQuoted = value.startsWith("'") && value.endsWith("'");
|
|
46
|
+
if (isDoubleQuoted || isSingleQuoted) {
|
|
47
|
+
value = value.slice(1, -1);
|
|
48
|
+
if (isDoubleQuoted) {
|
|
49
|
+
// Unescape sequences produced by buildSkillMarkdown's esc().
|
|
50
|
+
// Only for double-quoted values — single-quoted YAML treats backslashes literally.
|
|
51
|
+
// Single-pass to avoid misinterpreting \\n (escaped backslash + n) as a newline.
|
|
52
|
+
value = value.replace(/\\(["\\nr])/g, (_, ch) => {
|
|
53
|
+
if (ch === 'n') return '\n';
|
|
54
|
+
if (ch === 'r') return '\r';
|
|
55
|
+
return ch; // handles \\ → \ and \" → "
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
fields[key] = value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { fields, body: content.slice(match[0].length) };
|
|
63
|
+
}
|
|
@@ -155,6 +155,10 @@ export function formatUnknownSlashSkillMessage(
|
|
|
155
155
|
/**
|
|
156
156
|
* Rewrite user input for a known slash command into a model-facing prompt
|
|
157
157
|
* that explicitly instructs the model to invoke the skill.
|
|
158
|
+
*
|
|
159
|
+
* For the claude-code skill, trailing arguments are routed via the `command`
|
|
160
|
+
* input (not `prompt`) so that .claude/commands/*.md templates are loaded
|
|
161
|
+
* and $ARGUMENTS substitution is applied.
|
|
158
162
|
*/
|
|
159
163
|
export function rewriteKnownSlashCommandPrompt(params: {
|
|
160
164
|
rawInput: string;
|
|
@@ -162,6 +166,25 @@ export function rewriteKnownSlashCommandPrompt(params: {
|
|
|
162
166
|
skillName: string;
|
|
163
167
|
trailingArgs: string;
|
|
164
168
|
}): string {
|
|
169
|
+
// For the claude-code skill, route trailing args through the `command` input
|
|
170
|
+
// so CC command templates (.claude/commands/*.md) are loaded and $ARGUMENTS
|
|
171
|
+
// substitution is applied, rather than sending them as a raw prompt.
|
|
172
|
+
if (params.skillId === 'claude-code' && params.trailingArgs) {
|
|
173
|
+
// Extract the command name (first word of trailing args) and remaining arguments
|
|
174
|
+
const parts = params.trailingArgs.split(/\s+/);
|
|
175
|
+
const commandName = parts[0];
|
|
176
|
+
const commandArgs = parts.slice(1).join(' ');
|
|
177
|
+
|
|
178
|
+
const lines = [
|
|
179
|
+
`The user invoked the slash command \`/${params.skillId}\`.`,
|
|
180
|
+
`Execute the Claude Code command "${commandName}" using the claude_code tool with command="${commandName}".`,
|
|
181
|
+
];
|
|
182
|
+
if (commandArgs) {
|
|
183
|
+
lines.push(`Pass the following as the \`arguments\` input: ${commandArgs}`);
|
|
184
|
+
}
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
const lines = [
|
|
166
189
|
`The user invoked the slash command \`/${params.skillId}\`.`,
|
|
167
190
|
`Please invoke the "${params.skillName}" skill (ID: ${params.skillId}).`,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { CatalogEntry } from '../tools/skills/vellum-catalog.js';
|
|
5
|
+
import { getLogger } from '../util/logger.js';
|
|
6
|
+
|
|
7
|
+
const log = getLogger('vellum-catalog-remote');
|
|
8
|
+
|
|
9
|
+
const GITHUB_RAW_BASE =
|
|
10
|
+
'https://raw.githubusercontent.com/vellum-ai/vellum-assistant/main/assistant/src/config/vellum-skills';
|
|
11
|
+
|
|
12
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
13
|
+
|
|
14
|
+
interface CatalogManifest {
|
|
15
|
+
version: number;
|
|
16
|
+
skills: CatalogEntry[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let cachedEntries: CatalogEntry[] | null = null;
|
|
20
|
+
let cacheTimestamp = 0;
|
|
21
|
+
|
|
22
|
+
function getBundledCatalogPath(): string {
|
|
23
|
+
return join(import.meta.dir, '..', 'config', 'vellum-skills', 'catalog.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadBundledCatalog(): CatalogEntry[] {
|
|
27
|
+
try {
|
|
28
|
+
const raw = readFileSync(getBundledCatalogPath(), 'utf-8');
|
|
29
|
+
const manifest: CatalogManifest = JSON.parse(raw);
|
|
30
|
+
return manifest.skills ?? [];
|
|
31
|
+
} catch (err) {
|
|
32
|
+
log.warn({ err }, 'Failed to read bundled catalog.json');
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getBundledSkillContent(skillId: string): string | null {
|
|
38
|
+
try {
|
|
39
|
+
const skillPath = join(import.meta.dir, '..', 'config', 'vellum-skills', skillId, 'SKILL.md');
|
|
40
|
+
return readFileSync(skillPath, 'utf-8');
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fetch catalog entries (cached, async). Falls back to bundled copy. */
|
|
47
|
+
export async function fetchCatalogEntries(): Promise<CatalogEntry[]> {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
if (cachedEntries && now - cacheTimestamp < CACHE_TTL_MS) {
|
|
50
|
+
return cachedEntries;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const url = `${GITHUB_RAW_BASE}/catalog.json`;
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
signal: AbortSignal.timeout(5000),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const manifest: CatalogManifest = await response.json();
|
|
64
|
+
const skills = manifest.skills;
|
|
65
|
+
if (!Array.isArray(skills) || skills.length === 0) {
|
|
66
|
+
throw new Error('Remote catalog has invalid or empty skills array');
|
|
67
|
+
}
|
|
68
|
+
cachedEntries = skills;
|
|
69
|
+
cacheTimestamp = now;
|
|
70
|
+
log.info({ count: cachedEntries.length }, 'Fetched remote vellum-skills catalog');
|
|
71
|
+
return cachedEntries;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
log.warn({ err }, 'Failed to fetch remote catalog, falling back to bundled copy');
|
|
74
|
+
const bundled = loadBundledCatalog();
|
|
75
|
+
// Cache the bundled result too so we don't re-fetch on every call during outage
|
|
76
|
+
cachedEntries = bundled;
|
|
77
|
+
cacheTimestamp = now;
|
|
78
|
+
return bundled;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Fetch a skill's SKILL.md content from GitHub. Falls back to bundled copy. */
|
|
83
|
+
export async function fetchSkillContent(skillId: string): Promise<string | null> {
|
|
84
|
+
try {
|
|
85
|
+
const url = `${GITHUB_RAW_BASE}/${encodeURIComponent(skillId)}/SKILL.md`;
|
|
86
|
+
const response = await fetch(url, {
|
|
87
|
+
signal: AbortSignal.timeout(10000),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const content = await response.text();
|
|
95
|
+
log.info({ skillId }, 'Fetched remote SKILL.md');
|
|
96
|
+
return content;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
log.warn({ err, skillId }, 'Failed to fetch remote SKILL.md, falling back to bundled copy');
|
|
99
|
+
return getBundledSkillContent(skillId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Check if a skill ID exists in the remote catalog. */
|
|
104
|
+
export async function checkVellumSkill(skillId: string): Promise<boolean> {
|
|
105
|
+
const entries = await fetchCatalogEntries();
|
|
106
|
+
return entries.some((e) => e.id === skillId);
|
|
107
|
+
}
|
|
@@ -41,8 +41,8 @@ function formatBytes(bytes: number): string {
|
|
|
41
41
|
/**
|
|
42
42
|
* Load an attachment row (including base64 data) by its primary key.
|
|
43
43
|
*
|
|
44
|
-
* Not scoped by assistantId because
|
|
45
|
-
*
|
|
44
|
+
* Not scoped by assistantId because attachment access is enforced by
|
|
45
|
+
* conversation visibility checks in execute().
|
|
46
46
|
*/
|
|
47
47
|
function loadAttachmentById(
|
|
48
48
|
attachmentId: string,
|