@vellumai/assistant 0.8.11 → 0.8.12-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +633 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/bundler/app-compiler.ts +8 -0
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +225 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/security/redact-json.ts +61 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -2,6 +2,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
computeNextRunAt,
|
|
5
|
+
describeRRuleExpression,
|
|
6
|
+
isSingleFireRRule,
|
|
5
7
|
isValidScheduleExpression,
|
|
6
8
|
} from "../schedule/recurrence-engine.js";
|
|
7
9
|
|
|
@@ -117,3 +119,46 @@ describe("recurrence engine — rrule", () => {
|
|
|
117
119
|
expect(next).toBe(jan2);
|
|
118
120
|
});
|
|
119
121
|
});
|
|
122
|
+
|
|
123
|
+
describe("recurrence engine — rrule display helpers", () => {
|
|
124
|
+
const SINGLE_FIRE = "DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=1";
|
|
125
|
+
const WEEKLY = "DTSTART:20990612T080000\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE";
|
|
126
|
+
|
|
127
|
+
test("isSingleFireRRule detects COUNT=1 rules", () => {
|
|
128
|
+
expect(isSingleFireRRule(SINGLE_FIRE)).toBe(true);
|
|
129
|
+
expect(isSingleFireRRule(WEEKLY)).toBe(false);
|
|
130
|
+
expect(
|
|
131
|
+
isSingleFireRRule("DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=2"),
|
|
132
|
+
).toBe(false);
|
|
133
|
+
expect(isSingleFireRRule("not an rrule")).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("isSingleFireRRule is false for set constructs", () => {
|
|
137
|
+
expect(
|
|
138
|
+
isSingleFireRRule(
|
|
139
|
+
"DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=1\nRDATE:20990613T080000",
|
|
140
|
+
),
|
|
141
|
+
).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("describeRRuleExpression humanizes rules", () => {
|
|
145
|
+
expect(describeRRuleExpression(SINGLE_FIRE)).toBe("One-time");
|
|
146
|
+
expect(describeRRuleExpression(WEEKLY)).toBe(
|
|
147
|
+
"Every week on Monday, Wednesday",
|
|
148
|
+
);
|
|
149
|
+
expect(
|
|
150
|
+
describeRRuleExpression(
|
|
151
|
+
"DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=3",
|
|
152
|
+
),
|
|
153
|
+
).toBe("Every day for 3 times");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("describeRRuleExpression falls back instead of leaking raw text", () => {
|
|
157
|
+
expect(
|
|
158
|
+
describeRRuleExpression(
|
|
159
|
+
"DTSTART:20990612T080000\nRRULE:FREQ=DAILY\nEXDATE:20990613T080000",
|
|
160
|
+
),
|
|
161
|
+
).toBe("Custom recurrence");
|
|
162
|
+
expect(describeRRuleExpression("garbage")).toBe("Custom recurrence");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -492,6 +492,40 @@ describe("GET /schedules — default defer exclusion", () => {
|
|
|
492
492
|
});
|
|
493
493
|
});
|
|
494
494
|
|
|
495
|
+
test("humanizes rrule cadences and flags single-fire rrules as one-shot", () => {
|
|
496
|
+
createSchedule({
|
|
497
|
+
name: "RRule single fire",
|
|
498
|
+
cronExpression: "DTSTART:20990612T080000\nRRULE:FREQ=DAILY;COUNT=1",
|
|
499
|
+
syntax: "rrule",
|
|
500
|
+
message: "brush teeth",
|
|
501
|
+
});
|
|
502
|
+
createSchedule({
|
|
503
|
+
name: "RRule weekly",
|
|
504
|
+
cronExpression: "DTSTART:20990612T080000\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE",
|
|
505
|
+
syntax: "rrule",
|
|
506
|
+
message: "standup",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const route = findRoute("schedules", "GET");
|
|
510
|
+
const result = route.handler({}) as {
|
|
511
|
+
schedules: Array<{
|
|
512
|
+
name: string;
|
|
513
|
+
cadenceDescription: string;
|
|
514
|
+
isOneShot: boolean;
|
|
515
|
+
}>;
|
|
516
|
+
};
|
|
517
|
+
const byName = new Map(result.schedules.map((s) => [s.name, s]));
|
|
518
|
+
|
|
519
|
+
expect(byName.get("RRule single fire")).toMatchObject({
|
|
520
|
+
cadenceDescription: "One-time",
|
|
521
|
+
isOneShot: true,
|
|
522
|
+
});
|
|
523
|
+
expect(byName.get("RRule weekly")).toMatchObject({
|
|
524
|
+
cadenceDescription: "Every week on Monday, Wednesday",
|
|
525
|
+
isOneShot: false,
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
495
529
|
test("mutation responses also exclude deferred wakes", () => {
|
|
496
530
|
createSchedule({
|
|
497
531
|
name: "Agent schedule",
|
|
@@ -247,13 +247,18 @@ describe("session-manager", () => {
|
|
|
247
247
|
await startSession(session.id);
|
|
248
248
|
expect(getActiveSession(CONV_ID)).toBeDefined();
|
|
249
249
|
|
|
250
|
-
// Wait for the idle timeout to fire
|
|
251
|
-
|
|
250
|
+
// Wait for the idle timeout to fire and the async stop to finish.
|
|
251
|
+
// The session passes through "stopping" while the server closes, so
|
|
252
|
+
// poll for the terminal state rather than sleeping a fixed interval.
|
|
253
|
+
const deadline = Date.now() + 5000;
|
|
254
|
+
const findSession = () =>
|
|
255
|
+
getSessionsForConversation(CONV_ID).find((x) => x.id === session.id);
|
|
256
|
+
while (findSession()?.status !== "stopped" && Date.now() < deadline) {
|
|
257
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
258
|
+
}
|
|
252
259
|
|
|
253
260
|
expect(getActiveSession(CONV_ID)).toBeUndefined();
|
|
254
|
-
|
|
255
|
-
const s = all.find((x) => x.id === session.id);
|
|
256
|
-
expect(s?.status).toBe("stopped");
|
|
261
|
+
expect(findSession()?.status).toBe("stopped");
|
|
257
262
|
});
|
|
258
263
|
});
|
|
259
264
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared synthetic-secret fixtures for redaction tests.
|
|
3
|
+
*
|
|
4
|
+
* Constants only — per the test machinery isolation rule, shared helpers in
|
|
5
|
+
* `src/__tests__/` must not import from `src/`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A synthetic OpenAI project key that matches the scanner's
|
|
10
|
+
* `sk-proj-[A-Za-z0-9\-_]{40,}` pattern while deliberately dodging its
|
|
11
|
+
* placeholder filtering: no "test"/"example"/"xxxx"-style segments, not a
|
|
12
|
+
* repeated character, and it ends with an alphanumeric so the trailing `\b`
|
|
13
|
+
* boundary holds.
|
|
14
|
+
*/
|
|
15
|
+
export const SYNTHETIC_OPENAI_PROJECT_KEY =
|
|
16
|
+
"sk-proj-Ab1Cd2Ef3Gh4Ij5Kl6Mn7Op8Qr9St0Uv1Wx2Yz3Ab4Cd5Ef6Gh";
|
|
17
|
+
|
|
18
|
+
/** The marker `redactSecrets()` substitutes for the key above. */
|
|
19
|
+
export const OPENAI_PROJECT_KEY_REDACTION_MARKER =
|
|
20
|
+
'<redacted type="OpenAI Project Key" />';
|
|
@@ -347,6 +347,55 @@ describe("createSkillTool — required/type/enum validation", () => {
|
|
|
347
347
|
expect(result.content).toContain('mode must be one of "a", "b"');
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
+
test("coerces string booleans before validation and passes the coerced value to the executor", async () => {
|
|
351
|
+
const hash = computeSkillVersionHash(tempDir);
|
|
352
|
+
const tool = createSkillTool(
|
|
353
|
+
makeEntry({
|
|
354
|
+
executor: "echo.ts",
|
|
355
|
+
input_schema: {
|
|
356
|
+
type: "object",
|
|
357
|
+
properties: {
|
|
358
|
+
auto_open: { type: "boolean" },
|
|
359
|
+
name: { type: "string" },
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
tempDir,
|
|
364
|
+
hash,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const result = await tool.execute(
|
|
368
|
+
{ auto_open: "false", name: "x" },
|
|
369
|
+
makeContext(),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(result.isError).toBe(false);
|
|
373
|
+
const parsed = JSON.parse(result.content);
|
|
374
|
+
expect(parsed.input).toEqual({ auto_open: false, name: "x" });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("rejects non-coercible boolean strings with a self-correcting message", async () => {
|
|
378
|
+
const hash = computeSkillVersionHash(tempDir);
|
|
379
|
+
const tool = createSkillTool(
|
|
380
|
+
makeEntry({
|
|
381
|
+
executor: "echo.ts",
|
|
382
|
+
input_schema: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: { auto_open: { type: "boolean" } },
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
tempDir,
|
|
388
|
+
hash,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const result = await tool.execute({ auto_open: "yes" }, makeContext());
|
|
392
|
+
|
|
393
|
+
expect(result.isError).toBe(true);
|
|
394
|
+
expect(result.content).toContain(
|
|
395
|
+
"auto_open must be a boolean — pass true or false as a JSON boolean, not a string",
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
350
399
|
test("passes valid input through to the executor unchanged", async () => {
|
|
351
400
|
const hash = computeSkillVersionHash(tempDir);
|
|
352
401
|
const tool = createSkillTool(
|
|
@@ -7,7 +7,13 @@ import {
|
|
|
7
7
|
} from "../subagent/index.js";
|
|
8
8
|
|
|
9
9
|
/** All roles defined in the SubagentRole union. */
|
|
10
|
-
const ALL_ROLES: SubagentRole[] = [
|
|
10
|
+
const ALL_ROLES: SubagentRole[] = [
|
|
11
|
+
"general",
|
|
12
|
+
"researcher",
|
|
13
|
+
"coder",
|
|
14
|
+
"planner",
|
|
15
|
+
"investigator",
|
|
16
|
+
];
|
|
11
17
|
|
|
12
18
|
describe("SUBAGENT_ROLE_REGISTRY", () => {
|
|
13
19
|
test("covers all values in the SubagentRole union", () => {
|
|
@@ -64,6 +70,23 @@ describe("SUBAGENT_ROLE_REGISTRY", () => {
|
|
|
64
70
|
expect(SUBAGENT_ROLE_REGISTRY.planner.allowedTools!).toContain("recall");
|
|
65
71
|
});
|
|
66
72
|
|
|
73
|
+
test('investigator includes "bash" for grep/find-based code and log investigation', () => {
|
|
74
|
+
expect(SUBAGENT_ROLE_REGISTRY.investigator.allowedTools!).toContain("bash");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("investigator excludes file write tools (read-only investigation)", () => {
|
|
78
|
+
const tools = SUBAGENT_ROLE_REGISTRY.investigator.allowedTools!;
|
|
79
|
+
expect(tools).not.toContain("file_write");
|
|
80
|
+
expect(tools).not.toContain("file_edit");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("investigator preamble defines the root-cause report contract", () => {
|
|
84
|
+
const preamble = SUBAGENT_ROLE_REGISTRY.investigator.systemPromptPreamble;
|
|
85
|
+
expect(preamble).toContain("Root cause");
|
|
86
|
+
expect(preamble).toContain("Evidence");
|
|
87
|
+
expect(preamble).toContain("notify_parent");
|
|
88
|
+
});
|
|
89
|
+
|
|
67
90
|
test("no role references the old memory_recall tool name", () => {
|
|
68
91
|
for (const [_role, config] of Object.entries(SUBAGENT_ROLE_REGISTRY)) {
|
|
69
92
|
if (config.allowedTools !== undefined) {
|
|
@@ -110,6 +110,8 @@ mock.module("../credential-execution/managed-catalog.js", () => ({
|
|
|
110
110
|
// Import after mock
|
|
111
111
|
const { buildSystemPrompt, ensurePromptFiles, stripCommentLines } =
|
|
112
112
|
await import("../prompts/system-prompt.js");
|
|
113
|
+
const { SYSTEM_PROMPT_CACHE_BOUNDARY } =
|
|
114
|
+
await import("../prompts/cache-boundary.js");
|
|
113
115
|
|
|
114
116
|
describe("buildSystemPrompt", () => {
|
|
115
117
|
beforeEach(() => {
|
|
@@ -128,7 +130,6 @@ describe("buildSystemPrompt", () => {
|
|
|
128
130
|
"SOUL.md",
|
|
129
131
|
"USER.md",
|
|
130
132
|
"BOOTSTRAP.md",
|
|
131
|
-
"UPDATES.md",
|
|
132
133
|
"VOICE.md",
|
|
133
134
|
"skills",
|
|
134
135
|
"users",
|
|
@@ -603,16 +604,6 @@ describe("buildSystemPrompt", () => {
|
|
|
603
604
|
});
|
|
604
605
|
});
|
|
605
606
|
|
|
606
|
-
test("never includes UPDATES.md content in system prompt", () => {
|
|
607
|
-
const updatesBody = "# v1.2\n\nNew feature added. UNIQUE_UPDATES_MARKER.";
|
|
608
|
-
writeFileSync(join(TEST_DIR, "UPDATES.md"), updatesBody);
|
|
609
|
-
const result = buildSystemPrompt();
|
|
610
|
-
expect(result).not.toContain("## Recent Updates");
|
|
611
|
-
expect(result).not.toContain(updatesBody);
|
|
612
|
-
expect(result).not.toContain("UNIQUE_UPDATES_MARKER");
|
|
613
|
-
expect(result).not.toContain("Update Handling");
|
|
614
|
-
});
|
|
615
|
-
|
|
616
607
|
test("strips comment lines starting with _ from prompt files", () => {
|
|
617
608
|
writeFileSync(
|
|
618
609
|
join(TEST_DIR, "IDENTITY.md"),
|
|
@@ -879,6 +870,113 @@ describe("buildSystemPrompt", () => {
|
|
|
879
870
|
expect(orgIdx).toBeGreaterThan(parallelIdx);
|
|
880
871
|
});
|
|
881
872
|
|
|
873
|
+
describe("cache breakpoints", () => {
|
|
874
|
+
test("default breakpoint splits the prompt after 11-channel-persona", () => {
|
|
875
|
+
/**
|
|
876
|
+
* Tests that the bundled `cacheBreakpoint` on 11-channel-persona
|
|
877
|
+
* places the boundary marker between the stable sections and the
|
|
878
|
+
* volatile 12-voice section.
|
|
879
|
+
*/
|
|
880
|
+
// GIVEN a workspace where a volatile section (12-voice) renders
|
|
881
|
+
writeFileSync(join(TEST_DIR, "VOICE.md"), "- Prefers lowercase.");
|
|
882
|
+
|
|
883
|
+
// WHEN the system prompt is built
|
|
884
|
+
const result = buildSystemPrompt();
|
|
885
|
+
|
|
886
|
+
// THEN the boundary marker sits before the voice content and
|
|
887
|
+
// after the stable instruction sections
|
|
888
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
889
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
890
|
+
expect(result.indexOf("## Communication")).toBeLessThan(boundaryIdx);
|
|
891
|
+
expect(result.indexOf("# Voice Profile")).toBeGreaterThan(boundaryIdx);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
test("breakpoint splits even when the declaring section gates off", () => {
|
|
895
|
+
/**
|
|
896
|
+
* Tests that the split is resolved against the section ordering,
|
|
897
|
+
* not the rendered output — an absent channel persona must not
|
|
898
|
+
* merge the blocks around it.
|
|
899
|
+
*/
|
|
900
|
+
// GIVEN no channel persona file exists but a volatile section renders
|
|
901
|
+
writeFileSync(join(TEST_DIR, "VOICE.md"), "- Prefers lowercase.");
|
|
902
|
+
|
|
903
|
+
// WHEN the system prompt is built
|
|
904
|
+
const result = buildSystemPrompt();
|
|
905
|
+
|
|
906
|
+
// THEN the boundary marker is still present
|
|
907
|
+
expect(result).toContain(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
test("no boundary marker when nothing renders after the breakpoint", () => {
|
|
911
|
+
/**
|
|
912
|
+
* Tests that empty trailing blocks are dropped so the marker
|
|
913
|
+
* never dangles at the end of the prompt.
|
|
914
|
+
*/
|
|
915
|
+
// GIVEN a fresh workspace with no volatile sections (no VOICE.md,
|
|
916
|
+
// no BOOTSTRAP.md, no connections)
|
|
917
|
+
// WHEN the system prompt is built
|
|
918
|
+
const result = buildSystemPrompt();
|
|
919
|
+
|
|
920
|
+
// THEN no boundary marker appears
|
|
921
|
+
expect(result).not.toContain(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
test("workspace override of 11-channel-persona without cache_breakpoint clears the default", () => {
|
|
925
|
+
/**
|
|
926
|
+
* Tests that a workspace override takes full control of the
|
|
927
|
+
* section, including its breakpoint declaration.
|
|
928
|
+
*/
|
|
929
|
+
// GIVEN a workspace override of 11-channel-persona with no
|
|
930
|
+
// cache_breakpoint frontmatter, and a volatile section that renders
|
|
931
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
932
|
+
writeFileSync(
|
|
933
|
+
join(SYSTEM_PROMPTS_DIR, "11-channel-persona.md"),
|
|
934
|
+
"Override channel persona.\n",
|
|
935
|
+
);
|
|
936
|
+
writeFileSync(join(TEST_DIR, "VOICE.md"), "- Prefers lowercase.");
|
|
937
|
+
|
|
938
|
+
// WHEN the system prompt is built
|
|
939
|
+
const result = buildSystemPrompt();
|
|
940
|
+
|
|
941
|
+
// THEN the override body renders but no boundary marker appears
|
|
942
|
+
expect(result).toContain("Override channel persona.");
|
|
943
|
+
expect(result).not.toContain(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("frontmatter cache_breakpoint on a workspace section places the boundary there", () => {
|
|
947
|
+
/**
|
|
948
|
+
* Tests that users can declare the breakpoint position via
|
|
949
|
+
* `cache_breakpoint: true` frontmatter, and that only the first
|
|
950
|
+
* declaration (in id-sort order) is honored.
|
|
951
|
+
*/
|
|
952
|
+
// GIVEN a custom workspace section declaring a cache breakpoint
|
|
953
|
+
// ahead of the bundled default on 11-channel-persona
|
|
954
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
955
|
+
writeFileSync(
|
|
956
|
+
join(SYSTEM_PROMPTS_DIR, "00-aaa-custom.md"),
|
|
957
|
+
"---\ncache_breakpoint: true\n---\nCustom head section.\n",
|
|
958
|
+
);
|
|
959
|
+
writeFileSync(join(TEST_DIR, "VOICE.md"), "- Prefers lowercase.");
|
|
960
|
+
|
|
961
|
+
// WHEN the system prompt is built
|
|
962
|
+
const result = buildSystemPrompt();
|
|
963
|
+
|
|
964
|
+
// THEN exactly one boundary appears, right after the custom section
|
|
965
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
966
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
967
|
+
expect(result.indexOf("Custom head section.")).toBeLessThan(
|
|
968
|
+
boundaryIdx,
|
|
969
|
+
);
|
|
970
|
+
expect(result.indexOf("## Communication")).toBeGreaterThan(boundaryIdx);
|
|
971
|
+
|
|
972
|
+
// AND the bundled default declaration on 11-channel-persona is
|
|
973
|
+
// ignored — no second marker
|
|
974
|
+
expect(
|
|
975
|
+
result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY, boundaryIdx + 1),
|
|
976
|
+
).toBe(-1);
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
|
|
882
980
|
describe("containerized section (slot 02)", () => {
|
|
883
981
|
const CONTAINERIZED_FILE = join(
|
|
884
982
|
SYSTEM_PROMPTS_DIR,
|
|
@@ -532,6 +532,91 @@ describe("ToolApprovalHandler / pre-exec gate grant check", () => {
|
|
|
532
532
|
});
|
|
533
533
|
});
|
|
534
534
|
|
|
535
|
+
describe("ToolApprovalHandler / unparseable tool args gate", () => {
|
|
536
|
+
const handler = new ToolApprovalHandler();
|
|
537
|
+
const events: ToolLifecycleEvent[] = [];
|
|
538
|
+
const emitLifecycleEvent = (event: ToolLifecycleEvent) => {
|
|
539
|
+
events.push(event);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
beforeEach(() => {
|
|
543
|
+
clearTables();
|
|
544
|
+
events.length = 0;
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("input wrapped as { _raw } is rejected without executing", async () => {
|
|
548
|
+
const result = await handler.checkPreExecutionGates(
|
|
549
|
+
"bash",
|
|
550
|
+
{ _raw: '{"command": "ls -' },
|
|
551
|
+
makeContext({ trustClass: "guardian" }),
|
|
552
|
+
"sandbox",
|
|
553
|
+
"low",
|
|
554
|
+
Date.now(),
|
|
555
|
+
emitLifecycleEvent,
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
expect(result.allowed).toBe(false);
|
|
559
|
+
if (result.allowed) return;
|
|
560
|
+
expect(result.result.isError).toBe(true);
|
|
561
|
+
expect(result.result.content).toContain("were not valid JSON");
|
|
562
|
+
expect(result.result.content).toContain('{"command": "ls -');
|
|
563
|
+
expect(result.result.content).toContain("Retry");
|
|
564
|
+
|
|
565
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
566
|
+
expect(errorEvents).toHaveLength(1);
|
|
567
|
+
if (errorEvents[0].type === "error") {
|
|
568
|
+
expect(errorEvents[0].isExpected).toBe(true);
|
|
569
|
+
expect(errorEvents[0].errorCategory).toBe("tool_failure");
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("long raw args are truncated in the error message", async () => {
|
|
574
|
+
const raw = `{"data": "${"x".repeat(500)}`;
|
|
575
|
+
const result = await handler.checkPreExecutionGates(
|
|
576
|
+
"bash",
|
|
577
|
+
{ _raw: raw },
|
|
578
|
+
makeContext({ trustClass: "guardian" }),
|
|
579
|
+
"sandbox",
|
|
580
|
+
"low",
|
|
581
|
+
Date.now(),
|
|
582
|
+
emitLifecycleEvent,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
expect(result.allowed).toBe(false);
|
|
586
|
+
if (result.allowed) return;
|
|
587
|
+
expect(result.result.content).not.toContain(raw);
|
|
588
|
+
expect(result.result.content).toContain("…");
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("legitimate input containing a _raw field among others is not rejected", async () => {
|
|
592
|
+
const result = await handler.checkPreExecutionGates(
|
|
593
|
+
"bash",
|
|
594
|
+
{ _raw: "something", command: "ls" },
|
|
595
|
+
makeContext({ trustClass: "guardian" }),
|
|
596
|
+
"sandbox",
|
|
597
|
+
"low",
|
|
598
|
+
Date.now(),
|
|
599
|
+
emitLifecycleEvent,
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
expect(result.allowed).toBe(true);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("non-string _raw value is not treated as the marker", async () => {
|
|
606
|
+
const result = await handler.checkPreExecutionGates(
|
|
607
|
+
"bash",
|
|
608
|
+
{ _raw: 42 },
|
|
609
|
+
makeContext({ trustClass: "guardian" }),
|
|
610
|
+
"sandbox",
|
|
611
|
+
"low",
|
|
612
|
+
Date.now(),
|
|
613
|
+
emitLifecycleEvent,
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
expect(result.allowed).toBe(true);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
535
620
|
afterAll(() => {
|
|
536
621
|
mock.restore();
|
|
537
622
|
});
|
|
@@ -10,6 +10,10 @@ mock.module("../config/loader.js", () => ({
|
|
|
10
10
|
|
|
11
11
|
import { createToolAuditListener } from "../events/tool-audit-listener.js";
|
|
12
12
|
import type { ToolInvocationRecord } from "../memory/tool-usage-store.js";
|
|
13
|
+
import {
|
|
14
|
+
OPENAI_PROJECT_KEY_REDACTION_MARKER,
|
|
15
|
+
SYNTHETIC_OPENAI_PROJECT_KEY,
|
|
16
|
+
} from "./secret-fixtures.js";
|
|
13
17
|
|
|
14
18
|
describe("tool audit listener", () => {
|
|
15
19
|
beforeEach(() => {
|
|
@@ -101,6 +105,88 @@ describe("tool audit listener", () => {
|
|
|
101
105
|
expect(records[0].result).toContain("<redacted");
|
|
102
106
|
});
|
|
103
107
|
|
|
108
|
+
test("redacts known-pattern secrets in tool inputs before recording", () => {
|
|
109
|
+
const records: ToolInvocationRecord[] = [];
|
|
110
|
+
const listener = createToolAuditListener((record) => records.push(record));
|
|
111
|
+
|
|
112
|
+
const input = {
|
|
113
|
+
command: `export OPENAI_API_KEY="${SYNTHETIC_OPENAI_PROJECT_KEY}"`,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
listener({
|
|
117
|
+
type: "executed",
|
|
118
|
+
toolName: "bash",
|
|
119
|
+
input,
|
|
120
|
+
workingDir: "/tmp",
|
|
121
|
+
conversationId: "conv-input-redact",
|
|
122
|
+
riskLevel: "low",
|
|
123
|
+
decision: "allow",
|
|
124
|
+
durationMs: 5,
|
|
125
|
+
result: { content: "ok", isError: false },
|
|
126
|
+
});
|
|
127
|
+
listener({
|
|
128
|
+
type: "error",
|
|
129
|
+
toolName: "bash",
|
|
130
|
+
input,
|
|
131
|
+
workingDir: "/tmp",
|
|
132
|
+
conversationId: "conv-input-redact",
|
|
133
|
+
riskLevel: "low",
|
|
134
|
+
decision: "error",
|
|
135
|
+
durationMs: 5,
|
|
136
|
+
errorMessage: "boom",
|
|
137
|
+
isExpected: false,
|
|
138
|
+
errorCategory: "tool_failure",
|
|
139
|
+
});
|
|
140
|
+
listener({
|
|
141
|
+
type: "permission_denied",
|
|
142
|
+
toolName: "bash",
|
|
143
|
+
input,
|
|
144
|
+
workingDir: "/tmp",
|
|
145
|
+
conversationId: "conv-input-redact",
|
|
146
|
+
riskLevel: "high",
|
|
147
|
+
decision: "deny",
|
|
148
|
+
reason: "Permission denied by user",
|
|
149
|
+
durationMs: 5,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(records).toHaveLength(3);
|
|
153
|
+
for (const record of records) {
|
|
154
|
+
expect(record.input).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
155
|
+
// The stored input must remain parseable JSON: string leaves are
|
|
156
|
+
// redacted before stringification, so the marker lands inside a JSON
|
|
157
|
+
// string value (its quotes escaped) instead of corrupting the
|
|
158
|
+
// serialized form.
|
|
159
|
+
const parsed = JSON.parse(record.input) as { command: string };
|
|
160
|
+
expect(parsed.command).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("benign inputs round-trip byte-identical (no false-positive mangling)", () => {
|
|
165
|
+
const records: ToolInvocationRecord[] = [];
|
|
166
|
+
const listener = createToolAuditListener((record) => records.push(record));
|
|
167
|
+
|
|
168
|
+
const input = {
|
|
169
|
+
command: "git log --oneline -5 && echo done",
|
|
170
|
+
cwd: "/tmp/project",
|
|
171
|
+
env: { LANG: "en_US.UTF-8" },
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
listener({
|
|
175
|
+
type: "executed",
|
|
176
|
+
toolName: "bash",
|
|
177
|
+
input,
|
|
178
|
+
workingDir: "/tmp",
|
|
179
|
+
conversationId: "conv-benign-input",
|
|
180
|
+
riskLevel: "low",
|
|
181
|
+
decision: "allow",
|
|
182
|
+
durationMs: 2,
|
|
183
|
+
result: { content: "ok", isError: false },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(records).toHaveLength(1);
|
|
187
|
+
expect(records[0].input).toBe(JSON.stringify(input));
|
|
188
|
+
});
|
|
189
|
+
|
|
104
190
|
test("does not redact non-secret content like UUIDs or hashes", () => {
|
|
105
191
|
const records: ToolInvocationRecord[] = [];
|
|
106
192
|
const listener = createToolAuditListener((record) => records.push(record));
|