@vellumai/assistant 0.7.3 → 0.8.0
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 +29 -28
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/openapi.yaml +22 -4
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -26
- package/src/__tests__/context-search-pkb-source.test.ts +12 -6
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +3 -3
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -6
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
- package/src/__tests__/filing-service.test.ts +2 -19
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/injector-chain.test.ts +24 -16
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/relay-server.test.ts +46 -2
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-decision-primitive.ts +0 -13
- package/src/approvals/guardian-request-resolvers.ts +4 -32
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/memory-v2.ts +7 -7
- package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
- package/src/cli/commands/oauth/connect.ts +10 -52
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/loader.ts +72 -19
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
- package/src/daemon/conversation-agent-loop.ts +13 -10
- package/src/daemon/conversation-lifecycle.ts +22 -8
- package/src/daemon/conversation-surfaces.ts +16 -14
- package/src/daemon/conversation-tool-setup.ts +9 -5
- package/src/daemon/conversation.ts +1 -1
- package/src/daemon/handlers/shared.ts +26 -0
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +88 -73
- package/src/daemon/memory-v2-startup.ts +55 -14
- package/src/daemon/message-types/messages.ts +19 -1
- package/src/documents/document-store.ts +35 -1
- package/src/filing/filing-service.ts +2 -3
- package/src/heartbeat/heartbeat-service.ts +1 -1
- package/src/ipc/assistant-server.ts +93 -36
- package/src/ipc/skill-server.ts +99 -42
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
- package/src/memory/context-search/sources/memory-v2.ts +1 -17
- package/src/memory/context-search/sources/memory.ts +2 -2
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +32 -9
- package/src/memory/graph/graph-search.test.ts +6 -5
- package/src/memory/graph/graph-search.ts +3 -4
- package/src/memory/graph/retriever.test.ts +12 -7
- package/src/memory/graph/retriever.ts +4 -5
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +1 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-worker.ts +8 -4
- package/src/memory/pkb/pkb-search.test.ts +6 -5
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -0
- package/src/memory/search/semantic.ts +4 -5
- package/src/memory/v2/__tests__/activation.test.ts +35 -5
- package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
- package/src/memory/v2/__tests__/injection.test.ts +140 -23
- package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
- package/src/memory/v2/__tests__/sim.test.ts +118 -7
- package/src/memory/v2/__tests__/static-context.test.ts +1 -13
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/consolidation-job.ts +7 -8
- package/src/memory/v2/injection.ts +32 -12
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +5 -0
- package/src/memory/v2/qdrant.ts +209 -48
- package/src/memory/v2/sim.ts +67 -26
- package/src/memory/v2/static-context.ts +4 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +7 -0
- package/src/notifications/copy-composer.ts +46 -12
- package/src/notifications/decision-engine.ts +46 -0
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +1 -2
- package/src/proactive-artifact/job.test.ts +51 -4
- package/src/proactive-artifact/job.ts +16 -2
- package/src/proactive-artifact/message-copy.ts +18 -1
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/guardian-reply-router.ts +0 -10
- package/src/runtime/pending-interactions.ts +19 -15
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/debug-bash-routes.ts +2 -0
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
- package/src/runtime/routes/memory-item-routes.test.ts +3 -9
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +103 -17
- package/src/skills/include-graph.ts +35 -13
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/memory/register.test.ts +7 -5
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +19 -1
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +6 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for workspace migration `069-seed-onboarding-threads`.
|
|
3
|
+
*
|
|
4
|
+
* The migration writes onboarding bullet content to `memory/threads.md` only
|
|
5
|
+
* when the file exists and is empty (or whitespace-only). It must preserve
|
|
6
|
+
* any existing content and must be idempotent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
20
|
+
|
|
21
|
+
import { memoryV2InitMigration } from "../workspace/migrations/060-memory-v2-init.js";
|
|
22
|
+
import { seedOnboardingThreadsMigration } from "../workspace/migrations/069-seed-onboarding-threads.js";
|
|
23
|
+
|
|
24
|
+
let workspaceDir: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-migration-069-test-"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (existsSync(workspaceDir)) {
|
|
32
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function readThreads(): string {
|
|
37
|
+
return readFileSync(join(workspaceDir, "memory", "threads.md"), "utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("069-seed-onboarding-threads migration", () => {
|
|
41
|
+
test("has correct id and description", () => {
|
|
42
|
+
expect(seedOnboardingThreadsMigration.id).toBe(
|
|
43
|
+
"069-seed-onboarding-threads",
|
|
44
|
+
);
|
|
45
|
+
expect(seedOnboardingThreadsMigration.description).toContain("threads.md");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test.each([
|
|
49
|
+
["empty file", ""],
|
|
50
|
+
["whitespace-only file", " \n\n"],
|
|
51
|
+
])("seeds onboarding bullets when memory/threads.md is %s", (_, initial) => {
|
|
52
|
+
mkdirSync(join(workspaceDir, "memory"), { recursive: true });
|
|
53
|
+
writeFileSync(join(workspaceDir, "memory", "threads.md"), initial, "utf-8");
|
|
54
|
+
|
|
55
|
+
seedOnboardingThreadsMigration.run(workspaceDir);
|
|
56
|
+
|
|
57
|
+
const content = readThreads();
|
|
58
|
+
expect(content).toContain("Figure out what kind of personality");
|
|
59
|
+
expect(content).toContain("data/avatar/avatar-image.png");
|
|
60
|
+
expect(content).toContain("ChatGPT, Claude");
|
|
61
|
+
expect(content).toContain("Slack or Telegram");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("preserves existing content when memory/threads.md is non-empty", () => {
|
|
65
|
+
mkdirSync(join(workspaceDir, "memory"), { recursive: true });
|
|
66
|
+
const existing = "Follow up with Bob about the design review.\n";
|
|
67
|
+
writeFileSync(
|
|
68
|
+
join(workspaceDir, "memory", "threads.md"),
|
|
69
|
+
existing,
|
|
70
|
+
"utf-8",
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
seedOnboardingThreadsMigration.run(workspaceDir);
|
|
74
|
+
|
|
75
|
+
expect(readThreads()).toBe(existing);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("no-op when memory/threads.md does not exist", () => {
|
|
79
|
+
seedOnboardingThreadsMigration.run(workspaceDir);
|
|
80
|
+
expect(existsSync(join(workspaceDir, "memory", "threads.md"))).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("idempotent — second run does not duplicate or rewrite content", () => {
|
|
84
|
+
mkdirSync(join(workspaceDir, "memory"), { recursive: true });
|
|
85
|
+
writeFileSync(join(workspaceDir, "memory", "threads.md"), "", "utf-8");
|
|
86
|
+
|
|
87
|
+
seedOnboardingThreadsMigration.run(workspaceDir);
|
|
88
|
+
const afterFirst = readThreads();
|
|
89
|
+
|
|
90
|
+
seedOnboardingThreadsMigration.run(workspaceDir);
|
|
91
|
+
const afterSecond = readThreads();
|
|
92
|
+
|
|
93
|
+
expect(afterSecond).toBe(afterFirst);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("composes with 060: fresh workspace -> 060 -> 069 produces seeded threads.md", () => {
|
|
97
|
+
memoryV2InitMigration.run(workspaceDir);
|
|
98
|
+
seedOnboardingThreadsMigration.run(workspaceDir);
|
|
99
|
+
|
|
100
|
+
expect(readThreads()).toContain("Figure out what kind of personality");
|
|
101
|
+
// The other v2 prose files are untouched (still empty).
|
|
102
|
+
for (const filename of ["essentials.md", "recent.md", "buffer.md"]) {
|
|
103
|
+
expect(
|
|
104
|
+
readFileSync(join(workspaceDir, "memory", filename), "utf-8"),
|
|
105
|
+
).toBe("");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("down() is a no-op — seeded content remains", () => {
|
|
110
|
+
mkdirSync(join(workspaceDir, "memory"), { recursive: true });
|
|
111
|
+
writeFileSync(join(workspaceDir, "memory", "threads.md"), "", "utf-8");
|
|
112
|
+
|
|
113
|
+
seedOnboardingThreadsMigration.run(workspaceDir);
|
|
114
|
+
const seeded = readThreads();
|
|
115
|
+
|
|
116
|
+
seedOnboardingThreadsMigration.down(workspaceDir);
|
|
117
|
+
|
|
118
|
+
expect(readThreads()).toBe(seeded);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
afterAll,
|
|
12
|
+
afterEach,
|
|
13
|
+
beforeAll,
|
|
14
|
+
beforeEach,
|
|
15
|
+
describe,
|
|
16
|
+
expect,
|
|
17
|
+
test,
|
|
18
|
+
} from "bun:test";
|
|
19
|
+
|
|
20
|
+
import { removeSafeStorageReleaseNoteMigration } from "../workspace/migrations/071-remove-safe-storage-release-note.js";
|
|
21
|
+
|
|
22
|
+
const MIGRATION_ID = "071-remove-safe-storage-release-note";
|
|
23
|
+
const SAFE_STORAGE_MARKER =
|
|
24
|
+
"<!-- release-note-id:067-release-notes-safe-storage-limits -->";
|
|
25
|
+
const LATER_MARKER =
|
|
26
|
+
"<!-- release-note-id:068-release-notes-local-timezone -->";
|
|
27
|
+
|
|
28
|
+
const SAFE_STORAGE_RELEASE_NOTE = `${SAFE_STORAGE_MARKER}
|
|
29
|
+
## Safe storage limits
|
|
30
|
+
|
|
31
|
+
A new storage protection mode is available behind the safe-storage-limits
|
|
32
|
+
rollout flag. When enabled, the assistant watches workspace disk usage and
|
|
33
|
+
enters cleanup mode if the volume reaches the critical 95% threshold.
|
|
34
|
+
|
|
35
|
+
In cleanup mode, background processes pause and remote messages, including
|
|
36
|
+
trusted-contact messages, are blocked until the guardian frees enough space or
|
|
37
|
+
explicitly overrides the lock. The macOS app now shows a storage cleanup banner
|
|
38
|
+
that must be acknowledged before cleanup chat continues, then keeps a status
|
|
39
|
+
banner visible while cleanup mode is active.
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
let testRoot: string;
|
|
43
|
+
let workspaceDir: string;
|
|
44
|
+
|
|
45
|
+
beforeAll(() => {
|
|
46
|
+
testRoot = mkdtempSync(join(tmpdir(), "migration-071-remove-safe-storage-"));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(() => {
|
|
50
|
+
rmSync(testRoot, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
workspaceDir = mkdtempSync(join(testRoot, "ws-"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function updatesPath(): string {
|
|
62
|
+
return join(workspaceDir, "UPDATES.md");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("workspace migration 071-remove-safe-storage-release-note", () => {
|
|
66
|
+
test("has the correct id and description", () => {
|
|
67
|
+
expect(removeSafeStorageReleaseNoteMigration.id).toBe(MIGRATION_ID);
|
|
68
|
+
expect(removeSafeStorageReleaseNoteMigration.description).toContain(
|
|
69
|
+
"safe storage release note",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("missing UPDATES.md is a no-op", () => {
|
|
74
|
+
expect(existsSync(updatesPath())).toBe(false);
|
|
75
|
+
|
|
76
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
77
|
+
|
|
78
|
+
expect(existsSync(updatesPath())).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("removes UPDATES.md when it only contains the safe-storage bulletin", () => {
|
|
82
|
+
writeFileSync(updatesPath(), SAFE_STORAGE_RELEASE_NOTE, "utf-8");
|
|
83
|
+
|
|
84
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
85
|
+
|
|
86
|
+
expect(existsSync(updatesPath())).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("preserves prior unrelated release notes when removing the safe-storage block", () => {
|
|
90
|
+
const prior = `<!-- release-note-id:066-earlier-note -->
|
|
91
|
+
## Earlier note
|
|
92
|
+
|
|
93
|
+
This note should stay.
|
|
94
|
+
`;
|
|
95
|
+
writeFileSync(
|
|
96
|
+
updatesPath(),
|
|
97
|
+
`${prior}\n${SAFE_STORAGE_RELEASE_NOTE}`,
|
|
98
|
+
"utf-8",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
102
|
+
|
|
103
|
+
const content = readFileSync(updatesPath(), "utf-8");
|
|
104
|
+
expect(content).toContain("## Earlier note");
|
|
105
|
+
expect(content).toContain("This note should stay.");
|
|
106
|
+
expect(content).not.toContain(SAFE_STORAGE_MARKER);
|
|
107
|
+
expect(content).not.toContain("Safe storage limits");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("preserves a later release-note block after safe storage", () => {
|
|
111
|
+
const later = `${LATER_MARKER}
|
|
112
|
+
## Local timezone grounding
|
|
113
|
+
|
|
114
|
+
This later note should stay.
|
|
115
|
+
`;
|
|
116
|
+
writeFileSync(
|
|
117
|
+
updatesPath(),
|
|
118
|
+
`${SAFE_STORAGE_RELEASE_NOTE}\n${later}`,
|
|
119
|
+
"utf-8",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
123
|
+
|
|
124
|
+
const content = readFileSync(updatesPath(), "utf-8");
|
|
125
|
+
expect(content.startsWith(LATER_MARKER)).toBe(true);
|
|
126
|
+
expect(content).toContain("## Local timezone grounding");
|
|
127
|
+
expect(content).toContain("This later note should stay.");
|
|
128
|
+
expect(content).not.toContain(SAFE_STORAGE_MARKER);
|
|
129
|
+
expect(content).not.toContain("Safe storage limits");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("fallback preserves a later release-note block after a partial safe-storage block", () => {
|
|
133
|
+
const partialSafeStorage = `${SAFE_STORAGE_MARKER}
|
|
134
|
+
## Safe storage limits
|
|
135
|
+
|
|
136
|
+
Partially written note.
|
|
137
|
+
`;
|
|
138
|
+
const later = `${LATER_MARKER}
|
|
139
|
+
## Local timezone grounding
|
|
140
|
+
|
|
141
|
+
This later note should stay.
|
|
142
|
+
`;
|
|
143
|
+
writeFileSync(updatesPath(), `${partialSafeStorage}\n${later}`, "utf-8");
|
|
144
|
+
|
|
145
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
146
|
+
|
|
147
|
+
const content = readFileSync(updatesPath(), "utf-8");
|
|
148
|
+
expect(content.startsWith(LATER_MARKER)).toBe(true);
|
|
149
|
+
expect(content).toContain("## Local timezone grounding");
|
|
150
|
+
expect(content).not.toContain(SAFE_STORAGE_MARKER);
|
|
151
|
+
expect(content).not.toContain("Partially written note.");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("content without the safe-storage marker is byte-identical", () => {
|
|
155
|
+
const original =
|
|
156
|
+
"## Existing note\r\n\r\nNo safe storage marker appears here.\r\n";
|
|
157
|
+
writeFileSync(updatesPath(), original, "utf-8");
|
|
158
|
+
|
|
159
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
160
|
+
|
|
161
|
+
expect(readFileSync(updatesPath(), "utf-8")).toBe(original);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("preserves CRLF in unrelated content when removing the safe-storage block", () => {
|
|
165
|
+
const prior =
|
|
166
|
+
"<!-- release-note-id:066-earlier-note -->\r\n## Earlier note\r\n\r\nThis note should keep CRLF.\r\n";
|
|
167
|
+
writeFileSync(
|
|
168
|
+
updatesPath(),
|
|
169
|
+
`${prior}\r\n${SAFE_STORAGE_RELEASE_NOTE}`,
|
|
170
|
+
"utf-8",
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
174
|
+
|
|
175
|
+
expect(readFileSync(updatesPath(), "utf-8")).toBe(prior);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("running twice is idempotent", () => {
|
|
179
|
+
const prior = `<!-- release-note-id:066-earlier-note -->
|
|
180
|
+
## Earlier note
|
|
181
|
+
|
|
182
|
+
This note should stay.
|
|
183
|
+
`;
|
|
184
|
+
const later = `${LATER_MARKER}
|
|
185
|
+
## Local timezone grounding
|
|
186
|
+
|
|
187
|
+
This later note should stay.
|
|
188
|
+
`;
|
|
189
|
+
writeFileSync(
|
|
190
|
+
updatesPath(),
|
|
191
|
+
`${prior}\n${SAFE_STORAGE_RELEASE_NOTE}\n${later}`,
|
|
192
|
+
"utf-8",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
196
|
+
const afterFirst = readFileSync(updatesPath(), "utf-8");
|
|
197
|
+
|
|
198
|
+
removeSafeStorageReleaseNoteMigration.run(workspaceDir);
|
|
199
|
+
const afterSecond = readFileSync(updatesPath(), "utf-8");
|
|
200
|
+
|
|
201
|
+
expect(afterSecond).toBe(afterFirst);
|
|
202
|
+
expect(afterSecond).toContain("## Earlier note");
|
|
203
|
+
expect(afterSecond).toContain("## Local timezone grounding");
|
|
204
|
+
expect(afterSecond).not.toContain(SAFE_STORAGE_MARKER);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -12,7 +12,6 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
12
12
|
import { releaseNotesSafeStorageLimitsMigration } from "../workspace/migrations/067-release-notes-safe-storage-limits.js";
|
|
13
13
|
|
|
14
14
|
const MIGRATION_ID = "067-release-notes-safe-storage-limits";
|
|
15
|
-
const MARKER = `<!-- release-note-id:${MIGRATION_ID} -->`;
|
|
16
15
|
|
|
17
16
|
let workspaceDir: string;
|
|
18
17
|
|
|
@@ -28,10 +27,6 @@ function updatesPath(): string {
|
|
|
28
27
|
return join(workspaceDir, "UPDATES.md");
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
function markerCount(content: string): number {
|
|
32
|
-
return content.split(MARKER).length - 1;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
30
|
beforeEach(() => {
|
|
36
31
|
freshWorkspace();
|
|
37
32
|
});
|
|
@@ -47,44 +42,37 @@ describe("workspace migration 067-release-notes-safe-storage-limits", () => {
|
|
|
47
42
|
expect(releaseNotesSafeStorageLimitsMigration.id).toBe(MIGRATION_ID);
|
|
48
43
|
});
|
|
49
44
|
|
|
50
|
-
test("
|
|
45
|
+
test("does not create UPDATES.md when file is absent", () => {
|
|
51
46
|
expect(existsSync(updatesPath())).toBe(false);
|
|
52
47
|
|
|
53
48
|
releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
expect(content).toContain(MARKER);
|
|
57
|
-
expect(content).toContain("safe-storage-limits");
|
|
58
|
-
expect(content).toContain("critical 95% threshold");
|
|
59
|
-
expect(content).toContain("trusted-contact messages");
|
|
50
|
+
expect(existsSync(updatesPath())).toBe(false);
|
|
60
51
|
});
|
|
61
52
|
|
|
62
|
-
test("
|
|
63
|
-
|
|
53
|
+
test("leaves existing UPDATES.md byte-identical", () => {
|
|
54
|
+
const existing = "## Prior\n\nExisting release note.\n";
|
|
55
|
+
writeFileSync(updatesPath(), existing, "utf-8");
|
|
56
|
+
|
|
64
57
|
releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
|
|
65
58
|
|
|
66
|
-
|
|
67
|
-
expect(markerCount(content)).toBe(1);
|
|
68
|
-
expect(content.match(/Safe storage limits/g)?.length).toBe(1);
|
|
59
|
+
expect(readFileSync(updatesPath(), "utf-8")).toBe(existing);
|
|
69
60
|
});
|
|
70
61
|
|
|
71
|
-
test("
|
|
72
|
-
|
|
73
|
-
writeFileSync(updatesPath(), prior, "utf-8");
|
|
74
|
-
|
|
62
|
+
test("is idempotent when run twice in an empty workspace", () => {
|
|
63
|
+
releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
|
|
75
64
|
releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
|
|
76
65
|
|
|
77
|
-
|
|
78
|
-
expect(content.startsWith(prior)).toBe(true);
|
|
79
|
-
expect(content).toContain(MARKER);
|
|
66
|
+
expect(existsSync(updatesPath())).toBe(false);
|
|
80
67
|
});
|
|
81
68
|
|
|
82
|
-
test("is
|
|
83
|
-
const
|
|
84
|
-
writeFileSync(updatesPath(),
|
|
69
|
+
test("is idempotent when run twice with existing UPDATES.md", () => {
|
|
70
|
+
const existing = "## Prior\n\nExisting release note.\n";
|
|
71
|
+
writeFileSync(updatesPath(), existing, "utf-8");
|
|
85
72
|
|
|
73
|
+
releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
|
|
86
74
|
releaseNotesSafeStorageLimitsMigration.run(workspaceDir);
|
|
87
75
|
|
|
88
|
-
expect(readFileSync(updatesPath(), "utf-8")).toBe(
|
|
76
|
+
expect(readFileSync(updatesPath(), "utf-8")).toBe(existing);
|
|
89
77
|
});
|
|
90
78
|
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -96,6 +96,11 @@ export type AgentEvent =
|
|
|
96
96
|
matchedTrustRuleId?: string;
|
|
97
97
|
isContainerized?: boolean;
|
|
98
98
|
riskScopeOptions?: Array<{ pattern: string; label: string }>;
|
|
99
|
+
riskAllowlistOptions?: Array<{
|
|
100
|
+
label: string;
|
|
101
|
+
description: string;
|
|
102
|
+
pattern: string;
|
|
103
|
+
}>;
|
|
99
104
|
riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
|
|
100
105
|
approvalMode?: string;
|
|
101
106
|
approvalReason?: string;
|
|
@@ -282,6 +287,11 @@ export type LoopToolExecutor = (
|
|
|
282
287
|
matchedTrustRuleId?: string;
|
|
283
288
|
isContainerized?: boolean;
|
|
284
289
|
riskScopeOptions?: Array<{ pattern: string; label: string }>;
|
|
290
|
+
riskAllowlistOptions?: Array<{
|
|
291
|
+
label: string;
|
|
292
|
+
description: string;
|
|
293
|
+
pattern: string;
|
|
294
|
+
}>;
|
|
285
295
|
riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
|
|
286
296
|
approvalMode?: string;
|
|
287
297
|
approvalReason?: string;
|
|
@@ -1001,6 +1011,7 @@ export class AgentLoop {
|
|
|
1001
1011
|
matchedTrustRuleId: result.matchedTrustRuleId,
|
|
1002
1012
|
isContainerized: result.isContainerized,
|
|
1003
1013
|
riskScopeOptions: result.riskScopeOptions,
|
|
1014
|
+
riskAllowlistOptions: result.riskAllowlistOptions,
|
|
1004
1015
|
riskDirectoryScopeOptions: result.riskDirectoryScopeOptions,
|
|
1005
1016
|
approvalMode: result.approvalMode,
|
|
1006
1017
|
approvalReason: result.approvalReason,
|
|
@@ -355,12 +355,6 @@ export type CanonicalDecisionResult =
|
|
|
355
355
|
resolverFailed?: boolean;
|
|
356
356
|
resolverFailureReason?: string;
|
|
357
357
|
resolverReplyText?: string;
|
|
358
|
-
activatedContact?: {
|
|
359
|
-
sourceChannel: string;
|
|
360
|
-
externalUserId: string;
|
|
361
|
-
externalChatId?: string;
|
|
362
|
-
displayName?: string;
|
|
363
|
-
};
|
|
364
358
|
}
|
|
365
359
|
| {
|
|
366
360
|
applied: false;
|
|
@@ -527,9 +521,6 @@ export async function applyCanonicalGuardianDecision(
|
|
|
527
521
|
let resolverFailed = false;
|
|
528
522
|
let resolverFailureReason: string | undefined;
|
|
529
523
|
let resolverReplyText: string | undefined;
|
|
530
|
-
let activatedContact:
|
|
531
|
-
| { sourceChannel: string; externalUserId: string; externalChatId?: string; displayName?: string }
|
|
532
|
-
| undefined;
|
|
533
524
|
const resolver = getResolver(request.kind);
|
|
534
525
|
if (resolver) {
|
|
535
526
|
const resolverResult = await resolver.resolve({
|
|
@@ -558,9 +549,6 @@ export async function applyCanonicalGuardianDecision(
|
|
|
558
549
|
resolverFailureReason = resolverResult.reason;
|
|
559
550
|
} else {
|
|
560
551
|
resolverReplyText = resolverResult.guardianReplyText;
|
|
561
|
-
if (resolverResult.activatedContact) {
|
|
562
|
-
activatedContact = resolverResult.activatedContact;
|
|
563
|
-
}
|
|
564
552
|
}
|
|
565
553
|
} else {
|
|
566
554
|
log.info(
|
|
@@ -612,6 +600,5 @@ export async function applyCanonicalGuardianDecision(
|
|
|
612
600
|
grantMinted,
|
|
613
601
|
...(resolverFailed ? { resolverFailed, resolverFailureReason } : {}),
|
|
614
602
|
...(resolverReplyText ? { resolverReplyText } : {}),
|
|
615
|
-
...(activatedContact ? { activatedContact } : {}),
|
|
616
603
|
};
|
|
617
604
|
}
|
|
@@ -119,12 +119,6 @@ export type ResolverResult =
|
|
|
119
119
|
applied: true;
|
|
120
120
|
grantMinted?: boolean;
|
|
121
121
|
guardianReplyText?: string;
|
|
122
|
-
activatedContact?: {
|
|
123
|
-
sourceChannel: string;
|
|
124
|
-
externalUserId: string;
|
|
125
|
-
externalChatId?: string;
|
|
126
|
-
displayName?: string;
|
|
127
|
-
};
|
|
128
122
|
}
|
|
129
123
|
| { ok: false; reason: string };
|
|
130
124
|
|
|
@@ -192,28 +186,15 @@ const pendingInteractionResolver: GuardianRequestResolver = {
|
|
|
192
186
|
return { ok: false, reason: "pending_interaction_not_found" };
|
|
193
187
|
}
|
|
194
188
|
|
|
195
|
-
// Resolve the interaction: remove from tracker and get the session.
|
|
196
|
-
const resolved = pendingInteractions.resolve(request.id);
|
|
197
|
-
if (!resolved) {
|
|
198
|
-
// Race condition: interaction was consumed between get() and resolve().
|
|
199
|
-
log.warn(
|
|
200
|
-
{
|
|
201
|
-
event: "resolver_tool_approval_resolve_race",
|
|
202
|
-
requestId: request.id,
|
|
203
|
-
},
|
|
204
|
-
"Tool approval resolver: pending interaction consumed between lookup and resolve",
|
|
205
|
-
);
|
|
206
|
-
return { ok: false, reason: "pending_interaction_race" };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
189
|
// Map action to the permission system's UserDecision type and notify session.
|
|
190
|
+
// resolveConfirmation() owns pendingInteractions deregistration.
|
|
210
191
|
const userDecision: UserDecision =
|
|
211
192
|
decision.action === "reject" ? "deny" : "allow";
|
|
212
|
-
const conversation = findConversation(
|
|
193
|
+
const conversation = findConversation(interaction.conversationId);
|
|
213
194
|
if (!conversation) {
|
|
214
195
|
return {
|
|
215
196
|
ok: false,
|
|
216
|
-
reason: `conversation_not_found: ${
|
|
197
|
+
reason: `conversation_not_found: ${interaction.conversationId}`,
|
|
217
198
|
};
|
|
218
199
|
}
|
|
219
200
|
conversation.handleConfirmationResponse(
|
|
@@ -539,16 +520,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
539
520
|
);
|
|
540
521
|
});
|
|
541
522
|
|
|
542
|
-
return {
|
|
543
|
-
ok: true,
|
|
544
|
-
applied: true,
|
|
545
|
-
activatedContact: {
|
|
546
|
-
sourceChannel: "phone",
|
|
547
|
-
externalUserId: requesterExternalUserId,
|
|
548
|
-
...(requesterChatId ? { externalChatId: requesterChatId } : {}),
|
|
549
|
-
...(requesterDisplayName ? { displayName: requesterDisplayName } : {}),
|
|
550
|
-
},
|
|
551
|
-
};
|
|
523
|
+
return { ok: true, applied: true };
|
|
552
524
|
}
|
|
553
525
|
|
|
554
526
|
// Non-voice approvals: mint an identity-bound verification session so the
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { getConfig } from "../config/loader.js";
|
|
2
2
|
|
|
3
3
|
// Emergency/high-risk numbers that should never be called
|
|
4
|
-
const DENIED_NUMBERS = new Set([
|
|
5
|
-
"911",
|
|
6
|
-
"112",
|
|
7
|
-
"999",
|
|
8
|
-
"000",
|
|
9
|
-
"110",
|
|
10
|
-
"119",
|
|
11
|
-
]);
|
|
4
|
+
const DENIED_NUMBERS = new Set(["911", "112", "999", "000", "110", "119"]);
|
|
12
5
|
|
|
13
6
|
/**
|
|
14
7
|
* Check whether a phone number is a denied emergency number.
|
|
@@ -75,3 +68,7 @@ export function getGuardianWaitUpdateSteadyMaxIntervalMs(): number {
|
|
|
75
68
|
export function getSilenceTimeoutMs(): number {
|
|
76
69
|
return 30 * 1000; // 30 seconds
|
|
77
70
|
}
|
|
71
|
+
|
|
72
|
+
export function getEndCallListenWindowMs(): number {
|
|
73
|
+
return 15 * 1000;
|
|
74
|
+
}
|