@massa-ai/tools-api 1.51.0 → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +981 -118
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -7836,17 +7836,11 @@ function detectRoute(platform, host) {
|
|
|
7836
7836
|
if (route === "file")
|
|
7837
7837
|
return { kind: "proceed" };
|
|
7838
7838
|
if (route === "marketplace") {
|
|
7839
|
-
if (host === "claude")
|
|
7839
|
+
if (host === "claude" || host === "codex")
|
|
7840
7840
|
return { kind: "proceed" };
|
|
7841
|
-
if (host === "codex") {
|
|
7842
|
-
return {
|
|
7843
|
-
kind: "refuse",
|
|
7844
|
-
reason: "codex marketplace-route installs are refused (in-place bundle rewrite would dirty a checkout " + "and break the drift gate) \u2014 use the dev path: MASSA_AI_MODEL_PROFILE + regenerate."
|
|
7845
|
-
};
|
|
7846
|
-
}
|
|
7847
7841
|
return {
|
|
7848
7842
|
kind: "refuse",
|
|
7849
|
-
reason: "
|
|
7843
|
+
reason: "marketplace-route installs need an explicit, supported host before switching " + "(claude and codex proceed; no other host resolves a live write target yet) \u2014 " + "use the dev path: MASSA_AI_MODEL_PROFILE + regenerate."
|
|
7850
7844
|
};
|
|
7851
7845
|
}
|
|
7852
7846
|
return {
|
|
@@ -8045,6 +8039,61 @@ var init_lock = __esm(() => {
|
|
|
8045
8039
|
import fs7 from "fs";
|
|
8046
8040
|
import os5 from "os";
|
|
8047
8041
|
import path11 from "path";
|
|
8042
|
+
function splitPluginKey(pluginKey) {
|
|
8043
|
+
const at = pluginKey.indexOf("@");
|
|
8044
|
+
if (at === -1)
|
|
8045
|
+
return null;
|
|
8046
|
+
const pluginName = pluginKey.slice(0, at);
|
|
8047
|
+
const marketplaceName = pluginKey.slice(at + 1);
|
|
8048
|
+
if (!pluginName || !marketplaceName)
|
|
8049
|
+
return null;
|
|
8050
|
+
return { pluginName, marketplaceName };
|
|
8051
|
+
}
|
|
8052
|
+
function isContainedPath(root, candidate) {
|
|
8053
|
+
const rel = path11.relative(root, candidate);
|
|
8054
|
+
return rel === "" || !rel.startsWith("..") && !path11.isAbsolute(rel);
|
|
8055
|
+
}
|
|
8056
|
+
function resolveDirectorySourceRoot(targetHome, pluginKey) {
|
|
8057
|
+
const split = splitPluginKey(pluginKey);
|
|
8058
|
+
if (!split)
|
|
8059
|
+
return;
|
|
8060
|
+
const { pluginName, marketplaceName } = split;
|
|
8061
|
+
const knownMarketplacesPath = path11.join(targetHome, ".claude", "plugins", "known_marketplaces.json");
|
|
8062
|
+
let known;
|
|
8063
|
+
try {
|
|
8064
|
+
known = JSON.parse(fs7.readFileSync(knownMarketplacesPath, "utf8"));
|
|
8065
|
+
} catch {
|
|
8066
|
+
return;
|
|
8067
|
+
}
|
|
8068
|
+
const entry = known?.[marketplaceName];
|
|
8069
|
+
if (!entry || entry.source?.source !== "directory")
|
|
8070
|
+
return;
|
|
8071
|
+
const installLocation = entry.installLocation;
|
|
8072
|
+
if (!installLocation)
|
|
8073
|
+
return null;
|
|
8074
|
+
const manifestPath = path11.join(installLocation, ".claude-plugin", "marketplace.json");
|
|
8075
|
+
let manifest;
|
|
8076
|
+
try {
|
|
8077
|
+
manifest = JSON.parse(fs7.readFileSync(manifestPath, "utf8"));
|
|
8078
|
+
} catch {
|
|
8079
|
+
return null;
|
|
8080
|
+
}
|
|
8081
|
+
const plugin2 = manifest.plugins?.find((p) => p?.name === pluginName);
|
|
8082
|
+
const relSource = plugin2?.source;
|
|
8083
|
+
if (!relSource)
|
|
8084
|
+
return null;
|
|
8085
|
+
const resolvedInstallLocation = path11.resolve(installLocation);
|
|
8086
|
+
const composed = path11.resolve(resolvedInstallLocation, relSource);
|
|
8087
|
+
if (!isContainedPath(resolvedInstallLocation, composed))
|
|
8088
|
+
return null;
|
|
8089
|
+
try {
|
|
8090
|
+
if (!fs7.existsSync(composed))
|
|
8091
|
+
return null;
|
|
8092
|
+
} catch {
|
|
8093
|
+
return null;
|
|
8094
|
+
}
|
|
8095
|
+
return composed;
|
|
8096
|
+
}
|
|
8048
8097
|
function selectRecord(records) {
|
|
8049
8098
|
if (records.length === 0)
|
|
8050
8099
|
return;
|
|
@@ -8064,6 +8113,9 @@ function selectRecord(records) {
|
|
|
8064
8113
|
function resolveClaudeMarketplaceRoot(opts = {}) {
|
|
8065
8114
|
const targetHome = opts.targetHome ?? os5.homedir();
|
|
8066
8115
|
const pluginKey = opts.pluginKey ?? DEFAULT_PLUGIN_KEY;
|
|
8116
|
+
const directoryResult = resolveDirectorySourceRoot(targetHome, pluginKey);
|
|
8117
|
+
if (directoryResult !== undefined)
|
|
8118
|
+
return directoryResult;
|
|
8067
8119
|
const registryPath = path11.join(targetHome, ".claude", "plugins", "installed_plugins.json");
|
|
8068
8120
|
let records;
|
|
8069
8121
|
try {
|
|
@@ -8095,6 +8147,7 @@ import fs8 from "fs";
|
|
|
8095
8147
|
import path12 from "path";
|
|
8096
8148
|
import os6 from "os";
|
|
8097
8149
|
import crypto5 from "crypto";
|
|
8150
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
8098
8151
|
function namedError3(name, message) {
|
|
8099
8152
|
const err = new SwitchEngineError(message);
|
|
8100
8153
|
err.name = name;
|
|
@@ -8173,6 +8226,46 @@ function matchesGlob(filename, glob) {
|
|
|
8173
8226
|
const suffix = glob.slice(starIdx + 1);
|
|
8174
8227
|
return filename.length >= prefix.length + suffix.length && filename.startsWith(prefix) && filename.endsWith(suffix);
|
|
8175
8228
|
}
|
|
8229
|
+
function matchingFileNames(dir, glob) {
|
|
8230
|
+
return fs8.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && matchesGlob(e.name, glob)).map((e) => e.name);
|
|
8231
|
+
}
|
|
8232
|
+
function detectGitAvailability(dir) {
|
|
8233
|
+
try {
|
|
8234
|
+
const out = execFileSync2("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
|
|
8235
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
8236
|
+
});
|
|
8237
|
+
return out.toString().trim() === "true" ? "in-repo" : "not-a-repo";
|
|
8238
|
+
} catch (err) {
|
|
8239
|
+
return err.code === "ENOENT" ? "no-git" : "not-a-repo";
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8242
|
+
function gitTrackedFileNames(dir, filenames) {
|
|
8243
|
+
if (filenames.length === 0)
|
|
8244
|
+
return new Set;
|
|
8245
|
+
try {
|
|
8246
|
+
const out = execFileSync2("git", ["-C", dir, "ls-files", "--", ...filenames], {
|
|
8247
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
8248
|
+
});
|
|
8249
|
+
return new Set(out.toString().split(`
|
|
8250
|
+
`).map((line) => line.trim()).filter(Boolean));
|
|
8251
|
+
} catch {
|
|
8252
|
+
return new Set;
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
8255
|
+
function checkTrackedPathGuard(activeDir, filenames) {
|
|
8256
|
+
if (filenames.length === 0 || !fs8.existsSync(activeDir))
|
|
8257
|
+
return GUARD_PASS;
|
|
8258
|
+
const availability = detectGitAvailability(activeDir);
|
|
8259
|
+
if (availability === "no-git")
|
|
8260
|
+
return GUARD_UNCHECKED;
|
|
8261
|
+
if (availability === "not-a-repo")
|
|
8262
|
+
return GUARD_PASS;
|
|
8263
|
+
const tracked = gitTrackedFileNames(activeDir, filenames);
|
|
8264
|
+
if (tracked.size === 0)
|
|
8265
|
+
return GUARD_PASS;
|
|
8266
|
+
const offending = filenames.find((name) => tracked.has(name));
|
|
8267
|
+
return { blocked: true, path: path12.join(activeDir, offending), unchecked: false };
|
|
8268
|
+
}
|
|
8176
8269
|
function assertStateWritable(stateFilePath) {
|
|
8177
8270
|
const dir = path12.dirname(stateFilePath);
|
|
8178
8271
|
try {
|
|
@@ -8302,12 +8395,27 @@ function switchProfile(opts) {
|
|
|
8302
8395
|
rows.push({ host: h.host, status: "switched" });
|
|
8303
8396
|
continue;
|
|
8304
8397
|
}
|
|
8398
|
+
const candidateNames = matchingFileNames(h.variantDir, h.layout.activeGlob);
|
|
8399
|
+
const guard2 = checkTrackedPathGuard(h.layout.activeDir, candidateNames);
|
|
8400
|
+
if (guard2.blocked) {
|
|
8401
|
+
rows.push({
|
|
8402
|
+
host: h.host,
|
|
8403
|
+
status: "failed",
|
|
8404
|
+
reason: `refusing to write ${guard2.path} \u2014 it is tracked by git and this switch would dirty the checkout`
|
|
8405
|
+
});
|
|
8406
|
+
continue;
|
|
8407
|
+
}
|
|
8305
8408
|
try {
|
|
8306
8409
|
const filesChanged = copyVariant(h.host, h.layout, h.variantDir);
|
|
8307
8410
|
updatePlatform(stateFilePath, h.host, {
|
|
8308
8411
|
modelProfile: { profile: opts.profile, switchedAt: new Date().toISOString() }
|
|
8309
8412
|
});
|
|
8310
|
-
rows.push({
|
|
8413
|
+
rows.push({
|
|
8414
|
+
host: h.host,
|
|
8415
|
+
status: "switched",
|
|
8416
|
+
filesChanged,
|
|
8417
|
+
...guard2.unchecked ? { reason: "tracked-path guard could not verify (git unavailable) \u2014 proceeded unchecked" } : {}
|
|
8418
|
+
});
|
|
8311
8419
|
} catch (err) {
|
|
8312
8420
|
rows.push({ host: h.host, status: "failed", reason: err.message });
|
|
8313
8421
|
}
|
|
@@ -8323,7 +8431,7 @@ function orderRows(universe, rows) {
|
|
|
8323
8431
|
const byHost = new Map(rows.map((r2) => [r2.host, r2]));
|
|
8324
8432
|
return HOSTS.filter((h) => universe.includes(h) && byHost.has(h)).map((h) => byHost.get(h));
|
|
8325
8433
|
}
|
|
8326
|
-
var SwitchEngineError, UnknownProfileError = (profile, known) => namedError3("UnknownProfileError", `unknown profile "${profile}" \u2014 installed: ${known.length > 0 ? known.join(", ") : "none"}`), NoHostsDetectedError = () => namedError3("NoHostsDetectedError", "no installed hosts found");
|
|
8434
|
+
var SwitchEngineError, UnknownProfileError = (profile, known) => namedError3("UnknownProfileError", `unknown profile "${profile}" \u2014 installed: ${known.length > 0 ? known.join(", ") : "none"}`), NoHostsDetectedError = () => namedError3("NoHostsDetectedError", "no installed hosts found"), GUARD_PASS, GUARD_UNCHECKED;
|
|
8327
8435
|
var init_engine = __esm(() => {
|
|
8328
8436
|
init_hosts();
|
|
8329
8437
|
init_state();
|
|
@@ -8335,6 +8443,8 @@ var init_engine = __esm(() => {
|
|
|
8335
8443
|
this.name = "SwitchEngineError";
|
|
8336
8444
|
}
|
|
8337
8445
|
};
|
|
8446
|
+
GUARD_PASS = { blocked: false, unchecked: false };
|
|
8447
|
+
GUARD_UNCHECKED = { blocked: false, unchecked: true };
|
|
8338
8448
|
});
|
|
8339
8449
|
|
|
8340
8450
|
// ../../packages/shared/dist/profile-switch/report.js
|
|
@@ -128577,7 +128687,7 @@ var init_git_ref_validation = __esm(() => {
|
|
|
128577
128687
|
});
|
|
128578
128688
|
|
|
128579
128689
|
// ../../packages/core/dist/services/symbol/impact-analysis.js
|
|
128580
|
-
import { execFileSync as
|
|
128690
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
128581
128691
|
function readBfsCteFlag() {
|
|
128582
128692
|
try {
|
|
128583
128693
|
return Boolean(config.get("impact")?.bfsCteEnabled);
|
|
@@ -128903,19 +129013,19 @@ function defaultDiffRunner(projectPath2, scope, baseBranch, since) {
|
|
|
128903
129013
|
let diffRange;
|
|
128904
129014
|
if (since) {
|
|
128905
129015
|
try {
|
|
128906
|
-
ref =
|
|
129016
|
+
ref = execFileSync3("git", ["-C", projectPath2, "rev-parse", "--verify", `${since}^{commit}`], {
|
|
128907
129017
|
cwd: projectPath2,
|
|
128908
129018
|
encoding: "utf-8",
|
|
128909
129019
|
stdio: ["ignore", "pipe", "pipe"]
|
|
128910
129020
|
}).trim();
|
|
128911
129021
|
} catch {
|
|
128912
|
-
ref =
|
|
129022
|
+
ref = execFileSync3("git", ["-C", projectPath2, "rev-list", "-1", `--before=${since}`, "HEAD"], {
|
|
128913
129023
|
cwd: projectPath2,
|
|
128914
129024
|
encoding: "utf-8",
|
|
128915
129025
|
stdio: ["ignore", "pipe", "pipe"]
|
|
128916
129026
|
}).trim();
|
|
128917
129027
|
if (!ref) {
|
|
128918
|
-
const emptyTree =
|
|
129028
|
+
const emptyTree = execFileSync3("git", ["hash-object", "-t", "tree", "--stdin"], {
|
|
128919
129029
|
cwd: projectPath2,
|
|
128920
129030
|
encoding: "utf-8",
|
|
128921
129031
|
input: "",
|
|
@@ -128927,7 +129037,7 @@ function defaultDiffRunner(projectPath2, scope, baseBranch, since) {
|
|
|
128927
129037
|
}
|
|
128928
129038
|
args.push(diffRange ?? `${ref}...HEAD`);
|
|
128929
129039
|
}
|
|
128930
|
-
const out =
|
|
129040
|
+
const out = execFileSync3("git", args, {
|
|
128931
129041
|
cwd: projectPath2,
|
|
128932
129042
|
encoding: "utf-8",
|
|
128933
129043
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -128939,7 +129049,7 @@ function defaultDiffRunner(projectPath2, scope, baseBranch, since) {
|
|
|
128939
129049
|
let untrackedFiltered = 0;
|
|
128940
129050
|
const merged = new Set(tracked);
|
|
128941
129051
|
if (includeUntracked) {
|
|
128942
|
-
const untracked =
|
|
129052
|
+
const untracked = execFileSync3("git", ["-C", projectPath2, "ls-files", "--others", "--exclude-standard"], {
|
|
128943
129053
|
cwd: projectPath2,
|
|
128944
129054
|
encoding: "utf-8",
|
|
128945
129055
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -128977,7 +129087,7 @@ var init_impact_analysis = __esm(() => {
|
|
|
128977
129087
|
});
|
|
128978
129088
|
|
|
128979
129089
|
// ../../packages/core/dist/services/executor/runtime.js
|
|
128980
|
-
import { execFileSync as
|
|
129090
|
+
import { execFileSync as execFileSync4, execSync } from "child_process";
|
|
128981
129091
|
function commandExists(cmd) {
|
|
128982
129092
|
try {
|
|
128983
129093
|
const check3 = isWindows ? `where ${cmd}` : `command -v ${cmd}`;
|
|
@@ -129010,7 +129120,7 @@ function getVersion(cmd, args = ["--version"], deps) {
|
|
|
129010
129120
|
timeout: 5000
|
|
129011
129121
|
}).trim().split(/\r?\n/)[0];
|
|
129012
129122
|
}
|
|
129013
|
-
return
|
|
129123
|
+
return execFileSync4(cmd, args, {
|
|
129014
129124
|
encoding: "utf-8",
|
|
129015
129125
|
stdio: ["pipe", "pipe", "pipe"],
|
|
129016
129126
|
timeout: 5000
|
|
@@ -129151,12 +129261,12 @@ var init_runtime = __esm(() => {
|
|
|
129151
129261
|
|
|
129152
129262
|
// ../../packages/core/dist/services/executor/sandbox.js
|
|
129153
129263
|
import { realpathSync as realpathSync2 } from "fs";
|
|
129154
|
-
import { execFileSync as
|
|
129264
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
129155
129265
|
function isDockerAvailable() {
|
|
129156
129266
|
if (_dockerAvailable !== null)
|
|
129157
129267
|
return _dockerAvailable;
|
|
129158
129268
|
try {
|
|
129159
|
-
|
|
129269
|
+
execFileSync5("docker", ["--version"], { stdio: "pipe", timeout: 5000 });
|
|
129160
129270
|
_dockerAvailable = true;
|
|
129161
129271
|
} catch {
|
|
129162
129272
|
_dockerAvailable = false;
|
|
@@ -129167,7 +129277,7 @@ function isSeatbeltAvailable() {
|
|
|
129167
129277
|
if (_seatbeltAvailable !== null)
|
|
129168
129278
|
return _seatbeltAvailable;
|
|
129169
129279
|
try {
|
|
129170
|
-
|
|
129280
|
+
execFileSync5("sandbox-exec", ["--version"], { stdio: "pipe", timeout: 5000 });
|
|
129171
129281
|
_seatbeltAvailable = true;
|
|
129172
129282
|
} catch {
|
|
129173
129283
|
_seatbeltAvailable = false;
|
|
@@ -129272,7 +129382,7 @@ var init_sandbox = __esm(() => {
|
|
|
129272
129382
|
});
|
|
129273
129383
|
|
|
129274
129384
|
// ../../packages/core/dist/services/executor/executor.js
|
|
129275
|
-
import { spawn, execSync as execSync2, execFileSync as
|
|
129385
|
+
import { spawn, execSync as execSync2, execFileSync as execFileSync6 } from "child_process";
|
|
129276
129386
|
import { mkdtempSync, writeFileSync as writeFileSync2, rmSync, realpathSync as realpathSync3 } from "fs";
|
|
129277
129387
|
import { join as join3, resolve as resolve5, isAbsolute, relative as relative2 } from "path";
|
|
129278
129388
|
import { tmpdir } from "os";
|
|
@@ -129475,7 +129585,7 @@ ${body}`;
|
|
|
129475
129585
|
const binPath = srcPath.replace(/\.rs$/, isWin ? ".exe" : "");
|
|
129476
129586
|
try {
|
|
129477
129587
|
try {
|
|
129478
|
-
|
|
129588
|
+
execFileSync6("rustc", [srcPath, "-o", binPath], {
|
|
129479
129589
|
cwd,
|
|
129480
129590
|
timeout: Math.min(timeout, 60000),
|
|
129481
129591
|
encoding: "utf-8",
|
|
@@ -132417,23 +132527,94 @@ var init_scheduler = __esm(() => {
|
|
|
132417
132527
|
};
|
|
132418
132528
|
});
|
|
132419
132529
|
|
|
132530
|
+
// ../../packages/core/dist/data/proposal/proposal-payload-validation.js
|
|
132531
|
+
function isRecord(value) {
|
|
132532
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
132533
|
+
}
|
|
132534
|
+
function isStringArray(value) {
|
|
132535
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
132536
|
+
}
|
|
132537
|
+
function isString(value) {
|
|
132538
|
+
return typeof value === "string";
|
|
132539
|
+
}
|
|
132540
|
+
function isNumber(value) {
|
|
132541
|
+
return typeof value === "number";
|
|
132542
|
+
}
|
|
132543
|
+
function isValidProposalPayload(kind, value) {
|
|
132544
|
+
const rule = PROPOSAL_PAYLOAD_RULES[kind];
|
|
132545
|
+
if (!Object.keys(value).every((key) => rule.allowedKeys.includes(key)))
|
|
132546
|
+
return false;
|
|
132547
|
+
if (rule.requireNonEmpty && Object.keys(value).length === 0)
|
|
132548
|
+
return false;
|
|
132549
|
+
for (const [field3, fieldRule] of Object.entries(rule.fields)) {
|
|
132550
|
+
const present = value[field3] !== undefined;
|
|
132551
|
+
if (!present) {
|
|
132552
|
+
if (!fieldRule.optional)
|
|
132553
|
+
return false;
|
|
132554
|
+
continue;
|
|
132555
|
+
}
|
|
132556
|
+
if (!fieldRule.validate(value[field3]))
|
|
132557
|
+
return false;
|
|
132558
|
+
}
|
|
132559
|
+
return true;
|
|
132560
|
+
}
|
|
132561
|
+
function assertValidProposalPayload(kind, value) {
|
|
132562
|
+
if (!isRecord(value)) {
|
|
132563
|
+
throw new ProposalPayloadValidationError(kind, `proposal payload must be an object for kind "${kind}"`);
|
|
132564
|
+
}
|
|
132565
|
+
if (!isValidProposalPayload(kind, value)) {
|
|
132566
|
+
throw new ProposalPayloadValidationError(kind, `proposal payload is invalid for kind "${kind}"`);
|
|
132567
|
+
}
|
|
132568
|
+
}
|
|
132569
|
+
var PROPOSAL_PAYLOAD_RULES, ProposalPayloadValidationError;
|
|
132570
|
+
var init_proposal_payload_validation = __esm(() => {
|
|
132571
|
+
PROPOSAL_PAYLOAD_RULES = {
|
|
132572
|
+
"memory.create": {
|
|
132573
|
+
allowedKeys: ["content", "type", "level", "importance", "tags"],
|
|
132574
|
+
fields: {
|
|
132575
|
+
content: { optional: false, validate: isString },
|
|
132576
|
+
type: { optional: true, validate: isString },
|
|
132577
|
+
level: { optional: true, validate: isNumber },
|
|
132578
|
+
importance: { optional: true, validate: isNumber },
|
|
132579
|
+
tags: { optional: true, validate: isStringArray }
|
|
132580
|
+
}
|
|
132581
|
+
},
|
|
132582
|
+
"memory.update": {
|
|
132583
|
+
allowedKeys: ["content", "importance", "tags"],
|
|
132584
|
+
requireNonEmpty: true,
|
|
132585
|
+
fields: {
|
|
132586
|
+
content: { optional: true, validate: isString },
|
|
132587
|
+
importance: { optional: true, validate: isNumber },
|
|
132588
|
+
tags: { optional: true, validate: isStringArray }
|
|
132589
|
+
}
|
|
132590
|
+
},
|
|
132591
|
+
"memory.tag": {
|
|
132592
|
+
allowedKeys: ["tags"],
|
|
132593
|
+
fields: {
|
|
132594
|
+
tags: { optional: false, validate: isStringArray }
|
|
132595
|
+
}
|
|
132596
|
+
}
|
|
132597
|
+
};
|
|
132598
|
+
ProposalPayloadValidationError = class ProposalPayloadValidationError extends Error {
|
|
132599
|
+
kind;
|
|
132600
|
+
statusCode = 400;
|
|
132601
|
+
constructor(kind, message) {
|
|
132602
|
+
super(message);
|
|
132603
|
+
this.kind = kind;
|
|
132604
|
+
this.name = "ProposalPayloadValidationError";
|
|
132605
|
+
}
|
|
132606
|
+
};
|
|
132607
|
+
});
|
|
132608
|
+
|
|
132420
132609
|
// ../../packages/core/dist/data/proposal/proposal-contract.js
|
|
132421
132610
|
var PROPOSAL_STATUSES, PROPOSAL_KINDS;
|
|
132422
132611
|
var init_proposal_contract = __esm(() => {
|
|
132612
|
+
init_proposal_payload_validation();
|
|
132423
132613
|
PROPOSAL_STATUSES = ["pending", "approved", "rejected"];
|
|
132424
132614
|
PROPOSAL_KINDS = ["memory.create", "memory.update", "memory.tag"];
|
|
132425
132615
|
});
|
|
132426
132616
|
|
|
132427
132617
|
// ../../packages/core/dist/data/proposal/proposal-repository-pg.js
|
|
132428
|
-
function isRecord(value) {
|
|
132429
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
132430
|
-
}
|
|
132431
|
-
function stringArray(value) {
|
|
132432
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
132433
|
-
}
|
|
132434
|
-
function validKeys(value, allowed) {
|
|
132435
|
-
return Object.keys(value).every((key) => allowed.includes(key));
|
|
132436
|
-
}
|
|
132437
132618
|
function parsePayload(raw2, kind) {
|
|
132438
132619
|
let value;
|
|
132439
132620
|
try {
|
|
@@ -132444,15 +132625,7 @@ function parsePayload(raw2, kind) {
|
|
|
132444
132625
|
if (!isRecord(value)) {
|
|
132445
132626
|
throw storeCorruption("proposal.payload_json", new TypeError("expected object"));
|
|
132446
132627
|
}
|
|
132447
|
-
|
|
132448
|
-
if (kind === "memory.create") {
|
|
132449
|
-
valid = validKeys(value, ["content", "type", "level", "importance", "tags"]) && typeof value.content === "string" && (value.type === undefined || typeof value.type === "string") && (value.level === undefined || typeof value.level === "number") && (value.importance === undefined || typeof value.importance === "number") && (value.tags === undefined || stringArray(value.tags));
|
|
132450
|
-
} else if (kind === "memory.update") {
|
|
132451
|
-
valid = validKeys(value, ["content", "importance", "tags"]) && Object.keys(value).length > 0 && (value.content === undefined || typeof value.content === "string") && (value.importance === undefined || typeof value.importance === "number") && (value.tags === undefined || stringArray(value.tags));
|
|
132452
|
-
} else {
|
|
132453
|
-
valid = validKeys(value, ["tags"]) && stringArray(value.tags);
|
|
132454
|
-
}
|
|
132455
|
-
if (!valid) {
|
|
132628
|
+
if (!isValidProposalPayload(kind, value)) {
|
|
132456
132629
|
throw storeCorruption("proposal.payload_json", new TypeError("invalid proposal payload"));
|
|
132457
132630
|
}
|
|
132458
132631
|
return value;
|
|
@@ -132586,6 +132759,65 @@ class PgProposalStore {
|
|
|
132586
132759
|
this.mirror.set(id, persisted);
|
|
132587
132760
|
return structuredClone(persisted);
|
|
132588
132761
|
}
|
|
132762
|
+
async create(input) {
|
|
132763
|
+
assertValidProposalPayload(input.kind, input.payload);
|
|
132764
|
+
const record2 = {
|
|
132765
|
+
id: input.id,
|
|
132766
|
+
projectId: input.projectId,
|
|
132767
|
+
kind: input.kind,
|
|
132768
|
+
targetMemoryId: input.targetMemoryId ?? null,
|
|
132769
|
+
payload: input.payload,
|
|
132770
|
+
rationale: input.rationale ?? "",
|
|
132771
|
+
status: "pending",
|
|
132772
|
+
createdAt: input.createdAt ?? Date.now(),
|
|
132773
|
+
decidedAt: null
|
|
132774
|
+
};
|
|
132775
|
+
await this.insert(record2);
|
|
132776
|
+
return record2;
|
|
132777
|
+
}
|
|
132778
|
+
async update(id, patch) {
|
|
132779
|
+
await this.ensureHydrated();
|
|
132780
|
+
const current = this.mirror.get(id);
|
|
132781
|
+
if (!current)
|
|
132782
|
+
return null;
|
|
132783
|
+
if (patch.payload !== undefined) {
|
|
132784
|
+
assertValidProposalPayload(current.kind, patch.payload);
|
|
132785
|
+
}
|
|
132786
|
+
const merged = {
|
|
132787
|
+
...current,
|
|
132788
|
+
...patch.rationale !== undefined ? { rationale: patch.rationale } : {},
|
|
132789
|
+
...patch.payload !== undefined ? { payload: patch.payload } : {}
|
|
132790
|
+
};
|
|
132791
|
+
let rows;
|
|
132792
|
+
try {
|
|
132793
|
+
rows = await this.getClient().$queryRaw`
|
|
132794
|
+
UPDATE proposals
|
|
132795
|
+
SET rationale = ${merged.rationale}, payload_json = ${JSON.stringify(merged.payload)}
|
|
132796
|
+
WHERE id = ${id}
|
|
132797
|
+
RETURNING id, project_id, kind, target_memory_id, payload_json,
|
|
132798
|
+
rationale, status, created_at, decided_at`;
|
|
132799
|
+
} catch (error51) {
|
|
132800
|
+
throw searchBackendUnavailable("proposal_store", error51);
|
|
132801
|
+
}
|
|
132802
|
+
if (!rows[0])
|
|
132803
|
+
return null;
|
|
132804
|
+
const persisted = toRecord(rows[0]);
|
|
132805
|
+
this.mirror.set(id, persisted);
|
|
132806
|
+
return structuredClone(persisted);
|
|
132807
|
+
}
|
|
132808
|
+
async delete(id) {
|
|
132809
|
+
await this.ensureHydrated();
|
|
132810
|
+
let affected;
|
|
132811
|
+
try {
|
|
132812
|
+
affected = await this.getClient().$executeRaw`DELETE FROM proposals WHERE id = ${id}`;
|
|
132813
|
+
} catch (error51) {
|
|
132814
|
+
throw searchBackendUnavailable("proposal_store", error51);
|
|
132815
|
+
}
|
|
132816
|
+
if (affected === 0)
|
|
132817
|
+
return null;
|
|
132818
|
+
this.mirror.delete(id);
|
|
132819
|
+
return id;
|
|
132820
|
+
}
|
|
132589
132821
|
async journalMode() {
|
|
132590
132822
|
await this.ensureHydrated();
|
|
132591
132823
|
return "postgres";
|
|
@@ -132602,6 +132834,7 @@ var init_proposal_repository_pg = __esm(() => {
|
|
|
132602
132834
|
init_alias_resolver();
|
|
132603
132835
|
init_search_diagnostics();
|
|
132604
132836
|
init_proposal_contract();
|
|
132837
|
+
init_proposal_payload_validation();
|
|
132605
132838
|
});
|
|
132606
132839
|
|
|
132607
132840
|
// ../../packages/core/dist/data/proposal/proposal-repository.js
|
|
@@ -179181,6 +179414,53 @@ class PgHandoffStore {
|
|
|
179181
179414
|
this.mirror.set(id, persisted);
|
|
179182
179415
|
return structuredClone(persisted);
|
|
179183
179416
|
}
|
|
179417
|
+
async update(id, patch) {
|
|
179418
|
+
await this.ensureHydrated();
|
|
179419
|
+
const current = this.mirror.get(id);
|
|
179420
|
+
if (!current)
|
|
179421
|
+
return null;
|
|
179422
|
+
const merged = {
|
|
179423
|
+
...current,
|
|
179424
|
+
...patch.targetAgent !== undefined ? { targetAgent: patch.targetAgent } : {},
|
|
179425
|
+
...patch.summary !== undefined ? { summary: patch.summary } : {},
|
|
179426
|
+
...patch.openQuestions !== undefined ? { openQuestions: patch.openQuestions } : {},
|
|
179427
|
+
...patch.nextSteps !== undefined ? { nextSteps: patch.nextSteps } : {},
|
|
179428
|
+
...patch.files !== undefined ? { files: patch.files } : {}
|
|
179429
|
+
};
|
|
179430
|
+
let rows;
|
|
179431
|
+
try {
|
|
179432
|
+
rows = await this.getClient().$queryRaw`
|
|
179433
|
+
UPDATE handoffs
|
|
179434
|
+
SET target_agent = ${merged.targetAgent}, summary = ${merged.summary},
|
|
179435
|
+
open_questions_json = ${JSON.stringify(merged.openQuestions ?? [])},
|
|
179436
|
+
next_steps_json = ${JSON.stringify(merged.nextSteps ?? [])},
|
|
179437
|
+
files_json = ${JSON.stringify(merged.files ?? [])}
|
|
179438
|
+
WHERE id = ${id}
|
|
179439
|
+
RETURNING id, project_id, source_session_id, target_agent, summary,
|
|
179440
|
+
open_questions_json, next_steps_json, files_json, status,
|
|
179441
|
+
created_at, accepted_at`;
|
|
179442
|
+
} catch (error51) {
|
|
179443
|
+
throw searchBackendUnavailable("handoff_store", error51);
|
|
179444
|
+
}
|
|
179445
|
+
if (!rows[0])
|
|
179446
|
+
return null;
|
|
179447
|
+
const persisted = toRecord2(rows[0]);
|
|
179448
|
+
this.mirror.set(id, persisted);
|
|
179449
|
+
return structuredClone(persisted);
|
|
179450
|
+
}
|
|
179451
|
+
async delete(id) {
|
|
179452
|
+
await this.ensureHydrated();
|
|
179453
|
+
let affected;
|
|
179454
|
+
try {
|
|
179455
|
+
affected = await this.getClient().$executeRaw`DELETE FROM handoffs WHERE id = ${id}`;
|
|
179456
|
+
} catch (error51) {
|
|
179457
|
+
throw searchBackendUnavailable("handoff_store", error51);
|
|
179458
|
+
}
|
|
179459
|
+
if (affected === 0)
|
|
179460
|
+
return null;
|
|
179461
|
+
this.mirror.delete(id);
|
|
179462
|
+
return id;
|
|
179463
|
+
}
|
|
179184
179464
|
async journalMode() {
|
|
179185
179465
|
await this.ensureHydrated();
|
|
179186
179466
|
return "postgres";
|
|
@@ -179205,7 +179485,6 @@ function newHandoffId() {
|
|
|
179205
179485
|
// ../../packages/core/dist/services/handoff/handoff-service.js
|
|
179206
179486
|
init_zod();
|
|
179207
179487
|
init_dist();
|
|
179208
|
-
import { randomUUID as randomUUID10 } from "crypto";
|
|
179209
179488
|
init_memory_repository_factory();
|
|
179210
179489
|
init_event_bus();
|
|
179211
179490
|
init_llm_client();
|
|
@@ -179226,7 +179505,8 @@ class HandoffService {
|
|
|
179226
179505
|
this.idFactory = deps.idFactory ?? (() => newHandoffId());
|
|
179227
179506
|
const injectedRepo = deps.memoryRepo;
|
|
179228
179507
|
this.memoryRepo = injectedRepo ?? {
|
|
179229
|
-
insert: (i) => getMemoryRepository().insert(i)
|
|
179508
|
+
insert: (i) => getMemoryRepository().insert(i),
|
|
179509
|
+
update: (id, p) => getMemoryRepository().update(id, p)
|
|
179230
179510
|
};
|
|
179231
179511
|
}
|
|
179232
179512
|
async begin(input) {
|
|
@@ -179268,7 +179548,7 @@ class HandoffService {
|
|
|
179268
179548
|
return { ok: true, id, status: "open", memoryId };
|
|
179269
179549
|
}
|
|
179270
179550
|
async dualWrite(record2) {
|
|
179271
|
-
const memId =
|
|
179551
|
+
const memId = dualWriteMemoryId(record2.id);
|
|
179272
179552
|
const input = buildHandoffMemoryInput(memId, record2);
|
|
179273
179553
|
await Promise.resolve(this.memoryRepo.insert(input));
|
|
179274
179554
|
return memId;
|
|
@@ -179312,6 +179592,50 @@ class HandoffService {
|
|
|
179312
179592
|
async listPending(projectId, targetAgent) {
|
|
179313
179593
|
return this.store.listPending(projectId, targetAgent ?? undefined);
|
|
179314
179594
|
}
|
|
179595
|
+
async update(params) {
|
|
179596
|
+
if (!params || !params.id) {
|
|
179597
|
+
return { ok: false, reason: "missing-id" };
|
|
179598
|
+
}
|
|
179599
|
+
const row = await this.store.getById(params.id);
|
|
179600
|
+
if (!row)
|
|
179601
|
+
return { ok: false, reason: "not-found" };
|
|
179602
|
+
if (params.projectId && row.projectId !== params.projectId) {
|
|
179603
|
+
return { ok: false, reason: "project-mismatch" };
|
|
179604
|
+
}
|
|
179605
|
+
const updated = await this.store.update(params.id, params.patch);
|
|
179606
|
+
if (!updated)
|
|
179607
|
+
return { ok: false, reason: "not-found" };
|
|
179608
|
+
const touchesMemoryContent = params.patch.summary !== undefined || params.patch.openQuestions !== undefined || params.patch.nextSteps !== undefined || params.patch.files !== undefined;
|
|
179609
|
+
if (touchesMemoryContent) {
|
|
179610
|
+
await this.refreshDualWriteMemory(updated);
|
|
179611
|
+
}
|
|
179612
|
+
return { ok: true, handoff: updated };
|
|
179613
|
+
}
|
|
179614
|
+
async refreshDualWriteMemory(record2) {
|
|
179615
|
+
try {
|
|
179616
|
+
const memId = dualWriteMemoryId(record2.id);
|
|
179617
|
+
const content = formatMemoryContent(record2);
|
|
179618
|
+
await Promise.resolve(this.memoryRepo.update(memId, { content }));
|
|
179619
|
+
} catch {}
|
|
179620
|
+
}
|
|
179621
|
+
async delete(params) {
|
|
179622
|
+
if (!params || !params.id) {
|
|
179623
|
+
return { ok: false, reason: "missing-id" };
|
|
179624
|
+
}
|
|
179625
|
+
const row = await this.store.getById(params.id);
|
|
179626
|
+
if (!row)
|
|
179627
|
+
return { ok: false, reason: "not-found" };
|
|
179628
|
+
if (params.projectId && row.projectId !== params.projectId) {
|
|
179629
|
+
return { ok: false, reason: "project-mismatch" };
|
|
179630
|
+
}
|
|
179631
|
+
const deletedId = await this.store.delete(params.id);
|
|
179632
|
+
if (!deletedId)
|
|
179633
|
+
return { ok: false, reason: "not-found" };
|
|
179634
|
+
return { ok: true, id: deletedId };
|
|
179635
|
+
}
|
|
179636
|
+
}
|
|
179637
|
+
function dualWriteMemoryId(handoffId) {
|
|
179638
|
+
return `handoff-mem-${handoffId}`;
|
|
179315
179639
|
}
|
|
179316
179640
|
function buildHandoffMemoryInput(memId, record2) {
|
|
179317
179641
|
const content = formatMemoryContent(record2);
|
|
@@ -179406,6 +179730,7 @@ init_event_bus();
|
|
|
179406
179730
|
|
|
179407
179731
|
// ../../packages/core/dist/index.js
|
|
179408
179732
|
init_proposal_repository();
|
|
179733
|
+
init_proposal_payload_validation();
|
|
179409
179734
|
init_auto_improve_job();
|
|
179410
179735
|
|
|
179411
179736
|
// src/routes/search.ts
|
|
@@ -180400,12 +180725,35 @@ var systemRoutes = new Elysia({ prefix: "/api/v1/system" }).get("/info", async (
|
|
|
180400
180725
|
}
|
|
180401
180726
|
});
|
|
180402
180727
|
|
|
180728
|
+
// src/routes/sse-keepalive.ts
|
|
180729
|
+
var SSE_HEARTBEAT_MS_DEFAULT = 5000;
|
|
180730
|
+
var SSE_MAX_DURATION_MS_DEFAULT = 10 * 60 * 1000;
|
|
180731
|
+
var SSE_REQUEST_TIMEOUT_SECONDS = 120;
|
|
180732
|
+
function resolveHeartbeatMs() {
|
|
180733
|
+
return Number(process.env.MASSA_AI_SSE_HEARTBEAT_MS) || SSE_HEARTBEAT_MS_DEFAULT;
|
|
180734
|
+
}
|
|
180735
|
+
function resolveMaxDurationMs() {
|
|
180736
|
+
return Number(process.env.MASSA_AI_SSE_MAX_DURATION_MS) || SSE_MAX_DURATION_MS_DEFAULT;
|
|
180737
|
+
}
|
|
180738
|
+
var requestTimeoutSource;
|
|
180739
|
+
var applyCount = 0;
|
|
180740
|
+
function setSseRequestTimeoutSource(source) {
|
|
180741
|
+
requestTimeoutSource = source;
|
|
180742
|
+
}
|
|
180743
|
+
function applySseRequestTimeout(request) {
|
|
180744
|
+
const source = requestTimeoutSource;
|
|
180745
|
+
if (!source || typeof source.timeout !== "function")
|
|
180746
|
+
return;
|
|
180747
|
+
try {
|
|
180748
|
+
source.timeout(request, SSE_REQUEST_TIMEOUT_SECONDS);
|
|
180749
|
+
applyCount++;
|
|
180750
|
+
} catch {}
|
|
180751
|
+
}
|
|
180752
|
+
|
|
180403
180753
|
// src/routes/events.ts
|
|
180404
|
-
var
|
|
180405
|
-
|
|
180406
|
-
|
|
180407
|
-
const HEARTBEAT_MS = Number(process.env.MASSA_AI_SSE_HEARTBEAT_MS) || HEARTBEAT_MS_DEFAULT;
|
|
180408
|
-
const MAX_DURATION_MS = Number(process.env.MASSA_AI_SSE_MAX_DURATION_MS) || MAX_DURATION_MS_DEFAULT;
|
|
180754
|
+
var eventsRoutes = new Elysia({ prefix: "/api/v1" }).get("/events", async ({ query, set: set3, request }) => {
|
|
180755
|
+
const HEARTBEAT_MS = resolveHeartbeatMs();
|
|
180756
|
+
const MAX_DURATION_MS = resolveMaxDurationMs();
|
|
180409
180757
|
const projectIdFilter = query.projectId;
|
|
180410
180758
|
const jobIdFilter = query.jobId;
|
|
180411
180759
|
set3.headers["Content-Type"] = "text/event-stream";
|
|
@@ -180419,6 +180767,18 @@ var eventsRoutes = new Elysia({ prefix: "/api/v1" }).get("/events", async ({ que
|
|
|
180419
180767
|
let closeTimer;
|
|
180420
180768
|
const stream2 = new ReadableStream({
|
|
180421
180769
|
start(controller) {
|
|
180770
|
+
applySseRequestTimeout(request);
|
|
180771
|
+
const teardown = () => {
|
|
180772
|
+
closed = true;
|
|
180773
|
+
unsubscribers.forEach((u) => u());
|
|
180774
|
+
if (heartbeatTimer)
|
|
180775
|
+
clearInterval(heartbeatTimer);
|
|
180776
|
+
if (closeTimer)
|
|
180777
|
+
clearTimeout(closeTimer);
|
|
180778
|
+
try {
|
|
180779
|
+
controller.close();
|
|
180780
|
+
} catch {}
|
|
180781
|
+
};
|
|
180422
180782
|
const enqueue = (data) => {
|
|
180423
180783
|
if (closed)
|
|
180424
180784
|
return;
|
|
@@ -180427,7 +180787,7 @@ var eventsRoutes = new Elysia({ prefix: "/api/v1" }).get("/events", async ({ que
|
|
|
180427
180787
|
|
|
180428
180788
|
`));
|
|
180429
180789
|
} catch {
|
|
180430
|
-
|
|
180790
|
+
teardown();
|
|
180431
180791
|
}
|
|
180432
180792
|
};
|
|
180433
180793
|
const events = [
|
|
@@ -180455,18 +180815,10 @@ var eventsRoutes = new Elysia({ prefix: "/api/v1" }).get("/events", async ({ que
|
|
|
180455
180815
|
|
|
180456
180816
|
`));
|
|
180457
180817
|
} catch {
|
|
180458
|
-
|
|
180459
|
-
clearInterval(heartbeatTimer);
|
|
180818
|
+
teardown();
|
|
180460
180819
|
}
|
|
180461
180820
|
}, HEARTBEAT_MS);
|
|
180462
|
-
closeTimer = setTimeout(
|
|
180463
|
-
closed = true;
|
|
180464
|
-
unsubscribers.forEach((u) => u());
|
|
180465
|
-
clearInterval(heartbeatTimer);
|
|
180466
|
-
try {
|
|
180467
|
-
controller.close();
|
|
180468
|
-
} catch {}
|
|
180469
|
-
}, MAX_DURATION_MS);
|
|
180821
|
+
closeTimer = setTimeout(teardown, MAX_DURATION_MS);
|
|
180470
180822
|
enqueue({
|
|
180471
180823
|
event: "connected",
|
|
180472
180824
|
payload: {
|
|
@@ -181568,6 +181920,16 @@ function rethrowCanonicalHandoffError(error51) {
|
|
|
181568
181920
|
var EVENT_DETAIL3 = {
|
|
181569
181921
|
tags: ["handoffs"]
|
|
181570
181922
|
};
|
|
181923
|
+
var HANDOFF_PATCH_ALLOWED_FIELDS = [
|
|
181924
|
+
"targetAgent",
|
|
181925
|
+
"summary",
|
|
181926
|
+
"openQuestions",
|
|
181927
|
+
"nextSteps",
|
|
181928
|
+
"files"
|
|
181929
|
+
];
|
|
181930
|
+
function isStringArray2(value) {
|
|
181931
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
181932
|
+
}
|
|
181571
181933
|
var handoffRoutes = new Elysia({ prefix: "/api/v1/handoff" }).post("/begin", async ({ body, set: set3 }) => {
|
|
181572
181934
|
if (handoffsDisabled()) {
|
|
181573
181935
|
set3.status = 423;
|
|
@@ -181708,6 +182070,121 @@ var handoffRoutes = new Elysia({ prefix: "/api/v1/handoff" }).post("/begin", asy
|
|
|
181708
182070
|
summary: "List pending (open) handoffs",
|
|
181709
182071
|
description: "Lists open handoffs for a project, optionally filtered by target agent, ordered oldest-first."
|
|
181710
182072
|
}
|
|
182073
|
+
}).patch("/:id", async ({ params, query, body, set: set3 }) => {
|
|
182074
|
+
if (handoffsDisabled()) {
|
|
182075
|
+
set3.status = 423;
|
|
182076
|
+
return { status: 423, error: "handoffs disabled" };
|
|
182077
|
+
}
|
|
182078
|
+
const raw2 = body ?? {};
|
|
182079
|
+
const keys = Object.keys(raw2);
|
|
182080
|
+
const rejected = keys.filter((k2) => !HANDOFF_PATCH_ALLOWED_FIELDS.includes(k2));
|
|
182081
|
+
if (rejected.length > 0) {
|
|
182082
|
+
set3.status = 400;
|
|
182083
|
+
return { status: 400, error: `field not allowed: ${rejected.join(", ")}` };
|
|
182084
|
+
}
|
|
182085
|
+
const patch = {};
|
|
182086
|
+
if ("targetAgent" in raw2) {
|
|
182087
|
+
const v = raw2.targetAgent;
|
|
182088
|
+
if (v !== null && typeof v !== "string") {
|
|
182089
|
+
set3.status = 400;
|
|
182090
|
+
return { status: 400, error: "targetAgent must be a string or null" };
|
|
182091
|
+
}
|
|
182092
|
+
patch.targetAgent = v;
|
|
182093
|
+
}
|
|
182094
|
+
if ("summary" in raw2) {
|
|
182095
|
+
if (typeof raw2.summary !== "string") {
|
|
182096
|
+
set3.status = 400;
|
|
182097
|
+
return { status: 400, error: "summary must be a string" };
|
|
182098
|
+
}
|
|
182099
|
+
patch.summary = raw2.summary;
|
|
182100
|
+
}
|
|
182101
|
+
if ("openQuestions" in raw2) {
|
|
182102
|
+
if (!isStringArray2(raw2.openQuestions)) {
|
|
182103
|
+
set3.status = 400;
|
|
182104
|
+
return { status: 400, error: "openQuestions must be an array of strings" };
|
|
182105
|
+
}
|
|
182106
|
+
patch.openQuestions = raw2.openQuestions;
|
|
182107
|
+
}
|
|
182108
|
+
if ("nextSteps" in raw2) {
|
|
182109
|
+
if (!isStringArray2(raw2.nextSteps)) {
|
|
182110
|
+
set3.status = 400;
|
|
182111
|
+
return { status: 400, error: "nextSteps must be an array of strings" };
|
|
182112
|
+
}
|
|
182113
|
+
patch.nextSteps = raw2.nextSteps;
|
|
182114
|
+
}
|
|
182115
|
+
if ("files" in raw2) {
|
|
182116
|
+
if (!isStringArray2(raw2.files)) {
|
|
182117
|
+
set3.status = 400;
|
|
182118
|
+
return { status: 400, error: "files must be an array of strings" };
|
|
182119
|
+
}
|
|
182120
|
+
patch.files = raw2.files;
|
|
182121
|
+
}
|
|
182122
|
+
if (Object.keys(patch).length === 0) {
|
|
182123
|
+
set3.status = 400;
|
|
182124
|
+
return { status: 400, error: "no editable field provided" };
|
|
182125
|
+
}
|
|
182126
|
+
try {
|
|
182127
|
+
const result = await service3().update({
|
|
182128
|
+
id: params.id,
|
|
182129
|
+
projectId: query.projectId,
|
|
182130
|
+
patch
|
|
182131
|
+
});
|
|
182132
|
+
if (!result.ok) {
|
|
182133
|
+
set3.status = result.reason === "not-found" || result.reason === "project-mismatch" ? 404 : 400;
|
|
182134
|
+
return { status: set3.status, error: result.reason };
|
|
182135
|
+
}
|
|
182136
|
+
set3.status = 200;
|
|
182137
|
+
return { success: true, data: result.handoff };
|
|
182138
|
+
} catch (e) {
|
|
182139
|
+
rethrowCanonicalHandoffError(e);
|
|
182140
|
+
const err = e;
|
|
182141
|
+
logger.error("handoff update failed", err);
|
|
182142
|
+
set3.status = 500;
|
|
182143
|
+
return { success: false, error: `handoff update failed: ${err.message}` };
|
|
182144
|
+
}
|
|
182145
|
+
}, {
|
|
182146
|
+
params: t.Object({ id: t.String({ description: "Handoff id" }) }),
|
|
182147
|
+
query: t.Object({
|
|
182148
|
+
projectId: t.Optional(t.String({ description: "If supplied, must match the row's projectId or the request 404s" }))
|
|
182149
|
+
}),
|
|
182150
|
+
body: t.Record(t.String(), t.Unknown(), {
|
|
182151
|
+
description: "Only targetAgent/summary/openQuestions/nextSteps/files are accepted; any other key is a 400 naming it."
|
|
182152
|
+
}),
|
|
182153
|
+
detail: {
|
|
182154
|
+
...EVENT_DETAIL3,
|
|
182155
|
+
summary: "Edit a handoff (targetAgent/summary/openQuestions/nextSteps/files only)",
|
|
182156
|
+
description: "Allowlist PATCH: status/acceptedAt/id/projectId/createdAt/sourceSessionId and any unknown key are rejected " + "by name with 400. Refreshes the dual-written memory's content when a content-feeding field changes."
|
|
182157
|
+
}
|
|
182158
|
+
}).delete("/:id", async ({ params, query, set: set3 }) => {
|
|
182159
|
+
if (handoffsDisabled()) {
|
|
182160
|
+
set3.status = 423;
|
|
182161
|
+
return { status: 423, error: "handoffs disabled" };
|
|
182162
|
+
}
|
|
182163
|
+
try {
|
|
182164
|
+
const result = await service3().delete({ id: params.id, projectId: query.projectId });
|
|
182165
|
+
if (!result.ok) {
|
|
182166
|
+
set3.status = 404;
|
|
182167
|
+
return { status: 404, error: result.reason };
|
|
182168
|
+
}
|
|
182169
|
+
set3.status = 200;
|
|
182170
|
+
return { success: true, data: { id: result.id } };
|
|
182171
|
+
} catch (e) {
|
|
182172
|
+
rethrowCanonicalHandoffError(e);
|
|
182173
|
+
const err = e;
|
|
182174
|
+
logger.error("handoff delete failed", err);
|
|
182175
|
+
set3.status = 500;
|
|
182176
|
+
return { success: false, error: `handoff delete failed: ${err.message}` };
|
|
182177
|
+
}
|
|
182178
|
+
}, {
|
|
182179
|
+
params: t.Object({ id: t.String({ description: "Handoff id" }) }),
|
|
182180
|
+
query: t.Object({
|
|
182181
|
+
projectId: t.Optional(t.String({ description: "If supplied, must match the row's projectId or the request 404s" }))
|
|
182182
|
+
}),
|
|
182183
|
+
detail: {
|
|
182184
|
+
...EVENT_DETAIL3,
|
|
182185
|
+
summary: "Hard-delete a handoff",
|
|
182186
|
+
description: "Permitted in any status, including open. The dual-written memory row is left intact (dangling metadata.handoffId is accepted)."
|
|
182187
|
+
}
|
|
181711
182188
|
});
|
|
181712
182189
|
|
|
181713
182190
|
// src/routes/proposals.ts
|
|
@@ -181718,6 +182195,12 @@ function job() {
|
|
|
181718
182195
|
cachedJob2 = getAutoImproveJob();
|
|
181719
182196
|
return cachedJob2;
|
|
181720
182197
|
}
|
|
182198
|
+
var cachedStore4 = null;
|
|
182199
|
+
function store6() {
|
|
182200
|
+
if (!cachedStore4)
|
|
182201
|
+
cachedStore4 = getProposalStore();
|
|
182202
|
+
return cachedStore4;
|
|
182203
|
+
}
|
|
181721
182204
|
function autoImproveDisabled() {
|
|
181722
182205
|
try {
|
|
181723
182206
|
return config.get("memory")?.autoImprove?.enabled === false;
|
|
@@ -181728,6 +182211,16 @@ function autoImproveDisabled() {
|
|
|
181728
182211
|
var EVENT_DETAIL4 = {
|
|
181729
182212
|
tags: ["proposals"]
|
|
181730
182213
|
};
|
|
182214
|
+
var PROPOSAL_PATCH_ALLOWED_FIELDS = ["rationale", "payload"];
|
|
182215
|
+
function isRecord2(value) {
|
|
182216
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
182217
|
+
}
|
|
182218
|
+
function isProposalKind(value) {
|
|
182219
|
+
return typeof value === "string" && PROPOSAL_KINDS.includes(value);
|
|
182220
|
+
}
|
|
182221
|
+
function proposalPayloadValidationStatus(error51) {
|
|
182222
|
+
return error51 instanceof ProposalPayloadValidationError ? error51.statusCode : null;
|
|
182223
|
+
}
|
|
181731
182224
|
var proposalRoutes = new Elysia({ prefix: "/api/v1/proposal" }).post("/list", async ({ body, set: set3 }) => {
|
|
181732
182225
|
if (autoImproveDisabled()) {
|
|
181733
182226
|
set3.status = 423;
|
|
@@ -181828,6 +182321,191 @@ var proposalRoutes = new Elysia({ prefix: "/api/v1/proposal" }).post("/list", as
|
|
|
181828
182321
|
summary: "Reject a pending proposal",
|
|
181829
182322
|
description: "Flips status pending\u2192rejected (no apply, no event). Same failure semantics as approve on missing/non-pending/project-mismatch."
|
|
181830
182323
|
}
|
|
182324
|
+
}).post("/create", async ({ body, set: set3 }) => {
|
|
182325
|
+
if (autoImproveDisabled()) {
|
|
182326
|
+
set3.status = 423;
|
|
182327
|
+
return { status: 423, error: "auto-improve disabled" };
|
|
182328
|
+
}
|
|
182329
|
+
const raw2 = body ?? {};
|
|
182330
|
+
if (typeof raw2.projectId !== "string" || !raw2.projectId.trim()) {
|
|
182331
|
+
set3.status = 400;
|
|
182332
|
+
return { status: 400, error: "projectId required" };
|
|
182333
|
+
}
|
|
182334
|
+
if (!isProposalKind(raw2.kind)) {
|
|
182335
|
+
set3.status = 400;
|
|
182336
|
+
return { status: 400, error: `kind must be one of ${PROPOSAL_KINDS.join(", ")}` };
|
|
182337
|
+
}
|
|
182338
|
+
if (!isRecord2(raw2.payload)) {
|
|
182339
|
+
set3.status = 400;
|
|
182340
|
+
return { status: 400, error: "payload must be an object" };
|
|
182341
|
+
}
|
|
182342
|
+
if (raw2.rationale !== undefined && typeof raw2.rationale !== "string") {
|
|
182343
|
+
set3.status = 400;
|
|
182344
|
+
return { status: 400, error: "rationale must be a string" };
|
|
182345
|
+
}
|
|
182346
|
+
if (raw2.targetMemoryId !== undefined && raw2.targetMemoryId !== null && typeof raw2.targetMemoryId !== "string") {
|
|
182347
|
+
set3.status = 400;
|
|
182348
|
+
return { status: 400, error: "targetMemoryId must be a string or null" };
|
|
182349
|
+
}
|
|
182350
|
+
const projectId = raw2.projectId;
|
|
182351
|
+
const kind = raw2.kind;
|
|
182352
|
+
const payload = raw2.payload;
|
|
182353
|
+
try {
|
|
182354
|
+
const record2 = await store6().create({
|
|
182355
|
+
id: newProposalId(),
|
|
182356
|
+
projectId,
|
|
182357
|
+
kind,
|
|
182358
|
+
payload,
|
|
182359
|
+
rationale: raw2.rationale,
|
|
182360
|
+
targetMemoryId: raw2.targetMemoryId
|
|
182361
|
+
});
|
|
182362
|
+
set3.status = 200;
|
|
182363
|
+
return { success: true, data: record2 };
|
|
182364
|
+
} catch (e) {
|
|
182365
|
+
if (e instanceof SearchServiceError)
|
|
182366
|
+
throw e;
|
|
182367
|
+
const validationStatus = proposalPayloadValidationStatus(e);
|
|
182368
|
+
if (validationStatus !== null) {
|
|
182369
|
+
set3.status = validationStatus;
|
|
182370
|
+
return { status: validationStatus, error: e.message };
|
|
182371
|
+
}
|
|
182372
|
+
const err = e;
|
|
182373
|
+
logger.error("proposal create failed", err);
|
|
182374
|
+
set3.status = 500;
|
|
182375
|
+
return { success: false, error: `proposal create failed: ${err.message}` };
|
|
182376
|
+
}
|
|
182377
|
+
}, {
|
|
182378
|
+
body: t.Record(t.String(), t.Unknown(), {
|
|
182379
|
+
description: "projectId, kind (memory.create|memory.update|memory.tag), payload, rationale?, targetMemoryId?"
|
|
182380
|
+
}),
|
|
182381
|
+
detail: {
|
|
182382
|
+
...EVENT_DETAIL4,
|
|
182383
|
+
summary: "Create a pending proposal",
|
|
182384
|
+
description: "kind must be memory.create/memory.update/memory.tag; payload is validated against the same per-kind " + "rules the store enforces on read (AC-02.2), so a write the reader would reject is refused here instead."
|
|
182385
|
+
}
|
|
182386
|
+
}).patch("/:id", async ({ params, query, body, set: set3 }) => {
|
|
182387
|
+
if (autoImproveDisabled()) {
|
|
182388
|
+
set3.status = 423;
|
|
182389
|
+
return { status: 423, error: "auto-improve disabled" };
|
|
182390
|
+
}
|
|
182391
|
+
const raw2 = body ?? {};
|
|
182392
|
+
const keys = Object.keys(raw2);
|
|
182393
|
+
const rejected = keys.filter((k2) => !PROPOSAL_PATCH_ALLOWED_FIELDS.includes(k2));
|
|
182394
|
+
if (rejected.length > 0) {
|
|
182395
|
+
set3.status = 400;
|
|
182396
|
+
return { status: 400, error: `field not allowed: ${rejected.join(", ")}` };
|
|
182397
|
+
}
|
|
182398
|
+
const patch = {};
|
|
182399
|
+
if ("rationale" in raw2) {
|
|
182400
|
+
if (typeof raw2.rationale !== "string") {
|
|
182401
|
+
set3.status = 400;
|
|
182402
|
+
return { status: 400, error: "rationale must be a string" };
|
|
182403
|
+
}
|
|
182404
|
+
patch.rationale = raw2.rationale;
|
|
182405
|
+
}
|
|
182406
|
+
if ("payload" in raw2) {
|
|
182407
|
+
if (!isRecord2(raw2.payload)) {
|
|
182408
|
+
set3.status = 400;
|
|
182409
|
+
return { status: 400, error: "payload must be an object" };
|
|
182410
|
+
}
|
|
182411
|
+
patch.payload = raw2.payload;
|
|
182412
|
+
}
|
|
182413
|
+
if (Object.keys(patch).length === 0) {
|
|
182414
|
+
set3.status = 400;
|
|
182415
|
+
return { status: 400, error: "no editable field provided" };
|
|
182416
|
+
}
|
|
182417
|
+
try {
|
|
182418
|
+
const current = await store6().getById(params.id);
|
|
182419
|
+
if (!current) {
|
|
182420
|
+
set3.status = 404;
|
|
182421
|
+
return { status: 404, error: "not-found" };
|
|
182422
|
+
}
|
|
182423
|
+
if (query.projectId && current.projectId !== query.projectId) {
|
|
182424
|
+
set3.status = 404;
|
|
182425
|
+
return { status: 404, error: "project-mismatch" };
|
|
182426
|
+
}
|
|
182427
|
+
const updated = await store6().update(params.id, {
|
|
182428
|
+
...patch.rationale !== undefined ? { rationale: patch.rationale } : {},
|
|
182429
|
+
...patch.payload !== undefined ? { payload: patch.payload } : {}
|
|
182430
|
+
});
|
|
182431
|
+
if (!updated) {
|
|
182432
|
+
set3.status = 404;
|
|
182433
|
+
return { status: 404, error: "not-found" };
|
|
182434
|
+
}
|
|
182435
|
+
set3.status = 200;
|
|
182436
|
+
return { success: true, data: updated };
|
|
182437
|
+
} catch (e) {
|
|
182438
|
+
if (e instanceof SearchServiceError)
|
|
182439
|
+
throw e;
|
|
182440
|
+
const validationStatus = proposalPayloadValidationStatus(e);
|
|
182441
|
+
if (validationStatus !== null) {
|
|
182442
|
+
set3.status = validationStatus;
|
|
182443
|
+
return { status: validationStatus, error: e.message };
|
|
182444
|
+
}
|
|
182445
|
+
const err = e;
|
|
182446
|
+
logger.error("proposal update failed", err);
|
|
182447
|
+
set3.status = 500;
|
|
182448
|
+
return { success: false, error: `proposal update failed: ${err.message}` };
|
|
182449
|
+
}
|
|
182450
|
+
}, {
|
|
182451
|
+
params: t.Object({ id: t.String({ description: "Proposal id" }) }),
|
|
182452
|
+
query: t.Object({
|
|
182453
|
+
projectId: t.Optional(t.String({ description: "If supplied, must match the row's projectId or the request 404s" }))
|
|
182454
|
+
}),
|
|
182455
|
+
body: t.Record(t.String(), t.Unknown(), {
|
|
182456
|
+
description: "Only rationale/payload are accepted; any other key is a 400 naming it."
|
|
182457
|
+
}),
|
|
182458
|
+
detail: {
|
|
182459
|
+
...EVENT_DETAIL4,
|
|
182460
|
+
summary: "Edit a proposal (rationale/payload only)",
|
|
182461
|
+
description: "Allowlist PATCH: kind/targetMemoryId/status/decidedAt/id/projectId/createdAt and any unknown key are " + "rejected by name with 400. A payload edit re-validates against the row's existing kind (AC-02.3/AC-02.4)."
|
|
182462
|
+
}
|
|
182463
|
+
}).delete("/:id", async ({ params, query, set: set3 }) => {
|
|
182464
|
+
if (autoImproveDisabled()) {
|
|
182465
|
+
set3.status = 423;
|
|
182466
|
+
return { status: 423, error: "auto-improve disabled" };
|
|
182467
|
+
}
|
|
182468
|
+
try {
|
|
182469
|
+
const current = await store6().getById(params.id);
|
|
182470
|
+
if (!current) {
|
|
182471
|
+
set3.status = 404;
|
|
182472
|
+
return { status: 404, error: "not-found" };
|
|
182473
|
+
}
|
|
182474
|
+
if (query.projectId && current.projectId !== query.projectId) {
|
|
182475
|
+
set3.status = 404;
|
|
182476
|
+
return { status: 404, error: "project-mismatch" };
|
|
182477
|
+
}
|
|
182478
|
+
const deletedId = await store6().delete(params.id);
|
|
182479
|
+
if (!deletedId) {
|
|
182480
|
+
set3.status = 404;
|
|
182481
|
+
return { status: 404, error: "not-found" };
|
|
182482
|
+
}
|
|
182483
|
+
set3.status = 200;
|
|
182484
|
+
return {
|
|
182485
|
+
success: true,
|
|
182486
|
+
data: {
|
|
182487
|
+
id: deletedId,
|
|
182488
|
+
...current.status === "approved" ? { note: "approved proposal's applied memory edit was not reversed" } : {}
|
|
182489
|
+
}
|
|
182490
|
+
};
|
|
182491
|
+
} catch (e) {
|
|
182492
|
+
if (e instanceof SearchServiceError)
|
|
182493
|
+
throw e;
|
|
182494
|
+
const err = e;
|
|
182495
|
+
logger.error("proposal delete failed", err);
|
|
182496
|
+
set3.status = 500;
|
|
182497
|
+
return { success: false, error: `proposal delete failed: ${err.message}` };
|
|
182498
|
+
}
|
|
182499
|
+
}, {
|
|
182500
|
+
params: t.Object({ id: t.String({ description: "Proposal id" }) }),
|
|
182501
|
+
query: t.Object({
|
|
182502
|
+
projectId: t.Optional(t.String({ description: "If supplied, must match the row's projectId or the request 404s" }))
|
|
182503
|
+
}),
|
|
182504
|
+
detail: {
|
|
182505
|
+
...EVENT_DETAIL4,
|
|
182506
|
+
summary: "Hard-delete a proposal",
|
|
182507
|
+
description: "Permitted in any status. Deleting an already-approved proposal does not reverse the memory edit it " + "already applied (AC-02.5) \u2014 the response says so explicitly via a `note` field."
|
|
182508
|
+
}
|
|
181831
182509
|
});
|
|
181832
182510
|
|
|
181833
182511
|
// src/routes/executor.ts
|
|
@@ -181998,8 +182676,8 @@ function buildStaticDirCandidates(moduleDir, cwd) {
|
|
|
181998
182676
|
for (const root2 of [moduleDir, cwd]) {
|
|
181999
182677
|
let dir = root2;
|
|
182000
182678
|
for (let i = 0;i < 10; i++) {
|
|
182001
|
-
candidates2.push(path39.resolve(dir, "apps/web-ui/
|
|
182002
|
-
candidates2.push(path39.resolve(dir, "web-ui/
|
|
182679
|
+
candidates2.push(path39.resolve(dir, "apps/web-ui/dist/static"));
|
|
182680
|
+
candidates2.push(path39.resolve(dir, "web-ui/dist/static"));
|
|
182003
182681
|
const parent = path39.dirname(dir);
|
|
182004
182682
|
if (parent === dir)
|
|
182005
182683
|
break;
|
|
@@ -182352,6 +183030,13 @@ var REGISTRY_DETAIL = {
|
|
|
182352
183030
|
tags: ["model-registry"]
|
|
182353
183031
|
};
|
|
182354
183032
|
var OVERLAY_PATH = path41.join(configDir("massa-ai"), "model-profiles.json");
|
|
183033
|
+
var ZERO_OVERLAY_OVERRIDE_BREAKDOWN = {
|
|
183034
|
+
hostDefaults: 0,
|
|
183035
|
+
workflowTiers: 0,
|
|
183036
|
+
agentTiers: 0,
|
|
183037
|
+
tiers: 0,
|
|
183038
|
+
profiles: 0
|
|
183039
|
+
};
|
|
182355
183040
|
var modelRegistryRoutes = new Elysia({ prefix: "/api/v1/model-registry" }).get("/", async ({ set: set3 }) => {
|
|
182356
183041
|
const root2 = getDeploymentRoot();
|
|
182357
183042
|
if (!root2) {
|
|
@@ -182368,6 +183053,7 @@ var modelRegistryRoutes = new Elysia({ prefix: "/api/v1/model-registry" }).get("
|
|
|
182368
183053
|
registry: result.registry,
|
|
182369
183054
|
source: result.source,
|
|
182370
183055
|
overlayOverrideCount: result.overlayOverrideCount ?? 0,
|
|
183056
|
+
overlayOverrideBreakdown: result.overlayOverrideBreakdown ?? ZERO_OVERLAY_OVERRIDE_BREAKDOWN,
|
|
182371
183057
|
...result.overlayError ? { overlayError: result.overlayError } : {},
|
|
182372
183058
|
agents,
|
|
182373
183059
|
...agentsError ? { agentsError } : {}
|
|
@@ -182377,7 +183063,7 @@ var modelRegistryRoutes = new Elysia({ prefix: "/api/v1/model-registry" }).get("
|
|
|
182377
183063
|
detail: {
|
|
182378
183064
|
...REGISTRY_DETAIL,
|
|
182379
183065
|
summary: "Get effective registry (builtin + overlay) with source attribution",
|
|
182380
|
-
description: "Returns the merged registry (builtin + overlay), source attribution (builtin, overlay, tombstoned), overlayOverrideCount (APCR-01.10 \u2014 count of overlay entries surviving normalization, so an operator can see how much of the registry their overlay is overriding), overlayError if the overlay is corrupted, and agents (design D-3, APUX-03 \u2014 {name, charterTier} for every charter under skills/agents/, best-effort with agentsError on failure) (200 status, never fails)."
|
|
183066
|
+
description: "Returns the merged registry (builtin + overlay), source attribution (builtin, overlay, tombstoned), overlayOverrideCount (APCR-01.10 \u2014 count of overlay entries surviving normalization, so an operator can see how much of the registry their overlay is overriding), overlayOverrideBreakdown (WUT-17 \u2014 the same count broken down per category: hostDefaults, workflowTiers, agentTiers, tiers, profiles), overlayError if the overlay is corrupted, and agents (design D-3, APUX-03 \u2014 {name, charterTier} for every charter under skills/agents/, best-effort with agentsError on failure) (200 status, never fails)."
|
|
182381
183067
|
}
|
|
182382
183068
|
}).put("/", ({ body, set: set3 }) => {
|
|
182383
183069
|
const root2 = getDeploymentRoot();
|
|
@@ -182418,7 +183104,8 @@ var modelRegistryRoutes = new Elysia({ prefix: "/api/v1/model-registry" }).get("
|
|
|
182418
183104
|
data: {
|
|
182419
183105
|
registry: result.registry,
|
|
182420
183106
|
source: result.source,
|
|
182421
|
-
overlayOverrideCount: result.overlayOverrideCount ?? 0
|
|
183107
|
+
overlayOverrideCount: result.overlayOverrideCount ?? 0,
|
|
183108
|
+
overlayOverrideBreakdown: result.overlayOverrideBreakdown ?? ZERO_OVERLAY_OVERRIDE_BREAKDOWN
|
|
182422
183109
|
}
|
|
182423
183110
|
};
|
|
182424
183111
|
}, {
|
|
@@ -182426,7 +183113,7 @@ var modelRegistryRoutes = new Elysia({ prefix: "/api/v1/model-registry" }).get("
|
|
|
182426
183113
|
detail: {
|
|
182427
183114
|
...REGISTRY_DETAIL,
|
|
182428
183115
|
summary: "Write overlay (full-replace, validated, atomic)",
|
|
182429
|
-
description: "Accepts the full overlay object. Validates the merged result (builtin + overlay) via validateRegistry(). On success, writes atomically to ~/.config/massa-ai/model-profiles.json and returns the updated effective registry, including overlayOverrideCount (APCR-01.10). On failure, returns 400 with all violations."
|
|
183116
|
+
description: "Accepts the full overlay object. Validates the merged result (builtin + overlay) via validateRegistry(). On success, writes atomically to ~/.config/massa-ai/model-profiles.json and returns the updated effective registry, including overlayOverrideCount (APCR-01.10) and overlayOverrideBreakdown (WUT-17, per-category). On failure, returns 400 with all violations."
|
|
182430
183117
|
}
|
|
182431
183118
|
}).post("/regenerate", ({ set: set3 }) => {
|
|
182432
183119
|
const root2 = getDeploymentRoot();
|
|
@@ -182631,16 +183318,17 @@ var configRoutes = new Elysia({ prefix: "/api/v1/config" }).get("/", ({ set: set
|
|
|
182631
183318
|
const config3 = loadConfig();
|
|
182632
183319
|
const masked = maskSensitive(config3);
|
|
182633
183320
|
const restart = restartNeededSections(config3);
|
|
183321
|
+
const defaults2 = maskSensitive(defaultMassaAiConfig);
|
|
182634
183322
|
set3.status = 200;
|
|
182635
183323
|
return {
|
|
182636
183324
|
success: true,
|
|
182637
|
-
data: { config: masked, restartNeededSections: restart }
|
|
183325
|
+
data: { config: masked, restartNeededSections: restart, defaults: defaults2 }
|
|
182638
183326
|
};
|
|
182639
183327
|
}, {
|
|
182640
183328
|
detail: {
|
|
182641
183329
|
...CONFIG_DETAIL,
|
|
182642
183330
|
summary: "Get current config with sensitive fields masked",
|
|
182643
|
-
description: "Returns the current config.json with security.apiKey, llm.apiKey, embedding.apiKey, and database.url masked to '***'. Includes restartNeededSections \u2014 the subset of [database, embedding, llm, security] present in the config."
|
|
183331
|
+
description: "Returns the current config.json with security.apiKey, llm.apiKey, embedding.apiKey, and database.url masked to '***'. Includes restartNeededSections \u2014 the subset of [database, embedding, llm, security] present in the config \u2014 and defaults, the shipped default config (also masked) the Config tab falls back to for any field the persisted file omits."
|
|
182644
183332
|
}
|
|
182645
183333
|
}).get("/reveal", ({ query, set: set3 }) => {
|
|
182646
183334
|
const section = query.section;
|
|
@@ -182700,6 +183388,7 @@ var configRoutes = new Elysia({ prefix: "/api/v1/config" }).get("/", ({ set: set
|
|
|
182700
183388
|
// src/routes/model-registry-stream.ts
|
|
182701
183389
|
init_config();
|
|
182702
183390
|
init_dist();
|
|
183391
|
+
import fs28 from "fs";
|
|
182703
183392
|
import path42 from "path";
|
|
182704
183393
|
import { spawn as spawn3 } from "child_process";
|
|
182705
183394
|
var encoder3 = new TextEncoder;
|
|
@@ -182708,6 +183397,58 @@ function sseFrame(data) {
|
|
|
182708
183397
|
|
|
182709
183398
|
`);
|
|
182710
183399
|
}
|
|
183400
|
+
var KNOWN_GENERATOR_FILENAMES = ["generate-skill-artifacts.ts", "generate-subagent-artifacts.ts"];
|
|
183401
|
+
function deriveGeneratorScripts(root2) {
|
|
183402
|
+
const pkgPath = path42.join(root2, "package.json");
|
|
183403
|
+
let raw2;
|
|
183404
|
+
try {
|
|
183405
|
+
raw2 = fs28.readFileSync(pkgPath, "utf-8");
|
|
183406
|
+
} catch (e) {
|
|
183407
|
+
throw new Error(`cannot read ${pkgPath}: ${e.message}`);
|
|
183408
|
+
}
|
|
183409
|
+
let pkg;
|
|
183410
|
+
try {
|
|
183411
|
+
pkg = JSON.parse(raw2);
|
|
183412
|
+
} catch (e) {
|
|
183413
|
+
throw new Error(`${pkgPath} is not valid JSON: ${e.message}`);
|
|
183414
|
+
}
|
|
183415
|
+
const scriptsField = pkg?.scripts;
|
|
183416
|
+
const command = scriptsField?.["generate:artifacts"];
|
|
183417
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
183418
|
+
throw new Error(`${pkgPath}'s scripts."generate:artifacts" is missing or not a string`);
|
|
183419
|
+
}
|
|
183420
|
+
const segments = command.split("&&").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
183421
|
+
if (segments.length === 0) {
|
|
183422
|
+
throw new Error(`"generate:artifacts" parsed to zero commands: ${JSON.stringify(command)}`);
|
|
183423
|
+
}
|
|
183424
|
+
return segments.map((segment) => {
|
|
183425
|
+
const match2 = /^bun\s+(\S+\.ts)$/.exec(segment);
|
|
183426
|
+
if (!match2) {
|
|
183427
|
+
throw new Error(`"generate:artifacts" segment does not match the expected "bun <script.ts>" shape: ${JSON.stringify(segment)}`);
|
|
183428
|
+
}
|
|
183429
|
+
const relPath = match2[1];
|
|
183430
|
+
return { relPath, name: path42.basename(relPath) };
|
|
183431
|
+
});
|
|
183432
|
+
}
|
|
183433
|
+
function assertGeneratorBackstop(scripts) {
|
|
183434
|
+
const names = scripts.map((s) => s.name);
|
|
183435
|
+
const missing = KNOWN_GENERATOR_FILENAMES.filter((f) => !names.includes(f));
|
|
183436
|
+
if (scripts.length < 2 || missing.length > 0) {
|
|
183437
|
+
throw new Error(`generator list ${JSON.stringify(names)} does not contain the known generators ` + `${KNOWN_GENERATOR_FILENAMES.join(", ")} \u2014 refusing to spawn an implausibly short list`);
|
|
183438
|
+
}
|
|
183439
|
+
}
|
|
183440
|
+
function emitSkillsFrames(controller2, closedRef) {
|
|
183441
|
+
for (const host of HOSTS) {
|
|
183442
|
+
if (closedRef.closed)
|
|
183443
|
+
return;
|
|
183444
|
+
try {
|
|
183445
|
+
controller2.enqueue(sseFrame({ type: "skills", host, status: "generated" }));
|
|
183446
|
+
} catch {
|
|
183447
|
+
closedRef.closed = true;
|
|
183448
|
+
return;
|
|
183449
|
+
}
|
|
183450
|
+
}
|
|
183451
|
+
}
|
|
182711
183452
|
function deriveInstallStatus(report) {
|
|
182712
183453
|
if (report.hosts.some((h) => h.status === "switched"))
|
|
182713
183454
|
return "switched";
|
|
@@ -182782,17 +183523,15 @@ function createRegenerateStreamHandler() {
|
|
|
182782
183523
|
closedRef.closed = true;
|
|
182783
183524
|
return;
|
|
182784
183525
|
}
|
|
182785
|
-
|
|
183526
|
+
let generatorScripts;
|
|
182786
183527
|
try {
|
|
182787
|
-
|
|
182788
|
-
|
|
182789
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
182790
|
-
});
|
|
183528
|
+
generatorScripts = deriveGeneratorScripts(root2);
|
|
183529
|
+
assertGeneratorBackstop(generatorScripts);
|
|
182791
183530
|
} catch (e) {
|
|
182792
183531
|
controller2.enqueue(sseFrame({
|
|
182793
183532
|
type: "done",
|
|
182794
183533
|
exitCode: null,
|
|
182795
|
-
error: `
|
|
183534
|
+
error: `could not derive the generator list: ${e.message}`
|
|
182796
183535
|
}));
|
|
182797
183536
|
controller2.close();
|
|
182798
183537
|
closedRef.closed = true;
|
|
@@ -182815,35 +183554,57 @@ function createRegenerateStreamHandler() {
|
|
|
182815
183554
|
}
|
|
182816
183555
|
}
|
|
182817
183556
|
};
|
|
182818
|
-
|
|
182819
|
-
child.stderr?.on("data", (chunk) => emitLine("stderr", chunk));
|
|
182820
|
-
child.on("error", (e) => {
|
|
183557
|
+
const finish = (exitCode, error51) => {
|
|
182821
183558
|
if (closedRef.closed)
|
|
182822
183559
|
return;
|
|
182823
183560
|
closedRef.closed = true;
|
|
182824
183561
|
try {
|
|
182825
|
-
controller2.enqueue(sseFrame({ type: "done", exitCode
|
|
183562
|
+
controller2.enqueue(sseFrame(error51 !== undefined ? { type: "done", exitCode, error: error51 } : { type: "done", exitCode }));
|
|
182826
183563
|
controller2.close();
|
|
182827
183564
|
} catch {}
|
|
182828
|
-
}
|
|
182829
|
-
|
|
183565
|
+
};
|
|
183566
|
+
const runGenerator = (index) => {
|
|
182830
183567
|
if (closedRef.closed)
|
|
182831
183568
|
return;
|
|
182832
|
-
if (
|
|
183569
|
+
if (index >= generatorScripts.length) {
|
|
182833
183570
|
try {
|
|
182834
183571
|
controller2.enqueue(sseFrame({ type: "line", stream: "stdout", text: "Installing regenerated agents to active directories..." }));
|
|
182835
183572
|
} catch {
|
|
182836
183573
|
closedRef.closed = true;
|
|
182837
183574
|
return;
|
|
182838
183575
|
}
|
|
183576
|
+
emitSkillsFrames(controller2, closedRef);
|
|
182839
183577
|
installActiveProfiles(controller2, closedRef);
|
|
183578
|
+
finish(0);
|
|
183579
|
+
return;
|
|
182840
183580
|
}
|
|
182841
|
-
|
|
183581
|
+
const generator = generatorScripts[index];
|
|
183582
|
+
const scriptPath = path42.join(root2, generator.relPath);
|
|
182842
183583
|
try {
|
|
182843
|
-
|
|
182844
|
-
|
|
182845
|
-
|
|
182846
|
-
|
|
183584
|
+
child = spawn3("bun", [scriptPath], {
|
|
183585
|
+
env: { ...process.env },
|
|
183586
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
183587
|
+
});
|
|
183588
|
+
} catch (e) {
|
|
183589
|
+
finish(null, `spawn failed (${generator.name}): ${e.message}`);
|
|
183590
|
+
return;
|
|
183591
|
+
}
|
|
183592
|
+
child.stdout?.on("data", (chunk) => emitLine("stdout", chunk));
|
|
183593
|
+
child.stderr?.on("data", (chunk) => emitLine("stderr", chunk));
|
|
183594
|
+
child.on("error", (e) => {
|
|
183595
|
+
finish(null, `spawn error (${generator.name}): ${e.message}`);
|
|
183596
|
+
});
|
|
183597
|
+
child.on("close", (code) => {
|
|
183598
|
+
if (closedRef.closed)
|
|
183599
|
+
return;
|
|
183600
|
+
if (code !== 0) {
|
|
183601
|
+
finish(code, `${generator.name} failed with exit code ${code}`);
|
|
183602
|
+
return;
|
|
183603
|
+
}
|
|
183604
|
+
runGenerator(index + 1);
|
|
183605
|
+
});
|
|
183606
|
+
};
|
|
183607
|
+
runGenerator(0);
|
|
182847
183608
|
},
|
|
182848
183609
|
cancel() {
|
|
182849
183610
|
closedRef.closed = true;
|
|
@@ -182865,8 +183626,8 @@ function createRegenerateStreamHandler() {
|
|
|
182865
183626
|
var modelRegistryStreamRoutes = new Elysia({ prefix: "/api/v1/model-registry" }).post("/regenerate-and-install-stream", createRegenerateStreamHandler(), {
|
|
182866
183627
|
detail: {
|
|
182867
183628
|
tags: ["model-registry"],
|
|
182868
|
-
summary: "Regenerate subagent artifacts
|
|
182869
|
-
description: "Spawns `
|
|
183629
|
+
summary: "Regenerate skill + subagent artifacts, then auto-install to active dirs (streaming SSE)",
|
|
183630
|
+
description: "Spawns every generator named by package.json's `generate:artifacts` script (today `generate-skill-artifacts.ts` then `generate-subagent-artifacts.ts`), one at a time in that order, with child_process.spawn (non-blocking). Pipes stdout/stderr line-by-line as SSE; the first generator to exit non-zero stops the chain and names itself in the terminal `done` frame. Once every generator exits 0, emits one `skills` event per host, bridges the freshly generated agent-profiles trees into each host's installed variant root (`variant-sync` events per host), then calls switchProfile for every detected host to reinstall the active profile's agents from those synced variant dirs. Emits `install` events per host, then a terminal `done` event with the exit code."
|
|
182870
183631
|
}
|
|
182871
183632
|
}).post("/regenerate-stream", createRegenerateStreamHandler(), {
|
|
182872
183633
|
detail: {
|
|
@@ -182976,7 +183737,7 @@ var restartRoutes = new Elysia({ prefix: "/api/v1/system" }).onAfterResponse(()
|
|
|
182976
183737
|
|
|
182977
183738
|
// src/routes/logs.ts
|
|
182978
183739
|
init_dist();
|
|
182979
|
-
import
|
|
183740
|
+
import fs29 from "fs";
|
|
182980
183741
|
var LOGS_DETAIL = { tags: ["logs"] };
|
|
182981
183742
|
var MAX_SCAN_BYTES = 64 * 1024 * 1024;
|
|
182982
183743
|
var MAX_LIMIT = 1000;
|
|
@@ -183056,7 +183817,7 @@ function parseLine(line, prevTs) {
|
|
|
183056
183817
|
function realReadTail(filePath, maxBytes) {
|
|
183057
183818
|
let size;
|
|
183058
183819
|
try {
|
|
183059
|
-
size =
|
|
183820
|
+
size = fs29.statSync(filePath).size;
|
|
183060
183821
|
} catch {
|
|
183061
183822
|
return { content: "", truncated: false };
|
|
183062
183823
|
}
|
|
@@ -183064,24 +183825,24 @@ function realReadTail(filePath, maxBytes) {
|
|
|
183064
183825
|
return { content: "", truncated: false };
|
|
183065
183826
|
if (size <= maxBytes) {
|
|
183066
183827
|
try {
|
|
183067
|
-
return { content:
|
|
183828
|
+
return { content: fs29.readFileSync(filePath, "utf8"), truncated: false };
|
|
183068
183829
|
} catch {
|
|
183069
183830
|
return { content: "", truncated: false };
|
|
183070
183831
|
}
|
|
183071
183832
|
}
|
|
183072
183833
|
try {
|
|
183073
|
-
const fd =
|
|
183834
|
+
const fd = fs29.openSync(filePath, "r");
|
|
183074
183835
|
try {
|
|
183075
183836
|
const start = size - maxBytes;
|
|
183076
183837
|
const buf = Buffer.alloc(maxBytes);
|
|
183077
|
-
|
|
183838
|
+
fs29.readSync(fd, buf, 0, maxBytes, start);
|
|
183078
183839
|
let text3 = buf.toString("utf8");
|
|
183079
183840
|
const firstNewline = text3.indexOf(`
|
|
183080
183841
|
`);
|
|
183081
183842
|
text3 = firstNewline !== -1 ? text3.slice(firstNewline + 1) : "";
|
|
183082
183843
|
return { content: text3, truncated: true };
|
|
183083
183844
|
} finally {
|
|
183084
|
-
|
|
183845
|
+
fs29.closeSync(fd);
|
|
183085
183846
|
}
|
|
183086
183847
|
} catch {
|
|
183087
183848
|
return { content: "", truncated: true };
|
|
@@ -183091,7 +183852,7 @@ var realReader = {
|
|
|
183091
183852
|
listFiles(filePath, maxFiles) {
|
|
183092
183853
|
return sinkFiles(filePath, maxFiles).filter((f) => {
|
|
183093
183854
|
try {
|
|
183094
|
-
|
|
183855
|
+
fs29.accessSync(f, fs29.constants.R_OK);
|
|
183095
183856
|
return true;
|
|
183096
183857
|
} catch {
|
|
183097
183858
|
return false;
|
|
@@ -183167,8 +183928,6 @@ function renderTxtLine(entry2) {
|
|
|
183167
183928
|
const metaStr = entry2.meta ? ` ${JSON.stringify(entry2.meta)}` : "";
|
|
183168
183929
|
return `[${entry2.ts}] [${entry2.level.toUpperCase()}] ${entry2.message}${metaStr}`;
|
|
183169
183930
|
}
|
|
183170
|
-
var SSE_HEARTBEAT_MS_DEFAULT = 15000;
|
|
183171
|
-
var SSE_MAX_DURATION_MS_DEFAULT = 10 * 60 * 1000;
|
|
183172
183931
|
var SINK_POLL_MS_DEFAULT = 1000;
|
|
183173
183932
|
var SINK_POLL_MAX_BYTES = 1024 * 1024;
|
|
183174
183933
|
function startSinkTail(enqueue) {
|
|
@@ -183182,7 +183941,7 @@ function startSinkTail(enqueue) {
|
|
|
183182
183941
|
let currentFile = initial[0];
|
|
183183
183942
|
let offset;
|
|
183184
183943
|
try {
|
|
183185
|
-
offset =
|
|
183944
|
+
offset = fs29.statSync(currentFile).size;
|
|
183186
183945
|
} catch {
|
|
183187
183946
|
return;
|
|
183188
183947
|
}
|
|
@@ -183198,7 +183957,7 @@ function startSinkTail(enqueue) {
|
|
|
183198
183957
|
offset = 0;
|
|
183199
183958
|
carry = "";
|
|
183200
183959
|
}
|
|
183201
|
-
const size =
|
|
183960
|
+
const size = fs29.statSync(currentFile).size;
|
|
183202
183961
|
if (size < offset) {
|
|
183203
183962
|
offset = 0;
|
|
183204
183963
|
carry = "";
|
|
@@ -183207,11 +183966,11 @@ function startSinkTail(enqueue) {
|
|
|
183207
183966
|
return;
|
|
183208
183967
|
const length = Math.min(size - offset, SINK_POLL_MAX_BYTES);
|
|
183209
183968
|
const buf = Buffer.alloc(length);
|
|
183210
|
-
const fd =
|
|
183969
|
+
const fd = fs29.openSync(currentFile, "r");
|
|
183211
183970
|
try {
|
|
183212
|
-
|
|
183971
|
+
fs29.readSync(fd, buf, 0, length, offset);
|
|
183213
183972
|
} finally {
|
|
183214
|
-
|
|
183973
|
+
fs29.closeSync(fd);
|
|
183215
183974
|
}
|
|
183216
183975
|
offset += length;
|
|
183217
183976
|
const text3 = carry + buf.toString("utf8");
|
|
@@ -183274,9 +184033,9 @@ var logsRoutes = new Elysia({ prefix: "/api/v1/logs" }).get("/", ({ query, set:
|
|
|
183274
184033
|
summary: "Download the queried log range as jsonl or txt",
|
|
183275
184034
|
description: "Same query surface as GET /api/v1/logs (minus limit/offset \u2014 the full matching range is returned) plus format=jsonl|txt. Responds with Content-Disposition: attachment and a filename carrying the range. A range matching zero entries still returns an empty (200) download."
|
|
183276
184035
|
}
|
|
183277
|
-
}).get("/stream", () => {
|
|
183278
|
-
const HEARTBEAT_MS =
|
|
183279
|
-
const MAX_DURATION_MS =
|
|
184036
|
+
}).get("/stream", ({ request }) => {
|
|
184037
|
+
const HEARTBEAT_MS = resolveHeartbeatMs();
|
|
184038
|
+
const MAX_DURATION_MS = resolveMaxDurationMs();
|
|
183280
184039
|
const encoder4 = new TextEncoder;
|
|
183281
184040
|
let closed = false;
|
|
183282
184041
|
let unsubscribe;
|
|
@@ -183284,6 +184043,18 @@ var logsRoutes = new Elysia({ prefix: "/api/v1/logs" }).get("/", ({ query, set:
|
|
|
183284
184043
|
let closeTimer;
|
|
183285
184044
|
const stream2 = new ReadableStream({
|
|
183286
184045
|
start(controller2) {
|
|
184046
|
+
applySseRequestTimeout(request);
|
|
184047
|
+
const teardown = () => {
|
|
184048
|
+
closed = true;
|
|
184049
|
+
unsubscribe?.();
|
|
184050
|
+
if (heartbeatTimer)
|
|
184051
|
+
clearInterval(heartbeatTimer);
|
|
184052
|
+
if (closeTimer)
|
|
184053
|
+
clearTimeout(closeTimer);
|
|
184054
|
+
try {
|
|
184055
|
+
controller2.close();
|
|
184056
|
+
} catch {}
|
|
184057
|
+
};
|
|
183287
184058
|
const enqueue = (data) => {
|
|
183288
184059
|
if (closed)
|
|
183289
184060
|
return;
|
|
@@ -183292,7 +184063,7 @@ var logsRoutes = new Elysia({ prefix: "/api/v1/logs" }).get("/", ({ query, set:
|
|
|
183292
184063
|
|
|
183293
184064
|
`));
|
|
183294
184065
|
} catch {
|
|
183295
|
-
|
|
184066
|
+
teardown();
|
|
183296
184067
|
}
|
|
183297
184068
|
};
|
|
183298
184069
|
const tail = startSinkTail(enqueue);
|
|
@@ -183313,18 +184084,10 @@ var logsRoutes = new Elysia({ prefix: "/api/v1/logs" }).get("/", ({ query, set:
|
|
|
183313
184084
|
|
|
183314
184085
|
`));
|
|
183315
184086
|
} catch {
|
|
183316
|
-
|
|
183317
|
-
clearInterval(heartbeatTimer);
|
|
184087
|
+
teardown();
|
|
183318
184088
|
}
|
|
183319
184089
|
}, HEARTBEAT_MS);
|
|
183320
|
-
closeTimer = setTimeout(
|
|
183321
|
-
closed = true;
|
|
183322
|
-
unsubscribe?.();
|
|
183323
|
-
clearInterval(heartbeatTimer);
|
|
183324
|
-
try {
|
|
183325
|
-
controller2.close();
|
|
183326
|
-
} catch {}
|
|
183327
|
-
}, MAX_DURATION_MS);
|
|
184090
|
+
closeTimer = setTimeout(teardown, MAX_DURATION_MS);
|
|
183328
184091
|
},
|
|
183329
184092
|
cancel() {
|
|
183330
184093
|
closed = true;
|
|
@@ -183346,9 +184109,107 @@ var logsRoutes = new Elysia({ prefix: "/api/v1/logs" }).get("/", ({ query, set:
|
|
|
183346
184109
|
}, {
|
|
183347
184110
|
detail: {
|
|
183348
184111
|
...LOGS_DETAIL,
|
|
183349
|
-
summary: "SSE tail of newly
|
|
183350
|
-
description: "Server-Sent Events stream emitting `data: <LogEntry JSON>` for every entry subsequently
|
|
184112
|
+
summary: "SSE tail of newly appended log entries",
|
|
184113
|
+
description: "Server-Sent Events stream emitting `data: <LogEntry JSON>` for every entry subsequently appended, plus `: heartbeat` comments. Same heartbeat and max-duration auto-close behavior as GET /api/v1/events. Tails the shared file sink when one is readable \u2014 matching every massa-ai process, including the stdio MCP server, the same scope as GET /api/v1/logs \u2014 and falls back to the in-process ring buffer, scoped to this server process only, when no sink file is readable."
|
|
184114
|
+
}
|
|
184115
|
+
});
|
|
184116
|
+
|
|
184117
|
+
// src/middleware/write-mode-classification.ts
|
|
184118
|
+
var READ_ONLY_ROUTES = [
|
|
184119
|
+
{
|
|
184120
|
+
method: "POST",
|
|
184121
|
+
path: "/api/v1/search/project",
|
|
184122
|
+
justification: "WRITES when autoReindex+projectPath are both set. " + "apps/tools-api/src/routes/search.ts:48-106 passes the raw request body to " + "SearchProjectTool.handle(), which forwards it unchanged to " + "SearchController.searchProject " + "(packages/core/src/services/search/search-controller.ts:132-179): autoReindex " + "defaults to false at :140, and at :177-179 `if (autoReindex && projectPath) " + "this.handleAutoReindex(projectId, projectPath)` calls the private helper at " + ":364-389, which calls `this.contextualSearch.ensureFreshIndex(...)` \u2014 a real " + "index write. Kept on the allowlist with a sanitizer (AC-03.3a) instead of being " + "dropped, so read-only mode does not 403 project search entirely.",
|
|
184123
|
+
sanitizeBody: (body) => {
|
|
184124
|
+
body.autoReindex = false;
|
|
184125
|
+
}
|
|
184126
|
+
},
|
|
184127
|
+
{
|
|
184128
|
+
method: "POST",
|
|
184129
|
+
path: "/api/v1/search/code",
|
|
184130
|
+
justification: "Clean. apps/tools-api/src/routes/search.ts:108-126's request schema exposes only " + "{query, projectId, limit} \u2014 no `autoReindex` field exists on this route to flip. " + "packages/core/src/tools/search_code.ts:56-62 delegates to SearchProjectTool.handle " + "with `autoReindex: false` hardcoded at line 61 regardless of caller input, so the " + "write path proven above for /search/project is structurally unreachable from here."
|
|
184131
|
+
},
|
|
184132
|
+
{
|
|
184133
|
+
method: "POST",
|
|
184134
|
+
path: "/api/v1/memory/search",
|
|
184135
|
+
justification: "Clean. apps/tools-api/src/routes/memory.ts:126-138 (`POST /search`) delegates the " + "raw body to SearchMemoriesTool.handle(), which " + "(packages/core/src/tools/search_memories.ts:93-121) only calls " + "`MemoryController.search(...)` and formats the result for the response \u2014 no " + "repository mutation on any code path this handler can reach."
|
|
184136
|
+
},
|
|
184137
|
+
{
|
|
184138
|
+
method: "POST",
|
|
184139
|
+
path: "/api/v1/memory/list",
|
|
184140
|
+
justification: "Clean. apps/tools-api/src/routes/memory.ts:243-268 calls only " + "`getMemoryRepository().search(...)`, then filters/paginates the result in the " + "route handler itself \u2014 no repository write method is reachable from this handler."
|
|
184141
|
+
},
|
|
184142
|
+
{
|
|
184143
|
+
method: "POST",
|
|
184144
|
+
path: "/api/v1/checkpoints/list",
|
|
184145
|
+
justification: "Clean. apps/tools-api/src/routes/checkpoints.ts:56-68 delegates to " + "ListCheckpointsTool.handle(), which " + "(packages/core/src/tools/list_checkpoints.ts:90-99) calls only " + "`checkpointManager.listCheckpoints(...)` and `checkpointManager.getStats()` \u2014 both " + "reads over existing state, never a checkpoint create/delete/restore call."
|
|
184146
|
+
},
|
|
184147
|
+
{
|
|
184148
|
+
method: "POST",
|
|
184149
|
+
path: "/api/v1/handoff/list",
|
|
184150
|
+
justification: "Clean. apps/tools-api/src/routes/handoff.ts:174-213 calls only " + "`service().listPending(...)`, which " + "(packages/core/src/services/handoff/handoff-service.ts:228-229) is a direct " + "pass-through to `this.store.listPending(...)` \u2014 never `begin` (:115), `accept` " + "(:173) or `cancel` (:182), the service's three write entry points."
|
|
184151
|
+
},
|
|
184152
|
+
{
|
|
184153
|
+
method: "POST",
|
|
184154
|
+
path: "/api/v1/proposal/list",
|
|
184155
|
+
justification: "Clean. apps/tools-api/src/routes/proposals.ts:36-73 calls only " + "`job().listPending(...)`, which " + "(packages/core/src/services/jobs/auto-improve-job.ts:113) is a direct pass-through " + "to `this.proposalStore.listPending(...)` \u2014 never `approve` (:111) or `reject` " + "(:112), the job's two write entry points."
|
|
184156
|
+
},
|
|
184157
|
+
{
|
|
184158
|
+
method: "POST",
|
|
184159
|
+
path: "/api/v1/context/compress",
|
|
184160
|
+
justification: "Clean. apps/tools-api/src/routes/context.ts:29-68 delegates to " + "CompressContextTool.handle(), which " + "(packages/core/src/tools/compress_context.ts:67-118) runs `compressWithMetrics` " + "over the request's own `content` string in memory and returns the result \u2014 no " + "repository, index, or filesystem write on any code path."
|
|
184161
|
+
},
|
|
184162
|
+
{
|
|
184163
|
+
method: "POST",
|
|
184164
|
+
path: "/api/v1/context/optimized",
|
|
184165
|
+
justification: "Clean. apps/tools-api/src/routes/context.ts:70-101 delegates to " + "GetOptimizedContextTool.handle(), which " + "(packages/core/src/tools/get_optimized_context.ts:105-110) calls " + "`ContextController.getOptimizedContext(p)`. That controller " + "(packages/core/src/services/context/context-controller.ts:161) hardcodes " + "`autoReindex: false` on its own internal search call, so the write path proven " + "for /search/project is structurally unreachable here even though this route's " + "schema also carries `projectPath`."
|
|
184166
|
+
},
|
|
184167
|
+
{
|
|
184168
|
+
method: "POST",
|
|
184169
|
+
path: "/api/v1/symbol/impact",
|
|
184170
|
+
justification: "Clean. apps/tools-api/src/routes/workspace.ts:542-673 calls " + "`getGraphController().analyzeImpact(...)`, which " + "(packages/core/src/services/symbol/graph-controller.ts:189-232) delegates to " + "`impactAnalysisService.analyze(...)` " + "(packages/core/src/services/symbol/impact-analysis.ts). Every git invocation there " + "runs through `execFileSync` for `rev-parse`/`diff`/`status`-class read commands, " + "plus one `hash-object -t tree --stdin` with no `-w` flag (so nothing is written to " + "the git object database) \u2014 no `git commit`, `git add`, or `-w` flag anywhere in " + "the module."
|
|
184171
|
+
}
|
|
184172
|
+
];
|
|
184173
|
+
function normalizeRoutePath(path43) {
|
|
184174
|
+
return path43.length > 1 && path43.endsWith("/") ? path43.slice(0, -1) : path43;
|
|
184175
|
+
}
|
|
184176
|
+
var READ_ONLY_INDEX = new Map(READ_ONLY_ROUTES.map((entry2) => [`${entry2.method} ${entry2.path}`, entry2]));
|
|
184177
|
+
function findReadOnlyRoute(method, path43) {
|
|
184178
|
+
return READ_ONLY_INDEX.get(`${method.toUpperCase()} ${normalizeRoutePath(path43)}`);
|
|
184179
|
+
}
|
|
184180
|
+
|
|
184181
|
+
// src/middleware/write-mode.ts
|
|
184182
|
+
function defaultReadOnlyModeAccessor() {
|
|
184183
|
+
return process.env.MASSA_AI_READ_ONLY_MODE === "true";
|
|
184184
|
+
}
|
|
184185
|
+
var readOnlyModeAccessor = defaultReadOnlyModeAccessor;
|
|
184186
|
+
function isReadOnlyModeActive() {
|
|
184187
|
+
try {
|
|
184188
|
+
return readOnlyModeAccessor();
|
|
184189
|
+
} catch {
|
|
184190
|
+
return true;
|
|
184191
|
+
}
|
|
184192
|
+
}
|
|
184193
|
+
var WRITE_REFUSED = {
|
|
184194
|
+
success: false,
|
|
184195
|
+
error: "Write refused: read-only mode is active"
|
|
184196
|
+
};
|
|
184197
|
+
var writeModeMiddleware = new Elysia({ name: "write-mode" }).onBeforeHandle({ as: "global" }, ({ request, path: path43, set: set3, body }) => {
|
|
184198
|
+
if (request.method.toUpperCase() === "GET")
|
|
184199
|
+
return;
|
|
184200
|
+
if (isPublicPath(path43))
|
|
184201
|
+
return;
|
|
184202
|
+
if (!isReadOnlyModeActive())
|
|
184203
|
+
return;
|
|
184204
|
+
const entry2 = findReadOnlyRoute(request.method, path43);
|
|
184205
|
+
if (entry2) {
|
|
184206
|
+
if (entry2.sanitizeBody && body && typeof body === "object") {
|
|
184207
|
+
entry2.sanitizeBody(body);
|
|
184208
|
+
}
|
|
184209
|
+
return;
|
|
183351
184210
|
}
|
|
184211
|
+
set3.status = 403;
|
|
184212
|
+
return { ...WRITE_REFUSED };
|
|
183352
184213
|
});
|
|
183353
184214
|
|
|
183354
184215
|
// src/middleware/error.ts
|
|
@@ -183474,7 +184335,7 @@ var app = new Elysia({ adapter: node() }).use(cors(buildCorsOptions(config.get("
|
|
|
183474
184335
|
},
|
|
183475
184336
|
security: [{ ApiKeyAuth: [] }]
|
|
183476
184337
|
}
|
|
183477
|
-
})).use(errorHandler).use(authMiddleware).use(searchRoutes).use(memoryRoutes).use(checkpointRoutes).use(projectRoutes).use(contextRoutes).use(analyticsRoutes).use(systemRoutes).use(eventsRoutes).use(workspaceRoutes).use(fileRoutes).use(synapseRoutes).use(hookRoutes).use(bootstrapRoutes).use(handoffRoutes).use(proposalRoutes).use(executorRoutes).use(webRoutes).use(webUiRoutes).use(architectureRoutes).use(dashboardRoutes).use(profileRoutes).use(configRoutes).use(modelRegistryRoutes).use(modelRegistryStreamRoutes).use(restartRoutes).use(logsRoutes).get("/health", () => buildHealthResponse(getParserReadiness()));
|
|
184338
|
+
})).use(errorHandler).use(authMiddleware).use(writeModeMiddleware).use(searchRoutes).use(memoryRoutes).use(checkpointRoutes).use(projectRoutes).use(contextRoutes).use(analyticsRoutes).use(systemRoutes).use(eventsRoutes).use(workspaceRoutes).use(fileRoutes).use(synapseRoutes).use(hookRoutes).use(bootstrapRoutes).use(handoffRoutes).use(proposalRoutes).use(executorRoutes).use(webRoutes).use(webUiRoutes).use(architectureRoutes).use(dashboardRoutes).use(profileRoutes).use(configRoutes).use(modelRegistryRoutes).use(modelRegistryStreamRoutes).use(restartRoutes).use(logsRoutes).get("/health", () => buildHealthResponse(getParserReadiness()));
|
|
183478
184339
|
initAuthOrExit();
|
|
183479
184340
|
warnIfTrustOverrideEnabled();
|
|
183480
184341
|
await listenAfterParserValidation({
|
|
@@ -183483,6 +184344,8 @@ await listenAfterParserValidation({
|
|
|
183483
184344
|
try {
|
|
183484
184345
|
app.listen({ port: Number(PORT), reusePort: false }, (server) => {
|
|
183485
184346
|
setServerStopper(() => void server.stop());
|
|
184347
|
+
const bunServer = server.bun?.server;
|
|
184348
|
+
setSseRequestTimeoutSource(bunServer);
|
|
183486
184349
|
});
|
|
183487
184350
|
} catch (error51) {
|
|
183488
184351
|
if (error51?.code === "EADDRINUSE") {
|