@vellumai/assistant 0.6.0 → 0.6.1
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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -30,9 +30,11 @@ export const conversations = sqliteTable(
|
|
|
30
30
|
forkParentMessageId: text("fork_parent_message_id"),
|
|
31
31
|
isAutoTitle: integer("is_auto_title").notNull().default(1),
|
|
32
32
|
scheduleJobId: text("schedule_job_id"),
|
|
33
|
+
lastMessageAt: integer("last_message_at"),
|
|
33
34
|
},
|
|
34
35
|
(table) => [
|
|
35
36
|
index("idx_conversations_updated_at").on(table.updatedAt),
|
|
37
|
+
index("idx_conversations_last_message_at").on(table.lastMessageAt),
|
|
36
38
|
index("idx_conversations_conversation_type").on(table.conversationType),
|
|
37
39
|
index("idx_conversations_fork_parent_conversation_id").on(
|
|
38
40
|
table.forkParentConversationId,
|
|
@@ -109,6 +111,18 @@ export const messageAttachments = sqliteTable("message_attachments", {
|
|
|
109
111
|
createdAt: integer("created_at").notNull(),
|
|
110
112
|
});
|
|
111
113
|
|
|
114
|
+
export const conversationGraphMemoryState = sqliteTable(
|
|
115
|
+
"conversation_graph_memory_state",
|
|
116
|
+
{
|
|
117
|
+
conversationId: text("conversation_id")
|
|
118
|
+
.primaryKey()
|
|
119
|
+
.references(() => conversations.id, { onDelete: "cascade" }),
|
|
120
|
+
stateJson: text("state_json").notNull(),
|
|
121
|
+
createdAt: integer("created_at").notNull(),
|
|
122
|
+
updatedAt: integer("updated_at").notNull(),
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
112
126
|
export const channelInboundEvents = sqliteTable("channel_inbound_events", {
|
|
113
127
|
id: text("id").primaryKey(),
|
|
114
128
|
sourceChannel: text("source_channel").notNull(),
|
|
@@ -25,6 +25,9 @@ export const cronJobs = sqliteTable("cron_jobs", {
|
|
|
25
25
|
routingHintsJson: text("routing_hints_json").notNull().default("{}"),
|
|
26
26
|
status: text("status").notNull().default("active"), // 'active' | 'firing' | 'fired' | 'cancelled'
|
|
27
27
|
quiet: integer("quiet", { mode: "boolean" }).notNull().default(false), // suppress completion notifications
|
|
28
|
+
reuseConversation: integer("reuse_conversation", { mode: "boolean" })
|
|
29
|
+
.notNull()
|
|
30
|
+
.default(false), // reuse the same conversation across runs
|
|
28
31
|
createdAt: integer("created_at").notNull(),
|
|
29
32
|
updatedAt: integer("updated_at").notNull(),
|
|
30
33
|
});
|
|
@@ -118,7 +121,10 @@ export const llmRequestLogs = sqliteTable(
|
|
|
118
121
|
responsePayload: text("response_payload").notNull(),
|
|
119
122
|
createdAt: integer("created_at").notNull(),
|
|
120
123
|
},
|
|
121
|
-
(table) => [
|
|
124
|
+
(table) => [
|
|
125
|
+
index("idx_llm_request_logs_message_id").on(table.messageId),
|
|
126
|
+
index("idx_llm_request_logs_created_at").on(table.createdAt),
|
|
127
|
+
],
|
|
122
128
|
);
|
|
123
129
|
|
|
124
130
|
export const memoryRecallLogs = sqliteTable(
|
|
@@ -144,6 +150,7 @@ export const memoryRecallLogs = sqliteTable(
|
|
|
144
150
|
topCandidatesJson: text("top_candidates_json").notNull(),
|
|
145
151
|
injectedText: text("injected_text"),
|
|
146
152
|
reason: text("reason"),
|
|
153
|
+
queryContext: text("query_context"),
|
|
147
154
|
createdAt: integer("created_at").notNull(),
|
|
148
155
|
},
|
|
149
156
|
(table) => [
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
blob,
|
|
3
3
|
index,
|
|
4
4
|
integer,
|
|
5
|
-
real,
|
|
6
5
|
sqliteTable,
|
|
7
6
|
text,
|
|
8
7
|
uniqueIndex,
|
|
@@ -32,56 +31,6 @@ export const memorySegments = sqliteTable(
|
|
|
32
31
|
(table) => [index("idx_memory_segments_scope_id").on(table.scopeId)],
|
|
33
32
|
);
|
|
34
33
|
|
|
35
|
-
export const memoryItems = sqliteTable(
|
|
36
|
-
"memory_items",
|
|
37
|
-
{
|
|
38
|
-
id: text("id").primaryKey(),
|
|
39
|
-
kind: text("kind").notNull(),
|
|
40
|
-
subject: text("subject").notNull(),
|
|
41
|
-
statement: text("statement").notNull(),
|
|
42
|
-
status: text("status").notNull(),
|
|
43
|
-
confidence: real("confidence").notNull(),
|
|
44
|
-
importance: real("importance"),
|
|
45
|
-
accessCount: integer("access_count").notNull().default(0),
|
|
46
|
-
fingerprint: text("fingerprint").notNull(),
|
|
47
|
-
verificationState: text("verification_state")
|
|
48
|
-
.notNull()
|
|
49
|
-
.default("assistant_inferred"),
|
|
50
|
-
scopeId: text("scope_id").notNull().default("default"),
|
|
51
|
-
firstSeenAt: integer("first_seen_at").notNull(),
|
|
52
|
-
lastSeenAt: integer("last_seen_at").notNull(),
|
|
53
|
-
lastUsedAt: integer("last_used_at"),
|
|
54
|
-
validFrom: integer("valid_from"),
|
|
55
|
-
invalidAt: integer("invalid_at"),
|
|
56
|
-
supersedes: text("supersedes"),
|
|
57
|
-
supersededBy: text("superseded_by"),
|
|
58
|
-
overrideConfidence: text("override_confidence").default("inferred"),
|
|
59
|
-
sourceType: text("source_type").notNull().default("extraction"),
|
|
60
|
-
sourceMessageRole: text("source_message_role"),
|
|
61
|
-
},
|
|
62
|
-
(table) => [
|
|
63
|
-
index("idx_memory_items_scope_id").on(table.scopeId),
|
|
64
|
-
index("idx_memory_items_fingerprint").on(table.fingerprint),
|
|
65
|
-
],
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
export const memoryItemSources = sqliteTable(
|
|
69
|
-
"memory_item_sources",
|
|
70
|
-
{
|
|
71
|
-
memoryItemId: text("memory_item_id")
|
|
72
|
-
.notNull()
|
|
73
|
-
.references(() => memoryItems.id, { onDelete: "cascade" }),
|
|
74
|
-
messageId: text("message_id")
|
|
75
|
-
.notNull()
|
|
76
|
-
.references(() => messages.id, { onDelete: "cascade" }),
|
|
77
|
-
evidence: text("evidence"),
|
|
78
|
-
createdAt: integer("created_at").notNull(),
|
|
79
|
-
},
|
|
80
|
-
(table) => [
|
|
81
|
-
index("idx_memory_item_sources_memory_item_id").on(table.memoryItemId),
|
|
82
|
-
],
|
|
83
|
-
);
|
|
84
|
-
|
|
85
34
|
export const memorySummaries = sqliteTable(
|
|
86
35
|
"memory_summaries",
|
|
87
36
|
{
|
|
@@ -137,3 +137,18 @@ export const memoryGraphTriggers = sqliteTable(
|
|
|
137
137
|
index("idx_graph_triggers_type").on(table.type),
|
|
138
138
|
],
|
|
139
139
|
);
|
|
140
|
+
|
|
141
|
+
export const memoryGraphNodeEdits = sqliteTable(
|
|
142
|
+
"memory_graph_node_edits",
|
|
143
|
+
{
|
|
144
|
+
id: text("id").primaryKey(),
|
|
145
|
+
nodeId: text("node_id")
|
|
146
|
+
.notNull()
|
|
147
|
+
.references(() => memoryGraphNodes.id, { onDelete: "cascade" }),
|
|
148
|
+
previousContent: text("previous_content").notNull(),
|
|
149
|
+
newContent: text("new_content").notNull(),
|
|
150
|
+
source: text("source").notNull(),
|
|
151
|
+
conversationId: text("conversation_id"),
|
|
152
|
+
created: integer("created").notNull(),
|
|
153
|
+
},
|
|
154
|
+
);
|
|
@@ -9,16 +9,29 @@ const log = getLogger("task-memory-cleanup");
|
|
|
9
9
|
* so the check survives daemon restarts.
|
|
10
10
|
*/
|
|
11
11
|
export function isConversationFailed(conversationId: string): boolean {
|
|
12
|
+
// For reused schedule conversations the same conversation_id appears in
|
|
13
|
+
// multiple cron_runs. A single failed run should NOT mark the conversation
|
|
14
|
+
// as permanently failed — only the *most recent* run for that conversation
|
|
15
|
+
// matters. We therefore check whether the latest cron_run (by created_at,
|
|
16
|
+
// which is a monotonically increasing epoch timestamp) has an error status.
|
|
17
|
+
// Note: cron_runs.id is a UUID v4 (random), so we cannot use MAX(id).
|
|
12
18
|
const row = rawGet<{ found: number }>(
|
|
13
19
|
`SELECT 1 AS found
|
|
14
20
|
FROM (
|
|
15
21
|
SELECT 1 FROM task_runs WHERE conversation_id = ? AND status = 'failed'
|
|
16
22
|
UNION ALL
|
|
17
|
-
SELECT 1 FROM cron_runs
|
|
23
|
+
SELECT 1 FROM cron_runs
|
|
24
|
+
WHERE conversation_id = ?
|
|
25
|
+
AND status = 'error'
|
|
26
|
+
AND id = (
|
|
27
|
+
SELECT id FROM cron_runs WHERE conversation_id = ?
|
|
28
|
+
ORDER BY created_at DESC LIMIT 1
|
|
29
|
+
)
|
|
18
30
|
)
|
|
19
31
|
LIMIT 1`,
|
|
20
32
|
conversationId,
|
|
21
33
|
conversationId,
|
|
34
|
+
conversationId,
|
|
22
35
|
);
|
|
23
36
|
return row != null;
|
|
24
37
|
}
|
|
@@ -57,9 +70,17 @@ export function invalidateAssistantInferredItemsForConversation(
|
|
|
57
70
|
AND tr.status = 'failed'
|
|
58
71
|
)
|
|
59
72
|
AND NOT EXISTS (
|
|
73
|
+
-- Check only the most recent cron_run for each conversation
|
|
74
|
+
-- so reused conversations with historical errors but recent
|
|
75
|
+
-- successes are still treated as valid corroborators.
|
|
60
76
|
SELECT 1 FROM cron_runs cr
|
|
61
77
|
WHERE cr.conversation_id = jc2.value
|
|
62
78
|
AND cr.status = 'error'
|
|
79
|
+
AND cr.id = (
|
|
80
|
+
SELECT cr2.id FROM cron_runs cr2
|
|
81
|
+
WHERE cr2.conversation_id = jc2.value
|
|
82
|
+
ORDER BY cr2.created_at DESC LIMIT 1
|
|
83
|
+
)
|
|
63
84
|
)
|
|
64
85
|
)`,
|
|
65
86
|
Date.now(),
|
|
@@ -79,9 +100,9 @@ export function invalidateAssistantInferredItemsForConversation(
|
|
|
79
100
|
|
|
80
101
|
/**
|
|
81
102
|
* Cancel all pending/running memory jobs referencing the given conversation.
|
|
82
|
-
* Covers every job type: `
|
|
103
|
+
* Covers every job type: `embed_attachment` (keyed by messageId),
|
|
83
104
|
* `embed_segment` (keyed by segmentId via memory_segments),
|
|
84
|
-
* `build_conversation_summary` (keyed by conversationId),
|
|
105
|
+
* `graph_extract`, `build_conversation_summary` (keyed by conversationId),
|
|
85
106
|
* and `embed_graph_node` (keyed by nodeId sourced from the conversation).
|
|
86
107
|
*/
|
|
87
108
|
export function cancelPendingJobsForConversation(
|
|
@@ -91,7 +112,7 @@ export function cancelPendingJobsForConversation(
|
|
|
91
112
|
const now = Date.now();
|
|
92
113
|
let total = 0;
|
|
93
114
|
|
|
94
|
-
// Jobs keyed by messageId:
|
|
115
|
+
// Jobs keyed by messageId: embed_attachment
|
|
95
116
|
total += rawRun(
|
|
96
117
|
`UPDATE memory_jobs
|
|
97
118
|
SET status = 'failed',
|
|
@@ -106,7 +127,7 @@ export function cancelPendingJobsForConversation(
|
|
|
106
127
|
conversationId,
|
|
107
128
|
);
|
|
108
129
|
|
|
109
|
-
// Jobs keyed by conversationId: build_conversation_summary
|
|
130
|
+
// Jobs keyed by conversationId: graph_extract, build_conversation_summary
|
|
110
131
|
total += rawRun(
|
|
111
132
|
`UPDATE memory_jobs
|
|
112
133
|
SET status = 'failed',
|
|
@@ -162,8 +183,8 @@ export function cancelPendingJobsForConversation(
|
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
/**
|
|
165
|
-
* Cancel only pending/running `
|
|
166
|
-
*
|
|
186
|
+
* Cancel only pending/running `graph_extract` jobs for the given
|
|
187
|
+
* conversation. Used by the task-failure path where we want to
|
|
167
188
|
* stop new extractions but must NOT cancel `embed_graph_node` jobs —
|
|
168
189
|
* those nodes may be multi-sourced and still valid.
|
|
169
190
|
*/
|
|
@@ -177,10 +198,8 @@ function cancelPendingExtractionJobsForConversation(
|
|
|
177
198
|
last_error = 'conversation_failed',
|
|
178
199
|
updated_at = ?
|
|
179
200
|
WHERE status IN ('pending', 'running')
|
|
180
|
-
AND type = '
|
|
181
|
-
AND json_extract(payload, '$.
|
|
182
|
-
SELECT id FROM messages WHERE conversation_id = ?
|
|
183
|
-
)`,
|
|
201
|
+
AND type = 'graph_extract'
|
|
202
|
+
AND json_extract(payload, '$.conversationId') = ?`,
|
|
184
203
|
now,
|
|
185
204
|
conversationId,
|
|
186
205
|
);
|
|
@@ -408,6 +408,92 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
|
|
|
408
408
|
};
|
|
409
409
|
},
|
|
410
410
|
|
|
411
|
+
"ingress.trusted_contact.guardian_decision": (payload) => {
|
|
412
|
+
const decision = str(payload.decision, "decided on");
|
|
413
|
+
const sourceChannel =
|
|
414
|
+
typeof payload.sourceChannel === "string"
|
|
415
|
+
? payload.sourceChannel
|
|
416
|
+
: undefined;
|
|
417
|
+
|
|
418
|
+
const requesterDisplayName =
|
|
419
|
+
typeof payload.requesterDisplayName === "string" &&
|
|
420
|
+
payload.requesterDisplayName.length > 0
|
|
421
|
+
? payload.requesterDisplayName
|
|
422
|
+
: undefined;
|
|
423
|
+
const requesterExternalUserId =
|
|
424
|
+
typeof payload.requesterExternalUserId === "string" &&
|
|
425
|
+
payload.requesterExternalUserId.length > 0
|
|
426
|
+
? payload.requesterExternalUserId
|
|
427
|
+
: undefined;
|
|
428
|
+
const requesterLabel = sanitizeIdentityField(
|
|
429
|
+
requesterDisplayName ??
|
|
430
|
+
(sourceChannel === "slack" &&
|
|
431
|
+
requesterExternalUserId &&
|
|
432
|
+
/^U[A-Z0-9]+$/i.test(requesterExternalUserId)
|
|
433
|
+
? `<@${requesterExternalUserId}>`
|
|
434
|
+
: requesterExternalUserId) ??
|
|
435
|
+
"Someone",
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const decidedByDisplayName =
|
|
439
|
+
typeof payload.decidedByDisplayName === "string" &&
|
|
440
|
+
payload.decidedByDisplayName.length > 0
|
|
441
|
+
? payload.decidedByDisplayName
|
|
442
|
+
: undefined;
|
|
443
|
+
const decidedByExternalUserId =
|
|
444
|
+
typeof payload.decidedByExternalUserId === "string" &&
|
|
445
|
+
payload.decidedByExternalUserId.length > 0
|
|
446
|
+
? payload.decidedByExternalUserId
|
|
447
|
+
: undefined;
|
|
448
|
+
const decidedByLabel = sanitizeIdentityField(
|
|
449
|
+
decidedByDisplayName ??
|
|
450
|
+
(sourceChannel === "slack" &&
|
|
451
|
+
decidedByExternalUserId &&
|
|
452
|
+
/^U[A-Z0-9]+$/i.test(decidedByExternalUserId)
|
|
453
|
+
? `<@${decidedByExternalUserId}>`
|
|
454
|
+
: decidedByExternalUserId) ??
|
|
455
|
+
"a guardian",
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const verb = decision === "approved" ? "approved" : "denied";
|
|
459
|
+
return {
|
|
460
|
+
title: "Trusted Contact Decision",
|
|
461
|
+
body: `${requesterLabel}'s access request has been ${verb} by ${decidedByLabel}.`,
|
|
462
|
+
};
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
"ingress.trusted_contact.denied": (payload) => {
|
|
466
|
+
const sourceChannel =
|
|
467
|
+
typeof payload.sourceChannel === "string"
|
|
468
|
+
? payload.sourceChannel
|
|
469
|
+
: undefined;
|
|
470
|
+
|
|
471
|
+
const requesterDisplayName =
|
|
472
|
+
typeof payload.requesterDisplayName === "string" &&
|
|
473
|
+
payload.requesterDisplayName.length > 0
|
|
474
|
+
? payload.requesterDisplayName
|
|
475
|
+
: undefined;
|
|
476
|
+
const requesterExternalUserId =
|
|
477
|
+
typeof payload.requesterExternalUserId === "string" &&
|
|
478
|
+
payload.requesterExternalUserId.length > 0
|
|
479
|
+
? payload.requesterExternalUserId
|
|
480
|
+
: undefined;
|
|
481
|
+
const requesterLabel = sanitizeIdentityField(
|
|
482
|
+
requesterDisplayName ??
|
|
483
|
+
(sourceChannel === "slack" &&
|
|
484
|
+
requesterExternalUserId &&
|
|
485
|
+
/^U[A-Z0-9]+$/i.test(requesterExternalUserId)
|
|
486
|
+
? `<@${requesterExternalUserId}>`
|
|
487
|
+
: requesterExternalUserId) ??
|
|
488
|
+
"Someone",
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
title: "Trusted Contact Denied",
|
|
493
|
+
body: `A trusted contact request from ${requesterLabel} has been denied.`,
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
|
|
411
497
|
"ingress.escalation": (payload) => ({
|
|
412
498
|
title: "Escalation",
|
|
413
499
|
body:
|
|
@@ -13,6 +13,7 @@ import { v4 as uuid } from "uuid";
|
|
|
13
13
|
|
|
14
14
|
import { getDeliverableChannels } from "../channels/config.js";
|
|
15
15
|
import { getConfig } from "../config/loader.js";
|
|
16
|
+
import { listGuardianChannels } from "../contacts/contact-store.js";
|
|
16
17
|
import { resolveGuardianPersona } from "../prompts/persona-resolver.js";
|
|
17
18
|
import { buildCoreIdentityContext } from "../prompts/system-prompt.js";
|
|
18
19
|
import {
|
|
@@ -73,6 +74,7 @@ function buildSystemPrompt(
|
|
|
73
74
|
preferenceContext?: string,
|
|
74
75
|
candidateContext?: string,
|
|
75
76
|
identityContext?: string,
|
|
77
|
+
recipientNotes?: string,
|
|
76
78
|
): string {
|
|
77
79
|
const sections: string[] = [
|
|
78
80
|
`You are a notification routing engine. Given a signal describing an event, decide whether the user should be notified, on which channel(s), and compose the notification copy.`,
|
|
@@ -89,6 +91,16 @@ function buildSystemPrompt(
|
|
|
89
91
|
);
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
if (recipientNotes) {
|
|
95
|
+
sections.push(
|
|
96
|
+
``,
|
|
97
|
+
`<recipient-context>`,
|
|
98
|
+
`The following are notes about the notification recipient. Use this context to tailor notification tone, formality, and content to the recipient's preferences.`,
|
|
99
|
+
recipientNotes,
|
|
100
|
+
`</recipient-context>`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
if (identityContext) {
|
|
93
105
|
sections.push(
|
|
94
106
|
``,
|
|
@@ -807,11 +819,34 @@ async function classifyWithLLM(
|
|
|
807
819
|
const identityContext = rawIdentityContext
|
|
808
820
|
? truncate(rawIdentityContext, MAX_IDENTITY_CONTEXT_CHARS, "\n…[truncated]")
|
|
809
821
|
: undefined;
|
|
822
|
+
|
|
823
|
+
// Resolve guardian contact notes for recipient context. Use the channel-
|
|
824
|
+
// agnostic guardian lookup so notes are available even when the only
|
|
825
|
+
// deliverable channel is "vellum" (which has no contact channel type).
|
|
826
|
+
let recipientNotes: string | undefined;
|
|
827
|
+
try {
|
|
828
|
+
const guardianResult = listGuardianChannels();
|
|
829
|
+
if (guardianResult?.contact.notes) {
|
|
830
|
+
recipientNotes = truncate(
|
|
831
|
+
guardianResult.contact.notes,
|
|
832
|
+
MAX_IDENTITY_CONTEXT_CHARS,
|
|
833
|
+
"\n…[truncated]",
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
} catch (err) {
|
|
837
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
838
|
+
log.warn(
|
|
839
|
+
{ err: errMsg },
|
|
840
|
+
"Failed to resolve guardian contact notes, proceeding without recipient context",
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
810
844
|
const systemPrompt = buildSystemPrompt(
|
|
811
845
|
availableChannels,
|
|
812
846
|
preferenceContext,
|
|
813
847
|
candidateContext,
|
|
814
848
|
identityContext,
|
|
849
|
+
recipientNotes,
|
|
815
850
|
);
|
|
816
851
|
const prompt = buildUserPrompt(signal);
|
|
817
852
|
const tool = buildDecisionTool(availableChannels);
|
|
@@ -677,7 +677,7 @@ export async function classifyRisk(
|
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
|
|
680
|
+
let result = await classifyRiskUncached(
|
|
681
681
|
toolName,
|
|
682
682
|
input,
|
|
683
683
|
workingDir,
|
|
@@ -685,6 +685,17 @@ export async function classifyRisk(
|
|
|
685
685
|
manifestOverride,
|
|
686
686
|
);
|
|
687
687
|
|
|
688
|
+
// Proxied bash commands route through the credential proxy which handles
|
|
689
|
+
// per-request approval separately. Cap the bash tool's own risk at Medium
|
|
690
|
+
// so trust rules can auto-allow the command execution.
|
|
691
|
+
if (
|
|
692
|
+
toolName === "bash" &&
|
|
693
|
+
input.network_mode === "proxied" &&
|
|
694
|
+
result === RiskLevel.High
|
|
695
|
+
) {
|
|
696
|
+
result = RiskLevel.Medium;
|
|
697
|
+
}
|
|
698
|
+
|
|
688
699
|
if (cacheKey) {
|
|
689
700
|
if (riskCache.size >= RISK_CACHE_MAX) {
|
|
690
701
|
const oldest = riskCache.keys().next().value;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton runtime store for the two-axis permission mode.
|
|
3
|
+
*
|
|
4
|
+
* Reads initial state from the `permissions` section of config.json on
|
|
5
|
+
* initialization and persists mutations back to the config file via the
|
|
6
|
+
* raw-config read/write helpers so env-var–derived keys are never leaked
|
|
7
|
+
* to disk.
|
|
8
|
+
*
|
|
9
|
+
* Downstream consumers (e.g. SSE broadcast) register change listeners
|
|
10
|
+
* via `onModeChanged()`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
invalidateConfigCache,
|
|
15
|
+
loadConfig,
|
|
16
|
+
loadRawConfig,
|
|
17
|
+
saveRawConfig,
|
|
18
|
+
} from "../config/loader.js";
|
|
19
|
+
import { getLogger } from "../util/logger.js";
|
|
20
|
+
import type { PermissionMode } from "./permission-mode.js";
|
|
21
|
+
import { DEFAULT_PERMISSION_MODE } from "./permission-mode.js";
|
|
22
|
+
|
|
23
|
+
const log = getLogger("permission-mode-store");
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Types
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export type ModeChangeListener = (mode: PermissionMode) => void;
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Module-level state
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
let currentMode: PermissionMode = { ...DEFAULT_PERMISSION_MODE };
|
|
36
|
+
let initialized = false;
|
|
37
|
+
const listeners: ModeChangeListener[] = [];
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Internal helpers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function notifyListeners(): void {
|
|
44
|
+
const snapshot = { ...currentMode };
|
|
45
|
+
for (const listener of listeners) {
|
|
46
|
+
try {
|
|
47
|
+
listener(snapshot);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
log.error({ err }, "Error in permission mode change listener");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Persist the current in-memory permission mode to config.json.
|
|
56
|
+
*
|
|
57
|
+
* Uses the raw-config pattern (loadRawConfig → mutate → saveRawConfig) so
|
|
58
|
+
* that env-var–derived fields (API keys, dataDir) are never written to disk.
|
|
59
|
+
*/
|
|
60
|
+
function persistToConfig(): void {
|
|
61
|
+
try {
|
|
62
|
+
const raw = loadRawConfig();
|
|
63
|
+
|
|
64
|
+
// Ensure the permissions object exists
|
|
65
|
+
if (
|
|
66
|
+
raw.permissions == null ||
|
|
67
|
+
typeof raw.permissions !== "object" ||
|
|
68
|
+
Array.isArray(raw.permissions)
|
|
69
|
+
) {
|
|
70
|
+
raw.permissions = {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const permissions = raw.permissions as Record<string, unknown>;
|
|
74
|
+
permissions.askBeforeActing = currentMode.askBeforeActing;
|
|
75
|
+
permissions.hostAccess = currentMode.hostAccess;
|
|
76
|
+
|
|
77
|
+
saveRawConfig(raw);
|
|
78
|
+
|
|
79
|
+
// Invalidate the cached config so the next loadConfig() picks up the
|
|
80
|
+
// persisted values rather than returning stale in-memory state.
|
|
81
|
+
invalidateConfigCache();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
log.error({ err }, "Failed to persist permission mode to config");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Public API
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Initialize the store from the current config. Safe to call multiple times;
|
|
93
|
+
* subsequent calls are no-ops unless `resetForTesting()` has been called.
|
|
94
|
+
*/
|
|
95
|
+
export function initPermissionModeStore(): void {
|
|
96
|
+
if (initialized) return;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const config = loadConfig();
|
|
100
|
+
currentMode = {
|
|
101
|
+
askBeforeActing: config.permissions.askBeforeActing,
|
|
102
|
+
hostAccess: config.permissions.hostAccess,
|
|
103
|
+
};
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log.warn(
|
|
106
|
+
{ err },
|
|
107
|
+
"Failed to load permission mode from config; using defaults",
|
|
108
|
+
);
|
|
109
|
+
currentMode = { ...DEFAULT_PERMISSION_MODE };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
initialized = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Return the current permission mode. Initializes from config on first call
|
|
117
|
+
* if `initPermissionModeStore()` hasn't been called yet.
|
|
118
|
+
*/
|
|
119
|
+
export function getMode(): PermissionMode {
|
|
120
|
+
if (!initialized) {
|
|
121
|
+
initPermissionModeStore();
|
|
122
|
+
}
|
|
123
|
+
return { ...currentMode };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Update the `askBeforeActing` axis. Persists to config.json and notifies
|
|
128
|
+
* change listeners.
|
|
129
|
+
*/
|
|
130
|
+
export function setAskBeforeActing(value: boolean): void {
|
|
131
|
+
if (!initialized) {
|
|
132
|
+
initPermissionModeStore();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (currentMode.askBeforeActing === value) return;
|
|
136
|
+
|
|
137
|
+
currentMode.askBeforeActing = value;
|
|
138
|
+
persistToConfig();
|
|
139
|
+
notifyListeners();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Update the `hostAccess` axis. Persists to config.json and notifies
|
|
144
|
+
* change listeners.
|
|
145
|
+
*/
|
|
146
|
+
export function setHostAccess(value: boolean): void {
|
|
147
|
+
if (!initialized) {
|
|
148
|
+
initPermissionModeStore();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (currentMode.hostAccess === value) return;
|
|
152
|
+
|
|
153
|
+
currentMode.hostAccess = value;
|
|
154
|
+
persistToConfig();
|
|
155
|
+
notifyListeners();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Register a callback that fires whenever the permission mode changes.
|
|
160
|
+
* Returns an unsubscribe function.
|
|
161
|
+
*/
|
|
162
|
+
export function onModeChanged(callback: ModeChangeListener): () => void {
|
|
163
|
+
listeners.push(callback);
|
|
164
|
+
return () => {
|
|
165
|
+
const idx = listeners.indexOf(callback);
|
|
166
|
+
if (idx >= 0) {
|
|
167
|
+
listeners.splice(idx, 1);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Reset the store to uninitialized state. **Test-only** — production code
|
|
174
|
+
* should never call this.
|
|
175
|
+
*/
|
|
176
|
+
export function resetForTesting(): void {
|
|
177
|
+
currentMode = { ...DEFAULT_PERMISSION_MODE };
|
|
178
|
+
initialized = false;
|
|
179
|
+
listeners.length = 0;
|
|
180
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Two-axis permission model:
|
|
5
|
+
* - `askBeforeActing` — LLM behavior toggle: when true the assistant checks in
|
|
6
|
+
* with the user before taking actions.
|
|
7
|
+
* - `hostAccess` — System-enforced gate: when true the assistant can execute
|
|
8
|
+
* commands on the host machine without prompting.
|
|
9
|
+
*/
|
|
10
|
+
export type PermissionMode = {
|
|
11
|
+
askBeforeActing: boolean;
|
|
12
|
+
hostAccess: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_PERMISSION_MODE: PermissionMode = {
|
|
16
|
+
askBeforeActing: true,
|
|
17
|
+
hostAccess: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const PermissionModeSchema = z.object({
|
|
21
|
+
askBeforeActing: z
|
|
22
|
+
.boolean({ error: "permissionMode.askBeforeActing must be a boolean" })
|
|
23
|
+
.default(true)
|
|
24
|
+
.describe("Whether the assistant should check in before taking actions"),
|
|
25
|
+
hostAccess: z
|
|
26
|
+
.boolean({ error: "permissionMode.hostAccess must be a boolean" })
|
|
27
|
+
.default(false)
|
|
28
|
+
.describe(
|
|
29
|
+
"Whether the assistant can execute commands on the host machine without prompting",
|
|
30
|
+
),
|
|
31
|
+
});
|
|
@@ -74,8 +74,17 @@ const HOST_TOOLS = new Set([
|
|
|
74
74
|
"host_file_write",
|
|
75
75
|
"host_file_edit",
|
|
76
76
|
"host_bash",
|
|
77
|
+
"computer_use_run_applescript",
|
|
77
78
|
]);
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Check whether a tool name is a host-level tool that requires the
|
|
82
|
+
* `hostAccess` permission to execute.
|
|
83
|
+
*/
|
|
84
|
+
export function isHostTool(toolName: string): boolean {
|
|
85
|
+
return HOST_TOOLS.has(toolName);
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
/** Safe local-only tools that are always workspace-scoped. */
|
|
80
89
|
const ALWAYS_SCOPED_TOOLS = new Set([
|
|
81
90
|
"skill_load",
|