cascade-ai 0.12.23 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/cli.cjs +828 -177
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +823 -172
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1504 -893
- package/dist/index.cjs +779 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +139 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +756 -140
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var Database = require('better-sqlite3');
|
|
4
|
+
var path20 = require('path');
|
|
5
|
+
var fs19 = require('fs');
|
|
6
|
+
var os6 = require('os');
|
|
7
|
+
var crypto3 = require('crypto');
|
|
3
8
|
var EventEmitter = require('events');
|
|
4
|
-
var crypto = require('crypto');
|
|
5
9
|
var glob = require('glob');
|
|
6
10
|
var Anthropic = require('@anthropic-ai/sdk');
|
|
7
11
|
var OpenAI = require('openai');
|
|
@@ -12,12 +16,9 @@ var https = require('https');
|
|
|
12
16
|
var zlib = require('zlib');
|
|
13
17
|
var stream = require('stream');
|
|
14
18
|
var fs4 = require('fs/promises');
|
|
15
|
-
var path18 = require('path');
|
|
16
|
-
var os4 = require('os');
|
|
17
19
|
var ignoreFactory = require('ignore');
|
|
18
20
|
var child_process = require('child_process');
|
|
19
21
|
var util = require('util');
|
|
20
|
-
var fs17 = require('fs');
|
|
21
22
|
var simpleGit = require('simple-git');
|
|
22
23
|
var PDFDocument = require('pdfkit');
|
|
23
24
|
var dns2 = require('dns/promises');
|
|
@@ -26,7 +27,6 @@ var index_js = require('@modelcontextprotocol/sdk/client/index.js');
|
|
|
26
27
|
var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
27
28
|
var zod = require('zod');
|
|
28
29
|
var worker_threads = require('worker_threads');
|
|
29
|
-
var Database = require('better-sqlite3');
|
|
30
30
|
var url = require('url');
|
|
31
31
|
var express = require('express');
|
|
32
32
|
var rateLimit = require('express-rate-limit');
|
|
@@ -57,8 +57,12 @@ function _interopNamespace(e) {
|
|
|
57
57
|
return Object.freeze(n);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
61
|
+
var path20__default = /*#__PURE__*/_interopDefault(path20);
|
|
62
|
+
var fs19__default = /*#__PURE__*/_interopDefault(fs19);
|
|
63
|
+
var os6__default = /*#__PURE__*/_interopDefault(os6);
|
|
64
|
+
var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
|
|
60
65
|
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
61
|
-
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
62
66
|
var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
|
|
63
67
|
var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
|
|
64
68
|
var dns__default = /*#__PURE__*/_interopDefault(dns);
|
|
@@ -66,14 +70,10 @@ var http__default = /*#__PURE__*/_interopDefault(http);
|
|
|
66
70
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
67
71
|
var zlib__default = /*#__PURE__*/_interopDefault(zlib);
|
|
68
72
|
var fs4__default = /*#__PURE__*/_interopDefault(fs4);
|
|
69
|
-
var path18__default = /*#__PURE__*/_interopDefault(path18);
|
|
70
|
-
var os4__default = /*#__PURE__*/_interopDefault(os4);
|
|
71
73
|
var ignoreFactory__namespace = /*#__PURE__*/_interopNamespace(ignoreFactory);
|
|
72
|
-
var fs17__default = /*#__PURE__*/_interopDefault(fs17);
|
|
73
74
|
var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
|
|
74
75
|
var dns2__default = /*#__PURE__*/_interopDefault(dns2);
|
|
75
76
|
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
76
|
-
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
77
77
|
var express__default = /*#__PURE__*/_interopDefault(express);
|
|
78
78
|
var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
|
|
79
79
|
var bcrypt__default = /*#__PURE__*/_interopDefault(bcrypt);
|
|
@@ -82,10 +82,161 @@ var jwt__default = /*#__PURE__*/_interopDefault(jwt);
|
|
|
82
82
|
var cron__default = /*#__PURE__*/_interopDefault(cron);
|
|
83
83
|
|
|
84
84
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
85
|
+
var __defProp = Object.defineProperty;
|
|
86
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
87
|
+
var __esm = (fn, res) => function __init() {
|
|
88
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
89
|
+
};
|
|
90
|
+
var __export = (target, all) => {
|
|
91
|
+
for (var name in all)
|
|
92
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
93
|
+
};
|
|
85
94
|
|
|
95
|
+
// src/core/audit/audit-logger.ts
|
|
96
|
+
var audit_logger_exports = {};
|
|
97
|
+
__export(audit_logger_exports, {
|
|
98
|
+
AuditLogger: () => AuditLogger2
|
|
99
|
+
});
|
|
100
|
+
var AuditLogger2;
|
|
101
|
+
var init_audit_logger = __esm({
|
|
102
|
+
"src/core/audit/audit-logger.ts"() {
|
|
103
|
+
AuditLogger2 = class {
|
|
104
|
+
constructor(workspacePath, debugMode = false) {
|
|
105
|
+
this.workspacePath = workspacePath;
|
|
106
|
+
this.debugMode = debugMode;
|
|
107
|
+
const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
|
|
108
|
+
if (!fs19__default.default.existsSync(cascadeDir)) {
|
|
109
|
+
fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
this.keyPath = path20__default.default.join(cascadeDir, "audit_log.key");
|
|
112
|
+
this.dbPath = path20__default.default.join(cascadeDir, "audit_log.db");
|
|
113
|
+
this.initEncryptionKey();
|
|
114
|
+
this.db = new Database__default.default(this.dbPath);
|
|
115
|
+
this.db.pragma("journal_mode = WAL");
|
|
116
|
+
this.db.exec(`
|
|
117
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
118
|
+
id TEXT PRIMARY KEY,
|
|
119
|
+
timestamp TEXT NOT NULL,
|
|
120
|
+
event_type TEXT NOT NULL,
|
|
121
|
+
tier_id TEXT NOT NULL,
|
|
122
|
+
encrypted_payload TEXT NOT NULL,
|
|
123
|
+
prev_hash TEXT NOT NULL DEFAULT '',
|
|
124
|
+
hash TEXT NOT NULL DEFAULT ''
|
|
125
|
+
)
|
|
126
|
+
`);
|
|
127
|
+
const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
|
|
128
|
+
if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
|
|
129
|
+
if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
|
|
130
|
+
}
|
|
131
|
+
workspacePath;
|
|
132
|
+
debugMode;
|
|
133
|
+
db;
|
|
134
|
+
keyPath;
|
|
135
|
+
dbPath;
|
|
136
|
+
encryptionKey;
|
|
137
|
+
/** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
|
|
138
|
+
chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
|
|
139
|
+
return crypto3__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
140
|
+
}
|
|
141
|
+
initEncryptionKey() {
|
|
142
|
+
if (fs19__default.default.existsSync(this.keyPath)) {
|
|
143
|
+
this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
|
|
144
|
+
} else {
|
|
145
|
+
this.encryptionKey = crypto3__default.default.randomBytes(32);
|
|
146
|
+
fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
encrypt(text) {
|
|
150
|
+
const iv = crypto3__default.default.randomBytes(16);
|
|
151
|
+
const cipher = crypto3__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
152
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
153
|
+
encrypted += cipher.final("hex");
|
|
154
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
155
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
156
|
+
}
|
|
157
|
+
decrypt(text) {
|
|
158
|
+
const parts = text.split(":");
|
|
159
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
160
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
161
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
162
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
163
|
+
const decipher = crypto3__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
164
|
+
decipher.setAuthTag(authTag);
|
|
165
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
166
|
+
decrypted += decipher.final("utf8");
|
|
167
|
+
return decrypted;
|
|
168
|
+
}
|
|
169
|
+
logEvent(eventType, tierId, payloadObj) {
|
|
170
|
+
const id = crypto3__default.default.randomUUID();
|
|
171
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
172
|
+
const payload = JSON.stringify(payloadObj);
|
|
173
|
+
const encryptedPayload = this.encrypt(payload);
|
|
174
|
+
const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
|
|
175
|
+
const prevHash = last?.hash ?? "";
|
|
176
|
+
const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
|
|
177
|
+
const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
178
|
+
stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
|
|
179
|
+
this.dumpDebugIfNeeded();
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Recompute the whole hash chain in insertion (rowid) order. Any edited,
|
|
184
|
+
* removed, or reordered row makes every later hash mismatch. Rows written
|
|
185
|
+
* before the chain existed (empty hash) fail verification too — integrity
|
|
186
|
+
* can only be claimed for chained history.
|
|
187
|
+
*/
|
|
188
|
+
verifyChain() {
|
|
189
|
+
const rows = this.db.prepare(
|
|
190
|
+
"SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
|
|
191
|
+
).all();
|
|
192
|
+
let prevHash = "";
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
|
|
195
|
+
if (row.prev_hash !== prevHash || row.hash !== expected) {
|
|
196
|
+
return { ok: false, entries: rows.length, firstBadRow: row.rowid };
|
|
197
|
+
}
|
|
198
|
+
prevHash = row.hash;
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, entries: rows.length };
|
|
201
|
+
}
|
|
202
|
+
getAllLogs() {
|
|
203
|
+
const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
|
|
204
|
+
const rows = stmt.all();
|
|
205
|
+
return rows.map((row) => {
|
|
206
|
+
let payload = "";
|
|
207
|
+
try {
|
|
208
|
+
payload = this.decrypt(row.encrypted_payload);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
id: row.id,
|
|
214
|
+
timestamp: row.timestamp,
|
|
215
|
+
eventType: row.event_type,
|
|
216
|
+
tierId: row.tier_id,
|
|
217
|
+
payload
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
dumpDebugIfNeeded() {
|
|
222
|
+
if (!this.debugMode) return;
|
|
223
|
+
try {
|
|
224
|
+
const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
|
|
225
|
+
const entries = this.getAllLogs();
|
|
226
|
+
fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error("Failed to dump debug audit logs", err);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
close() {
|
|
232
|
+
this.db.close();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
86
237
|
|
|
87
238
|
// src/constants.ts
|
|
88
|
-
var CASCADE_VERSION = "0.
|
|
239
|
+
var CASCADE_VERSION = "0.13.1";
|
|
89
240
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
90
241
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
91
242
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -2017,7 +2168,7 @@ function computeDelegationSavings(stats, t1Model) {
|
|
|
2017
2168
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
2018
2169
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
2019
2170
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
2020
|
-
var DEFAULT_CACHE_FILE =
|
|
2171
|
+
var DEFAULT_CACHE_FILE = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
2021
2172
|
function normalizeModelId(id) {
|
|
2022
2173
|
let s = id.toLowerCase();
|
|
2023
2174
|
const slash = s.lastIndexOf("/");
|
|
@@ -2132,7 +2283,7 @@ var LiveDataProvider = class {
|
|
|
2132
2283
|
}
|
|
2133
2284
|
async saveCache() {
|
|
2134
2285
|
try {
|
|
2135
|
-
await fs4__default.default.mkdir(
|
|
2286
|
+
await fs4__default.default.mkdir(path20__default.default.dirname(this.opts.cacheFile), { recursive: true });
|
|
2136
2287
|
const cache = {
|
|
2137
2288
|
fetchedAt: this.fetchedAt,
|
|
2138
2289
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -2262,7 +2413,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2262
2413
|
costByTier: {},
|
|
2263
2414
|
tokensByTier: {},
|
|
2264
2415
|
inputTokensByTier: {},
|
|
2265
|
-
outputTokensByTier: {}
|
|
2416
|
+
outputTokensByTier: {},
|
|
2417
|
+
costByFeature: {}
|
|
2266
2418
|
};
|
|
2267
2419
|
tierModels = /* @__PURE__ */ new Map();
|
|
2268
2420
|
config;
|
|
@@ -2284,6 +2436,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2284
2436
|
tpmLimiter;
|
|
2285
2437
|
localQueue;
|
|
2286
2438
|
taskAnalyzer;
|
|
2439
|
+
worldStateDB;
|
|
2440
|
+
privacyPaths;
|
|
2441
|
+
guidanceQueue;
|
|
2287
2442
|
liveData;
|
|
2288
2443
|
/** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
|
|
2289
2444
|
originalTierModels;
|
|
@@ -2441,7 +2596,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2441
2596
|
if (options.model && !requireVision) {
|
|
2442
2597
|
this.ensureProvider(options.model, this.config.providers);
|
|
2443
2598
|
}
|
|
2444
|
-
|
|
2599
|
+
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
|
|
2600
|
+
if (options.forceLocal && model && !this.isPrivateModel(model)) {
|
|
2601
|
+
const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
|
|
2602
|
+
if (!localModel) {
|
|
2603
|
+
throw new Error(
|
|
2604
|
+
"privacy.paths: this subtask touches a local-only path but no LOCAL model is available. Configure Ollama or an OpenAI-compatible endpoint on a loopback/private host, or remove the privacy policy."
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
this.ensureProvider(localModel, this.config.providers);
|
|
2608
|
+
model = localModel;
|
|
2609
|
+
}
|
|
2445
2610
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
2446
2611
|
const provider = this.getProvider(model);
|
|
2447
2612
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
@@ -2513,7 +2678,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2513
2678
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
2514
2679
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
2515
2680
|
}
|
|
2516
|
-
this.recordStats(tier, model, result.usage);
|
|
2681
|
+
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
2517
2682
|
this.failover.recordSuccess(model.provider);
|
|
2518
2683
|
return result;
|
|
2519
2684
|
} catch (err) {
|
|
@@ -2618,6 +2783,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2618
2783
|
setTaskAnalyzer(analyzer) {
|
|
2619
2784
|
this.taskAnalyzer = analyzer;
|
|
2620
2785
|
}
|
|
2786
|
+
setWorldStateDB(db) {
|
|
2787
|
+
this.worldStateDB = db;
|
|
2788
|
+
}
|
|
2789
|
+
getWorldStateDB() {
|
|
2790
|
+
return this.worldStateDB;
|
|
2791
|
+
}
|
|
2792
|
+
setPrivacyPaths(paths) {
|
|
2793
|
+
this.privacyPaths = paths;
|
|
2794
|
+
}
|
|
2795
|
+
getPrivacyPaths() {
|
|
2796
|
+
return this.privacyPaths;
|
|
2797
|
+
}
|
|
2798
|
+
setGuidanceQueue(queue) {
|
|
2799
|
+
this.guidanceQueue = queue;
|
|
2800
|
+
}
|
|
2801
|
+
getGuidanceQueue() {
|
|
2802
|
+
return this.guidanceQueue;
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* "Private" = inference never leaves the user's machine/network: Ollama
|
|
2806
|
+
* models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
|
|
2807
|
+
* LM Studio) whose configured host is loopback or a private range. A cloud
|
|
2808
|
+
* OpenAI-compatible endpoint (public host) does NOT qualify.
|
|
2809
|
+
*/
|
|
2810
|
+
isPrivateModel(model) {
|
|
2811
|
+
if (model.isLocal) return true;
|
|
2812
|
+
if (model.provider !== "openai-compatible") return false;
|
|
2813
|
+
const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
|
|
2814
|
+
if (!baseUrl) return false;
|
|
2815
|
+
try {
|
|
2816
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
2817
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.endsWith(".local");
|
|
2818
|
+
} catch {
|
|
2819
|
+
return false;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2621
2822
|
/**
|
|
2622
2823
|
* Cascade Auto per-subtask routing: pick the benchmark-best model for a
|
|
2623
2824
|
* specific subtask's text, scoped to the tier's eligible candidates. Returns
|
|
@@ -2641,7 +2842,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2641
2842
|
costByTier: { ...this.stats.costByTier },
|
|
2642
2843
|
tokensByTier: { ...this.stats.tokensByTier },
|
|
2643
2844
|
inputTokensByTier: { ...this.stats.inputTokensByTier },
|
|
2644
|
-
outputTokensByTier: { ...this.stats.outputTokensByTier }
|
|
2845
|
+
outputTokensByTier: { ...this.stats.outputTokensByTier },
|
|
2846
|
+
costByFeature: { ...this.stats.costByFeature }
|
|
2645
2847
|
};
|
|
2646
2848
|
}
|
|
2647
2849
|
/**
|
|
@@ -2691,7 +2893,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2691
2893
|
costByTier: {},
|
|
2692
2894
|
tokensByTier: {},
|
|
2693
2895
|
inputTokensByTier: {},
|
|
2694
|
-
outputTokensByTier: {}
|
|
2896
|
+
outputTokensByTier: {},
|
|
2897
|
+
costByFeature: {}
|
|
2695
2898
|
};
|
|
2696
2899
|
this.sessionCostUsd = 0;
|
|
2697
2900
|
this.budgetState = "ok";
|
|
@@ -2857,7 +3060,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2857
3060
|
}
|
|
2858
3061
|
return void 0;
|
|
2859
3062
|
}
|
|
2860
|
-
recordStats(tier, model, usage) {
|
|
3063
|
+
recordStats(tier, model, usage, featureTag) {
|
|
2861
3064
|
this.stats.totalTokens += usage.totalTokens;
|
|
2862
3065
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
2863
3066
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -2867,6 +3070,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
2867
3070
|
this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
|
|
2868
3071
|
this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
|
|
2869
3072
|
this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
|
|
3073
|
+
if (featureTag) {
|
|
3074
|
+
this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
|
|
3075
|
+
}
|
|
2870
3076
|
this.runTokens += usage.totalTokens;
|
|
2871
3077
|
this.runCostUsd += usage.estimatedCostUsd;
|
|
2872
3078
|
this.updateBudgetState();
|
|
@@ -2966,7 +3172,7 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
2966
3172
|
constructor(role, id, parentId) {
|
|
2967
3173
|
super();
|
|
2968
3174
|
this.role = role;
|
|
2969
|
-
this.id = id ?? `${role}_${
|
|
3175
|
+
this.id = id ?? `${role}_${crypto3.randomUUID().slice(0, 8)}`;
|
|
2970
3176
|
this.parentId = parentId;
|
|
2971
3177
|
this.label = this.id;
|
|
2972
3178
|
}
|
|
@@ -3217,7 +3423,7 @@ var AuditLogger = class {
|
|
|
3217
3423
|
logAt(level, tierId, action, details) {
|
|
3218
3424
|
if (!this.shouldLog(level)) return;
|
|
3219
3425
|
const entry = {
|
|
3220
|
-
id:
|
|
3426
|
+
id: crypto3.randomUUID(),
|
|
3221
3427
|
sessionId: this.sessionId,
|
|
3222
3428
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3223
3429
|
tierId,
|
|
@@ -3403,6 +3609,8 @@ var T3Worker = class extends BaseTier {
|
|
|
3403
3609
|
toolRegistry;
|
|
3404
3610
|
context;
|
|
3405
3611
|
assignment;
|
|
3612
|
+
/** True when this subtask matched a privacy.paths local-only pattern. */
|
|
3613
|
+
localOnlyMatch = false;
|
|
3406
3614
|
peerSyncBuffer = [];
|
|
3407
3615
|
store;
|
|
3408
3616
|
audit;
|
|
@@ -3458,6 +3666,11 @@ var T3Worker = class extends BaseTier {
|
|
|
3458
3666
|
this.taskId = taskId;
|
|
3459
3667
|
this.setLabel(assignment.subtaskTitle);
|
|
3460
3668
|
this.setStatus("ACTIVE");
|
|
3669
|
+
const privacy = this.router.getPrivacyPaths?.();
|
|
3670
|
+
this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
|
|
3671
|
+
if (this.localOnlyMatch) {
|
|
3672
|
+
this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
|
|
3673
|
+
}
|
|
3461
3674
|
this.tools = this.toolRegistry.getToolDefinitions();
|
|
3462
3675
|
if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
|
|
3463
3676
|
this.tools = [...this.tools, {
|
|
@@ -3588,9 +3801,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
3588
3801
|
}
|
|
3589
3802
|
const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
|
|
3590
3803
|
if (reflectCfg.enabled) {
|
|
3591
|
-
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
|
|
3804
|
+
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
|
|
3592
3805
|
output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
|
|
3593
3806
|
}
|
|
3807
|
+
const db = this.router.getWorldStateDB?.();
|
|
3808
|
+
if (db) {
|
|
3809
|
+
try {
|
|
3810
|
+
db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
|
|
3811
|
+
} catch (e) {
|
|
3812
|
+
this.log("Failed to write to World State DB");
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3594
3815
|
this.setStatus("COMPLETED", output);
|
|
3595
3816
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
3596
3817
|
this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
|
|
@@ -3663,6 +3884,16 @@ Now execute your subtask using this context where relevant.`
|
|
|
3663
3884
|
while (iterations < MAX_ITERATIONS) {
|
|
3664
3885
|
iterations++;
|
|
3665
3886
|
this.throwIfCancelled();
|
|
3887
|
+
const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
|
|
3888
|
+
for (const g of guidance) {
|
|
3889
|
+
this.log(`User intervention received: ${g.text}`);
|
|
3890
|
+
this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
|
|
3891
|
+
await this.context.addMessage({
|
|
3892
|
+
role: "user",
|
|
3893
|
+
content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
|
|
3894
|
+
${g.text}`
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3666
3897
|
const options = {
|
|
3667
3898
|
messages: this.context.getMessages(),
|
|
3668
3899
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -3671,7 +3902,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3671
3902
|
// Don't pass tools array when model can't use them natively
|
|
3672
3903
|
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
3673
3904
|
maxTokens: 4096,
|
|
3674
|
-
...subtaskModel ? { model: subtaskModel } : {}
|
|
3905
|
+
...subtaskModel ? { model: subtaskModel } : {},
|
|
3906
|
+
featureTag: this.assignment?.sectionTitle,
|
|
3907
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
3675
3908
|
};
|
|
3676
3909
|
const result = await this.router.generate(
|
|
3677
3910
|
"T3",
|
|
@@ -3835,8 +4068,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
3835
4068
|
tierId: this.id,
|
|
3836
4069
|
sessionId: this.taskId,
|
|
3837
4070
|
requireApproval: false,
|
|
3838
|
-
saveSnapshot: async (
|
|
3839
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
4071
|
+
saveSnapshot: async (path21, content) => {
|
|
4072
|
+
this.store?.addFileSnapshot(this.taskId, path21, content);
|
|
3840
4073
|
},
|
|
3841
4074
|
sendPeerSync: (to, syncType, content) => {
|
|
3842
4075
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -4006,7 +4239,7 @@ ${assignment.expectedOutput}`;
|
|
|
4006
4239
|
const { promisify: promisify4 } = await import('util');
|
|
4007
4240
|
const execAsync2 = promisify4(exec2);
|
|
4008
4241
|
for (const artifactPath of artifactPaths) {
|
|
4009
|
-
const absolutePath =
|
|
4242
|
+
const absolutePath = path20__default.default.resolve(process.cwd(), artifactPath);
|
|
4010
4243
|
try {
|
|
4011
4244
|
const stat = await fs4__default.default.stat(absolutePath);
|
|
4012
4245
|
if (!stat.isFile()) {
|
|
@@ -4027,7 +4260,7 @@ ${assignment.expectedOutput}`;
|
|
|
4027
4260
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
4028
4261
|
continue;
|
|
4029
4262
|
}
|
|
4030
|
-
const ext =
|
|
4263
|
+
const ext = path20__default.default.extname(absolutePath).toLowerCase();
|
|
4031
4264
|
try {
|
|
4032
4265
|
if (ext === ".ts" || ext === ".tsx") {
|
|
4033
4266
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -4056,30 +4289,35 @@ ${stdout}`);
|
|
|
4056
4289
|
* needed. Best-effort: any parse/error just keeps the current output.
|
|
4057
4290
|
*/
|
|
4058
4291
|
async reflectAndImprove(assignment, output, maxRounds) {
|
|
4059
|
-
const sys = this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4060
|
-
|
|
4061
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
|
|
4062
4292
|
let current = output;
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
const
|
|
4293
|
+
try {
|
|
4294
|
+
for (let round = 0; round < Math.max(1, maxRounds); round++) {
|
|
4295
|
+
const verdictResult = await this.router.generate("T2", {
|
|
4066
4296
|
messages: [{
|
|
4067
4297
|
role: "user",
|
|
4068
|
-
content: `
|
|
4298
|
+
content: `You are an independent critic reviewing another worker's output against its assignment.
|
|
4069
4299
|
|
|
4070
|
-
Goal
|
|
4300
|
+
Goal: ${assignment.expectedOutput}
|
|
4071
4301
|
Subtask: ${assignment.description}
|
|
4072
|
-
|
|
4073
|
-
Output:
|
|
4302
|
+
Current Output:
|
|
4074
4303
|
${current}
|
|
4075
4304
|
|
|
4076
|
-
|
|
4305
|
+
Is this output sufficient and correct? Respond with ONLY a JSON object:
|
|
4306
|
+
{"sufficient": true|false, "notes": "what is wrong or missing if false"}`
|
|
4077
4307
|
}],
|
|
4078
|
-
systemPrompt:
|
|
4079
|
-
maxTokens: 400
|
|
4308
|
+
systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
|
|
4309
|
+
maxTokens: 400,
|
|
4310
|
+
signal: this.signal,
|
|
4311
|
+
featureTag: assignment.sectionTitle,
|
|
4312
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4080
4313
|
});
|
|
4081
|
-
const
|
|
4082
|
-
|
|
4314
|
+
const match = /\{[\s\S]*\}/.exec(verdictResult.content);
|
|
4315
|
+
const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
|
|
4316
|
+
if (parsed.sufficient !== false) {
|
|
4317
|
+
this.log("T2-Critic approved output.");
|
|
4318
|
+
break;
|
|
4319
|
+
}
|
|
4320
|
+
this.log(`T2-Critic rejected output: ${parsed.notes}`);
|
|
4083
4321
|
const improved = await this.router.generate("T3", {
|
|
4084
4322
|
messages: [{
|
|
4085
4323
|
role: "user",
|
|
@@ -4091,16 +4329,20 @@ Goal / expected: ${assignment.expectedOutput}
|
|
|
4091
4329
|
Current output:
|
|
4092
4330
|
${current}`
|
|
4093
4331
|
}],
|
|
4094
|
-
systemPrompt:
|
|
4095
|
-
|
|
4332
|
+
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4333
|
+
|
|
4334
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
4335
|
+
maxTokens: 4096,
|
|
4336
|
+
featureTag: assignment.sectionTitle,
|
|
4337
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4096
4338
|
});
|
|
4097
4339
|
const next = (improved.content ?? "").trim();
|
|
4098
4340
|
if (!next) break;
|
|
4099
4341
|
current = next;
|
|
4100
4342
|
this.log("Reflection: revised output for better goal alignment.");
|
|
4101
|
-
} catch {
|
|
4102
|
-
break;
|
|
4103
4343
|
}
|
|
4344
|
+
} catch (e) {
|
|
4345
|
+
this.log(`T2-Critic reflection failed: ${e}`);
|
|
4104
4346
|
}
|
|
4105
4347
|
return current;
|
|
4106
4348
|
}
|
|
@@ -4121,7 +4363,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
4121
4363
|
maxTokens: 500,
|
|
4122
4364
|
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
4123
4365
|
|
|
4124
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
|
|
4366
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
4367
|
+
featureTag: assignment.sectionTitle,
|
|
4368
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
4125
4369
|
});
|
|
4126
4370
|
try {
|
|
4127
4371
|
const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
|
|
@@ -4216,6 +4460,7 @@ Begin execution now.`;
|
|
|
4216
4460
|
issues,
|
|
4217
4461
|
peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
|
|
4218
4462
|
correctionAttempts,
|
|
4463
|
+
localOnly: this.localOnlyMatch || void 0,
|
|
4219
4464
|
reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
|
|
4220
4465
|
};
|
|
4221
4466
|
}
|
|
@@ -4507,6 +4752,42 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
4507
4752
|
}
|
|
4508
4753
|
};
|
|
4509
4754
|
|
|
4755
|
+
// src/core/audit/redaction.ts
|
|
4756
|
+
var RedactionLayer = class {
|
|
4757
|
+
// Regexes for common secrets/PII
|
|
4758
|
+
static RULES = [
|
|
4759
|
+
// IPv4 addresses (basic approximation)
|
|
4760
|
+
{ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
|
|
4761
|
+
// Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
|
|
4762
|
+
{ pattern: /(?:\b(?:api_key|apikey|secret|token|password|bearer|auth|authorization)\b[^a-zA-Z0-9_]{1,4})([a-zA-Z0-9_\-\.]{16,})/gi, replacement: "$1[REDACTED_SECRET]" },
|
|
4763
|
+
// Email addresses
|
|
4764
|
+
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
|
|
4765
|
+
// Phone numbers (simplistic)
|
|
4766
|
+
{ pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
|
|
4767
|
+
// AWS Access Key ID
|
|
4768
|
+
{ pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
|
|
4769
|
+
];
|
|
4770
|
+
/**
|
|
4771
|
+
* Applies all redaction rules to the input string.
|
|
4772
|
+
*/
|
|
4773
|
+
static redact(text) {
|
|
4774
|
+
if (!text) return text;
|
|
4775
|
+
let redacted = text;
|
|
4776
|
+
for (const rule of this.RULES) {
|
|
4777
|
+
if (rule.pattern.test(redacted)) {
|
|
4778
|
+
rule.pattern.lastIndex = 0;
|
|
4779
|
+
redacted = redacted.replace(rule.pattern, (match, p1, offset, str) => {
|
|
4780
|
+
if (p1 && match.includes(p1) && p1 !== match) {
|
|
4781
|
+
return match.replace(p1, rule.replacement.replace("$1", ""));
|
|
4782
|
+
}
|
|
4783
|
+
return rule.replacement;
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
return redacted;
|
|
4788
|
+
}
|
|
4789
|
+
};
|
|
4790
|
+
|
|
4510
4791
|
// src/core/tiers/t2-manager.ts
|
|
4511
4792
|
var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
|
|
4512
4793
|
Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
|
|
@@ -4735,16 +5016,19 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4735
5016
|
try {
|
|
4736
5017
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
4737
5018
|
if (!jsonMatch) throw new Error("No JSON array found");
|
|
4738
|
-
|
|
5019
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
5020
|
+
return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
|
|
4739
5021
|
} catch {
|
|
4740
5022
|
return [{
|
|
4741
|
-
subtaskId:
|
|
5023
|
+
subtaskId: crypto3.randomUUID(),
|
|
4742
5024
|
subtaskTitle: assignment.sectionTitle,
|
|
4743
5025
|
description: assignment.description,
|
|
4744
5026
|
expectedOutput: assignment.expectedOutput,
|
|
4745
5027
|
constraints: assignment.constraints,
|
|
4746
5028
|
peerT3Ids: [],
|
|
4747
5029
|
parentT2: this.id,
|
|
5030
|
+
sectionTitle: assignment.sectionTitle,
|
|
5031
|
+
dependsOn: [],
|
|
4748
5032
|
executionMode: "parallel"
|
|
4749
5033
|
}];
|
|
4750
5034
|
}
|
|
@@ -4864,6 +5148,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4864
5148
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
4865
5149
|
const worker = workerMap.get(id);
|
|
4866
5150
|
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
5151
|
+
if (result.localOnly) {
|
|
5152
|
+
result.output = `[local-only path \u2014 output withheld by privacy policy; status: ${result.status}; checks passed: ${result.testResults.passed.length}/${result.testResults.checksRun.length || 0}]`;
|
|
5153
|
+
} else {
|
|
5154
|
+
if (typeof result.output === "string" && result.output) {
|
|
5155
|
+
result.output = RedactionLayer.redact(result.output);
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
|
|
4867
5159
|
resultMap.set(id, result);
|
|
4868
5160
|
return result;
|
|
4869
5161
|
};
|
|
@@ -5302,7 +5594,7 @@ var T1Administrator = class extends BaseTier {
|
|
|
5302
5594
|
}
|
|
5303
5595
|
async execute(userPrompt, images, systemContext, signal) {
|
|
5304
5596
|
this.signal = signal;
|
|
5305
|
-
this.taskId =
|
|
5597
|
+
this.taskId = crypto3.randomUUID();
|
|
5306
5598
|
this.setLabel("Administrator");
|
|
5307
5599
|
this.setStatus("ACTIVE");
|
|
5308
5600
|
this.taskGoal = userPrompt;
|
|
@@ -5493,10 +5785,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
5493
5785
|
}
|
|
5494
5786
|
}
|
|
5495
5787
|
async decomposeTask(prompt, systemContext) {
|
|
5788
|
+
const db = this.router.getWorldStateDB?.();
|
|
5789
|
+
const worldStateContext = db ? `
|
|
5790
|
+
|
|
5791
|
+
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
5792
|
+
${db.getFormattedState()}` : "";
|
|
5496
5793
|
const contextSection = systemContext ? `
|
|
5497
5794
|
Project context:
|
|
5498
5795
|
${systemContext}` : "";
|
|
5499
|
-
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
|
|
5796
|
+
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
|
|
5500
5797
|
|
|
5501
5798
|
Task: ${prompt}
|
|
5502
5799
|
|
|
@@ -5978,16 +6275,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
5978
6275
|
if (typeof input !== "string" || input.length === 0) {
|
|
5979
6276
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
5980
6277
|
}
|
|
5981
|
-
const root =
|
|
5982
|
-
const abs =
|
|
5983
|
-
const rel =
|
|
5984
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
6278
|
+
const root = path20__default.default.resolve(workspaceRoot);
|
|
6279
|
+
const abs = path20__default.default.isAbsolute(input) ? path20__default.default.resolve(input) : path20__default.default.resolve(root, input);
|
|
6280
|
+
const rel = path20__default.default.relative(root, abs);
|
|
6281
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path20__default.default.isAbsolute(rel)) {
|
|
5985
6282
|
throw new WorkspaceSandboxError(input, root);
|
|
5986
6283
|
}
|
|
5987
6284
|
try {
|
|
5988
|
-
const real =
|
|
5989
|
-
const realRel =
|
|
5990
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
6285
|
+
const real = fs19__default.default.realpathSync(abs);
|
|
6286
|
+
const realRel = path20__default.default.relative(root, real);
|
|
6287
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path20__default.default.isAbsolute(realRel))) {
|
|
5991
6288
|
throw new WorkspaceSandboxError(input, root);
|
|
5992
6289
|
}
|
|
5993
6290
|
} catch (e) {
|
|
@@ -6048,7 +6345,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
6048
6345
|
} catch {
|
|
6049
6346
|
}
|
|
6050
6347
|
}
|
|
6051
|
-
await fs4__default.default.mkdir(
|
|
6348
|
+
await fs4__default.default.mkdir(path20__default.default.dirname(absPath), { recursive: true });
|
|
6052
6349
|
await fs4__default.default.writeFile(absPath, content, "utf-8");
|
|
6053
6350
|
return `Written ${content.length} characters to ${filePath}`;
|
|
6054
6351
|
}
|
|
@@ -6546,7 +6843,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
6546
6843
|
};
|
|
6547
6844
|
async function fileToImageAttachment(filePath) {
|
|
6548
6845
|
const data = await fs4__default.default.readFile(filePath);
|
|
6549
|
-
const ext =
|
|
6846
|
+
const ext = path20__default.default.extname(filePath).toLowerCase();
|
|
6550
6847
|
const mimeMap = {
|
|
6551
6848
|
".jpg": "image/jpeg",
|
|
6552
6849
|
".jpeg": "image/jpeg",
|
|
@@ -6580,14 +6877,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
6580
6877
|
const filePath = input["path"];
|
|
6581
6878
|
const content = input["content"];
|
|
6582
6879
|
const title = input["title"];
|
|
6583
|
-
const dir =
|
|
6584
|
-
if (!
|
|
6585
|
-
|
|
6880
|
+
const dir = path20__default.default.dirname(filePath);
|
|
6881
|
+
if (!fs19__default.default.existsSync(dir)) {
|
|
6882
|
+
fs19__default.default.mkdirSync(dir, { recursive: true });
|
|
6586
6883
|
}
|
|
6587
6884
|
return new Promise((resolve, reject) => {
|
|
6588
6885
|
try {
|
|
6589
6886
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
6590
|
-
const stream =
|
|
6887
|
+
const stream = fs19__default.default.createWriteStream(filePath);
|
|
6591
6888
|
doc.pipe(stream);
|
|
6592
6889
|
if (title) {
|
|
6593
6890
|
doc.info["Title"] = title;
|
|
@@ -6665,22 +6962,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
6665
6962
|
}
|
|
6666
6963
|
cmdPrefix = NODE_CMD;
|
|
6667
6964
|
}
|
|
6668
|
-
const tmpDir =
|
|
6669
|
-
if (!
|
|
6670
|
-
|
|
6965
|
+
const tmpDir = path20__default.default.join(this.workspaceRoot, ".cascade", "tmp");
|
|
6966
|
+
if (!fs19__default.default.existsSync(tmpDir)) {
|
|
6967
|
+
fs19__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
6671
6968
|
}
|
|
6672
6969
|
const extension = language === "python" ? "py" : "js";
|
|
6673
|
-
const fileName = `intp_${
|
|
6674
|
-
const filePath =
|
|
6675
|
-
|
|
6970
|
+
const fileName = `intp_${crypto3.randomUUID().slice(0, 8)}.${extension}`;
|
|
6971
|
+
const filePath = path20__default.default.join(tmpDir, fileName);
|
|
6972
|
+
fs19__default.default.writeFileSync(filePath, code, "utf-8");
|
|
6676
6973
|
const execArgs = [filePath, ...args];
|
|
6677
6974
|
return new Promise((resolve) => {
|
|
6678
6975
|
const startMs = Date.now();
|
|
6679
6976
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
6680
6977
|
const duration = Date.now() - startMs;
|
|
6681
6978
|
try {
|
|
6682
|
-
if (
|
|
6683
|
-
|
|
6979
|
+
if (fs19__default.default.existsSync(filePath)) {
|
|
6980
|
+
fs19__default.default.unlinkSync(filePath);
|
|
6684
6981
|
}
|
|
6685
6982
|
} catch (cleanupErr) {
|
|
6686
6983
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -6959,7 +7256,7 @@ var GlobTool = class extends BaseTool {
|
|
|
6959
7256
|
};
|
|
6960
7257
|
async execute(input, _options) {
|
|
6961
7258
|
const pattern = input["pattern"];
|
|
6962
|
-
const searchPath = input["path"] ?
|
|
7259
|
+
const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
6963
7260
|
const matches = await glob.glob(pattern, {
|
|
6964
7261
|
cwd: searchPath,
|
|
6965
7262
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -6972,7 +7269,7 @@ var GlobTool = class extends BaseTool {
|
|
|
6972
7269
|
const withMtime = await Promise.all(
|
|
6973
7270
|
matches.map(async (rel) => {
|
|
6974
7271
|
try {
|
|
6975
|
-
const stat = await fs4__default.default.stat(
|
|
7272
|
+
const stat = await fs4__default.default.stat(path20__default.default.join(searchPath, rel));
|
|
6976
7273
|
return { rel, mtime: stat.mtimeMs };
|
|
6977
7274
|
} catch {
|
|
6978
7275
|
return { rel, mtime: 0 };
|
|
@@ -7021,7 +7318,7 @@ var GrepTool = class extends BaseTool {
|
|
|
7021
7318
|
};
|
|
7022
7319
|
async execute(input, _options) {
|
|
7023
7320
|
const pattern = input["pattern"];
|
|
7024
|
-
const searchPath = input["path"] ?
|
|
7321
|
+
const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
7025
7322
|
const globPattern = input["glob"];
|
|
7026
7323
|
const outputMode = input["output_mode"] ?? "content";
|
|
7027
7324
|
const context = input["context"] ?? 0;
|
|
@@ -7075,12 +7372,12 @@ var GrepTool = class extends BaseTool {
|
|
|
7075
7372
|
nodir: true
|
|
7076
7373
|
});
|
|
7077
7374
|
} catch {
|
|
7078
|
-
files = [
|
|
7375
|
+
files = [path20__default.default.relative(searchPath, searchPath) || "."];
|
|
7079
7376
|
}
|
|
7080
7377
|
const results = [];
|
|
7081
7378
|
let totalCount = 0;
|
|
7082
7379
|
for (const rel of files) {
|
|
7083
|
-
const abs =
|
|
7380
|
+
const abs = path20__default.default.join(searchPath, rel);
|
|
7084
7381
|
let content;
|
|
7085
7382
|
try {
|
|
7086
7383
|
content = await fs4__default.default.readFile(abs, "utf-8");
|
|
@@ -7442,10 +7739,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
7442
7739
|
}
|
|
7443
7740
|
isIgnored(filePath) {
|
|
7444
7741
|
if (!filePath) return false;
|
|
7445
|
-
const abs =
|
|
7446
|
-
const rel =
|
|
7447
|
-
if (!rel || rel.startsWith("..") ||
|
|
7448
|
-
const posixRel = rel.split(
|
|
7742
|
+
const abs = path20__default.default.resolve(this.workspaceRoot, filePath);
|
|
7743
|
+
const rel = path20__default.default.relative(this.workspaceRoot, abs);
|
|
7744
|
+
if (!rel || rel.startsWith("..") || path20__default.default.isAbsolute(rel)) return true;
|
|
7745
|
+
const posixRel = rel.split(path20__default.default.sep).join("/");
|
|
7449
7746
|
return this.ignoreMatcher.ignores(posixRel);
|
|
7450
7747
|
}
|
|
7451
7748
|
};
|
|
@@ -7562,6 +7859,9 @@ var McpClient = class _McpClient {
|
|
|
7562
7859
|
return this.clients.has(serverName);
|
|
7563
7860
|
}
|
|
7564
7861
|
};
|
|
7862
|
+
|
|
7863
|
+
// src/core/cascade.ts
|
|
7864
|
+
init_audit_logger();
|
|
7565
7865
|
var SAFE_TOOLS = /* @__PURE__ */ new Set([
|
|
7566
7866
|
"file_read",
|
|
7567
7867
|
"file_list",
|
|
@@ -7814,7 +8114,8 @@ var WorkspaceConfigSchema = zod.z.object({
|
|
|
7814
8114
|
cascadeMdPath: zod.z.string().default("CASCADE.md"),
|
|
7815
8115
|
configPath: zod.z.string().default(".cascade/config.json"),
|
|
7816
8116
|
keystorePath: zod.z.string().default(".cascade/keystore.enc"),
|
|
7817
|
-
auditLogPath: zod.z.string().default(".cascade/audit.log")
|
|
8117
|
+
auditLogPath: zod.z.string().default(".cascade/audit.log"),
|
|
8118
|
+
debugWorldState: zod.z.boolean().default(false)
|
|
7818
8119
|
});
|
|
7819
8120
|
var CascadeConfigSchema = zod.z.object({
|
|
7820
8121
|
version: zod.z.literal("1.0").default("1.0"),
|
|
@@ -7959,6 +8260,14 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
7959
8260
|
* 'parallel' / 'sequential' — force it.
|
|
7960
8261
|
*/
|
|
7961
8262
|
t3Execution: zod.z.enum(["auto", "parallel", "sequential"]).default("auto"),
|
|
8263
|
+
/**
|
|
8264
|
+
* Per-path privacy tiers. A subtask touching a `local-only` path is forced
|
|
8265
|
+
* onto LOCAL models (never cloud) and its raw output is withheld from the
|
|
8266
|
+
* tiers above. Patterns use .gitignore syntax, like .cascadeignore.
|
|
8267
|
+
*/
|
|
8268
|
+
privacy: zod.z.object({
|
|
8269
|
+
paths: zod.z.array(zod.z.object({ pattern: zod.z.string().min(1), policy: zod.z.enum(["local-only"]) })).default([])
|
|
8270
|
+
}).optional(),
|
|
7962
8271
|
/**
|
|
7963
8272
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
7964
8273
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -8227,9 +8536,10 @@ var TaskAnalyzer = class {
|
|
|
8227
8536
|
analysisCache.clear();
|
|
8228
8537
|
}
|
|
8229
8538
|
};
|
|
8230
|
-
var DEFAULT_STATS_FILE =
|
|
8539
|
+
var DEFAULT_STATS_FILE = path20__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
|
|
8231
8540
|
var ModelPerformanceTracker = class {
|
|
8232
8541
|
stats = /* @__PURE__ */ new Map();
|
|
8542
|
+
featureStats = /* @__PURE__ */ new Map();
|
|
8233
8543
|
statsFile;
|
|
8234
8544
|
loaded = false;
|
|
8235
8545
|
constructor(statsFile = DEFAULT_STATS_FILE) {
|
|
@@ -8241,18 +8551,29 @@ var ModelPerformanceTracker = class {
|
|
|
8241
8551
|
try {
|
|
8242
8552
|
const raw = await fs4__default.default.readFile(this.statsFile, "utf-8");
|
|
8243
8553
|
const parsed = JSON.parse(raw);
|
|
8244
|
-
|
|
8245
|
-
this.stats.set(key, stat);
|
|
8554
|
+
if (parsed.models) {
|
|
8555
|
+
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
8556
|
+
} else {
|
|
8557
|
+
for (const [key, stat] of Object.entries(parsed)) {
|
|
8558
|
+
if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
|
|
8559
|
+
this.stats.set(key, stat);
|
|
8560
|
+
}
|
|
8561
|
+
}
|
|
8562
|
+
}
|
|
8563
|
+
if (parsed.features) {
|
|
8564
|
+
for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
|
|
8246
8565
|
}
|
|
8247
8566
|
} catch {
|
|
8248
8567
|
}
|
|
8249
8568
|
}
|
|
8250
8569
|
async save() {
|
|
8251
8570
|
try {
|
|
8252
|
-
await fs4__default.default.mkdir(
|
|
8253
|
-
const
|
|
8254
|
-
|
|
8255
|
-
|
|
8571
|
+
await fs4__default.default.mkdir(path20__default.default.dirname(this.statsFile), { recursive: true });
|
|
8572
|
+
const modelsObj = {};
|
|
8573
|
+
const featuresObj = {};
|
|
8574
|
+
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
8575
|
+
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
8576
|
+
await fs4__default.default.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
|
|
8256
8577
|
} catch {
|
|
8257
8578
|
}
|
|
8258
8579
|
}
|
|
@@ -8273,6 +8594,13 @@ var ModelPerformanceTracker = class {
|
|
|
8273
8594
|
sampleCount: s.sampleCount + 1
|
|
8274
8595
|
});
|
|
8275
8596
|
}
|
|
8597
|
+
recordFeatureCost(featureTag, costUsd) {
|
|
8598
|
+
const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
|
|
8599
|
+
this.featureStats.set(featureTag, {
|
|
8600
|
+
totalCostUsd: s.totalCostUsd + costUsd,
|
|
8601
|
+
runCount: s.runCount + 1
|
|
8602
|
+
});
|
|
8603
|
+
}
|
|
8276
8604
|
/**
|
|
8277
8605
|
* Record an explicit user rating (good/bad). Counts as 3 automatic samples
|
|
8278
8606
|
* so user feedback carries significantly more weight than auto-detected outcomes.
|
|
@@ -8287,6 +8615,9 @@ var ModelPerformanceTracker = class {
|
|
|
8287
8615
|
getAll() {
|
|
8288
8616
|
return new Map(this.stats);
|
|
8289
8617
|
}
|
|
8618
|
+
getAllFeatures() {
|
|
8619
|
+
return new Map(this.featureStats);
|
|
8620
|
+
}
|
|
8290
8621
|
/**
|
|
8291
8622
|
* Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
|
|
8292
8623
|
* High retry counts penalise the score.
|
|
@@ -8651,7 +8982,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8651
8982
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
8652
8983
|
async loadPersistedTools() {
|
|
8653
8984
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
8654
|
-
const file =
|
|
8985
|
+
const file = path20__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
8655
8986
|
try {
|
|
8656
8987
|
const raw = await fs4__default.default.readFile(file, "utf-8");
|
|
8657
8988
|
const specs = JSON.parse(raw);
|
|
@@ -8675,8 +9006,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8675
9006
|
}
|
|
8676
9007
|
async persist() {
|
|
8677
9008
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
8678
|
-
const dir =
|
|
8679
|
-
const file =
|
|
9009
|
+
const dir = path20__default.default.join(this.workspacePath, ".cascade");
|
|
9010
|
+
const file = path20__default.default.join(dir, DYNAMIC_TOOLS_FILE);
|
|
8680
9011
|
try {
|
|
8681
9012
|
await fs4__default.default.mkdir(dir, { recursive: true });
|
|
8682
9013
|
await fs4__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -8689,6 +9020,166 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
8689
9020
|
return Array.from(this.specs.keys());
|
|
8690
9021
|
}
|
|
8691
9022
|
};
|
|
9023
|
+
var WorldStateDB = class {
|
|
9024
|
+
constructor(workspacePath, debugMode = false) {
|
|
9025
|
+
this.workspacePath = workspacePath;
|
|
9026
|
+
this.debugMode = debugMode;
|
|
9027
|
+
const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
|
|
9028
|
+
if (!fs19__default.default.existsSync(cascadeDir)) {
|
|
9029
|
+
fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
9030
|
+
}
|
|
9031
|
+
this.keyPath = path20__default.default.join(cascadeDir, "world_state.key");
|
|
9032
|
+
this.dbPath = path20__default.default.join(cascadeDir, "world_state.db");
|
|
9033
|
+
this.initEncryptionKey();
|
|
9034
|
+
this.db = new Database__default.default(this.dbPath);
|
|
9035
|
+
this.db.pragma("journal_mode = WAL");
|
|
9036
|
+
this.db.exec(`
|
|
9037
|
+
CREATE TABLE IF NOT EXISTS world_state (
|
|
9038
|
+
id TEXT PRIMARY KEY,
|
|
9039
|
+
timestamp TEXT NOT NULL,
|
|
9040
|
+
worker_id TEXT NOT NULL,
|
|
9041
|
+
encrypted_payload TEXT NOT NULL
|
|
9042
|
+
)
|
|
9043
|
+
`);
|
|
9044
|
+
}
|
|
9045
|
+
workspacePath;
|
|
9046
|
+
debugMode;
|
|
9047
|
+
db;
|
|
9048
|
+
keyPath;
|
|
9049
|
+
dbPath;
|
|
9050
|
+
encryptionKey;
|
|
9051
|
+
initEncryptionKey() {
|
|
9052
|
+
if (fs19__default.default.existsSync(this.keyPath)) {
|
|
9053
|
+
this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
|
|
9054
|
+
} else {
|
|
9055
|
+
this.encryptionKey = crypto3__default.default.randomBytes(32);
|
|
9056
|
+
fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
9057
|
+
}
|
|
9058
|
+
}
|
|
9059
|
+
encrypt(text) {
|
|
9060
|
+
const iv = crypto3__default.default.randomBytes(16);
|
|
9061
|
+
const cipher = crypto3__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
9062
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
9063
|
+
encrypted += cipher.final("hex");
|
|
9064
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
9065
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
9066
|
+
}
|
|
9067
|
+
decrypt(text) {
|
|
9068
|
+
const parts = text.split(":");
|
|
9069
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
9070
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
9071
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
9072
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
9073
|
+
const decipher = crypto3__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
9074
|
+
decipher.setAuthTag(authTag);
|
|
9075
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
9076
|
+
decrypted += decipher.final("utf8");
|
|
9077
|
+
return decrypted;
|
|
9078
|
+
}
|
|
9079
|
+
addEntry(workerId, summary) {
|
|
9080
|
+
const id = crypto3__default.default.randomUUID();
|
|
9081
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
9082
|
+
const payload = JSON.stringify({ summary });
|
|
9083
|
+
const encryptedPayload = this.encrypt(payload);
|
|
9084
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
9085
|
+
stmt.run(id, timestamp, workerId, encryptedPayload);
|
|
9086
|
+
this.dumpDebugIfNeeded();
|
|
9087
|
+
return id;
|
|
9088
|
+
}
|
|
9089
|
+
getAllEntries() {
|
|
9090
|
+
const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
|
|
9091
|
+
const rows = stmt.all();
|
|
9092
|
+
return rows.map((row) => {
|
|
9093
|
+
try {
|
|
9094
|
+
const decrypted = this.decrypt(row.encrypted_payload);
|
|
9095
|
+
const parsed = JSON.parse(decrypted);
|
|
9096
|
+
return {
|
|
9097
|
+
id: row.id,
|
|
9098
|
+
timestamp: row.timestamp,
|
|
9099
|
+
workerId: row.worker_id,
|
|
9100
|
+
summary: parsed.summary
|
|
9101
|
+
};
|
|
9102
|
+
} catch (err) {
|
|
9103
|
+
return {
|
|
9104
|
+
id: row.id,
|
|
9105
|
+
timestamp: row.timestamp,
|
|
9106
|
+
workerId: row.worker_id,
|
|
9107
|
+
summary: "[Decryption Failed - Payload Corrupted]"
|
|
9108
|
+
};
|
|
9109
|
+
}
|
|
9110
|
+
});
|
|
9111
|
+
}
|
|
9112
|
+
getFormattedState() {
|
|
9113
|
+
const entries = this.getAllEntries();
|
|
9114
|
+
if (entries.length === 0) return "World State is currently empty.";
|
|
9115
|
+
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
9116
|
+
}
|
|
9117
|
+
dumpDebugIfNeeded() {
|
|
9118
|
+
if (!this.debugMode) return;
|
|
9119
|
+
try {
|
|
9120
|
+
const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
|
|
9121
|
+
const entries = this.getAllEntries();
|
|
9122
|
+
fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
9123
|
+
} catch (err) {
|
|
9124
|
+
console.error("Failed to dump debug world state", err);
|
|
9125
|
+
}
|
|
9126
|
+
}
|
|
9127
|
+
close() {
|
|
9128
|
+
this.db.close();
|
|
9129
|
+
}
|
|
9130
|
+
};
|
|
9131
|
+
var ignore2 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
|
|
9132
|
+
var PrivacyPaths = class {
|
|
9133
|
+
localOnly;
|
|
9134
|
+
hasRules;
|
|
9135
|
+
constructor(policies = []) {
|
|
9136
|
+
this.localOnly = ignore2();
|
|
9137
|
+
const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
|
|
9138
|
+
if (patterns.length) this.localOnly.add(patterns);
|
|
9139
|
+
this.hasRules = patterns.length > 0;
|
|
9140
|
+
}
|
|
9141
|
+
/** True when any privacy rules are configured at all (cheap short-circuit). */
|
|
9142
|
+
hasPolicies() {
|
|
9143
|
+
return this.hasRules;
|
|
9144
|
+
}
|
|
9145
|
+
/** True when the given workspace-relative path falls under a local-only policy. */
|
|
9146
|
+
isLocalOnly(relativePath) {
|
|
9147
|
+
if (!this.hasRules || !relativePath) return false;
|
|
9148
|
+
try {
|
|
9149
|
+
return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
|
|
9150
|
+
} catch {
|
|
9151
|
+
return false;
|
|
9152
|
+
}
|
|
9153
|
+
}
|
|
9154
|
+
/** True when ANY of the given paths falls under a local-only policy. */
|
|
9155
|
+
anyLocalOnly(relativePaths) {
|
|
9156
|
+
return relativePaths.some((p) => this.isLocalOnly(p));
|
|
9157
|
+
}
|
|
9158
|
+
};
|
|
9159
|
+
|
|
9160
|
+
// src/core/steering/guidance.ts
|
|
9161
|
+
var GuidanceQueue = class {
|
|
9162
|
+
entries = [];
|
|
9163
|
+
/** Per-consumer read cursor so a broadcast entry reaches each worker once. */
|
|
9164
|
+
cursors = /* @__PURE__ */ new Map();
|
|
9165
|
+
push(text, nodeId) {
|
|
9166
|
+
const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9167
|
+
this.entries.push(entry);
|
|
9168
|
+
return entry;
|
|
9169
|
+
}
|
|
9170
|
+
/**
|
|
9171
|
+
* New entries for this consumer since its last drain, filtered to entries
|
|
9172
|
+
* that target it (or target everyone). Advances the consumer's cursor.
|
|
9173
|
+
*/
|
|
9174
|
+
drain(consumerId) {
|
|
9175
|
+
const from = this.cursors.get(consumerId) ?? 0;
|
|
9176
|
+
this.cursors.set(consumerId, this.entries.length);
|
|
9177
|
+
return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
|
|
9178
|
+
}
|
|
9179
|
+
get size() {
|
|
9180
|
+
return this.entries.length;
|
|
9181
|
+
}
|
|
9182
|
+
};
|
|
8692
9183
|
|
|
8693
9184
|
// src/core/cascade.ts
|
|
8694
9185
|
var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
@@ -8708,6 +9199,9 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
8708
9199
|
taskAnalyzer;
|
|
8709
9200
|
perfTracker;
|
|
8710
9201
|
toolCreator;
|
|
9202
|
+
worldStateDB;
|
|
9203
|
+
encryptedAuditLogger;
|
|
9204
|
+
guidanceQueue;
|
|
8711
9205
|
workspacePath;
|
|
8712
9206
|
constructor(config, workspacePath, store) {
|
|
8713
9207
|
super();
|
|
@@ -8729,6 +9223,23 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
8729
9223
|
});
|
|
8730
9224
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
8731
9225
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
9226
|
+
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
9227
|
+
this.router.setWorldStateDB(this.worldStateDB);
|
|
9228
|
+
this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
9229
|
+
const privacyPolicies = this.config.privacy?.paths ?? [];
|
|
9230
|
+
if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
|
|
9231
|
+
this.guidanceQueue = new GuidanceQueue();
|
|
9232
|
+
this.router.setGuidanceQueue(this.guidanceQueue);
|
|
9233
|
+
}
|
|
9234
|
+
/**
|
|
9235
|
+
* Live intervention: inject a user correction into the running hierarchy.
|
|
9236
|
+
* Every active T3 worker (or only the one matching `nodeId`) picks it up at
|
|
9237
|
+
* the top of its next agent-loop iteration as a USER INTERVENTION message.
|
|
9238
|
+
*/
|
|
9239
|
+
injectGuidance(text, nodeId) {
|
|
9240
|
+
const entry = this.guidanceQueue.push(text, nodeId);
|
|
9241
|
+
this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
|
|
9242
|
+
this.emit("guidance:injected", entry);
|
|
8732
9243
|
}
|
|
8733
9244
|
initOptionalFeatures() {
|
|
8734
9245
|
if (this.config.cascadeAuto === true) {
|
|
@@ -8891,6 +9402,12 @@ ${last.partialOutput}` : "");
|
|
|
8891
9402
|
if (!prompt) return null;
|
|
8892
9403
|
return this.run({ prompt });
|
|
8893
9404
|
}
|
|
9405
|
+
getWorkspacePath() {
|
|
9406
|
+
return this.workspacePath;
|
|
9407
|
+
}
|
|
9408
|
+
getWorldStateDB() {
|
|
9409
|
+
return this.worldStateDB;
|
|
9410
|
+
}
|
|
8894
9411
|
/**
|
|
8895
9412
|
* Record an explicit user rating for the last completed run.
|
|
8896
9413
|
* Explicit ratings carry 3× the weight of auto-detected outcomes so user
|
|
@@ -8977,6 +9494,11 @@ ${last.partialOutput}` : "");
|
|
|
8977
9494
|
}
|
|
8978
9495
|
this.initOptionalFeatures();
|
|
8979
9496
|
if (this.toolCreator) await this.toolCreator.loadPersistedTools();
|
|
9497
|
+
if (this.encryptedAuditLogger) {
|
|
9498
|
+
this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
|
|
9499
|
+
this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
|
|
9500
|
+
this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
|
|
9501
|
+
}
|
|
8980
9502
|
this.initialized = true;
|
|
8981
9503
|
})();
|
|
8982
9504
|
try {
|
|
@@ -9124,7 +9646,7 @@ ${prompt}` : prompt;
|
|
|
9124
9646
|
this.router.beginRun();
|
|
9125
9647
|
this.router.setRunSignal(options.signal);
|
|
9126
9648
|
const startMs = Date.now();
|
|
9127
|
-
const taskId =
|
|
9649
|
+
const taskId = crypto3.randomUUID();
|
|
9128
9650
|
this.decisionLog = [];
|
|
9129
9651
|
const escalator = new PermissionEscalator(this.config.approvalTimeoutMs ?? 6e5, this.config.autonomy === "auto");
|
|
9130
9652
|
escalator.on("permission:user-required", async (req) => {
|
|
@@ -9402,6 +9924,7 @@ ${prompt}` : prompt;
|
|
|
9402
9924
|
durationMs,
|
|
9403
9925
|
costByTier: stats.costByTier,
|
|
9404
9926
|
tokensByTier: stats.tokensByTier,
|
|
9927
|
+
costByFeature: stats.costByFeature,
|
|
9405
9928
|
costPercentByTier: this.router.getTierCostPercentages()
|
|
9406
9929
|
};
|
|
9407
9930
|
}
|
|
@@ -9469,7 +9992,7 @@ var Keystore = class {
|
|
|
9469
9992
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
9470
9993
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
9471
9994
|
this.backend = "keytar";
|
|
9472
|
-
if (password &&
|
|
9995
|
+
if (password && fs19__default.default.existsSync(this.storePath)) {
|
|
9473
9996
|
try {
|
|
9474
9997
|
const fileEntries = this.decryptFile(password);
|
|
9475
9998
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -9488,8 +10011,8 @@ var Keystore = class {
|
|
|
9488
10011
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
9489
10012
|
);
|
|
9490
10013
|
}
|
|
9491
|
-
if (!
|
|
9492
|
-
const salt =
|
|
10014
|
+
if (!fs19__default.default.existsSync(this.storePath)) {
|
|
10015
|
+
const salt = crypto3__default.default.randomBytes(SALT_LEN);
|
|
9493
10016
|
this.masterKey = this.deriveKey(password, salt);
|
|
9494
10017
|
this.writeWithSalt({}, salt);
|
|
9495
10018
|
this.cache = {};
|
|
@@ -9502,8 +10025,8 @@ var Keystore = class {
|
|
|
9502
10025
|
}
|
|
9503
10026
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
9504
10027
|
unlockSync(password) {
|
|
9505
|
-
if (!
|
|
9506
|
-
const salt =
|
|
10028
|
+
if (!fs19__default.default.existsSync(this.storePath)) {
|
|
10029
|
+
const salt = crypto3__default.default.randomBytes(SALT_LEN);
|
|
9507
10030
|
this.masterKey = this.deriveKey(password, salt);
|
|
9508
10031
|
this.writeWithSalt({}, salt);
|
|
9509
10032
|
this.cache = {};
|
|
@@ -9560,12 +10083,12 @@ var Keystore = class {
|
|
|
9560
10083
|
}
|
|
9561
10084
|
}
|
|
9562
10085
|
decryptFile(password, knownSalt) {
|
|
9563
|
-
if (!
|
|
10086
|
+
if (!fs19__default.default.existsSync(this.storePath)) return {};
|
|
9564
10087
|
try {
|
|
9565
10088
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
9566
10089
|
const useSalt = knownSalt ?? salt;
|
|
9567
10090
|
const key = this.masterKey ?? this.deriveKey(password, useSalt);
|
|
9568
|
-
const decipher =
|
|
10091
|
+
const decipher = crypto3__default.default.createDecipheriv(ALGORITHM, key, iv);
|
|
9569
10092
|
decipher.setAuthTag(tag);
|
|
9570
10093
|
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
9571
10094
|
return JSON.parse(decrypted.toString("utf-8"));
|
|
@@ -9576,28 +10099,28 @@ var Keystore = class {
|
|
|
9576
10099
|
saveAll(data) {
|
|
9577
10100
|
if (!this.masterKey) return;
|
|
9578
10101
|
const raw = this.readRaw();
|
|
9579
|
-
const iv =
|
|
9580
|
-
const cipher =
|
|
10102
|
+
const iv = crypto3__default.default.randomBytes(IV_LEN);
|
|
10103
|
+
const cipher = crypto3__default.default.createCipheriv(ALGORITHM, this.masterKey, iv);
|
|
9581
10104
|
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
9582
10105
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
9583
10106
|
const tag = cipher.getAuthTag();
|
|
9584
10107
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
9585
|
-
|
|
9586
|
-
|
|
10108
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
|
|
10109
|
+
fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
9587
10110
|
}
|
|
9588
10111
|
writeWithSalt(data, salt) {
|
|
9589
10112
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
9590
|
-
const iv =
|
|
9591
|
-
const cipher =
|
|
10113
|
+
const iv = crypto3__default.default.randomBytes(IV_LEN);
|
|
10114
|
+
const cipher = crypto3__default.default.createCipheriv(ALGORITHM, this.masterKey, iv);
|
|
9592
10115
|
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
9593
10116
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
9594
10117
|
const tag = cipher.getAuthTag();
|
|
9595
10118
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
9596
|
-
|
|
9597
|
-
|
|
10119
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
|
|
10120
|
+
fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
9598
10121
|
}
|
|
9599
10122
|
readRaw() {
|
|
9600
|
-
const buf =
|
|
10123
|
+
const buf = fs19__default.default.readFileSync(this.storePath);
|
|
9601
10124
|
let offset = 0;
|
|
9602
10125
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
9603
10126
|
offset += SALT_LEN;
|
|
@@ -9609,15 +10132,15 @@ var Keystore = class {
|
|
|
9609
10132
|
return { salt, iv, tag, ciphertext };
|
|
9610
10133
|
}
|
|
9611
10134
|
deriveKey(password, salt) {
|
|
9612
|
-
return
|
|
10135
|
+
return crypto3__default.default.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LEN, "sha256");
|
|
9613
10136
|
}
|
|
9614
10137
|
};
|
|
9615
|
-
var
|
|
10138
|
+
var ignore3 = ignoreFactory__namespace.default ?? ignoreFactory__namespace;
|
|
9616
10139
|
var CascadeIgnore = class {
|
|
9617
10140
|
ig;
|
|
9618
10141
|
loaded = false;
|
|
9619
10142
|
constructor() {
|
|
9620
|
-
this.ig =
|
|
10143
|
+
this.ig = ignore3();
|
|
9621
10144
|
this.ig.add([
|
|
9622
10145
|
".cascade/keystore.enc",
|
|
9623
10146
|
".cascade/memory.db",
|
|
@@ -9630,7 +10153,7 @@ var CascadeIgnore = class {
|
|
|
9630
10153
|
]);
|
|
9631
10154
|
}
|
|
9632
10155
|
async load(workspacePath) {
|
|
9633
|
-
const filePath =
|
|
10156
|
+
const filePath = path20__default.default.join(workspacePath, ".cascadeignore");
|
|
9634
10157
|
try {
|
|
9635
10158
|
const content = await fs4__default.default.readFile(filePath, "utf-8");
|
|
9636
10159
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -9641,7 +10164,7 @@ var CascadeIgnore = class {
|
|
|
9641
10164
|
}
|
|
9642
10165
|
isIgnored(filePath, workspacePath) {
|
|
9643
10166
|
try {
|
|
9644
|
-
const relative = workspacePath ?
|
|
10167
|
+
const relative = workspacePath ? path20__default.default.relative(workspacePath, filePath) : filePath;
|
|
9645
10168
|
return this.ig.ignores(relative);
|
|
9646
10169
|
} catch {
|
|
9647
10170
|
return false;
|
|
@@ -9652,7 +10175,7 @@ var CascadeIgnore = class {
|
|
|
9652
10175
|
}
|
|
9653
10176
|
};
|
|
9654
10177
|
async function loadCascadeMd(workspacePath) {
|
|
9655
|
-
const filePath =
|
|
10178
|
+
const filePath = path20__default.default.join(workspacePath, "CASCADE.md");
|
|
9656
10179
|
try {
|
|
9657
10180
|
const raw = await fs4__default.default.readFile(filePath, "utf-8");
|
|
9658
10181
|
return parseCascadeMd(raw);
|
|
@@ -9683,7 +10206,7 @@ ${raw.trim()}`;
|
|
|
9683
10206
|
var MemoryStore = class _MemoryStore {
|
|
9684
10207
|
db;
|
|
9685
10208
|
constructor(dbPath) {
|
|
9686
|
-
|
|
10209
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(dbPath), { recursive: true });
|
|
9687
10210
|
try {
|
|
9688
10211
|
this.db = new Database__default.default(dbPath, { timeout: 5e3 });
|
|
9689
10212
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -9807,7 +10330,7 @@ Original error: ${err.message}`
|
|
|
9807
10330
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
9808
10331
|
`);
|
|
9809
10332
|
for (const msg of messages) {
|
|
9810
|
-
stmt.run(
|
|
10333
|
+
stmt.run(crypto3.randomUUID(), newId, msg.role, msg.content, msg.timestamp, msg.tokens, msg.agent_messages);
|
|
9811
10334
|
}
|
|
9812
10335
|
const snapshots = this.db.prepare("SELECT * FROM file_snapshots WHERE session_id = ?").all(originalId);
|
|
9813
10336
|
const snapStmt = this.db.prepare(`
|
|
@@ -9815,7 +10338,7 @@ Original error: ${err.message}`
|
|
|
9815
10338
|
VALUES (?, ?, ?, ?, ?)
|
|
9816
10339
|
`);
|
|
9817
10340
|
for (const snap of snapshots) {
|
|
9818
|
-
snapStmt.run(
|
|
10341
|
+
snapStmt.run(crypto3.randomUUID(), newId, snap.file_path, snap.content, snap.timestamp);
|
|
9819
10342
|
}
|
|
9820
10343
|
}
|
|
9821
10344
|
// ── Runtime Sessions / Nodes ─────────────────
|
|
@@ -10113,7 +10636,7 @@ Original error: ${err.message}`
|
|
|
10113
10636
|
this.db.prepare(`
|
|
10114
10637
|
INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
|
|
10115
10638
|
VALUES (?, ?, ?, ?, ?)
|
|
10116
|
-
`).run(
|
|
10639
|
+
`).run(crypto3.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
|
|
10117
10640
|
});
|
|
10118
10641
|
}
|
|
10119
10642
|
getLatestFileSnapshots(sessionId) {
|
|
@@ -10441,15 +10964,15 @@ var ConfigManager = class {
|
|
|
10441
10964
|
globalDir;
|
|
10442
10965
|
constructor(workspacePath = process.cwd()) {
|
|
10443
10966
|
this.workspacePath = workspacePath;
|
|
10444
|
-
this.globalDir =
|
|
10967
|
+
this.globalDir = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
10445
10968
|
}
|
|
10446
10969
|
async load() {
|
|
10447
10970
|
this.config = await this.loadConfig();
|
|
10448
10971
|
this.ignore = new CascadeIgnore();
|
|
10449
10972
|
await this.ignore.load(this.workspacePath);
|
|
10450
10973
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
10451
|
-
this.keystore = new Keystore(
|
|
10452
|
-
this.store = new MemoryStore(
|
|
10974
|
+
this.keystore = new Keystore(path20__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
10975
|
+
this.store = new MemoryStore(path20__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
10453
10976
|
await this.injectEnvKeys();
|
|
10454
10977
|
await this.ensureDefaultIdentity();
|
|
10455
10978
|
}
|
|
@@ -10472,8 +10995,8 @@ var ConfigManager = class {
|
|
|
10472
10995
|
return this.workspacePath;
|
|
10473
10996
|
}
|
|
10474
10997
|
async save() {
|
|
10475
|
-
const configPath =
|
|
10476
|
-
await fs4__default.default.mkdir(
|
|
10998
|
+
const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
10999
|
+
await fs4__default.default.mkdir(path20__default.default.dirname(configPath), { recursive: true });
|
|
10477
11000
|
await fs4__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
10478
11001
|
}
|
|
10479
11002
|
async updateConfig(updates) {
|
|
@@ -10497,7 +11020,7 @@ var ConfigManager = class {
|
|
|
10497
11020
|
return configProvider?.apiKey;
|
|
10498
11021
|
}
|
|
10499
11022
|
async loadConfig() {
|
|
10500
|
-
const configPath =
|
|
11023
|
+
const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
10501
11024
|
try {
|
|
10502
11025
|
const raw = await fs4__default.default.readFile(configPath, "utf-8");
|
|
10503
11026
|
return validateConfig(JSON.parse(raw));
|
|
@@ -10534,7 +11057,7 @@ var ConfigManager = class {
|
|
|
10534
11057
|
const existing = this.store.getDefaultIdentity();
|
|
10535
11058
|
if (existing) return;
|
|
10536
11059
|
const identity = {
|
|
10537
|
-
id:
|
|
11060
|
+
id: crypto3.randomUUID(),
|
|
10538
11061
|
name: "Default",
|
|
10539
11062
|
description: "Default Cascade identity",
|
|
10540
11063
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -10727,6 +11250,24 @@ var DashboardSocket = class {
|
|
|
10727
11250
|
});
|
|
10728
11251
|
});
|
|
10729
11252
|
}
|
|
11253
|
+
onSessionHalt(callback) {
|
|
11254
|
+
this.io.on("connection", (socket) => {
|
|
11255
|
+
socket.on("session:halt", (payload) => {
|
|
11256
|
+
if (typeof payload?.sessionId === "string") {
|
|
11257
|
+
callback(payload.sessionId);
|
|
11258
|
+
}
|
|
11259
|
+
});
|
|
11260
|
+
});
|
|
11261
|
+
}
|
|
11262
|
+
onSessionSteer(callback) {
|
|
11263
|
+
this.io.on("connection", (socket) => {
|
|
11264
|
+
socket.on("session:steer", (payload) => {
|
|
11265
|
+
if (typeof payload?.message === "string" && payload.message.trim()) {
|
|
11266
|
+
callback(payload.message.trim(), payload.sessionId, payload.nodeId);
|
|
11267
|
+
}
|
|
11268
|
+
});
|
|
11269
|
+
});
|
|
11270
|
+
}
|
|
10730
11271
|
onConfigUpdate(callback) {
|
|
10731
11272
|
this.io.on("connection", (socket) => {
|
|
10732
11273
|
socket.on("config:update", (payload) => {
|
|
@@ -10758,7 +11299,7 @@ var DashboardSocket = class {
|
|
|
10758
11299
|
this.io.close();
|
|
10759
11300
|
}
|
|
10760
11301
|
};
|
|
10761
|
-
var __dirname$1 =
|
|
11302
|
+
var __dirname$1 = path20__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
10762
11303
|
var DashboardServer = class {
|
|
10763
11304
|
app;
|
|
10764
11305
|
httpServer;
|
|
@@ -10769,6 +11310,13 @@ var DashboardServer = class {
|
|
|
10769
11310
|
globalStore = null;
|
|
10770
11311
|
broadcastTimer = null;
|
|
10771
11312
|
activeSessions = /* @__PURE__ */ new Map();
|
|
11313
|
+
activeControllers = /* @__PURE__ */ new Map();
|
|
11314
|
+
/**
|
|
11315
|
+
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
11316
|
+
* taskId (see t3-worker saveSnapshot), so session rollback needs the list
|
|
11317
|
+
* of runs the session performed in this server's lifetime.
|
|
11318
|
+
*/
|
|
11319
|
+
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
10772
11320
|
port;
|
|
10773
11321
|
host;
|
|
10774
11322
|
workspacePath;
|
|
@@ -10828,7 +11376,9 @@ var DashboardServer = class {
|
|
|
10828
11376
|
this.persistConfig();
|
|
10829
11377
|
});
|
|
10830
11378
|
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
10831
|
-
const sessionId = requestedSessionId ??
|
|
11379
|
+
const sessionId = requestedSessionId ?? crypto3.randomUUID();
|
|
11380
|
+
const abortController = new AbortController();
|
|
11381
|
+
this.activeControllers.set(sessionId, abortController);
|
|
10832
11382
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
10833
11383
|
const title = this.persistRunStart(sessionId, prompt);
|
|
10834
11384
|
const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
@@ -10847,7 +11397,8 @@ var DashboardServer = class {
|
|
|
10847
11397
|
this.socket.emitPeerMessage(e);
|
|
10848
11398
|
});
|
|
10849
11399
|
try {
|
|
10850
|
-
const result = await cascade.run({ prompt: runPrompt });
|
|
11400
|
+
const result = await cascade.run({ prompt: runPrompt, signal: abortController.signal });
|
|
11401
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
10851
11402
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
10852
11403
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
10853
11404
|
this.socket.broadcast("cost:update", {
|
|
@@ -10864,6 +11415,16 @@ var DashboardServer = class {
|
|
|
10864
11415
|
});
|
|
10865
11416
|
} finally {
|
|
10866
11417
|
this.activeSessions.delete(sessionId);
|
|
11418
|
+
this.activeControllers.delete(sessionId);
|
|
11419
|
+
}
|
|
11420
|
+
});
|
|
11421
|
+
this.socket.onSessionHalt((sessionId) => {
|
|
11422
|
+
this.activeControllers.get(sessionId)?.abort();
|
|
11423
|
+
});
|
|
11424
|
+
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
11425
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
11426
|
+
if (steered > 0) {
|
|
11427
|
+
this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
10867
11428
|
}
|
|
10868
11429
|
});
|
|
10869
11430
|
}
|
|
@@ -10911,9 +11472,9 @@ var DashboardServer = class {
|
|
|
10911
11472
|
*/
|
|
10912
11473
|
persistConfig() {
|
|
10913
11474
|
try {
|
|
10914
|
-
const configPath =
|
|
10915
|
-
|
|
10916
|
-
|
|
11475
|
+
const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11476
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(configPath), { recursive: true });
|
|
11477
|
+
fs19__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
10917
11478
|
} catch (err) {
|
|
10918
11479
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
10919
11480
|
}
|
|
@@ -10928,15 +11489,15 @@ var DashboardServer = class {
|
|
|
10928
11489
|
resolveDashboardSecret() {
|
|
10929
11490
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
10930
11491
|
if (fromConfig) return fromConfig;
|
|
10931
|
-
const secretPath =
|
|
11492
|
+
const secretPath = path20__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
10932
11493
|
try {
|
|
10933
|
-
if (
|
|
10934
|
-
const existing =
|
|
11494
|
+
if (fs19__default.default.existsSync(secretPath)) {
|
|
11495
|
+
const existing = fs19__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
10935
11496
|
if (existing.length >= 16) return existing;
|
|
10936
11497
|
}
|
|
10937
|
-
const generated =
|
|
10938
|
-
|
|
10939
|
-
|
|
11498
|
+
const generated = crypto3.randomUUID();
|
|
11499
|
+
fs19__default.default.mkdirSync(path20__default.default.dirname(secretPath), { recursive: true });
|
|
11500
|
+
fs19__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
10940
11501
|
if (this.config.dashboard.auth) {
|
|
10941
11502
|
console.warn(
|
|
10942
11503
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -10945,7 +11506,7 @@ var DashboardServer = class {
|
|
|
10945
11506
|
return generated;
|
|
10946
11507
|
} catch {
|
|
10947
11508
|
console.warn("Unable to persist dashboard secret; falling back to a process-ephemeral secret.");
|
|
10948
|
-
return
|
|
11509
|
+
return crypto3.randomUUID();
|
|
10949
11510
|
}
|
|
10950
11511
|
}
|
|
10951
11512
|
/**
|
|
@@ -10963,7 +11524,7 @@ var DashboardServer = class {
|
|
|
10963
11524
|
// ── Setup ─────────────────────────────────────
|
|
10964
11525
|
getGlobalStore() {
|
|
10965
11526
|
if (!this.globalStore) {
|
|
10966
|
-
const globalDbPath =
|
|
11527
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
10967
11528
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
10968
11529
|
}
|
|
10969
11530
|
return this.globalStore;
|
|
@@ -10993,7 +11554,7 @@ var DashboardServer = class {
|
|
|
10993
11554
|
metadata: { totalTokens: 0, totalCostUsd: 0, modelsUsed: [], toolsUsed: [], taskCount: 0 }
|
|
10994
11555
|
});
|
|
10995
11556
|
}
|
|
10996
|
-
this.store.addMessage({ id:
|
|
11557
|
+
this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "user", content: prompt, timestamp: now });
|
|
10997
11558
|
} catch (err) {
|
|
10998
11559
|
console.warn("[dashboard] failed to persist run start:", err);
|
|
10999
11560
|
}
|
|
@@ -11004,13 +11565,28 @@ var DashboardServer = class {
|
|
|
11004
11565
|
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
11005
11566
|
try {
|
|
11006
11567
|
if (reply && reply.trim()) {
|
|
11007
|
-
this.store.addMessage({ id:
|
|
11568
|
+
this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
11008
11569
|
}
|
|
11009
11570
|
} catch (err) {
|
|
11010
11571
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
11011
11572
|
}
|
|
11012
11573
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
11013
11574
|
}
|
|
11575
|
+
/**
|
|
11576
|
+
* Route steering text into running Cascade instances. Targets the given
|
|
11577
|
+
* session, or every active session when none is specified (the desktop
|
|
11578
|
+
* usually has exactly one run in flight). Returns how many were reached.
|
|
11579
|
+
*/
|
|
11580
|
+
steerSessions(message, sessionId, nodeId) {
|
|
11581
|
+
const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
|
|
11582
|
+
for (const cascade of targets) cascade.injectGuidance(message, nodeId);
|
|
11583
|
+
return targets.length;
|
|
11584
|
+
}
|
|
11585
|
+
recordSessionTask(sessionId, taskId) {
|
|
11586
|
+
const list = this.sessionTaskIds.get(sessionId) ?? [];
|
|
11587
|
+
list.push(taskId);
|
|
11588
|
+
this.sessionTaskIds.set(sessionId, list);
|
|
11589
|
+
}
|
|
11014
11590
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
11015
11591
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11016
11592
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -11104,12 +11680,12 @@ ${prompt}`;
|
|
|
11104
11680
|
}
|
|
11105
11681
|
}
|
|
11106
11682
|
watchRuntimeChanges() {
|
|
11107
|
-
const workspaceDbPath =
|
|
11108
|
-
const globalDbPath =
|
|
11683
|
+
const workspaceDbPath = path20__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
11684
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11109
11685
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
11110
11686
|
for (const watchPath of watchPaths) {
|
|
11111
|
-
if (!
|
|
11112
|
-
|
|
11687
|
+
if (!fs19__default.default.existsSync(watchPath)) continue;
|
|
11688
|
+
fs19__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
11113
11689
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
11114
11690
|
});
|
|
11115
11691
|
}
|
|
@@ -11179,7 +11755,7 @@ ${prompt}`;
|
|
|
11179
11755
|
const truthy = Buffer.from("1");
|
|
11180
11756
|
const falsy = Buffer.from("0");
|
|
11181
11757
|
const probe = ok ? truthy : falsy;
|
|
11182
|
-
const authorized =
|
|
11758
|
+
const authorized = crypto3.timingSafeEqual(probe, truthy);
|
|
11183
11759
|
if (authorized) {
|
|
11184
11760
|
const token = createToken(
|
|
11185
11761
|
{ id: username, username, role: "admin" },
|
|
@@ -11217,7 +11793,8 @@ ${prompt}`;
|
|
|
11217
11793
|
res.status(400).json({ error: "message is required and must be a string" });
|
|
11218
11794
|
return;
|
|
11219
11795
|
}
|
|
11220
|
-
const
|
|
11796
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
11797
|
+
const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
11221
11798
|
this.socket.broadcast("session:message-injected", payload);
|
|
11222
11799
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
11223
11800
|
res.json({ success: true, ...payload });
|
|
@@ -11239,7 +11816,7 @@ ${prompt}`;
|
|
|
11239
11816
|
const sessionId = req.params.id;
|
|
11240
11817
|
this.store.deleteSession(sessionId);
|
|
11241
11818
|
this.store.deleteRuntimeSession(sessionId);
|
|
11242
|
-
const globalDbPath =
|
|
11819
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11243
11820
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11244
11821
|
try {
|
|
11245
11822
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -11251,9 +11828,34 @@ ${prompt}`;
|
|
|
11251
11828
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
11252
11829
|
res.json({ ok: true });
|
|
11253
11830
|
});
|
|
11831
|
+
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
11832
|
+
const sessionId = req.params.id;
|
|
11833
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
11834
|
+
if (!taskIds.length) {
|
|
11835
|
+
res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
|
|
11836
|
+
return;
|
|
11837
|
+
}
|
|
11838
|
+
const toRestore = /* @__PURE__ */ new Map();
|
|
11839
|
+
for (const taskId of taskIds) {
|
|
11840
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
11841
|
+
if (!toRestore.has(filePath)) toRestore.set(filePath, content);
|
|
11842
|
+
}
|
|
11843
|
+
}
|
|
11844
|
+
const { writeFile } = await import('fs/promises');
|
|
11845
|
+
let restored = 0;
|
|
11846
|
+
for (const [filePath, content] of toRestore) {
|
|
11847
|
+
try {
|
|
11848
|
+
await writeFile(filePath, content, "utf-8");
|
|
11849
|
+
restored++;
|
|
11850
|
+
} catch (err) {
|
|
11851
|
+
console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
|
|
11852
|
+
}
|
|
11853
|
+
}
|
|
11854
|
+
res.json({ ok: true, restored });
|
|
11855
|
+
});
|
|
11254
11856
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
11255
11857
|
const body = req.body;
|
|
11256
|
-
const globalDbPath =
|
|
11858
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11257
11859
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
11258
11860
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11259
11861
|
try {
|
|
@@ -11276,7 +11878,7 @@ ${prompt}`;
|
|
|
11276
11878
|
});
|
|
11277
11879
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
11278
11880
|
this.store.deleteAllRuntimeNodes();
|
|
11279
|
-
const globalDbPath =
|
|
11881
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11280
11882
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11281
11883
|
try {
|
|
11282
11884
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -11300,7 +11902,7 @@ ${prompt}`;
|
|
|
11300
11902
|
const existing = this.store.getDefaultIdentity();
|
|
11301
11903
|
if (existing) this.store.updateIdentity(existing.id, { isDefault: false });
|
|
11302
11904
|
}
|
|
11303
|
-
const id =
|
|
11905
|
+
const id = crypto3.randomUUID();
|
|
11304
11906
|
const identity = {
|
|
11305
11907
|
id,
|
|
11306
11908
|
name: body.name,
|
|
@@ -11327,6 +11929,19 @@ ${prompt}`;
|
|
|
11327
11929
|
this.store.deleteIdentity(req.params.id);
|
|
11328
11930
|
res.json({ ok: true });
|
|
11329
11931
|
});
|
|
11932
|
+
this.app.get("/api/audit/verify", auth, async (_req, res) => {
|
|
11933
|
+
try {
|
|
11934
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
11935
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
11936
|
+
try {
|
|
11937
|
+
res.json(logger.verifyChain());
|
|
11938
|
+
} finally {
|
|
11939
|
+
logger.close();
|
|
11940
|
+
}
|
|
11941
|
+
} catch (err) {
|
|
11942
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
11943
|
+
}
|
|
11944
|
+
});
|
|
11330
11945
|
this.app.get("/api/audit/:sessionId", auth, (req, res) => {
|
|
11331
11946
|
const log = this.store.getAuditLog(req.params.sessionId);
|
|
11332
11947
|
res.json(log);
|
|
@@ -11349,12 +11964,12 @@ ${prompt}`;
|
|
|
11349
11964
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
11350
11965
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
11351
11966
|
try {
|
|
11352
|
-
const configPath =
|
|
11353
|
-
const existing =
|
|
11967
|
+
const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11968
|
+
const existing = fs19__default.default.existsSync(configPath) ? JSON.parse(fs19__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
11354
11969
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
11355
11970
|
const tmp = configPath + ".tmp";
|
|
11356
|
-
|
|
11357
|
-
|
|
11971
|
+
fs19__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
11972
|
+
fs19__default.default.renameSync(tmp, configPath);
|
|
11358
11973
|
res.json({ ok: true });
|
|
11359
11974
|
} catch (err) {
|
|
11360
11975
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -11382,7 +11997,7 @@ ${prompt}`;
|
|
|
11382
11997
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
11383
11998
|
const scope = req.query["scope"] ?? "workspace";
|
|
11384
11999
|
if (scope === "global") {
|
|
11385
|
-
const globalDbPath =
|
|
12000
|
+
const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
11386
12001
|
const globalStore = new MemoryStore(globalDbPath);
|
|
11387
12002
|
try {
|
|
11388
12003
|
res.json({
|
|
@@ -11410,7 +12025,7 @@ ${prompt}`;
|
|
|
11410
12025
|
return;
|
|
11411
12026
|
}
|
|
11412
12027
|
const requestedSessionId = typeof body.sessionId === "string" && body.sessionId.trim() ? body.sessionId.trim() : void 0;
|
|
11413
|
-
const sessionId = requestedSessionId ??
|
|
12028
|
+
const sessionId = requestedSessionId ?? crypto3.randomUUID();
|
|
11414
12029
|
res.json({ sessionId, status: "ACTIVE" });
|
|
11415
12030
|
const prompt = body.prompt;
|
|
11416
12031
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
@@ -11432,6 +12047,7 @@ ${prompt}`;
|
|
|
11432
12047
|
});
|
|
11433
12048
|
try {
|
|
11434
12049
|
const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
|
|
12050
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
11435
12051
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
11436
12052
|
this.socket.broadcast("cost:update", {
|
|
11437
12053
|
sessionId,
|
|
@@ -11462,13 +12078,13 @@ ${prompt}`;
|
|
|
11462
12078
|
}))
|
|
11463
12079
|
});
|
|
11464
12080
|
});
|
|
11465
|
-
const prodPath =
|
|
11466
|
-
const devPath =
|
|
11467
|
-
const webDistPath =
|
|
11468
|
-
if (
|
|
12081
|
+
const prodPath = path20__default.default.resolve(__dirname$1, "../web/dist");
|
|
12082
|
+
const devPath = path20__default.default.resolve(__dirname$1, "../../web/dist");
|
|
12083
|
+
const webDistPath = fs19__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
12084
|
+
if (fs19__default.default.existsSync(webDistPath)) {
|
|
11469
12085
|
this.app.use(express__default.default.static(webDistPath));
|
|
11470
12086
|
this.app.get("*", (_req, res) => {
|
|
11471
|
-
res.sendFile(
|
|
12087
|
+
res.sendFile(path20__default.default.join(webDistPath, "index.html"));
|
|
11472
12088
|
});
|
|
11473
12089
|
} else {
|
|
11474
12090
|
this.app.get("/", (_req, res) => {
|