@vellumai/assistant 0.5.7 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/eslint.config.mjs +0 -31
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +227 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-agent-loop.ts +6 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-runtime-assembly.ts +61 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +30 -0
- package/src/prompts/templates/NOW.md +26 -0
- package/src/prompts/templates/SOUL.md +20 -0
- package/src/prompts/update-bulletin-format.ts +0 -2
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/inline-command-expansions.ts +7 -7
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/sensitive-output-placeholders.ts +2 -2
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
injectChannelCapabilityContext,
|
|
12
12
|
injectChannelCommandContext,
|
|
13
13
|
injectInboundActorContext,
|
|
14
|
+
injectNowScratchpad,
|
|
14
15
|
injectTemporalContext,
|
|
15
16
|
injectTurnContext,
|
|
16
17
|
isGroupChatType,
|
|
@@ -18,6 +19,8 @@ import {
|
|
|
18
19
|
stripChannelCapabilityContext,
|
|
19
20
|
stripChannelTurnContext,
|
|
20
21
|
stripInboundActorContext,
|
|
22
|
+
stripInjectedContext,
|
|
23
|
+
stripNowScratchpad,
|
|
21
24
|
stripTemporalContext,
|
|
22
25
|
} from "../daemon/conversation-runtime-assembly.js";
|
|
23
26
|
import type { Message } from "../providers/types.js";
|
|
@@ -1318,6 +1321,7 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
1318
1321
|
canonicalActorIdentity: "user-1",
|
|
1319
1322
|
trustClass: "guardian",
|
|
1320
1323
|
} as InboundActorContext,
|
|
1324
|
+
nowScratchpad: "Current focus: shipping PR 3",
|
|
1321
1325
|
isNonInteractive: true,
|
|
1322
1326
|
};
|
|
1323
1327
|
|
|
@@ -1337,6 +1341,7 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
1337
1341
|
expect(allText).toContain("<turn_context>");
|
|
1338
1342
|
expect(allText).toContain("<inbound_actor_context>");
|
|
1339
1343
|
expect(allText).toContain("<non_interactive_context>");
|
|
1344
|
+
expect(allText).toContain("<now_scratchpad>");
|
|
1340
1345
|
});
|
|
1341
1346
|
|
|
1342
1347
|
test("explicit mode: 'full' behaves the same as default", () => {
|
|
@@ -1353,6 +1358,7 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
1353
1358
|
expect(allText).toContain("<temporal_context>");
|
|
1354
1359
|
expect(allText).toContain("<channel_command_context>");
|
|
1355
1360
|
expect(allText).toContain("<active_workspace>");
|
|
1361
|
+
expect(allText).toContain("<now_scratchpad>");
|
|
1356
1362
|
});
|
|
1357
1363
|
|
|
1358
1364
|
test("minimal mode skips high-token optional blocks", () => {
|
|
@@ -1370,6 +1376,7 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
1370
1376
|
expect(allText).not.toContain("<temporal_context>");
|
|
1371
1377
|
expect(allText).not.toContain("<channel_command_context>");
|
|
1372
1378
|
expect(allText).not.toContain("<active_workspace>");
|
|
1379
|
+
expect(allText).not.toContain("<now_scratchpad>");
|
|
1373
1380
|
});
|
|
1374
1381
|
|
|
1375
1382
|
test("minimal mode preserves safety-critical blocks", () => {
|
|
@@ -1417,3 +1424,223 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
1417
1424
|
expect(texts).toContain("Hello");
|
|
1418
1425
|
});
|
|
1419
1426
|
});
|
|
1427
|
+
|
|
1428
|
+
// ---------------------------------------------------------------------------
|
|
1429
|
+
// injectNowScratchpad
|
|
1430
|
+
// ---------------------------------------------------------------------------
|
|
1431
|
+
|
|
1432
|
+
describe("injectNowScratchpad", () => {
|
|
1433
|
+
const baseUserMessage: Message = {
|
|
1434
|
+
role: "user",
|
|
1435
|
+
content: [{ type: "text", text: "What should I work on?" }],
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
test("appends now_scratchpad block to user message", () => {
|
|
1439
|
+
const result = injectNowScratchpad(
|
|
1440
|
+
baseUserMessage,
|
|
1441
|
+
"Current focus: shipping PR 3",
|
|
1442
|
+
);
|
|
1443
|
+
expect(result.content.length).toBe(2);
|
|
1444
|
+
// Original content comes first
|
|
1445
|
+
expect((result.content[0] as { type: "text"; text: string }).text).toBe(
|
|
1446
|
+
"What should I work on?",
|
|
1447
|
+
);
|
|
1448
|
+
// Scratchpad is appended (not prepended)
|
|
1449
|
+
const injected = result.content[1];
|
|
1450
|
+
expect(injected.type).toBe("text");
|
|
1451
|
+
const text = (injected as { type: "text"; text: string }).text;
|
|
1452
|
+
expect(text).toBe(
|
|
1453
|
+
"<now_scratchpad>\nCurrent focus: shipping PR 3\n</now_scratchpad>",
|
|
1454
|
+
);
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
test("preserves existing multi-block content and appends at end", () => {
|
|
1458
|
+
const multiBlockMessage: Message = {
|
|
1459
|
+
role: "user",
|
|
1460
|
+
content: [
|
|
1461
|
+
{ type: "text", text: "First block" },
|
|
1462
|
+
{ type: "text", text: "Second block" },
|
|
1463
|
+
],
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
const result = injectNowScratchpad(multiBlockMessage, "scratchpad notes");
|
|
1467
|
+
expect(result.content.length).toBe(3);
|
|
1468
|
+
expect((result.content[0] as { type: "text"; text: string }).text).toBe(
|
|
1469
|
+
"First block",
|
|
1470
|
+
);
|
|
1471
|
+
expect((result.content[1] as { type: "text"; text: string }).text).toBe(
|
|
1472
|
+
"Second block",
|
|
1473
|
+
);
|
|
1474
|
+
expect(
|
|
1475
|
+
(result.content[2] as { type: "text"; text: string }).text,
|
|
1476
|
+
).toContain("<now_scratchpad>");
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
// ---------------------------------------------------------------------------
|
|
1481
|
+
// stripNowScratchpad
|
|
1482
|
+
// ---------------------------------------------------------------------------
|
|
1483
|
+
|
|
1484
|
+
describe("stripNowScratchpad", () => {
|
|
1485
|
+
test("strips now_scratchpad blocks from user messages", () => {
|
|
1486
|
+
const messages: Message[] = [
|
|
1487
|
+
{
|
|
1488
|
+
role: "user",
|
|
1489
|
+
content: [
|
|
1490
|
+
{ type: "text", text: "Hello" },
|
|
1491
|
+
{
|
|
1492
|
+
type: "text",
|
|
1493
|
+
text: "<now_scratchpad>\nSome notes\n</now_scratchpad>",
|
|
1494
|
+
},
|
|
1495
|
+
],
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
role: "assistant",
|
|
1499
|
+
content: [{ type: "text", text: "Hi there" }],
|
|
1500
|
+
},
|
|
1501
|
+
];
|
|
1502
|
+
|
|
1503
|
+
const result = stripNowScratchpad(messages);
|
|
1504
|
+
|
|
1505
|
+
expect(result.length).toBe(2);
|
|
1506
|
+
expect(result[0].content.length).toBe(1);
|
|
1507
|
+
expect((result[0].content[0] as { type: "text"; text: string }).text).toBe(
|
|
1508
|
+
"Hello",
|
|
1509
|
+
);
|
|
1510
|
+
// Assistant message untouched
|
|
1511
|
+
expect(result[1].content.length).toBe(1);
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
test("removes user messages that only contain now_scratchpad", () => {
|
|
1515
|
+
const messages: Message[] = [
|
|
1516
|
+
{
|
|
1517
|
+
role: "user",
|
|
1518
|
+
content: [
|
|
1519
|
+
{
|
|
1520
|
+
type: "text",
|
|
1521
|
+
text: "<now_scratchpad>\nSome notes\n</now_scratchpad>",
|
|
1522
|
+
},
|
|
1523
|
+
],
|
|
1524
|
+
},
|
|
1525
|
+
];
|
|
1526
|
+
|
|
1527
|
+
const result = stripNowScratchpad(messages);
|
|
1528
|
+
expect(result.length).toBe(0);
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
test("leaves messages without now_scratchpad untouched", () => {
|
|
1532
|
+
const messages: Message[] = [
|
|
1533
|
+
{
|
|
1534
|
+
role: "user",
|
|
1535
|
+
content: [{ type: "text", text: "Normal message" }],
|
|
1536
|
+
},
|
|
1537
|
+
];
|
|
1538
|
+
|
|
1539
|
+
const result = stripNowScratchpad(messages);
|
|
1540
|
+
expect(result.length).toBe(1);
|
|
1541
|
+
expect(result[0]).toBe(messages[0]); // Same reference — untouched
|
|
1542
|
+
});
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
// ---------------------------------------------------------------------------
|
|
1546
|
+
// stripInjectedContext removes now_scratchpad blocks
|
|
1547
|
+
// ---------------------------------------------------------------------------
|
|
1548
|
+
|
|
1549
|
+
describe("stripInjectedContext with now_scratchpad", () => {
|
|
1550
|
+
test("strips now_scratchpad blocks alongside other injections", () => {
|
|
1551
|
+
const messages: Message[] = [
|
|
1552
|
+
{
|
|
1553
|
+
role: "user",
|
|
1554
|
+
content: [
|
|
1555
|
+
{
|
|
1556
|
+
type: "text",
|
|
1557
|
+
text: "<channel_capabilities>\nchannel: telegram\n</channel_capabilities>",
|
|
1558
|
+
},
|
|
1559
|
+
{ type: "text", text: "Hello" },
|
|
1560
|
+
{
|
|
1561
|
+
type: "text",
|
|
1562
|
+
text: "<now_scratchpad>\nCurrent focus\n</now_scratchpad>",
|
|
1563
|
+
},
|
|
1564
|
+
],
|
|
1565
|
+
},
|
|
1566
|
+
];
|
|
1567
|
+
|
|
1568
|
+
const result = stripInjectedContext(messages);
|
|
1569
|
+
expect(result.length).toBe(1);
|
|
1570
|
+
expect(result[0].content.length).toBe(1);
|
|
1571
|
+
expect((result[0].content[0] as { type: "text"; text: string }).text).toBe(
|
|
1572
|
+
"Hello",
|
|
1573
|
+
);
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
// ---------------------------------------------------------------------------
|
|
1578
|
+
// applyRuntimeInjections with nowScratchpad
|
|
1579
|
+
// ---------------------------------------------------------------------------
|
|
1580
|
+
|
|
1581
|
+
describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
1582
|
+
const baseMessages: Message[] = [
|
|
1583
|
+
{
|
|
1584
|
+
role: "user",
|
|
1585
|
+
content: [{ type: "text", text: "What should I do?" }],
|
|
1586
|
+
},
|
|
1587
|
+
];
|
|
1588
|
+
|
|
1589
|
+
test("injects now_scratchpad block when provided", () => {
|
|
1590
|
+
const result = applyRuntimeInjections(baseMessages, {
|
|
1591
|
+
nowScratchpad: "Current focus: fix the bug",
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
expect(result.length).toBe(1);
|
|
1595
|
+
expect(result[0].content.length).toBe(2);
|
|
1596
|
+
const injected = result[0].content[1];
|
|
1597
|
+
const text = (injected as { type: "text"; text: string }).text;
|
|
1598
|
+
expect(text).toContain("<now_scratchpad>");
|
|
1599
|
+
expect(text).toContain("Current focus: fix the bug");
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
test("appended block appears after user's original text content", () => {
|
|
1603
|
+
const result = applyRuntimeInjections(baseMessages, {
|
|
1604
|
+
nowScratchpad: "scratchpad notes",
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// Original text is first
|
|
1608
|
+
expect(
|
|
1609
|
+
(result[0].content[0] as { type: "text"; text: string }).text,
|
|
1610
|
+
).toBe("What should I do?");
|
|
1611
|
+
// Scratchpad is appended after
|
|
1612
|
+
expect(
|
|
1613
|
+
(result[0].content[1] as { type: "text"; text: string }).text,
|
|
1614
|
+
).toContain("<now_scratchpad>");
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
test("does not inject when nowScratchpad is null", () => {
|
|
1618
|
+
const result = applyRuntimeInjections(baseMessages, {
|
|
1619
|
+
nowScratchpad: null,
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
expect(result.length).toBe(1);
|
|
1623
|
+
expect(result[0].content.length).toBe(1);
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
test("does not inject when nowScratchpad is omitted", () => {
|
|
1627
|
+
const result = applyRuntimeInjections(baseMessages, {});
|
|
1628
|
+
|
|
1629
|
+
expect(result.length).toBe(1);
|
|
1630
|
+
expect(result[0].content.length).toBe(1);
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
test("skipped in minimal mode", () => {
|
|
1634
|
+
const result = applyRuntimeInjections(baseMessages, {
|
|
1635
|
+
nowScratchpad: "Current focus: fix the bug",
|
|
1636
|
+
mode: "minimal",
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
const allText = result[0].content
|
|
1640
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
1641
|
+
.map((b) => b.text)
|
|
1642
|
+
.join("\n");
|
|
1643
|
+
|
|
1644
|
+
expect(allText).not.toContain("<now_scratchpad>");
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
@@ -84,6 +84,9 @@ describe("local CES discovery", () => {
|
|
|
84
84
|
if (result.mode === "unavailable") {
|
|
85
85
|
expect(result.reason).toContain("CES executable not found");
|
|
86
86
|
expect(result.mode).toBe("unavailable");
|
|
87
|
+
} else if (result.mode === "local-source") {
|
|
88
|
+
// Source entry point exists in the monorepo — verify the success shape.
|
|
89
|
+
expect(result.sourcePath).toBeTruthy();
|
|
87
90
|
} else {
|
|
88
91
|
// Binary exists in this environment — verify the success shape.
|
|
89
92
|
expect(result.mode).toBe("local");
|
|
@@ -95,9 +98,9 @@ describe("local CES discovery", () => {
|
|
|
95
98
|
|
|
96
99
|
test("never returns a fallback or in-process mode", () => {
|
|
97
100
|
const result = discoverLocalCes();
|
|
98
|
-
// The result must be
|
|
101
|
+
// The result must be "local", "local-source", or "unavailable".
|
|
99
102
|
// There must never be a fallback mode like "in-process" or "degraded".
|
|
100
|
-
expect(["local", "unavailable"]).toContain(result.mode);
|
|
103
|
+
expect(["local", "local-source", "unavailable"]).toContain(result.mode);
|
|
101
104
|
});
|
|
102
105
|
});
|
|
103
106
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Tests for CES (Credential Execution Service) feature gates.
|
|
3
3
|
*
|
|
4
4
|
* Verifies:
|
|
5
|
-
* -
|
|
6
|
-
* - Each flag can be
|
|
5
|
+
* - Each CES flag defaults to its registry-declared value.
|
|
6
|
+
* - Each flag can be toggled independently via config overrides.
|
|
7
7
|
* - Enabling CES flags does not implicitly change unrelated approval
|
|
8
8
|
* behavior or existing feature flags.
|
|
9
9
|
*/
|
|
@@ -54,28 +54,37 @@ const ALL_CES_FLAG_KEYS = [
|
|
|
54
54
|
CES_MANAGED_SIDECAR_FLAG_KEY,
|
|
55
55
|
] as const;
|
|
56
56
|
|
|
57
|
-
/** All CES predicate functions paired with their flag keys. */
|
|
57
|
+
/** All CES predicate functions paired with their flag keys and expected defaults. */
|
|
58
58
|
const ALL_CES_PREDICATES = [
|
|
59
|
-
{
|
|
59
|
+
{
|
|
60
|
+
name: "isCesToolsEnabled",
|
|
61
|
+
fn: isCesToolsEnabled,
|
|
62
|
+
key: CES_TOOLS_FLAG_KEY,
|
|
63
|
+
defaultEnabled: false,
|
|
64
|
+
},
|
|
60
65
|
{
|
|
61
66
|
name: "isCesShellLockdownEnabled",
|
|
62
67
|
fn: isCesShellLockdownEnabled,
|
|
63
68
|
key: CES_SHELL_LOCKDOWN_FLAG_KEY,
|
|
69
|
+
defaultEnabled: false,
|
|
64
70
|
},
|
|
65
71
|
{
|
|
66
72
|
name: "isCesSecureInstallEnabled",
|
|
67
73
|
fn: isCesSecureInstallEnabled,
|
|
68
74
|
key: CES_SECURE_INSTALL_FLAG_KEY,
|
|
75
|
+
defaultEnabled: false,
|
|
69
76
|
},
|
|
70
77
|
{
|
|
71
78
|
name: "isCesGrantAuditEnabled",
|
|
72
79
|
fn: isCesGrantAuditEnabled,
|
|
73
80
|
key: CES_GRANT_AUDIT_FLAG_KEY,
|
|
81
|
+
defaultEnabled: false,
|
|
74
82
|
},
|
|
75
83
|
{
|
|
76
84
|
name: "isCesManagedSidecarEnabled",
|
|
77
85
|
fn: isCesManagedSidecarEnabled,
|
|
78
86
|
key: CES_MANAGED_SIDECAR_FLAG_KEY,
|
|
87
|
+
defaultEnabled: true,
|
|
79
88
|
},
|
|
80
89
|
] as const;
|
|
81
90
|
|
|
@@ -92,21 +101,23 @@ describe("CES flag key format", () => {
|
|
|
92
101
|
});
|
|
93
102
|
|
|
94
103
|
// ---------------------------------------------------------------------------
|
|
95
|
-
//
|
|
104
|
+
// Defaults: each CES flag matches its registry-declared default
|
|
96
105
|
// ---------------------------------------------------------------------------
|
|
97
106
|
|
|
98
|
-
describe("CES flags
|
|
99
|
-
for (const { name, fn } of ALL_CES_PREDICATES) {
|
|
100
|
-
test(`${name} returns
|
|
107
|
+
describe("CES flags match registry defaults", () => {
|
|
108
|
+
for (const { name, fn, defaultEnabled } of ALL_CES_PREDICATES) {
|
|
109
|
+
test(`${name} returns ${defaultEnabled} with no config overrides`, () => {
|
|
101
110
|
const config = makeConfig();
|
|
102
|
-
expect(fn(config)).toBe(
|
|
111
|
+
expect(fn(config)).toBe(defaultEnabled);
|
|
103
112
|
});
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
for (const
|
|
107
|
-
test(`isAssistantFeatureFlagEnabled('${key}') returns
|
|
115
|
+
for (const pred of ALL_CES_PREDICATES) {
|
|
116
|
+
test(`isAssistantFeatureFlagEnabled('${pred.key}') returns ${pred.defaultEnabled} with no overrides`, () => {
|
|
108
117
|
const config = makeConfig();
|
|
109
|
-
expect(isAssistantFeatureFlagEnabled(key, config)).toBe(
|
|
118
|
+
expect(isAssistantFeatureFlagEnabled(pred.key, config)).toBe(
|
|
119
|
+
pred.defaultEnabled,
|
|
120
|
+
);
|
|
110
121
|
});
|
|
111
122
|
}
|
|
112
123
|
});
|
|
@@ -115,7 +126,7 @@ describe("CES flags default safely (all disabled)", () => {
|
|
|
115
126
|
// Independent enablement: each flag can be enabled without affecting others
|
|
116
127
|
// ---------------------------------------------------------------------------
|
|
117
128
|
|
|
118
|
-
describe("CES flags can be
|
|
129
|
+
describe("CES flags can be toggled independently", () => {
|
|
119
130
|
for (const { name, fn, key } of ALL_CES_PREDICATES) {
|
|
120
131
|
test(`enabling ${key} makes ${name} return true`, () => {
|
|
121
132
|
_setOverridesForTesting({ [key]: true });
|
|
@@ -123,12 +134,16 @@ describe("CES flags can be enabled independently", () => {
|
|
|
123
134
|
expect(fn(config)).toBe(true);
|
|
124
135
|
});
|
|
125
136
|
|
|
126
|
-
test(`enabling ${key} does not
|
|
137
|
+
test(`enabling ${key} does not change other CES flags from their defaults`, () => {
|
|
127
138
|
_setOverridesForTesting({ [key]: true });
|
|
128
139
|
const config = makeConfig();
|
|
129
|
-
for (const {
|
|
140
|
+
for (const {
|
|
141
|
+
fn: otherFn,
|
|
142
|
+
key: otherKey,
|
|
143
|
+
defaultEnabled,
|
|
144
|
+
} of ALL_CES_PREDICATES) {
|
|
130
145
|
if (otherKey === key) continue;
|
|
131
|
-
expect(otherFn(config)).toBe(
|
|
146
|
+
expect(otherFn(config)).toBe(defaultEnabled);
|
|
132
147
|
}
|
|
133
148
|
});
|
|
134
149
|
}
|
|
@@ -446,9 +446,9 @@ describe("managed OAuth materialization through CES sidecar", () => {
|
|
|
446
446
|
// ---------------------------------------------------------------------------
|
|
447
447
|
|
|
448
448
|
describe("feature-flag rollback safety", () => {
|
|
449
|
-
test("managed sidecar flag defaults to
|
|
449
|
+
test("managed sidecar flag defaults to true (enabled by default)", () => {
|
|
450
450
|
const config = makeConfig();
|
|
451
|
-
expect(isCesManagedSidecarEnabled(config)).toBe(
|
|
451
|
+
expect(isCesManagedSidecarEnabled(config)).toBe(true);
|
|
452
452
|
});
|
|
453
453
|
|
|
454
454
|
test("managed sidecar flag can be explicitly enabled", () => {
|
|
@@ -66,7 +66,7 @@ mock.module("../security/secure-keys.js", () => {
|
|
|
66
66
|
setSecureKeyAsync: async (key: string, value: string) =>
|
|
67
67
|
syncSet(key, value),
|
|
68
68
|
deleteSecureKeyAsync: async (key: string) => syncDelete(key),
|
|
69
|
-
listSecureKeysAsync: async () => [...storedKeys.keys()],
|
|
69
|
+
listSecureKeysAsync: async () => ({ accounts: [...storedKeys.keys()], unreachable: false }),
|
|
70
70
|
};
|
|
71
71
|
});
|
|
72
72
|
|
|
@@ -180,8 +180,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
180
180
|
"calls/twilio-rest.ts", // Twilio REST API credential lookup
|
|
181
181
|
"calls/fish-audio-client.ts", // Fish Audio TTS API key lookup
|
|
182
182
|
"runtime/channel-invite-transports/telegram.ts", // Telegram invite transport bot token lookup
|
|
183
|
-
"cli/
|
|
184
|
-
"cli/commands/credentials.ts", // CLI credential management commands
|
|
183
|
+
"cli/lib/daemon-credential-client.ts", // CLI-to-daemon credential routing intermediary
|
|
185
184
|
"messaging/providers/telegram-bot/adapter.ts", // Telegram bot token lookup for connectivity check
|
|
186
185
|
"runtime/channel-readiness-service.ts", // channel readiness probes for Telegram connectivity
|
|
187
186
|
"messaging/providers/whatsapp/adapter.ts", // WhatsApp credential lookup for connectivity check
|
|
@@ -198,9 +197,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
198
197
|
"daemon/conversation-messaging.ts", // credential storage during session messaging
|
|
199
198
|
"runtime/routes/settings-routes.ts", // settings routes OAuth credential lookup (client_secret)
|
|
200
199
|
"oauth/oauth-store.ts", // OAuth provider disconnect (delete stored tokens)
|
|
201
|
-
"cli/commands/oauth/connections.ts", // CLI OAuth connection delete (legacy credential cleanup)
|
|
202
200
|
"oauth/manual-token-connection.ts", // manual-token provider backfill (keychain credential existence check)
|
|
203
|
-
"cli/commands/doctor.ts", // CLI diagnostic API key verification via secure storage
|
|
204
201
|
"workspace/provider-commit-message-generator.ts", // commit message generation provider key lookup
|
|
205
202
|
"config/bundled-skills/transcribe/tools/transcribe-media.ts", // transcription tool API key lookup
|
|
206
203
|
"config/bundled-skills/image-studio/tools/media-generate-image.ts", // image generation tool API key lookup
|
|
@@ -213,7 +210,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
213
210
|
"memory/embedding-backend.ts", // embedding backend API key lookup
|
|
214
211
|
"daemon/providers-setup.ts", // provider initialization API key lookup
|
|
215
212
|
"workspace/migrations/006-services-config.ts", // services config migration reads provider API keys
|
|
216
|
-
"
|
|
213
|
+
"workspace/migrations/018-rekey-compound-credential-keys.ts", // re-key compound credential storage keys
|
|
217
214
|
"config/bundled-skills/slack/tools/shared.ts", // Slack skill bot token lookup
|
|
218
215
|
"daemon/conversation-process.ts", // masked provider key display
|
|
219
216
|
"daemon/handlers/config-model.ts", // masked provider key display
|
|
@@ -40,9 +40,10 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
40
40
|
}
|
|
41
41
|
return "not-found";
|
|
42
42
|
},
|
|
43
|
-
listSecureKeysAsync: async ()
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
listSecureKeysAsync: async () => ({
|
|
44
|
+
accounts: [...secureKeyStore.keys()],
|
|
45
|
+
unreachable: false,
|
|
46
|
+
}),
|
|
46
47
|
getSecureKeyAsync: async (account: string): Promise<string | undefined> => {
|
|
47
48
|
return secureKeyStore.get(account);
|
|
48
49
|
},
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
4
|
+
|
|
5
|
+
let fallbackValues = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
mock.module("../config/env.js", () => ({
|
|
8
|
+
getRuntimeHttpHost: () => "127.0.0.1",
|
|
9
|
+
getRuntimeHttpPort: () => 4123,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
mock.module("../daemon/daemon-control.js", () => ({
|
|
13
|
+
healthCheckHost: (host: string) => host,
|
|
14
|
+
isHttpHealthy: async () => true,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
mock.module("../runtime/auth/token-service.js", () => ({
|
|
18
|
+
initAuthSigningKey: () => {},
|
|
19
|
+
loadOrCreateSigningKey: () => "signing-key",
|
|
20
|
+
mintDaemonDeliveryToken: () => "daemon-token",
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
24
|
+
deleteSecureKeyAsync: async () => "deleted" as const,
|
|
25
|
+
getSecureKeyAsync: async (account: string) => fallbackValues.get(account),
|
|
26
|
+
getSecureKeyResultAsync: async (account: string) => ({
|
|
27
|
+
value: fallbackValues.get(account),
|
|
28
|
+
unreachable: false,
|
|
29
|
+
}),
|
|
30
|
+
setSecureKeyAsync: async () => true,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module("../util/logger.js", () => ({
|
|
34
|
+
getLogger: () =>
|
|
35
|
+
new Proxy({} as Record<string, unknown>, {
|
|
36
|
+
get: () => () => {},
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import {
|
|
41
|
+
getSecureKeyResultViaDaemon,
|
|
42
|
+
getSecureKeyViaDaemon,
|
|
43
|
+
} from "../cli/lib/daemon-credential-client.js";
|
|
44
|
+
|
|
45
|
+
const originalFetch = globalThis.fetch;
|
|
46
|
+
const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
|
|
47
|
+
|
|
48
|
+
function getRequestBody(index = 0): Record<string, unknown> {
|
|
49
|
+
const body = fetchCalls[index]?.init?.body;
|
|
50
|
+
if (typeof body !== "string") {
|
|
51
|
+
throw new Error("Expected fetch body to be a JSON string");
|
|
52
|
+
}
|
|
53
|
+
return JSON.parse(body) as Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
fallbackValues = new Map();
|
|
58
|
+
fetchCalls.length = 0;
|
|
59
|
+
const mockFetch = mock(
|
|
60
|
+
async (input: string | URL | Request, init?: RequestInit) => {
|
|
61
|
+
const url =
|
|
62
|
+
typeof input === "string"
|
|
63
|
+
? input
|
|
64
|
+
: input instanceof URL
|
|
65
|
+
? input.toString()
|
|
66
|
+
: input.url;
|
|
67
|
+
fetchCalls.push({ url, init });
|
|
68
|
+
return new Response(
|
|
69
|
+
JSON.stringify({ found: true, value: "secret-value" }),
|
|
70
|
+
{
|
|
71
|
+
status: 200,
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
globalThis.fetch = mockFetch as unknown as typeof globalThis.fetch;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
globalThis.fetch = originalFetch;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("daemon credential read requests", () => {
|
|
85
|
+
test("keeps provider secrets on the api_key path", async () => {
|
|
86
|
+
const value = await getSecureKeyViaDaemon("openai");
|
|
87
|
+
|
|
88
|
+
expect(value).toBe("secret-value");
|
|
89
|
+
expect(fetchCalls).toHaveLength(1);
|
|
90
|
+
expect(fetchCalls[0]?.url).toBe("http://127.0.0.1:4123/v1/secrets/read");
|
|
91
|
+
expect(getRequestBody()).toEqual({
|
|
92
|
+
type: "api_key",
|
|
93
|
+
name: "openai",
|
|
94
|
+
reveal: true,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("converts canonical credential keys into credential reads", async () => {
|
|
99
|
+
const value = await getSecureKeyViaDaemon(
|
|
100
|
+
credentialKey("vellum", "platform_base_url"),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(value).toBe("secret-value");
|
|
104
|
+
expect(getRequestBody()).toEqual({
|
|
105
|
+
type: "credential",
|
|
106
|
+
name: "vellum:platform_base_url",
|
|
107
|
+
reveal: true,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("preserves compound credential service names on metadata reads", async () => {
|
|
112
|
+
const result = await getSecureKeyResultViaDaemon(
|
|
113
|
+
credentialKey("integration:google", "client_secret"),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(result).toEqual({ value: "secret-value", unreachable: false });
|
|
117
|
+
expect(getRequestBody()).toEqual({
|
|
118
|
+
type: "credential",
|
|
119
|
+
name: "integration:google:client_secret",
|
|
120
|
+
reveal: true,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -128,6 +128,7 @@ describe("TwiML parameter propagation", () => {
|
|
|
128
128
|
transcriptionProvider: "deepgram",
|
|
129
129
|
ttsProvider: "google",
|
|
130
130
|
voice: "en-US-Standard-A",
|
|
131
|
+
interruptSensitivity: "low",
|
|
131
132
|
};
|
|
132
133
|
|
|
133
134
|
test("includes verificationSessionId as Parameter when provided", () => {
|