@animus-labs/cortex 0.2.0 → 0.2.3

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.
Files changed (47) hide show
  1. package/dist/cortex-agent.d.ts +1 -0
  2. package/dist/cortex-agent.d.ts.map +1 -1
  3. package/dist/cortex-agent.js +34 -10
  4. package/dist/cortex-agent.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/provider-manager.d.ts +42 -2
  10. package/dist/provider-manager.d.ts.map +1 -1
  11. package/dist/provider-manager.js +195 -33
  12. package/dist/provider-manager.js.map +1 -1
  13. package/dist/provider-registry.d.ts +7 -9
  14. package/dist/provider-registry.d.ts.map +1 -1
  15. package/dist/provider-registry.js +11 -19
  16. package/dist/provider-registry.js.map +1 -1
  17. package/dist/tools/bash/index.d.ts +2 -0
  18. package/dist/tools/bash/index.d.ts.map +1 -1
  19. package/dist/tools/bash/index.js +3 -0
  20. package/dist/tools/bash/index.js.map +1 -1
  21. package/dist/tools/bash/safety.d.ts +2 -4
  22. package/dist/tools/bash/safety.d.ts.map +1 -1
  23. package/dist/tools/bash/safety.js +103 -20
  24. package/dist/tools/bash/safety.js.map +1 -1
  25. package/dist/tools/edit.d.ts.map +1 -1
  26. package/dist/tools/edit.js +4 -0
  27. package/dist/tools/edit.js.map +1 -1
  28. package/dist/tools/write.d.ts.map +1 -1
  29. package/dist/tools/write.js +13 -0
  30. package/dist/tools/write.js.map +1 -1
  31. package/dist/types.d.ts +5 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/utility-model-inference.d.ts +5 -0
  34. package/dist/utility-model-inference.d.ts.map +1 -0
  35. package/dist/utility-model-inference.js +174 -0
  36. package/dist/utility-model-inference.js.map +1 -0
  37. package/package.json +1 -1
  38. package/src/cortex-agent.ts +36 -10
  39. package/src/index.ts +5 -0
  40. package/src/provider-manager.ts +299 -39
  41. package/src/provider-registry.ts +12 -19
  42. package/src/tools/bash/index.ts +5 -0
  43. package/src/tools/bash/safety.ts +113 -23
  44. package/src/tools/edit.ts +6 -0
  45. package/src/tools/write.ts +14 -0
  46. package/src/types.ts +6 -0
  47. package/src/utility-model-inference.ts +203 -0
package/src/tools/edit.ts CHANGED
@@ -19,6 +19,7 @@ import type { ToolContentDetails } from '../types.js';
19
19
  import { computeDiff, type DiffHunk } from './write.js';
20
20
  import type { CortexToolRuntime } from './runtime.js';
21
21
  import { attachRuntimeAwareTool } from './runtime.js';
22
+ import { isCriticalPathOrDescendant } from './bash/safety.js';
22
23
  import {
23
24
  findMatch,
24
25
  findNearestMatch,
@@ -196,6 +197,11 @@ export function createEditTool(config: EditToolConfig): {
196
197
  const newString = params.new_string;
197
198
  const replaceAll = params.replace_all ?? false;
198
199
 
200
+ if (isCriticalPathOrDescendant(filePath)) {
201
+ return noChange(filePath, oldString, newString, replaceAll,
202
+ `Refusing to edit critical system path: ${filePath}`);
203
+ }
204
+
199
205
  // Check identical strings (no lock needed)
200
206
  if (oldString === newString) {
201
207
  return noChange(filePath, oldString, newString, replaceAll,
@@ -18,6 +18,7 @@ import type { ReadRegistry } from './shared/read-registry.js';
18
18
  import type { ToolContentDetails } from '../types.js';
19
19
  import type { CortexToolRuntime } from './runtime.js';
20
20
  import { attachRuntimeAwareTool } from './runtime.js';
21
+ import { isCriticalPathOrDescendant } from './bash/safety.js';
21
22
 
22
23
  // ---------------------------------------------------------------------------
23
24
  // Schema
@@ -148,6 +149,19 @@ export function createWriteTool(config: WriteToolConfig): {
148
149
  const filePath = path.resolve(params.file_path);
149
150
  const newContent = params.content;
150
151
 
152
+ if (isCriticalPathOrDescendant(filePath)) {
153
+ return {
154
+ content: [{ type: 'text', text: `Refusing to write to critical system path: ${filePath}` }],
155
+ details: {
156
+ filePath,
157
+ isCreate: false,
158
+ bytesWritten: 0,
159
+ diff: null,
160
+ originalContent: null,
161
+ },
162
+ };
163
+ }
164
+
151
165
  // Check if file exists (before acquiring lock)
152
166
  let fileExists = false;
153
167
  try {
package/src/types.ts CHANGED
@@ -230,6 +230,12 @@ export interface CortexAgentConfig {
230
230
  shellPath?: string;
231
231
  };
232
232
 
233
+ /**
234
+ * Whether the consumer is currently auto-approving tool calls.
235
+ * Used by built-in tool safety gates that need stricter checks in auto mode.
236
+ */
237
+ isAutoApprove?: () => boolean;
238
+
233
239
  /**
234
240
  * Disable specific built-in tools by name.
235
241
  * Built-in tools (Read, Write, Edit, Glob, Grep, Bash, WebFetch, TaskOutput)
@@ -0,0 +1,203 @@
1
+ type RawModel = Record<string, unknown>;
2
+
3
+ const MIN_UTILITY_CONTEXT_WINDOW = 32_000;
4
+
5
+ const SPECIAL_PURPOSE_MODEL_PATTERN = /(?:embedding|embed|rerank|moderation|whisper|tts|audio|speech|image-generation|vision|live|deep-research|safety|safeguard|guard|search|transcrib)/i;
6
+
7
+ const UTILITY_TERMS: Array<{ pattern: RegExp; score: number }> = [
8
+ { pattern: /(?:^|[\s._/-])flash[\s._/-]?lite(?:$|[\s._/-])/, score: 110 },
9
+ { pattern: /(?:^|[\s._/-])nano(?:$|[\s._/-])/, score: 100 },
10
+ { pattern: /(?:^|[\s._/-])mini(?:$|[\s._/-])/, score: 95 },
11
+ { pattern: /(?:^|[\s._/-])haiku(?:$|[\s._/-])/, score: 95 },
12
+ { pattern: /(?:^|[\s._/-])small(?:$|[\s._/-])/, score: 85 },
13
+ { pattern: /(?:^|[\s._/-])fast(?:$|[\s._/-])/, score: 80 },
14
+ { pattern: /(?:^|[\s._/-])spark(?:$|[\s._/-])/, score: 80 },
15
+ { pattern: /(?:^|[\s._/-])instant(?:$|[\s._/-])/, score: 75 },
16
+ { pattern: /(?:^|[\s._/-])lite(?:$|[\s._/-])/, score: 70 },
17
+ { pattern: /(?:^|[\s._/-])flash(?:$|[\s._/-])/, score: 65 },
18
+ { pattern: /(?:^|[\s._/-])(?:7|8)b(?:$|[\s._/-])/, score: 65 },
19
+ { pattern: /(?:^|[\s._/-])(?:12|20)b(?:$|[\s._/-])/, score: 45 },
20
+ { pattern: /(?:^|[\s._/-])32b(?:$|[\s._/-])/, score: 25 },
21
+ ];
22
+
23
+ interface UtilityCandidate {
24
+ model: RawModel;
25
+ id: string;
26
+ name: string;
27
+ utilityScore: number;
28
+ recencyScore: number;
29
+ costScore: number;
30
+ }
31
+
32
+ export function inferUtilityModel(models: readonly RawModel[] | null | undefined): RawModel | null {
33
+ if (!Array.isArray(models)) return null;
34
+
35
+ const capable = models
36
+ .map(toUtilityCandidate)
37
+ .filter((candidate): candidate is UtilityCandidate => candidate !== null);
38
+
39
+ const utilityCandidates = capable.filter(candidate => candidate.utilityScore > 0);
40
+ if (utilityCandidates.length > 0) {
41
+ return [...utilityCandidates].sort(compareUtilityCandidates)[0]!.model;
42
+ }
43
+ if (capable.length === 0) return null;
44
+
45
+ return [...capable].sort(compareFallbackCandidates)[0]!.model;
46
+ }
47
+
48
+ export function inferUtilityModelId(models: readonly RawModel[] | null | undefined): string | null {
49
+ const model = inferUtilityModel(models);
50
+ const id = model?.['id'];
51
+ if (typeof id === 'string') return id;
52
+ const name = model?.['name'];
53
+ return typeof name === 'string' ? name : null;
54
+ }
55
+
56
+ function toUtilityCandidate(model: RawModel): UtilityCandidate | null {
57
+ const id = getString(model['id']) ?? getString(model['name']);
58
+ if (!id) return null;
59
+
60
+ const name = getString(model['name']) ?? id;
61
+ const searchable = `${id} ${name}`.toLowerCase();
62
+ if (SPECIAL_PURPOSE_MODEL_PATTERN.test(searchable)) return null;
63
+
64
+ if (!supportsText(model)) return null;
65
+
66
+ const contextWindow = getNumber(model['contextWindow']) ?? 0;
67
+ if (contextWindow > 0 && contextWindow < MIN_UTILITY_CONTEXT_WINDOW) return null;
68
+
69
+ return {
70
+ model,
71
+ id,
72
+ name,
73
+ utilityScore: inferUtilityScore(searchable),
74
+ recencyScore: inferRecencyScore(searchable),
75
+ costScore: inferCostScore(model),
76
+ };
77
+ }
78
+
79
+ function compareUtilityCandidates(a: UtilityCandidate, b: UtilityCandidate): number {
80
+ if (b.recencyScore !== a.recencyScore) return b.recencyScore - a.recencyScore;
81
+ if (a.costScore !== b.costScore) return a.costScore - b.costScore;
82
+ if (b.utilityScore !== a.utilityScore) return b.utilityScore - a.utilityScore;
83
+ return a.id.localeCompare(b.id);
84
+ }
85
+
86
+ function compareFallbackCandidates(a: UtilityCandidate, b: UtilityCandidate): number {
87
+ if (a.costScore !== b.costScore) return a.costScore - b.costScore;
88
+ if (b.recencyScore !== a.recencyScore) return b.recencyScore - a.recencyScore;
89
+ return a.id.localeCompare(b.id);
90
+ }
91
+
92
+ function supportsText(model: RawModel): boolean {
93
+ const input = model['input'];
94
+ if (!Array.isArray(input)) return true;
95
+ return input.includes('text');
96
+ }
97
+
98
+ function inferUtilityScore(searchable: string): number {
99
+ let score = 0;
100
+ for (const term of UTILITY_TERMS) {
101
+ if (term.pattern.test(searchable)) {
102
+ score = Math.max(score, term.score);
103
+ }
104
+ }
105
+ return score;
106
+ }
107
+
108
+ function inferRecencyScore(searchable: string): number {
109
+ const dateScore = inferDateScore(searchable);
110
+ const versionScore = inferVersionScore(searchable);
111
+ return Math.max(dateScore, versionScore);
112
+ }
113
+
114
+ function inferDateScore(searchable: string): number {
115
+ let score = 0;
116
+
117
+ for (const match of searchable.matchAll(/20\d{6}/g)) {
118
+ const value = Number(match[0]);
119
+ if (isValidDateScore(value)) {
120
+ score = Math.max(score, value);
121
+ }
122
+ }
123
+
124
+ for (const match of searchable.matchAll(/(20\d{2})[-_/](0[1-9]|1[0-2])/g)) {
125
+ score = Math.max(score, Number(match[1]) * 10_000 + Number(match[2]) * 100);
126
+ }
127
+
128
+ for (const match of searchable.matchAll(/(0[1-9]|1[0-2])[-_/](20\d{2})/g)) {
129
+ score = Math.max(score, Number(match[2]) * 10_000 + Number(match[1]) * 100);
130
+ }
131
+
132
+ for (const match of searchable.matchAll(/(?:^|[^\d])(\d{4})(?:$|[^\da-z])/g)) {
133
+ const raw = match[1]!;
134
+ const year = Number(raw.slice(0, 2));
135
+ const month = Number(raw.slice(2, 4));
136
+ if (year >= 20 && year <= 40 && month >= 1 && month <= 12) {
137
+ score = Math.max(score, 20_000_000 + year * 10_000 + month * 100);
138
+ }
139
+ }
140
+
141
+ return score;
142
+ }
143
+
144
+ function inferVersionScore(searchable: string): number {
145
+ const scrubbed = searchable
146
+ .replace(/20\d{6}/g, ' ')
147
+ .replace(/(20\d{2})[-_/](0[1-9]|1[0-2])/g, ' ')
148
+ .replace(/(0[1-9]|1[0-2])[-_/](20\d{2})/g, ' ')
149
+ .replace(/(?:^|[^\d])(\d{4})(?:$|[^\da-z])/g, ' ')
150
+ .replace(/\b\d+(?:\.\d+)?b\b/g, ' ');
151
+
152
+ let score = 0;
153
+ for (const match of scrubbed.matchAll(/\d+(?:\.\d+)+/g)) {
154
+ score = Math.max(score, scoreVersionParts(match[0]!.split('.').map(Number)));
155
+ }
156
+
157
+ const tokens = scrubbed.split(/[^a-z0-9]+/).filter(Boolean);
158
+ for (let i = 0; i < tokens.length; i++) {
159
+ if (!/^\d+$/.test(tokens[i]!)) continue;
160
+ const parts: number[] = [];
161
+ for (let j = i; j < tokens.length && /^\d+$/.test(tokens[j]!) && parts.length < 4; j++) {
162
+ parts.push(Number(tokens[j]));
163
+ }
164
+ if (parts.length >= 2) {
165
+ score = Math.max(score, scoreVersionParts(parts));
166
+ }
167
+ }
168
+
169
+ return score;
170
+ }
171
+
172
+ function scoreVersionParts(parts: number[]): number {
173
+ const weights = [1_000_000, 10_000, 100, 1];
174
+ return parts.slice(0, weights.length).reduce((score, part, index) => (
175
+ Number.isFinite(part) ? score + part * weights[index]! : score
176
+ ), 0);
177
+ }
178
+
179
+ function inferCostScore(model: RawModel): number {
180
+ const cost = model['cost'] ?? model['pricing'];
181
+ if (!cost || typeof cost !== 'object') return Number.MAX_SAFE_INTEGER;
182
+
183
+ const rawCost = cost as RawModel;
184
+ const input = getNumber(rawCost['input']) ?? 0;
185
+ const output = getNumber(rawCost['output']) ?? 0;
186
+ if (input === 0 && output === 0) return Number.MAX_SAFE_INTEGER - 1;
187
+ return input + output * 3;
188
+ }
189
+
190
+ function isValidDateScore(value: number): boolean {
191
+ const year = Math.floor(value / 10_000);
192
+ const month = Math.floor((value % 10_000) / 100);
193
+ const day = value % 100;
194
+ return year >= 2020 && year <= 2040 && month >= 1 && month <= 12 && day >= 1 && day <= 31;
195
+ }
196
+
197
+ function getString(value: unknown): string | null {
198
+ return typeof value === 'string' && value.length > 0 ? value : null;
199
+ }
200
+
201
+ function getNumber(value: unknown): number | null {
202
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
203
+ }