@exulu/backend 1.63.3 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -26,6 +26,17 @@ type User = {
26
26
  agent_ids?: string[];
27
27
  role: UserRole;
28
28
  team?: ExuluTeam;
29
+ /**
30
+ * Live LiteLLM budget snapshot for the user, attached at context time when
31
+ * the "show user budget in chat" setting is on. Not a Postgres column.
32
+ */
33
+ budget?: UserBudgetView | null;
34
+ };
35
+ type UserBudgetView = {
36
+ spend: number;
37
+ max_budget: number;
38
+ budget_duration: string | null;
39
+ budget_reset_at: string | null;
29
40
  };
30
41
  type UserRole = {
31
42
  id: string;
@@ -35,6 +46,7 @@ type UserRole = {
35
46
  workflows: "read" | "write";
36
47
  variables: "read" | "write";
37
48
  users: "read" | "write";
49
+ budget_management?: "read" | "write";
38
50
  };
39
51
  type ExuluTeam = {
40
52
  id: string;
@@ -72,15 +84,6 @@ type ExuluProviderConfig = {
72
84
  };
73
85
  };
74
86
 
75
- type AgentRateLimitBucket = {
76
- limit: number;
77
- window_seconds: number;
78
- };
79
- type AgentRateLimits = {
80
- requests?: AgentRateLimitBucket;
81
- input_tokens?: AgentRateLimitBucket;
82
- output_tokens?: AgentRateLimitBucket;
83
- };
84
87
  interface ExuluAgent {
85
88
  id: string;
86
89
  modelName?: string;
@@ -151,7 +154,6 @@ interface ExuluAgent {
151
154
  rights: 'read' | 'write';
152
155
  }>;
153
156
  };
154
- rate_limits?: AgentRateLimits;
155
157
  createdAt?: string;
156
158
  updatedAt?: string;
157
159
  }
@@ -160,14 +162,6 @@ type fileTypes$1 = '.pdf' | '.docx' | '.xlsx' | '.xls' | '.csv' | '.pptx' | '.pp
160
162
  type audioTypes$1 = '.mp3' | '.wav' | '.m4a' | '.mp4' | '.mpeg';
161
163
  type videoTypes$1 = '.mp4' | '.m4a' | '.mp3' | '.mpeg' | '.wav';
162
164
 
163
- interface RateLimiterRule {
164
- name?: string;
165
- rate_limit: {
166
- time: number;
167
- limit: number;
168
- };
169
- }
170
-
171
165
  interface Item {
172
166
  id?: string;
173
167
  name?: string;
@@ -190,13 +184,16 @@ interface Item {
190
184
  [key: string]: any;
191
185
  }
192
186
 
187
+ declare const PUBLIC_TOOL_TYPES: readonly ["function", "web_search", "skill"];
188
+ type PublicToolType = (typeof PUBLIC_TOOL_TYPES)[number];
189
+ type ToolType = PublicToolType | "agent" | "context";
193
190
  declare class ExuluTool {
194
191
  id: string;
195
192
  name: string;
196
193
  description: string;
197
194
  category: string;
198
195
  inputSchema?: z.ZodType;
199
- type: "context" | "function" | "agent" | "web_search" | "skill";
196
+ type: ToolType;
200
197
  tool: Tool;
201
198
  needsApproval: boolean;
202
199
  config: {
@@ -211,7 +208,7 @@ declare class ExuluTool {
211
208
  description: string;
212
209
  category?: string;
213
210
  inputSchema?: z.ZodType;
214
- type: "context" | "function" | "agent" | "web_search" | "skill";
211
+ type: PublicToolType;
215
212
  config: {
216
213
  name: string;
217
214
  description: string;
@@ -229,6 +226,17 @@ declare class ExuluTool {
229
226
  items?: Item[];
230
227
  }>;
231
228
  });
229
+ /**
230
+ * Framework-only factory for tools whose `type` is managed by Exulu itself —
231
+ * "agent" (agent-as-tool instrumentation) and "context" (internal retrieval
232
+ * tools). NOT part of the public API: package consumers must use
233
+ * `new ExuluTool(...)`, which only accepts a {@link PublicToolType}. Building
234
+ * the tool as a "function" and then setting the managed type bypasses the
235
+ * constructor guard without weakening it for consumers.
236
+ */
237
+ static internal(params: Omit<ConstructorParameters<typeof ExuluTool>[0], "type"> & {
238
+ type: ToolType;
239
+ }): ExuluTool;
232
240
  execute: ({ agent: agentId, config, user, inputs, project, items, }: {
233
241
  agent: string;
234
242
  config: ExuluConfig;
@@ -530,7 +538,6 @@ declare class ExuluContext {
530
538
  active: boolean;
531
539
  fields: ExuluContextFieldDefinition[];
532
540
  processor?: ExuluContextProcessor;
533
- rateLimit?: RateLimiterRule;
534
541
  description: string;
535
542
  embedder?: ExuluEmbedder;
536
543
  queryRewriter?: (query: string) => Promise<string>;
@@ -573,7 +580,7 @@ declare class ExuluContext {
573
580
  languages?: ("german" | "english")[];
574
581
  };
575
582
  sources: ExuluContextSource[];
576
- constructor({ id, name, description, embedder, processor, active, rateLimit, fields, queryRewriter, resultReranker, configuration, sources, }: {
583
+ constructor({ id, name, description, embedder, processor, active, fields, queryRewriter, resultReranker, configuration, sources, }: {
577
584
  id: string;
578
585
  name: string;
579
586
  fields: ExuluContextFieldDefinition[];
@@ -583,7 +590,6 @@ declare class ExuluContext {
583
590
  category?: string;
584
591
  active: boolean;
585
592
  processor?: ExuluContextProcessor;
586
- rateLimit?: RateLimiterRule;
587
593
  queryRewriter?: (query: string) => Promise<string>;
588
594
  resultReranker?: (results: any[]) => Promise<any[]>;
589
595
  configuration?: {
@@ -760,7 +766,6 @@ interface ExuluProviderParams {
760
766
  audio: audioTypes$1[];
761
767
  video: videoTypes$1[];
762
768
  };
763
- rateLimit?: RateLimiterRule;
764
769
  }
765
770
  declare class ExuluProvider {
766
771
  id: string;
@@ -774,7 +779,6 @@ declare class ExuluProvider {
774
779
  maxContextLength?: number;
775
780
  workflows?: ExuluProviderWorkflowConfig;
776
781
  queue?: ExuluQueueConfig;
777
- rateLimit?: RateLimiterRule;
778
782
  config?: ExuluProviderConfig | undefined;
779
783
  model?: {
780
784
  create: ({ apiKey, user, role, project, agent }: {
@@ -792,7 +796,7 @@ declare class ExuluProvider {
792
796
  audio: string[];
793
797
  video: string[];
794
798
  };
795
- constructor({ id, name, description, config, rateLimit, capabilities, type, maxContextLength, provider, queue, authenticationInformation, workflows, }: ExuluProviderParams);
799
+ constructor({ id, name, description, config, capabilities, type, maxContextLength, provider, queue, authenticationInformation, workflows, }: ExuluProviderParams);
796
800
  get providerName(): string;
797
801
  get modelName(): string;
798
802
  tool: (instance: string, providers: ExuluProvider[], contexts: ExuluContext[], rerankers: ExuluReranker[]) => Promise<ExuluTool | null>;
package/dist/index.d.ts CHANGED
@@ -26,6 +26,17 @@ type User = {
26
26
  agent_ids?: string[];
27
27
  role: UserRole;
28
28
  team?: ExuluTeam;
29
+ /**
30
+ * Live LiteLLM budget snapshot for the user, attached at context time when
31
+ * the "show user budget in chat" setting is on. Not a Postgres column.
32
+ */
33
+ budget?: UserBudgetView | null;
34
+ };
35
+ type UserBudgetView = {
36
+ spend: number;
37
+ max_budget: number;
38
+ budget_duration: string | null;
39
+ budget_reset_at: string | null;
29
40
  };
30
41
  type UserRole = {
31
42
  id: string;
@@ -35,6 +46,7 @@ type UserRole = {
35
46
  workflows: "read" | "write";
36
47
  variables: "read" | "write";
37
48
  users: "read" | "write";
49
+ budget_management?: "read" | "write";
38
50
  };
39
51
  type ExuluTeam = {
40
52
  id: string;
@@ -72,15 +84,6 @@ type ExuluProviderConfig = {
72
84
  };
73
85
  };
74
86
 
75
- type AgentRateLimitBucket = {
76
- limit: number;
77
- window_seconds: number;
78
- };
79
- type AgentRateLimits = {
80
- requests?: AgentRateLimitBucket;
81
- input_tokens?: AgentRateLimitBucket;
82
- output_tokens?: AgentRateLimitBucket;
83
- };
84
87
  interface ExuluAgent {
85
88
  id: string;
86
89
  modelName?: string;
@@ -151,7 +154,6 @@ interface ExuluAgent {
151
154
  rights: 'read' | 'write';
152
155
  }>;
153
156
  };
154
- rate_limits?: AgentRateLimits;
155
157
  createdAt?: string;
156
158
  updatedAt?: string;
157
159
  }
@@ -160,14 +162,6 @@ type fileTypes$1 = '.pdf' | '.docx' | '.xlsx' | '.xls' | '.csv' | '.pptx' | '.pp
160
162
  type audioTypes$1 = '.mp3' | '.wav' | '.m4a' | '.mp4' | '.mpeg';
161
163
  type videoTypes$1 = '.mp4' | '.m4a' | '.mp3' | '.mpeg' | '.wav';
162
164
 
163
- interface RateLimiterRule {
164
- name?: string;
165
- rate_limit: {
166
- time: number;
167
- limit: number;
168
- };
169
- }
170
-
171
165
  interface Item {
172
166
  id?: string;
173
167
  name?: string;
@@ -190,13 +184,16 @@ interface Item {
190
184
  [key: string]: any;
191
185
  }
192
186
 
187
+ declare const PUBLIC_TOOL_TYPES: readonly ["function", "web_search", "skill"];
188
+ type PublicToolType = (typeof PUBLIC_TOOL_TYPES)[number];
189
+ type ToolType = PublicToolType | "agent" | "context";
193
190
  declare class ExuluTool {
194
191
  id: string;
195
192
  name: string;
196
193
  description: string;
197
194
  category: string;
198
195
  inputSchema?: z.ZodType;
199
- type: "context" | "function" | "agent" | "web_search" | "skill";
196
+ type: ToolType;
200
197
  tool: Tool;
201
198
  needsApproval: boolean;
202
199
  config: {
@@ -211,7 +208,7 @@ declare class ExuluTool {
211
208
  description: string;
212
209
  category?: string;
213
210
  inputSchema?: z.ZodType;
214
- type: "context" | "function" | "agent" | "web_search" | "skill";
211
+ type: PublicToolType;
215
212
  config: {
216
213
  name: string;
217
214
  description: string;
@@ -229,6 +226,17 @@ declare class ExuluTool {
229
226
  items?: Item[];
230
227
  }>;
231
228
  });
229
+ /**
230
+ * Framework-only factory for tools whose `type` is managed by Exulu itself —
231
+ * "agent" (agent-as-tool instrumentation) and "context" (internal retrieval
232
+ * tools). NOT part of the public API: package consumers must use
233
+ * `new ExuluTool(...)`, which only accepts a {@link PublicToolType}. Building
234
+ * the tool as a "function" and then setting the managed type bypasses the
235
+ * constructor guard without weakening it for consumers.
236
+ */
237
+ static internal(params: Omit<ConstructorParameters<typeof ExuluTool>[0], "type"> & {
238
+ type: ToolType;
239
+ }): ExuluTool;
232
240
  execute: ({ agent: agentId, config, user, inputs, project, items, }: {
233
241
  agent: string;
234
242
  config: ExuluConfig;
@@ -530,7 +538,6 @@ declare class ExuluContext {
530
538
  active: boolean;
531
539
  fields: ExuluContextFieldDefinition[];
532
540
  processor?: ExuluContextProcessor;
533
- rateLimit?: RateLimiterRule;
534
541
  description: string;
535
542
  embedder?: ExuluEmbedder;
536
543
  queryRewriter?: (query: string) => Promise<string>;
@@ -573,7 +580,7 @@ declare class ExuluContext {
573
580
  languages?: ("german" | "english")[];
574
581
  };
575
582
  sources: ExuluContextSource[];
576
- constructor({ id, name, description, embedder, processor, active, rateLimit, fields, queryRewriter, resultReranker, configuration, sources, }: {
583
+ constructor({ id, name, description, embedder, processor, active, fields, queryRewriter, resultReranker, configuration, sources, }: {
577
584
  id: string;
578
585
  name: string;
579
586
  fields: ExuluContextFieldDefinition[];
@@ -583,7 +590,6 @@ declare class ExuluContext {
583
590
  category?: string;
584
591
  active: boolean;
585
592
  processor?: ExuluContextProcessor;
586
- rateLimit?: RateLimiterRule;
587
593
  queryRewriter?: (query: string) => Promise<string>;
588
594
  resultReranker?: (results: any[]) => Promise<any[]>;
589
595
  configuration?: {
@@ -760,7 +766,6 @@ interface ExuluProviderParams {
760
766
  audio: audioTypes$1[];
761
767
  video: videoTypes$1[];
762
768
  };
763
- rateLimit?: RateLimiterRule;
764
769
  }
765
770
  declare class ExuluProvider {
766
771
  id: string;
@@ -774,7 +779,6 @@ declare class ExuluProvider {
774
779
  maxContextLength?: number;
775
780
  workflows?: ExuluProviderWorkflowConfig;
776
781
  queue?: ExuluQueueConfig;
777
- rateLimit?: RateLimiterRule;
778
782
  config?: ExuluProviderConfig | undefined;
779
783
  model?: {
780
784
  create: ({ apiKey, user, role, project, agent }: {
@@ -792,7 +796,7 @@ declare class ExuluProvider {
792
796
  audio: string[];
793
797
  video: string[];
794
798
  };
795
- constructor({ id, name, description, config, rateLimit, capabilities, type, maxContextLength, provider, queue, authenticationInformation, workflows, }: ExuluProviderParams);
799
+ constructor({ id, name, description, config, capabilities, type, maxContextLength, provider, queue, authenticationInformation, workflows, }: ExuluProviderParams);
796
800
  get providerName(): string;
797
801
  get modelName(): string;
798
802
  tool: (instance: string, providers: ExuluProvider[], contexts: ExuluContext[], rerankers: ExuluReranker[]) => Promise<ExuluTool | null>;
package/dist/index.js CHANGED
@@ -51,8 +51,9 @@ import {
51
51
  transcriptionService,
52
52
  updateStatistic,
53
53
  uploadFile,
54
+ validateHermesAtBoot,
54
55
  withRetry
55
- } from "./chunk-CUCJNEPB.js";
56
+ } from "./chunk-DQR5LQOE.js";
56
57
  import "./chunk-YCE44CMU.js";
57
58
 
58
59
  // src/index.ts
@@ -2738,6 +2739,7 @@ var ExuluApp = class {
2738
2739
  );
2739
2740
  }
2740
2741
  }
2742
+ validateHermesAtBoot();
2741
2743
  if (process.env.TRANSCRIPTION_SERVER) {
2742
2744
  try {
2743
2745
  const health = await transcriptionClient.health();
@@ -3031,7 +3033,6 @@ var ExuluApp = class {
3031
3033
  this._config,
3032
3034
  this._evals,
3033
3035
  tracer,
3034
- this._queues,
3035
3036
  this._rerankers
3036
3037
  );
3037
3038
  if (this._config?.MCP.enabled) {
@@ -4747,7 +4748,8 @@ var execute = async ({ contexts }) => {
4747
4748
  workflows: "write",
4748
4749
  variables: "write",
4749
4750
  users: "write",
4750
- evals: "write"
4751
+ evals: "write",
4752
+ budget_management: "write"
4751
4753
  }).returning("id");
4752
4754
  adminRoleId = role[0].id;
4753
4755
  } else {
@@ -213,7 +213,7 @@ export function createAgenticRetrievalToolV3({
213
213
 
214
214
  const contextNames = contexts.map((c) => c.id).join(", ");
215
215
 
216
- return new ExuluTool({
216
+ return ExuluTool.internal({
217
217
  id: "agentic_context_search",
218
218
  name: "Context Search",
219
219
  description: `Intelligent context search with query classification, strategy-based retrieval, and virtual filesystem filtering. Searches: ${contextNames}`,
@@ -152,7 +152,7 @@ export function createAgenticRetrievalToolV4({
152
152
 
153
153
  const contextNames = contexts.map((c) => c.id).join(", ");
154
154
 
155
- return new ExuluTool({
155
+ return ExuluTool.internal({
156
156
  id: "agentic_context_search",
157
157
  name: "Context Search",
158
158
  description: `Intelligent context search with query classification, strategy-based retrieval, and virtual filesystem filtering. Searches: ${contextNames}`,
@@ -0,0 +1,8 @@
1
+ # EXAMPLE ONLY — Exulu generates the real .env per profile at runtime (mode 0600).
2
+ # Profile-local secrets referenced by ${VAR} in config.yaml.
3
+ #
4
+ # Runtime API-server params (API_SERVER_ENABLED / HOST / PORT / KEY) are NOT
5
+ # stored here — the supervisor injects them via the child process environment so
6
+ # port and key allocation stay owned by Exulu and a profile dir is portable.
7
+
8
+ LITELLM_MASTER_KEY=replace-with-your-litellm-master-key
@@ -0,0 +1,44 @@
1
+ # Hermes Agent profiles (advanced agent mode)
2
+
3
+ This directory documents the per-profile files Exulu generates for the
4
+ [Hermes Agent](https://hermes-agent.nousresearch.com) harness when an Exulu
5
+ agent has **advanced mode** enabled. You do **not** edit anything here — Exulu's
6
+ provisioner writes the real files at runtime under `${HERMES_HOME}/profiles/<id>/`.
7
+
8
+ ## How it fits together
9
+
10
+ - One Hermes **profile** per Exulu agent (`<agentId>`), or per agent/user
11
+ (`<agentId>/<userId>`) when the agent's
12
+ `advanced_agent_profile_scope` is `private`.
13
+ - Each in-use profile runs its own `hermes gateway` process on its own port,
14
+ supervised by `src/exulu/hermes/supervisor.ts` (lazy start + idle eviction).
15
+ - Every model call still flows through the LiteLLM proxy — Hermes' `model`
16
+ block points `base_url` at LiteLLM.
17
+
18
+ ## Enabling
19
+
20
+ 1. `ENABLE_HERMES_AGENT=true` (gates install + the whole code path).
21
+ 2. Run `npm run python:setup` — installs the `hermes` binary when the flag is on.
22
+ 3. Toggle **advanced mode** on an individual agent in the agent form.
23
+
24
+ ## Env vars
25
+
26
+ | Var | Default | Purpose |
27
+ | --- | --- | --- |
28
+ | `ENABLE_HERMES_AGENT` | (unset) | Global gate for advanced mode. |
29
+ | `HERMES_HOME` | `~/.hermes` | Root for profile directories. |
30
+ | `HERMES_BIN` | (auto) | Override path to the `hermes` binary. |
31
+ | `HERMES_PORT_RANGE` | `8642-8700` | Gateway port pool. |
32
+ | `HERMES_MAX_GATEWAYS` | `20` | LRU cap on concurrent gateways. |
33
+ | `HERMES_IDLE_TIMEOUT_MS` | `900000` | Idle eviction threshold (15 min). |
34
+ | `HERMES_APPROVALS_MODE` | `smart` | Tool-approval policy written to config.yaml. |
35
+ | `HERMES_TERMINAL_BACKEND` | `docker` | Backend that runs native shell/file tools (`docker` isolates without host user namespaces; `local`/`ssh`/`modal`/`daytona`/`singularity` also selectable). Docker must be available to the host process. |
36
+ | `HERMES_DOCKER_IMAGE` | `nikolaik/python-nodejs:python3.11-nodejs20` | Image for the docker backend (needs python + node). |
37
+ | `BACKEND` | `http://127.0.0.1:<PORT>` | URL a gateway uses to reach Exulu's `/mcp/:agentId` (set this if the host app's port isn't `PORT`/`EXULU_PORT`). |
38
+ | `EXULU_MCP_KEY` | `LITELLM_MASTER_KEY` | Bearer token guarding the ExuluTools MCP endpoint. |
39
+
40
+ ExuluTools reach the agent over HTTP MCP at `/mcp/<agentId>` and **add to** Hermes'
41
+ native tools (bash, filesystem, …) rather than replacing them.
42
+
43
+ See `config.yaml.example`, `.env.example`, and `SOUL.md.example` in this folder
44
+ for the shape of the generated files.
@@ -0,0 +1,8 @@
1
+ <!-- EXAMPLE ONLY — Exulu generates the real SOUL.md per profile from the agent's
2
+ `instructions`. SOUL.md is slot #1 of the Hermes system prompt and defines
3
+ who the agent is. Exulu owns this file and rewrites it whenever the agent's
4
+ instructions change (Hermes never overwrites an existing SOUL.md). -->
5
+
6
+ You are Acme Corp's research assistant. You are precise, cite your sources, and
7
+ prefer primary documents over summaries. When unsure, you say so rather than
8
+ guessing.
@@ -0,0 +1,55 @@
1
+ # EXAMPLE ONLY — Exulu generates the real config.yaml per profile at runtime.
2
+ # Documentation: https://hermes-agent.nousresearch.com/docs/user-guide/configuration
3
+ #
4
+ # The model block points Hermes at the LiteLLM proxy so every model call still
5
+ # flows through the single model gateway. NOTE: the model-name key is `default`,
6
+ # not `model`. When base_url is set, Hermes calls it directly using api_key.
7
+
8
+ model:
9
+ default: "claude-haiku" # a LiteLLM model_name from config.litellm.yaml
10
+ provider: custom
11
+ base_url: "http://127.0.0.1:4000/v1"
12
+ api_key: "${LITELLM_MASTER_KEY}" # resolved from the profile .env / process env
13
+ api_mode: chat_completions
14
+
15
+ # Tool-approval policy: `smart` auto-approves low-risk actions and emits an
16
+ # approval event (requiring a decision) before destructive ones.
17
+ approvals:
18
+ mode: smart
19
+
20
+ # Native shell/file tools run via this backend. Default is `docker`: a hardened,
21
+ # Hermes-managed container (cap-drop ALL, no-new-privileges) that isolates the
22
+ # tools from the host WITHOUT needing user namespaces — so it behaves the same
23
+ # on macOS (Docker Desktop) and Linux. Volumes are bind-mounted host->same path
24
+ # so the absolute cwd / skills.external_dirs resolve inside the container;
25
+ # secrets are not mounted. Set HERMES_TERMINAL_BACKEND=local to disable.
26
+ terminal:
27
+ backend: docker
28
+ # No bind mount: the Files panel talks to the container's /root directly via
29
+ # docker exec/cp. We stamp a deterministic label so Exulu can find the
30
+ # container (docker ps --filter label=exulu-profile=<profileId>), and keep it
31
+ # persistent so files survive between runs.
32
+ docker_image: "nikolaik/python-nodejs:python3.11-nodejs20"
33
+ container_persistent: true
34
+ lifetime_seconds: 86400
35
+ # Run as root (home /root) so the agent's working dir is a predictable /root
36
+ # the Files panel reads — NOT the host user's home (e.g. /Users/<you>), which
37
+ # is what docker_run_as_host_user (default) would replicate.
38
+ docker_run_as_host_user: false
39
+ docker_mount_cwd_to_workspace: false
40
+ docker_extra_args: ["--label", "exulu-profile=<profileId>"]
41
+ - "/abs/.../profiles/<profileId>/exulu-skills:/abs/.../profiles/<profileId>/exulu-skills:ro"
42
+
43
+ # Added in Phase 3 — ExuluTools exposed over HTTP MCP at /mcp/<agentId>:
44
+ # mcp_servers:
45
+ # exulu:
46
+ # url: "http://127.0.0.1:<exulu-port>/mcp/<agentId>"
47
+ # headers:
48
+ # Authorization: "Bearer ${EXULU_MCP_KEY}"
49
+
50
+ # Enabled Exulu skills, synced from S3 into the profile (Anthropic Agent Skills
51
+ # format). ADDS to Hermes' own skills home (learned/bundled skills); only
52
+ # written when the agent has skills enabled.
53
+ # skills:
54
+ # external_dirs:
55
+ # - "/abs/path/to/${HERMES_HOME}/profiles/<profileId>/exulu-skills"
@@ -253,6 +253,46 @@ if [ -n "$LITELLM_PROXY_DIR" ] && [ -f "$LITELLM_PROXY_DIR/schema.prisma" ]; the
253
253
  || print_warning "Prisma generate failed; LiteLLM database mode (database_url in config.litellm.yaml) may not work until you run 'cd $LITELLM_PROXY_DIR && PATH=$VENV_DIR/bin:\$PATH $VENV_DIR/bin/prisma generate'"
254
254
  fi
255
255
 
256
+ # Step 6.6: Install the Hermes Agent harness (advanced agent mode).
257
+ # Opt-in via ENABLE_HERMES_AGENT=true. Hermes is NOT a pip package — it ships
258
+ # as a standalone binary via Nous Research's official installer (lands in
259
+ # ~/.local/bin/hermes). We only install if it's not already present so re-runs
260
+ # are fast, and we never fail the whole setup if the install fails (advanced
261
+ # mode is optional; the operator can install it manually and retry).
262
+ if [ "${ENABLE_HERMES_AGENT}" = "true" ]; then
263
+ echo ""
264
+ echo "Step 6.6: Installing Hermes Agent harness (ENABLE_HERMES_AGENT=true)..."
265
+ if command -v hermes &> /dev/null || [ -x "$HOME/.local/bin/hermes" ]; then
266
+ HERMES_VERSION=$( (command -v hermes &> /dev/null && hermes --version 2>/dev/null) || "$HOME/.local/bin/hermes" --version 2>/dev/null || echo "unknown")
267
+ print_success "Hermes already installed ($HERMES_VERSION) — skipping installer"
268
+ else
269
+ print_info "Running Hermes official installer..."
270
+ if curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash; then
271
+ print_success "Hermes Agent installed (binary at ~/.local/bin/hermes)"
272
+ else
273
+ print_warning "Hermes installer failed. Advanced agent mode will be unavailable until 'hermes' is on PATH. Install manually: https://hermes-agent.nousresearch.com/docs/getting-started/installation"
274
+ fi
275
+ fi
276
+
277
+ # Pre-pull the docker terminal-backend image so the first agent request
278
+ # isn't blocked on a cold image pull (~minute). Only when the backend is
279
+ # docker (the default) and docker is available; non-fatal otherwise.
280
+ HERMES_BACKEND="${HERMES_TERMINAL_BACKEND:-docker}"
281
+ if [ "${HERMES_BACKEND}" = "docker" ]; then
282
+ HERMES_IMG="${HERMES_DOCKER_IMAGE:-nikolaik/python-nodejs:python3.11-nodejs20}"
283
+ if command -v docker &> /dev/null; then
284
+ print_info "Pre-pulling Hermes docker backend image: ${HERMES_IMG}..."
285
+ if docker pull "${HERMES_IMG}" > /dev/null 2>&1; then
286
+ print_success "Docker backend image ready (${HERMES_IMG})"
287
+ else
288
+ print_warning "Could not pre-pull ${HERMES_IMG}; the first advanced-mode request will pull it (slower)."
289
+ fi
290
+ else
291
+ print_warning "Docker not found, but HERMES_TERMINAL_BACKEND=docker. Install Docker, or set HERMES_TERMINAL_BACKEND=local (unsandboxed)."
292
+ fi
293
+ fi
294
+ fi
295
+
256
296
  # Step 7: Validate installation
257
297
  echo ""
258
298
  echo "Step 7: Validating installation..."
@@ -269,6 +309,15 @@ $PYTHON_CMD -c "import whisperx" 2>/dev/null && print_success "whisperx imported
269
309
  $PYTHON_CMD -c "import pyannote.audio" 2>/dev/null && print_success "pyannote.audio imported successfully" || print_warning "pyannote.audio not importable (diarization will be disabled even with HF_AUTH_TOKEN)"
270
310
  $PYTHON_CMD -c "import fastapi, uvicorn" 2>/dev/null && print_success "fastapi/uvicorn imported successfully" || print_warning "fastapi/uvicorn not importable (transcription server will not start)"
271
311
 
312
+ # Hermes Agent binary check (advanced agent mode) — only when opted in.
313
+ if [ "${ENABLE_HERMES_AGENT}" = "true" ]; then
314
+ if command -v hermes &> /dev/null || [ -x "$HOME/.local/bin/hermes" ]; then
315
+ print_success "hermes binary available (advanced agent mode ready)"
316
+ else
317
+ print_warning "hermes binary not found (advanced agent mode will be unavailable)"
318
+ fi
319
+ fi
320
+
272
321
  # Step 8: Display summary
273
322
  echo ""
274
323
  echo -e "${GREEN}========================================${NC}"
package/ee/schemas.ts CHANGED
@@ -73,6 +73,10 @@ export const rolesSchema: ExuluTableDefinition = {
73
73
  name: "evals",
74
74
  type: "text", // write | read access to evals
75
75
  },
76
+ {
77
+ name: "budget_management",
78
+ type: "text", // write | read access to budgets
79
+ },
76
80
  ],
77
81
  };
78
82
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "1.63.3",
4
+ "version": "1.65.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {