@hailer/mcp 1.1.12 → 1.1.13
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/CHANGELOG.md +0 -7
- package/{.claude → dist}/CLAUDE.md +2 -2
- package/dist/app.js +18 -5
- package/dist/bot/bot-config.d.ts +10 -1
- package/dist/bot/bot-config.js +64 -3
- package/dist/bot/bot-manager.d.ts +2 -0
- package/dist/bot/bot-manager.js +9 -2
- package/dist/bot/bot.d.ts +33 -0
- package/dist/bot/bot.js +461 -160
- package/dist/bot/services/message-classifier.js +17 -0
- package/dist/bot/services/permission-guard.d.ts +52 -0
- package/dist/bot/services/permission-guard.js +149 -0
- package/dist/bot/services/types.d.ts +5 -0
- package/dist/bot/services/typing-indicator.d.ts +6 -1
- package/dist/bot/services/typing-indicator.js +19 -3
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +43 -0
- package/dist/core.js +3 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.d.ts +5 -0
- package/dist/mcp/UserContextCache.js +51 -19
- package/dist/mcp/hailer-clients.d.ts +19 -1
- package/dist/mcp/hailer-clients.js +158 -24
- package/dist/mcp/session-store.d.ts +68 -0
- package/dist/mcp/session-store.js +169 -0
- package/dist/mcp/signal-handler.js +2 -0
- package/dist/mcp/tool-registry.d.ts +17 -4
- package/dist/mcp/tool-registry.js +37 -7
- package/dist/mcp/tools/activity.js +99 -7
- package/dist/mcp/tools/app-scaffold.js +304 -336
- package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
- package/dist/mcp/tools/bot-config/constants.js +94 -0
- package/dist/mcp/tools/bot-config/core.d.ts +253 -0
- package/dist/mcp/tools/bot-config/core.js +2456 -0
- package/dist/mcp/tools/bot-config/index.d.ts +10 -0
- package/dist/mcp/tools/bot-config/index.js +59 -0
- package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
- package/dist/mcp/tools/bot-config/tools.js +15 -0
- package/dist/mcp/tools/bot-config/types.d.ts +50 -0
- package/dist/mcp/tools/bot-config/types.js +6 -0
- package/dist/mcp/tools/bug-fixer-tools.d.ts +45 -0
- package/dist/mcp/tools/bug-fixer-tools.js +1096 -0
- package/dist/mcp/tools/company.d.ts +9 -0
- package/dist/mcp/tools/company.js +88 -0
- package/dist/mcp/tools/discussion.js +68 -0
- package/dist/mcp/tools/document.d.ts +11 -0
- package/dist/mcp/tools/document.js +741 -0
- package/dist/mcp/tools/investigate.d.ts +9 -0
- package/dist/mcp/tools/investigate.js +254 -0
- package/dist/mcp/tools/workflow-permissions.d.ts +15 -0
- package/dist/mcp/tools/workflow-permissions.js +204 -0
- package/dist/mcp/tools/workflow.js +57 -18
- package/dist/mcp/utils/index.d.ts +2 -0
- package/dist/mcp/utils/index.js +12 -1
- package/dist/mcp/utils/role-utils.d.ts +74 -0
- package/dist/mcp/utils/role-utils.js +151 -0
- package/dist/mcp/utils/types.d.ts +43 -1
- package/dist/mcp/utils/types.js +14 -0
- package/dist/mcp/webhook-handler.d.ts +4 -0
- package/dist/mcp/webhook-handler.js +8 -0
- package/dist/mcp-server.d.ts +23 -2
- package/dist/mcp-server.js +639 -127
- package/dist/plugins/vipunen/client.d.ts +150 -0
- package/dist/plugins/vipunen/client.js +535 -0
- package/dist/plugins/vipunen/config/schema-config.json +19 -0
- package/dist/plugins/vipunen/config/schema-doc.json +22 -0
- package/dist/plugins/vipunen/index.d.ts +41 -0
- package/dist/plugins/vipunen/index.js +88 -0
- package/dist/plugins/vipunen/tools.d.ts +26 -0
- package/dist/plugins/vipunen/tools.js +501 -0
- package/dist/stdio-server.d.ts +14 -0
- package/dist/stdio-server.js +101 -0
- package/package.json +2 -1
- package/.claude/agents/agent-ada-skill-builder.md +0 -94
- package/.claude/agents/agent-alejandro-function-fields.md +0 -342
- package/.claude/agents/agent-bjorn-config-audit.md +0 -103
- package/.claude/agents/agent-builder-agent-creator.md +0 -130
- package/.claude/agents/agent-code-simplifier.md +0 -53
- package/.claude/agents/agent-dmitri-activity-crud.md +0 -159
- package/.claude/agents/agent-giuseppe-app-builder.md +0 -247
- package/.claude/agents/agent-gunther-mcp-tools.md +0 -39
- package/.claude/agents/agent-helga-workflow-config.md +0 -204
- package/.claude/agents/agent-igor-activity-mover-automation.md +0 -125
- package/.claude/agents/agent-ingrid-doc-templates.md +0 -261
- package/.claude/agents/agent-ivan-monolith.md +0 -154
- package/.claude/agents/agent-kenji-data-reader.md +0 -86
- package/.claude/agents/agent-lars-code-inspector.md +0 -102
- package/.claude/agents/agent-marco-mockup-builder.md +0 -110
- package/.claude/agents/agent-marcus-api-documenter.md +0 -323
- package/.claude/agents/agent-marketplace-publisher.md +0 -280
- package/.claude/agents/agent-marketplace-reviewer.md +0 -309
- package/.claude/agents/agent-permissions-handler.md +0 -208
- package/.claude/agents/agent-simple-writer.md +0 -48
- package/.claude/agents/agent-svetlana-code-review.md +0 -171
- package/.claude/agents/agent-tanya-test-runner.md +0 -333
- package/.claude/agents/agent-ui-designer.md +0 -100
- package/.claude/agents/agent-viktor-sql-insights.md +0 -212
- package/.claude/agents/agent-web-search.md +0 -55
- package/.claude/agents/agent-yevgeni-discussions.md +0 -45
- package/.claude/agents/agent-zara-zapier.md +0 -159
- package/.claude/commands/app-squad.md +0 -135
- package/.claude/commands/audit-squad.md +0 -158
- package/.claude/commands/autoplan.md +0 -563
- package/.claude/commands/cleanup-squad.md +0 -98
- package/.claude/commands/config-squad.md +0 -106
- package/.claude/commands/crud-squad.md +0 -87
- package/.claude/commands/data-squad.md +0 -97
- package/.claude/commands/debug-squad.md +0 -303
- package/.claude/commands/doc-squad.md +0 -65
- package/.claude/commands/handoff.md +0 -137
- package/.claude/commands/health.md +0 -49
- package/.claude/commands/help.md +0 -29
- package/.claude/commands/help:agents.md +0 -151
- package/.claude/commands/help:commands.md +0 -78
- package/.claude/commands/help:faq.md +0 -79
- package/.claude/commands/help:plugins.md +0 -50
- package/.claude/commands/help:skills.md +0 -93
- package/.claude/commands/help:tools.md +0 -75
- package/.claude/commands/hotfix-squad.md +0 -112
- package/.claude/commands/integration-squad.md +0 -82
- package/.claude/commands/janitor-squad.md +0 -167
- package/.claude/commands/learn-auto.md +0 -120
- package/.claude/commands/learn.md +0 -120
- package/.claude/commands/mcp-list.md +0 -27
- package/.claude/commands/onboard-squad.md +0 -140
- package/.claude/commands/plan-workspace.md +0 -732
- package/.claude/commands/prd.md +0 -130
- package/.claude/commands/project-status.md +0 -82
- package/.claude/commands/publish.md +0 -138
- package/.claude/commands/recap.md +0 -69
- package/.claude/commands/restore.md +0 -64
- package/.claude/commands/review-squad.md +0 -152
- package/.claude/commands/save.md +0 -24
- package/.claude/commands/stats.md +0 -19
- package/.claude/commands/swarm.md +0 -210
- package/.claude/commands/tool-builder.md +0 -39
- package/.claude/commands/ws-pull.md +0 -44
- package/.claude/hooks/_shared-memory.cjs +0 -305
- package/.claude/hooks/_utils.cjs +0 -108
- package/.claude/hooks/agent-failure-detector.cjs +0 -383
- package/.claude/hooks/agent-usage-logger.cjs +0 -204
- package/.claude/hooks/app-edit-guard.cjs +0 -494
- package/.claude/hooks/auto-learn.cjs +0 -304
- package/.claude/hooks/bash-guard.cjs +0 -272
- package/.claude/hooks/builder-mode-manager.cjs +0 -354
- package/.claude/hooks/bulk-activity-guard.cjs +0 -271
- package/.claude/hooks/context-watchdog.cjs +0 -230
- package/.claude/hooks/delegation-reminder.cjs +0 -465
- package/.claude/hooks/design-system-lint.cjs +0 -271
- package/.claude/hooks/post-scaffold-hook.cjs +0 -181
- package/.claude/hooks/prompt-guard.cjs +0 -354
- package/.claude/hooks/publish-template-guard.cjs +0 -147
- package/.claude/hooks/session-start.cjs +0 -35
- package/.claude/hooks/shared-memory-writer.cjs +0 -147
- package/.claude/hooks/skill-injector.cjs +0 -140
- package/.claude/hooks/skill-usage-logger.cjs +0 -258
- package/.claude/hooks/src-edit-guard.cjs +0 -240
- package/.claude/hooks/sync-marketplace-agents.cjs +0 -346
- package/.claude/settings.json +0 -257
- package/.claude/skills/SDK-activity-patterns/SKILL.md +0 -428
- package/.claude/skills/SDK-document-templates/SKILL.md +0 -1033
- package/.claude/skills/SDK-function-fields/SKILL.md +0 -542
- package/.claude/skills/SDK-generate-skill/SKILL.md +0 -92
- package/.claude/skills/SDK-init-skill/SKILL.md +0 -127
- package/.claude/skills/SDK-insight-queries/SKILL.md +0 -787
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +0 -1139
- package/.claude/skills/agent-structure/SKILL.md +0 -98
- package/.claude/skills/api-documentation-patterns/SKILL.md +0 -474
- package/.claude/skills/chrome-mcp-reference/SKILL.md +0 -370
- package/.claude/skills/delegation-routing/SKILL.md +0 -202
- package/.claude/skills/frontend-design/SKILL.md +0 -254
- package/.claude/skills/hailer-activity-mover/SKILL.md +0 -213
- package/.claude/skills/hailer-api-client/SKILL.md +0 -518
- package/.claude/skills/hailer-app-builder/SKILL.md +0 -1434
- package/.claude/skills/hailer-apps-pictures/SKILL.md +0 -269
- package/.claude/skills/hailer-design-system/SKILL.md +0 -235
- package/.claude/skills/hailer-monolith-automations/SKILL.md +0 -686
- package/.claude/skills/hailer-permissions-system/SKILL.md +0 -121
- package/.claude/skills/hailer-project-protocol/SKILL.md +0 -488
- package/.claude/skills/hailer-rest-api/SKILL.md +0 -61
- package/.claude/skills/hailer-rest-api/hailer-activities.md +0 -184
- package/.claude/skills/hailer-rest-api/hailer-admin.md +0 -473
- package/.claude/skills/hailer-rest-api/hailer-calendar.md +0 -256
- package/.claude/skills/hailer-rest-api/hailer-feed.md +0 -249
- package/.claude/skills/hailer-rest-api/hailer-insights.md +0 -195
- package/.claude/skills/hailer-rest-api/hailer-messaging.md +0 -276
- package/.claude/skills/hailer-rest-api/hailer-workflows.md +0 -283
- package/.claude/skills/insight-join-patterns/SKILL.md +0 -174
- package/.claude/skills/integration-patterns/SKILL.md +0 -421
- package/.claude/skills/json-only-output/SKILL.md +0 -72
- package/.claude/skills/lsp-setup/SKILL.md +0 -160
- package/.claude/skills/mcp-direct-tools/SKILL.md +0 -153
- package/.claude/skills/optional-parameters/SKILL.md +0 -72
- package/.claude/skills/publish-hailer-app/SKILL.md +0 -244
- package/.claude/skills/testing-patterns/SKILL.md +0 -630
- package/.claude/skills/tool-builder/SKILL.md +0 -250
- package/.claude/skills/tool-parameter-usage/SKILL.md +0 -126
- package/.claude/skills/tool-response-verification/SKILL.md +0 -92
- package/.claude/skills/zapier-hailer-patterns/SKILL.md +0 -581
- package/.mcp.json +0 -13
- package/.opencode/agent/agent-ada-skill-builder.md +0 -35
- package/.opencode/agent/agent-alejandro-function-fields.md +0 -39
- package/.opencode/agent/agent-bjorn-config-audit.md +0 -36
- package/.opencode/agent/agent-builder-agent-creator.md +0 -39
- package/.opencode/agent/agent-code-simplifier.md +0 -31
- package/.opencode/agent/agent-dmitri-activity-crud.md +0 -40
- package/.opencode/agent/agent-giuseppe-app-builder.md +0 -37
- package/.opencode/agent/agent-gunther-mcp-tools.md +0 -39
- package/.opencode/agent/agent-helga-workflow-config.md +0 -203
- package/.opencode/agent/agent-igor-activity-mover-automation.md +0 -46
- package/.opencode/agent/agent-ingrid-doc-templates.md +0 -39
- package/.opencode/agent/agent-ivan-monolith.md +0 -46
- package/.opencode/agent/agent-kenji-data-reader.md +0 -53
- package/.opencode/agent/agent-lars-code-inspector.md +0 -28
- package/.opencode/agent/agent-marco-mockup-builder.md +0 -42
- package/.opencode/agent/agent-marcus-api-documenter.md +0 -53
- package/.opencode/agent/agent-marketplace-publisher.md +0 -44
- package/.opencode/agent/agent-marketplace-reviewer.md +0 -42
- package/.opencode/agent/agent-permissions-handler.md +0 -50
- package/.opencode/agent/agent-simple-writer.md +0 -45
- package/.opencode/agent/agent-svetlana-code-review.md +0 -39
- package/.opencode/agent/agent-tanya-test-runner.md +0 -57
- package/.opencode/agent/agent-ui-designer.md +0 -56
- package/.opencode/agent/agent-viktor-sql-insights.md +0 -34
- package/.opencode/agent/agent-web-search.md +0 -42
- package/.opencode/agent/agent-yevgeni-discussions.md +0 -37
- package/.opencode/agent/agent-zara-zapier.md +0 -53
- package/.opencode/commands/app-squad.md +0 -135
- package/.opencode/commands/audit-squad.md +0 -158
- package/.opencode/commands/autoplan.md +0 -563
- package/.opencode/commands/cleanup-squad.md +0 -98
- package/.opencode/commands/config-squad.md +0 -106
- package/.opencode/commands/crud-squad.md +0 -87
- package/.opencode/commands/data-squad.md +0 -97
- package/.opencode/commands/debug-squad.md +0 -303
- package/.opencode/commands/doc-squad.md +0 -65
- package/.opencode/commands/handoff.md +0 -137
- package/.opencode/commands/health.md +0 -49
- package/.opencode/commands/help-agents.md +0 -151
- package/.opencode/commands/help-commands.md +0 -32
- package/.opencode/commands/help-faq.md +0 -29
- package/.opencode/commands/help-plugins.md +0 -28
- package/.opencode/commands/help-skills.md +0 -7
- package/.opencode/commands/help-tools.md +0 -40
- package/.opencode/commands/help.md +0 -28
- package/.opencode/commands/hotfix-squad.md +0 -112
- package/.opencode/commands/integration-squad.md +0 -82
- package/.opencode/commands/janitor-squad.md +0 -167
- package/.opencode/commands/learn-auto.md +0 -120
- package/.opencode/commands/learn.md +0 -120
- package/.opencode/commands/mcp-list.md +0 -27
- package/.opencode/commands/onboard-squad.md +0 -140
- package/.opencode/commands/plan-workspace.md +0 -732
- package/.opencode/commands/prd.md +0 -131
- package/.opencode/commands/project-status.md +0 -82
- package/.opencode/commands/publish.md +0 -138
- package/.opencode/commands/recap.md +0 -69
- package/.opencode/commands/restore.md +0 -64
- package/.opencode/commands/review-squad.md +0 -152
- package/.opencode/commands/save.md +0 -24
- package/.opencode/commands/stats.md +0 -19
- package/.opencode/commands/swarm.md +0 -210
- package/.opencode/commands/tool-builder.md +0 -39
- package/.opencode/commands/ws-pull.md +0 -44
- package/.opencode/opencode.json +0 -28
- package/SESSION-HANDOFF.md +0 -68
- package/inbox/2026-03-04-bot-config-patterns.md +0 -24
- package/scripts/postinstall.cjs +0 -64
- package/scripts/test-hal-tools.ts +0 -154
package/dist/bot/bot.js
CHANGED
|
@@ -15,12 +15,14 @@ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
|
15
15
|
const tool_registry_1 = require("../mcp/tool-registry");
|
|
16
16
|
const hailer_clients_1 = require("../mcp/hailer-clients");
|
|
17
17
|
const hailer_api_client_1 = require("../mcp/utils/hailer-api-client");
|
|
18
|
+
const types_1 = require("../mcp/utils/types");
|
|
18
19
|
const workspace_cache_1 = require("../mcp/workspace-cache");
|
|
19
20
|
const tool_executor_1 = require("./tool-executor");
|
|
20
21
|
const workspace_overview_1 = require("./workspace-overview");
|
|
21
22
|
const operation_logger_1 = require("./operation-logger");
|
|
22
23
|
const services_1 = require("./services");
|
|
23
24
|
const logger_1 = require("../lib/logger");
|
|
25
|
+
const permission_guard_1 = require("./services/permission-guard");
|
|
24
26
|
/**
|
|
25
27
|
* Tools available to the bot. Add/remove tool names here to control what the LLM can use.
|
|
26
28
|
* The MCP server (Claude Code terminal) is unaffected - it keeps full access to all tools.
|
|
@@ -59,34 +61,31 @@ const BOT_TOOLS = new Set([
|
|
|
59
61
|
'preview_insight',
|
|
60
62
|
'create_insight',
|
|
61
63
|
'update_insight',
|
|
64
|
+
// Permissions
|
|
65
|
+
'list_workflow_permissions',
|
|
66
|
+
'check_user_permissions',
|
|
67
|
+
'grant_workflow_permission',
|
|
68
|
+
'revoke_workflow_permission',
|
|
62
69
|
]);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'install_workflow',
|
|
70
|
+
/** Tools that require the requesting user to be a workspace admin or owner */
|
|
71
|
+
const ADMIN_ONLY_TOOLS = new Set([
|
|
72
|
+
'grant_workflow_permission',
|
|
73
|
+
'revoke_workflow_permission',
|
|
68
74
|
]);
|
|
69
|
-
/**
|
|
70
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Tools where the workflowId only appears in the result, not the args.
|
|
77
|
+
* These run first, then the result is checked for workflow access.
|
|
78
|
+
*/
|
|
79
|
+
const POST_CHECK_TOOLS = new Set([
|
|
71
80
|
'show_activity_by_id',
|
|
72
81
|
'fetch_discussion_messages',
|
|
73
82
|
'get_activity_from_discussion',
|
|
74
83
|
]);
|
|
75
84
|
/** Minimal tool set for SIMPLE-routed messages (greetings, quick lookups). */
|
|
76
|
-
const SIMPLE_TOOLS = new Set([
|
|
77
|
-
'list_workflows_minimal',
|
|
78
|
-
'list_activities',
|
|
79
|
-
'show_activity_by_id',
|
|
80
|
-
'count_activities',
|
|
81
|
-
'search_workspace_users',
|
|
82
|
-
'get_workspace_balance',
|
|
83
|
-
'list_my_discussions',
|
|
84
|
-
'fetch_discussion_messages',
|
|
85
|
-
'add_discussion_message',
|
|
86
|
-
'get_insight_data',
|
|
87
|
-
]);
|
|
88
85
|
const MODEL_HAIKU = 'claude-haiku-4-5-20251001';
|
|
89
86
|
const MODEL_SONNET = 'claude-sonnet-4-5-20250929';
|
|
87
|
+
const IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
|
|
88
|
+
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB — skip vision for larger images
|
|
90
89
|
const MAX_TOOL_ITERATIONS = 10;
|
|
91
90
|
const RATE_LIMIT_WINDOW = 60_000; // 60 seconds
|
|
92
91
|
const RATE_LIMIT_PER_DISCUSSION = 30; // max signals per discussion per window
|
|
@@ -104,8 +103,9 @@ class Bot {
|
|
|
104
103
|
workspaceOverview = '';
|
|
105
104
|
userId = '';
|
|
106
105
|
_workspaceId;
|
|
107
|
-
// Live-updatable
|
|
106
|
+
// Live-updatable config (can be hot-swapped without restart)
|
|
108
107
|
_systemPrompt;
|
|
108
|
+
_responseMode = 'always';
|
|
109
109
|
// Services
|
|
110
110
|
conversationManager = null;
|
|
111
111
|
messageClassifier = null;
|
|
@@ -113,6 +113,9 @@ class Bot {
|
|
|
113
113
|
typingIndicator = null;
|
|
114
114
|
tokenBilling = null;
|
|
115
115
|
sessionLogger = null;
|
|
116
|
+
hailerApi = null;
|
|
117
|
+
/** Cache of insightId → Set<workflowId> for permission checks on ID-based insight tools */
|
|
118
|
+
insightWorkflowCache = new Map();
|
|
116
119
|
opLogger = new operation_logger_1.OperationLogger();
|
|
117
120
|
// State
|
|
118
121
|
discussionStates = new Map();
|
|
@@ -134,22 +137,34 @@ class Bot {
|
|
|
134
137
|
permissionIndex = new Map();
|
|
135
138
|
adminUserIds = new Set();
|
|
136
139
|
ownerUserIds = new Set();
|
|
140
|
+
workspaceMemberIds = new Set();
|
|
141
|
+
membersLoaded = false;
|
|
137
142
|
// Config
|
|
138
143
|
config;
|
|
139
144
|
botManager; // BotManager ref for cross-bot awareness
|
|
140
145
|
constructor(config) {
|
|
141
146
|
this.config = {
|
|
142
|
-
|
|
147
|
+
email: config.email,
|
|
148
|
+
password: config.password,
|
|
149
|
+
apiHost: config.apiHost,
|
|
150
|
+
anthropicApiKey: config.anthropicApiKey,
|
|
143
151
|
model: config.model || 'claude-haiku-4-5-20251001',
|
|
152
|
+
toolRegistry: config.toolRegistry,
|
|
153
|
+
workspaceId: config.workspaceId,
|
|
154
|
+
systemPrompt: config.systemPrompt,
|
|
155
|
+
accessLevel: config.accessLevel || 'all',
|
|
156
|
+
allowedWorkflows: config.allowedWorkflows || [],
|
|
144
157
|
};
|
|
145
158
|
this.botManager = config.botManager || null;
|
|
146
159
|
this._systemPrompt = config.systemPrompt;
|
|
160
|
+
this._responseMode = config.responseMode || 'always';
|
|
147
161
|
this.logger = (0, logger_1.createLogger)({ component: 'Bot' });
|
|
148
162
|
this.clientManager = new hailer_clients_1.HailerClientManager(config.apiHost, config.email, config.password);
|
|
149
163
|
this.toolExecutor = new tool_executor_1.ToolExecutor(config.toolRegistry);
|
|
150
164
|
}
|
|
151
165
|
get email() { return this.config.email; }
|
|
152
166
|
get password() { return this.config.password; }
|
|
167
|
+
get accessLevel() { return this.config.accessLevel; }
|
|
153
168
|
/**
|
|
154
169
|
* Hot-update the system prompt without restarting the bot.
|
|
155
170
|
* Takes effect on the next message processed.
|
|
@@ -158,6 +173,10 @@ class Bot {
|
|
|
158
173
|
this._systemPrompt = prompt;
|
|
159
174
|
this.logger.info('System prompt updated live', { hasPrompt: !!prompt });
|
|
160
175
|
}
|
|
176
|
+
updateResponseMode(mode) {
|
|
177
|
+
this._responseMode = mode || 'always';
|
|
178
|
+
this.logger.info('Response mode updated live', { responseMode: this._responseMode });
|
|
179
|
+
}
|
|
161
180
|
get connected() {
|
|
162
181
|
return this._connected;
|
|
163
182
|
}
|
|
@@ -188,9 +207,9 @@ class Bot {
|
|
|
188
207
|
this.init = await this.client.socket.request('v2.core.init', [
|
|
189
208
|
['processes', 'users', 'network', 'networks', 'teams', 'groups'],
|
|
190
209
|
]);
|
|
210
|
+
(0, types_1.normalizeInitProcesses)(this.init);
|
|
191
211
|
this._workspaceId = this.init.network?._id || this.config.workspaceId;
|
|
192
|
-
|
|
193
|
-
// 4. Build workspace cache and overview
|
|
212
|
+
// 4. Build workspace cache and overview (before permission index so names resolve)
|
|
194
213
|
this.workspaceCache = (0, workspace_cache_1.createWorkspaceCache)(this.init, {
|
|
195
214
|
excludeTranslations: true,
|
|
196
215
|
excludeSystemMessages: true,
|
|
@@ -198,6 +217,8 @@ class Bot {
|
|
|
198
217
|
compactUserData: true,
|
|
199
218
|
includeWorkspaceNamesInTools: false,
|
|
200
219
|
});
|
|
220
|
+
this.backfillWorkspaceCacheMembers();
|
|
221
|
+
this.buildPermissionIndex();
|
|
201
222
|
this.workspaceOverview = (0, workspace_overview_1.generateWorkspaceOverview)(this.init);
|
|
202
223
|
// 5. Create Anthropic client
|
|
203
224
|
this.anthropic = new sdk_1.default({ apiKey: this.config.anthropicApiKey });
|
|
@@ -216,6 +237,7 @@ class Bot {
|
|
|
216
237
|
this.typingIndicator = new services_1.TypingIndicatorService(botConnection, this.logger);
|
|
217
238
|
// Token billing
|
|
218
239
|
const hailerApi = new hailer_api_client_1.HailerApiClient(this.client);
|
|
240
|
+
this.hailerApi = hailerApi;
|
|
219
241
|
this.tokenBilling = new services_1.TokenBillingService(this.logger, hailerApi);
|
|
220
242
|
// Message formatter - wrap ToolExecutor as MCP-style callback
|
|
221
243
|
const toolCallback = async (name, args) => {
|
|
@@ -226,6 +248,7 @@ class Bot {
|
|
|
226
248
|
this.sessionLogger = new services_1.SessionLoggerService(null, this.logger, toolCallback, () => this.getDefaultTeamId());
|
|
227
249
|
this.sessionLogger.setAnthropicClient(this.anthropic);
|
|
228
250
|
// 8. Build UserContext for tool execution
|
|
251
|
+
const currentWorkspaceId = this.init.network?._id || '';
|
|
229
252
|
this.userContext = {
|
|
230
253
|
client: this.client,
|
|
231
254
|
hailer: hailerApi,
|
|
@@ -235,6 +258,9 @@ class Bot {
|
|
|
235
258
|
createdAt: Date.now(),
|
|
236
259
|
email: this.config.email,
|
|
237
260
|
password: this.config.password,
|
|
261
|
+
workspaceRoles: { [currentWorkspaceId]: 'admin' },
|
|
262
|
+
currentWorkspaceId,
|
|
263
|
+
allowedGroups: ['read', 'write', 'bot_internal'],
|
|
238
264
|
};
|
|
239
265
|
// 9. Subscribe to messenger.new signals
|
|
240
266
|
this.signalHandler = (eventData) => {
|
|
@@ -254,6 +280,9 @@ class Bot {
|
|
|
254
280
|
this.clientManager.onSignal('process.updated', this.processUpdatedHandler);
|
|
255
281
|
this.clientManager.onSignal('workspace.updated', this.workspaceUpdatedHandler);
|
|
256
282
|
this.clientManager.onSignal('cache.invalidate', this.cacheInvalidateHandler);
|
|
283
|
+
// TODO: Subscribe to notification.reload for reminder handling
|
|
284
|
+
// Blocked: need to confirm reminderWorker is running and how the signal
|
|
285
|
+
// reaches the bot's socket (room join + event channel). See memory/project_reminder_bot_integration.md
|
|
257
286
|
this._connected = true;
|
|
258
287
|
this.logger.debug('Bot started', {
|
|
259
288
|
userId: this.userId,
|
|
@@ -301,6 +330,19 @@ class Bot {
|
|
|
301
330
|
this.client = null;
|
|
302
331
|
this.logger.debug('Bot stopped');
|
|
303
332
|
}
|
|
333
|
+
// ===== RESPONSE MODE =====
|
|
334
|
+
checkTriggerCondition(message) {
|
|
335
|
+
// Private DMs always pass — 1:1 conversations are always intentional
|
|
336
|
+
if (message.isPrivateDiscussion)
|
|
337
|
+
return true;
|
|
338
|
+
switch (this._responseMode) {
|
|
339
|
+
case 'mention_only': return message.isMention;
|
|
340
|
+
case 'reply_only': return message.isReplyToBot;
|
|
341
|
+
case 'mention_or_reply': return message.isMention || message.isReplyToBot;
|
|
342
|
+
case 'always':
|
|
343
|
+
default: return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
304
346
|
// ===== SIGNAL HANDLING =====
|
|
305
347
|
async handleSignal(signal) {
|
|
306
348
|
if (!this._connected || !this.messageClassifier)
|
|
@@ -367,37 +409,71 @@ class Bot {
|
|
|
367
409
|
return;
|
|
368
410
|
}
|
|
369
411
|
this.globalSignalTimestamps.push(now);
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
412
|
+
// Layer 1: Workspace membership check (fail-closed)
|
|
413
|
+
// Only process messages from verified workspace members.
|
|
414
|
+
// If member data hasn't loaded yet, reject — don't fail open.
|
|
415
|
+
if (!this.membersLoaded || !this.workspaceMemberIds.has(message.senderId)) {
|
|
416
|
+
this.logger.debug('Ignoring message - sender not a verified workspace member', {
|
|
373
417
|
senderId: message.senderId,
|
|
374
418
|
senderName: message.senderName,
|
|
419
|
+
membersLoaded: this.membersLoaded,
|
|
375
420
|
});
|
|
376
421
|
if (rawMsgId)
|
|
377
422
|
this.processedMessageIds.delete(rawMsgId);
|
|
378
423
|
return;
|
|
379
424
|
}
|
|
425
|
+
// Layer 2: AI Hub access gate — who can talk to this bot
|
|
426
|
+
if (this.config.accessLevel !== 'all') {
|
|
427
|
+
const isAdmin = this.adminUserIds.has(message.senderId);
|
|
428
|
+
const isOwner = this.ownerUserIds.has(message.senderId);
|
|
429
|
+
const allowed = this.config.accessLevel === 'owner' ? isOwner : (isAdmin || isOwner);
|
|
430
|
+
if (!allowed) {
|
|
431
|
+
this.logger.debug('Ignoring message - sender does not meet access level', {
|
|
432
|
+
senderId: message.senderId,
|
|
433
|
+
senderName: message.senderName,
|
|
434
|
+
requiredLevel: this.config.accessLevel,
|
|
435
|
+
isAdmin,
|
|
436
|
+
isOwner,
|
|
437
|
+
});
|
|
438
|
+
if (rawMsgId)
|
|
439
|
+
this.processedMessageIds.delete(rawMsgId);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
380
443
|
// Prune dedup set
|
|
381
444
|
if (this.processedMessageIds.size > 500) {
|
|
382
445
|
const ids = Array.from(this.processedMessageIds);
|
|
383
446
|
this.processedMessageIds = new Set(ids.slice(-250));
|
|
384
447
|
}
|
|
385
448
|
const state = this.getOrCreateDiscussionState(discId);
|
|
386
|
-
// Always buffer the message
|
|
449
|
+
// Always buffer the message (provides context when bot IS triggered later)
|
|
387
450
|
state.contextBuffer.push(message);
|
|
451
|
+
// Check response mode trigger condition BEFORE engagement logic
|
|
452
|
+
// DMs always pass — 1:1 conversations are always intentional
|
|
453
|
+
const triggerMet = this.checkTriggerCondition(message);
|
|
454
|
+
if (!triggerMet) {
|
|
455
|
+
return; // Signal dropped — no LLM call, no token cost
|
|
456
|
+
}
|
|
388
457
|
// Determine if this should trigger processing
|
|
389
|
-
|
|
458
|
+
// Private discussions (DMs) are always explicit triggers
|
|
459
|
+
const isExplicitTrigger = message.isMention || message.isReplyToBot || message.isPrivateDiscussion;
|
|
390
460
|
if (state.state === 'idle') {
|
|
391
|
-
|
|
461
|
+
// In 'always' mode, every message triggers engagement (original behavior)
|
|
462
|
+
if (isExplicitTrigger || this._responseMode === 'always') {
|
|
392
463
|
state.state = 'engaged';
|
|
393
464
|
state.consecutiveNonResponses = 0;
|
|
394
|
-
this.opLogger.engage(discId, message.isMention ? 'mention' : message.isReplyToBot ? 'reply' : 'dm');
|
|
465
|
+
this.opLogger.engage(discId, message.isMention ? 'mention' : message.isReplyToBot ? 'reply' : message.isDirectMessage ? 'dm' : 'always');
|
|
395
466
|
}
|
|
396
467
|
else {
|
|
397
468
|
// Not addressed while idle — message sits in buffer as context only
|
|
398
469
|
return;
|
|
399
470
|
}
|
|
400
471
|
}
|
|
472
|
+
// In non-'always' modes, each message must independently meet the trigger condition
|
|
473
|
+
// This prevents the bot from staying engaged and responding to everything after one mention
|
|
474
|
+
if (this._responseMode !== 'always' && !isExplicitTrigger) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
401
477
|
// State is 'engaged' — trigger processing if not already running
|
|
402
478
|
if (!this.processingDiscussions.has(discId)) {
|
|
403
479
|
this.processDiscussion(discId).catch(err => {
|
|
@@ -436,7 +512,17 @@ class Bot {
|
|
|
436
512
|
if (messages.length > 1) {
|
|
437
513
|
merged = `<context type="coalesced" count="${messages.length}">These ${messages.length} messages arrived while you were processing. Respond to the most recent/relevant.</context>\n\n${merged}`;
|
|
438
514
|
}
|
|
439
|
-
|
|
515
|
+
// Resolve image attachments for vision
|
|
516
|
+
const imageBlocks = await this.resolveImageAttachments(messages);
|
|
517
|
+
if (imageBlocks.length > 0) {
|
|
518
|
+
conversation.push({
|
|
519
|
+
role: 'user',
|
|
520
|
+
content: [{ type: 'text', text: merged }, ...imageBlocks],
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
conversation.push({ role: 'user', content: merged });
|
|
525
|
+
}
|
|
440
526
|
// Use last message for routing/billing context
|
|
441
527
|
const primaryMessage = messages[messages.length - 1];
|
|
442
528
|
state.lastProgressTime = 0;
|
|
@@ -498,7 +584,6 @@ class Bot {
|
|
|
498
584
|
const defaultRoute = {
|
|
499
585
|
model: MODEL_HAIKU,
|
|
500
586
|
maxTokens: 2000,
|
|
501
|
-
classification: 'SIMPLE',
|
|
502
587
|
};
|
|
503
588
|
try {
|
|
504
589
|
const wsName = this.init?.network?.name || 'Workspace';
|
|
@@ -540,12 +625,11 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
540
625
|
const route = {
|
|
541
626
|
model: MODEL_SONNET,
|
|
542
627
|
maxTokens: 16384,
|
|
543
|
-
classification: 'COMPLEX',
|
|
544
628
|
};
|
|
545
|
-
this.opLogger.route(message.discussionId,
|
|
629
|
+
this.opLogger.route(message.discussionId, 'COMPLEX', route.model, message.content);
|
|
546
630
|
return route;
|
|
547
631
|
}
|
|
548
|
-
this.opLogger.route(message.discussionId,
|
|
632
|
+
this.opLogger.route(message.discussionId, 'SIMPLE', defaultRoute.model, message.content);
|
|
549
633
|
return defaultRoute;
|
|
550
634
|
}
|
|
551
635
|
catch (error) {
|
|
@@ -611,7 +695,7 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
611
695
|
}
|
|
612
696
|
// ===== MESSAGE PROCESSING =====
|
|
613
697
|
async processMessage(message, signal) {
|
|
614
|
-
this.typingIndicator?.start(message.discussionId);
|
|
698
|
+
this.typingIndicator?.start(message.discussionId, 'Reading');
|
|
615
699
|
// Check token balance
|
|
616
700
|
const billingWorkspaceId = this._workspaceId || message.workspaceId;
|
|
617
701
|
if (this.tokenBilling && billingWorkspaceId) {
|
|
@@ -645,12 +729,13 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
645
729
|
async runLlmLoop(message, route, signal) {
|
|
646
730
|
const conversation = this.conversationManager.getConversation(message.discussionId);
|
|
647
731
|
const systemPrompt = this.buildSystemPrompt();
|
|
648
|
-
const tools = this.getAnthropicTools(
|
|
732
|
+
const tools = this.getAnthropicTools();
|
|
649
733
|
const processingStartTime = Date.now();
|
|
650
734
|
for (let i = 0; i < MAX_TOOL_ITERATIONS; i++) {
|
|
651
735
|
const cachedConversation = this.conversationManager.prepareForCaching(conversation);
|
|
652
736
|
let response;
|
|
653
737
|
let llmStart = Date.now();
|
|
738
|
+
this.typingIndicator?.updateStatus(message.discussionId, i === 0 ? 'Thinking' : 'Processing');
|
|
654
739
|
try {
|
|
655
740
|
response = await this.anthropic.messages.create({
|
|
656
741
|
model: route.model,
|
|
@@ -673,7 +758,8 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
673
758
|
this.logger.warn('Sonnet API call failed, falling back to Haiku', {
|
|
674
759
|
error: error instanceof Error ? error.message : String(error),
|
|
675
760
|
});
|
|
676
|
-
route = { model: MODEL_HAIKU, maxTokens: 2000
|
|
761
|
+
route = { model: MODEL_HAIKU, maxTokens: 2000 };
|
|
762
|
+
this.typingIndicator?.updateStatus(message.discussionId, 'Switching models');
|
|
677
763
|
llmStart = Date.now();
|
|
678
764
|
response = await this.anthropic.messages.create({
|
|
679
765
|
model: MODEL_HAIKU,
|
|
@@ -729,10 +815,11 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
729
815
|
// Check for tool calls
|
|
730
816
|
const toolUseBlocks = response.content.filter((b) => b.type === 'tool_use');
|
|
731
817
|
if (toolUseBlocks.length > 0) {
|
|
818
|
+
this.typingIndicator?.updateStatus(message.discussionId, this.getToolStatus(toolUseBlocks));
|
|
732
819
|
const toolResults = await this.executeTools(toolUseBlocks, message, signal);
|
|
733
820
|
conversation.push({ role: 'user', content: toolResults });
|
|
734
821
|
// Inject any messages that arrived during tool execution
|
|
735
|
-
this.injectPendingContext(message.discussionId, conversation);
|
|
822
|
+
await this.injectPendingContext(message.discussionId, conversation);
|
|
736
823
|
// Auto-escalate: if Haiku is failing tool calls, switch to Sonnet
|
|
737
824
|
if (route.model === MODEL_HAIKU) {
|
|
738
825
|
const failedCount = toolResults
|
|
@@ -743,7 +830,7 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
743
830
|
failedTools: failedCount,
|
|
744
831
|
iteration: i,
|
|
745
832
|
});
|
|
746
|
-
route = { model: MODEL_SONNET, maxTokens: 16384
|
|
833
|
+
route = { model: MODEL_SONNET, maxTokens: 16384 };
|
|
747
834
|
await this.sendMessage(message.discussionId, `Switching to a more capable model to handle this.`);
|
|
748
835
|
}
|
|
749
836
|
}
|
|
@@ -777,6 +864,7 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
777
864
|
break;
|
|
778
865
|
}
|
|
779
866
|
// Post the response
|
|
867
|
+
this.typingIndicator?.updateStatus(message.discussionId, 'Writing response');
|
|
780
868
|
this.typingIndicator?.stop(message.discussionId);
|
|
781
869
|
const formatted = await this.formatOutgoingMessage(responseText);
|
|
782
870
|
const links = this.messageFormatter.extractTagLinks(formatted);
|
|
@@ -793,17 +881,36 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
793
881
|
const WRITE_TOOLS = new Set(['create_activity', 'update_activity']);
|
|
794
882
|
for (const block of toolUseBlocks) {
|
|
795
883
|
const toolStart = Date.now();
|
|
796
|
-
//
|
|
884
|
+
// ── Admin-only gate ──
|
|
885
|
+
// Some tools require workspace admin/owner regardless of workflow access.
|
|
886
|
+
if (ADMIN_ONLY_TOOLS.has(block.name)) {
|
|
887
|
+
if (!this.adminUserIds.has(message.senderId) && !this.ownerUserIds.has(message.senderId)) {
|
|
888
|
+
this.opLogger.permDenied(discussionId, block.name, 'n/a', 'requires-admin');
|
|
889
|
+
results.push({
|
|
890
|
+
type: 'tool_result',
|
|
891
|
+
tool_use_id: block.id,
|
|
892
|
+
content: `Permission denied: Only workspace admins can modify workflow permissions.`,
|
|
893
|
+
is_error: true,
|
|
894
|
+
});
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// ── Unified permission gate ──
|
|
899
|
+
// Extract all workflow IDs from tool args (any shape) and check access.
|
|
900
|
+
// POST_CHECK_TOOLS are checked after execution (workflow ID is in the result, not the args).
|
|
797
901
|
const args = block.input;
|
|
798
|
-
const
|
|
799
|
-
if (
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
902
|
+
const permCtx = this.getPermissionContext();
|
|
903
|
+
if (!POST_CHECK_TOOLS.has(block.name)) {
|
|
904
|
+
const workflowIds = (0, permission_guard_1.extractWorkflowIdsFromArgs)(args, this.insightWorkflowCache);
|
|
905
|
+
const denied = (0, permission_guard_1.checkWorkflowAccess)(workflowIds, message.senderId, permCtx);
|
|
906
|
+
if (denied) {
|
|
907
|
+
this.opLogger.permDenied(discussionId, block.name, denied.workflowId, denied.reason);
|
|
803
908
|
results.push({
|
|
804
909
|
type: 'tool_result',
|
|
805
910
|
tool_use_id: block.id,
|
|
806
|
-
content:
|
|
911
|
+
content: denied.reason === 'bot-scope'
|
|
912
|
+
? `Permission denied: This bot does not have access to workflow ${denied.workflowId}.`
|
|
913
|
+
: `Permission denied: You do not have access to workflow ${denied.workflowId}.`,
|
|
807
914
|
is_error: true,
|
|
808
915
|
});
|
|
809
916
|
continue;
|
|
@@ -831,40 +938,32 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
831
938
|
const text = result?.content?.[0]?.text ?? JSON.stringify(result);
|
|
832
939
|
let contentStr = typeof text === 'string' ? text : JSON.stringify(text);
|
|
833
940
|
const toolDuration = (Date.now() - toolStart) / 1000;
|
|
834
|
-
// Post-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
return true;
|
|
844
|
-
const allowed = this.permissionIndex.get(wfId);
|
|
845
|
-
return !allowed || allowed.has(message.senderId);
|
|
846
|
-
});
|
|
847
|
-
if (parsed.workflows.length < before) {
|
|
848
|
-
this.opLogger.permFiltered(discussionId, block.name, before, parsed.workflows.length);
|
|
849
|
-
}
|
|
850
|
-
contentStr = JSON.stringify(parsed);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
catch {
|
|
854
|
-
// Not JSON or unexpected format, pass through unfiltered
|
|
941
|
+
// ── Post-execution permission checks ──
|
|
942
|
+
// Cache insight → workflow mapping after successful create_insight
|
|
943
|
+
if (block.name === 'create_insight' && !contentStr.includes('❌')) {
|
|
944
|
+
const idMatch = contentStr.match(/\*\*Insight ID:\*\*\s+`([a-f0-9]{24})`/);
|
|
945
|
+
if (idMatch) {
|
|
946
|
+
const sources = args.sources || [];
|
|
947
|
+
const wfIds = new Set(sources.map((s) => s.workflowId).filter(Boolean));
|
|
948
|
+
if (wfIds.size > 0)
|
|
949
|
+
this.insightWorkflowCache.set(idMatch[1], wfIds);
|
|
855
950
|
}
|
|
856
951
|
}
|
|
857
|
-
// Post-
|
|
858
|
-
|
|
952
|
+
// Post-filter list results: remove items the user can't access
|
|
953
|
+
contentStr = await this.postFilterListResults(block.name, contentStr, message.senderId, discussionId);
|
|
954
|
+
// Post-execution permission check for tools where workflowId is in the result
|
|
955
|
+
if (POST_CHECK_TOOLS.has(block.name)) {
|
|
859
956
|
const extractedWorkflowId = this.extractWorkflowIdFromResult(block.name, contentStr);
|
|
860
957
|
if (extractedWorkflowId) {
|
|
861
|
-
const
|
|
862
|
-
if (
|
|
863
|
-
this.opLogger.permDenied(discussionId, block.name,
|
|
958
|
+
const denied = (0, permission_guard_1.checkWorkflowAccess)([extractedWorkflowId], message.senderId, permCtx);
|
|
959
|
+
if (denied) {
|
|
960
|
+
this.opLogger.permDenied(discussionId, block.name, denied.workflowId, denied.reason);
|
|
864
961
|
results.push({
|
|
865
962
|
type: 'tool_result',
|
|
866
963
|
tool_use_id: block.id,
|
|
867
|
-
content:
|
|
964
|
+
content: denied.reason === 'bot-scope'
|
|
965
|
+
? `Permission denied: This bot does not have access to that resource.`
|
|
966
|
+
: `Permission denied: You do not have access to that resource.`,
|
|
868
967
|
is_error: true,
|
|
869
968
|
});
|
|
870
969
|
continue;
|
|
@@ -913,7 +1012,7 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
913
1012
|
* between LLM iterations. Merges into the existing user message to preserve
|
|
914
1013
|
* Anthropic's alternation rule (user/assistant/user/assistant).
|
|
915
1014
|
*/
|
|
916
|
-
injectPendingContext(discussionId, conversation) {
|
|
1015
|
+
async injectPendingContext(discussionId, conversation) {
|
|
917
1016
|
const state = this.discussionStates.get(discussionId);
|
|
918
1017
|
if (!state || state.contextBuffer.length === 0)
|
|
919
1018
|
return;
|
|
@@ -925,6 +1024,10 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
925
1024
|
type: 'text',
|
|
926
1025
|
text: `<context type="messages-during-processing">\n${contextText}\n</context>`,
|
|
927
1026
|
});
|
|
1027
|
+
const imageBlocks = await this.resolveImageAttachments(pending);
|
|
1028
|
+
for (const block of imageBlocks) {
|
|
1029
|
+
lastMsg.content.push(block);
|
|
1030
|
+
}
|
|
928
1031
|
this.opLogger.contextInject(discussionId, pending.length);
|
|
929
1032
|
}
|
|
930
1033
|
}
|
|
@@ -954,11 +1057,59 @@ Reply with exactly one word: SIMPLE or COMPLEX`,
|
|
|
954
1057
|
const nameAttr = discName ? ` discussion_name="${discName}"` : '';
|
|
955
1058
|
let fileInfo = '';
|
|
956
1059
|
if (message.fileAttachments?.length) {
|
|
957
|
-
const
|
|
958
|
-
|
|
1060
|
+
const imageFiles = message.fileAttachments.filter(f => f.mime && IMAGE_MIME_TYPES.has(f.mime) && (!f.size || f.size <= MAX_IMAGE_SIZE));
|
|
1061
|
+
const nonImageFiles = message.fileAttachments.filter(f => !f.mime || !IMAGE_MIME_TYPES.has(f.mime) || (f.size && f.size > MAX_IMAGE_SIZE));
|
|
1062
|
+
const parts = [];
|
|
1063
|
+
for (const img of imageFiles) {
|
|
1064
|
+
parts.push(`[Image attached: ${img.filename} — included below for vision]`);
|
|
1065
|
+
}
|
|
1066
|
+
if (nonImageFiles.length) {
|
|
1067
|
+
const ids = nonImageFiles.map(f => f.fileId).join(', ');
|
|
1068
|
+
parts.push(`[File attached: ${nonImageFiles.length} file(s) - IDs: ${ids}]\nUse download_file tool with fileId to read file contents.`);
|
|
1069
|
+
}
|
|
1070
|
+
if (parts.length)
|
|
1071
|
+
fileInfo = '\n' + parts.join('\n');
|
|
959
1072
|
}
|
|
960
1073
|
return `<incoming discussion="${message.discussionId}"${nameAttr}${activityAttr} from="${message.senderName}" user_id="${message.senderId}" timestamp="${new Date(message.timestamp).toISOString()}">\n${message.content}${fileInfo}\n</incoming>`;
|
|
961
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Download image attachments and return Anthropic image content blocks.
|
|
1077
|
+
* Falls back silently on errors — the text note still tells the LLM about the file.
|
|
1078
|
+
*/
|
|
1079
|
+
async resolveImageAttachments(messages) {
|
|
1080
|
+
const blocks = [];
|
|
1081
|
+
if (!this.hailerApi)
|
|
1082
|
+
return blocks;
|
|
1083
|
+
const imageFiles = [];
|
|
1084
|
+
for (const msg of messages) {
|
|
1085
|
+
if (!msg.fileAttachments?.length)
|
|
1086
|
+
continue;
|
|
1087
|
+
for (const f of msg.fileAttachments) {
|
|
1088
|
+
if (f.mime && IMAGE_MIME_TYPES.has(f.mime) && (!f.size || f.size <= MAX_IMAGE_SIZE)) {
|
|
1089
|
+
imageFiles.push({ fileId: f.fileId, filename: f.filename, mime: f.mime });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
for (const img of imageFiles) {
|
|
1094
|
+
try {
|
|
1095
|
+
const result = await this.hailerApi.downloadFile(img.fileId);
|
|
1096
|
+
if (result.encoding === 'base64') {
|
|
1097
|
+
blocks.push({
|
|
1098
|
+
type: 'image',
|
|
1099
|
+
source: {
|
|
1100
|
+
type: 'base64',
|
|
1101
|
+
media_type: img.mime,
|
|
1102
|
+
data: result.content,
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
catch (err) {
|
|
1108
|
+
this.logger.warn('Failed to download image for vision', { fileId: img.fileId, filename: img.filename, error: err });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return blocks;
|
|
1112
|
+
}
|
|
962
1113
|
async formatOutgoingMessage(text) {
|
|
963
1114
|
let formatted = await this.messageFormatter.resolveUserTags(text);
|
|
964
1115
|
formatted = await this.messageFormatter.resolveActivityTags(formatted);
|
|
@@ -1056,6 +1207,22 @@ HAILERTAG LINKING (mandatory):
|
|
|
1056
1207
|
- NEVER use hailerTag for workflows, phases, teams, or groups — these don't support linking. Just use their plain text names.
|
|
1057
1208
|
- NEVER output bare 24-character hex IDs to the user.
|
|
1058
1209
|
</platform-rules>`;
|
|
1210
|
+
const now = new Date();
|
|
1211
|
+
const isoDate = now.toISOString().split('T')[0];
|
|
1212
|
+
const dayOfWeek = now.getUTCDay(); // 0=Sun, 1=Mon, ...
|
|
1213
|
+
const weekNum = Math.ceil(((+now - +new Date(now.getFullYear(), 0, 1)) / 86400000 + new Date(now.getFullYear(), 0, 1).getDay() + 1) / 7);
|
|
1214
|
+
const dateContext = `<current-date>
|
|
1215
|
+
Today: ${now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} (${isoDate}, ISO week ${weekNum}, day ${dayOfWeek})
|
|
1216
|
+
Time: ${now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Helsinki' })} Europe/Helsinki
|
|
1217
|
+
Year: ${now.getFullYear()}
|
|
1218
|
+
|
|
1219
|
+
DATE HANDLING — CRITICAL:
|
|
1220
|
+
- The current year is ${now.getFullYear()}. When no year is specified, ALWAYS use ${now.getFullYear()} (or ${now.getFullYear() + 1} if the date has already passed this year).
|
|
1221
|
+
- European/Finnish dates use DD.MM.YYYY or DD.M.YYYY format (day first, NOT month first). "18.7.2026" = July 18, 2026. "3.12" = December 3. Never confuse day and month.
|
|
1222
|
+
- Resolve relative dates in any language (Finnish, Swedish, English, etc.) by calculating from today's date.
|
|
1223
|
+
- For activity date fields, always output YYYY-MM-DD format. The system auto-converts to timestamps.
|
|
1224
|
+
- If a date is ambiguous, ask the user to clarify.
|
|
1225
|
+
</current-date>`;
|
|
1059
1226
|
const wsContext = `<workspace-context>
|
|
1060
1227
|
${this.workspaceOverview}
|
|
1061
1228
|
</workspace-context>`;
|
|
@@ -1067,7 +1234,7 @@ ${this.workspaceOverview}
|
|
|
1067
1234
|
.replace(/\{wsName\}/g, wsName)
|
|
1068
1235
|
.replace(/\{userId\}/g, this.userId)
|
|
1069
1236
|
.replace(/\{botName\}/g, botName);
|
|
1070
|
-
return `${body}\n\n${platformRules}\n\n${identity}\n\n${wsContext}`;
|
|
1237
|
+
return `${body}\n\n${platformRules}\n\n${identity}\n\n${dateContext}\n\n${wsContext}`;
|
|
1071
1238
|
}
|
|
1072
1239
|
// Default prompt — general-purpose workspace assistant with behavioral defaults.
|
|
1073
1240
|
const defaultBehavior = `<behavior>
|
|
@@ -1087,21 +1254,54 @@ ${platformRules}
|
|
|
1087
1254
|
|
|
1088
1255
|
${defaultBehavior}
|
|
1089
1256
|
|
|
1257
|
+
${dateContext}
|
|
1258
|
+
|
|
1090
1259
|
${wsContext}`;
|
|
1091
1260
|
}
|
|
1092
1261
|
// ===== TOOLS =====
|
|
1093
|
-
|
|
1094
|
-
|
|
1262
|
+
static TOOL_STATUS_LABELS = {
|
|
1263
|
+
list_activities: 'Searching activities',
|
|
1264
|
+
count_activities: 'Counting activities',
|
|
1265
|
+
show_activity_by_id: 'Reading activity',
|
|
1266
|
+
create_activity: 'Creating activity',
|
|
1267
|
+
update_activity: 'Updating activity',
|
|
1268
|
+
list_workflows: 'Loading workflows',
|
|
1269
|
+
list_workflows_minimal: 'Loading workflows',
|
|
1270
|
+
get_workflow_schema: 'Reading workflow schema',
|
|
1271
|
+
list_workflow_phases: 'Loading phases',
|
|
1272
|
+
preview_insight: 'Running query',
|
|
1273
|
+
get_insight_data: 'Running report',
|
|
1274
|
+
search_workspace_users: 'Searching users',
|
|
1275
|
+
fetch_discussion_messages: 'Reading messages',
|
|
1276
|
+
add_discussion_message: 'Sending message',
|
|
1277
|
+
join_discussion: 'Joining discussion',
|
|
1278
|
+
};
|
|
1279
|
+
getToolStatus(toolUseBlocks) {
|
|
1280
|
+
if (toolUseBlocks.length === 1) {
|
|
1281
|
+
return Bot.TOOL_STATUS_LABELS[toolUseBlocks[0].name]
|
|
1282
|
+
|| toolUseBlocks[0].name.replace(/_/g, ' ');
|
|
1283
|
+
}
|
|
1284
|
+
const firstLabel = Bot.TOOL_STATUS_LABELS[toolUseBlocks[0].name]
|
|
1285
|
+
|| toolUseBlocks[0].name.replace(/_/g, ' ');
|
|
1286
|
+
return `${firstLabel} (+${toolUseBlocks.length - 1} more)`;
|
|
1287
|
+
}
|
|
1288
|
+
getAnthropicTools() {
|
|
1289
|
+
const allowedTools = BOT_TOOLS;
|
|
1095
1290
|
const defs = this.toolExecutor.getToolDefinitions({
|
|
1096
1291
|
allowedGroups: [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND, tool_registry_1.ToolGroup.BOT_INTERNAL],
|
|
1097
1292
|
});
|
|
1098
|
-
|
|
1293
|
+
const tools = defs
|
|
1099
1294
|
.filter(d => allowedTools.has(d.name))
|
|
1100
1295
|
.map(d => ({
|
|
1101
1296
|
name: d.name,
|
|
1102
1297
|
description: d.description,
|
|
1103
1298
|
input_schema: d.inputSchema,
|
|
1104
1299
|
}));
|
|
1300
|
+
// Mark last tool for prompt caching — tools never change per session
|
|
1301
|
+
if (tools.length > 0) {
|
|
1302
|
+
tools[tools.length - 1].cache_control = { type: 'ephemeral' };
|
|
1303
|
+
}
|
|
1304
|
+
return tools;
|
|
1105
1305
|
}
|
|
1106
1306
|
// ===== HAILER MESSAGING =====
|
|
1107
1307
|
async sendMessage(discussionId, text, links) {
|
|
@@ -1166,6 +1366,7 @@ ${wsContext}`;
|
|
|
1166
1366
|
this.logger.debug('Refreshing workspace data', { keys, trigger: isFull ? 'full' : Array.from(scopes).join(',') });
|
|
1167
1367
|
const freshData = await this.client.socket.request('v2.core.init', [keys]);
|
|
1168
1368
|
// Merge fresh data into existing init (partial update)
|
|
1369
|
+
(0, types_1.normalizeInitProcesses)(freshData);
|
|
1169
1370
|
if (!this.init) {
|
|
1170
1371
|
this.init = freshData;
|
|
1171
1372
|
}
|
|
@@ -1192,6 +1393,7 @@ ${wsContext}`;
|
|
|
1192
1393
|
compactUserData: true,
|
|
1193
1394
|
includeWorkspaceNamesInTools: false,
|
|
1194
1395
|
});
|
|
1396
|
+
this.backfillWorkspaceCacheMembers();
|
|
1195
1397
|
// Rebuild workspace overview for system prompt
|
|
1196
1398
|
this.workspaceOverview = (0, workspace_overview_1.generateWorkspaceOverview)(this.init);
|
|
1197
1399
|
// Update UserContext with fresh data
|
|
@@ -1218,95 +1420,112 @@ ${wsContext}`;
|
|
|
1218
1420
|
}
|
|
1219
1421
|
}
|
|
1220
1422
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1423
|
+
/**
|
|
1424
|
+
* Backfill workspaceCache.usersById with entries from network.members
|
|
1425
|
+
* that are missing from init.users (which is only a partial user list).
|
|
1426
|
+
*/
|
|
1427
|
+
backfillWorkspaceCacheMembers() {
|
|
1428
|
+
if (!this.workspaceCache || !this.init)
|
|
1225
1429
|
return;
|
|
1226
|
-
// Extract workspace admins from network members
|
|
1227
|
-
this.adminUserIds.clear();
|
|
1228
1430
|
const network = this.init.network;
|
|
1229
|
-
if (network?.members)
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
const groups = this.init.groups?.[wsId] || {};
|
|
1249
|
-
let networkOpenCount = 0;
|
|
1250
|
-
for (const proc of this.init.processes) {
|
|
1251
|
-
const members = proc.members || [];
|
|
1252
|
-
// If any member is network_ (all workspace members), skip indexing — everyone has access
|
|
1253
|
-
if (members.some((m) => m.id.startsWith('network_'))) {
|
|
1254
|
-
networkOpenCount++;
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
// If any member references a public team, treat as open — all workspace members have access
|
|
1258
|
-
let hasPublicTeam = false;
|
|
1259
|
-
for (const m of members) {
|
|
1260
|
-
if (!m.id.startsWith('team_'))
|
|
1261
|
-
continue;
|
|
1262
|
-
const team = teams[m.id.slice(5)];
|
|
1263
|
-
if (team) {
|
|
1264
|
-
if (team.public) {
|
|
1265
|
-
hasPublicTeam = true;
|
|
1266
|
-
break;
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
if (hasPublicTeam) {
|
|
1271
|
-
networkOpenCount++;
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
const userIds = new Set();
|
|
1275
|
-
for (const member of members) {
|
|
1276
|
-
const id = member.id;
|
|
1277
|
-
if (id.startsWith('user_')) {
|
|
1278
|
-
userIds.add(id.slice(5));
|
|
1279
|
-
}
|
|
1280
|
-
else if (id.startsWith('team_')) {
|
|
1281
|
-
const team = teams[id.slice(5)];
|
|
1282
|
-
if (team?.members) {
|
|
1283
|
-
for (const uid of team.members) {
|
|
1284
|
-
userIds.add(typeof uid === 'string' ? uid : uid._id || uid.id || uid.uid);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
else if (id.startsWith('group_')) {
|
|
1289
|
-
const group = groups[id.slice(6)];
|
|
1290
|
-
if (group?.members) {
|
|
1291
|
-
for (const uid of group.members) {
|
|
1292
|
-
userIds.add(typeof uid === 'string' ? uid : uid._id || uid.id || uid.uid);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1431
|
+
if (!network?.members)
|
|
1432
|
+
return;
|
|
1433
|
+
const members = Array.isArray(network.members) ? network.members : Object.values(network.members);
|
|
1434
|
+
let backfilled = 0;
|
|
1435
|
+
for (const member of members) {
|
|
1436
|
+
if (member.uid && !this.workspaceCache.usersById[member.uid]) {
|
|
1437
|
+
const stub = {
|
|
1438
|
+
id: member.uid,
|
|
1439
|
+
firstname: member.firstname || '',
|
|
1440
|
+
lastname: member.lastname || '',
|
|
1441
|
+
fullName: member.firstname && member.lastname
|
|
1442
|
+
? `${member.firstname} ${member.lastname}`
|
|
1443
|
+
: member.firstname || member.lastname || '',
|
|
1444
|
+
companies: [],
|
|
1445
|
+
lastSeen: 0,
|
|
1446
|
+
};
|
|
1447
|
+
this.workspaceCache.usersById[member.uid] = stub;
|
|
1448
|
+
this.workspaceCache.users.push(stub);
|
|
1449
|
+
backfilled++;
|
|
1296
1450
|
}
|
|
1297
|
-
// Workflow creator always has access
|
|
1298
|
-
if (proc.uid)
|
|
1299
|
-
userIds.add(proc.uid);
|
|
1300
|
-
this.permissionIndex.set(proc._id, userIds);
|
|
1301
1451
|
}
|
|
1452
|
+
this.logger.debug('Workspace cache member backfill', {
|
|
1453
|
+
totalMembers: members.length,
|
|
1454
|
+
inInitUsers: members.length - backfilled,
|
|
1455
|
+
backfilled,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
buildPermissionIndex() {
|
|
1459
|
+
const wsId = this._workspaceId;
|
|
1460
|
+
if (!wsId)
|
|
1461
|
+
return;
|
|
1462
|
+
const result = (0, permission_guard_1.buildPermissionIndex)(this.init, wsId);
|
|
1463
|
+
this.permissionIndex = result.permissionIndex;
|
|
1464
|
+
this.adminUserIds = result.adminUserIds;
|
|
1465
|
+
this.ownerUserIds = result.ownerUserIds;
|
|
1466
|
+
this.workspaceMemberIds = result.workspaceMemberIds;
|
|
1467
|
+
this.membersLoaded = result.workspaceMemberIds.size > 0;
|
|
1468
|
+
const totalWorkflows = this.init?.processes?.length || 0;
|
|
1302
1469
|
this.logger.debug('Permission index built', {
|
|
1303
1470
|
restricted: this.permissionIndex.size,
|
|
1304
|
-
open:
|
|
1305
|
-
totalWorkflows
|
|
1471
|
+
open: totalWorkflows - this.permissionIndex.size,
|
|
1472
|
+
totalWorkflows,
|
|
1306
1473
|
totalEntries: Array.from(this.permissionIndex.values()).reduce((n, s) => n + s.size, 0),
|
|
1307
1474
|
admins: this.adminUserIds.size,
|
|
1308
1475
|
owners: this.ownerUserIds.size,
|
|
1476
|
+
membersLoaded: this.membersLoaded,
|
|
1477
|
+
workspaceMembers: this.workspaceMemberIds.size,
|
|
1478
|
+
accessLevel: this.config.accessLevel,
|
|
1479
|
+
botWorkflowScope: this.config.allowedWorkflows.length > 0
|
|
1480
|
+
? `${this.config.allowedWorkflows.length} workflows`
|
|
1481
|
+
: 'all',
|
|
1482
|
+
});
|
|
1483
|
+
// Detailed permission map for verification
|
|
1484
|
+
const processNames = new Map();
|
|
1485
|
+
for (const proc of (this.init?.processes || [])) {
|
|
1486
|
+
processNames.set(proc._id, proc.name || proc._id);
|
|
1487
|
+
}
|
|
1488
|
+
// Resolve user IDs to names where possible
|
|
1489
|
+
const userNames = new Map();
|
|
1490
|
+
if (this.workspaceCache?.usersById) {
|
|
1491
|
+
for (const uid of this.workspaceMemberIds) {
|
|
1492
|
+
const user = this.workspaceCache.usersById[uid];
|
|
1493
|
+
if (user)
|
|
1494
|
+
userNames.set(uid, user.fullName || user.email || uid);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
const resolve = (uid) => {
|
|
1498
|
+
const name = userNames.get(uid);
|
|
1499
|
+
return name ? `${name} (${uid})` : uid;
|
|
1500
|
+
};
|
|
1501
|
+
this.logger.debug('Permission map — admins', {
|
|
1502
|
+
admins: [...this.adminUserIds].map(resolve),
|
|
1503
|
+
});
|
|
1504
|
+
this.logger.debug('Permission map — owners', {
|
|
1505
|
+
owners: [...this.ownerUserIds].map(resolve),
|
|
1506
|
+
});
|
|
1507
|
+
this.logger.debug('Permission map — workspace members', {
|
|
1508
|
+
members: [...this.workspaceMemberIds].map(resolve),
|
|
1309
1509
|
});
|
|
1510
|
+
// Log each restricted workflow with its allowed users
|
|
1511
|
+
for (const [wfId, userIds] of this.permissionIndex) {
|
|
1512
|
+
this.logger.debug('Permission map — restricted workflow', {
|
|
1513
|
+
workflow: processNames.get(wfId) || wfId,
|
|
1514
|
+
workflowId: wfId,
|
|
1515
|
+
allowedUsers: [...userIds].map(resolve),
|
|
1516
|
+
allowedCount: userIds.size,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
// Log open workflows
|
|
1520
|
+
const openWorkflows = (this.init?.processes || [])
|
|
1521
|
+
.filter((p) => !this.permissionIndex.has(p._id))
|
|
1522
|
+
.map((p) => p.name || p._id);
|
|
1523
|
+
if (openWorkflows.length > 0) {
|
|
1524
|
+
this.logger.debug('Permission map — open workflows (all members)', {
|
|
1525
|
+
workflows: openWorkflows,
|
|
1526
|
+
count: openWorkflows.length,
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1310
1529
|
}
|
|
1311
1530
|
getOrCreateDiscussionState(discussionId) {
|
|
1312
1531
|
let state = this.discussionStates.get(discussionId);
|
|
@@ -1346,6 +1565,88 @@ ${wsContext}`;
|
|
|
1346
1565
|
return undefined;
|
|
1347
1566
|
return Object.keys(workspaceTeams)[0];
|
|
1348
1567
|
}
|
|
1568
|
+
// ===== UNIFIED PERMISSION SYSTEM =====
|
|
1569
|
+
/** Get the current permission context for checkWorkflowAccess calls */
|
|
1570
|
+
getPermissionContext() {
|
|
1571
|
+
return {
|
|
1572
|
+
allowedWorkflows: this.config.allowedWorkflows,
|
|
1573
|
+
permissionIndex: this.permissionIndex,
|
|
1574
|
+
adminUserIds: this.adminUserIds,
|
|
1575
|
+
ownerUserIds: this.ownerUserIds,
|
|
1576
|
+
insightWorkflowCache: this.insightWorkflowCache,
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Post-filter list-type tool results to remove items the user can't access.
|
|
1581
|
+
* Handles list_workflows, list_workflows_minimal, and list_insights.
|
|
1582
|
+
*/
|
|
1583
|
+
async postFilterListResults(toolName, contentStr, senderId, discussionId) {
|
|
1584
|
+
const ctx = this.getPermissionContext();
|
|
1585
|
+
// Filter workflow lists (JSON response)
|
|
1586
|
+
if (toolName === 'list_workflows' || toolName === 'list_workflows_minimal') {
|
|
1587
|
+
try {
|
|
1588
|
+
const parsed = JSON.parse(contentStr);
|
|
1589
|
+
if (parsed.workflows && Array.isArray(parsed.workflows)) {
|
|
1590
|
+
const before = parsed.workflows.length;
|
|
1591
|
+
parsed.workflows = parsed.workflows.filter((wf) => {
|
|
1592
|
+
const wfId = wf.id || wf._id || wf.workflowId;
|
|
1593
|
+
if (!wfId)
|
|
1594
|
+
return true;
|
|
1595
|
+
return !(0, permission_guard_1.checkWorkflowAccess)([wfId], senderId, ctx);
|
|
1596
|
+
});
|
|
1597
|
+
if (parsed.workflows.length < before) {
|
|
1598
|
+
this.opLogger.permFiltered(discussionId, toolName, before, parsed.workflows.length);
|
|
1599
|
+
}
|
|
1600
|
+
return JSON.stringify(parsed);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
catch {
|
|
1604
|
+
// Not JSON (markdown response) — pass through unmodified
|
|
1605
|
+
return contentStr;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
// Filter insight lists (formatted text response — need raw API data)
|
|
1609
|
+
if (toolName === 'list_insights') {
|
|
1610
|
+
try {
|
|
1611
|
+
const wsId = this._workspaceId || this.init?.network?._id;
|
|
1612
|
+
if (wsId && this.hailerApi) {
|
|
1613
|
+
const rawInsights = await this.hailerApi.request('v3.insight.list', [wsId]);
|
|
1614
|
+
const insights = Array.isArray(rawInsights) ? rawInsights : [];
|
|
1615
|
+
const deniedIds = new Set();
|
|
1616
|
+
for (const insight of insights) {
|
|
1617
|
+
const wfIds = [];
|
|
1618
|
+
for (const src of (insight.sources || [])) {
|
|
1619
|
+
if (src.workflowId)
|
|
1620
|
+
wfIds.push(src.workflowId);
|
|
1621
|
+
}
|
|
1622
|
+
// Always cache the mapping
|
|
1623
|
+
if (wfIds.length > 0) {
|
|
1624
|
+
this.insightWorkflowCache.set(insight._id, new Set(wfIds));
|
|
1625
|
+
}
|
|
1626
|
+
// Check access
|
|
1627
|
+
if ((0, permission_guard_1.checkWorkflowAccess)(wfIds, senderId, ctx)) {
|
|
1628
|
+
deniedIds.add(insight._id);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (deniedIds.size > 0) {
|
|
1632
|
+
let filtered = contentStr;
|
|
1633
|
+
for (const id of deniedIds) {
|
|
1634
|
+
const pattern = new RegExp(`\\d+\\.\\s+\\*\\*[^*]+\\*\\*\\n(?:.*\\n)*?\\s+-\\s+Insight ID:\\s+\`${id}\`(?:.*\\n)*?(?=\\d+\\.\\s+\\*\\*|💡|$)`, 'g');
|
|
1635
|
+
filtered = filtered.replace(pattern, '');
|
|
1636
|
+
}
|
|
1637
|
+
const visibleCount = insights.length - deniedIds.size;
|
|
1638
|
+
filtered = filtered.replace(/\*\*Insights Found\*\*\s+\(\d+ total\)/, `**Insights Found** (${visibleCount} total)`);
|
|
1639
|
+
this.opLogger.permFiltered(discussionId, toolName, insights.length, visibleCount);
|
|
1640
|
+
return filtered;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
catch (err) {
|
|
1645
|
+
this.logger.warn('Failed to filter list_insights by permissions', { error: err });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return contentStr;
|
|
1649
|
+
}
|
|
1349
1650
|
/**
|
|
1350
1651
|
* Extract workflow ID from tool result for post-execution permission checks.
|
|
1351
1652
|
* Returns the workflow ID if found, or undefined if extraction fails.
|