@vellumai/assistant 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -21
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
- package/src/__tests__/app-git-history.test.ts +22 -27
- package/src/__tests__/app-git-service.test.ts +44 -78
- package/src/__tests__/call-orchestrator.test.ts +321 -0
- package/src/__tests__/channel-approval-routes.test.ts +1267 -93
- package/src/__tests__/channel-approval.test.ts +2 -0
- package/src/__tests__/channel-approvals.test.ts +51 -2
- package/src/__tests__/channel-delivery-store.test.ts +130 -1
- package/src/__tests__/channel-guardian.test.ts +371 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +635 -0
- package/src/__tests__/daemon-server-session-init.test.ts +5 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
- package/src/__tests__/handlers-telegram-config.test.ts +82 -0
- package/src/__tests__/handlers-twilio-config.test.ts +738 -5
- package/src/__tests__/ingress-url-consistency.test.ts +64 -0
- package/src/__tests__/ipc-snapshot.test.ts +10 -0
- package/src/__tests__/run-orchestrator.test.ts +1 -1
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-process-bridge.test.ts +2 -0
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/calls/call-orchestrator.ts +63 -11
- package/src/calls/twilio-config.ts +10 -1
- package/src/calls/twilio-rest.ts +70 -0
- 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/email-setup/SKILL.md +56 -0
- package/src/config/bundled-skills/messaging/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +6 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +16 -0
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +52 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
- 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 +193 -17
- package/src/daemon/handlers/sessions.ts +5 -3
- package/src/daemon/handlers/skills.ts +60 -17
- package/src/daemon/ipc-contract-inventory.json +4 -0
- package/src/daemon/ipc-contract.ts +16 -0
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +105 -502
- package/src/daemon/session-agent-loop.ts +9 -14
- package/src/daemon/session-process.ts +20 -3
- package/src/daemon/session-runtime-assembly.ts +60 -44
- package/src/daemon/session-slash.ts +50 -2
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session.ts +8 -1
- package/src/inbound/public-ingress-urls.ts +20 -3
- package/src/index.ts +1 -23
- package/src/memory/app-git-service.ts +24 -0
- package/src/memory/app-store.ts +0 -21
- package/src/memory/channel-delivery-store.ts +74 -3
- package/src/memory/channel-guardian-store.ts +54 -26
- package/src/memory/conversation-key-store.ts +20 -0
- package/src/memory/conversation-store.ts +14 -2
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1019 -0
- package/src/memory/db.ts +2 -1995
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-worker.ts +7 -1
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +30 -1
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +6 -0
- package/src/memory/search/types.ts +2 -0
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/channel-approvals.ts +17 -3
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +28 -9
- package/src/runtime/routes/channel-routes.ts +279 -100
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +8 -1
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/clawhub.ts +6 -2
- 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/subagent/manager.ts +4 -1
- package/src/subagent/types.ts +2 -0
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/executor.ts +10 -2
- package/src/tools/skills/vellum-catalog.ts +75 -127
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/terminal/parser.ts +21 -5
- package/src/util/platform.ts +8 -1
- package/src/util/retry.ts +4 -4
|
@@ -211,4 +211,68 @@ describe('Ingress URL consistency between assistant and gateway', () => {
|
|
|
211
211
|
|
|
212
212
|
expect(gatewayCanonical).toBe(callbackUrl);
|
|
213
213
|
});
|
|
214
|
+
|
|
215
|
+
// ── SMS-specific URL consistency ──────────────────────────────────
|
|
216
|
+
|
|
217
|
+
test('SMS webhook URL consistency: gateway signature URL matches configured ingress', () => {
|
|
218
|
+
const publicBase = 'https://sms-gateway.example.com';
|
|
219
|
+
const authToken = 'test-sms-auth-token';
|
|
220
|
+
|
|
221
|
+
// The gateway registers /webhooks/twilio/sms with Twilio. Twilio signs
|
|
222
|
+
// inbound SMS requests against the full public URL.
|
|
223
|
+
const smsWebhookUrl = `${publicBase}/webhooks/twilio/sms`;
|
|
224
|
+
|
|
225
|
+
const params = {
|
|
226
|
+
Body: 'hello',
|
|
227
|
+
From: '+15551234567',
|
|
228
|
+
To: '+15559876543',
|
|
229
|
+
MessageSid: 'SM123',
|
|
230
|
+
};
|
|
231
|
+
const twilioSignature = computeTwilioSignature(smsWebhookUrl, params, authToken);
|
|
232
|
+
|
|
233
|
+
// Gateway receives the request on its local address and reconstructs
|
|
234
|
+
// the canonical URL using the configured ingress base.
|
|
235
|
+
const localUrl = 'http://127.0.0.1:7830/webhooks/twilio/sms';
|
|
236
|
+
const canonicalUrl = reconstructGatewayCanonicalUrl(publicBase, localUrl);
|
|
237
|
+
|
|
238
|
+
expect(canonicalUrl).toBe(smsWebhookUrl);
|
|
239
|
+
|
|
240
|
+
const recomputed = computeTwilioSignature(canonicalUrl, params, authToken);
|
|
241
|
+
expect(recomputed).toBe(twilioSignature);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('SMS webhook signature fails when ingress URL is not configured (fail-visible)', () => {
|
|
245
|
+
const publicBase = 'https://sms-gateway.example.com';
|
|
246
|
+
const authToken = 'test-sms-auth-token';
|
|
247
|
+
|
|
248
|
+
const smsWebhookUrl = `${publicBase}/webhooks/twilio/sms`;
|
|
249
|
+
const params = { Body: 'test', From: '+15550001111', MessageSid: 'SM456' };
|
|
250
|
+
const twilioSignature = computeTwilioSignature(smsWebhookUrl, params, authToken);
|
|
251
|
+
|
|
252
|
+
// Without ingress config, the gateway uses the local URL — signature mismatch.
|
|
253
|
+
const localUrl = 'http://127.0.0.1:7830/webhooks/twilio/sms';
|
|
254
|
+
const canonicalWithout = reconstructGatewayCanonicalUrl(undefined, localUrl);
|
|
255
|
+
const recomputedWithout = computeTwilioSignature(canonicalWithout, params, authToken);
|
|
256
|
+
expect(recomputedWithout).not.toBe(twilioSignature);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('all Twilio webhook paths share the /webhooks/twilio/ prefix consistently', () => {
|
|
260
|
+
const config: IngressConfig = {
|
|
261
|
+
ingress: { publicBaseUrl: 'https://consistent.example.com' },
|
|
262
|
+
};
|
|
263
|
+
const base = getPublicBaseUrl(config);
|
|
264
|
+
|
|
265
|
+
// Document the path contract: all Twilio webhooks live under /webhooks/twilio/
|
|
266
|
+
const voiceUrl = getTwilioVoiceWebhookUrl(config, 'sess');
|
|
267
|
+
const statusUrl = getTwilioStatusCallbackUrl(config);
|
|
268
|
+
|
|
269
|
+
// Verify they all share the same base and prefix
|
|
270
|
+
expect(voiceUrl).toStartWith(`${base}/webhooks/twilio/`);
|
|
271
|
+
expect(statusUrl).toStartWith(`${base}/webhooks/twilio/`);
|
|
272
|
+
|
|
273
|
+
// SMS is currently handled at the gateway level (/webhooks/twilio/sms)
|
|
274
|
+
// but the path pattern is the same
|
|
275
|
+
const smsUrl = `${base}/webhooks/twilio/sms`;
|
|
276
|
+
expect(smsUrl).toStartWith(`${base}/webhooks/twilio/`);
|
|
277
|
+
});
|
|
214
278
|
});
|
|
@@ -378,6 +378,10 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
|
|
|
378
378
|
type: 'telegram_config',
|
|
379
379
|
action: 'get',
|
|
380
380
|
},
|
|
381
|
+
twilio_config: {
|
|
382
|
+
type: 'twilio_config',
|
|
383
|
+
action: 'get',
|
|
384
|
+
},
|
|
381
385
|
guardian_verification: {
|
|
382
386
|
type: 'guardian_verification',
|
|
383
387
|
action: 'create_challenge',
|
|
@@ -1218,6 +1222,12 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1218
1222
|
connected: true,
|
|
1219
1223
|
hasWebhookSecret: true,
|
|
1220
1224
|
},
|
|
1225
|
+
twilio_config_response: {
|
|
1226
|
+
type: 'twilio_config_response',
|
|
1227
|
+
success: true,
|
|
1228
|
+
hasCredentials: true,
|
|
1229
|
+
phoneNumber: '+15551234567',
|
|
1230
|
+
},
|
|
1221
1231
|
guardian_verification_response: {
|
|
1222
1232
|
type: 'guardian_verification_response',
|
|
1223
1233
|
success: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterAll, mock
|
|
1
|
+
import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test';
|
|
2
2
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
_isPlaceholder,
|
|
7
7
|
_redact,
|
|
8
8
|
_hasSecretContext,
|
|
9
|
+
_tryDecodeBase64,
|
|
10
|
+
_tryDecodePercentEncoded,
|
|
11
|
+
_tryDecodeHexEscapes,
|
|
12
|
+
_tryDecodeContinuousHex,
|
|
9
13
|
type SecretMatch,
|
|
10
14
|
} from '../security/secret-scanner.js';
|
|
11
15
|
|
|
@@ -898,3 +902,222 @@ describe('word-boundary context keywords', () => {
|
|
|
898
902
|
expect(entropy.length).toBeGreaterThan(0);
|
|
899
903
|
});
|
|
900
904
|
});
|
|
905
|
+
|
|
906
|
+
// ---------------------------------------------------------------------------
|
|
907
|
+
// Encoded secret detection — decode + re-scan
|
|
908
|
+
// ---------------------------------------------------------------------------
|
|
909
|
+
describe('encoded secret detection', () => {
|
|
910
|
+
// -- Base64-encoded secrets --
|
|
911
|
+
describe('base64-encoded', () => {
|
|
912
|
+
test('detects base64-encoded Stripe key', () => {
|
|
913
|
+
const secret = 'sk_live_abcdefghijklmnopqrstuvwx';
|
|
914
|
+
const encoded = Buffer.from(secret).toString('base64');
|
|
915
|
+
const input = `config: ${encoded}`;
|
|
916
|
+
const matches = scanText(input);
|
|
917
|
+
const found = matches.find((m) => m.type === 'Stripe Secret Key (base64-encoded)');
|
|
918
|
+
expect(found).toBeDefined();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test('detects base64-encoded GitHub token', () => {
|
|
922
|
+
const secret = `ghp_${'A'.repeat(36)}`;
|
|
923
|
+
const encoded = Buffer.from(secret).toString('base64');
|
|
924
|
+
const input = `value=${encoded}`;
|
|
925
|
+
const matches = scanText(input);
|
|
926
|
+
const found = matches.find((m) => m.type === 'GitHub Token (base64-encoded)');
|
|
927
|
+
expect(found).toBeDefined();
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test('detects base64-encoded private key header', () => {
|
|
931
|
+
const secret = '-----BEGIN RSA PRIVATE KEY-----';
|
|
932
|
+
const encoded = Buffer.from(secret).toString('base64');
|
|
933
|
+
const input = `data: ${encoded}`;
|
|
934
|
+
const matches = scanText(input);
|
|
935
|
+
const found = matches.find((m) => m.type === 'Private Key (base64-encoded)');
|
|
936
|
+
expect(found).toBeDefined();
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
test('does not flag base64 that decodes to non-secret text', () => {
|
|
940
|
+
const encoded = Buffer.from('Hello, this is just normal text!').toString('base64');
|
|
941
|
+
const input = `data: ${encoded}`;
|
|
942
|
+
const matches = scanText(input);
|
|
943
|
+
const encoded_matches = matches.filter((m) => m.type.includes('base64-encoded'));
|
|
944
|
+
expect(encoded_matches).toHaveLength(0);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test('does not flag base64 that decodes to binary data', () => {
|
|
948
|
+
// Create a base64 string that decodes to non-printable bytes
|
|
949
|
+
const binary = Buffer.from([0x00, 0x01, 0x02, 0x80, 0xff, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15]);
|
|
950
|
+
const encoded = binary.toString('base64');
|
|
951
|
+
const input = `data: ${encoded}`;
|
|
952
|
+
const matches = scanText(input);
|
|
953
|
+
const encoded_matches = matches.filter((m) => m.type.includes('base64-encoded'));
|
|
954
|
+
expect(encoded_matches).toHaveLength(0);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test('does not double-count secrets already detected by raw patterns', () => {
|
|
958
|
+
// A JWT is already detected directly — should not be re-detected as base64-encoded
|
|
959
|
+
const header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
|
|
960
|
+
const payload = 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ';
|
|
961
|
+
const signature = 'SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
962
|
+
const jwt = `${header}.${payload}.${signature}`;
|
|
963
|
+
const input = `token: ${jwt}`;
|
|
964
|
+
const matches = scanText(input);
|
|
965
|
+
const jwtMatches = matches.filter((m) => m.type === 'JSON Web Token');
|
|
966
|
+
const encodedMatches = matches.filter((m) => m.type.includes('base64-encoded'));
|
|
967
|
+
expect(jwtMatches).toHaveLength(1);
|
|
968
|
+
expect(encodedMatches).toHaveLength(0);
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// -- Percent-encoded secrets --
|
|
973
|
+
describe('percent-encoded', () => {
|
|
974
|
+
test('detects percent-encoded database connection string', () => {
|
|
975
|
+
const secret = 'postgres://user:secret@db.example.com:5432/mydb';
|
|
976
|
+
const encoded = encodeURIComponent(secret);
|
|
977
|
+
const input = `url=${encoded}`;
|
|
978
|
+
const matches = scanText(input);
|
|
979
|
+
const found = matches.find((m) => m.type === 'Database Connection String (percent-encoded)');
|
|
980
|
+
expect(found).toBeDefined();
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
test('detects percent-encoded secret assignment', () => {
|
|
984
|
+
const encoded = 'password%3D%22SuperSecret123%21%22';
|
|
985
|
+
const input = `data=${encoded}`;
|
|
986
|
+
const matches = scanText(input);
|
|
987
|
+
const found = matches.find((m) => m.type.includes('percent-encoded'));
|
|
988
|
+
expect(found).toBeDefined();
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
test('does not flag percent-encoded non-secret text', () => {
|
|
992
|
+
const encoded = 'hello%20world%20this%20is%20normal';
|
|
993
|
+
const input = `text=${encoded}`;
|
|
994
|
+
const matches = scanText(input);
|
|
995
|
+
const encoded_matches = matches.filter((m) => m.type.includes('percent-encoded'));
|
|
996
|
+
expect(encoded_matches).toHaveLength(0);
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// -- Hex-escaped secrets --
|
|
1001
|
+
describe('hex-escaped', () => {
|
|
1002
|
+
test('detects hex-escaped Stripe key', () => {
|
|
1003
|
+
const secret = 'sk_live_abcdefghijklmnopqrstuvwx';
|
|
1004
|
+
const escaped = Array.from(secret).map((c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`).join('');
|
|
1005
|
+
const input = `value = "${escaped}"`;
|
|
1006
|
+
const matches = scanText(input);
|
|
1007
|
+
const found = matches.find((m) => m.type === 'Stripe Secret Key (hex-escaped)');
|
|
1008
|
+
expect(found).toBeDefined();
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test('does not flag hex-escaped non-secret text', () => {
|
|
1012
|
+
const escaped = '\\x48\\x65\\x6c\\x6c\\x6f';
|
|
1013
|
+
const input = `value = "${escaped}"`;
|
|
1014
|
+
const matches = scanText(input);
|
|
1015
|
+
const encoded_matches = matches.filter((m) => m.type.includes('hex-escaped'));
|
|
1016
|
+
expect(encoded_matches).toHaveLength(0);
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// -- Continuous hex-encoded secrets --
|
|
1021
|
+
describe('hex-encoded (continuous)', () => {
|
|
1022
|
+
test('detects hex-encoded GitHub token', () => {
|
|
1023
|
+
const secret = `ghp_${'A'.repeat(36)}`;
|
|
1024
|
+
const hexEncoded = Buffer.from(secret).toString('hex');
|
|
1025
|
+
const input = `payload: ${hexEncoded}`;
|
|
1026
|
+
const matches = scanText(input);
|
|
1027
|
+
const found = matches.find((m) => m.type === 'GitHub Token (hex-encoded)');
|
|
1028
|
+
expect(found).toBeDefined();
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test('detects hex-encoded AWS access key', () => {
|
|
1032
|
+
const secret = 'AKIAIOSFODNN7REALKEY';
|
|
1033
|
+
const hexEncoded = Buffer.from(secret).toString('hex');
|
|
1034
|
+
const input = `data: ${hexEncoded}`;
|
|
1035
|
+
const matches = scanText(input);
|
|
1036
|
+
const found = matches.find((m) => m.type === 'AWS Access Key (hex-encoded)');
|
|
1037
|
+
expect(found).toBeDefined();
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
test('does not flag hex strings that decode to non-secret text', () => {
|
|
1041
|
+
const hexEncoded = Buffer.from('This is just normal harmless text').toString('hex');
|
|
1042
|
+
const input = `data: ${hexEncoded}`;
|
|
1043
|
+
const matches = scanText(input);
|
|
1044
|
+
const encoded_matches = matches.filter((m) => m.type.includes('hex-encoded'));
|
|
1045
|
+
expect(encoded_matches).toHaveLength(0);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
test('does not flag git SHAs or similar hex hashes', () => {
|
|
1049
|
+
// 40-char hex SHA — too short to decode to a meaningful secret
|
|
1050
|
+
const sha = '4b825dc642cb6eb9a060e54bf899d15f13fe1d7a';
|
|
1051
|
+
const input = `commit: ${sha}`;
|
|
1052
|
+
const matches = scanText(input);
|
|
1053
|
+
const encoded_matches = matches.filter((m) => m.type.includes('hex-encoded'));
|
|
1054
|
+
expect(encoded_matches).toHaveLength(0);
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
// -- Redaction of encoded secrets --
|
|
1059
|
+
describe('redaction of encoded secrets', () => {
|
|
1060
|
+
test('redactSecrets replaces base64-encoded secrets', () => {
|
|
1061
|
+
const secret = 'sk_live_abcdefghijklmnopqrstuvwx';
|
|
1062
|
+
const encoded = Buffer.from(secret).toString('base64');
|
|
1063
|
+
const input = `config: ${encoded}`;
|
|
1064
|
+
const result = redactSecrets(input);
|
|
1065
|
+
expect(result).toContain('<redacted type="Stripe Secret Key (base64-encoded)" />');
|
|
1066
|
+
expect(result).not.toContain(encoded);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
test('redactSecrets replaces hex-encoded secrets', () => {
|
|
1070
|
+
const secret = `ghp_${'A'.repeat(36)}`;
|
|
1071
|
+
const hexEncoded = Buffer.from(secret).toString('hex');
|
|
1072
|
+
const input = `data: ${hexEncoded}`;
|
|
1073
|
+
const result = redactSecrets(input);
|
|
1074
|
+
expect(result).toContain('<redacted type="GitHub Token (hex-encoded)" />');
|
|
1075
|
+
expect(result).not.toContain(hexEncoded);
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// ---------------------------------------------------------------------------
|
|
1081
|
+
// Decode helper unit tests
|
|
1082
|
+
// ---------------------------------------------------------------------------
|
|
1083
|
+
describe('decode helpers', () => {
|
|
1084
|
+
test('tryDecodeBase64 returns decoded text for valid base64', () => {
|
|
1085
|
+
const encoded = Buffer.from('sk_live_abcdefghijklmnopqrstuvwx').toString('base64');
|
|
1086
|
+
expect(_tryDecodeBase64(encoded)).toBe('sk_live_abcdefghijklmnopqrstuvwx');
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
test('tryDecodeBase64 returns null for binary content', () => {
|
|
1090
|
+
const binary = Buffer.from([0x00, 0x01, 0x80, 0xff]).toString('base64');
|
|
1091
|
+
expect(_tryDecodeBase64(binary)).toBeNull();
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
test('tryDecodeBase64 returns null for invalid base64', () => {
|
|
1095
|
+
expect(_tryDecodeBase64('not!!valid!!base64!!')).toBeNull();
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test('tryDecodePercentEncoded returns decoded text', () => {
|
|
1099
|
+
expect(_tryDecodePercentEncoded('hello%20world%21')).toBe('hello world!');
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
test('tryDecodePercentEncoded returns null when nothing to decode', () => {
|
|
1103
|
+
expect(_tryDecodePercentEncoded('no-encoding-here')).toBeNull();
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
test('tryDecodeHexEscapes returns decoded text', () => {
|
|
1107
|
+
expect(_tryDecodeHexEscapes('\\x48\\x65\\x6c\\x6c\\x6f')).toBe('Hello');
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
test('tryDecodeHexEscapes returns null when no escapes', () => {
|
|
1111
|
+
expect(_tryDecodeHexEscapes('plain text')).toBeNull();
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
test('tryDecodeContinuousHex returns decoded text', () => {
|
|
1115
|
+
const hex = Buffer.from('Hello').toString('hex');
|
|
1116
|
+
expect(_tryDecodeContinuousHex(hex)).toBe('Hello');
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
test('tryDecodeContinuousHex returns null for non-printable result', () => {
|
|
1120
|
+
// Hex that decodes to binary
|
|
1121
|
+
expect(_tryDecodeContinuousHex('0001ff80')).toBeNull();
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
@@ -33,6 +33,7 @@ mock.module('../config/loader.js', () => ({
|
|
|
33
33
|
provider: 'anthropic',
|
|
34
34
|
memory: { enabled: false },
|
|
35
35
|
calls: { enabled: false },
|
|
36
|
+
contextWindow: { maxInputTokens: 200000 },
|
|
36
37
|
}),
|
|
37
38
|
}));
|
|
38
39
|
|
|
@@ -75,6 +76,7 @@ function createMockSession(overrides?: Partial<ProcessSessionContext>): ProcessS
|
|
|
75
76
|
traceEmitter: {
|
|
76
77
|
emit: () => {},
|
|
77
78
|
} as unknown as ProcessSessionContext['traceEmitter'],
|
|
79
|
+
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
78
80
|
persistUserMessage: mock((_content: string, _attachments: unknown[], _requestId?: string) => 'mock-msg-id'),
|
|
79
81
|
runAgentLoop: mock(async () => {}),
|
|
80
82
|
...overrides,
|